diff --git a/kittens/mouse_demo/main.py b/kittens/mouse_demo/main.py index 2d4e0dcb3..bf407d3f8 100644 --- a/kittens/mouse_demo/main.py +++ b/kittens/mouse_demo/main.py @@ -4,11 +4,33 @@ import sys from typing import List, Optional -from ..show_key.kitty_mode import format_mods +from kitty.key_encoding import ALT, CAPS_LOCK, CTRL, HYPER, META, NUM_LOCK, SHIFT, SUPER + from ..tui.handler import Handler from ..tui.loop import Loop, MouseEvent from ..tui.operations import MouseTracking +mod_names = { + SHIFT: 'Shift', + ALT: 'Alt', + CTRL: 'Ctrl', + SUPER: 'Super', + HYPER: 'Hyper', + META: 'Meta', + NUM_LOCK: 'NumLock', + CAPS_LOCK: 'CapsLock', +} + + +def format_mods(mods: int) -> str: + if not mods: + return '' + lmods = [] + for m, name in mod_names.items(): + if mods & m: + lmods.append(name) + return '+'.join(lmods) + class Mouse(Handler): mouse_tracking = MouseTracking.full diff --git a/kittens/show_key/kitty.go b/kittens/show_key/kitty.go new file mode 100644 index 000000000..480a31662 --- /dev/null +++ b/kittens/show_key/kitty.go @@ -0,0 +1,78 @@ +// License: GPLv3 Copyright: 2023, Kovid Goyal, + +package show_key + +import ( + "fmt" + "strings" + + "kitty/tools/cli/markup" + "kitty/tools/tui/loop" +) + +var _ = fmt.Print + +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) { + lp, err := loop.New(loop.FullKeyboardProtocol) + if err != nil { + return err + } + ctx := markup.New(true) + + lp.OnInitialize = func() (string, error) { + lp.SetCursorVisible(false) + lp.SetWindowTitle("kitty extended keyboard protocol demo") + lp.Println("Press any keys - Ctrl+C or Ctrl+D will terminate") + return "", nil + } + + lp.OnKeyEvent = func(e *loop.KeyEvent) (err error) { + e.Handled = true + if e.MatchesPressOrRepeat("ctrl+c") || e.MatchesPressOrRepeat("ctrl+d") { + lp.Quit(0) + return + } + mods := e.Mods.String() + if mods != "" { + mods += "+" + } + etype := e.Type.String() + key := e.Key + if key == " " { + key = "space" + } + key = mods + key + lp.Printf("%s %s %s\r\n", ctx.Green(key), ctx.Yellow(etype), e.Text) + lp.Println(ctx.Cyan(csi(e.CSI))) + if e.AlternateKey != "" || e.ShiftedKey != "" { + if e.ShiftedKey != "" { + lp.QueueWriteString(ctx.Dim("Shifted key: ")) + lp.QueueWriteString(e.ShiftedKey + " ") + } + if e.AlternateKey != "" { + lp.QueueWriteString(ctx.Dim("Alternate key: ")) + lp.QueueWriteString(e.AlternateKey + " ") + } + lp.Println() + } + lp.Println() + return + } + + err = lp.Run() + if err != nil { + return + } + ds := lp.DeathSignalName() + if ds != "" { + fmt.Println("Killed by signal: ", ds) + lp.KillIfSignalled() + return + } + + return +} diff --git a/kittens/show_key/kitty_mode.py b/kittens/show_key/kitty_mode.py deleted file mode 100644 index 80553d85e..000000000 --- a/kittens/show_key/kitty_mode.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 -# License: GPL v3 Copyright: 2018, Kovid Goyal - -from kittens.tui.handler import Handler -from kittens.tui.loop import Loop -from kitty.key_encoding import ALT, CAPS_LOCK, CTRL, HYPER, META, NUM_LOCK, PRESS, RELEASE, REPEAT, SHIFT, SUPER, KeyEvent, encode_key_event - -mod_names = { - SHIFT: 'Shift', - ALT: 'Alt', - CTRL: 'Ctrl', - SUPER: 'Super', - HYPER: 'Hyper', - META: 'Meta', - NUM_LOCK: 'NumLock', - CAPS_LOCK: 'CapsLock', -} - - -def format_mods(mods: int) -> str: - if not mods: - return '' - lmods = [] - for m, name in mod_names.items(): - if mods & m: - lmods.append(name) - return '+'.join(lmods) - - -class KeysHandler(Handler): - - def initialize(self) -> None: - self.cmd.set_window_title('kitty extended keyboard protocol demo') - self.cmd.set_cursor_visible(False) - self.print('Press any keys - Ctrl+C or Ctrl+D will terminate') - - def on_key_event(self, key_event: KeyEvent, in_bracketed_paste: bool = False) -> None: - etype = { - PRESS: 'PRESS', - REPEAT: 'REPEAT', - RELEASE: 'RELEASE' - }[key_event.type] - mods = format_mods(key_event.mods) - if mods: - mods += '+' - kk = key_event.key - if kk == ' ': - kk = 'SPACE' - key = f'{mods}{kk} ' - self.cmd.colored(key, 'green') - self.cmd.colored(etype + ' ', 'yellow') - self.cmd.styled(key_event.text, italic=True) - self.print() - rep = f'CSI {encode_key_event(key_event)[2:]}' - rep = rep.replace(';', ' ; ').replace(':', ' : ')[:-1] + ' ' + rep[-1] - self.cmd.styled(rep, fg='magenta') - if (key_event.shifted_key or key_event.alternate_key): - self.print() - if key_event.shifted_key: - self.cmd.colored('Shifted key: ', 'gray') - self.print(key_event.shifted_key + ' ', end='') - if key_event.alternate_key: - self.cmd.colored('Alternate key: ', 'gray') - self.print(key_event.alternate_key + ' ', end='') - self.print() - self.print() - - def on_interrupt(self) -> None: - self.quit_loop(0) - - def on_eot(self) -> None: - self.quit_loop(0) - - -def main() -> None: - loop = Loop() - handler = KeysHandler() - loop.loop(handler) - raise SystemExit(loop.return_code) diff --git a/kittens/show_key/legacy.go b/kittens/show_key/legacy.go new file mode 100644 index 000000000..ae7410a6d --- /dev/null +++ b/kittens/show_key/legacy.go @@ -0,0 +1,76 @@ +// License: GPLv3 Copyright: 2023, Kovid Goyal, + +package show_key + +import ( + "errors" + "fmt" + "io" + "kitty/tools/cli/markup" + "kitty/tools/tty" + "os" +) + +var _ = fmt.Print + +func print_key(buf []byte, ctx *markup.Context) { + const ctrl_keys = "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + unix := "" + send_text := "" + for _, ch := range buf { + switch { + case int(ch) < len(ctrl_keys): + unix += "^" + ctrl_keys[ch:ch+1] + case ch == 127: + unix += "^?" + default: + unix += string(rune(ch)) + } + } + for _, ch := range string(buf) { + q := fmt.Sprintf("%#v", string(ch)) + send_text += q[1 : len(q)-1] + } + os.Stdout.WriteString(unix + "\t\t") + os.Stdout.WriteString(ctx.Yellow(send_text) + "\r\n") +} + +func run_legacy_loop(opts *Options) (err error) { + term, err := tty.OpenControllingTerm(tty.SetRaw) + if err != nil { + return err + } + defer func() { + term.RestoreAndClose() + }() + if opts.KeyMode != "unchanged" { + os.Stdout.WriteString("\x1b[?1") + switch opts.KeyMode { + case "normal": + os.Stdout.WriteString("l") + default: + os.Stdout.WriteString("h") + } + defer func() { + os.Stdout.WriteString("\x1b[?1l") + }() + } + fmt.Print("Press any keys - Ctrl+D will terminate this program\r\n") + ctx := markup.New(true) + fmt.Print(ctx.Green("UNIX\t\tsend_text\r\n")) + buf := make([]byte, 64) + for { + n, err := term.Read(buf) + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return err + } + print_key(buf[:n], ctx) + if n == 1 && buf[0] == 4 { + break + } + } + return +} diff --git a/kittens/show_key/main.go b/kittens/show_key/main.go new file mode 100644 index 000000000..fba280f83 --- /dev/null +++ b/kittens/show_key/main.go @@ -0,0 +1,27 @@ +// License: GPLv3 Copyright: 2023, Kovid Goyal, + +package show_key + +import ( + "fmt" + + "kitty/tools/cli" +) + +var _ = fmt.Print + +func main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) { + if opts.KeyMode == "kitty" { + err = run_kitty_loop(opts) + } else { + err = run_legacy_loop(opts) + } + if err != nil { + rc = 1 + } + return +} + +func EntryPoint(parent *cli.Command) { + create_cmd(parent, main) +} diff --git a/kittens/show_key/main.py b/kittens/show_key/main.py index 4b39be125..0b6c22e4a 100644 --- a/kittens/show_key/main.py +++ b/kittens/show_key/main.py @@ -2,56 +2,9 @@ # License: GPLv3 Copyright: 2021, Kovid Goyal -import os import sys from typing import List -from kittens.tui.operations import raw_mode, styled -from kitty.cli import parse_args -from kitty.cli_stub import ShowKeyCLIOptions - -ctrl_keys = '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_' - - -def print_key(raw: bytearray) -> None: - unix = '' - for ch in raw: - if ch < len(ctrl_keys): - unix += f'^{ctrl_keys[ch]}' - elif ch == 127: - unix += '^?' - else: - unix += chr(ch) - print(unix + '\t\t', end='') - for ch in raw: - x = chr(ch).encode('utf-8') - print(styled(repr(x)[2:-1], fg='yellow'), end='') - print(end='\r\n', flush=True) - - -def read_keys() -> None: - fd = sys.stdin.fileno() - while True: - try: - raw = bytearray(os.read(fd, 64)) - except OSError as err: - print(err, file=sys.stderr, flush=True) - break - if not raw: - break - print_key(raw) - if len(raw) == 1 and raw[0] == 4: - break - - -def legacy_main() -> None: - print('Press any keys - Ctrl+D will terminate this program', end='\r\n', flush=True) - print(styled('UNIX', italic=True, fg='green'), styled('send_text', italic=True, fg='green'), sep='\t\t', end='\r\n') - - with raw_mode(): - read_keys() - - OPTIONS = r''' --key-mode -m default=normal @@ -66,17 +19,7 @@ usage = '' def main(args: List[str]) -> None: - cli_opts, items = parse_args(args[1:], OPTIONS, '', help_text, 'kitty +kitten show_key', result_class=ShowKeyCLIOptions) - if cli_opts.key_mode == 'kitty': - from .kitty_mode import main as kitty_main - return kitty_main() - if cli_opts.key_mode != 'unchanged': - print(end='\x1b[?1' + ('l' if cli_opts.key_mode == 'normal' else 'h'), flush=True) - try: - return legacy_main() - finally: - if cli_opts.key_mode != 'unchanged': - print(end='\x1b[?1l', flush=True) + raise SystemExit('This should be reun as kitten show_key') if __name__ == '__main__': @@ -86,3 +29,4 @@ elif __name__ == '__doc__': cd['usage'] = usage cd['options'] = OPTIONS cd['help_text'] = help_text + cd['short_desc'] = help_text diff --git a/shell-integration/ssh/kitty b/shell-integration/ssh/kitty index 06f418adf..264e9b823 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" + wrapped_kittens="clipboard icat hyperlinked_grep ask hints unicode_input ssh themes diff show_key" [ -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 89205736c..01eff9579 100644 --- a/tools/cmd/tool/main.go +++ b/tools/cmd/tool/main.go @@ -11,6 +11,7 @@ import ( "kitty/kittens/hints" "kitty/kittens/hyperlinked_grep" "kitty/kittens/icat" + "kitty/kittens/show_key" "kitty/kittens/ssh" "kitty/kittens/themes" "kitty/kittens/unicode_input" @@ -41,6 +42,8 @@ func KittyToolEntryPoints(root *cli.Command) { ssh.EntryPoint(root) // unicode_input unicode_input.EntryPoint(root) + // show_key + show_key.EntryPoint(root) // hyperlinked_grep hyperlinked_grep.EntryPoint(root) // ask diff --git a/tools/tui/loop/key-encoding.go b/tools/tui/loop/key-encoding.go index a7be0c916..07a65fe17 100644 --- a/tools/tui/loop/key-encoding.go +++ b/tools/tui/loop/key-encoding.go @@ -105,6 +105,9 @@ type KeyEvent struct { AlternateKey string Text string Handled bool + + // The CSI string this key event was decoded from. Empty if not decoded from CSI. + CSI string } func (self *KeyEvent) String() string { @@ -133,6 +136,7 @@ func KeyEventFromCSI(csi string) *KeyEvent { if len(csi) == 0 { return nil } + orig_csi := csi last_char := csi[len(csi)-1:] if !strings.Contains("u~ABCDEHFPQRS", last_char) || (last_char == "~" && (csi == "200~" || csi == "201~")) { return nil @@ -165,7 +169,7 @@ func KeyEventFromCSI(csi string) *KeyEvent { if len(sections) > 2 { third_section = get_sub_sections(sections[2], 0) } - var ans = KeyEvent{Type: PRESS} + var ans = KeyEvent{Type: PRESS, CSI: orig_csi} var keynum int if val, ok := letter_trailer_to_csi_number_map[last_char]; ok { keynum = val