Skip to content

Commit

Permalink
A new option menu_map that allows adding entries to the global menuba…
Browse files Browse the repository at this point in the history
…r on macOS
  • Loading branch information
kovidgoyal committed Oct 9, 2023
1 parent 3338e4f commit f73d32e
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 7 deletions.
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ Detailed list of changes
0.30.2 [future]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

- A new option :opt:`menu_map` that allows adding entries to the global menubar on macOS (:disc:`6680`)

- A new mouse action ``mouse_selection word_and_line_from_point`` to select the current word under the mouse cursor and extend to end of line (:pull:`6663`)

- macOS: When running the default shell with the login program fix :file:`~/.hushlogin` not being respected when opening windows not in the home directory (:iss:`6689`)
Expand Down
4 changes: 4 additions & 0 deletions kitty/boss.py
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,10 @@ def report_match(f: Callable[..., Any]) -> None:
return True
return False

def user_menu_action(self, defn: str) -> None:
' Callback from user actions in the macOS global menu bar or other menus '
self.combine(defn)

@ac('misc', '''
Combine multiple actions and map to a single keypress
Expand Down
1 change: 1 addition & 0 deletions kitty/child-monitor.c
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,7 @@ process_cocoa_pending_actions(void) {
if (cocoa_pending_actions_data.wd) {
if (cocoa_pending_actions[NEW_OS_WINDOW_WITH_WD]) { call_boss(new_os_window_with_wd, "sO", cocoa_pending_actions_data.wd, Py_True); }
if (cocoa_pending_actions[NEW_TAB_WITH_WD]) { call_boss(new_tab_with_wd, "sO", cocoa_pending_actions_data.wd, Py_True); }
if (cocoa_pending_actions[USER_MENU_ACTION]) { call_boss(user_menu_action, "s", cocoa_pending_actions_data.wd); }
free(cocoa_pending_actions_data.wd);
cocoa_pending_actions_data.wd = NULL;
}
Expand Down
56 changes: 56 additions & 0 deletions kitty/cocoa_window.m
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,16 @@ - (void)update {
@end
// }}}

@interface UserMenuItem : NSMenuItem
@property (nonatomic) size_t action_index;
@end

@implementation UserMenuItem {
}
@end



@interface GlobalMenuTarget : NSObject
+ (GlobalMenuTarget *) shared_instance;
@end
Expand All @@ -220,6 +230,13 @@ + (GlobalMenuTarget *) shared_instance;

@implementation GlobalMenuTarget

- (void)user_menu_action:(id)sender {
UserMenuItem *m = sender;
if (m.action_index < OPT(global_menu).count && OPT(global_menu.entries)) {
set_cocoa_pending_action(USER_MENU_ACTION, OPT(global_menu).entries[m.action_index].definition);
}
}

PENDING(edit_config_file, PREFERENCES_WINDOW)
PENDING(new_os_window, NEW_OS_WINDOW)
PENDING(detach_tab, DETACH_TAB)
Expand Down Expand Up @@ -559,6 +576,35 @@ - (BOOL)openFileURLs:(NSPasteboard*)pasteboard

// global menu {{{

static void
add_user_global_menu_entry(struct MenuItem *e, NSMenu *bar, size_t action_index) {
NSMenu *parent = bar;
UserMenuItem *final_item = nil;
GlobalMenuTarget *global_menu_target = [GlobalMenuTarget shared_instance];
for (size_t i = 0; i < e->location_count; i++) {
NSMenuItem *item = [parent itemWithTitle:@(e->location[i])];
if (!item) {
final_item = [[UserMenuItem alloc] initWithTitle:@(e->location[i]) action:@selector(user_menu_action:) keyEquivalent:@""];
final_item.target = global_menu_target;
[parent addItem:final_item];
item = final_item;
[final_item release];
}
if (i + 1 < e->location_count) {
if (![item hasSubmenu]) {
NSMenu* sub_menu = [[NSMenu alloc] initWithTitle:item.title];
[item setSubmenu:sub_menu];
[sub_menu release];
}
parent = [item submenu];
if (!parent) return;
}
}
if (final_item != nil) {
final_item.action_index = action_index;
}
}

void
cocoa_create_global_menu(void) {
NSString* app_name = find_app_name();
Expand Down Expand Up @@ -665,8 +711,17 @@ - (BOOL)openFileURLs:(NSPasteboard*)pasteboard
[NSApp setHelpMenu:helpMenu];
[helpMenu release];

if (OPT(global_menu.entries)) {
for (size_t i = 0; i < OPT(global_menu.count); i++) {
struct MenuItem *e = OPT(global_menu.entries) + i;
if (e->definition && e->location && e->location_count > 1) {
add_user_global_menu_entry(e, bar, i);
}
}
}
[bar release];


class_addMethod(
object_getClass([NSApp delegate]),
@selector(applicationDockMenu:),
Expand All @@ -675,6 +730,7 @@ - (BOOL)openFileURLs:(NSPasteboard*)pasteboard


[NSApp setServicesProvider:[[[ServiceProvider alloc] init] autorelease]];

#undef MENU_ITEM
}

Expand Down
14 changes: 14 additions & 0 deletions kitty/options/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -3096,6 +3096,20 @@
environment variable so child processes know about the forwarding.
''')

opt('+menu_map', '',
option_type='menu_map', add_to_default=False, ctype='!menu_map',
long_text='''
Specify entries for various menus in kitty. Currently only the global menubar on macOS
is supported. For example::
menu_map global "Actions::Launch something special" launch --hold --type=os-window sh -c "echo hello world"
This will create a menu entry named "Launch something special" in an "Actions" menu in the macOS global menubar.
Sub-menus can be created by adding more levels separated by ::.
'''
)


egr() # }}}


Expand Down
19 changes: 12 additions & 7 deletions kitty/options/parse.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions kitty/options/to-c-generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions kitty/options/to-c.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,49 @@ url_prefixes(PyObject *up, Options *opts) {
}
}

static inline void
free_menu_map(Options *opts) {
if (opts->global_menu.entries) {
for (size_t i=0; i < opts->global_menu.count; i++) {
struct MenuItem *e = opts->global_menu.entries + i;
if (e->definition) { free((void*)e->definition); }
if (e->location) {
for (size_t l=0; l < e->location_count; l++) { free((void*)e->location[l]); }
free(e->location);
}
}
free(opts->global_menu.entries); opts->global_menu.entries = NULL;
}
}

static void
menu_map(PyObject *entry_dict, Options *opts) {
if (!PyDict_Check(entry_dict)) { PyErr_SetString(PyExc_TypeError, "menu_map entries must be a dict"); return; }
free_menu_map(opts);
size_t maxnum = PyDict_Size(entry_dict);
opts->global_menu.count = 0;
opts->global_menu.entries = calloc(maxnum, sizeof(opts->global_menu.entries[0]));
if (!opts->global_menu.entries) { PyErr_NoMemory(); return; }

PyObject *key, *value;
Py_ssize_t pos = 0;

while (PyDict_Next(entry_dict, &pos, &key, &value)) {
if (PyTuple_Check(key) && PyTuple_GET_SIZE(key) > 1 && PyUnicode_Check(value) && PyUnicode_CompareWithASCIIString(PyTuple_GET_ITEM(key, 0), "global") == 0) {
struct MenuItem *e = opts->global_menu.entries + opts->global_menu.count++;
e->location_count = PyTuple_GET_SIZE(key) - 1;
e->location = calloc(e->location_count, sizeof(e->location[0]));
if (!e->location) { PyErr_NoMemory(); return; }
e->definition = strdup(PyUnicode_AsUTF8(value));
if (!e->definition) { PyErr_NoMemory(); return; }
for (size_t i = 0; i < e->location_count; i++) {
e->location[i] = strdup(PyUnicode_AsUTF8(PyTuple_GET_ITEM(key, i+1)));
if (!e->location[i]) { PyErr_NoMemory(); return; }
}
}
}
}

static void
text_composition_strategy(PyObject *val, Options *opts) {
if (!PyUnicode_Check(val)) { PyErr_SetString(PyExc_TypeError, "text_rendering_strategy must be a string"); return; }
Expand Down
3 changes: 3 additions & 0 deletions kitty/options/types.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions kitty/options/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,26 @@ def store_multiple(val: str, current_val: Container[str]) -> Iterable[Tuple[str,
yield val, val


def menu_map(val: str, current_val: Container[str]) -> Iterable[Tuple[Tuple[str, ...], str]]:
parts = val.split(maxsplit=1)
if len(parts) != 2:
raise ValueError(f'Ignoring invalid menu action: {val}')
if parts[0] != 'global':
raise ValueError(f'Unknown menu type: {parts[0]}. Known types: global')
start = 0
if parts[1].startswith('"'):
start = 1
idx = parts[1].find('"', 1)
if idx == -1:
raise ValueError(f'The menu entry name in {val} must end with a double quote')
else:
idx = parts[1].find(' ')
if idx == -1:
raise ValueError(f'The menu entry {val} must have an action')
location = ('global',) + tuple(parts[1][start:idx].split('::'))
yield location, parts[1][idx+1:].lstrip()


allowed_shell_integration_values = frozenset({'enabled', 'disabled', 'no-rc', 'no-cursor', 'no-title', 'no-prompt-mark', 'no-complete', 'no-cwd', 'no-sudo'})


Expand Down
8 changes: 8 additions & 0 deletions kitty/state.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ typedef struct {

typedef enum AdjustmentUnit { POINT = 0, PERCENT = 1, PIXEL = 2 } AdjustmentUnit;

struct MenuItem {
const char* *location;
size_t location_count;
const char *definition;
};

typedef struct {
monotonic_t visual_bell_duration, cursor_blink_interval, cursor_stop_blinking_after, mouse_hide_wait, click_interval;
double wheel_scroll_multiplier, touch_scroll_multiplier;
Expand Down Expand Up @@ -92,6 +98,7 @@ typedef struct {
int background_blur;
long macos_titlebar_color;
unsigned long wayland_titlebar_color;
struct { struct MenuItem *entries; size_t count; } global_menu;
} Options;

typedef struct WindowLogoRenderData {
Expand Down Expand Up @@ -343,6 +350,7 @@ typedef enum {
HIDE_OTHERS,
MINIMIZE,
QUIT,
USER_MENU_ACTION,

NUM_COCOA_PENDING_ACTIONS
} CocoaPendingAction;
Expand Down

0 comments on commit f73d32e

Please sign in to comment.