diff --git a/docs/changelog.rst b/docs/changelog.rst index 8bd5ba752..a4bc91410 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -55,6 +55,8 @@ To update |kitty|, :doc:`follow the instructions `. - macOS: Add menu items to close the OS window and the current tab (:pull:`3240`, :iss:`3246`) +- macOS: Allow opening script and command files with kitty (:iss:`3366`) + - Also detect ``gemini://`` URLs when hovering with the mouse (:iss:`3370`) - When using a non-US keyboard layout and pressing :kbd:`ctrl+key` when diff --git a/glfw/cocoa_init.m b/glfw/cocoa_init.m index 1455ec071..8df4a64e6 100644 --- a/glfw/cocoa_init.m +++ b/glfw/cocoa_init.m @@ -346,10 +346,25 @@ static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL; finish_launching_callback(); } +- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename { + (void)theApplication; + if (!filename || !_glfw.ns.file_open_callback) return NO; + const char *path = NULL; + @try { + path = [[NSFileManager defaultManager] fileSystemRepresentationWithPath: filename]; + } @catch(NSException *exc) { + NSLog(@"Converting openFile filename: %@ failed with error: %@", filename, exc.reason); + return NO; + } + if (!path) return NO; + return _glfw.ns.file_open_callback(path); +} + - (void)applicationDidFinishLaunching:(NSNotification *)notification { (void)notification; [NSApp stop:nil]; + if (_glfw.ns.file_open_callback) _glfw.ns.file_open_callback(":cocoa::application launched::"); CGDisplayRegisterReconfigurationCallback(display_reconfigured, NULL); _glfwCocoaPostEmptyEvent(); diff --git a/glfw/cocoa_platform.h b/glfw/cocoa_platform.h index 664c3e2b4..6bbb527e0 100644 --- a/glfw/cocoa_platform.h +++ b/glfw/cocoa_platform.h @@ -67,6 +67,7 @@ typedef void* CVDisplayLinkRef; typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int, unsigned long); typedef bool (* GLFWapplicationshouldhandlereopenfun)(int); +typedef bool (* GLFWhandlefileopen)(const char*); typedef void (* GLFWapplicationwillfinishlaunchingfun)(void); typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*); typedef void (* GLFWcocoarenderframefun)(GLFWwindow*); @@ -197,6 +198,8 @@ typedef struct _GLFWlibraryNS _GLFWDisplayLinkNS entries[256]; size_t count; } displayLinks; + // the callback to handle file open events + GLFWhandlefileopen file_open_callback; } _GLFWlibraryNS; diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index 800505672..b319e5060 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -2482,6 +2482,13 @@ GLFWAPI GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow *hand return previous; } +GLFWAPI GLFWhandlefileopen glfwSetCocoaFileOpenCallback(GLFWhandlefileopen callback) { + _GLFW_REQUIRE_INIT_OR_RETURN(nil); + GLFWhandlefileopen prev = _glfw.ns.file_open_callback; + _glfw.ns.file_open_callback = callback; + return prev; +} + GLFWAPI GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *handle, GLFWcocoatogglefullscreenfun callback) { _GLFWwindow* window = (_GLFWwindow*) handle; _GLFW_REQUIRE_INIT_OR_RETURN(nil); diff --git a/glfw/glfw.py b/glfw/glfw.py index 15d7fb967..f9d181319 100755 --- a/glfw/glfw.py +++ b/glfw/glfw.py @@ -207,6 +207,7 @@ def generate_wrappers(glfw_header: str) -> None: void* glfwGetNSGLContext(GLFWwindow *window) uint32_t glfwGetCocoaMonitor(GLFWmonitor* monitor) GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow* window, GLFWcocoatextinputfilterfun callback) + GLFWhandlefileopen glfwSetCocoaFileOpenCallback(GLFWhandlefileopen callback) GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *window, GLFWcocoatogglefullscreenfun callback) GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback) GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback) @@ -248,6 +249,7 @@ const char *action_text, int32_t timeout, GLFWDBusnotificationcreatedfun callbac typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long); typedef bool (* GLFWapplicationshouldhandlereopenfun)(int); +typedef bool (* GLFWhandlefileopen)(const char*); typedef void (* GLFWapplicationwillfinishlaunchingfun)(void); typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*); typedef void (* GLFWcocoarenderframefun)(GLFWwindow*); diff --git a/kitty/boss.py b/kitty/boss.py index e5572fb4b..3c6c12daa 100755 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -144,6 +144,7 @@ class Boss: global_shortcuts: Dict[str, SingleKey] ): set_layout_options(opts) + self.cocoa_application_launched = False self.clipboard_buffers: Dict[str, str] = {} self.update_check_process: Optional[PopenType] = None self.window_id_map: WeakValueDictionary[int, Window] = WeakValueDictionary() @@ -1641,3 +1642,21 @@ class Boss: if w: output = '\n'.join(f'{k}={v}' for k, v in os.environ.items()).encode('utf-8') self.display_scrollback(w, output, ['less']) + + def open_file(self, path: str) -> None: + if path == ":cocoa::application launched::": + self.cocoa_application_launched = True + return + + def new_os_window() -> None: + self.new_os_window(path) + + if self.cocoa_application_launched or not self.os_window_map: + return new_os_window() + tab = self.active_tab + if tab is None: + return new_os_window() + w = tab.active_window + self.new_window(path) + if w is not None: + tab.remove_window(w) diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 1182fa8b4..b4ecac771 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -935,14 +935,25 @@ process_pending_closes(ChildMonitor *self) { // If we create new OS windows during wait_events(), using global menu actions // via the mouse causes a crash because of the way autorelease pools work in // glfw/cocoa. So we use a flag instead. -static unsigned int cocoa_pending_actions = 0; -static char *cocoa_pending_actions_wd = NULL; +static CocoaPendingAction cocoa_pending_actions = NO_COCOA_PENDING_ACTION; +typedef struct { + char* wd; + char **open_files; + size_t open_files_count; + size_t open_files_capacity; +} CocoaPendingActionsData; +static CocoaPendingActionsData cocoa_pending_actions_data = {0}; void set_cocoa_pending_action(CocoaPendingAction action, const char *wd) { if (wd) { - if (cocoa_pending_actions_wd) free(cocoa_pending_actions_wd); - cocoa_pending_actions_wd = strdup(wd); + if (action == OPEN_FILE) { + ensure_space_for(&cocoa_pending_actions_data, open_files, char*, cocoa_pending_actions_data.open_files_count + 8, open_files_capacity, 8, true); + cocoa_pending_actions_data.open_files[cocoa_pending_actions_data.open_files_count++] = strdup(wd); + } else { + if (cocoa_pending_actions_data.wd) free(cocoa_pending_actions_data.wd); + cocoa_pending_actions_data.wd = strdup(wd); + } } cocoa_pending_actions |= action; // The main loop may be blocking on the event queue, if e.g. unfocused. @@ -986,11 +997,21 @@ process_global_state(void *data) { if (cocoa_pending_actions & NEXT_TAB) { call_boss(next_tab, NULL); } if (cocoa_pending_actions & PREVIOUS_TAB) { call_boss(previous_tab, NULL); } if (cocoa_pending_actions & DETACH_TAB) { call_boss(detach_tab, NULL); } - if (cocoa_pending_actions_wd) { - if (cocoa_pending_actions & NEW_OS_WINDOW_WITH_WD) { call_boss(new_os_window_with_wd, "s", cocoa_pending_actions_wd); } - if (cocoa_pending_actions & NEW_TAB_WITH_WD) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_wd); } - free(cocoa_pending_actions_wd); - cocoa_pending_actions_wd = NULL; + if (cocoa_pending_actions_data.wd) { + if (cocoa_pending_actions & NEW_OS_WINDOW_WITH_WD) { call_boss(new_os_window_with_wd, "s", cocoa_pending_actions_data.wd); } + if (cocoa_pending_actions & NEW_TAB_WITH_WD) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_data.wd); } + free(cocoa_pending_actions_data.wd); + cocoa_pending_actions_data.wd = NULL; + } + if (cocoa_pending_actions_data.open_files_count) { + for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_files_count; cpa++) { + if (cocoa_pending_actions_data.open_files[cpa]) { + call_boss(open_file, "s", cocoa_pending_actions_data.open_files[cpa]); + free(cocoa_pending_actions_data.open_files[cpa]); + cocoa_pending_actions_data.open_files[cpa] = NULL; + } + } + cocoa_pending_actions_data.open_files_count = 0; } cocoa_pending_actions = 0; } @@ -1015,7 +1036,13 @@ main_loop(ChildMonitor *self, PyObject *a UNUSED) { state_check_timer = add_main_loop_timer(1000, true, do_state_check, self, NULL); run_main_loop(process_global_state, self); #ifdef __APPLE__ - if (cocoa_pending_actions_wd) { free(cocoa_pending_actions_wd); cocoa_pending_actions_wd = NULL; } + if (cocoa_pending_actions_data.wd) { free(cocoa_pending_actions_data.wd); cocoa_pending_actions_data.wd = NULL; } + if (cocoa_pending_actions_data.open_files) { + for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_files_count; cpa++) { + if (cocoa_pending_actions_data.open_files[cpa]) free(cocoa_pending_actions_data.open_files[cpa]); + } + free(cocoa_pending_actions_data.open_files); cocoa_pending_actions_data.open_files = NULL; + } #endif if (PyErr_Occurred()) return NULL; Py_RETURN_NONE; diff --git a/kitty/glfw-wrapper.c b/kitty/glfw-wrapper.c index 637aef8df..f1e2fdab9 100644 --- a/kitty/glfw-wrapper.c +++ b/kitty/glfw-wrapper.c @@ -388,6 +388,8 @@ load_glfw(const char* path) { *(void **) (&glfwSetCocoaTextInputFilter_impl) = dlsym(handle, "glfwSetCocoaTextInputFilter"); + *(void **) (&glfwSetCocoaFileOpenCallback_impl) = dlsym(handle, "glfwSetCocoaFileOpenCallback"); + *(void **) (&glfwSetCocoaToggleFullscreenIntercept_impl) = dlsym(handle, "glfwSetCocoaToggleFullscreenIntercept"); *(void **) (&glfwSetApplicationShouldHandleReopen_impl) = dlsym(handle, "glfwSetApplicationShouldHandleReopen"); diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index 6ff5a4718..797085bc2 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -1553,6 +1553,7 @@ typedef struct GLFWgamepadstate typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long); typedef bool (* GLFWapplicationshouldhandlereopenfun)(int); +typedef bool (* GLFWhandlefileopen)(const char*); typedef void (* GLFWapplicationwillfinishlaunchingfun)(void); typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*); typedef void (* GLFWcocoarenderframefun)(GLFWwindow*); @@ -2063,6 +2064,10 @@ typedef GLFWcocoatextinputfilterfun (*glfwSetCocoaTextInputFilter_func)(GLFWwind GFW_EXTERN glfwSetCocoaTextInputFilter_func glfwSetCocoaTextInputFilter_impl; #define glfwSetCocoaTextInputFilter glfwSetCocoaTextInputFilter_impl +typedef GLFWhandlefileopen (*glfwSetCocoaFileOpenCallback_func)(GLFWhandlefileopen); +GFW_EXTERN glfwSetCocoaFileOpenCallback_func glfwSetCocoaFileOpenCallback_impl; +#define glfwSetCocoaFileOpenCallback glfwSetCocoaFileOpenCallback_impl + typedef GLFWcocoatogglefullscreenfun (*glfwSetCocoaToggleFullscreenIntercept_func)(GLFWwindow*, GLFWcocoatogglefullscreenfun); GFW_EXTERN glfwSetCocoaToggleFullscreenIntercept_func glfwSetCocoaToggleFullscreenIntercept_impl; #define glfwSetCocoaToggleFullscreenIntercept glfwSetCocoaToggleFullscreenIntercept_impl diff --git a/kitty/glfw.c b/kitty/glfw.c index 12e34027d..b0ff141b3 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -379,6 +379,14 @@ application_close_requested_callback(int flags) { } } } + +#ifdef __APPLE__ +static bool +apple_file_open_callback(const char* filepath) { + set_cocoa_pending_action(OPEN_FILE, filepath); + return true; +} +#endif // }}} void @@ -844,6 +852,9 @@ glfw_init(PyObject UNUSED *self, PyObject *args) { #endif PyObject *ans = glfwInit(monotonic_start_time) ? Py_True: Py_False; if (ans == Py_True) { +#ifdef __APPLE__ + glfwSetCocoaFileOpenCallback(apple_file_open_callback); +#endif OSWindow w = {0}; set_os_window_dpi(&w); global_state.default_dpi.x = w.logical_dpi_x; diff --git a/kitty/state.h b/kitty/state.h index 1282ee3f8..1e0d237dc 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -261,6 +261,7 @@ void send_prerendered_sprites_for_window(OSWindow *w); #ifdef __APPLE__ void get_cocoa_key_equivalent(uint32_t, int, char *key, size_t key_sz, int*); typedef enum { + NO_COCOA_PENDING_ACTION = 0, PREFERENCES_WINDOW = 1, NEW_OS_WINDOW = 2, NEW_OS_WINDOW_WITH_WD = 4, @@ -271,6 +272,7 @@ typedef enum { NEXT_TAB = 128, PREVIOUS_TAB = 256, DETACH_TAB = 512, + OPEN_FILE = 1024 } CocoaPendingAction; void set_cocoa_pending_action(CocoaPendingAction action, const char*); #endif diff --git a/setup.py b/setup.py index a018fcd0a..1b21c39ef 100755 --- a/setup.py +++ b/setup.py @@ -914,6 +914,24 @@ def macos_info_plist() -> bytes: def access(what: str, verb: str = 'would like to access') -> str: return f'A program running inside kitty {verb} {what}' + docs = [ + { + 'CFBundleTypeName': 'Terminal scripts', + 'CFBundleTypeExtensions': ['command', 'sh', 'zsh', 'bash', 'fish', 'tool'], + 'CFBundleTypeIconFile': appname + '.icns', + 'CFBundleTypeRole': 'Editor', + }, + { + 'CFBundleTypeName': 'Folders', + 'CFBundleTypeOSTypes': ['fold'], + 'CFBundleTypeRole': 'Editor', + }, + { + 'LSItemContentTypes': ['public.unix-executable'], + 'CFBundleTypeRole': 'Shell', + }, + ] + pl = dict( # see https://github.com/kovidgoyal/kitty/issues/1233 CFBundleDevelopmentRegion='English', @@ -927,6 +945,7 @@ def macos_info_plist() -> bytes: CFBundlePackageType='APPL', CFBundleSignature='????', CFBundleExecutable=appname, + CFBundleDocumentTypes=docs, LSMinimumSystemVersion='10.12.0', LSRequiresNativeExecution=True, NSAppleScriptEnabled=False,