show_key kitten: In kitty mode show the actual bytes sent by the terminal rather than a re-encoding of the parsed key event

Also port the kitten to Go
This commit is contained in:
Kovid Goyal 2023-04-26 21:48:53 +05:30
parent 7b6d11fd1e
commit 019359b219
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
9 changed files with 215 additions and 140 deletions

View File

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

78
kittens/show_key/kitty.go Normal file
View File

@ -0,0 +1,78 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
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
}

View File

@ -1,79 +0,0 @@
#!/usr/bin/env python3
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
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)

View File

@ -0,0 +1,76 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
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
}

27
kittens/show_key/main.go Normal file
View File

@ -0,0 +1,27 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
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)
}

View File

@ -2,56 +2,9 @@
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
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

View File

@ -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" ;;

View File

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

View File

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