From b0ae88ada9de1d1373c76a3522819aec88455887 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Apr 2025 22:04:56 +0530 Subject: [PATCH] Wire up the codegen C cli parser --- kittens/panel/main.py | 173 +----------------------------- kitty/cli.py | 38 +++++-- kitty/launcher/cli-parser.h | 17 +-- kitty/launcher/launcher.h | 4 +- kitty/launcher/main.c | 182 +++++++++++++++----------------- kitty/main.py | 3 +- kitty/simple_cli_definitions.py | 180 +++++++++++++++++++++++++++++++ kitty_tests/options.py | 6 +- 8 files changed, 314 insertions(+), 289 deletions(-) diff --git a/kittens/panel/main.py b/kittens/panel/main.py index e3d723308..32b771a6d 100644 --- a/kittens/panel/main.py +++ b/kittens/panel/main.py @@ -9,7 +9,7 @@ from kitty.cli import parse_args from kitty.cli_stub import PanelCLIOptions -from kitty.constants import appname, is_macos, is_wayland, kitten_exe +from kitty.constants import is_macos, is_wayland, kitten_exe from kitty.fast_data_types import ( GLFW_EDGE_BOTTOM, GLFW_EDGE_CENTER, @@ -29,7 +29,7 @@ toggle_os_window_visibility, ) from kitty.os_window_size import WindowSizeData, edge_spacing -from kitty.simple_cli_definitions import listen_on_defn +from kitty.simple_cli_definitions import panel_kitten_options_spec from kitty.types import LayerShellConfig from kitty.typing_compat import BossType, EdgeLiteral from kitty.utils import log_error @@ -41,171 +41,6 @@ default_quake_cmdline = f'{quake} --exclusive-zone=0 --override-exclusive-zone --detach' default_macos_quake_cmdline = f'{quake}' -OPTIONS = r''' ---lines -default=1 -The number of lines shown in the panel. Ignored for background, centered, and vertical panels. -If it has the suffix :code:`px` then it sets the height of the panel in pixels instead of lines. - - ---columns -default=1 -The number of columns shown in the panel. Ignored for background, centered, and horizontal panels. -If it has the suffix :code:`px` then it sets the width of the panel in pixels instead of columns. - - ---margin-top -type=int -default=0 -Set the top margin for the panel, in pixels. Has no effect for bottom edge panels. -Only works on macOS and Wayland compositors that supports the wlr layer shell protocol. - - ---margin-left -type=int -default=0 -Set the left margin for the panel, in pixels. Has no effect for right edge panels. -Only works on macOS and Wayland compositors that supports the wlr layer shell protocol. - - ---margin-bottom -type=int -default=0 -Set the bottom margin for the panel, in pixels. Has no effect for top edge panels. -Only works on macOS and Wayland compositors that supports the wlr layer shell protocol. - - ---margin-right -type=int -default=0 -Set the right margin for the panel, in pixels. Has no effect for left edge panels. -Only works on macOS and Wayland compositors that supports the wlr layer shell protocol. - - ---edge -choices=top,bottom,left,right,background,center,none -default=top -Which edge of the screen to place the panel on. Note that some window managers -(such as i3) do not support placing docked windows on the left and right edges. -The value :code:`background` means make the panel the "desktop wallpaper". This -is not supported on X11 and note that when using sway if you set -a background in your sway config it will cover the background drawn using this -kitten. -Additionally, there are two more values: :code:`center` and :code:`none`. -The value :code:`center` anchors the panel to all sides and covers the entire -display (on macOS the part of the display not covered by titlebar and dock). -The panel can be shrunk and placed using the margin parameters. -The value :code:`none` anchors the panel to the top left corner and should be -placed and using the margin parameters. It's size is set bye :option:`--lines` -and :option:`--columns`. - - ---layer -choices=background,bottom,top,overlay -default=bottom -On a Wayland compositor that supports the wlr layer shell protocol, specifies the layer -on which the panel should be drawn. This parameter is ignored and set to -:code:`background` if :option:`--edge` is set to :code:`background`. On macOS, maps -these to appropriate NSWindow *levels*. - - ---config -c -type=list -Path to config file to use for kitty when drawing the panel. - - ---override -o -type=list -Override individual kitty configuration options, can be specified multiple times. -Syntax: :italic:`name=value`. For example: :option:`kitty +kitten panel -o` font_size=20 - - ---output-name -On Wayland, the panel can only be displayed on a single monitor (output) at a time. This allows -you to specify which output is used, by name. If not specified the compositor will choose an -output automatically, typically the last output the user interacted with or the primary monitor. - - ---class --app-id -dest=cls -default={appname}-panel -condition=not is_macos -Set the class part of the :italic:`WM_CLASS` window property. On Wayland, it sets the app id. - - ---name -condition=not is_macos -Set the name part of the :italic:`WM_CLASS` property (defaults to using the value from :option:`{appname} --class`) - - ---focus-policy -choices=not-allowed,exclusive,on-demand -default=not-allowed -On a Wayland compositor that supports the wlr layer shell protocol, specify the focus policy for keyboard -interactivity with the panel. Please refer to the wlr layer shell protocol documentation for more details. -On macOS, :code:`exclusive` and :code:`on-demand` are currently the same. Ignored on X11. - - ---exclusive-zone -type=int -default=-1 -On a Wayland compositor that supports the wlr layer shell protocol, request a given exclusive zone for the panel. -Please refer to the wlr layer shell documentation for more details on the meaning of exclusive and its value. -If :option:`--edge` is set to anything other than :code:`center` or :code:`none`, this flag will not have any -effect unless the flag :option:`--override-exclusive-zone` is also set. -If :option:`--edge` is set to :code:`background`, this option has no effect. -Ignored on X11 and macOS. - - ---override-exclusive-zone -type=bool-set -On a Wayland compositor that supports the wlr layer shell protocol, override the default exclusive zone. -This has effect only if :option:`--edge` is set to :code:`top`, :code:`left`, :code:`bottom` or :code:`right`. -Ignored on X11 and macOS. - - ---single-instance -1 -type=bool-set -If specified only a single instance of the panel will run. New -invocations will instead create a new top-level window in the existing -panel instance. - - ---instance-group -Used in combination with the :option:`--single-instance` option. All -panel invocations with the same :option:`--instance-group` will result -in new panels being created in the first panel instance within that group. - - -{listen_on_defn} - - ---toggle-visibility -type=bool-set -When set and using :option:`--single-instance` will toggle the visibility of the -existing panel rather than creating a new one. - - ---start-as-hidden -type=bool-set -Start in hidden mode, useful with :option:`--toggle-visibility`. - - ---detach -type=bool-set -Detach from the controlling terminal, if any, running in an independent child process, -the parent process exits immediately. - - ---detached-log -Path to a log file to store STDOUT/STDERR when using :option:`--detach` - - ---debug-rendering -type=bool-set -For internal debugging use. -'''.format(appname=appname, listen_on_defn=listen_on_defn).format - args = PanelCLIOptions() help_text = 'Use a command line program to draw a GPU accelerated panel on your desktop' @@ -213,7 +48,7 @@ def parse_panel_args(args: list[str]) -> tuple[PanelCLIOptions, list[str]]: - return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten panel', result_class=PanelCLIOptions) + return parse_args(args, panel_kitten_options_spec, usage, help_text, 'kitty +kitten panel', result_class=PanelCLIOptions) Strut = tuple[int, int, int, int, int, int, int, int, int, int, int, int] @@ -396,6 +231,6 @@ def main(sys_args: list[str]) -> None: elif __name__ == '__doc__': cd: dict = sys.cli_docs # type: ignore cd['usage'] = usage - cd['options'] = OPTIONS + cd['options'] = panel_kitten_options_spec cd['help_text'] = help_text cd['short_desc'] = help_text diff --git a/kitty/cli.py b/kitty/cli.py index e19cbf091..c99a7be58 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -580,6 +580,19 @@ def handle_version(self) -> NoReturn: raise SystemExit(0) +PreparsedCLIFlags = tuple[dict[str, tuple[Any, bool]], list[str]] + + +def apply_preparsed_cli_flags(preparsed_from_c: PreparsedCLIFlags, ans: Any, create_oc: Callable[[], Options]) -> list[str]: + for key, (val, is_seen) in preparsed_from_c[0].items(): + if key == 'help' and is_seen and val: + create_oc().handle_help() + elif key == 'version' and is_seen and val: + create_oc().handle_version() + setattr(ans, key, val) + return preparsed_from_c[1] + + def parse_cmdline(oc: Options, disabled: OptionSpecSeq, ans: Any, args: list[str] | None = None) -> list[str]: names_map = oc.names_map.copy() values_map = oc.values_map.copy() @@ -590,16 +603,11 @@ def parse_cmdline(oc: Options, disabled: OptionSpecSeq, ans: Any, args: list[str names_map['version'] = {'type': 'bool-set', 'aliases': ('--version', '-v')} # type: ignore values_map['version'] = False try: - vals, leftover_args = parse_cli_from_spec(sys.argv[1:] if args is None else args, names_map, values_map) + preparsed = parse_cli_from_spec(sys.argv[1:] if args is None else args, names_map, values_map) except Exception as e: raise SystemExit(str(e)) + leftover_args = apply_preparsed_cli_flags(preparsed, ans, lambda: oc) - for key, (val, is_seen) in vals.items(): - if key == 'help' and is_seen and val: - oc.handle_help() - elif key == 'version' and is_seen and val: - oc.handle_version() - setattr(ans, key, val) for opt in disabled: if not isinstance(opt, str): setattr(ans, opt['dest'], defval_for_opt(opt)) @@ -634,14 +642,24 @@ def parse_args( message: str | None = None, appname: str | None = None, result_class: type[T] | None = None, + preparsed_from_c: PreparsedCLIFlags | None = None, ) -> tuple[T, list[str]]: - options = parse_option_spec(ospec()) - seq, disabled = options - oc = Options(seq, usage, message, appname) if result_class is not None: ans = result_class() else: ans = cast(T, CLIOptions()) + + def create_oc() -> Options: + options = parse_option_spec(ospec()) + seq, disabled = options + return Options(seq, usage, message, appname) + + if preparsed_from_c: + return ans, apply_preparsed_cli_flags(preparsed_from_c, ans, create_oc) + + options = parse_option_spec(ospec()) + seq, disabled = options + oc = Options(seq, usage, message, appname) return ans, parse_cmdline(oc, disabled, ans, args=args) diff --git a/kitty/launcher/cli-parser.h b/kitty/launcher/cli-parser.h index a88215c89..6b22194bb 100644 --- a/kitty/launcher/cli-parser.h +++ b/kitty/launcher/cli-parser.h @@ -231,12 +231,12 @@ dealloc_cli_spec(void *v) { #define RAII_CLISpec(name) __attribute__((cleanup(dealloc_cli_spec))) CLISpec name = {0}; alloc_cli_spec(&name) static bool -parse_cli_loop(CLISpec *spec, bool save_original_argv, int argc, char **argv) { // argv must contain arg1 and beyond +parse_cli_loop(CLISpec *spec, bool save_original_argv, int argc, char **argv) { enum { NORMAL, EXPECTING_ARG } state = NORMAL; spec->argc = 0; spec->argv = NULL; spec->errmsg = NULL; spec->original_argc = argc; spec->original_argv = NULL; if (save_original_argv) { char **copy = alloc_for_cli(spec, sizeof(char*) * (argc + 1)); - if (!spec->original_argv) OOM; + if (!copy) OOM; copy[argc] = NULL; for (int i = 0; i < argc; i++) { size_t len = strlen(argv[i]); @@ -249,7 +249,7 @@ parse_cli_loop(CLISpec *spec, bool save_original_argv, int argc, char **argv) { } char flag[3] = {'-', 0, 0}; const char *current_option = NULL; - for (int i = 0; i < argc; i++) { + for (int i = 1; i < argc; i++) { char *arg = argv[i]; switch(state) { case NORMAL: { @@ -383,16 +383,17 @@ parse_cli_from_python_spec(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "O!O!O!", &PyList_Type, &pyargs, &PyDict_Type, &names_map, &PyDict_Type, &defval_map)) return NULL; int argc = PyList_GET_SIZE(pyargs); RAII_CLISpec(spec); - char **argv = alloc_for_cli(&spec, sizeof(char*) * (argc + 1)); + char **argv = alloc_for_cli(&spec, sizeof(char*) * (argc + 2)); if (!argv) return PyErr_NoMemory(); + argv[0] = "parse_cli_from_python_spec"; for (int i = 0; i < argc; i++) { Py_ssize_t sz; const char *src = PyUnicode_AsUTF8AndSize(PyList_GET_ITEM(pyargs, i), &sz); - argv[i] = alloc_for_cli(&spec, sz); - if (!argv[i]) return PyErr_NoMemory(); - memcpy(argv[i], src, sz); + argv[i + 1] = alloc_for_cli(&spec, sz); + if (!argv[i + 1]) return PyErr_NoMemory(); + memcpy(argv[i + 1], src, sz); } - argv[argc] = 0; + argv[++argc] = 0; PyObject *key = NULL, *opt = NULL; Py_ssize_t pos = 0; while (PyDict_Next(names_map, &pos, &key, &opt)) { diff --git a/kitty/launcher/launcher.h b/kitty/launcher/launcher.h index a9cca92bc..70570ea1b 100644 --- a/kitty/launcher/launcher.h +++ b/kitty/launcher/launcher.h @@ -11,8 +11,8 @@ #include typedef struct CLIOptions { - const char *session, *instance_group, *detached_log; - bool single_instance, version_requested, wait_for_single_instance_window_close, detach; + const char *session, *instance_group; + bool wait_for_single_instance_window_close; int open_url_count; char **open_urls; } CLIOptions; diff --git a/kitty/launcher/main.c b/kitty/launcher/main.c index 07ea97b98..1075cc5a0 100644 --- a/kitty/launcher/main.c +++ b/kitty/launcher/main.c @@ -13,11 +13,14 @@ #include #include #include +#include #else #include #endif #include "launcher.h" #include "utils.h" +#define FOR_LAUNCHER +#include "cli-parser-data_generated.h" #ifndef KITTY_LIB_PATH #define KITTY_LIB_PATH "../.." @@ -42,8 +45,7 @@ safe_realpath(const char* src, char *buf, size_t buf_sz) { typedef struct { const char *exe, *exe_dir, *lc_ctype, *lib_dir, *config_dir; - char **argv; - int argc; + CLISpec *cli_spec; bool launched_by_launch_services, is_quick_access_terminal; } RunData; @@ -83,6 +85,12 @@ set_kitty_run_data(RunData *run_data, bool from_source, wchar_t *extensions_dir) if (!cdir) { PyErr_Print(); return false; } S(config_dir, cdir); } + PyObject *cli_flags = cli_parse_result_as_python(run_data->cli_spec); + if (!cli_flags) { + if (PyErr_Occurred()) PyErr_Print(); + return false; + } + S(cli_flags, cli_flags); #undef S int ret = PySys_SetObject("kitty_run_data", ans); @@ -120,7 +128,7 @@ run_embedded(RunData *run_data) { wchar_t python_home[PATH_MAX]; canonicalize_path_wide(python_home_full, python_home, PATH_MAX); bypy_initialize_interpreter( - L"kitty", python_home, L"kitty_main", extensions_dir, run_data->argc, run_data->argv); + L"kitty", python_home, L"kitty_main", extensions_dir, run_data->cli_spec->original_argc, run_data->cli_spec->original_argv); if (!set_kitty_run_data(run_data, false, extensions_dir)) return 1; set_sys_bool("frozen", true); return bypy_run_interpreter(); @@ -148,7 +156,7 @@ run_embedded(RunData *run_data) { PyConfig_InitPythonConfig(&config); config.parse_argv = 0; config.optimization_level = 2; - status = PyConfig_SetBytesArgv(&config, run_data->argc, run_data->argv); + status = PyConfig_SetBytesArgv(&config, run_data->cli_spec->original_argc, run_data->cli_spec->original_argv); if (PyStatus_Exception(status)) goto fail; status = PyConfig_SetBytesString(&config, &config.executable, run_data->exe); if (PyStatus_Exception(status)) goto fail; @@ -307,87 +315,53 @@ exec_kitten(int argc, char *argv[], char *exe_dir) { } static bool -is_boolean_flag(const char *x) { - static const char *all_boolean_options = ""; - char buf[128]; - safe_snprintf(buf, sizeof(buf), " %s ", x); - return strstr(all_boolean_options, buf) != NULL; +parse_and_check_kitty_cli(CLISpec *cli_spec, int argc, char **argv) { + parse_cli_for_kitty(cli_spec, argc, argv); + if (cli_spec->errmsg) { + fprintf(stderr, "%s\n", cli_spec->errmsg); +#ifdef __APPLE__ + os_log_error(OS_LOG_DEFAULT, "%{public}s", cli_spec->errmsg); +#endif + return false; + } + return true; } -static void -handle_fast_commandline(int argc, char *argv[], const char *instance_group_prefix) { - char current_option_expecting_argument[128] = {0}; - CLIOptions opts = {0}; - int first_arg = 1; - if (argc > 1 && strcmp(argv[1], "+open") == 0) { - first_arg = 2; - } else if (argc > 2 && strcmp(argv[1], "+") == 0 && strcmp(argv[2], "open") == 0) { - first_arg = 3; - } - for (int i = first_arg; i < argc; i++) { - const char *arg = argv[i]; - if (current_option_expecting_argument[0]) { -handle_option_value: - if (strcmp(current_option_expecting_argument, "session") == 0) { - opts.session = arg; - } else if (strcmp(current_option_expecting_argument, "instance-group") == 0) { - opts.instance_group = arg; - } else if (strcmp(current_option_expecting_argument, "detached-log") == 0) { - opts.detached_log = arg; - } - current_option_expecting_argument[0] = 0; - } else { - if (!arg || !arg[0] || !arg[1] || arg[0] != '-' || strcmp(arg, "--") == 0) { - if (first_arg > 1) { - opts.open_urls = argv + i; - opts.open_url_count = argc - i; - } - break; - } - if (arg[1] == '-') { // long opt - const char *equal = strchr(arg, '='); - const char *q = arg + 2; - if (equal == NULL) { - if (strcmp(q, "version") == 0) { - opts.version_requested = true; - } else if (strcmp(q, "single-instance") == 0) { - opts.single_instance = true; - } else if (strcmp(q, "wait-for-single-instance-window-close") == 0) { - opts.wait_for_single_instance_window_close = true; - } else if (strcmp(q, "detach") == 0) { - opts.detach = true; - } else if (strcmp(q, "help") == 0) { - return; - } else if (!is_boolean_flag(arg+2)) { - strncpy(current_option_expecting_argument, q, sizeof(current_option_expecting_argument)-1); - } - } else { - memcpy(current_option_expecting_argument, q, equal - q); - current_option_expecting_argument[equal - q] = 0; - arg = equal + 1; - goto handle_option_value; - } - } else { - char buf[2] = {0}; - current_option_expecting_argument[0] = 0; - for (int i = 1; arg[i] != 0; i++) { - switch(arg[i]) { - case '=': - arg = arg + i + 1; - goto handle_option_value; - case 'v': opts.version_requested = true; break; - case '1': opts.single_instance = true; break; - case 'h': return; - default: - buf[0] = arg[i]; buf[1] = 0; - if (!is_boolean_flag(buf)) { current_option_expecting_argument[0] = arg[i]; current_option_expecting_argument[1] = 0; } - } - } - } - } +static bool +parse_and_check_panel_kitten_cli(CLISpec *cli_spec, int argc, char **argv) { + parse_cli_for_panel_kitten(cli_spec, argc, argv); + if (cli_spec->errmsg) { + fprintf(stderr, "%s\n", cli_spec->errmsg); +#ifdef __APPLE__ + os_log_error(OS_LOG_DEFAULT, "%{public}s", cli_spec->errmsg); +#endif + return false; } + return true; +} - if (opts.version_requested) { + +static void +handle_fast_commandline(CLISpec *cli_spec, const char *instance_group_prefix, int offset_for_panel_kitten) { + CLIOptions opts = {0}; + RAII_CLISpec(subcommand_cli_spec); + if (offset_for_panel_kitten < 0) { + // Look for +open + int offset = 0; + if (cli_spec->original_argc > 1 && strcmp("+open", cli_spec->original_argv[1]) == 0) offset = 1; + else if (cli_spec->original_argc > 2 && strcmp("+", cli_spec->original_argv[1]) == 0 && strcmp("open", cli_spec->original_argv[2]) == 0) offset = 2; + if (offset) { + if (!parse_and_check_kitty_cli(&subcommand_cli_spec, cli_spec->original_argc - offset, cli_spec->original_argv + offset)) exit(1); + cli_spec = &subcommand_cli_spec; + opts.open_url_count = cli_spec->argc; + opts.open_urls = cli_spec->argv; + } + } else if (offset_for_panel_kitten > 0) { + parse_and_check_panel_kitten_cli(&subcommand_cli_spec, cli_spec->original_argc - offset_for_panel_kitten, cli_spec->original_argv + offset_for_panel_kitten); + cli_spec = &subcommand_cli_spec; + } + if (get_bool_cli_val(cli_spec, "help")) return; + if (get_bool_cli_val(cli_spec, "version")) { if (isatty(STDOUT_FILENO)) { printf("\x1b[3mkitty\x1b[23m \x1b[32m%s\x1b[39m created by \x1b[1;34mKovid Goyal\x1b[22;39m\n", KITTY_VERSION); } else { @@ -395,21 +369,25 @@ handle_option_value: } exit(0); } - if (opts.detach) { + opts.session = get_string_cli_val(cli_spec, "session"); + if (get_bool_cli_val(cli_spec, "detach")) { #define reopen_or_fail(path, mode, which) { errno = 0; if (freopen(path, mode, which) == NULL) { int s = errno; fprintf(stderr, "Failed to redirect %s to %s with error: ", #which, path); errno = s; perror(NULL); exit(1); } } - if (!(opts.session && ((opts.session[0] == '-' && opts.session[1] == 0) || strcmp(opts.session, "/dev/stdin") == 0)) - ) reopen_or_fail("/dev/null", "rb", stdin); - if (!opts.detached_log || !opts.detached_log[0]) opts.detached_log = "/dev/null"; - reopen_or_fail(opts.detached_log, "ab", stdout); - reopen_or_fail(opts.detached_log, "ab", stderr); + if (!(opts.session && ((opts.session[0] == '-' && opts.session[1] == 0) || strcmp(opts.session, "/dev/stdin") == 0))) + reopen_or_fail("/dev/null", "rb", stdin); + const char *detached_log = get_string_cli_val(cli_spec, "detached_log"); + if (!detached_log || !detached_log[0]) detached_log = "/dev/null"; + reopen_or_fail(detached_log, "ab", stdout); + reopen_or_fail(detached_log, "ab", stderr); #undef reopen_or_fail if (fork() != 0) exit(0); setsid(); } unsetenv("KITTY_SI_DATA"); - if (opts.single_instance) { + if (get_bool_cli_val(cli_spec, "single_instance")) { char igbuf[256]; + opts.wait_for_single_instance_window_close = get_bool_cli_val(cli_spec, "wait_for_single_instance_window_close"); if (instance_group_prefix && instance_group_prefix[0]) { + opts.instance_group = get_string_cli_val(cli_spec, "instance_group"); if (opts.instance_group && opts.instance_group[0]) { safe_snprintf(igbuf, sizeof(igbuf), "%s-%s", instance_group_prefix, opts.instance_group ? opts.instance_group : ""); opts.instance_group = igbuf; @@ -417,24 +395,25 @@ handle_option_value: opts.instance_group = instance_group_prefix; } } - single_instance_main(argc, argv, &opts); + single_instance_main(cli_spec->original_argc, cli_spec->original_argv, &opts); } } static bool -delegate_to_kitten_if_possible(int argc, char *argv[], char* exe_dir) { +delegate_to_kitten_if_possible(CLISpec *cli_spec, char* exe_dir) { + int argc = cli_spec->original_argc; char **argv = cli_spec->original_argv; if (argc > 1 && argv[1][0] == '@') exec_kitten(argc, argv, exe_dir); if (argc > 2 && strcmp(argv[1], "+kitten") == 0) { if (is_wrapped_kitten(argv[2])) exec_kitten(argc - 1, argv + 1, exe_dir); if (strcmp(argv[2], "panel") == 0) { - handle_fast_commandline(argc - 2, argv + 2, "panel"); + handle_fast_commandline(cli_spec, "panel", 2); return true; } } if (argc > 3 && strcmp(argv[1], "+") == 0 && strcmp(argv[2], "kitten") == 0) { if (is_wrapped_kitten(argv[3])) exec_kitten(argc - 2, argv + 2, exe_dir); - if (strcmp(argv[3], "panel") == 0) { - handle_fast_commandline(argc - 3, argv + 3, "panel"); + if (strcmp(argv[2], "panel") == 0) { + handle_fast_commandline(cli_spec, "panel", 3); return true; } } @@ -483,7 +462,19 @@ int main(int argc_, char *argv_[], char* envp[]) { (void)endswith; #endif (void)read_full_file; - if (!delegate_to_kitten_if_possible(argva.count, argva.argv, exe_dir)) handle_fast_commandline(argva.count, argva.argv, NULL); + RAII_CLISpec(cli_spec); + bool ok = parse_and_check_kitty_cli(&cli_spec, argva.count, argva.argv); + free_argv_array(&argva); + if (!ok) return 1; + if (cli_spec.errmsg) { + fprintf(stderr, "%s\n", cli_spec.errmsg); +#ifdef __APPLE__ + os_log_error(OS_LOG_DEFAULT, "%{public}s", cli_spec.errmsg); +#endif + return 1; + } + + if (!delegate_to_kitten_if_possible(&cli_spec, exe_dir)) handle_fast_commandline(&cli_spec, NULL, -1); int ret=0; char lib[PATH_MAX+1] = {0}; if (KITTY_LIB_PATH[0] == '/') { @@ -492,11 +483,10 @@ int main(int argc_, char *argv_[], char* envp[]) { safe_snprintf(lib, PATH_MAX, "%s/%s", exe_dir, KITTY_LIB_PATH); } RunData run_data = { - .exe = exe, .exe_dir = exe_dir, .lib_dir = lib, .argc = argva.count, .argv = argva.argv, .lc_ctype = lc_ctype, + .exe = exe, .exe_dir = exe_dir, .lib_dir = lib, .cli_spec = &cli_spec, .lc_ctype = lc_ctype, .launched_by_launch_services=launched_by_launch_services, .config_dir = config_dir, .is_quick_access_terminal=is_quick_access_terminal, }; ret = run_embedded(&run_data); - free_argv_array(&argva); single_instance_main(-1, NULL, NULL); Py_FinalizeEx(); return ret; diff --git a/kitty/main.py b/kitty/main.py index 1a28c5b9a..a27869c03 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -472,7 +472,8 @@ def kitty_main() -> None: '\n\nAll the normal kitty options can be used.') else: usage = msg = appname = None - cli_opts, rest = parse_args(args=args, result_class=CLIOptions, usage=usage, message=msg, appname=appname) + cli_flags = getattr(sys, 'kitty_run_data', {}).get('cli_flags', None) + cli_opts, rest = parse_args(args=args, result_class=CLIOptions, usage=usage, message=msg, appname=appname, preparsed_from_c=cli_flags) if getattr(sys, 'cmdline_args_for_open', False): setattr(sys, 'cmdline_args_for_open', rest) cli_opts.args = [] diff --git a/kitty/simple_cli_definitions.py b/kitty/simple_cli_definitions.py index a88ae9236..195bb560d 100644 --- a/kitty/simple_cli_definitions.py +++ b/kitty/simple_cli_definitions.py @@ -302,8 +302,12 @@ def generate_c_parsers() -> Iterator[str]: yield '#include "cli-parser.h"' yield from generate_c_parser_for('kitty', kitty_options_spec()) yield '' + yield '' + yield from generate_c_parser_for('panel_kitten', panel_kitten_options_spec()) + yield '' +# kitty CLI spec {{{ listen_on_defn = f'''\ --listen-on completion=type:special group:complete_kitty_listen_on @@ -516,3 +520,179 @@ def kitty_options_spec() -> str: )) ans: str = getattr(kitty_options_spec, 'ans') return ans +# }}} + + +# panel CLI spec {{{ +def panel_kitten_options_spec() -> str: + if not hasattr(panel_kitten_options_spec, 'ans'): + OPTIONS = r''' +--lines +default=1 +The number of lines shown in the panel. Ignored for background, centered, and vertical panels. +If it has the suffix :code:`px` then it sets the height of the panel in pixels instead of lines. + + +--columns +default=1 +The number of columns shown in the panel. Ignored for background, centered, and horizontal panels. +If it has the suffix :code:`px` then it sets the width of the panel in pixels instead of columns. + + +--margin-top +type=int +default=0 +Set the top margin for the panel, in pixels. Has no effect for bottom edge panels. +Only works on macOS and Wayland compositors that supports the wlr layer shell protocol. + + +--margin-left +type=int +default=0 +Set the left margin for the panel, in pixels. Has no effect for right edge panels. +Only works on macOS and Wayland compositors that supports the wlr layer shell protocol. + + +--margin-bottom +type=int +default=0 +Set the bottom margin for the panel, in pixels. Has no effect for top edge panels. +Only works on macOS and Wayland compositors that supports the wlr layer shell protocol. + + +--margin-right +type=int +default=0 +Set the right margin for the panel, in pixels. Has no effect for left edge panels. +Only works on macOS and Wayland compositors that supports the wlr layer shell protocol. + + +--edge +choices=top,bottom,left,right,background,center,none +default=top +Which edge of the screen to place the panel on. Note that some window managers +(such as i3) do not support placing docked windows on the left and right edges. +The value :code:`background` means make the panel the "desktop wallpaper". This +is not supported on X11 and note that when using sway if you set +a background in your sway config it will cover the background drawn using this +kitten. +Additionally, there are two more values: :code:`center` and :code:`none`. +The value :code:`center` anchors the panel to all sides and covers the entire +display (on macOS the part of the display not covered by titlebar and dock). +The panel can be shrunk and placed using the margin parameters. +The value :code:`none` anchors the panel to the top left corner and should be +placed and using the margin parameters. It's size is set bye :option:`--lines` +and :option:`--columns`. + + +--layer +choices=background,bottom,top,overlay +default=bottom +On a Wayland compositor that supports the wlr layer shell protocol, specifies the layer +on which the panel should be drawn. This parameter is ignored and set to +:code:`background` if :option:`--edge` is set to :code:`background`. On macOS, maps +these to appropriate NSWindow *levels*. + + +--config -c +type=list +Path to config file to use for kitty when drawing the panel. + + +--override -o +type=list +Override individual kitty configuration options, can be specified multiple times. +Syntax: :italic:`name=value`. For example: :option:`kitty +kitten panel -o` font_size=20 + + +--output-name +On Wayland, the panel can only be displayed on a single monitor (output) at a time. This allows +you to specify which output is used, by name. If not specified the compositor will choose an +output automatically, typically the last output the user interacted with or the primary monitor. + + +--class --app-id +dest=cls +default={appname}-panel +condition=not is_macos +Set the class part of the :italic:`WM_CLASS` window property. On Wayland, it sets the app id. + + +--name +condition=not is_macos +Set the name part of the :italic:`WM_CLASS` property (defaults to using the value from :option:`{appname} --class`) + + +--focus-policy +choices=not-allowed,exclusive,on-demand +default=not-allowed +On a Wayland compositor that supports the wlr layer shell protocol, specify the focus policy for keyboard +interactivity with the panel. Please refer to the wlr layer shell protocol documentation for more details. +On macOS, :code:`exclusive` and :code:`on-demand` are currently the same. Ignored on X11. + + +--exclusive-zone +type=int +default=-1 +On a Wayland compositor that supports the wlr layer shell protocol, request a given exclusive zone for the panel. +Please refer to the wlr layer shell documentation for more details on the meaning of exclusive and its value. +If :option:`--edge` is set to anything other than :code:`center` or :code:`none`, this flag will not have any +effect unless the flag :option:`--override-exclusive-zone` is also set. +If :option:`--edge` is set to :code:`background`, this option has no effect. +Ignored on X11 and macOS. + + +--override-exclusive-zone +type=bool-set +On a Wayland compositor that supports the wlr layer shell protocol, override the default exclusive zone. +This has effect only if :option:`--edge` is set to :code:`top`, :code:`left`, :code:`bottom` or :code:`right`. +Ignored on X11 and macOS. + + +--single-instance -1 +type=bool-set +If specified only a single instance of the panel will run. New +invocations will instead create a new top-level window in the existing +panel instance. + + +--instance-group +Used in combination with the :option:`--single-instance` option. All +panel invocations with the same :option:`--instance-group` will result +in new panels being created in the first panel instance within that group. + + +{listen_on_defn} + + +--toggle-visibility +type=bool-set +When set and using :option:`--single-instance` will toggle the visibility of the +existing panel rather than creating a new one. + + +--start-as-hidden +type=bool-set +Start in hidden mode, useful with :option:`--toggle-visibility`. + + +--detach +type=bool-set +Detach from the controlling terminal, if any, running in an independent child process, +the parent process exits immediately. + + +--detached-log +Path to a log file to store STDOUT/STDERR when using :option:`--detach` + + +--debug-rendering +type=bool-set +For internal debugging use. +''' + setattr(panel_kitten_options_spec, 'ans', OPTIONS.format( + appname=appname, listen_on_defn=listen_on_defn, + )) + ans: str = getattr(panel_kitten_options_spec, 'ans') + return ans +# }}} diff --git a/kitty_tests/options.py b/kitty_tests/options.py index b76e1db26..ce883e13e 100644 --- a/kitty_tests/options.py +++ b/kitty_tests/options.py @@ -93,9 +93,9 @@ def t(args, leftover=(), fails=False, version_called=False, help_called=False, * self.assertEqual(tuple(leftover), tuple(actual_leftover), f'{args}\n{ans}') for dest, defval in oc.values_map.items(): val = expected.get(dest, defval) - self.assertEqual(val, getattr(ans, dest, BaseTest), f'Failed to parse {dest} correctly for: {args} \n{ans}') - self.assertEqual(version_called, oc.version_called) - self.assertEqual(help_called, oc.help_called) + self.assertEqual(val, getattr(ans, dest, BaseTest), f'Failed to parse {dest} correctly for: {args}\n{ans}') + self.assertEqual(version_called, oc.version_called, f'Failed to call version for: {args}\n{ans}') + self.assertEqual(help_called, oc.help_called, f'Failed to call help for: {args}\n{ans}') t('-1', bool_set=True) t('-01', bool_reset=False, bool_set=True)