diff --git a/glfw/cocoa_init.m b/glfw/cocoa_init.m index 6aac09149..ab3f94b29 100644 --- a/glfw/cocoa_init.m +++ b/glfw/cocoa_init.m @@ -358,32 +358,49 @@ 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; +- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename { + (void)sender; + if (!filename || !_glfw.ns.url_open_callback) return NO; + const char *url = NULL; @try { - path = [[NSFileManager defaultManager] fileSystemRepresentationWithPath: filename]; + url = [[[NSURL fileURLWithPath:filename] absoluteString] UTF8String]; } @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); + if (!url) return NO; + return _glfw.ns.url_open_callback(url); } - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames { (void)sender; - if (!_glfw.ns.file_open_callback || !filenames) return; + if (!_glfw.ns.url_open_callback || !filenames) return; for (id x in filenames) { NSString *filename = x; - const char *path = NULL; + const char *url = NULL; @try { - path = [[NSFileManager defaultManager] fileSystemRepresentationWithPath: filename]; + url = [[[NSURL fileURLWithPath:filename] absoluteString] UTF8String]; } @catch(NSException *exc) { NSLog(@"Converting openFiles filename: %@ failed with error: %@", filename, exc.reason); } - if (path) _glfw.ns.file_open_callback(path); + if (url) _glfw.ns.url_open_callback(url); + } +} + +// Remove openFile and openFiles when the minimum supported macOS version is 10.13 +- (void)application:(NSApplication *)sender openURLs:(NSArray *)urls +{ + (void)sender; + if (!_glfw.ns.url_open_callback || !urls) return; + for (id x in urls) { + NSURL *ns_url = x; + const char *url = NULL; + @try { + url = [[ns_url absoluteString] UTF8String]; + } @catch(NSException *exc) { + NSLog(@"Converting openURLs url: %@ failed with error: %@", ns_url, exc.reason); + } + if (url) _glfw.ns.url_open_callback(url); } } @@ -391,7 +408,7 @@ static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL; { (void)notification; [NSApp stop:nil]; - if (_glfw.ns.file_open_callback) _glfw.ns.file_open_callback(":cocoa::application launched::"); + if (_glfw.ns.url_open_callback) _glfw.ns.url_open_callback(":cocoa::application launched::"); CGDisplayRegisterReconfigurationCallback(display_reconfigured, NULL); _glfwCocoaPostEmptyEvent(); diff --git a/glfw/cocoa_platform.h b/glfw/cocoa_platform.h index 4d6a16300..08be21b5e 100644 --- a/glfw/cocoa_platform.h +++ b/glfw/cocoa_platform.h @@ -68,7 +68,7 @@ typedef void* CVDisplayLinkRef; typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int, unsigned long); typedef bool (* GLFWapplicationshouldhandlereopenfun)(int); -typedef bool (* GLFWhandlefileopen)(const char*); +typedef bool (* GLFWhandleurlopen)(const char*); typedef void (* GLFWapplicationwillfinishlaunchingfun)(void); typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*); typedef void (* GLFWcocoarenderframefun)(GLFWwindow*); @@ -199,8 +199,8 @@ typedef struct _GLFWlibraryNS _GLFWDisplayLinkNS entries[256]; size_t count; } displayLinks; - // the callback to handle file open events - GLFWhandlefileopen file_open_callback; + // the callback to handle url open events + GLFWhandleurlopen url_open_callback; } _GLFWlibraryNS; diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index 94c693d6e..16b24f71f 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -2642,10 +2642,10 @@ GLFWAPI GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow *hand return previous; } -GLFWAPI GLFWhandlefileopen glfwSetCocoaFileOpenCallback(GLFWhandlefileopen callback) { +GLFWAPI GLFWhandleurlopen glfwSetCocoaURLOpenCallback(GLFWhandleurlopen callback) { _GLFW_REQUIRE_INIT_OR_RETURN(nil); - GLFWhandlefileopen prev = _glfw.ns.file_open_callback; - _glfw.ns.file_open_callback = callback; + GLFWhandleurlopen prev = _glfw.ns.url_open_callback; + _glfw.ns.url_open_callback = callback; return prev; } diff --git a/glfw/glfw.py b/glfw/glfw.py index e7a5cf0c7..4c23a15d5 100755 --- a/glfw/glfw.py +++ b/glfw/glfw.py @@ -233,7 +233,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) + GLFWhandleurlopen glfwSetCocoaURLOpenCallback(GLFWhandleurlopen callback) GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *window, GLFWcocoatogglefullscreenfun callback) GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback) GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback) @@ -277,7 +277,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 bool (* GLFWhandleurlopen)(const char*); typedef void (* GLFWapplicationwillfinishlaunchingfun)(void); typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*); typedef void (* GLFWcocoarenderframefun)(GLFWwindow*); @@ -292,6 +292,8 @@ const char* load_glfw(const char* path); f.write(header) code = ''' +// generated by glfw.py DO NOT edit + #define GFW_EXTERN #include "data-types.h" #include "glfw-wrapper.h" diff --git a/kittens/themes/collection.py b/kittens/themes/collection.py index c7d24e79a..6373d6322 100644 --- a/kittens/themes/collection.py +++ b/kittens/themes/collection.py @@ -36,7 +36,7 @@ def patch_conf(raw: str, theme_name: str) -> str: nraw = raw + addition # comment out all existing color definitions color_conf_items = ( # {{{ - # generated by gen_config.py do not EDIT + # generated by gen-config.py DO NOT edit # ALL_COLORS_START 'active_border_color', 'active_tab_background', diff --git a/kitty/boss.py b/kitty/boss.py index d98927134..c9d611ce9 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -2234,13 +2234,13 @@ class Boss: output = '\n'.join(f'{k}={v}' for k, v in os.environ.items()) self.display_scrollback(w, output, title=_('Current kitty env vars'), report_cursor=False) - def open_file(self, path: str) -> None: - if path == ":cocoa::application launched::": + def launch_url(self, url: str) -> None: + if url == ":cocoa::application launched::": self.cocoa_application_launched = True return from .open_actions import actions_for_launch from .launch import force_window_launch - actions = list(actions_for_launch(path)) + actions = list(actions_for_launch(url)) tab = self.active_tab if tab is not None: w = tab.active_window @@ -2254,7 +2254,7 @@ class Boss: if not actions: with force_window_launch(needs_window_replaced): - self.launch(kitty_exe(), '+runpy', f'print("The file:", {path!r}, "is of unknown type, cannot open it.");' + self.launch(kitty_exe(), '+runpy', f'print("The url:", {url!r}, "is of unknown type, cannot open it.");' 'from kitty.utils import hold_till_enter; hold_till_enter(); raise SystemExit(1)') clear_initial_window() else: diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index b51d67fab..396181b6e 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -1001,18 +1001,18 @@ static bool cocoa_pending_actions[NUM_COCOA_PENDING_ACTIONS] = {0}; static bool has_cocoa_pending_actions = false; typedef struct { char* wd; - char **open_files; - size_t open_files_count; - size_t open_files_capacity; + char **open_urls; + size_t open_urls_count; + size_t open_urls_capacity; } CocoaPendingActionsData; static CocoaPendingActionsData cocoa_pending_actions_data = {0}; void set_cocoa_pending_action(CocoaPendingAction action, const char *wd) { if (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); + if (action == LAUNCH_URL) { + ensure_space_for(&cocoa_pending_actions_data, open_urls, char*, cocoa_pending_actions_data.open_urls_count + 8, open_urls_capacity, 8, true); + cocoa_pending_actions_data.open_urls[cocoa_pending_actions_data.open_urls_count++] = strdup(wd); } else { if (cocoa_pending_actions_data.wd) free(cocoa_pending_actions_data.wd); cocoa_pending_actions_data.wd = strdup(wd); @@ -1047,15 +1047,15 @@ process_cocoa_pending_actions(void) { 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; + if (cocoa_pending_actions_data.open_urls_count) { + for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_urls_count; cpa++) { + if (cocoa_pending_actions_data.open_urls[cpa]) { + call_boss(launch_url, "s", cocoa_pending_actions_data.open_urls[cpa]); + free(cocoa_pending_actions_data.open_urls[cpa]); + cocoa_pending_actions_data.open_urls[cpa] = NULL; } } - cocoa_pending_actions_data.open_files_count = 0; + cocoa_pending_actions_data.open_urls_count = 0; } memset(cocoa_pending_actions, 0, sizeof(cocoa_pending_actions)); has_cocoa_pending_actions = false; @@ -1115,11 +1115,11 @@ main_loop(ChildMonitor *self, PyObject *a UNUSED) { run_main_loop(process_global_state, self); #ifdef __APPLE__ 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]); + if (cocoa_pending_actions_data.open_urls) { + for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_urls_count; cpa++) { + if (cocoa_pending_actions_data.open_urls[cpa]) free(cocoa_pending_actions_data.open_urls[cpa]); } - free(cocoa_pending_actions_data.open_files); cocoa_pending_actions_data.open_files = NULL; + free(cocoa_pending_actions_data.open_urls); cocoa_pending_actions_data.open_urls = NULL; } #endif if (PyErr_Occurred()) return NULL; diff --git a/kitty/glfw-wrapper.c b/kitty/glfw-wrapper.c index 46f0ba106..7ad755dfb 100644 --- a/kitty/glfw-wrapper.c +++ b/kitty/glfw-wrapper.c @@ -1,4 +1,6 @@ +// generated by glfw.py DO NOT edit + #define GFW_EXTERN #include "data-types.h" #include "glfw-wrapper.h" @@ -405,8 +407,8 @@ load_glfw(const char* path) { *(void **) (&glfwSetCocoaTextInputFilter_impl) = dlsym(handle, "glfwSetCocoaTextInputFilter"); if (glfwSetCocoaTextInputFilter_impl == NULL) dlerror(); // clear error indicator - *(void **) (&glfwSetCocoaFileOpenCallback_impl) = dlsym(handle, "glfwSetCocoaFileOpenCallback"); - if (glfwSetCocoaFileOpenCallback_impl == NULL) dlerror(); // clear error indicator + *(void **) (&glfwSetCocoaURLOpenCallback_impl) = dlsym(handle, "glfwSetCocoaURLOpenCallback"); + if (glfwSetCocoaURLOpenCallback_impl == NULL) dlerror(); // clear error indicator *(void **) (&glfwSetCocoaToggleFullscreenIntercept_impl) = dlsym(handle, "glfwSetCocoaToggleFullscreenIntercept"); if (glfwSetCocoaToggleFullscreenIntercept_impl == NULL) dlerror(); // clear error indicator diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index 0b9b1750b..7bc8a1ce1 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -1597,7 +1597,7 @@ typedef struct GLFWgamepadstate typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long); typedef bool (* GLFWapplicationshouldhandlereopenfun)(int); -typedef bool (* GLFWhandlefileopen)(const char*); +typedef bool (* GLFWhandleurlopen)(const char*); typedef void (* GLFWapplicationwillfinishlaunchingfun)(void); typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*); typedef void (* GLFWcocoarenderframefun)(GLFWwindow*); @@ -2124,9 +2124,9 @@ 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 GLFWhandleurlopen (*glfwSetCocoaURLOpenCallback_func)(GLFWhandleurlopen); +GFW_EXTERN glfwSetCocoaURLOpenCallback_func glfwSetCocoaURLOpenCallback_impl; +#define glfwSetCocoaURLOpenCallback glfwSetCocoaURLOpenCallback_impl typedef GLFWcocoatogglefullscreenfun (*glfwSetCocoaToggleFullscreenIntercept_func)(GLFWwindow*, GLFWcocoatogglefullscreenfun); GFW_EXTERN glfwSetCocoaToggleFullscreenIntercept_func glfwSetCocoaToggleFullscreenIntercept_impl; diff --git a/kitty/glfw.c b/kitty/glfw.c index 6a89bde29..9d92013cf 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -455,8 +455,8 @@ static void get_window_dpi(GLFWwindow *w, double *x, double *y); #ifdef __APPLE__ static bool -apple_file_open_callback(const char* filepath) { - set_cocoa_pending_action(OPEN_FILE, filepath); +apple_url_open_callback(const char* url) { + set_cocoa_pending_action(LAUNCH_URL, url); return true; } @@ -1033,7 +1033,7 @@ glfw_init(PyObject UNUSED *self, PyObject *args) { PyObject *ans = glfwInit(monotonic_start_time) ? Py_True: Py_False; if (ans == Py_True) { #ifdef __APPLE__ - glfwSetCocoaFileOpenCallback(apple_file_open_callback); + glfwSetCocoaURLOpenCallback(apple_url_open_callback); #else glfwSetDrawTextFunction(draw_text_callback); #endif diff --git a/kitty/open_actions.py b/kitty/open_actions.py index 5009b81da..5495d9a30 100644 --- a/kitty/open_actions.py +++ b/kitty/open_actions.py @@ -9,7 +9,7 @@ from contextlib import suppress from typing import ( Any, Dict, Iterable, Iterator, List, NamedTuple, Optional, Tuple, cast ) -from urllib.parse import ParseResult, quote, unquote, urlparse +from urllib.parse import ParseResult, unquote, urlparse from .conf.utils import KeyAction, to_cmdline_implementation from .constants import config_dir @@ -243,6 +243,10 @@ action launch --type=os-window $EDITOR $FILE_PATH protocol file mime image/* action launch --type=os-window kitty +kitten icat --hold $FILE_PATH + +# Open ssh URLs with ssh command +protocol ssh +action launch --type=os-window ssh $URL '''.splitlines())) @@ -254,8 +258,10 @@ def actions_for_url(url: str, actions_spec: Optional[str] = None) -> Iterator[Ke yield from actions_for_url_from_list(url, actions) -def actions_for_launch(path: str) -> Iterator[KeyAction]: - url = f'file://{quote(path)}' +def actions_for_launch(url: str) -> Iterator[KeyAction]: + # Custom launch actions using kitty URL scheme needs to be prefixed with `kitty:///launch/` + if url.startswith('kitty://') and not url.startswith('kitty:///launch/'): + return found = False for action in actions_for_url_from_list(url, load_launch_actions()): found = True diff --git a/kitty/options/definition.py b/kitty/options/definition.py index ea453812f..64be54704 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -377,7 +377,7 @@ The program with which to open URLs that are clicked on. The special value ''' ) -opt('url_prefixes', 'http https file ftp gemini irc gopher mailto news git', +opt('url_prefixes', 'file ftp ftps gemini git gopher http https irc ircs kitty mailto news sftp ssh', option_type='url_prefixes', ctype='!url_prefixes', long_text=''' The set of URL prefixes to look for when detecting a URL under the mouse cursor. diff --git a/kitty/state.h b/kitty/state.h index bacc9cc14..910059f48 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -298,7 +298,7 @@ typedef enum { NEXT_TAB, PREVIOUS_TAB, DETACH_TAB, - OPEN_FILE, + LAUNCH_URL, NEW_WINDOW, CLOSE_WINDOW, RESET_TERMINAL, diff --git a/setup.py b/setup.py index 30f1426fc..b609de2c7 100755 --- a/setup.py +++ b/setup.py @@ -1026,6 +1026,74 @@ def macos_info_plist() -> bytes: }, ] + url_schemes = [ + { + 'CFBundleURLName': 'File URL', + 'CFBundleURLSchemes': ['file'], + }, + { + 'CFBundleURLName': 'FTP URL', + 'CFBundleURLSchemes': ['ftp', 'ftps'], + }, + { + 'CFBundleURLName': 'Gemini URL', + 'CFBundleURLSchemes': ['gemini'], + }, + { + 'CFBundleURLName': 'Git URL', + 'CFBundleURLSchemes': ['git'], + }, + { + 'CFBundleURLName': 'Gopher URL', + 'CFBundleURLSchemes': ['gopher'], + }, + { + 'CFBundleURLName': 'HTTP URL', + 'CFBundleURLSchemes': ['http', 'https'], + }, + { + 'CFBundleURLName': 'IRC URL', + 'CFBundleURLSchemes': ['irc', 'irc6', 'ircs'], + }, + { + 'CFBundleURLName': 'kitty URL', + 'CFBundleURLSchemes': ['kitty'], + 'LSHandlerRank': 'Owner', + 'LSIsAppleDefaultForScheme': True, + }, + { + 'CFBundleURLName': 'Mail Address URL', + 'CFBundleURLSchemes': ['mailto'], + }, + { + 'CFBundleURLName': 'News URL', + 'CFBundleURLSchemes': ['news', 'nntp'], + }, + { + 'CFBundleURLName': 'SSH and SFTP URL', + 'CFBundleURLSchemes': ['ssh', 'sftp'], + }, + { + 'CFBundleURLName': 'Telnet URL', + 'CFBundleURLSchemes': ['telnet'], + }, + ] + + services = [ + { + 'NSMenuItem': {'default': f'New {appname} Tab Here'}, + 'NSMessage': 'openTab', + 'NSRequiredContext': {'NSTextContent': 'FilePath'}, + 'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'], + }, + { + 'NSMenuItem': {'default': f'New {appname} Window Here'}, + 'NSMessage': 'openOSWindow', + 'NSRequiredContext': {'NSTextContent': 'FilePath'}, + 'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'], + }, + ] + pl = dict( # Naming CFBundleName=appname, @@ -1043,7 +1111,6 @@ def macos_info_plist() -> bytes: # Categorization CFBundlePackageType='APPL', CFBundleSignature='????', - CFBundleDocumentTypes=docs, LSApplicationCategoryType='public.app-category.utilities', # App Execution CFBundleExecutable=appname, @@ -1061,21 +1128,11 @@ def macos_info_plist() -> bytes: NSSupportsAutomaticGraphicsSwitching=True, # Needed for dark mode in Mojave when linking against older SDKs NSRequiresAquaSystemAppearance='NO', + # Document and URL Types + CFBundleDocumentTypes=docs, + CFBundleURLTypes=url_schemes, # Services - NSServices=[ - { - 'NSMenuItem': {'default': f'New {appname} Tab Here'}, - 'NSMessage': 'openTab', - 'NSRequiredContext': {'NSTextContent': 'FilePath'}, - 'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'], - }, - { - 'NSMenuItem': {'default': f'New {appname} Window Here'}, - 'NSMessage': 'openOSWindow', - 'NSRequiredContext': {'NSTextContent': 'FilePath'}, - 'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'], - }, - ], + NSServices=services, # Calendar and Reminders NSCalendarsUsageDescription=access('your calendar data.'), NSRemindersUsageDescription=access('your reminders.'),