diff --git a/gen-go-code.py b/gen-go-code.py index 867d171f1..673a35a56 100755 --- a/gen-go-code.py +++ b/gen-go-code.py @@ -57,6 +57,23 @@ def generate_completion_for_rc(name: str) -> None: print(opt.as_completion_option(name)) +def generate_kittens_completion() -> None: + from kittens.runner import all_kitten_names, get_kitten_cli_docs + for kitten in all_kitten_names(): + kn = 'kitten_' + kitten + print(f'{kn} := plus_kitten.add_command("{kitten}", "Kittens")') + kcd = get_kitten_cli_docs(kitten) + if kcd: + ospec = kcd['options'] + for opt in go_options_for_seq(parse_option_spec(ospec())[0]): + print(opt.as_completion_option(kn)) + ac = kcd.get('args_completion') + if ac is not None: + print(''.join(ac.as_go_code(kn))) + else: + print(f'{kn}.Description = ""') + + def generate_completions_for_kitty() -> None: print('package completion\n') print('func kitty(root *Command) {') @@ -71,22 +88,27 @@ def generate_completions_for_kitty() -> None: print(f'k.find_option("-o").Completion_for_arg = complete_kitty_override("Config directives", []string{{{conf_names}}})') print('k.find_option("--listen-on").Completion_for_arg = complete_kitty_listen_on') - print('plus := k.add_command("+", "Entry point")') + print('plus := k.add_command("+", "Entry points")') print('plus.Description = "Various special purpose tools and kittens"') - print('plus_launch := plus.add_command("launch", "Launch Python script")') + print('plus_launch := plus.add_command("launch", "Entry points")') print('plus_launch.Completion_for_arg = complete_plus_launch') print('k.add_clone("+launch", "Launch Python scripts", plus_launch)') - print('plus_runpy := plus.add_command("runpy", "Run python code")') + print('plus_runpy := plus.add_command("runpy", "Entry points")') print('plus_runpy.Completion_for_arg = complete_plus_runpy') print('k.add_clone("+runpy", "Run Python code", plus_runpy)') - print('plus_open := plus.add_command("open", "Open files and URLs")') + print('plus_open := plus.add_command("open", "Entry points")') print('plus_open.Completion_for_arg = complete_plus_open') print('plus_open.clone_options_from(k)') print('k.add_clone("+open", "Open files and URLs", plus_open)') + print('plus_kitten := plus.add_command("kitten", "Kittens")') + print('plus_kitten.Subcommand_must_be_first = true') + generate_kittens_completion() + print('k.add_clone("+kitten", "Kittens", plus_kitten)') + print('at := k.add_command("@", "Remote control")') print('at.Description = "Control kitty using commands"') for go_name in all_command_names(): diff --git a/kittens/diff/main.py b/kittens/diff/main.py index 271a36c31..09ad37020 100644 --- a/kittens/diff/main.py +++ b/kittens/diff/main.py @@ -17,7 +17,7 @@ from typing import ( Any, DefaultDict, Dict, Iterable, Iterator, List, Optional, Tuple, Union ) -from kitty.cli import CONFIG_HELP, parse_args +from kitty.cli import CONFIG_HELP, parse_args, CompletionSpec from kitty.cli_stub import DiffCLIOptions from kitty.conf.utils import KeyAction from kitty.constants import appname @@ -577,6 +577,7 @@ number set in :file:`diff.conf`. --config type=list +completion=type:file ext:conf group:"Config files" kwds:none,NONE {config_help} @@ -687,6 +688,7 @@ elif __name__ == '__doc__': cd['usage'] = usage cd['options'] = OPTIONS cd['help_text'] = help_text + cd['args_completion'] = CompletionSpec.from_string('type:file mime:text/* mime:image/* group:"Text and image files"') elif __name__ == '__conf__': from .options.definition import definition sys.options_definition = definition # type: ignore diff --git a/kittens/icat/main.py b/kittens/icat/main.py index 6e25473de..410eb26fe 100644 --- a/kittens/icat/main.py +++ b/kittens/icat/main.py @@ -15,7 +15,7 @@ from typing import ( Dict, Generator, List, NamedTuple, Optional, Pattern, Tuple, Union ) -from kitty.cli import parse_args +from kitty.cli import parse_args, CompletionSpec from kitty.cli_stub import IcatCLIOptions from kitty.constants import appname from kitty.guess_mime_type import guess_type @@ -622,3 +622,4 @@ elif __name__ == '__doc__': cd['usage'] = usage cd['options'] = options_spec cd['help_text'] = help_text + cd['args_completion'] = CompletionSpec.from_string('type:file mime:image/* group:Images') diff --git a/kittens/themes/collection.py b/kittens/themes/collection.py index c8b3e8c4c..986fd5130 100644 --- a/kittens/themes/collection.py +++ b/kittens/themes/collection.py @@ -8,17 +8,20 @@ import os import re import shutil import signal +import sys import tempfile import zipfile from contextlib import suppress -from typing import Any, Callable, Dict, Iterator, Match, Optional, Tuple, Union, Type +from typing import ( + Any, Callable, Dict, Iterator, Match, Optional, Tuple, Type, 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.options.types import Options as KittyOptions from kitty.fast_data_types import Color +from kitty.options.types import Options as KittyOptions from kitty.utils import reload_conf_in_all_kitties from ..choose.match import match @@ -647,3 +650,13 @@ def load_themes(cache_age: float = 1., ignore_no_cache: bool = False) -> Themes: ans.load_from_dir(os.path.join(config_dir, 'themes')) ans.index_map = tuple(ans.themes) return ans + + +def print_theme_names() -> None: + found = False + for theme in load_themes(cache_age=-1, ignore_no_cache=True): + print(theme.name) + found = True + if not found: + print('Default') + sys.stdout.flush() diff --git a/kittens/themes/main.py b/kittens/themes/main.py index 751e2c5e7..5504d8da7 100644 --- a/kittens/themes/main.py +++ b/kittens/themes/main.py @@ -11,12 +11,12 @@ from typing import ( Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union ) -from kitty.cli import create_default_opts, parse_args +from kitty.cli import CompletionSpec, create_default_opts, parse_args from kitty.cli_stub import ThemesCLIOptions from kitty.config import cached_values_for -from kitty.options.types import Options as KittyOptions from kitty.constants import config_dir from kitty.fast_data_types import truncate_point_for_length, wcswidth +from kitty.options.types import Options as KittyOptions from kitty.rgb import color_as_sharp, color_from_int from kitty.typing import KeyEventType from kitty.utils import ScreenSize @@ -616,3 +616,4 @@ elif __name__ == '__doc__': cd['usage'] = usage cd['options'] = OPTIONS cd['help_text'] = help_text + cd['args_completion'] = CompletionSpec.from_string('type:special group:complete_themes') diff --git a/kitty/cli.py b/kitty/cli.py index e6e427111..6884c41df 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -30,6 +30,7 @@ class CompletionType(Enum): file = auto() directory = auto() keyword = auto() + special = auto() none = auto() @@ -89,6 +90,8 @@ class CompletionSpec: if self.type is CompletionType.directory: g = serialize_as_go_string(self.group or 'Directories') completers.append(f'directory_completer("{g}", {relative_to})') + if self.type is CompletionType.special: + completers.append(self.group) if go_name: go_name += '.' if len(completers) > 1: @@ -113,7 +116,9 @@ def serialize_as_go_string(x: str) -> str: return x.replace('\\', '\\\\').replace('\n', '\\n').replace('"', '\\"') -go_type_map = {'bool-set': 'bool', 'bool-reset': 'bool', 'int': 'int', 'float': 'float64', '': 'string', 'list': '[]string', 'choices': 'string'} +go_type_map = { + 'bool-set': 'bool', 'bool-reset': 'bool', 'int': 'int', 'float': 'float64', + '': 'string', 'list': '[]string', 'choices': 'string', 'str': 'string'} go_getter_map = { 'bool-set': 'GetBool', 'bool-reset': 'GetBool', 'int': 'GetInt', 'float': 'GetFloat64', '': 'GetString', 'list': 'GetStringArray', 'choices': 'GetString' @@ -460,6 +465,8 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option if k == 'default': current_cmd['default'] = v elif k == 'type': + if v == 'choice': + v = 'choices' current_cmd['type'] = v elif k == 'dest': current_cmd['dest'] = v diff --git a/kitty_tests/completion.py b/kitty_tests/completion.py index 170a9fadd..10ca88448 100644 --- a/kitty_tests/completion.py +++ b/kitty_tests/completion.py @@ -136,6 +136,14 @@ def completion(self: TestCompletion, tdir: str): add('kitty @launch --logo ', all_words('exe-not3.png')) add('kitty @launch --logo ~', all_words('~/exe-not3.png')) + add('kitty + ', has_words('launch', 'kitten')) + add('kitty + kitten ', has_words('icat', 'diff')) + add('kitty +kitten icat ', has_words('sub/', 'exe-not2.jpeg')) + add('kitty + kitten icat --pr', has_words('--print-window-size')) + add('kitty + kitten diff ', has_words('exe-not2.jpeg')) + add('kitty + kitten themes --', has_words('--cache-age')) + add('kitty + kitten themes D', has_words('Default')) + for cmd, tests, result in zip(all_cmds, all_tests, run_tool()): self.current_cmd = cmd for test in tests: diff --git a/tools/completion/files.go b/tools/completion/files.go index 182bacd08..2b3bb3809 100644 --- a/tools/completion/files.go +++ b/tools/completion/files.go @@ -185,7 +185,17 @@ func complete_by_fnmatch(prefix, cwd string, patterns []string) []string { } func complete_by_mimepat(prefix, cwd string, patterns []string) []string { + all_allowed := false + for _, p := range patterns { + if p == "*" { + all_allowed = true + break + } + } return fname_based_completer(prefix, cwd, func(name string) bool { + if all_allowed { + return true + } idx := strings.Index(name, ".") if idx < 1 { return false diff --git a/tools/completion/kitty.go b/tools/completion/kitty.go index d892bd5e1..a71a330ab 100644 --- a/tools/completion/kitty.go +++ b/tools/completion/kitty.go @@ -3,8 +3,12 @@ package completion import ( + "bufio" + "bytes" "fmt" + "kitty/tools/utils" "os" + "os/exec" "path/filepath" "strings" @@ -97,3 +101,20 @@ func complete_plus_runpy(completions *Completions, word string, arg_num int) { func complete_plus_open(completions *Completions, word string, arg_num int) { fnmatch_completer("Files", CWD, "*")(completions, word, arg_num) } + +func complete_themes(completions *Completions, word string, arg_num int) { + kitty, err := utils.KittyExe() + if err == nil { + out, err := exec.Command(kitty, "+runpy", "from kittens.themes.collection import *; print_theme_names()").Output() + if err == nil { + mg := completions.add_match_group("Themes") + scanner := bufio.NewScanner(bytes.NewReader(out)) + for scanner.Scan() { + theme_name := strings.TrimSpace(scanner.Text()) + if theme_name != "" && strings.HasPrefix(theme_name, word) { + mg.add_match(theme_name) + } + } + } + } +} diff --git a/tools/completion/parse-args.go b/tools/completion/parse-args.go index bf6977b25..e570378cf 100644 --- a/tools/completion/parse-args.go +++ b/tools/completion/parse-args.go @@ -139,6 +139,7 @@ func (cmd *Command) parse_args(words []string, completions *Completions) { for i, word := range words { cmd = completions.current_cmd completions.current_word_idx = i + completions.current_word_idx_in_parent++ is_last_word := i == len(words)-1 if expecting_arg_for == nil && !strings.HasPrefix(word, "-") { arg_num++ @@ -172,6 +173,7 @@ func (cmd *Command) parse_args(words []string, completions *Completions) { completions.current_cmd = sc cmd = sc arg_num = 0 + completions.current_word_idx_in_parent = 0 only_args_allowed = false } else if cmd.Stop_processing_at_arg > 0 && arg_num >= cmd.Stop_processing_at_arg { return diff --git a/tools/completion/types.go b/tools/completion/types.go index ab755a25b..bc4316061 100644 --- a/tools/completion/types.go +++ b/tools/completion/types.go @@ -31,9 +31,10 @@ func (self *MatchGroup) add_prefix_to_all_matches(prefix string) { type Completions struct { Groups []*MatchGroup `json:"groups,omitempty"` - current_cmd *Command - all_words []string // all words passed to parse_args() - current_word_idx int // index of current word in all_words + current_cmd *Command + all_words []string // all words passed to parse_args() + current_word_idx int // index of current word in all_words + current_word_idx_in_parent int // index of current word in parents command line 1 for first word after parent } func (self *Completions) add_prefix_to_all_matches(prefix string) { @@ -141,7 +142,7 @@ func (self *Command) has_subcommands() bool { func (self *Command) sub_command_allowed_at(completions *Completions, arg_num int) bool { if self.Subcommand_must_be_first { - return arg_num == 1 && completions.current_word_idx == 0 + return arg_num == 1 && completions.current_word_idx_in_parent == 1 } return arg_num == 1 } diff --git a/tools/utils/paths.go b/tools/utils/paths.go index 9c55b4590..f5318d1ba 100644 --- a/tools/utils/paths.go +++ b/tools/utils/paths.go @@ -9,6 +9,8 @@ import ( "path/filepath" "runtime" "strings" + + "golang.org/x/sys/unix" ) var Sep = string(os.PathSeparator) @@ -56,6 +58,15 @@ func Abspath(path string) string { var config_dir string +func KittyExe() (string, error) { + exe, err := os.Executable() + if err != nil { + return "", err + } + ans := filepath.Join(filepath.Dir(exe), "kitty") + return ans, unix.Access(ans, unix.X_OK) +} + func ConfigDir() string { if config_dir != "" { return config_dir