Start work on quick-access-terminal kitten

This commit is contained in:
Kovid Goyal
2025-04-28 19:52:04 +05:30
parent 83bb1553f7
commit 5683314784
13 changed files with 164 additions and 46 deletions

View File

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

View File

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

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>
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

View File

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

View File

@@ -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':

View File

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

View File

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

View File

@@ -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:

View File

@@ -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 {

View File

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

View File

@@ -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", " "))

View File

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