diff --git a/kittens/themes/collection.py b/kittens/themes/collection.py index bb2c84f13..f70ac47f4 100644 --- a/kittens/themes/collection.py +++ b/kittens/themes/collection.py @@ -12,12 +12,15 @@ import signal import tempfile import zipfile from contextlib import suppress -from typing import Any, Callable, Dict, Iterator, Match, Optional, Tuple, Union +from typing import ( + Any, Callable, Dict, Iterable, Iterator, List, Match, Optional, Tuple, + Union +) from urllib.error import HTTPError from urllib.request import Request, urlopen from kitty.config import atomic_save, parse_config -from kitty.constants import cache_dir, config_dir +from kitty.constants import cache_dir, config_dir, is_macos from kitty.options.types import Options as KittyOptions from kitty.rgb import Color @@ -27,6 +30,28 @@ MARK_BEFORE = '\033[33m' MARK_AFTER = '\033[39m' +def is_kitty_gui(cmd: List[str]) -> bool: + if not cmd: + return False + if os.path.basename(cmd[0]) != 'kitty': + return False + if len(cmd) == 1: + return True + if '+' in cmd or '@' in cmd or cmd[1].startswith('+') or cmd[1].startswith('@'): + return False + return True + + +def get_all_processes() -> Iterable[int]: + if is_macos: + from kitty.fast_data_types import get_all_processes as f + yield from f() + else: + for c in os.listdir('/proc'): + if c.isdigit(): + yield int(c) + + def patch_conf(raw: str) -> str: addition = '# BEGIN_KITTY_THEME\ninclude current-theme.conf\n# END_KITTY_THEME' nraw, num = re.subn(r'^# BEGIN_KITTY_THEME.+?# END_KITTY_THEME', addition, raw, flags=re.MULTILINE | re.DOTALL) @@ -254,7 +279,7 @@ class Theme: def save_in_dir(self, dirpath: str) -> None: atomic_save(self.raw.encode('utf-8'), os.path.join(dirpath, f'{self.name}.conf')) - def save_in_conf(self, confdir: str) -> None: + def save_in_conf(self, confdir: str, reload_in: str) -> None: atomic_save(self.raw.encode('utf-8'), os.path.join(confdir, 'current-theme.conf')) confpath = os.path.join(confdir, 'kitty.conf') try: @@ -267,8 +292,18 @@ class Theme: with open(confpath + '.bak', 'w') as f: f.write(raw) atomic_save(nraw.encode('utf-8'), confpath) - if 'KITTY_PID' in os.environ: - os.kill(int(os.environ['KITTY_PID']), signal.SIGUSR1) + if reload_in == 'parent': + if 'KITTY_PID' in os.environ: + os.kill(int(os.environ['KITTY_PID']), signal.SIGUSR1) + elif reload_in == 'all': + from kitty.child import cmdline_of_process # type: ignore + for pid in get_all_processes(): + try: + cmd = cmdline_of_process(pid) + except Exception: + continue + if cmd and is_kitty_gui(cmd): + os.kill(pid, signal.SIGUSR1) class Themes: diff --git a/kittens/themes/main.py b/kittens/themes/main.py index 20a7183f2..8c6ae4ca5 100644 --- a/kittens/themes/main.py +++ b/kittens/themes/main.py @@ -466,7 +466,7 @@ class ThemesHandler(Handler): self.quit_loop(0) return if key_event.matches('m'): - self.themes_list.current_theme.save_in_conf(config_dir) + self.themes_list.current_theme.save_in_conf(config_dir, self.cli_opts.reload_in) self.update_recent() self.quit_loop(0) return @@ -520,6 +520,20 @@ for new themes, instead raising an error if a local copy of the themes is not available. +--reload-in +default=parent +choices=none,parent,all +By default, this kitten will signal only the parent kitty instance it is +running in to reload its config, after making changes. Use this option +to instead either not reload the config at all or in all running +kitty instances. + + +--dump-theme +type=bool-set +default=false +When running non-interactively, dump the specified theme to STDOUT +instead of changing kitty.conf. '''.format @@ -536,7 +550,10 @@ def non_interactive(cli_opts: ThemesCLIOptions, theme_name: str) -> None: theme = themes[theme_name] except KeyError: raise SystemExit(f'No theme named: {theme_name}') - theme.save_in_conf(config_dir) + if cli_opts.dump_theme: + print(theme.raw) + return + theme.save_in_conf(config_dir, cli_opts.reload_in) def main(args: List[str]) -> None: @@ -548,7 +565,7 @@ def main(args: List[str]) -> None: input(_('Press Enter to quit')) return None if len(items) > 1: - raise SystemExit('At most one theme name must be specified') + items = [' '.join(items[1:])] if len(items) == 1: return non_interactive(cli_opts, items[0]) diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 5dcb3016c..1274d93d4 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1213,3 +1213,7 @@ class OSWindowSize(TypedDict): def get_os_window_size(os_window_id: int) -> Optional[OSWindowSize]: pass + + +def get_all_processes() -> Tuple[int, ...]: + pass diff --git a/kitty/macos_process_info.c b/kitty/macos_process_info.c index 341556201..5d2a9973e 100644 --- a/kitty/macos_process_info.c +++ b/kitty/macos_process_info.c @@ -34,6 +34,25 @@ get_argmax() { return 0; } +static PyObject* +get_all_processes(PyObject *self UNUSED, PyObject *args UNUSED) { + pid_t num = proc_listallpids(NULL, 0); + if (num <= 0) return PyTuple_New(0); + size_t sz = sizeof(pid_t) * num * 2; + pid_t *buf = malloc(sz); + if (!buf) return PyErr_NoMemory(); + num = proc_listallpids(buf, sz); + if (num <= 0) { free(buf); return PyTuple_New(0); } + PyObject *ans = PyTuple_New(num); + if (!ans) { free(buf); return NULL; } + for (pid_t i = 0; i < num; i++) { + long long pid = buf[i]; + PyObject *t = PyLong_FromLongLong(pid); + if (!t) { free(buf); Py_CLEAR(ans); return NULL; } + PyTuple_SET_ITEM(ans, i, t); + } + return ans; +} static PyObject* cmdline_of_process(PyObject *self UNUSED, PyObject *pid_) { @@ -255,6 +274,7 @@ static PyMethodDef module_methods[] = { {"cwd_of_process", (PyCFunction)cwd_of_process, METH_O, ""}, {"cmdline_of_process", (PyCFunction)cmdline_of_process, METH_O, ""}, {"environ_of_process", (PyCFunction)environ_of_process, METH_O, ""}, + {"get_all_processes", (PyCFunction)get_all_processes, METH_NOARGS, ""}, {NULL, NULL, 0, NULL} /* Sentinel */ };