From 4d3f3b5e91c08b6d3c89e29e5f1222af1974c6a8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 25 Nov 2022 12:58:10 +0530 Subject: [PATCH] Start moving clipboard kitten to kitty-tool --- gen-go-code.py | 104 +++++++++++++++++++++++++++--------- kittens/clipboard/main.py | 1 + kitty/data-types.c | 18 +++++++ kitty/fast_data_types.pyi | 5 +- setup.py | 26 +++++++++ shell-integration/ssh/kitty | 15 ++++++ tools/cli/command.go | 4 +- tools/cmd/clipboard/main.go | 18 +++++++ tools/cmd/tool/main.go | 4 ++ 9 files changed, 167 insertions(+), 28 deletions(-) create mode 100644 tools/cmd/clipboard/main.go diff --git a/gen-go-code.py b/gen-go-code.py index 8da084006..96cdcb7f3 100755 --- a/gen-go-code.py +++ b/gen-go-code.py @@ -8,7 +8,7 @@ import subprocess import sys from contextlib import contextmanager, suppress from functools import lru_cache -from typing import Dict, Iterator, List, Set, Tuple, Union, Sequence +from typing import Any, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Union import kitty.constants as kc from kittens.tui.operations import Mode @@ -50,10 +50,24 @@ def replace(template: str, **kw: str) -> str: # Completions {{{ + +@lru_cache +def kitten_cli_docs(kitten: str) -> Any: + from kittens.runner import get_kitten_cli_docs + return get_kitten_cli_docs(kitten) + + +@lru_cache +def go_options_for_kitten(kitten: str) -> Tuple[Sequence[GoOption], Optional[CompletionSpec]]: + kcd = kitten_cli_docs(kitten) + if kcd: + ospec = kcd['options'] + return (tuple(go_options_for_seq(parse_option_spec(ospec())[0])), kcd.get('args_completion')) + return (), None + + def generate_kittens_completion() -> None: - from kittens.runner import ( - all_kitten_names, get_kitten_cli_docs, get_kitten_wrapper_of, - ) + from kittens.runner import all_kitten_names, get_kitten_wrapper_of for kitten in sorted(all_kitten_names()): kn = 'kitten_' + kitten print(f'{kn} := plus_kitten.AddSubCommand(&cli.Command{{Name:"{kitten}", Group: "Kittens"}})') @@ -62,12 +76,10 @@ def generate_kittens_completion() -> None: print(f'{kn}.ArgCompleter = cli.CompletionForWrapper("{serialize_as_go_string(wof)}")') print(f'{kn}.OnlyArgsAllowed = true') continue - kcd = get_kitten_cli_docs(kitten) - if kcd: - ospec = kcd['options'] - for opt in go_options_for_seq(parse_option_spec(ospec())[0]): + gopts, ac = go_options_for_kitten(kitten) + if gopts or ac: + for opt in gopts: print(opt.as_option(kn)) - ac = kcd.get('args_completion') if ac is not None: print(''.join(ac.as_go_code(kn + '.ArgCompleter', ' = '))) else: @@ -265,6 +277,48 @@ def go_code_for_remote_command(name: str, cmd: RemoteCommand, template: str) -> # }}} +# kittens {{{ + +@lru_cache +def wrapped_kittens() -> Sequence[str]: + with open('shell-integration/ssh/kitty') as f: + for line in f: + if line.startswith(' wrapped_kittens="'): + val = line.strip().partition('"')[2][:-1] + return tuple(sorted(filter(None, val.split()))) + raise Exception('Failed to read wrapped kittens from kitty wrapper script') + + +def kitten_clis() -> None: + for kitten in wrapped_kittens(): + with replace_if_needed(f'tools/cmd/{kitten}/cli_generated.go'): + kcd = kitten_cli_docs(kitten) + has_underscore = '_' in kitten + print(f'package {kitten}') + print('import "kitty/tools/cli"') + print('func create_cmd(root *cli.Command, run_func cli.RunFunc) {') + print('ans := root.AddSubCommand(&cli.Command{') + print(f'Name: "{kitten}",') + print(f'ShortDescription: "{serialize_as_go_string(kcd["short_desc"])}",') + print(f'Usage: "{serialize_as_go_string(kcd["usage"])}",') + print(f'HelpText: "{serialize_as_go_string(kcd["help_text"])}",') + print('Run: run_func,') + if has_underscore: + print('Hidden: true,') + print('})') + gopts, ac = go_options_for_kitten(kitten) + for opt in gopts: + print(opt.as_option('ans')) + if ac is not None: + print(''.join(ac.as_go_code('ans.ArgCompleter', ' = '))) + if has_underscore: + print("clone := root.AddClone(ans.Group, ans)") + print('clone.Hidden = false') + print(f'clone.Name = "{serialize_as_go_string(kitten.replace("_", "-"))}"') + print('}') +# }}} + + # Constants {{{ def generate_spinners() -> str: @@ -335,7 +389,12 @@ var DocTitleMap = map[string]string{serialize_go_dict(ref_map['doc'])} @contextmanager def replace_if_needed(path: str, show_diff: bool = False) -> Iterator[io.StringIO]: buf = io.StringIO() - yield buf + origb = sys.stdout + sys.stdout = buf + try: + yield buf + finally: + sys.stdout = origb orig = '' with suppress(FileNotFoundError), open(path, 'r') as f: orig = f.read() @@ -391,21 +450,15 @@ func add_rc_global_opts(cmd *cli.Command) {{ def update_completion() -> None: - orig = sys.stdout - try: - with replace_if_needed('tools/cmd/completion/kitty_generated.go') as f: - sys.stdout = f - generate_completions_for_kitty() - with replace_if_needed('tools/cmd/edit_in_kitty/launch_generated.go') as f: - sys.stdout = f - print('package edit_in_kitty') - print('import "kitty/tools/cli"') - print('func AddCloneSafeOpts(cmd *cli.Command) {') - completion_for_launch_wrappers('cmd') - print(''.join(CompletionSpec.from_string('type:file mime:text/* group:"Text files"').as_go_code('cmd.ArgCompleter', ' = '))) - print('}') - finally: - sys.stdout = orig + with replace_if_needed('tools/cmd/completion/kitty_generated.go'): + generate_completions_for_kitty() + with replace_if_needed('tools/cmd/edit_in_kitty/launch_generated.go'): + print('package edit_in_kitty') + print('import "kitty/tools/cli"') + print('func AddCloneSafeOpts(cmd *cli.Command) {') + completion_for_launch_wrappers('cmd') + print(''.join(CompletionSpec.from_string('type:file mime:text/* group:"Text files"').as_go_code('cmd.ArgCompleter', ' = '))) + print('}') def define_enum(package_name: str, type_name: str, items: str, underlying_type: str = 'uint') -> str: @@ -496,6 +549,7 @@ def main() -> None: f.write(generate_spinners()) update_completion() update_at_commands() + kitten_clis() print(json.dumps(changed, indent=2)) diff --git a/kittens/clipboard/main.py b/kittens/clipboard/main.py index 57b707456..dd7bd23ec 100644 --- a/kittens/clipboard/main.py +++ b/kittens/clipboard/main.py @@ -147,3 +147,4 @@ elif __name__ == '__doc__': cd['usage'] = usage cd['options'] = OPTIONS cd['help_text'] = help_text + cd['short_desc'] = 'Copy/paste with the system clipboard, even over SSH' diff --git a/kitty/data-types.c b/kitty/data-types.c index 351f443ab..9e802c3eb 100644 --- a/kitty/data-types.c +++ b/kitty/data-types.c @@ -216,6 +216,23 @@ get_docs_ref_map(PyObject *self UNUSED, PyObject *args UNUSED) { return PyBytes_FromStringAndSize(docs_ref_map, sizeof(docs_ref_map)); } +#include "wrapped_kitten_names_generated.h" + +static PyObject* +wrapped_kittens(PyObject *self UNUSED, PyObject *args UNUSED) { + PyObject *ans = PyFrozenSet_New(NULL); + if (ans != NULL) { + for (int i = 0; wrapped_kitten_names[i] != NULL; i++) { + PyObject *n = PyUnicode_FromString(wrapped_kitten_names[i]); + if (n == NULL) break; + if (PySet_Add(ans, n) != 0) { Py_DECREF(n); break; } + Py_DECREF(n); + } + } + if (PyErr_Occurred()) { Py_CLEAR(ans); } + return ans; +} + static PyMethodDef module_methods[] = { {"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""}, {"get_docs_ref_map", (PyCFunction)get_docs_ref_map, METH_NOARGS, ""}, @@ -234,6 +251,7 @@ static PyMethodDef module_methods[] = { {"locale_is_valid", (PyCFunction)locale_is_valid, METH_VARARGS, ""}, {"shm_open", (PyCFunction)py_shm_open, METH_VARARGS, ""}, {"shm_unlink", (PyCFunction)py_shm_unlink, METH_VARARGS, ""}, + {"wrapped_kitten_names", (PyCFunction)wrapped_kittens, METH_NOARGS, ""}, #ifdef __APPLE__ METHODB(user_cache_dir, METH_NOARGS), METHODB(process_group_map, METH_NOARGS), diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 031aa3fa6..ab27b4d40 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1,8 +1,8 @@ import termios from ctypes import Array, c_ubyte from typing import ( - Any, Callable, Dict, List, NewType, Optional, Tuple, TypedDict, - Union, Iterator + Any, Callable, Dict, FrozenSet, Iterator, List, NewType, Optional, Tuple, TypedDict, + Union, ) from kitty.boss import Boss @@ -1493,3 +1493,4 @@ def get_clipboard_mime(ct: int, mime: Optional[str], callback: Callable[[bytes], def run_with_activation_token(func: Callable[[str], None]) -> None: ... def make_x11_window_a_dock_window(x11_window_id: int, strut: Tuple[int, int, int, int, int, int, int, int, int, int, int, int]) -> None: ... def unicode_database_version() -> Tuple[int, int, int]: ... +def wrapped_kittens() -> FrozenSet[str]: ... diff --git a/setup.py b/setup.py index e32f29b59..7fce13503 100755 --- a/setup.py +++ b/setup.py @@ -860,11 +860,37 @@ def build_ref_map() -> str: return dest +@lru_cache +def wrapped_kittens() -> Sequence[str]: + with open('shell-integration/ssh/kitty') as f: + for line in f: + if line.startswith(' wrapped_kittens="'): + val = line.strip().partition('"')[2][:-1] + return tuple(sorted(filter(None, val.split()))) + raise Exception('Failed to read wrapped kittens from kitty wrapper script') + + +def build_wrapped_kittens() -> str: + h = 'static const char* wrapped_kitten_names[] = {\n' + h += ', '.join(f'"{x}"' for x in wrapped_kittens()) + h += ', NULL' + h += '\n};\n' + dest = 'kitty/wrapped_kitten_names_generated.h' + q = '' + with suppress(FileNotFoundError), open(dest) as f: + q = f.read() + if q != h: + with open(dest, 'w') as f: + f.write(h) + return dest + + def build(args: Options, native_optimizations: bool = True, call_init: bool = True) -> None: if call_init: init_env_from_args(args, native_optimizations) sources, headers = find_c_files() headers.append(build_ref_map()) + headers.append(build_wrapped_kittens()) compile_c_extension( kitty_env(), 'kitty/fast_data_types', args.compilation_database, sources, headers ) diff --git a/shell-integration/ssh/kitty b/shell-integration/ssh/kitty index d9a175036..f1925e804 100755 --- a/shell-integration/ssh/kitty +++ b/shell-integration/ssh/kitty @@ -22,6 +22,21 @@ exec_kitty() { die "Failed to execute kitty" } + +is_wrapped_kitten() { + wrapped_kittens="clipboard" + [ -n "$1" ] && { + case " $wrapped_kittens " in + *" $1 "*) printf "%s" "$1" ;; + esac + } +} + +test "(" "$1" = "+kitten" -a -n "$(is_wrapped_kitten "$2")" ")" -o "(" "$1" = "+" -a "$2" = "kitten" -a "$(is_wrapped_kitten "$3")" ")" && { + if [ "$1" = "+kitten" ]; then shift "1"; else shift "2"; fi + exec kitty-tool "$@" +} + lock_dir="" script_path="$(command readlink -f "$0" 2> /dev/null)" [ $? = 0 ] || script_path="$0" diff --git a/tools/cli/command.go b/tools/cli/command.go index fd1ac51f5..f232307fa 100644 --- a/tools/cli/command.go +++ b/tools/cli/command.go @@ -14,6 +14,8 @@ import ( var _ = fmt.Print +type RunFunc = func(cmd *Command, args []string) (int, error) + type Command struct { Name, Group string Usage, ShortDescription, HelpText string @@ -26,7 +28,7 @@ type Command struct { // If true subcommands are ignored unless they are the first non-option argument SubCommandMustBeFirst bool // The entry point for this command - Run func(cmd *Command, args []string) (int, error) + Run RunFunc // The completer for args ArgCompleter CompletionFunc // Stop completion processing at this arg num diff --git a/tools/cmd/clipboard/main.go b/tools/cmd/clipboard/main.go new file mode 100644 index 000000000..7fe3eea00 --- /dev/null +++ b/tools/cmd/clipboard/main.go @@ -0,0 +1,18 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package clipboard + +import ( + "fmt" + "kitty/tools/cli" +) + +var _ = fmt.Print + +func clipboard_main(cmd *cli.Command, args []string) (int, error) { + return 0, nil +} + +func EntryPoint(parent *cli.Command) { + create_cmd(parent, clipboard_main) +} diff --git a/tools/cmd/tool/main.go b/tools/cmd/tool/main.go index 36537a5d3..248ba2c43 100644 --- a/tools/cmd/tool/main.go +++ b/tools/cmd/tool/main.go @@ -4,8 +4,10 @@ package tool import ( "fmt" + "kitty/tools/cli" "kitty/tools/cmd/at" + "kitty/tools/cmd/clipboard" "kitty/tools/cmd/edit_in_kitty" "kitty/tools/cmd/update_self" ) @@ -21,4 +23,6 @@ func KittyToolEntryPoints(root *cli.Command) { update_self.EntryPoint(root) // edit-in-kitty edit_in_kitty.EntryPoint(root) + // clipboard + clipboard.EntryPoint(root) }