macOS: Allow opening script and command

Fixes #3366
This commit is contained in:
Kovid Goyal 2021-03-10 21:33:29 +05:30
parent 0f020d5b37
commit f70c9842f5
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
12 changed files with 124 additions and 10 deletions

View File

@ -55,6 +55,8 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
- 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

View File

@ -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();

View File

@ -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;

View File

@ -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);

View File

@ -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*);

View File

@ -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)

View File

@ -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;

2
kitty/glfw-wrapper.c generated
View File

@ -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");

5
kitty/glfw-wrapper.h generated
View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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,