diff --git a/kittens/panel/main.go b/kittens/panel/main.go index a6ff0372a..bead3d307 100644 --- a/kittens/panel/main.go +++ b/kittens/panel/main.go @@ -30,6 +30,8 @@ func complete_kitty_listen_on(completions *cli.Completions, word string, arg_num } } +var CompleteKittyListenOn = complete_kitty_listen_on + func GetQuickAccessKittyExe() (kitty_exe string, err error) { if kitty_exe, err = filepath.EvalSymlinks(utils.KittyExe()); err != nil { return "", fmt.Errorf("Failed to find path to the kitty executable, this kitten requires the kitty executable to function. The kitty executable or a symlink to it must be placed in the same directory as the kitten executable. Error: %w", err) diff --git a/kittens/quick_access_terminal/__init__.py b/kittens/quick_access_terminal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kittens/quick_access_terminal/main.go b/kittens/quick_access_terminal/main.go new file mode 100644 index 000000000..837a8deda --- /dev/null +++ b/kittens/quick_access_terminal/main.go @@ -0,0 +1,32 @@ +package quick_access_terminal + +import ( + "fmt" + "os" + + "kitty/kittens/panel" + "kitty/tools/cli" + + "golang.org/x/sys/unix" +) + +var _ = fmt.Print + +var complete_kitty_listen_on = panel.CompleteKittyListenOn + +func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) { + kitty_exe, err := panel.GetQuickAccessKittyExe() + if err != nil { + return 1, err + } + argv := []string{kitty_exe, "+kitten", "panel"} + argv = append(argv, o.AsCommandLine()...) + argv = append(argv, args...) + err = unix.Exec(kitty_exe, argv, os.Environ()) + rc = 1 + return +} + +func EntryPoint(parent *cli.Command) { + create_cmd(parent, main) +} diff --git a/kittens/quick_access_terminal/main.py b/kittens/quick_access_terminal/main.py new file mode 100644 index 000000000..7aba40415 --- /dev/null +++ b/kittens/quick_access_terminal/main.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2025, Kovid Goyal + +import sys + +from kitty.simple_cli_definitions import build_panel_cli_spec + +help_text = 'A quick access terminal window that you can bring up instantly with a keypress or a command.' + + +def options_spec() -> str: + if not (ans := getattr(options_spec, 'ans', '')): + ans = build_panel_cli_spec({ + 'lines': '25', + 'columns': '80', + 'edge': 'top', + 'layer': 'overlay', + 'toggle_visibility': 'yes', + 'single_instance': 'yes', + 'instance_group': 'quake', + 'focus_policy': 'exclusive', + 'cls': 'kitty-quick-access', + 'exclusive_zone': '0', + 'override_exclusive_zone': 'yes', + 'override': 'background_opacity=0.8', + }) + setattr(options_spec, 'ans', ans) + return ans + + +def main(args: list[str]) -> None: + from ..panel.main import main as panel_main + return panel_main(args) + + +if __name__ == '__main__': + main(sys.argv) +elif __name__ == '__doc__': + cd: dict = sys.cli_docs # type: ignore + cd['usage'] = '[cmdline-to-run ...]' + cd['options'] = options_spec + cd['help_text'] = help_text + cd['short_desc'] = help_text diff --git a/kittens/ssh/config_test.go b/kittens/ssh/config_test.go index 4d782afd9..a0008918b 100644 --- a/kittens/ssh/config_test.go +++ b/kittens/ssh/config_test.go @@ -112,7 +112,7 @@ func TestSSHConfigParsing(t *testing.T) { hostname = "2" rt() - ci, err := ParseCopyInstruction("--exclude moose --dest=target " + cf) + ci, err := ParseCopyInstruction("--exclude moose --exclude second --dest=target " + cf) if err != nil { t.Fatal(err) } @@ -124,7 +124,7 @@ func TestSSHConfigParsing(t *testing.T) { if diff != "" { t.Fatalf("Incorrect local_path:\n%s", diff) } - diff = cmp.Diff([]string{"moose"}, ci[0].exclude_patterns) + diff = cmp.Diff([]string{"moose", "second"}, ci[0].exclude_patterns) if diff != "" { t.Fatalf("Incorrect excludes:\n%s", diff) } diff --git a/kitty/cli.py b/kitty/cli.py index c99a7be58..ca87d8749 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -80,9 +80,9 @@ class GoOption: elif self.obj_dict['completion'].type is not CompletionType.none: ans += ''.join(self.obj_dict['completion'].as_go_code('Completer', ': ')) + ',' if depth > 0: - ans += f'\nDepth: {depth},\n' + ans += f'\n\tDepth: {depth},\n' if self.default: - ans += f'\nDefault: "{serialize_as_go_string(self.default)}",\n' + ans += f'\n\tDefault: "{serialize_as_go_string(self.default)}",\n' return ans + '})' def as_string_for_commandline(self) -> str: @@ -94,7 +94,7 @@ class GoOption: case 'int': val = f'fmt.Sprintf(`%d`, {val})' case 'string': - val = val + return f'if {val} != "" {{ ans = append(ans, `{flag}=` + {val}) }}' case 'float64': val = f'fmt.Sprintf(`%f`, {val})' case '[]string': diff --git a/kitty/launcher/cli-parser.h b/kitty/launcher/cli-parser.h index 7e7efb680..79a500bef 100644 --- a/kitty/launcher/cli-parser.h +++ b/kitty/launcher/cli-parser.h @@ -487,6 +487,9 @@ parse_cli_from_python_spec(PyObject *self, PyObject *args) { flag.defval.floatval = PyFloat_AsDouble(defval); } else if (strcmp(type, "list") == 0) { flag.defval.type = CLI_VALUE_LIST; + if (PyObject_IsTrue(defval)) { + for (ssize_t l = 0; l < PyList_GET_SIZE(defval); l++) add_to_listval(&spec, &flag.defval, PyUnicode_AsUTF8(PyList_GET_ITEM(defval, l))); + } } else if (strcmp(type, "choices") == 0) { flag.defval.type = CLI_VALUE_CHOICE; flag.defval.strval = PyUnicode_AsUTF8(defval); diff --git a/kitty/simple_cli_definitions.py b/kitty/simple_cli_definitions.py index 834ad877b..362d6b929 100644 --- a/kitty/simple_cli_definitions.py +++ b/kitty/simple_cli_definitions.py @@ -224,7 +224,7 @@ def defval_for_opt(opt: OptionDict) -> Any: else: dv = dv.lower() in ('true', 'yes', 'y') elif typ == 'list': - dv = [] + dv = list(shlex_split(dv)) if dv else [] elif typ in ('int', 'float'): dv = (int if typ == 'int' else float)(dv or 0) return dv @@ -250,6 +250,15 @@ def c_str(x: str) -> str: return f'"{x}"' +def add_list_values(*values: str) -> Iterator[str]: + yield f'\tflag.defval.listval.items = alloc_for_cli(spec, {len(values)} * sizeof(flag.defval.listval.items[0]));' + yield '\tif (!flag.defval.listval.items) OOM;' + yield f'\tflag.defval.listval.count = {len(values)};' + yield f'\tflag.defval.listval.capacity = {len(values)};' + for n, value in enumerate(values): + yield f'\tflag.defval.listval.items[{n}] = {c_str(value)};' + + def generate_c_parser_for(funcname: str, spec: str) -> Iterator[str]: names_map, _, defaults_map = get_option_maps(parse_option_spec(spec)[0]) if 'help' not in names_map: @@ -278,16 +287,12 @@ def generate_c_parser_for(funcname: str, spec: str) -> Iterator[str]: yield f'\tflag.defval.floatval = {defval};' case 'list': yield '\tflag.defval.type = CLI_VALUE_LIST;' + if defval: + yield from add_list_values(*defval) case 'choices': yield '\tflag.defval.type = CLI_VALUE_CHOICE;' yield f'\tflag.defval.strval = {c_str(defval)};' - choices = opt['choices'] - yield f'\tflag.defval.listval.items = alloc_for_cli(spec, {len(choices)} * sizeof(flag.defval.listval.items[0]));' - yield '\tif (!flag.defval.listval.items) OOM;' - yield f'\tflag.defval.listval.count = {len(choices)};' - yield f'\tflag.defval.listval.capacity = {len(choices)};' - for n, choice in enumerate(choices): - yield f'\tflag.defval.listval.items[{n}] = {c_str(choice)};' + yield from add_list_values(*opt['choices']) case _: yield '\tflag.defval.type = CLI_VALUE_STRING;' yield f'\tflag.defval.strval = {"NULL" if defval is None else c_str(defval)};' @@ -524,51 +529,62 @@ type=bool-set # panel CLI spec {{{ +panel_defaults = { + 'lines': '1', 'columns': '1', + 'margin_left': '0', 'margin_top': '0', 'margin_right': '0', 'margin_bottom': '0', + 'edge': 'top', 'layer': 'bottom', 'override': '', 'cls': f'{appname}-panel', + 'focus_policy': 'not-allowed', 'exclusive_zone': '-1', 'override_exclusive_zone': 'no', + 'single_instance': 'no', 'instance_group': '', 'toggle_visibility': 'no', + 'start_as_hidden': 'no', 'detach': 'no', 'detached_log': '', +} + def build_panel_cli_spec(defaults: dict[str, str]) -> str: + d = panel_defaults.copy() + d.update(defaults) return r''' --lines -default=1 +default={lines} 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 +default={columns} 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 +default={margin_top} 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 +default={margin_left} 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 +default={margin_bottom} 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 +default={margin_right} 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 +default={edge} 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 @@ -586,7 +602,7 @@ and :option:`--columns`. --layer choices=background,bottom,top,overlay -default=bottom +default={layer} 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 @@ -600,6 +616,7 @@ Path to config file to use for kitty when drawing the panel. --override -o type=list +default={override} Override individual kitty configuration options, can be specified multiple times. Syntax: :italic:`name=value`. For example: :option:`kitty +kitten panel -o` font_size=20 @@ -612,7 +629,7 @@ output automatically, typically the last output the user interacted with or the --class --app-id dest=cls -default={appname}-panel +default={cls} condition=not is_macos Set the class part of the :italic:`WM_CLASS` window property. On Wayland, it sets the app id. @@ -624,7 +641,7 @@ Set the name part of the :italic:`WM_CLASS` property (defaults to using the valu --focus-policy choices=not-allowed,exclusive,on-demand -default=not-allowed +default={focus_policy} 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. @@ -632,7 +649,7 @@ On macOS, :code:`exclusive` and :code:`on-demand` are currently the same. Ignore --exclusive-zone type=int -default=-1 +default={exclusive_zone} 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 @@ -643,6 +660,7 @@ Ignored on X11 and macOS. --override-exclusive-zone type=bool-set +default={override_exclusive_zone} 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. @@ -650,12 +668,14 @@ Ignored on X11 and macOS. --single-instance -1 type=bool-set +default={single_instance} 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 +default={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. @@ -666,29 +686,33 @@ in new panels being created in the first panel instance within that group. --toggle-visibility type=bool-set +default={toggle_visibility} 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 +default={start_as_hidden} Start in hidden mode, useful with :option:`--toggle-visibility`. --detach type=bool-set +default={detach} Detach from the controlling terminal, if any, running in an independent child process, the parent process exits immediately. --detached-log +default={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, **defaults) +'''.format(appname=appname, listen_on_defn=listen_on_defn, **d) # }}} diff --git a/tools/cli/help.go b/tools/cli/help.go index 13bd1736e..fa08eaf69 100644 --- a/tools/cli/help.go +++ b/tools/cli/help.go @@ -78,10 +78,6 @@ func (self *Option) FormatOptionForMan(output io.Writer) { fmt.Fprint(output, "\" ") defval := self.Default switch self.OptionType { - case StringOption: - if self.IsList { - defval = "" - } case CountOption: defval = "" case BoolOption: @@ -109,10 +105,6 @@ func (self *Option) FormatOption(output io.Writer, formatter *markup.Context, sc } defval := self.Default switch self.OptionType { - case StringOption: - if self.IsList { - defval = "" - } case CountOption: defval = "" case BoolOption: diff --git a/tools/cli/option-from-string.go b/tools/cli/option-from-string.go index 4685f36ff..8262a76e6 100644 --- a/tools/cli/option-from-string.go +++ b/tools/cli/option-from-string.go @@ -11,6 +11,7 @@ import ( "kitty/tools/cli/markup" "kitty/tools/utils" + "kitty/tools/utils/shlex" ) var _ = fmt.Print @@ -178,7 +179,11 @@ func option_from_spec(spec OptionSpec) (*Option, error) { } ans.parsed_default = pval if ans.IsList { - ans.parsed_default = []string{} + if ans.Default == "" { + ans.parsed_default = nil + } else if ans.parsed_default, err = shlex.Split(ans.Default); err != nil { + return nil, err + } } ans.Completer = spec.Completer if ans.Aliases == nil || len(ans.Aliases) == 0 { diff --git a/tools/cli/option.go b/tools/cli/option.go index 7e5892b4c..0bcda0fc6 100644 --- a/tools/cli/option.go +++ b/tools/cli/option.go @@ -105,6 +105,11 @@ func NormalizeOptionName(name string) string { func (self *Option) parsed_value() any { if len(self.values_from_cmdline) == 0 { + if self.IsList { + if self.parsed_default == nil { + return []string{} + } + } return self.parsed_default } switch self.OptionType { @@ -112,9 +117,12 @@ func (self *Option) parsed_value() any { return len(self.parsed_values_from_cmdline) case StringOption: if self.IsList { - ans := make([]string, len(self.parsed_values_from_cmdline)) - for i, x := range self.parsed_values_from_cmdline { - ans[i] = x.(string) + ans := make([]string, 0, len(self.parsed_values_from_cmdline)+2) + if self.parsed_default != nil { + ans = append(ans, self.parsed_default.([]string)...) + } + for _, x := range self.parsed_values_from_cmdline { + ans = append(ans, x.(string)) } return ans } @@ -128,9 +136,9 @@ func (self *Option) parse_value(val string) (any, error) { switch self.OptionType { case BoolOption: switch val { - case "true": + case "y", "yes", "true": return true, nil - case "false": + case "n", "no", "false": return false, nil default: return nil, &ParseError{Option: self, Message: fmt.Sprintf(":yellow:`%s` is not a valid value for :bold:`%s`.", val, self.seen_option)} diff --git a/tools/cli/types_test.go b/tools/cli/types_test.go index 17e9f42a3..b96f66992 100644 --- a/tools/cli/types_test.go +++ b/tools/cli/types_test.go @@ -23,18 +23,22 @@ type base_options struct { } type options struct { - FromParent int - SimpleString string - Choices string - SetMe bool - Int int - Float float64 - List []string + FromParent int + SimpleString string + Choices string + SetMe bool + Int int + Float float64 + List []string + ListWithDefault []string } func TestCLIParsing(t *testing.T) { rt := func(expected_cmd *Command, cmdline string, expected_options any, expected_args ...string) { + if opts, ok := expected_options.(*options); ok && opts.ListWithDefault == nil { + opts.ListWithDefault = []string{`1`, `2`} + } cp, err := shlex.Split(cmdline) if err != nil { t.Fatal(err) @@ -77,6 +81,7 @@ func TestCLIParsing(t *testing.T) { child1.Add(OptionSpec{Name: "--int -i", Type: "int"}) child1.Add(OptionSpec{Name: "--float", Type: "float"}) child1.Add(OptionSpec{Name: "--list", Type: "list"}) + child1.Add(OptionSpec{Name: "--list-with-default -L", Type: "list", Default: "1 2"}) child1.SubCommandIsOptional = true gc1 := child1.AddSubCommand(&Command{Name: "gc1"}) @@ -97,6 +102,7 @@ func TestCLIParsing(t *testing.T) { rt(child1, "test child1 --int -3 --simple-s -s --float=3.3", &options{SimpleString: "-s", Int: -3, Float: 3.3}) rt(child1, "test child1 -bi=3 --float=3.3", &options{SetMe: true, Int: 3, Float: 3.3}) rt(child1, "test child1 --list -3 -p --list one", &options{FromParent: 1, List: []string{"-3", "one"}}) + rt(child1, "test child1 -L 3 -L 4", &options{ListWithDefault: []string{`1`, `2`, `3`, `4`}}) rt(gc1, "test -p child1 -p gc1 xxx", &empty_options{}, "xxx") _, err := child1.ParseArgs(strings.Split("test child1 --choices x", " ")) diff --git a/tools/cmd/tool/main.go b/tools/cmd/tool/main.go index 1643ee715..14240286b 100644 --- a/tools/cmd/tool/main.go +++ b/tools/cmd/tool/main.go @@ -15,6 +15,7 @@ import ( "kitty/kittens/notify" "kitty/kittens/panel" "kitty/kittens/query_terminal" + "kitty/kittens/quick_access_terminal" "kitty/kittens/show_key" "kitty/kittens/ssh" "kitty/kittens/themes" @@ -56,6 +57,8 @@ func KittyToolEntryPoints(root *cli.Command) { transfer.EntryPoint(root) // panel panel.EntryPoint(root) + // quick_access_terminal + quick_access_terminal.EntryPoint(root) // unicode_input unicode_input.EntryPoint(root) // show_key