Move the query_terminal implementation to Go

This commit is contained in:
Kovid Goyal
2024-05-08 13:00:40 +05:30
parent f0cac79143
commit 2be91d73dd
9 changed files with 98 additions and 60 deletions

View File

@@ -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 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. depending on the kitten, you can do so, by processing the same escape codes.
Search `this page <https://invisible-island.net/xterm/ctlseqs/ctlseqs.html>`__ Search `this page <https://invisible-island.net/xterm/ctlseqs/ctlseqs.html>`__
for *XTGETTCAP* to see the syntax for the escape code and read the source of for *XTGETTCAP* to see the syntax for the escape code. The kitty specific keys
this kitten to find the values of the keys for the various queries. are all documented below, when sent via escape code they must be prefixed with
``kitty-query-``.
.. include:: ../generated/cli-kitten-query_terminal.rst .. include:: ../generated/cli-kitten-query_terminal.rst

View File

@@ -570,10 +570,12 @@ def load_ref_map() -> Dict[str, Dict[str, str]]:
def generate_constants() -> str: def generate_constants() -> str:
from kittens.hints.main import DEFAULT_REGEX 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.config import option_names_for_completion
from kitty.fast_data_types import FILE_TRANSFER_CODE from kitty.fast_data_types import FILE_TRANSFER_CODE
from kitty.options.utils import allowed_shell_integration_values, url_style_map from kitty.options.utils import allowed_shell_integration_values, url_style_map
del sys.modules['kittens.hints.main'] del sys.modules['kittens.hints.main']
del sys.modules['kittens.query_terminal.main']
ref_map = load_ref_map() ref_map = load_ref_map()
with open('kitty/data-types.h') as dt: with open('kitty/data-types.h') as dt:
m = re.search(r'^#define IMAGE_PLACEHOLDER_CHAR (\S+)', dt.read(), flags=re.M) 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) url_prefixes = ','.join(f'"{x}"' for x in Options.url_prefixes)
option_names = '`' + '\n'.join(option_names_for_completion()) + '`' option_names = '`' + '\n'.join(option_names_for_completion()) + '`'
url_style = {v:k for k, v in url_style_map.items()}[Options.url_style] 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'''\ return f'''\
package kitty 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 RefMap = map[string]string{serialize_go_dict(ref_map['ref'])}
var DocTitleMap = map[string]string{serialize_go_dict(ref_map['doc'])} var DocTitleMap = map[string]string{serialize_go_dict(ref_map['doc'])}
var AllowedShellIntegrationValues = []string{{ {str(sorted(allowed_shell_integration_values))[1:-1].replace("'", '"')} }} var AllowedShellIntegrationValues = []string{{ {str(sorted(allowed_shell_integration_values))[1:-1].replace("'", '"')} }}
var QueryNames = []string{{ {query_names} }}
var KittyConfigDefaults = struct {{ var KittyConfigDefaults = struct {{
Term, Shell_integration, Select_by_word_characters, Url_excluded_characters, Shell string Term, Shell_integration, Select_by_word_characters, Url_excluded_characters, Shell string
Wheel_scroll_multiplier int Wheel_scroll_multiplier int

View File

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

View File

@@ -5,14 +5,11 @@ import re
import sys import sys
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from contextlib import suppress 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.constants import appname, str_version
from kitty.options.types import Options from kitty.options.types import Options
from kitty.terminfo import names from kitty.terminfo import names
from kitty.utils import TTYIO
class Query: 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) 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: def options_spec() -> str:
return '''\ return '''\
--wait-for --wait-for
@@ -289,29 +261,8 @@ Available queries are:
usage = '[query1 query2 ...]' 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__': if __name__ == '__main__':
main() raise SystemExit('Should be run as kitten hints')
elif __name__ == '__doc__': elif __name__ == '__doc__':
cd = sys.cli_docs # type: ignore cd = sys.cli_docs # type: ignore
cd['usage'] = usage cd['usage'] = usage

View File

@@ -16,7 +16,7 @@ func csi(csi string) string {
return "CSI " + strings.NewReplacer(":", " : ", ";", " ; ").Replace(csi[:len(csi)-1]) + " " + csi[len(csi)-1:] 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) lp, err := loop.New(loop.FullKeyboardProtocol)
if err != nil { if err != nil {
return err return err

View File

@@ -12,7 +12,7 @@ class CLIOptions:
LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions
HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions
QueryTerminalCLIOptions = BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions
ThemesCLIOptions = TransferCLIOptions = LoadConfigRCOptions = ActionRCOptions = CLIOptions ThemesCLIOptions = TransferCLIOptions = LoadConfigRCOptions = ActionRCOptions = CLIOptions
@@ -57,9 +57,6 @@ def generate_stub() -> None:
from kittens.icat.main import OPTIONS from kittens.icat.main import OPTIONS
do(OPTIONS, 'IcatCLIOptions') do(OPTIONS, 'IcatCLIOptions')
from kittens.query_terminal.main import options_spec
do(options_spec(), 'QueryTerminalCLIOptions')
from kittens.panel.main import OPTIONS from kittens.panel.main import OPTIONS
do(OPTIONS(), 'PanelCLIOptions') do(OPTIONS(), 'PanelCLIOptions')

View File

@@ -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 <https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html>` combination. The text to be sent decodes :link:`ANSI C escapes <https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html>`
so you can use escapes like :code:`\\\\e` to send control codes or :code:`\\\\u21fb` to send 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 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. codes you want to emulate.
The first argument to :code:`send_text` is the keyboard modes in which to The first argument to :code:`send_text` is the keyboard modes in which to

View File

@@ -24,7 +24,7 @@ exec_kitty() {
is_wrapped_kitten() { 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" ] && { [ -n "$1" ] && {
case " $wrapped_kittens " in case " $wrapped_kittens " in
*" $1 "*) printf "%s" "$1" ;; *" $1 "*) printf "%s" "$1" ;;

View File

@@ -12,6 +12,7 @@ import (
"kitty/kittens/hints" "kitty/kittens/hints"
"kitty/kittens/hyperlinked_grep" "kitty/kittens/hyperlinked_grep"
"kitty/kittens/icat" "kitty/kittens/icat"
"kitty/kittens/query_terminal"
"kitty/kittens/show_key" "kitty/kittens/show_key"
"kitty/kittens/ssh" "kitty/kittens/ssh"
"kitty/kittens/themes" "kitty/kittens/themes"
@@ -79,6 +80,8 @@ func KittyToolEntryPoints(root *cli.Command) {
show_error.EntryPoint(root) show_error.EntryPoint(root)
// choose-fonts // choose-fonts
choose_fonts.EntryPoint(root) choose_fonts.EntryPoint(root)
// query-terminal
query_terminal.EntryPoint(root)
// __pytest__ // __pytest__
pytest.EntryPoint(root) pytest.EntryPoint(root)
// __hold_till_enter__ // __hold_till_enter__