A new option menu_map that allows adding entries to the global menubar on macOS
This commit is contained in:
parent
3338e4fb25
commit
f73d32e15d
11 changed files with 178 additions and 7 deletions
|
|
@ -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`)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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();
|
||||
|
|
@ -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:),
|
||||
|
|
@ -675,6 +730,7 @@ - (BOOL)openFileURLs:(NSPasteboard*)pasteboard
|
|||
|
||||
|
||||
[NSApp setServicesProvider:[[[ServiceProvider alloc] init] autorelease]];
|
||||
|
||||
#undef MENU_ITEM
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() # }}}
|
||||
|
||||
|
||||
|
|
|
|||
19
kitty/options/parse.py
generated
19
kitty/options/parse.py
generated
|
|
@ -12,13 +12,13 @@
|
|||
config_or_absolute_path, copy_on_select, cursor_text_color, deprecated_adjust_line_height,
|
||||
deprecated_hide_window_decorations_aliases, deprecated_macos_show_window_title_in_menubar_alias,
|
||||
deprecated_send_text, disable_ligatures, edge_width, env, font_features, hide_window_decorations,
|
||||
macos_option_as_alt, macos_titlebar_color, modify_font, narrow_symbols, optional_edge_width,
|
||||
parse_map, parse_mouse_map, paste_actions, remote_control_password, resize_debounce_time,
|
||||
scrollback_lines, scrollback_pager_history_size, shell_integration, store_multiple, symbol_map,
|
||||
tab_activity_symbol, tab_bar_edge, tab_bar_margin_height, tab_bar_min_tabs, tab_fade,
|
||||
tab_font_style, tab_separator, tab_title_template, titlebar_color, to_cursor_shape, to_font_size,
|
||||
to_layout_names, to_modifiers, url_prefixes, url_style, visual_window_select_characters,
|
||||
window_border_width, window_size
|
||||
macos_option_as_alt, macos_titlebar_color, menu_map, modify_font, narrow_symbols,
|
||||
optional_edge_width, parse_map, parse_mouse_map, paste_actions, remote_control_password,
|
||||
resize_debounce_time, scrollback_lines, scrollback_pager_history_size, shell_integration,
|
||||
store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge, tab_bar_margin_height,
|
||||
tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, tab_title_template, titlebar_color,
|
||||
to_cursor_shape, to_font_size, to_layout_names, to_modifiers, url_prefixes, url_style,
|
||||
visual_window_select_characters, window_border_width, window_size
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1105,6 +1105,10 @@ def mark3_background(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|||
def mark3_foreground(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['mark3_foreground'] = to_color(val)
|
||||
|
||||
def menu_map(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
for k, v in menu_map(val, ans["menu_map"]):
|
||||
ans["menu_map"][k] = v
|
||||
|
||||
def modify_font(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
for k, v in modify_font(val):
|
||||
ans["modify_font"][k] = v
|
||||
|
|
@ -1407,6 +1411,7 @@ def create_result_dict() -> typing.Dict[str, typing.Any]:
|
|||
'exe_search_path': {},
|
||||
'font_features': {},
|
||||
'kitten_alias': {},
|
||||
'menu_map': {},
|
||||
'modify_font': {},
|
||||
'narrow_symbols': {},
|
||||
'remote_control_password': {},
|
||||
|
|
|
|||
15
kitty/options/to-c-generated.h
generated
15
kitty/options/to-c-generated.h
generated
|
|
@ -941,6 +941,19 @@ convert_from_opts_allow_hyperlinks(PyObject *py_opts, Options *opts) {
|
|||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_python_menu_map(PyObject *val, Options *opts) {
|
||||
menu_map(val, opts);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_opts_menu_map(PyObject *py_opts, Options *opts) {
|
||||
PyObject *ret = PyObject_GetAttrString(py_opts, "menu_map");
|
||||
if (ret == NULL) return;
|
||||
convert_from_python_menu_map(ret, opts);
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_python_wayland_titlebar_color(PyObject *val, Options *opts) {
|
||||
opts->wayland_titlebar_color = PyLong_AsUnsignedLong(val);
|
||||
|
|
@ -1230,6 +1243,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
|
|||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_allow_hyperlinks(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_menu_map(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_wayland_titlebar_color(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_macos_titlebar_color(py_opts, opts);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
3
kitty/options/types.py
generated
3
kitty/options/types.py
generated
|
|
@ -399,6 +399,7 @@
|
|||
'mark2_foreground',
|
||||
'mark3_background',
|
||||
'mark3_foreground',
|
||||
'menu_map',
|
||||
'modify_font',
|
||||
'mouse_hide_wait',
|
||||
'mouse_map',
|
||||
|
|
@ -629,6 +630,7 @@ class Options:
|
|||
exe_search_path: typing.Dict[str, str] = {}
|
||||
font_features: typing.Dict[str, typing.Tuple[kitty.fonts.FontFeature, ...]] = {}
|
||||
kitten_alias: typing.Dict[str, str] = {}
|
||||
menu_map: typing.Dict[typing.Tuple[str, ...], str] = {}
|
||||
modify_font: typing.Dict[str, kitty.fonts.FontModification] = {}
|
||||
narrow_symbols: typing.Dict[typing.Tuple[int, int], int] = {}
|
||||
remote_control_password: typing.Dict[str, typing.Sequence[str]] = {}
|
||||
|
|
@ -750,6 +752,7 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any:
|
|||
defaults.exe_search_path = {}
|
||||
defaults.font_features = {}
|
||||
defaults.kitten_alias = {}
|
||||
defaults.menu_map = {}
|
||||
defaults.modify_font = {}
|
||||
defaults.narrow_symbols = {}
|
||||
defaults.remote_control_password = {}
|
||||
|
|
|
|||
|
|
@ -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'})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 {
|
||||
|
|
@ -343,6 +350,7 @@ typedef enum {
|
|||
HIDE_OTHERS,
|
||||
MINIMIZE,
|
||||
QUIT,
|
||||
USER_MENU_ACTION,
|
||||
|
||||
NUM_COCOA_PENDING_ACTIONS
|
||||
} CocoaPendingAction;
|
||||
|
|
|
|||
Loading…
Reference in a new issue