mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
428 lines
10 KiB
Go
428 lines
10 KiB
Go
package tui
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Key is a parsed keypress.
|
|
type Key struct {
|
|
Kind KeyKind
|
|
Rune rune // for KeyRune
|
|
Paste string // for KeyPaste
|
|
Ctrl bool
|
|
Alt bool
|
|
Shift bool
|
|
Super bool
|
|
}
|
|
|
|
type KeyKind int
|
|
|
|
const (
|
|
KeyRune KeyKind = iota
|
|
KeyEnter
|
|
KeyBackspace
|
|
KeyTab
|
|
KeyShiftTab
|
|
KeyEsc
|
|
KeyUp
|
|
KeyDown
|
|
KeyLeft
|
|
KeyRight
|
|
KeyHome
|
|
KeyEnd
|
|
KeyPageUp
|
|
KeyPageDown
|
|
KeyDelete
|
|
KeyCtrlC
|
|
KeyCtrlD
|
|
KeyCtrlL
|
|
KeyCtrlU
|
|
KeyCtrlK
|
|
KeyCtrlA
|
|
KeyCtrlE
|
|
KeyCtrlW
|
|
KeyCtrlO
|
|
KeyPaste
|
|
KeyPasteClipboard
|
|
KeyMouseWheelUp
|
|
KeyMouseWheelDown
|
|
KeyUnknown
|
|
)
|
|
|
|
// Reader parses a byte stream into Key events. It understands basic
|
|
// xterm escape sequences and bracketed paste.
|
|
type Reader struct {
|
|
src func() (byte, error)
|
|
peek func(time.Duration) (byte, bool, error) // optional; may be nil
|
|
}
|
|
|
|
// NewReader returns a Reader that pulls bytes from read.
|
|
func NewReader(read func() (byte, error)) *Reader { return &Reader{src: read} }
|
|
|
|
// NewReaderWithPeek returns a Reader that pulls bytes from read and uses
|
|
// peek to disambiguate bare Esc from the start of an escape sequence.
|
|
func NewReaderWithPeek(read func() (byte, error), peek func(time.Duration) (byte, bool, error)) *Reader {
|
|
return &Reader{src: read, peek: peek}
|
|
}
|
|
|
|
// Read returns the next parsed Key.
|
|
func (r *Reader) Read() (Key, error) {
|
|
b, err := r.src()
|
|
if err != nil {
|
|
return Key{}, err
|
|
}
|
|
switch {
|
|
case b == 0x03:
|
|
return Key{Kind: KeyCtrlC}, nil
|
|
case b == 0x04:
|
|
return Key{Kind: KeyCtrlD}, nil
|
|
case b == 0x0c:
|
|
return Key{Kind: KeyCtrlL}, nil
|
|
case b == 0x15:
|
|
return Key{Kind: KeyCtrlU}, nil
|
|
case b == 0x0b:
|
|
return Key{Kind: KeyCtrlK}, nil
|
|
case b == 0x01:
|
|
return Key{Kind: KeyCtrlA}, nil
|
|
case b == 0x05:
|
|
return Key{Kind: KeyCtrlE}, nil
|
|
case b == 0x17:
|
|
return Key{Kind: KeyCtrlW}, nil
|
|
case b == 0x0f:
|
|
return Key{Kind: KeyCtrlO}, nil
|
|
case b == 0x16:
|
|
return Key{Kind: KeyPasteClipboard, Ctrl: true}, nil
|
|
case b == '\r', b == '\n':
|
|
return Key{Kind: KeyEnter}, nil
|
|
case b == '\t':
|
|
return Key{Kind: KeyTab}, nil
|
|
case b == 0x7f, b == 0x08:
|
|
return Key{Kind: KeyBackspace}, nil
|
|
case b == 0x1b:
|
|
return r.readEscape()
|
|
case b < 0x20:
|
|
return Key{Kind: KeyUnknown}, nil
|
|
}
|
|
// UTF-8 multibyte?
|
|
if b < 0x80 {
|
|
return Key{Kind: KeyRune, Rune: rune(b)}, nil
|
|
}
|
|
// Decode UTF-8 (up to 4 bytes).
|
|
n := utf8Len(b)
|
|
buf := []byte{b}
|
|
for i := 1; i < n; i++ {
|
|
bb, err := r.src()
|
|
if err != nil {
|
|
return Key{}, err
|
|
}
|
|
buf = append(buf, bb)
|
|
}
|
|
rn, _ := decodeRune(buf)
|
|
return Key{Kind: KeyRune, Rune: rn}, nil
|
|
}
|
|
|
|
func utf8Len(b byte) int {
|
|
switch {
|
|
case b&0xe0 == 0xc0:
|
|
return 2
|
|
case b&0xf0 == 0xe0:
|
|
return 3
|
|
case b&0xf8 == 0xf0:
|
|
return 4
|
|
}
|
|
return 1
|
|
}
|
|
|
|
func decodeRune(b []byte) (rune, int) {
|
|
// Minimal decoder; invalid runes become U+FFFD.
|
|
if len(b) == 1 {
|
|
return rune(b[0]), 1
|
|
}
|
|
var r rune
|
|
switch len(b) {
|
|
case 2:
|
|
r = rune(b[0]&0x1f)<<6 | rune(b[1]&0x3f)
|
|
case 3:
|
|
r = rune(b[0]&0x0f)<<12 | rune(b[1]&0x3f)<<6 | rune(b[2]&0x3f)
|
|
case 4:
|
|
r = rune(b[0]&0x07)<<18 | rune(b[1]&0x3f)<<12 | rune(b[2]&0x3f)<<6 | rune(b[3]&0x3f)
|
|
default:
|
|
r = 0xFFFD
|
|
}
|
|
return r, len(b)
|
|
}
|
|
|
|
// readEscape handles sequences starting with 0x1b.
|
|
func (r *Reader) readEscape() (Key, error) {
|
|
// Bare ESC: maybe followed by another byte within a short window.
|
|
b, have, err := r.readEscapeNext(50 * time.Millisecond)
|
|
if err != nil || !have {
|
|
return Key{Kind: KeyEsc}, nil
|
|
}
|
|
switch b {
|
|
case '[':
|
|
return r.readCSI()
|
|
case 'O':
|
|
// SS3 sequences (function keys in some terminals).
|
|
c, err := r.src()
|
|
if err != nil {
|
|
return Key{}, err
|
|
}
|
|
switch c {
|
|
case 'H':
|
|
return Key{Kind: KeyHome}, nil
|
|
case 'F':
|
|
return Key{Kind: KeyEnd}, nil
|
|
}
|
|
return Key{Kind: KeyUnknown}, nil
|
|
case 0x7f, 0x08:
|
|
// Alt+Backspace (Option+Delete on macOS) — most terminals send
|
|
// ESC + DEL for this. Surface as a dedicated "alt backspace"
|
|
// so the editor can map it to delete-word.
|
|
return Key{Kind: KeyBackspace, Alt: true}, nil
|
|
case 'b':
|
|
// Emacs-style word-left, also emitted by some terminals for
|
|
// Option+LeftArrow.
|
|
return Key{Kind: KeyLeft, Alt: true}, nil
|
|
case 'f':
|
|
// Emacs-style word-right, also emitted for Option+RightArrow.
|
|
return Key{Kind: KeyRight, Alt: true}, nil
|
|
default:
|
|
// Alt+<char>
|
|
if b < 0x80 {
|
|
return Key{Kind: KeyRune, Rune: rune(b), Alt: true}, nil
|
|
}
|
|
}
|
|
return Key{Kind: KeyUnknown}, nil
|
|
}
|
|
|
|
// readEscapeNext tries to read one byte within d. If peek is available
|
|
// we use it (true non-blocking). Otherwise we fall back to a blocking
|
|
// read, which means bare Esc is only detected after the next keystroke.
|
|
func (r *Reader) readEscapeNext(d time.Duration) (byte, bool, error) {
|
|
if r.peek != nil {
|
|
return r.peek(d)
|
|
}
|
|
b, err := r.src()
|
|
if err != nil {
|
|
return 0, false, err
|
|
}
|
|
return b, true, nil
|
|
}
|
|
|
|
// readCSI parses a CSI sequence after ESC [.
|
|
func (r *Reader) readCSI() (Key, error) {
|
|
var params []byte
|
|
for {
|
|
c, err := r.src()
|
|
if err != nil {
|
|
return Key{}, err
|
|
}
|
|
if c >= 0x30 && c <= 0x3f {
|
|
params = append(params, c)
|
|
continue
|
|
}
|
|
// Final byte.
|
|
return r.dispatchCSI(string(params), c), nil
|
|
}
|
|
}
|
|
|
|
func (r *Reader) dispatchCSI(params string, final byte) Key {
|
|
// SGR mouse mode: CSI < button ; x ; y M/m. Wheel events use
|
|
// button codes 64 (up) and 65 (down). We ignore coordinates for
|
|
// now; the chat view only needs scroll direction.
|
|
if strings.HasPrefix(params, "<") && (final == 'M' || final == 'm') {
|
|
parts := strings.Split(strings.TrimPrefix(params, "<"), ";")
|
|
if len(parts) >= 1 {
|
|
switch parts[0] {
|
|
case "64":
|
|
return Key{Kind: KeyMouseWheelUp}
|
|
case "65":
|
|
return Key{Kind: KeyMouseWheelDown}
|
|
}
|
|
}
|
|
return Key{Kind: KeyUnknown}
|
|
}
|
|
|
|
shift, alt, super := parseCSIModifiers(params)
|
|
if final == 'u' {
|
|
if key, ok := parseCSIU(params); ok {
|
|
return key
|
|
}
|
|
}
|
|
if final == '~' {
|
|
if key, ok := parseModifyOtherKeys(params); ok {
|
|
return key
|
|
}
|
|
}
|
|
switch final {
|
|
case 'A':
|
|
return Key{Kind: KeyUp, Alt: alt, Shift: shift, Super: super}
|
|
case 'B':
|
|
return Key{Kind: KeyDown, Alt: alt, Shift: shift, Super: super}
|
|
case 'C':
|
|
return Key{Kind: KeyRight, Alt: alt, Shift: shift, Super: super}
|
|
case 'D':
|
|
return Key{Kind: KeyLeft, Alt: alt, Shift: shift, Super: super}
|
|
case 'H':
|
|
return Key{Kind: KeyHome}
|
|
case 'F':
|
|
return Key{Kind: KeyEnd}
|
|
case 'Z':
|
|
return Key{Kind: KeyShiftTab}
|
|
case '~':
|
|
switch params {
|
|
case "3":
|
|
return Key{Kind: KeyDelete}
|
|
case "5":
|
|
return Key{Kind: KeyPageUp}
|
|
case "6":
|
|
return Key{Kind: KeyPageDown}
|
|
case "200":
|
|
// Start of bracketed paste.
|
|
return r.readPaste()
|
|
}
|
|
}
|
|
return Key{Kind: KeyUnknown}
|
|
}
|
|
|
|
func parseCSIModifiers(params string) (shift, alt, super bool) {
|
|
if params == "" {
|
|
return false, false, false
|
|
}
|
|
i := strings.LastIndexByte(params, ';')
|
|
if i < 0 || i+1 >= len(params) {
|
|
return false, false, false
|
|
}
|
|
mod, ok := parseModifierParam(params[i+1:])
|
|
if !ok {
|
|
return false, false, false
|
|
}
|
|
shift, alt, _, super = modifierBits(mod)
|
|
return shift, alt, super
|
|
}
|
|
|
|
func parseCSIU(params string) (Key, bool) {
|
|
parts := strings.Split(params, ";")
|
|
if len(parts) == 0 {
|
|
return Key{}, false
|
|
}
|
|
code, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return Key{}, false
|
|
}
|
|
mod := 1
|
|
if len(parts) >= 2 {
|
|
var ok bool
|
|
mod, ok = parseModifierParam(parts[1])
|
|
if !ok {
|
|
return Key{}, false
|
|
}
|
|
}
|
|
return keyFromModifiedCode(code, mod)
|
|
}
|
|
|
|
func parseModifyOtherKeys(params string) (Key, bool) {
|
|
parts := strings.Split(params, ";")
|
|
if len(parts) != 3 || parts[0] != "27" {
|
|
return Key{}, false
|
|
}
|
|
mod, ok := parseModifierParam(parts[1])
|
|
if !ok {
|
|
return Key{}, false
|
|
}
|
|
code, err := strconv.Atoi(parts[2])
|
|
if err != nil {
|
|
return Key{}, false
|
|
}
|
|
return keyFromModifiedCode(code, mod)
|
|
}
|
|
|
|
func parseModifierParam(s string) (int, bool) {
|
|
if i := strings.IndexByte(s, ':'); i >= 0 {
|
|
s = s[:i]
|
|
}
|
|
mod, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return mod, true
|
|
}
|
|
|
|
func modifierBits(mod int) (shift, alt, ctrl, super bool) {
|
|
bits := mod - 1
|
|
return bits&1 != 0, bits&2 != 0, bits&4 != 0, bits&8 != 0 || bits&32 != 0
|
|
}
|
|
|
|
func keyFromModifiedCode(code, mod int) (Key, bool) {
|
|
shift, alt, ctrl, super := modifierBits(mod)
|
|
// Kitty keyboard protocol (CSI ... u) reports control keys as their
|
|
// codepoints: Esc=27, Enter=13, Tab=9, Backspace=127. Without the
|
|
// enhanced-mode handling these arrive as raw bytes; with it enabled
|
|
// they come through here, so map them back to their dedicated keys.
|
|
switch code {
|
|
case 13:
|
|
return Key{Kind: KeyEnter, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
|
|
case 27:
|
|
return Key{Kind: KeyEsc, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
|
|
case 9:
|
|
if shift {
|
|
return Key{Kind: KeyShiftTab, Alt: alt, Ctrl: ctrl, Super: super}, true
|
|
}
|
|
return Key{Kind: KeyTab, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
|
|
case 127, 8:
|
|
return Key{Kind: KeyBackspace, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
|
|
}
|
|
if ctrl {
|
|
switch code {
|
|
case 'c', 'C':
|
|
return Key{Kind: KeyCtrlC, Shift: shift, Alt: alt, Ctrl: true}, true
|
|
case 'd', 'D':
|
|
return Key{Kind: KeyCtrlD, Shift: shift, Alt: alt, Ctrl: true}, true
|
|
case 'l', 'L':
|
|
return Key{Kind: KeyCtrlL, Shift: shift, Alt: alt, Ctrl: true}, true
|
|
case 'u', 'U':
|
|
return Key{Kind: KeyCtrlU, Shift: shift, Alt: alt, Ctrl: true}, true
|
|
case 'k', 'K':
|
|
return Key{Kind: KeyCtrlK, Shift: shift, Alt: alt, Ctrl: true}, true
|
|
case 'a', 'A':
|
|
return Key{Kind: KeyCtrlA, Shift: shift, Alt: alt, Ctrl: true}, true
|
|
case 'e', 'E':
|
|
return Key{Kind: KeyCtrlE, Shift: shift, Alt: alt, Ctrl: true}, true
|
|
case 'w', 'W':
|
|
return Key{Kind: KeyCtrlW, Shift: shift, Alt: alt, Ctrl: true}, true
|
|
case 'o', 'O':
|
|
return Key{Kind: KeyCtrlO, Shift: shift, Alt: alt, Ctrl: true}, true
|
|
case 'v', 'V':
|
|
return Key{Kind: KeyPasteClipboard, Shift: shift, Alt: alt, Ctrl: true}, true
|
|
}
|
|
}
|
|
if code >= '0' && code <= '9' {
|
|
return Key{Kind: KeyRune, Rune: rune(code), Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
|
|
}
|
|
return Key{}, false
|
|
}
|
|
|
|
// readPaste reads until ESC [ 2 0 1 ~ and returns the pasted text.
|
|
func (r *Reader) readPaste() Key {
|
|
var sb strings.Builder
|
|
const end = "\x1b[201~"
|
|
tail := make([]byte, 0, len(end))
|
|
for {
|
|
b, err := r.src()
|
|
if err != nil {
|
|
break
|
|
}
|
|
tail = append(tail, b)
|
|
if len(tail) > len(end) {
|
|
sb.WriteByte(tail[0])
|
|
tail = tail[1:]
|
|
}
|
|
if string(tail) == end {
|
|
break
|
|
}
|
|
}
|
|
return Key{Kind: KeyPaste, Paste: sb.String()}
|
|
}
|