mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-07-03 11:12:30 +08:00
Start work on quick-access-terminal kitten
This commit is contained in:
@@ -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)
|
||||
|
||||
0
kittens/quick_access_terminal/__init__.py
Normal file
0
kittens/quick_access_terminal/__init__.py
Normal file
32
kittens/quick_access_terminal/main.go
Normal file
32
kittens/quick_access_terminal/main.go
Normal 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)
|
||||
}
|
||||
43
kittens/quick_access_terminal/main.py
Normal file
43
kittens/quick_access_terminal/main.py
Normal 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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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", " "))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user