A new option menu_map that allows adding entries to the global menubar on macOS

This commit is contained in:
Kovid Goyal
2023-10-09 19:47:25 +05:30
parent 3338e4fb25
commit f73d32e15d
11 changed files with 178 additions and 7 deletions

View File

@@ -3096,6 +3096,20 @@ STDOUT. When enabled, this also sets the :code:`KITTY_STDIO_FORWARDED=3`
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
View File

@@ -12,13 +12,13 @@ from kitty.options.utils import (
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 @@ class Parser:
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': {},

View File

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

View File

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

View File

@@ -399,6 +399,7 @@ option_names = ( # {{{
'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 @@ defaults.env = {}
defaults.exe_search_path = {}
defaults.font_features = {}
defaults.kitten_alias = {}
defaults.menu_map = {}
defaults.modify_font = {}
defaults.narrow_symbols = {}
defaults.remote_control_password = {}

View File

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