diff --git a/docs/kittens/query_terminal.rst b/docs/kittens/query_terminal.rst index 91f41f4d5..9bbbddcdf 100644 --- a/docs/kittens/query_terminal.rst +++ b/docs/kittens/query_terminal.rst @@ -17,8 +17,9 @@ slow, since it requires a roundtrip to the terminal emulator and back. If you want to do some of the same querying in your terminal program without depending on the kitten, you can do so, by processing the same escape codes. Search `this page `__ -for *XTGETTCAP* to see the syntax for the escape code and read the source of -this kitten to find the values of the keys for the various queries. +for *XTGETTCAP* to see the syntax for the escape code. The kitty specific keys +are all documented below, when sent via escape code they must be prefixed with +``kitty-query-``. .. include:: ../generated/cli-kitten-query_terminal.rst diff --git a/gen/go_code.py b/gen/go_code.py index 6757c19e0..8dd0b9cba 100755 --- a/gen/go_code.py +++ b/gen/go_code.py @@ -570,10 +570,12 @@ def load_ref_map() -> Dict[str, Dict[str, str]]: def generate_constants() -> str: from kittens.hints.main import DEFAULT_REGEX + from kittens.query_terminal.main import all_queries from kitty.config import option_names_for_completion from kitty.fast_data_types import FILE_TRANSFER_CODE from kitty.options.utils import allowed_shell_integration_values, url_style_map del sys.modules['kittens.hints.main'] + del sys.modules['kittens.query_terminal.main'] ref_map = load_ref_map() with open('kitty/data-types.h') as dt: m = re.search(r'^#define IMAGE_PLACEHOLDER_CHAR (\S+)', dt.read(), flags=re.M) @@ -583,6 +585,7 @@ def generate_constants() -> str: url_prefixes = ','.join(f'"{x}"' for x in Options.url_prefixes) option_names = '`' + '\n'.join(option_names_for_completion()) + '`' url_style = {v:k for k, v in url_style_map.items()}[Options.url_style] + query_names = ', '.join(f'"{name}"' for name in all_queries) return f'''\ package kitty @@ -611,6 +614,7 @@ var ConfigModMap = map[string]uint16{serialize_go_dict(config_mod_map)} var RefMap = map[string]string{serialize_go_dict(ref_map['ref'])} var DocTitleMap = map[string]string{serialize_go_dict(ref_map['doc'])} var AllowedShellIntegrationValues = []string{{ {str(sorted(allowed_shell_integration_values))[1:-1].replace("'", '"')} }} +var QueryNames = []string{{ {query_names} }} var KittyConfigDefaults = struct {{ Term, Shell_integration, Select_by_word_characters, Url_excluded_characters, Shell string Wheel_scroll_multiplier int diff --git a/kittens/query_terminal/main.go b/kittens/query_terminal/main.go new file mode 100644 index 000000000..1fe0c8ac5 --- /dev/null +++ b/kittens/query_terminal/main.go @@ -0,0 +1,82 @@ +package query_terminal + +import ( + "bytes" + "fmt" + "kitty" + "os" + "slices" + "strings" + "time" + + "kitty/tools/cli" + "kitty/tools/tui/loop" +) + +var _ = fmt.Print + +func main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) { + queries := kitty.QueryNames + if len(args) > 0 && !slices.Contains(args, "all") { + queries = make([]string, len(args)) + for i, x := range args { + if !slices.Contains(kitty.QueryNames, x) { + return 1, fmt.Errorf("Unknown query: %s", x) + } + queries[i] = x + } + } + lp, err := loop.New(loop.NoAlternateScreen, loop.NoKeyboardStateChange, loop.NoMouseTracking, loop.NoRestoreColors) + if err != nil { + return 1, err + } + timed_out := false + lp.OnInitialize = func() (string, error) { + lp.QueryTerminal(queries...) + lp.QueueWriteString("\x1b[c") + _, err := lp.AddTimer(time.Duration(opts.WaitFor*float64(time.Second)), false, func(timer_id loop.IdType) error { + timed_out = true + lp.Quit(1) + return nil + }) + + return "", err + } + buf := strings.Builder{} + + lp.OnQueryResponse = func(key, val string, found bool) error { + if found { + fmt.Fprintf(&buf, "%s: %s\n", key, val) + } else { + fmt.Fprintf(&buf, "%s:\n", key) + } + return nil + } + lp.OnEscapeCode = func(typ loop.EscapeCodeType, data []byte) error { + if typ == loop.CSI && bytes.HasSuffix(data, []byte{'c'}) { + lp.Quit(0) + } + return nil + } + err = lp.Run() + rc = lp.ExitCode() + if err != nil { + return 1, err + } + ds := lp.DeathSignalName() + if ds != "" { + fmt.Println("Killed by signal: ", ds) + lp.KillIfSignalled() + return + } + os.Stdout.WriteString(buf.String()) + + if timed_out { + return 1, fmt.Errorf("timed out waiting for response from terminal") + } + return +} + +func EntryPoint(parent *cli.Command) { + create_cmd(parent, main) +} diff --git a/kittens/query_terminal/main.py b/kittens/query_terminal/main.py index 8a5aefee0..06f30d884 100644 --- a/kittens/query_terminal/main.py +++ b/kittens/query_terminal/main.py @@ -5,14 +5,11 @@ import re import sys from binascii import hexlify, unhexlify from contextlib import suppress -from typing import Dict, Iterable, List, Optional, Type +from typing import Dict, Optional, Type -from kitty.cli import parse_args -from kitty.cli_stub import QueryTerminalCLIOptions from kitty.constants import appname, str_version from kitty.options.types import Options from kitty.terminfo import names -from kitty.utils import TTYIO class Query: @@ -228,31 +225,6 @@ def get_result(name: str, window_id: int, os_window_id: int) -> Optional[str]: return q.get_result(get_options(), window_id, os_window_id) -def do_queries(queries: Iterable[str], cli_opts: QueryTerminalCLIOptions) -> Dict[str, str]: - actions = tuple(all_queries[x]() for x in queries) - qstring = ''.join(a.query_code() for a in actions) - received = b'' - pat = re.compile(rb'\x1b\[\?.+?c') - - def more_needed(data: bytes) -> bool: - nonlocal received - received += data - has_da1_response = pat.search(received) is not None - if has_da1_response: - return False - for a in actions: - if a.more_needed(received): - return True - return has_da1_response - - with TTYIO() as ttyio: - ttyio.send(qstring) - ttyio.send('\x1b[c') # DA1 query https://vt100.net/docs/vt510-rm/DA1.html - ttyio.recv(more_needed, timeout=cli_opts.wait_for) - - return {a.name: a.output_line() for a in actions} - - def options_spec() -> str: return '''\ --wait-for @@ -289,29 +261,8 @@ Available queries are: usage = '[query1 query2 ...]' -def main(args: List[str] = sys.argv) -> None: - cli_opts, items_ = parse_args( - args[1:], - options_spec, - usage, - help_text, - f'{appname} +kitten query_terminal', - result_class=QueryTerminalCLIOptions - ) - queries: List[str] = list(items_) - if 'all' in queries or not queries: - queries = sorted(all_queries) - else: - extra = frozenset(queries) - frozenset(all_queries) - if extra: - raise SystemExit(f'Unknown queries: {", ".join(extra)}') - - for key, val in do_queries(queries, cli_opts).items(): - print(f'{key}:', val) - - if __name__ == '__main__': - main() + raise SystemExit('Should be run as kitten hints') elif __name__ == '__doc__': cd = sys.cli_docs # type: ignore cd['usage'] = usage diff --git a/kittens/show_key/kitty.go b/kittens/show_key/kitty.go index a9e26656c..278a740c5 100644 --- a/kittens/show_key/kitty.go +++ b/kittens/show_key/kitty.go @@ -16,7 +16,7 @@ func csi(csi string) string { return "CSI " + strings.NewReplacer(":", " : ", ";", " ; ").Replace(csi[:len(csi)-1]) + " " + csi[len(csi)-1:] } -func run_kitty_loop(opts *Options) (err error) { +func run_kitty_loop(_ *Options) (err error) { lp, err := loop.New(loop.FullKeyboardProtocol) if err != nil { return err diff --git a/kitty/cli_stub.py b/kitty/cli_stub.py index a55b7dcf7..7ac3f5bcc 100644 --- a/kitty/cli_stub.py +++ b/kitty/cli_stub.py @@ -12,7 +12,7 @@ class CLIOptions: LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions -QueryTerminalCLIOptions = BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions +BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions ThemesCLIOptions = TransferCLIOptions = LoadConfigRCOptions = ActionRCOptions = CLIOptions @@ -57,9 +57,6 @@ def generate_stub() -> None: from kittens.icat.main import OPTIONS do(OPTIONS, 'IcatCLIOptions') - from kittens.query_terminal.main import options_spec - do(options_spec(), 'QueryTerminalCLIOptions') - from kittens.panel.main import OPTIONS do(OPTIONS(), 'PanelCLIOptions') diff --git a/kitty/options/definition.py b/kitty/options/definition.py index de49a714a..1880122b4 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -4279,7 +4279,7 @@ This will send "Special text" when you press the :kbd:`Ctrl+Alt+A` key combination. The text to be sent decodes :link:`ANSI C escapes ` so you can use escapes like :code:`\\\\e` to send control codes or :code:`\\\\u21fb` to send Unicode characters (or you can just input the Unicode characters directly as -UTF-8 text). You can use ``kitten show_key`` to get the key escape +UTF-8 text). You can use ``kitten show-key`` to get the key escape codes you want to emulate. The first argument to :code:`send_text` is the keyboard modes in which to diff --git a/shell-integration/ssh/kitty b/shell-integration/ssh/kitty index cd2b5600f..8bb0a9176 100755 --- a/shell-integration/ssh/kitty +++ b/shell-integration/ssh/kitty @@ -24,7 +24,7 @@ exec_kitty() { is_wrapped_kitten() { - wrapped_kittens="clipboard icat hyperlinked_grep ask hints unicode_input ssh themes diff show_key transfer" + wrapped_kittens="clipboard icat hyperlinked_grep ask hints unicode_input ssh themes diff show_key transfer query_terminal" [ -n "$1" ] && { case " $wrapped_kittens " in *" $1 "*) printf "%s" "$1" ;; diff --git a/tools/cmd/tool/main.go b/tools/cmd/tool/main.go index 413a9fa18..1ce84014d 100644 --- a/tools/cmd/tool/main.go +++ b/tools/cmd/tool/main.go @@ -12,6 +12,7 @@ import ( "kitty/kittens/hints" "kitty/kittens/hyperlinked_grep" "kitty/kittens/icat" + "kitty/kittens/query_terminal" "kitty/kittens/show_key" "kitty/kittens/ssh" "kitty/kittens/themes" @@ -79,6 +80,8 @@ func KittyToolEntryPoints(root *cli.Command) { show_error.EntryPoint(root) // choose-fonts choose_fonts.EntryPoint(root) + // query-terminal + query_terminal.EntryPoint(root) // __pytest__ pytest.EntryPoint(root) // __hold_till_enter__