diff options
| author | Dax Raad <[email protected]> | 2025-11-02 18:43:17 -0500 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-11-02 18:43:33 -0500 |
| commit | f68374ad2223ddc213bdea9519ca6a699819ee0e (patch) | |
| tree | 04f0fe21b8e12cd62d7274961bb0cff64f966f40 /packages/tui/input | |
| parent | 5e86c9b7916f75c7ad227b80eab18c7c54fc8ffe (diff) | |
| download | opencode-f68374ad2223ddc213bdea9519ca6a699819ee0e.tar.gz opencode-f68374ad2223ddc213bdea9519ca6a699819ee0e.zip | |
DELETE GO BUBBLETEA CRAP HOORAY
Diffstat (limited to 'packages/tui/input')
31 files changed, 0 insertions, 6102 deletions
diff --git a/packages/tui/input/cancelreader_other.go b/packages/tui/input/cancelreader_other.go deleted file mode 100644 index dbd22a2e9..000000000 --- a/packages/tui/input/cancelreader_other.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !windows -// +build !windows - -package input - -import ( - "io" - - "github.com/muesli/cancelreader" -) - -func newCancelreader(r io.Reader, _ int) (cancelreader.CancelReader, error) { - return cancelreader.NewReader(r) //nolint:wrapcheck -} diff --git a/packages/tui/input/cancelreader_windows.go b/packages/tui/input/cancelreader_windows.go deleted file mode 100644 index 19abfce4a..000000000 --- a/packages/tui/input/cancelreader_windows.go +++ /dev/null @@ -1,143 +0,0 @@ -//go:build windows -// +build windows - -package input - -import ( - "fmt" - "io" - "os" - "sync" - - xwindows "github.com/charmbracelet/x/windows" - "github.com/muesli/cancelreader" - "golang.org/x/sys/windows" -) - -type conInputReader struct { - cancelMixin - conin windows.Handle - originalMode uint32 -} - -var _ cancelreader.CancelReader = &conInputReader{} - -func newCancelreader(r io.Reader, flags int) (cancelreader.CancelReader, error) { - fallback := func(io.Reader) (cancelreader.CancelReader, error) { - return cancelreader.NewReader(r) - } - - var dummy uint32 - if f, ok := r.(cancelreader.File); !ok || f.Fd() != os.Stdin.Fd() || - // If data was piped to the standard input, it does not emit events - // anymore. We can detect this if the console mode cannot be set anymore, - // in this case, we fallback to the default cancelreader implementation. - windows.GetConsoleMode(windows.Handle(f.Fd()), &dummy) != nil { - return fallback(r) - } - - conin, err := windows.GetStdHandle(windows.STD_INPUT_HANDLE) - if err != nil { - return fallback(r) - } - - // Discard any pending input events. - if err := xwindows.FlushConsoleInputBuffer(conin); err != nil { - return fallback(r) - } - - modes := []uint32{ - windows.ENABLE_WINDOW_INPUT, - windows.ENABLE_EXTENDED_FLAGS, - } - - // Enabling mouse mode implicitly blocks console text selection. Thus, we - // need to enable it only if the mouse mode is requested. - // In order to toggle mouse mode, the caller must recreate the reader with - // the appropriate flag toggled. - if flags&FlagMouseMode != 0 { - modes = append(modes, windows.ENABLE_MOUSE_INPUT) - } - - originalMode, err := prepareConsole(conin, modes...) - if err != nil { - return nil, fmt.Errorf("failed to prepare console input: %w", err) - } - - return &conInputReader{ - conin: conin, - originalMode: originalMode, - }, nil -} - -// Cancel implements cancelreader.CancelReader. -func (r *conInputReader) Cancel() bool { - r.setCanceled() - - return windows.CancelIoEx(r.conin, nil) == nil || windows.CancelIo(r.conin) == nil -} - -// Close implements cancelreader.CancelReader. -func (r *conInputReader) Close() error { - if r.originalMode != 0 { - err := windows.SetConsoleMode(r.conin, r.originalMode) - if err != nil { - return fmt.Errorf("reset console mode: %w", err) - } - } - - return nil -} - -// Read implements cancelreader.CancelReader. -func (r *conInputReader) Read(data []byte) (int, error) { - if r.isCanceled() { - return 0, cancelreader.ErrCanceled - } - - var n uint32 - if err := windows.ReadFile(r.conin, data, &n, nil); err != nil { - return int(n), fmt.Errorf("read console input: %w", err) - } - - return int(n), nil -} - -func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32, err error) { - err = windows.GetConsoleMode(input, &originalMode) - if err != nil { - return 0, fmt.Errorf("get console mode: %w", err) - } - - var newMode uint32 - for _, mode := range modes { - newMode |= mode - } - - err = windows.SetConsoleMode(input, newMode) - if err != nil { - return 0, fmt.Errorf("set console mode: %w", err) - } - - return originalMode, nil -} - -// cancelMixin represents a goroutine-safe cancelation status. -type cancelMixin struct { - unsafeCanceled bool - lock sync.Mutex -} - -func (c *cancelMixin) setCanceled() { - c.lock.Lock() - defer c.lock.Unlock() - - c.unsafeCanceled = true -} - -func (c *cancelMixin) isCanceled() bool { - c.lock.Lock() - defer c.lock.Unlock() - - return c.unsafeCanceled -} diff --git a/packages/tui/input/clipboard.go b/packages/tui/input/clipboard.go deleted file mode 100644 index 725a2d955..000000000 --- a/packages/tui/input/clipboard.go +++ /dev/null @@ -1,25 +0,0 @@ -package input - -import "github.com/charmbracelet/x/ansi" - -// ClipboardSelection represents a clipboard selection. The most common -// clipboard selections are "system" and "primary" and selections. -type ClipboardSelection = byte - -// Clipboard selections. -const ( - SystemClipboard ClipboardSelection = ansi.SystemClipboard - PrimaryClipboard ClipboardSelection = ansi.PrimaryClipboard -) - -// ClipboardEvent is a clipboard read message event. This message is emitted when -// a terminal receives an OSC52 clipboard read message event. -type ClipboardEvent struct { - Content string - Selection ClipboardSelection -} - -// String returns the string representation of the clipboard message. -func (e ClipboardEvent) String() string { - return e.Content -} diff --git a/packages/tui/input/color.go b/packages/tui/input/color.go deleted file mode 100644 index 9bcf74999..000000000 --- a/packages/tui/input/color.go +++ /dev/null @@ -1,136 +0,0 @@ -package input - -import ( - "fmt" - "image/color" - "math" -) - -// ForegroundColorEvent represents a foreground color event. This event is -// emitted when the terminal requests the terminal foreground color using -// [ansi.RequestForegroundColor]. -type ForegroundColorEvent struct{ color.Color } - -// String returns the hex representation of the color. -func (e ForegroundColorEvent) String() string { - return colorToHex(e.Color) -} - -// IsDark returns whether the color is dark. -func (e ForegroundColorEvent) IsDark() bool { - return isDarkColor(e.Color) -} - -// BackgroundColorEvent represents a background color event. This event is -// emitted when the terminal requests the terminal background color using -// [ansi.RequestBackgroundColor]. -type BackgroundColorEvent struct{ color.Color } - -// String returns the hex representation of the color. -func (e BackgroundColorEvent) String() string { - return colorToHex(e) -} - -// IsDark returns whether the color is dark. -func (e BackgroundColorEvent) IsDark() bool { - return isDarkColor(e.Color) -} - -// CursorColorEvent represents a cursor color change event. This event is -// emitted when the program requests the terminal cursor color using -// [ansi.RequestCursorColor]. -type CursorColorEvent struct{ color.Color } - -// String returns the hex representation of the color. -func (e CursorColorEvent) String() string { - return colorToHex(e) -} - -// IsDark returns whether the color is dark. -func (e CursorColorEvent) IsDark() bool { - return isDarkColor(e) -} - -type shiftable interface { - ~uint | ~uint16 | ~uint32 | ~uint64 -} - -func shift[T shiftable](x T) T { - if x > 0xff { - x >>= 8 - } - return x -} - -func colorToHex(c color.Color) string { - if c == nil { - return "" - } - r, g, b, _ := c.RGBA() - return fmt.Sprintf("#%02x%02x%02x", shift(r), shift(g), shift(b)) -} - -func getMaxMin(a, b, c float64) (ma, mi float64) { - if a > b { - ma = a - mi = b - } else { - ma = b - mi = a - } - if c > ma { - ma = c - } else if c < mi { - mi = c - } - return ma, mi -} - -func round(x float64) float64 { - return math.Round(x*1000) / 1000 -} - -// rgbToHSL converts an RGB triple to an HSL triple. -func rgbToHSL(r, g, b uint8) (h, s, l float64) { - // convert uint32 pre-multiplied value to uint8 - // The r,g,b values are divided by 255 to change the range from 0..255 to 0..1: - Rnot := float64(r) / 255 - Gnot := float64(g) / 255 - Bnot := float64(b) / 255 - Cmax, Cmin := getMaxMin(Rnot, Gnot, Bnot) - Δ := Cmax - Cmin - // Lightness calculation: - l = (Cmax + Cmin) / 2 - // Hue and Saturation Calculation: - if Δ == 0 { - h = 0 - s = 0 - } else { - switch Cmax { - case Rnot: - h = 60 * (math.Mod((Gnot-Bnot)/Δ, 6)) - case Gnot: - h = 60 * (((Bnot - Rnot) / Δ) + 2) - case Bnot: - h = 60 * (((Rnot - Gnot) / Δ) + 4) - } - if h < 0 { - h += 360 - } - - s = Δ / (1 - math.Abs((2*l)-1)) - } - - return h, round(s), round(l) -} - -// isDarkColor returns whether the given color is dark. -func isDarkColor(c color.Color) bool { - if c == nil { - return true - } - - r, g, b, _ := c.RGBA() - _, _, l := rgbToHSL(uint8(r>>8), uint8(g>>8), uint8(b>>8)) //nolint:gosec - return l < 0.5 -} diff --git a/packages/tui/input/cursor.go b/packages/tui/input/cursor.go deleted file mode 100644 index cf4e973d2..000000000 --- a/packages/tui/input/cursor.go +++ /dev/null @@ -1,7 +0,0 @@ -package input - -import "image" - -// CursorPositionEvent represents a cursor position event. Where X is the -// zero-based column and Y is the zero-based row. -type CursorPositionEvent image.Point diff --git a/packages/tui/input/da1.go b/packages/tui/input/da1.go deleted file mode 100644 index c2cd94cf7..000000000 --- a/packages/tui/input/da1.go +++ /dev/null @@ -1,18 +0,0 @@ -package input - -import "github.com/charmbracelet/x/ansi" - -// PrimaryDeviceAttributesEvent is an event that represents the terminal -// primary device attributes. -type PrimaryDeviceAttributesEvent []int - -func parsePrimaryDevAttrs(params ansi.Params) Event { - // Primary Device Attributes - da1 := make(PrimaryDeviceAttributesEvent, len(params)) - for i, p := range params { - if !p.HasMore() { - da1[i] = p.Param(0) - } - } - return da1 -} diff --git a/packages/tui/input/doc.go b/packages/tui/input/doc.go deleted file mode 100644 index 2877d496e..000000000 --- a/packages/tui/input/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package input provides a set of utilities for handling input events in a -// terminal environment. It includes support for reading input events, parsing -// escape sequences, and handling clipboard events. -// The package is designed to work with various terminal types and supports -// customization through flags and options. -package input diff --git a/packages/tui/input/driver.go b/packages/tui/input/driver.go deleted file mode 100644 index 1e34677ab..000000000 --- a/packages/tui/input/driver.go +++ /dev/null @@ -1,192 +0,0 @@ -//nolint:unused,revive,nolintlint -package input - -import ( - "bytes" - "io" - "unicode/utf8" - - "github.com/muesli/cancelreader" -) - -// Logger is a simple logger interface. -type Logger interface { - Printf(format string, v ...any) -} - -// win32InputState is a state machine for parsing key events from the Windows -// Console API into escape sequences and utf8 runes, and keeps track of the last -// control key state to determine modifier key changes. It also keeps track of -// the last mouse button state and window size changes to determine which mouse -// buttons were released and to prevent multiple size events from firing. -type win32InputState struct { - ansiBuf [256]byte - ansiIdx int - utf16Buf [2]rune - utf16Half bool - lastCks uint32 // the last control key state for the previous event - lastMouseBtns uint32 // the last mouse button state for the previous event - lastWinsizeX, lastWinsizeY int16 // the last window size for the previous event to prevent multiple size events from firing -} - -// Reader represents an input event reader. It reads input events and parses -// escape sequences from the terminal input buffer and translates them into -// human‑readable events. -type Reader struct { - rd cancelreader.CancelReader - table map[string]Key // table is a lookup table for key sequences. - term string // $TERM - paste []byte // bracketed paste buffer; nil when disabled - buf [256]byte // read buffer - partialSeq []byte // holds incomplete escape sequences - keyState win32InputState - parser Parser - logger Logger -} - -// NewReader returns a new input event reader. -func NewReader(r io.Reader, termType string, flags int) (*Reader, error) { - d := new(Reader) - cr, err := newCancelreader(r, flags) - if err != nil { - return nil, err - } - - d.rd = cr - d.table = buildKeysTable(flags, termType) - d.term = termType - d.parser.flags = flags - return d, nil -} - -// SetLogger sets a logger for the reader. -func (d *Reader) SetLogger(l Logger) { d.logger = l } - -// Read implements io.Reader. -func (d *Reader) Read(p []byte) (int, error) { return d.rd.Read(p) } - -// Cancel cancels the underlying reader. -func (d *Reader) Cancel() bool { return d.rd.Cancel() } - -// Close closes the underlying reader. -func (d *Reader) Close() error { return d.rd.Close() } - -func (d *Reader) readEvents() ([]Event, error) { - nb, err := d.rd.Read(d.buf[:]) - if err != nil { - return nil, err - } - - var events []Event - - // Combine any partial sequence from previous read with new data. - var buf []byte - if len(d.partialSeq) > 0 { - buf = make([]byte, len(d.partialSeq)+nb) - copy(buf, d.partialSeq) - copy(buf[len(d.partialSeq):], d.buf[:nb]) - d.partialSeq = nil - } else { - buf = d.buf[:nb] - } - - // Fast path: direct lookup for simple escape sequences. - if bytes.HasPrefix(buf, []byte{0x1b}) { - if k, ok := d.table[string(buf)]; ok { - if d.logger != nil { - d.logger.Printf("input: %q", buf) - } - events = append(events, KeyPressEvent(k)) - return events, nil - } - } - - var i int - for i < len(buf) { - consumed, ev := d.parser.parseSequence(buf[i:]) - if d.logger != nil && consumed > 0 { - d.logger.Printf("input: %q", buf[i:i+consumed]) - } - - // Incomplete sequence – store remainder and exit. - if consumed == 0 && ev == nil { - rem := len(buf) - i - if rem > 0 { - d.partialSeq = make([]byte, rem) - copy(d.partialSeq, buf[i:]) - } - break - } - - // Handle bracketed paste specially so we don’t emit a paste event for - // every byte. - if d.paste != nil { - if _, ok := ev.(PasteEndEvent); !ok { - d.paste = append(d.paste, buf[i]) - i++ - continue - } - } - - switch ev.(type) { - case PasteStartEvent: - d.paste = []byte{} - case PasteEndEvent: - var paste []rune - for len(d.paste) > 0 { - r, w := utf8.DecodeRune(d.paste) - if r != utf8.RuneError { - paste = append(paste, r) - } - d.paste = d.paste[w:] - } - d.paste = nil - events = append(events, PasteEvent(paste)) - case nil: - i++ - continue - } - - if mevs, ok := ev.(MultiEvent); ok { - events = append(events, []Event(mevs)...) - } else { - events = append(events, ev) - } - i += consumed - } - - // Collapse bursts of wheel/motion events into a single event each. - events = coalesceMouseEvents(events) - return events, nil -} - -// coalesceMouseEvents reduces the volume of MouseWheelEvent and MouseMotionEvent -// objects that arrive in rapid succession by keeping only the most recent -// event in each contiguous run. -func coalesceMouseEvents(in []Event) []Event { - if len(in) < 2 { - return in - } - - out := make([]Event, 0, len(in)) - for _, ev := range in { - switch ev.(type) { - case MouseWheelEvent: - if len(out) > 0 { - if _, ok := out[len(out)-1].(MouseWheelEvent); ok { - out[len(out)-1] = ev // replace previous wheel event - continue - } - } - case MouseMotionEvent: - if len(out) > 0 { - if _, ok := out[len(out)-1].(MouseMotionEvent); ok { - out[len(out)-1] = ev // replace previous motion event - continue - } - } - } - out = append(out, ev) - } - return out -} diff --git a/packages/tui/input/driver_other.go b/packages/tui/input/driver_other.go deleted file mode 100644 index fd3df06c6..000000000 --- a/packages/tui/input/driver_other.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build !windows -// +build !windows - -package input - -// ReadEvents reads input events from the terminal. -// -// It reads the events available in the input buffer and returns them. -func (d *Reader) ReadEvents() ([]Event, error) { - return d.readEvents() -} - -// parseWin32InputKeyEvent parses a Win32 input key events. This function is -// only available on Windows. -func (p *Parser) parseWin32InputKeyEvent(*win32InputState, uint16, uint16, rune, bool, uint32, uint16) Event { - return nil -} diff --git a/packages/tui/input/driver_test.go b/packages/tui/input/driver_test.go deleted file mode 100644 index affdf5b88..000000000 --- a/packages/tui/input/driver_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package input - -import ( - "io" - "strings" - "testing" -) - -func BenchmarkDriver(b *testing.B) { - input := "\x1b\x1b[Ztest\x00\x1b]10;1234/1234/1234\x07\x1b[27;2;27~" - rdr := strings.NewReader(input) - drv, err := NewReader(rdr, "dumb", 0) - if err != nil { - b.Fatalf("could not create driver: %v", err) - } - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - rdr.Reset(input) - if _, err := drv.ReadEvents(); err != nil && err != io.EOF { - b.Errorf("error reading input: %v", err) - } - } -} diff --git a/packages/tui/input/driver_windows.go b/packages/tui/input/driver_windows.go deleted file mode 100644 index b9121734f..000000000 --- a/packages/tui/input/driver_windows.go +++ /dev/null @@ -1,642 +0,0 @@ -//go:build windows -// +build windows - -package input - -import ( - "errors" - "fmt" - "strings" - "time" - "unicode" - "unicode/utf16" - "unicode/utf8" - - "github.com/charmbracelet/x/ansi" - xwindows "github.com/charmbracelet/x/windows" - "github.com/muesli/cancelreader" - "golang.org/x/sys/windows" -) - -// ReadEvents reads input events from the terminal. -// -// It reads the events available in the input buffer and returns them. -func (d *Reader) ReadEvents() ([]Event, error) { - events, err := d.handleConInput() - if errors.Is(err, errNotConInputReader) { - return d.readEvents() - } - return events, err -} - -var errNotConInputReader = fmt.Errorf("handleConInput: not a conInputReader") - -func (d *Reader) handleConInput() ([]Event, error) { - cc, ok := d.rd.(*conInputReader) - if !ok { - return nil, errNotConInputReader - } - - var ( - events []xwindows.InputRecord - err error - ) - for { - // Peek up to 256 events, this is to allow for sequences events reported as - // key events. - events, err = peekNConsoleInputs(cc.conin, 256) - if cc.isCanceled() { - return nil, cancelreader.ErrCanceled - } - if err != nil { - return nil, fmt.Errorf("peek coninput events: %w", err) - } - if len(events) > 0 { - break - } - - // Sleep for a bit to avoid busy waiting. - time.Sleep(10 * time.Millisecond) - } - - events, err = readNConsoleInputs(cc.conin, uint32(len(events))) - if cc.isCanceled() { - return nil, cancelreader.ErrCanceled - } - if err != nil { - return nil, fmt.Errorf("read coninput events: %w", err) - } - - var evs []Event - for _, event := range events { - if e := d.parser.parseConInputEvent(event, &d.keyState); e != nil { - if multi, ok := e.(MultiEvent); ok { - evs = append(evs, multi...) - } else { - evs = append(evs, e) - } - } - } - - return evs, nil -} - -func (p *Parser) parseConInputEvent(event xwindows.InputRecord, keyState *win32InputState) Event { - switch event.EventType { - case xwindows.KEY_EVENT: - kevent := event.KeyEvent() - return p.parseWin32InputKeyEvent(keyState, kevent.VirtualKeyCode, kevent.VirtualScanCode, - kevent.Char, kevent.KeyDown, kevent.ControlKeyState, kevent.RepeatCount) - - case xwindows.WINDOW_BUFFER_SIZE_EVENT: - wevent := event.WindowBufferSizeEvent() - if wevent.Size.X != keyState.lastWinsizeX || wevent.Size.Y != keyState.lastWinsizeY { - keyState.lastWinsizeX, keyState.lastWinsizeY = wevent.Size.X, wevent.Size.Y - return WindowSizeEvent{ - Width: int(wevent.Size.X), - Height: int(wevent.Size.Y), - } - } - case xwindows.MOUSE_EVENT: - mevent := event.MouseEvent() - Event := mouseEvent(keyState.lastMouseBtns, mevent) - keyState.lastMouseBtns = mevent.ButtonState - return Event - case xwindows.FOCUS_EVENT: - fevent := event.FocusEvent() - if fevent.SetFocus { - return FocusEvent{} - } - return BlurEvent{} - case xwindows.MENU_EVENT: - // ignore - } - return nil -} - -func mouseEventButton(p, s uint32) (MouseButton, bool) { - var isRelease bool - button := MouseNone - btn := p ^ s - if btn&s == 0 { - isRelease = true - } - - if btn == 0 { - switch { - case s&xwindows.FROM_LEFT_1ST_BUTTON_PRESSED > 0: - button = MouseLeft - case s&xwindows.FROM_LEFT_2ND_BUTTON_PRESSED > 0: - button = MouseMiddle - case s&xwindows.RIGHTMOST_BUTTON_PRESSED > 0: - button = MouseRight - case s&xwindows.FROM_LEFT_3RD_BUTTON_PRESSED > 0: - button = MouseBackward - case s&xwindows.FROM_LEFT_4TH_BUTTON_PRESSED > 0: - button = MouseForward - } - return button, isRelease - } - - switch btn { - case xwindows.FROM_LEFT_1ST_BUTTON_PRESSED: // left button - button = MouseLeft - case xwindows.RIGHTMOST_BUTTON_PRESSED: // right button - button = MouseRight - case xwindows.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button - button = MouseMiddle - case xwindows.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward) - button = MouseBackward - case xwindows.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward) - button = MouseForward - } - - return button, isRelease -} - -func mouseEvent(p uint32, e xwindows.MouseEventRecord) (ev Event) { - var mod KeyMod - var isRelease bool - if e.ControlKeyState&(xwindows.LEFT_ALT_PRESSED|xwindows.RIGHT_ALT_PRESSED) != 0 { - mod |= ModAlt - } - if e.ControlKeyState&(xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_CTRL_PRESSED) != 0 { - mod |= ModCtrl - } - if e.ControlKeyState&(xwindows.SHIFT_PRESSED) != 0 { - mod |= ModShift - } - - m := Mouse{ - X: int(e.MousePositon.X), - Y: int(e.MousePositon.Y), - Mod: mod, - } - - wheelDirection := int16(highWord(e.ButtonState)) //nolint:gosec - switch e.EventFlags { - case 0, xwindows.DOUBLE_CLICK: - m.Button, isRelease = mouseEventButton(p, e.ButtonState) - case xwindows.MOUSE_WHEELED: - if wheelDirection > 0 { - m.Button = MouseWheelUp - } else { - m.Button = MouseWheelDown - } - case xwindows.MOUSE_HWHEELED: - if wheelDirection > 0 { - m.Button = MouseWheelRight - } else { - m.Button = MouseWheelLeft - } - case xwindows.MOUSE_MOVED: - m.Button, _ = mouseEventButton(p, e.ButtonState) - return MouseMotionEvent(m) - } - - if isWheel(m.Button) { - return MouseWheelEvent(m) - } else if isRelease { - return MouseReleaseEvent(m) - } - - return MouseClickEvent(m) -} - -func highWord(data uint32) uint16 { - return uint16((data & 0xFFFF0000) >> 16) //nolint:gosec -} - -func readNConsoleInputs(console windows.Handle, maxEvents uint32) ([]xwindows.InputRecord, error) { - if maxEvents == 0 { - return nil, fmt.Errorf("maxEvents cannot be zero") - } - - records := make([]xwindows.InputRecord, maxEvents) - n, err := readConsoleInput(console, records) - return records[:n], err -} - -func readConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecord) (uint32, error) { - if len(inputRecords) == 0 { - return 0, fmt.Errorf("size of input record buffer cannot be zero") - } - - var read uint32 - - err := xwindows.ReadConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read) //nolint:gosec - - return read, err //nolint:wrapcheck -} - -func peekConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecord) (uint32, error) { - if len(inputRecords) == 0 { - return 0, fmt.Errorf("size of input record buffer cannot be zero") - } - - var read uint32 - - err := xwindows.PeekConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read) //nolint:gosec - - return read, err //nolint:wrapcheck -} - -func peekNConsoleInputs(console windows.Handle, maxEvents uint32) ([]xwindows.InputRecord, error) { - if maxEvents == 0 { - return nil, fmt.Errorf("maxEvents cannot be zero") - } - - records := make([]xwindows.InputRecord, maxEvents) - n, err := peekConsoleInput(console, records) - return records[:n], err -} - -// parseWin32InputKeyEvent parses a single key event from either the Windows -// Console API or win32-input-mode events. When state is nil, it means this is -// an event from win32-input-mode. Otherwise, it's a key event from the Windows -// Console API and needs a state to decode ANSI escape sequences and utf16 -// runes. -func (p *Parser) parseWin32InputKeyEvent(state *win32InputState, vkc uint16, _ uint16, r rune, keyDown bool, cks uint32, repeatCount uint16) (event Event) { - defer func() { - // Respect the repeat count. - if repeatCount > 1 { - var multi MultiEvent - for i := 0; i < int(repeatCount); i++ { - multi = append(multi, event) - } - event = multi - } - }() - if state != nil { - defer func() { - state.lastCks = cks - }() - } - - var utf8Buf [utf8.UTFMax]byte - var key Key - if state != nil && state.utf16Half { - state.utf16Half = false - state.utf16Buf[1] = r - codepoint := utf16.DecodeRune(state.utf16Buf[0], state.utf16Buf[1]) - rw := utf8.EncodeRune(utf8Buf[:], codepoint) - r, _ = utf8.DecodeRune(utf8Buf[:rw]) - key.Code = r - key.Text = string(r) - key.Mod = translateControlKeyState(cks) - key = ensureKeyCase(key, cks) - if keyDown { - return KeyPressEvent(key) - } - return KeyReleaseEvent(key) - } - - var baseCode rune - switch { - case vkc == 0: - // Zero means this event is either an escape code or a unicode - // codepoint. - if state != nil && state.ansiIdx == 0 && r != ansi.ESC { - // This is a unicode codepoint. - baseCode = r - break - } - - if state != nil { - // Collect ANSI escape code. - state.ansiBuf[state.ansiIdx] = byte(r) - state.ansiIdx++ - if state.ansiIdx <= 2 { - // We haven't received enough bytes to determine if this is an - // ANSI escape code. - return nil - } - if r == ansi.ESC { - // We're expecting a closing String Terminator [ansi.ST]. - return nil - } - - n, event := p.parseSequence(state.ansiBuf[:state.ansiIdx]) - if n == 0 { - return nil - } - if _, ok := event.(UnknownEvent); ok { - return nil - } - - state.ansiIdx = 0 - return event - } - case vkc == xwindows.VK_BACK: - baseCode = KeyBackspace - case vkc == xwindows.VK_TAB: - baseCode = KeyTab - case vkc == xwindows.VK_RETURN: - baseCode = KeyEnter - case vkc == xwindows.VK_SHIFT: - //nolint:nestif - if cks&xwindows.SHIFT_PRESSED != 0 { - if cks&xwindows.ENHANCED_KEY != 0 { - baseCode = KeyRightShift - } else { - baseCode = KeyLeftShift - } - } else if state != nil { - if state.lastCks&xwindows.SHIFT_PRESSED != 0 { - if state.lastCks&xwindows.ENHANCED_KEY != 0 { - baseCode = KeyRightShift - } else { - baseCode = KeyLeftShift - } - } - } - case vkc == xwindows.VK_CONTROL: - if cks&xwindows.LEFT_CTRL_PRESSED != 0 { - baseCode = KeyLeftCtrl - } else if cks&xwindows.RIGHT_CTRL_PRESSED != 0 { - baseCode = KeyRightCtrl - } else if state != nil { - if state.lastCks&xwindows.LEFT_CTRL_PRESSED != 0 { - baseCode = KeyLeftCtrl - } else if state.lastCks&xwindows.RIGHT_CTRL_PRESSED != 0 { - baseCode = KeyRightCtrl - } - } - case vkc == xwindows.VK_MENU: - if cks&xwindows.LEFT_ALT_PRESSED != 0 { - baseCode = KeyLeftAlt - } else if cks&xwindows.RIGHT_ALT_PRESSED != 0 { - baseCode = KeyRightAlt - } else if state != nil { - if state.lastCks&xwindows.LEFT_ALT_PRESSED != 0 { - baseCode = KeyLeftAlt - } else if state.lastCks&xwindows.RIGHT_ALT_PRESSED != 0 { - baseCode = KeyRightAlt - } - } - case vkc == xwindows.VK_PAUSE: - baseCode = KeyPause - case vkc == xwindows.VK_CAPITAL: - baseCode = KeyCapsLock - case vkc == xwindows.VK_ESCAPE: - baseCode = KeyEscape - case vkc == xwindows.VK_SPACE: - baseCode = KeySpace - case vkc == xwindows.VK_PRIOR: - baseCode = KeyPgUp - case vkc == xwindows.VK_NEXT: - baseCode = KeyPgDown - case vkc == xwindows.VK_END: - baseCode = KeyEnd - case vkc == xwindows.VK_HOME: - baseCode = KeyHome - case vkc == xwindows.VK_LEFT: - baseCode = KeyLeft - case vkc == xwindows.VK_UP: - baseCode = KeyUp - case vkc == xwindows.VK_RIGHT: - baseCode = KeyRight - case vkc == xwindows.VK_DOWN: - baseCode = KeyDown - case vkc == xwindows.VK_SELECT: - baseCode = KeySelect - case vkc == xwindows.VK_SNAPSHOT: - baseCode = KeyPrintScreen - case vkc == xwindows.VK_INSERT: - baseCode = KeyInsert - case vkc == xwindows.VK_DELETE: - baseCode = KeyDelete - case vkc >= '0' && vkc <= '9': - baseCode = rune(vkc) - case vkc >= 'A' && vkc <= 'Z': - // Convert to lowercase. - baseCode = rune(vkc) + 32 - case vkc == xwindows.VK_LWIN: - baseCode = KeyLeftSuper - case vkc == xwindows.VK_RWIN: - baseCode = KeyRightSuper - case vkc == xwindows.VK_APPS: - baseCode = KeyMenu - case vkc >= xwindows.VK_NUMPAD0 && vkc <= xwindows.VK_NUMPAD9: - baseCode = rune(vkc-xwindows.VK_NUMPAD0) + KeyKp0 - case vkc == xwindows.VK_MULTIPLY: - baseCode = KeyKpMultiply - case vkc == xwindows.VK_ADD: - baseCode = KeyKpPlus - case vkc == xwindows.VK_SEPARATOR: - baseCode = KeyKpComma - case vkc == xwindows.VK_SUBTRACT: - baseCode = KeyKpMinus - case vkc == xwindows.VK_DECIMAL: - baseCode = KeyKpDecimal - case vkc == xwindows.VK_DIVIDE: - baseCode = KeyKpDivide - case vkc >= xwindows.VK_F1 && vkc <= xwindows.VK_F24: - baseCode = rune(vkc-xwindows.VK_F1) + KeyF1 - case vkc == xwindows.VK_NUMLOCK: - baseCode = KeyNumLock - case vkc == xwindows.VK_SCROLL: - baseCode = KeyScrollLock - case vkc == xwindows.VK_LSHIFT: - baseCode = KeyLeftShift - case vkc == xwindows.VK_RSHIFT: - baseCode = KeyRightShift - case vkc == xwindows.VK_LCONTROL: - baseCode = KeyLeftCtrl - case vkc == xwindows.VK_RCONTROL: - baseCode = KeyRightCtrl - case vkc == xwindows.VK_LMENU: - baseCode = KeyLeftAlt - case vkc == xwindows.VK_RMENU: - baseCode = KeyRightAlt - case vkc == xwindows.VK_VOLUME_MUTE: - baseCode = KeyMute - case vkc == xwindows.VK_VOLUME_DOWN: - baseCode = KeyLowerVol - case vkc == xwindows.VK_VOLUME_UP: - baseCode = KeyRaiseVol - case vkc == xwindows.VK_MEDIA_NEXT_TRACK: - baseCode = KeyMediaNext - case vkc == xwindows.VK_MEDIA_PREV_TRACK: - baseCode = KeyMediaPrev - case vkc == xwindows.VK_MEDIA_STOP: - baseCode = KeyMediaStop - case vkc == xwindows.VK_MEDIA_PLAY_PAUSE: - baseCode = KeyMediaPlayPause - case vkc == xwindows.VK_OEM_1, vkc == xwindows.VK_OEM_PLUS, vkc == xwindows.VK_OEM_COMMA, - vkc == xwindows.VK_OEM_MINUS, vkc == xwindows.VK_OEM_PERIOD, vkc == xwindows.VK_OEM_2, - vkc == xwindows.VK_OEM_3, vkc == xwindows.VK_OEM_4, vkc == xwindows.VK_OEM_5, - vkc == xwindows.VK_OEM_6, vkc == xwindows.VK_OEM_7: - // Use the actual character provided by Windows for current keyboard layout - // instead of hardcoded US layout mappings - if !unicode.IsControl(r) && unicode.IsPrint(r) { - baseCode = r - } else { - // Fallback to original hardcoded mappings for non-printable cases - switch vkc { - case xwindows.VK_OEM_1: - baseCode = ';' - case xwindows.VK_OEM_PLUS: - baseCode = '+' - case xwindows.VK_OEM_COMMA: - baseCode = ',' - case xwindows.VK_OEM_MINUS: - baseCode = '-' - case xwindows.VK_OEM_PERIOD: - baseCode = '.' - case xwindows.VK_OEM_2: - baseCode = '/' - case xwindows.VK_OEM_3: - baseCode = '`' - case xwindows.VK_OEM_4: - baseCode = '[' - case xwindows.VK_OEM_5: - baseCode = '\\' - case xwindows.VK_OEM_6: - baseCode = ']' - case xwindows.VK_OEM_7: - baseCode = '\'' - } - } - } - - if utf16.IsSurrogate(r) { - if state != nil { - state.utf16Buf[0] = r - state.utf16Half = true - } - return nil - } - - // AltGr is left ctrl + right alt. On non-US keyboards, this is used to type - // special characters and produce printable events. - // XXX: Should this be a KeyMod? - altGr := cks&(xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_ALT_PRESSED) == xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_ALT_PRESSED - - // FIXED: Remove numlock and scroll lock states when checking for printable text - // These lock states shouldn't affect normal typing - cksForTextCheck := cks &^ (xwindows.NUMLOCK_ON | xwindows.SCROLLLOCK_ON) - - var text string - keyCode := baseCode - if !unicode.IsControl(r) { - rw := utf8.EncodeRune(utf8Buf[:], r) - keyCode, _ = utf8.DecodeRune(utf8Buf[:rw]) - if unicode.IsPrint(keyCode) && (cksForTextCheck == 0 || - cksForTextCheck == xwindows.SHIFT_PRESSED || - cksForTextCheck == xwindows.CAPSLOCK_ON || - altGr) { - // If the control key state is 0, shift is pressed, or caps lock - // then the key event is a printable event i.e. [text] is not empty. - text = string(keyCode) - } - } - - // Special case: numeric keypad divide should produce "/" text on all layouts (fix french keyboard layout) - if baseCode == KeyKpDivide { - text = "/" - } - - key.Code = keyCode - key.Text = text - key.Mod = translateControlKeyState(cks) - key.BaseCode = baseCode - key = ensureKeyCase(key, cks) - if keyDown { - return KeyPressEvent(key) - } - - return KeyReleaseEvent(key) -} - -// ensureKeyCase ensures that the key's text is in the correct case based on the -// control key state. -func ensureKeyCase(key Key, cks uint32) Key { - if len(key.Text) == 0 { - return key - } - - hasShift := cks&xwindows.SHIFT_PRESSED != 0 - hasCaps := cks&xwindows.CAPSLOCK_ON != 0 - if hasShift || hasCaps { - if unicode.IsLower(key.Code) { - key.ShiftedCode = unicode.ToUpper(key.Code) - key.Text = string(key.ShiftedCode) - } - } else { - if unicode.IsUpper(key.Code) { - key.ShiftedCode = unicode.ToLower(key.Code) - key.Text = string(key.ShiftedCode) - } - } - - return key -} - -// translateControlKeyState translates the control key state from the Windows -// Console API into a Mod bitmask. -func translateControlKeyState(cks uint32) (m KeyMod) { - if cks&xwindows.LEFT_CTRL_PRESSED != 0 || cks&xwindows.RIGHT_CTRL_PRESSED != 0 { - m |= ModCtrl - } - if cks&xwindows.LEFT_ALT_PRESSED != 0 || cks&xwindows.RIGHT_ALT_PRESSED != 0 { - m |= ModAlt - } - if cks&xwindows.SHIFT_PRESSED != 0 { - m |= ModShift - } - if cks&xwindows.CAPSLOCK_ON != 0 { - m |= ModCapsLock - } - if cks&xwindows.NUMLOCK_ON != 0 { - m |= ModNumLock - } - if cks&xwindows.SCROLLLOCK_ON != 0 { - m |= ModScrollLock - } - return -} - -//nolint:unused -func keyEventString(vkc, sc uint16, r rune, keyDown bool, cks uint32, repeatCount uint16) string { - var s strings.Builder - s.WriteString("vkc: ") - s.WriteString(fmt.Sprintf("%d, 0x%02x", vkc, vkc)) - s.WriteString(", sc: ") - s.WriteString(fmt.Sprintf("%d, 0x%02x", sc, sc)) - s.WriteString(", r: ") - s.WriteString(fmt.Sprintf("%q", r)) - s.WriteString(", down: ") - s.WriteString(fmt.Sprintf("%v", keyDown)) - s.WriteString(", cks: [") - if cks&xwindows.LEFT_ALT_PRESSED != 0 { - s.WriteString("left alt, ") - } - if cks&xwindows.RIGHT_ALT_PRESSED != 0 { - s.WriteString("right alt, ") - } - if cks&xwindows.LEFT_CTRL_PRESSED != 0 { - s.WriteString("left ctrl, ") - } - if cks&xwindows.RIGHT_CTRL_PRESSED != 0 { - s.WriteString("right ctrl, ") - } - if cks&xwindows.SHIFT_PRESSED != 0 { - s.WriteString("shift, ") - } - if cks&xwindows.CAPSLOCK_ON != 0 { - s.WriteString("caps lock, ") - } - if cks&xwindows.NUMLOCK_ON != 0 { - s.WriteString("num lock, ") - } - if cks&xwindows.SCROLLLOCK_ON != 0 { - s.WriteString("scroll lock, ") - } - if cks&xwindows.ENHANCED_KEY != 0 { - s.WriteString("enhanced key, ") - } - s.WriteString("], repeat count: ") - s.WriteString(fmt.Sprintf("%d", repeatCount)) - return s.String() -} diff --git a/packages/tui/input/driver_windows_test.go b/packages/tui/input/driver_windows_test.go deleted file mode 100644 index 45371fd13..000000000 --- a/packages/tui/input/driver_windows_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package input - -import ( - "encoding/binary" - "image/color" - "reflect" - "testing" - "unicode/utf16" - - "github.com/charmbracelet/x/ansi" - xwindows "github.com/charmbracelet/x/windows" - "golang.org/x/sys/windows" -) - -func TestWindowsInputEvents(t *testing.T) { - cases := []struct { - name string - events []xwindows.InputRecord - expected []Event - sequence bool // indicates that the input events are ANSI sequence or utf16 - }{ - { - name: "single key event", - events: []xwindows.InputRecord{ - encodeKeyEvent(xwindows.KeyEventRecord{ - KeyDown: true, - Char: 'a', - VirtualKeyCode: 'A', - }), - }, - expected: []Event{KeyPressEvent{Code: 'a', BaseCode: 'a', Text: "a"}}, - }, - { - name: "single key event with control key", - events: []xwindows.InputRecord{ - encodeKeyEvent(xwindows.KeyEventRecord{ - KeyDown: true, - Char: 'a', - VirtualKeyCode: 'A', - ControlKeyState: xwindows.LEFT_CTRL_PRESSED, - }), - }, - expected: []Event{KeyPressEvent{Code: 'a', BaseCode: 'a', Mod: ModCtrl}}, - }, - { - name: "escape alt key event", - events: []xwindows.InputRecord{ - encodeKeyEvent(xwindows.KeyEventRecord{ - KeyDown: true, - Char: ansi.ESC, - VirtualKeyCode: ansi.ESC, - ControlKeyState: xwindows.LEFT_ALT_PRESSED, - }), - }, - expected: []Event{KeyPressEvent{Code: ansi.ESC, BaseCode: ansi.ESC, Mod: ModAlt}}, - }, - { - name: "single shifted key event", - events: []xwindows.InputRecord{ - encodeKeyEvent(xwindows.KeyEventRecord{ - KeyDown: true, - Char: 'A', - VirtualKeyCode: 'A', - ControlKeyState: xwindows.SHIFT_PRESSED, - }), - }, - expected: []Event{KeyPressEvent{Code: 'A', BaseCode: 'a', Text: "A", Mod: ModShift}}, - }, - { - name: "utf16 rune", - events: encodeUtf16Rune('😊'), // smiley emoji '😊' - expected: []Event{ - KeyPressEvent{Code: '😊', Text: "😊"}, - }, - sequence: true, - }, - { - name: "background color response", - events: encodeSequence("\x1b]11;rgb:ff/ff/ff\x07"), - expected: []Event{BackgroundColorEvent{Color: color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}}}, - sequence: true, - }, - { - name: "st terminated background color response", - events: encodeSequence("\x1b]11;rgb:ffff/ffff/ffff\x1b\\"), - expected: []Event{BackgroundColorEvent{Color: color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}}}, - sequence: true, - }, - { - name: "simple mouse event", - events: []xwindows.InputRecord{ - encodeMouseEvent(xwindows.MouseEventRecord{ - MousePositon: windows.Coord{X: 10, Y: 20}, - ButtonState: xwindows.FROM_LEFT_1ST_BUTTON_PRESSED, - EventFlags: 0, - }), - encodeMouseEvent(xwindows.MouseEventRecord{ - MousePositon: windows.Coord{X: 10, Y: 20}, - EventFlags: 0, - }), - }, - expected: []Event{ - MouseClickEvent{Button: MouseLeft, X: 10, Y: 20}, - MouseReleaseEvent{Button: MouseLeft, X: 10, Y: 20}, - }, - }, - { - name: "focus event", - events: []xwindows.InputRecord{ - encodeFocusEvent(xwindows.FocusEventRecord{ - SetFocus: true, - }), - encodeFocusEvent(xwindows.FocusEventRecord{ - SetFocus: false, - }), - }, - expected: []Event{ - FocusEvent{}, - BlurEvent{}, - }, - }, - { - name: "window size event", - events: []xwindows.InputRecord{ - encodeWindowBufferSizeEvent(xwindows.WindowBufferSizeRecord{ - Size: windows.Coord{X: 10, Y: 20}, - }), - }, - expected: []Event{ - WindowSizeEvent{Width: 10, Height: 20}, - }, - }, - } - - // p is the parser to parse the input events - var p Parser - - // keep track of the state of the driver to handle ANSI sequences and utf16 - var state win32InputState - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - if tc.sequence { - var Event Event - for _, ev := range tc.events { - if ev.EventType != xwindows.KEY_EVENT { - t.Fatalf("expected key event, got %v", ev.EventType) - } - - key := ev.KeyEvent() - Event = p.parseWin32InputKeyEvent(&state, key.VirtualKeyCode, key.VirtualScanCode, key.Char, key.KeyDown, key.ControlKeyState, key.RepeatCount) - } - if len(tc.expected) != 1 { - t.Fatalf("expected 1 event, got %d", len(tc.expected)) - } - if !reflect.DeepEqual(Event, tc.expected[0]) { - t.Errorf("expected %v, got %v", tc.expected[0], Event) - } - } else { - if len(tc.events) != len(tc.expected) { - t.Fatalf("expected %d events, got %d", len(tc.expected), len(tc.events)) - } - for j, ev := range tc.events { - Event := p.parseConInputEvent(ev, &state) - if !reflect.DeepEqual(Event, tc.expected[j]) { - t.Errorf("expected %#v, got %#v", tc.expected[j], Event) - } - } - } - }) - } -} - -func boolToUint32(b bool) uint32 { - if b { - return 1 - } - return 0 -} - -func encodeMenuEvent(menu xwindows.MenuEventRecord) xwindows.InputRecord { - var bts [16]byte - binary.LittleEndian.PutUint32(bts[0:4], menu.CommandID) - return xwindows.InputRecord{ - EventType: xwindows.MENU_EVENT, - Event: bts, - } -} - -func encodeWindowBufferSizeEvent(size xwindows.WindowBufferSizeRecord) xwindows.InputRecord { - var bts [16]byte - binary.LittleEndian.PutUint16(bts[0:2], uint16(size.Size.X)) - binary.LittleEndian.PutUint16(bts[2:4], uint16(size.Size.Y)) - return xwindows.InputRecord{ - EventType: xwindows.WINDOW_BUFFER_SIZE_EVENT, - Event: bts, - } -} - -func encodeFocusEvent(focus xwindows.FocusEventRecord) xwindows.InputRecord { - var bts [16]byte - if focus.SetFocus { - bts[0] = 1 - } - return xwindows.InputRecord{ - EventType: xwindows.FOCUS_EVENT, - Event: bts, - } -} - -func encodeMouseEvent(mouse xwindows.MouseEventRecord) xwindows.InputRecord { - var bts [16]byte - binary.LittleEndian.PutUint16(bts[0:2], uint16(mouse.MousePositon.X)) - binary.LittleEndian.PutUint16(bts[2:4], uint16(mouse.MousePositon.Y)) - binary.LittleEndian.PutUint32(bts[4:8], mouse.ButtonState) - binary.LittleEndian.PutUint32(bts[8:12], mouse.ControlKeyState) - binary.LittleEndian.PutUint32(bts[12:16], mouse.EventFlags) - return xwindows.InputRecord{ - EventType: xwindows.MOUSE_EVENT, - Event: bts, - } -} - -func encodeKeyEvent(key xwindows.KeyEventRecord) xwindows.InputRecord { - var bts [16]byte - binary.LittleEndian.PutUint32(bts[0:4], boolToUint32(key.KeyDown)) - binary.LittleEndian.PutUint16(bts[4:6], key.RepeatCount) - binary.LittleEndian.PutUint16(bts[6:8], key.VirtualKeyCode) - binary.LittleEndian.PutUint16(bts[8:10], key.VirtualScanCode) - binary.LittleEndian.PutUint16(bts[10:12], uint16(key.Char)) - binary.LittleEndian.PutUint32(bts[12:16], key.ControlKeyState) - return xwindows.InputRecord{ - EventType: xwindows.KEY_EVENT, - Event: bts, - } -} - -// encodeSequence encodes a string of ANSI escape sequences into a slice of -// Windows input key records. -func encodeSequence(s string) (evs []xwindows.InputRecord) { - var state byte - for len(s) > 0 { - seq, _, n, newState := ansi.DecodeSequence(s, state, nil) - for i := 0; i < n; i++ { - evs = append(evs, encodeKeyEvent(xwindows.KeyEventRecord{ - KeyDown: true, - Char: rune(seq[i]), - })) - } - state = newState - s = s[n:] - } - return -} - -func encodeUtf16Rune(r rune) []xwindows.InputRecord { - r1, r2 := utf16.EncodeRune(r) - return encodeUtf16Pair(r1, r2) -} - -func encodeUtf16Pair(r1, r2 rune) []xwindows.InputRecord { - return []xwindows.InputRecord{ - encodeKeyEvent(xwindows.KeyEventRecord{ - KeyDown: true, - Char: r1, - }), - encodeKeyEvent(xwindows.KeyEventRecord{ - KeyDown: true, - Char: r2, - }), - } -} diff --git a/packages/tui/input/focus.go b/packages/tui/input/focus.go deleted file mode 100644 index 796d95f64..000000000 --- a/packages/tui/input/focus.go +++ /dev/null @@ -1,9 +0,0 @@ -package input - -// FocusEvent represents a terminal focus event. -// This occurs when the terminal gains focus. -type FocusEvent struct{} - -// BlurEvent represents a terminal blur event. -// This occurs when the terminal loses focus. -type BlurEvent struct{} diff --git a/packages/tui/input/focus_test.go b/packages/tui/input/focus_test.go deleted file mode 100644 index 2d35e4768..000000000 --- a/packages/tui/input/focus_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package input - -import ( - "testing" -) - -func TestFocus(t *testing.T) { - var p Parser - _, e := p.parseSequence([]byte("\x1b[I")) - switch e.(type) { - case FocusEvent: - // ok - default: - t.Error("invalid sequence") - } -} - -func TestBlur(t *testing.T) { - var p Parser - _, e := p.parseSequence([]byte("\x1b[O")) - switch e.(type) { - case BlurEvent: - // ok - default: - t.Error("invalid sequence") - } -} diff --git a/packages/tui/input/go.mod b/packages/tui/input/go.mod deleted file mode 100644 index 36a9a92ab..000000000 --- a/packages/tui/input/go.mod +++ /dev/null @@ -1,18 +0,0 @@ -module github.com/charmbracelet/x/input - -go 1.23.0 - -require ( - github.com/charmbracelet/x/ansi v0.9.3 - github.com/charmbracelet/x/windows v0.2.1 - github.com/muesli/cancelreader v0.2.2 - github.com/rivo/uniseg v0.4.7 - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e - golang.org/x/sys v0.33.0 -) - -require ( - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect -) diff --git a/packages/tui/input/go.sum b/packages/tui/input/go.sum deleted file mode 100644 index 7bc7a2ebd..000000000 --- a/packages/tui/input/go.sum +++ /dev/null @@ -1,19 +0,0 @@ -github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= -github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= -github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I= -github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/packages/tui/input/input.go b/packages/tui/input/input.go deleted file mode 100644 index da5e4f0be..000000000 --- a/packages/tui/input/input.go +++ /dev/null @@ -1,45 +0,0 @@ -package input - -import ( - "fmt" - "strings" -) - -// Event represents a terminal event. -type Event any - -// UnknownEvent represents an unknown event. -type UnknownEvent string - -// String returns a string representation of the unknown event. -func (e UnknownEvent) String() string { - return fmt.Sprintf("%q", string(e)) -} - -// MultiEvent represents multiple messages event. -type MultiEvent []Event - -// String returns a string representation of the multiple messages event. -func (e MultiEvent) String() string { - var sb strings.Builder - for _, ev := range e { - sb.WriteString(fmt.Sprintf("%v\n", ev)) - } - return sb.String() -} - -// WindowSizeEvent is used to report the terminal size. Note that Windows does -// not have support for reporting resizes via SIGWINCH signals and relies on -// the Windows Console API to report window size changes. -type WindowSizeEvent struct { - Width int - Height int -} - -// WindowOpEvent is a window operation (XTWINOPS) report event. This is used to -// report various window operations such as reporting the window size or cell -// size. -type WindowOpEvent struct { - Op int - Args []int -} diff --git a/packages/tui/input/key.go b/packages/tui/input/key.go deleted file mode 100644 index 8d3e3ebe3..000000000 --- a/packages/tui/input/key.go +++ /dev/null @@ -1,574 +0,0 @@ -package input - -import ( - "fmt" - "strings" - "unicode" - - "github.com/charmbracelet/x/ansi" -) - -const ( - // KeyExtended is a special key code used to signify that a key event - // contains multiple runes. - KeyExtended = unicode.MaxRune + 1 -) - -// Special key symbols. -const ( - - // Special keys. - - KeyUp rune = KeyExtended + iota + 1 - KeyDown - KeyRight - KeyLeft - KeyBegin - KeyFind - KeyInsert - KeyDelete - KeySelect - KeyPgUp - KeyPgDown - KeyHome - KeyEnd - - // Keypad keys. - - KeyKpEnter - KeyKpEqual - KeyKpMultiply - KeyKpPlus - KeyKpComma - KeyKpMinus - KeyKpDecimal - KeyKpDivide - KeyKp0 - KeyKp1 - KeyKp2 - KeyKp3 - KeyKp4 - KeyKp5 - KeyKp6 - KeyKp7 - KeyKp8 - KeyKp9 - - //nolint:godox - // The following are keys defined in the Kitty keyboard protocol. - // TODO: Investigate the names of these keys. - - KeyKpSep - KeyKpUp - KeyKpDown - KeyKpLeft - KeyKpRight - KeyKpPgUp - KeyKpPgDown - KeyKpHome - KeyKpEnd - KeyKpInsert - KeyKpDelete - KeyKpBegin - - // Function keys. - - KeyF1 - KeyF2 - KeyF3 - KeyF4 - KeyF5 - KeyF6 - KeyF7 - KeyF8 - KeyF9 - KeyF10 - KeyF11 - KeyF12 - KeyF13 - KeyF14 - KeyF15 - KeyF16 - KeyF17 - KeyF18 - KeyF19 - KeyF20 - KeyF21 - KeyF22 - KeyF23 - KeyF24 - KeyF25 - KeyF26 - KeyF27 - KeyF28 - KeyF29 - KeyF30 - KeyF31 - KeyF32 - KeyF33 - KeyF34 - KeyF35 - KeyF36 - KeyF37 - KeyF38 - KeyF39 - KeyF40 - KeyF41 - KeyF42 - KeyF43 - KeyF44 - KeyF45 - KeyF46 - KeyF47 - KeyF48 - KeyF49 - KeyF50 - KeyF51 - KeyF52 - KeyF53 - KeyF54 - KeyF55 - KeyF56 - KeyF57 - KeyF58 - KeyF59 - KeyF60 - KeyF61 - KeyF62 - KeyF63 - - //nolint:godox - // The following are keys defined in the Kitty keyboard protocol. - // TODO: Investigate the names of these keys. - - KeyCapsLock - KeyScrollLock - KeyNumLock - KeyPrintScreen - KeyPause - KeyMenu - - KeyMediaPlay - KeyMediaPause - KeyMediaPlayPause - KeyMediaReverse - KeyMediaStop - KeyMediaFastForward - KeyMediaRewind - KeyMediaNext - KeyMediaPrev - KeyMediaRecord - - KeyLowerVol - KeyRaiseVol - KeyMute - - KeyLeftShift - KeyLeftAlt - KeyLeftCtrl - KeyLeftSuper - KeyLeftHyper - KeyLeftMeta - KeyRightShift - KeyRightAlt - KeyRightCtrl - KeyRightSuper - KeyRightHyper - KeyRightMeta - KeyIsoLevel3Shift - KeyIsoLevel5Shift - - // Special names in C0. - - KeyBackspace = rune(ansi.DEL) - KeyTab = rune(ansi.HT) - KeyEnter = rune(ansi.CR) - KeyReturn = KeyEnter - KeyEscape = rune(ansi.ESC) - KeyEsc = KeyEscape - - // Special names in G0. - - KeySpace = rune(ansi.SP) -) - -// KeyPressEvent represents a key press event. -type KeyPressEvent Key - -// String implements [fmt.Stringer] and is quite useful for matching key -// events. For details, on what this returns see [Key.String]. -func (k KeyPressEvent) String() string { - return Key(k).String() -} - -// Keystroke returns the keystroke representation of the [Key]. While less type -// safe than looking at the individual fields, it will usually be more -// convenient and readable to use this method when matching against keys. -// -// Note that modifier keys are always printed in the following order: -// - ctrl -// - alt -// - shift -// - meta -// - hyper -// - super -// -// For example, you'll always see "ctrl+shift+alt+a" and never -// "shift+ctrl+alt+a". -func (k KeyPressEvent) Keystroke() string { - return Key(k).Keystroke() -} - -// Key returns the underlying key event. This is a syntactic sugar for casting -// the key event to a [Key]. -func (k KeyPressEvent) Key() Key { - return Key(k) -} - -// KeyReleaseEvent represents a key release event. -type KeyReleaseEvent Key - -// String implements [fmt.Stringer] and is quite useful for matching key -// events. For details, on what this returns see [Key.String]. -func (k KeyReleaseEvent) String() string { - return Key(k).String() -} - -// Keystroke returns the keystroke representation of the [Key]. While less type -// safe than looking at the individual fields, it will usually be more -// convenient and readable to use this method when matching against keys. -// -// Note that modifier keys are always printed in the following order: -// - ctrl -// - alt -// - shift -// - meta -// - hyper -// - super -// -// For example, you'll always see "ctrl+shift+alt+a" and never -// "shift+ctrl+alt+a". -func (k KeyReleaseEvent) Keystroke() string { - return Key(k).Keystroke() -} - -// Key returns the underlying key event. This is a convenience method and -// syntactic sugar to satisfy the [KeyEvent] interface, and cast the key event to -// [Key]. -func (k KeyReleaseEvent) Key() Key { - return Key(k) -} - -// KeyEvent represents a key event. This can be either a key press or a key -// release event. -type KeyEvent interface { - fmt.Stringer - - // Key returns the underlying key event. - Key() Key -} - -// Key represents a Key press or release event. It contains information about -// the Key pressed, like the runes, the type of Key, and the modifiers pressed. -// There are a couple general patterns you could use to check for key presses -// or releases: -// -// // Switch on the string representation of the key (shorter) -// switch ev := ev.(type) { -// case KeyPressEvent: -// switch ev.String() { -// case "enter": -// fmt.Println("you pressed enter!") -// case "a": -// fmt.Println("you pressed a!") -// } -// } -// -// // Switch on the key type (more foolproof) -// switch ev := ev.(type) { -// case KeyEvent: -// // catch both KeyPressEvent and KeyReleaseEvent -// switch key := ev.Key(); key.Code { -// case KeyEnter: -// fmt.Println("you pressed enter!") -// default: -// switch key.Text { -// case "a": -// fmt.Println("you pressed a!") -// } -// } -// } -// -// Note that [Key.Text] will be empty for special keys like [KeyEnter], -// [KeyTab], and for keys that don't represent printable characters like key -// combos with modifier keys. In other words, [Key.Text] is populated only for -// keys that represent printable characters shifted or unshifted (like 'a', -// 'A', '1', '!', etc.). -type Key struct { - // Text contains the actual characters received. This usually the same as - // [Key.Code]. When [Key.Text] is non-empty, it indicates that the key - // pressed represents printable character(s). - Text string - - // Mod represents modifier keys, like [ModCtrl], [ModAlt], and so on. - Mod KeyMod - - // Code represents the key pressed. This is usually a special key like - // [KeyTab], [KeyEnter], [KeyF1], or a printable character like 'a'. - Code rune - - // ShiftedCode is the actual, shifted key pressed by the user. For example, - // if the user presses shift+a, or caps lock is on, [Key.ShiftedCode] will - // be 'A' and [Key.Code] will be 'a'. - // - // In the case of non-latin keyboards, like Arabic, [Key.ShiftedCode] is the - // unshifted key on the keyboard. - // - // This is only available with the Kitty Keyboard Protocol or the Windows - // Console API. - ShiftedCode rune - - // BaseCode is the key pressed according to the standard PC-101 key layout. - // On international keyboards, this is the key that would be pressed if the - // keyboard was set to US PC-101 layout. - // - // For example, if the user presses 'q' on a French AZERTY keyboard, - // [Key.BaseCode] will be 'q'. - // - // This is only available with the Kitty Keyboard Protocol or the Windows - // Console API. - BaseCode rune - - // IsRepeat indicates whether the key is being held down and sending events - // repeatedly. - // - // This is only available with the Kitty Keyboard Protocol or the Windows - // Console API. - IsRepeat bool -} - -// String implements [fmt.Stringer] and is quite useful for matching key -// events. It will return the textual representation of the [Key] if there is -// one, otherwise, it will fallback to [Key.Keystroke]. -// -// For example, you'll always get "?" and instead of "shift+/" on a US ANSI -// keyboard. -func (k Key) String() string { - if len(k.Text) > 0 && k.Text != " " { - return k.Text - } - return k.Keystroke() -} - -// Keystroke returns the keystroke representation of the [Key]. While less type -// safe than looking at the individual fields, it will usually be more -// convenient and readable to use this method when matching against keys. -// -// Note that modifier keys are always printed in the following order: -// - ctrl -// - alt -// - shift -// - meta -// - hyper -// - super -// -// For example, you'll always see "ctrl+shift+alt+a" and never -// "shift+ctrl+alt+a". -func (k Key) Keystroke() string { - var sb strings.Builder - if k.Mod.Contains(ModCtrl) && k.Code != KeyLeftCtrl && k.Code != KeyRightCtrl { - sb.WriteString("ctrl+") - } - if k.Mod.Contains(ModAlt) && k.Code != KeyLeftAlt && k.Code != KeyRightAlt { - sb.WriteString("alt+") - } - if k.Mod.Contains(ModShift) && k.Code != KeyLeftShift && k.Code != KeyRightShift { - sb.WriteString("shift+") - } - if k.Mod.Contains(ModMeta) && k.Code != KeyLeftMeta && k.Code != KeyRightMeta { - sb.WriteString("meta+") - } - if k.Mod.Contains(ModHyper) && k.Code != KeyLeftHyper && k.Code != KeyRightHyper { - sb.WriteString("hyper+") - } - if k.Mod.Contains(ModSuper) && k.Code != KeyLeftSuper && k.Code != KeyRightSuper { - sb.WriteString("super+") - } - - if kt, ok := keyTypeString[k.Code]; ok { - sb.WriteString(kt) - } else { - code := k.Code - if k.BaseCode != 0 { - // If a [Key.BaseCode] is present, use it to represent a key using the standard - // PC-101 key layout. - code = k.BaseCode - } - - switch code { - case KeySpace: - // Space is the only invisible printable character. - sb.WriteString("space") - case KeyExtended: - // Write the actual text of the key when the key contains multiple - // runes. - sb.WriteString(k.Text) - default: - sb.WriteRune(code) - } - } - - return sb.String() -} - -var keyTypeString = map[rune]string{ - KeyEnter: "enter", - KeyTab: "tab", - KeyBackspace: "backspace", - KeyEscape: "esc", - KeySpace: "space", - KeyUp: "up", - KeyDown: "down", - KeyLeft: "left", - KeyRight: "right", - KeyBegin: "begin", - KeyFind: "find", - KeyInsert: "insert", - KeyDelete: "delete", - KeySelect: "select", - KeyPgUp: "pgup", - KeyPgDown: "pgdown", - KeyHome: "home", - KeyEnd: "end", - KeyKpEnter: "kpenter", - KeyKpEqual: "kpequal", - KeyKpMultiply: "kpmul", - KeyKpPlus: "kpplus", - KeyKpComma: "kpcomma", - KeyKpMinus: "kpminus", - KeyKpDecimal: "kpperiod", - KeyKpDivide: "kpdiv", - KeyKp0: "kp0", - KeyKp1: "kp1", - KeyKp2: "kp2", - KeyKp3: "kp3", - KeyKp4: "kp4", - KeyKp5: "kp5", - KeyKp6: "kp6", - KeyKp7: "kp7", - KeyKp8: "kp8", - KeyKp9: "kp9", - - // Kitty keyboard extension - KeyKpSep: "kpsep", - KeyKpUp: "kpup", - KeyKpDown: "kpdown", - KeyKpLeft: "kpleft", - KeyKpRight: "kpright", - KeyKpPgUp: "kppgup", - KeyKpPgDown: "kppgdown", - KeyKpHome: "kphome", - KeyKpEnd: "kpend", - KeyKpInsert: "kpinsert", - KeyKpDelete: "kpdelete", - KeyKpBegin: "kpbegin", - - KeyF1: "f1", - KeyF2: "f2", - KeyF3: "f3", - KeyF4: "f4", - KeyF5: "f5", - KeyF6: "f6", - KeyF7: "f7", - KeyF8: "f8", - KeyF9: "f9", - KeyF10: "f10", - KeyF11: "f11", - KeyF12: "f12", - KeyF13: "f13", - KeyF14: "f14", - KeyF15: "f15", - KeyF16: "f16", - KeyF17: "f17", - KeyF18: "f18", - KeyF19: "f19", - KeyF20: "f20", - KeyF21: "f21", - KeyF22: "f22", - KeyF23: "f23", - KeyF24: "f24", - KeyF25: "f25", - KeyF26: "f26", - KeyF27: "f27", - KeyF28: "f28", - KeyF29: "f29", - KeyF30: "f30", - KeyF31: "f31", - KeyF32: "f32", - KeyF33: "f33", - KeyF34: "f34", - KeyF35: "f35", - KeyF36: "f36", - KeyF37: "f37", - KeyF38: "f38", - KeyF39: "f39", - KeyF40: "f40", - KeyF41: "f41", - KeyF42: "f42", - KeyF43: "f43", - KeyF44: "f44", - KeyF45: "f45", - KeyF46: "f46", - KeyF47: "f47", - KeyF48: "f48", - KeyF49: "f49", - KeyF50: "f50", - KeyF51: "f51", - KeyF52: "f52", - KeyF53: "f53", - KeyF54: "f54", - KeyF55: "f55", - KeyF56: "f56", - KeyF57: "f57", - KeyF58: "f58", - KeyF59: "f59", - KeyF60: "f60", - KeyF61: "f61", - KeyF62: "f62", - KeyF63: "f63", - - // Kitty keyboard extension - KeyCapsLock: "capslock", - KeyScrollLock: "scrolllock", - KeyNumLock: "numlock", - KeyPrintScreen: "printscreen", - KeyPause: "pause", - KeyMenu: "menu", - KeyMediaPlay: "mediaplay", - KeyMediaPause: "mediapause", - KeyMediaPlayPause: "mediaplaypause", - KeyMediaReverse: "mediareverse", - KeyMediaStop: "mediastop", - KeyMediaFastForward: "mediafastforward", - KeyMediaRewind: "mediarewind", - KeyMediaNext: "medianext", - KeyMediaPrev: "mediaprev", - KeyMediaRecord: "mediarecord", - KeyLowerVol: "lowervol", - KeyRaiseVol: "raisevol", - KeyMute: "mute", - KeyLeftShift: "leftshift", - KeyLeftAlt: "leftalt", - KeyLeftCtrl: "leftctrl", - KeyLeftSuper: "leftsuper", - KeyLeftHyper: "lefthyper", - KeyLeftMeta: "leftmeta", - KeyRightShift: "rightshift", - KeyRightAlt: "rightalt", - KeyRightCtrl: "rightctrl", - KeyRightSuper: "rightsuper", - KeyRightHyper: "righthyper", - KeyRightMeta: "rightmeta", - KeyIsoLevel3Shift: "isolevel3shift", - KeyIsoLevel5Shift: "isolevel5shift", -} diff --git a/packages/tui/input/key_test.go b/packages/tui/input/key_test.go deleted file mode 100644 index b09f2f859..000000000 --- a/packages/tui/input/key_test.go +++ /dev/null @@ -1,880 +0,0 @@ -package input - -import ( - "bytes" - "context" - "errors" - "flag" - "fmt" - "image/color" - "io" - "math/rand" - "reflect" - "regexp" - "runtime" - "sort" - "strings" - "sync" - "testing" - "time" - - "github.com/charmbracelet/x/ansi" - "github.com/charmbracelet/x/ansi/kitty" -) - -var sequences = buildKeysTable(FlagTerminfo, "dumb") - -func TestKeyString(t *testing.T) { - t.Run("alt+space", func(t *testing.T) { - k := KeyPressEvent{Code: KeySpace, Mod: ModAlt} - if got := k.String(); got != "alt+space" { - t.Fatalf(`expected a "alt+space", got %q`, got) - } - }) - - t.Run("runes", func(t *testing.T) { - k := KeyPressEvent{Code: 'a', Text: "a"} - if got := k.String(); got != "a" { - t.Fatalf(`expected an "a", got %q`, got) - } - }) - - t.Run("invalid", func(t *testing.T) { - k := KeyPressEvent{Code: 99999} - if got := k.String(); got != "𘚟" { - t.Fatalf(`expected a "unknown", got %q`, got) - } - }) - - t.Run("space", func(t *testing.T) { - k := KeyPressEvent{Code: KeySpace, Text: " "} - if got := k.String(); got != "space" { - t.Fatalf(`expected a "space", got %q`, got) - } - }) - - t.Run("shift+space", func(t *testing.T) { - k := KeyPressEvent{Code: KeySpace, Mod: ModShift} - if got := k.String(); got != "shift+space" { - t.Fatalf(`expected a "shift+space", got %q`, got) - } - }) - - t.Run("?", func(t *testing.T) { - k := KeyPressEvent{Code: '/', Mod: ModShift, Text: "?"} - if got := k.String(); got != "?" { - t.Fatalf(`expected a "?", got %q`, got) - } - }) -} - -type seqTest struct { - seq []byte - Events []Event -} - -var f3CurPosRegexp = regexp.MustCompile(`\x1b\[1;(\d+)R`) - -// buildBaseSeqTests returns sequence tests that are valid for the -// detectSequence() function. -func buildBaseSeqTests() []seqTest { - td := []seqTest{} - for seq, key := range sequences { - k := KeyPressEvent(key) - st := seqTest{seq: []byte(seq), Events: []Event{k}} - - // XXX: This is a special case to handle F3 key sequence and cursor - // position report having the same sequence. See [parseCsi] for more - // information. - if f3CurPosRegexp.MatchString(seq) { - st.Events = []Event{k, CursorPositionEvent{Y: 0, X: int(key.Mod)}} - } - td = append(td, st) - } - - // Additional special cases. - td = append(td, - // Unrecognized CSI sequence. - seqTest{ - []byte{'\x1b', '[', '-', '-', '-', '-', 'X'}, - []Event{ - UnknownEvent([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'}), - }, - }, - // A lone space character. - seqTest{ - []byte{' '}, - []Event{ - KeyPressEvent{Code: KeySpace, Text: " "}, - }, - }, - // An escape character with the alt modifier. - seqTest{ - []byte{'\x1b', ' '}, - []Event{ - KeyPressEvent{Code: KeySpace, Mod: ModAlt}, - }, - }, - ) - return td -} - -func TestParseSequence(t *testing.T) { - td := buildBaseSeqTests() - td = append(td, - // Background color. - seqTest{ - []byte("\x1b]11;rgb:1234/1234/1234\x07"), - []Event{BackgroundColorEvent{ - Color: color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff}, - }}, - }, - seqTest{ - []byte("\x1b]11;rgb:1234/1234/1234\x1b\\"), - []Event{BackgroundColorEvent{ - Color: color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff}, - }}, - }, - seqTest{ - []byte("\x1b]11;rgb:1234/1234/1234\x1b"), // Incomplete sequences are ignored. - []Event{ - UnknownEvent("\x1b]11;rgb:1234/1234/1234\x1b"), - }, - }, - - // Kitty Graphics response. - seqTest{ - []byte("\x1b_Ga=t;OK\x1b\\"), - []Event{KittyGraphicsEvent{ - Options: kitty.Options{Action: kitty.Transmit}, - Payload: []byte("OK"), - }}, - }, - seqTest{ - []byte("\x1b_Gi=99,I=13;OK\x1b\\"), - []Event{KittyGraphicsEvent{ - Options: kitty.Options{ID: 99, Number: 13}, - Payload: []byte("OK"), - }}, - }, - seqTest{ - []byte("\x1b_Gi=1337,q=1;EINVAL:your face\x1b\\"), - []Event{KittyGraphicsEvent{ - Options: kitty.Options{ID: 1337, Quite: 1}, - Payload: []byte("EINVAL:your face"), - }}, - }, - - // Xterm modifyOtherKeys CSI 27 ; <modifier> ; <code> ~ - seqTest{ - []byte("\x1b[27;3;20320~"), - []Event{KeyPressEvent{Code: '你', Mod: ModAlt}}, - }, - seqTest{ - []byte("\x1b[27;3;65~"), - []Event{KeyPressEvent{Code: 'A', Mod: ModAlt}}, - }, - seqTest{ - []byte("\x1b[27;3;8~"), - []Event{KeyPressEvent{Code: KeyBackspace, Mod: ModAlt}}, - }, - seqTest{ - []byte("\x1b[27;3;27~"), - []Event{KeyPressEvent{Code: KeyEscape, Mod: ModAlt}}, - }, - seqTest{ - []byte("\x1b[27;3;127~"), - []Event{KeyPressEvent{Code: KeyBackspace, Mod: ModAlt}}, - }, - - // Xterm report window text area size. - seqTest{ - []byte("\x1b[4;24;80t"), - []Event{ - WindowOpEvent{Op: 4, Args: []int{24, 80}}, - }, - }, - - // Kitty keyboard / CSI u (fixterms) - seqTest{ - []byte("\x1b[1B"), - []Event{KeyPressEvent{Code: KeyDown}}, - }, - seqTest{ - []byte("\x1b[1;B"), - []Event{KeyPressEvent{Code: KeyDown}}, - }, - seqTest{ - []byte("\x1b[1;4B"), - []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyDown}}, - }, - seqTest{ - []byte("\x1b[1;4:1B"), - []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyDown}}, - }, - seqTest{ - []byte("\x1b[1;4:2B"), - []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyDown, IsRepeat: true}}, - }, - seqTest{ - []byte("\x1b[1;4:3B"), - []Event{KeyReleaseEvent{Mod: ModShift | ModAlt, Code: KeyDown}}, - }, - seqTest{ - []byte("\x1b[8~"), - []Event{KeyPressEvent{Code: KeyEnd}}, - }, - seqTest{ - []byte("\x1b[8;~"), - []Event{KeyPressEvent{Code: KeyEnd}}, - }, - seqTest{ - []byte("\x1b[8;10~"), - []Event{KeyPressEvent{Mod: ModShift | ModMeta, Code: KeyEnd}}, - }, - seqTest{ - []byte("\x1b[27;4u"), - []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyEscape}}, - }, - seqTest{ - []byte("\x1b[127;4u"), - []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyBackspace}}, - }, - seqTest{ - []byte("\x1b[57358;4u"), - []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyCapsLock}}, - }, - seqTest{ - []byte("\x1b[9;2u"), - []Event{KeyPressEvent{Mod: ModShift, Code: KeyTab}}, - }, - seqTest{ - []byte("\x1b[195;u"), - []Event{KeyPressEvent{Text: "Ã", Code: 'Ã'}}, - }, - seqTest{ - []byte("\x1b[20320;2u"), - []Event{KeyPressEvent{Text: "你", Mod: ModShift, Code: '你'}}, - }, - seqTest{ - []byte("\x1b[195;:1u"), - []Event{KeyPressEvent{Text: "Ã", Code: 'Ã'}}, - }, - seqTest{ - []byte("\x1b[195;2:3u"), - []Event{KeyReleaseEvent{Code: 'Ã', Text: "Ã", Mod: ModShift}}, - }, - seqTest{ - []byte("\x1b[195;2:2u"), - []Event{KeyPressEvent{Code: 'Ã', Text: "Ã", IsRepeat: true, Mod: ModShift}}, - }, - seqTest{ - []byte("\x1b[195;2:1u"), - []Event{KeyPressEvent{Code: 'Ã', Text: "Ã", Mod: ModShift}}, - }, - seqTest{ - []byte("\x1b[195;2:3u"), - []Event{KeyReleaseEvent{Code: 'Ã', Text: "Ã", Mod: ModShift}}, - }, - seqTest{ - []byte("\x1b[97;2;65u"), - []Event{KeyPressEvent{Code: 'a', Text: "A", Mod: ModShift}}, - }, - seqTest{ - []byte("\x1b[97;;229u"), - []Event{KeyPressEvent{Code: 'a', Text: "å"}}, - }, - - // focus/blur - seqTest{ - []byte{'\x1b', '[', 'I'}, - []Event{ - FocusEvent{}, - }, - }, - seqTest{ - []byte{'\x1b', '[', 'O'}, - []Event{ - BlurEvent{}, - }, - }, - // Mouse event. - seqTest{ - []byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)}, - []Event{ - MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelUp}, - }, - }, - // SGR Mouse event. - seqTest{ - []byte("\x1b[<0;33;17M"), - []Event{ - MouseClickEvent{X: 32, Y: 16, Button: MouseLeft}, - }, - }, - // Runes. - seqTest{ - []byte{'a'}, - []Event{ - KeyPressEvent{Code: 'a', Text: "a"}, - }, - }, - seqTest{ - []byte{'\x1b', 'a'}, - []Event{ - KeyPressEvent{Code: 'a', Mod: ModAlt}, - }, - }, - seqTest{ - []byte{'a', 'a', 'a'}, - []Event{ - KeyPressEvent{Code: 'a', Text: "a"}, - KeyPressEvent{Code: 'a', Text: "a"}, - KeyPressEvent{Code: 'a', Text: "a"}, - }, - }, - // Multi-byte rune. - seqTest{ - []byte("☃"), - []Event{ - KeyPressEvent{Code: '☃', Text: "☃"}, - }, - }, - seqTest{ - []byte("\x1b☃"), - []Event{ - KeyPressEvent{Code: '☃', Mod: ModAlt}, - }, - }, - // Standalone control characters. - seqTest{ - []byte{'\x1b'}, - []Event{ - KeyPressEvent{Code: KeyEscape}, - }, - }, - seqTest{ - []byte{ansi.SOH}, - []Event{ - KeyPressEvent{Code: 'a', Mod: ModCtrl}, - }, - }, - seqTest{ - []byte{'\x1b', ansi.SOH}, - []Event{ - KeyPressEvent{Code: 'a', Mod: ModCtrl | ModAlt}, - }, - }, - seqTest{ - []byte{ansi.NUL}, - []Event{ - KeyPressEvent{Code: KeySpace, Mod: ModCtrl}, - }, - }, - seqTest{ - []byte{'\x1b', ansi.NUL}, - []Event{ - KeyPressEvent{Code: KeySpace, Mod: ModCtrl | ModAlt}, - }, - }, - // C1 control characters. - seqTest{ - []byte{'\x80'}, - []Event{ - KeyPressEvent{Code: rune(0x80 - '@'), Mod: ModCtrl | ModAlt}, - }, - }, - ) - - if runtime.GOOS != "windows" { - // Sadly, utf8.DecodeRune([]byte(0xfe)) returns a valid rune on windows. - // This is incorrect, but it makes our test fail if we try it out. - td = append(td, seqTest{ - []byte{'\xfe'}, - []Event{ - UnknownEvent(rune(0xfe)), - }, - }) - } - - var p Parser - for _, tc := range td { - t.Run(fmt.Sprintf("%q", string(tc.seq)), func(t *testing.T) { - var events []Event - buf := tc.seq - for len(buf) > 0 { - width, Event := p.parseSequence(buf) - switch Event := Event.(type) { - case MultiEvent: - events = append(events, Event...) - default: - events = append(events, Event) - } - buf = buf[width:] - } - if !reflect.DeepEqual(tc.Events, events) { - t.Errorf("\nexpected event for %q:\n %#v\ngot:\n %#v", tc.seq, tc.Events, events) - } - }) - } -} - -func TestReadLongInput(t *testing.T) { - expect := make([]Event, 1000) - for i := range 1000 { - expect[i] = KeyPressEvent{Code: 'a', Text: "a"} - } - input := strings.Repeat("a", 1000) - drv, err := NewReader(strings.NewReader(input), "dumb", 0) - if err != nil { - t.Fatalf("unexpected input driver error: %v", err) - } - - var Events []Event - for { - events, err := drv.ReadEvents() - if err == io.EOF { - break - } - if err != nil { - t.Fatalf("unexpected input error: %v", err) - } - Events = append(Events, events...) - } - - if !reflect.DeepEqual(expect, Events) { - t.Errorf("unexpected messages, expected:\n %+v\ngot:\n %+v", expect, Events) - } -} - -func TestReadInput(t *testing.T) { - type test struct { - keyname string - in []byte - out []Event - } - testData := []test{ - { - "a", - []byte{'a'}, - []Event{ - KeyPressEvent{Code: 'a', Text: "a"}, - }, - }, - { - "space", - []byte{' '}, - []Event{ - KeyPressEvent{Code: KeySpace, Text: " "}, - }, - }, - { - "a alt+a", - []byte{'a', '\x1b', 'a'}, - []Event{ - KeyPressEvent{Code: 'a', Text: "a"}, - KeyPressEvent{Code: 'a', Mod: ModAlt}, - }, - }, - { - "a alt+a a", - []byte{'a', '\x1b', 'a', 'a'}, - []Event{ - KeyPressEvent{Code: 'a', Text: "a"}, - KeyPressEvent{Code: 'a', Mod: ModAlt}, - KeyPressEvent{Code: 'a', Text: "a"}, - }, - }, - { - "ctrl+a", - []byte{byte(ansi.SOH)}, - []Event{ - KeyPressEvent{Code: 'a', Mod: ModCtrl}, - }, - }, - { - "ctrl+a ctrl+b", - []byte{byte(ansi.SOH), byte(ansi.STX)}, - []Event{ - KeyPressEvent{Code: 'a', Mod: ModCtrl}, - KeyPressEvent{Code: 'b', Mod: ModCtrl}, - }, - }, - { - "alt+a", - []byte{byte(0x1b), 'a'}, - []Event{ - KeyPressEvent{Code: 'a', Mod: ModAlt}, - }, - }, - { - "a b c d", - []byte{'a', 'b', 'c', 'd'}, - []Event{ - KeyPressEvent{Code: 'a', Text: "a"}, - KeyPressEvent{Code: 'b', Text: "b"}, - KeyPressEvent{Code: 'c', Text: "c"}, - KeyPressEvent{Code: 'd', Text: "d"}, - }, - }, - { - "up", - []byte("\x1b[A"), - []Event{ - KeyPressEvent{Code: KeyUp}, - }, - }, - { - "wheel up", - []byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)}, - []Event{ - MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelUp}, - }, - }, - { - "left motion release", - []byte{ - '\x1b', '[', 'M', byte(32) + 0b0010_0000, byte(32 + 33), byte(16 + 33), - '\x1b', '[', 'M', byte(32) + 0b0000_0011, byte(64 + 33), byte(32 + 33), - }, - []Event{ - MouseMotionEvent{X: 32, Y: 16, Button: MouseLeft}, - MouseReleaseEvent{X: 64, Y: 32, Button: MouseNone}, - }, - }, - { - "shift+tab", - []byte{'\x1b', '[', 'Z'}, - []Event{ - KeyPressEvent{Code: KeyTab, Mod: ModShift}, - }, - }, - { - "enter", - []byte{'\r'}, - []Event{KeyPressEvent{Code: KeyEnter}}, - }, - { - "alt+enter", - []byte{'\x1b', '\r'}, - []Event{ - KeyPressEvent{Code: KeyEnter, Mod: ModAlt}, - }, - }, - { - "insert", - []byte{'\x1b', '[', '2', '~'}, - []Event{ - KeyPressEvent{Code: KeyInsert}, - }, - }, - { - "ctrl+alt+a", - []byte{'\x1b', byte(ansi.SOH)}, - []Event{ - KeyPressEvent{Code: 'a', Mod: ModCtrl | ModAlt}, - }, - }, - { - "CSI?----X?", - []byte{'\x1b', '[', '-', '-', '-', '-', 'X'}, - []Event{UnknownEvent([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'})}, - }, - // Powershell sequences. - { - "up", - []byte{'\x1b', 'O', 'A'}, - []Event{KeyPressEvent{Code: KeyUp}}, - }, - { - "down", - []byte{'\x1b', 'O', 'B'}, - []Event{KeyPressEvent{Code: KeyDown}}, - }, - { - "right", - []byte{'\x1b', 'O', 'C'}, - []Event{KeyPressEvent{Code: KeyRight}}, - }, - { - "left", - []byte{'\x1b', 'O', 'D'}, - []Event{KeyPressEvent{Code: KeyLeft}}, - }, - { - "alt+enter", - []byte{'\x1b', '\x0d'}, - []Event{KeyPressEvent{Code: KeyEnter, Mod: ModAlt}}, - }, - { - "alt+backspace", - []byte{'\x1b', '\x7f'}, - []Event{KeyPressEvent{Code: KeyBackspace, Mod: ModAlt}}, - }, - { - "ctrl+space", - []byte{'\x00'}, - []Event{KeyPressEvent{Code: KeySpace, Mod: ModCtrl}}, - }, - { - "ctrl+alt+space", - []byte{'\x1b', '\x00'}, - []Event{KeyPressEvent{Code: KeySpace, Mod: ModCtrl | ModAlt}}, - }, - { - "esc", - []byte{'\x1b'}, - []Event{KeyPressEvent{Code: KeyEscape}}, - }, - { - "alt+esc", - []byte{'\x1b', '\x1b'}, - []Event{KeyPressEvent{Code: KeyEscape, Mod: ModAlt}}, - }, - { - "a b o", - []byte{ - '\x1b', '[', '2', '0', '0', '~', - 'a', ' ', 'b', - '\x1b', '[', '2', '0', '1', '~', - 'o', - }, - []Event{ - PasteStartEvent{}, - PasteEvent("a b"), - PasteEndEvent{}, - KeyPressEvent{Code: 'o', Text: "o"}, - }, - }, - { - "a\x03\nb", - []byte{ - '\x1b', '[', '2', '0', '0', '~', - 'a', '\x03', '\n', 'b', - '\x1b', '[', '2', '0', '1', '~', - }, - []Event{ - PasteStartEvent{}, - PasteEvent("a\x03\nb"), - PasteEndEvent{}, - }, - }, - { - "?0xfe?", - []byte{'\xfe'}, - []Event{ - UnknownEvent(rune(0xfe)), - }, - }, - { - "a ?0xfe? b", - []byte{'a', '\xfe', ' ', 'b'}, - []Event{ - KeyPressEvent{Code: 'a', Text: "a"}, - UnknownEvent(rune(0xfe)), - KeyPressEvent{Code: KeySpace, Text: " "}, - KeyPressEvent{Code: 'b', Text: "b"}, - }, - }, - } - - for i, td := range testData { - t.Run(fmt.Sprintf("%d: %s", i, td.keyname), func(t *testing.T) { - Events := testReadInputs(t, bytes.NewReader(td.in)) - var buf strings.Builder - for i, Event := range Events { - if i > 0 { - buf.WriteByte(' ') - } - if s, ok := Event.(fmt.Stringer); ok { - buf.WriteString(s.String()) - } else { - fmt.Fprintf(&buf, "%#v:%T", Event, Event) - } - } - - if len(Events) != len(td.out) { - t.Fatalf("unexpected message list length: got %d, expected %d\n got: %#v\n expected: %#v\n", len(Events), len(td.out), Events, td.out) - } - - if !reflect.DeepEqual(td.out, Events) { - t.Fatalf("expected:\n%#v\ngot:\n%#v", td.out, Events) - } - }) - } -} - -func testReadInputs(t *testing.T, input io.Reader) []Event { - // We'll check that the input reader finishes at the end - // without error. - var wg sync.WaitGroup - var inputErr error - ctx, cancel := context.WithCancel(context.Background()) - defer func() { - cancel() - wg.Wait() - if inputErr != nil && !errors.Is(inputErr, io.EOF) { - t.Fatalf("unexpected input error: %v", inputErr) - } - }() - - dr, err := NewReader(input, "dumb", 0) - if err != nil { - t.Fatalf("unexpected input driver error: %v", err) - } - - // The messages we're consuming. - EventsC := make(chan Event) - - // Start the reader in the background. - wg.Add(1) - go func() { - defer wg.Done() - var events []Event - events, inputErr = dr.ReadEvents() - out: - for _, ev := range events { - select { - case EventsC <- ev: - case <-ctx.Done(): - break out - } - } - EventsC <- nil - }() - - var Events []Event -loop: - for { - select { - case Event := <-EventsC: - if Event == nil { - // end of input marker for the test. - break loop - } - Events = append(Events, Event) - case <-time.After(2 * time.Second): - t.Errorf("timeout waiting for input event") - break loop - } - } - return Events -} - -// randTest defines the test input and expected output for a sequence -// of interleaved control sequences and control characters. -type randTest struct { - data []byte - lengths []int - names []string -} - -// seed is the random seed to randomize the input. This helps check -// that all the sequences get ultimately exercised. -var seed = flag.Int64("seed", 0, "random seed (0 to autoselect)") - -// genRandomData generates a randomized test, with a random seed unless -// the seed flag was set. -func genRandomData(logfn func(int64), length int) randTest { - // We'll use a random source. However, we give the user the option - // to override it to a specific value for reproducibility. - s := *seed - if s == 0 { - s = time.Now().UnixNano() - } - // Inform the user so they know what to reuse to get the same data. - logfn(s) - return genRandomDataWithSeed(s, length) -} - -// genRandomDataWithSeed generates a randomized test with a fixed seed. -func genRandomDataWithSeed(s int64, length int) randTest { - src := rand.NewSource(s) - r := rand.New(src) - - // allseqs contains all the sequences, in sorted order. We sort - // to make the test deterministic (when the seed is also fixed). - type seqpair struct { - seq string - name string - } - var allseqs []seqpair - for seq, key := range sequences { - allseqs = append(allseqs, seqpair{seq, key.String()}) - } - sort.Slice(allseqs, func(i, j int) bool { return allseqs[i].seq < allseqs[j].seq }) - - // res contains the computed test. - var res randTest - - for len(res.data) < length { - alt := r.Intn(2) - prefix := "" - esclen := 0 - if alt == 1 { - prefix = "alt+" - esclen = 1 - } - kind := r.Intn(3) - switch kind { - case 0: - // A control character. - if alt == 1 { - res.data = append(res.data, '\x1b') - } - res.data = append(res.data, 1) - res.names = append(res.names, "ctrl+"+prefix+"a") - res.lengths = append(res.lengths, 1+esclen) - - case 1, 2: - // A sequence. - seqi := r.Intn(len(allseqs)) - s := allseqs[seqi] - if strings.Contains(s.name, "alt+") || strings.Contains(s.name, "meta+") { - esclen = 0 - prefix = "" - alt = 0 - } - if alt == 1 { - res.data = append(res.data, '\x1b') - } - res.data = append(res.data, s.seq...) - if strings.HasPrefix(s.name, "ctrl+") { - prefix = "ctrl+" + prefix - } - name := prefix + strings.TrimPrefix(s.name, "ctrl+") - res.names = append(res.names, name) - res.lengths = append(res.lengths, len(s.seq)+esclen) - } - } - return res -} - -func FuzzParseSequence(f *testing.F) { - var p Parser - for seq := range sequences { - f.Add(seq) - } - f.Add("\x1b]52;?\x07") // OSC 52 - f.Add("\x1b]11;rgb:0000/0000/0000\x1b\\") // OSC 11 - f.Add("\x1bP>|charm terminal(0.1.2)\x1b\\") // DCS (XTVERSION) - f.Add("\x1b_Gi=123\x1b\\") // APC - f.Fuzz(func(t *testing.T, seq string) { - n, _ := p.parseSequence([]byte(seq)) - if n == 0 && seq != "" { - t.Errorf("expected a non-zero width for %q", seq) - } - }) -} - -// BenchmarkDetectSequenceMap benchmarks the map-based sequence -// detector. -func BenchmarkDetectSequenceMap(b *testing.B) { - var p Parser - td := genRandomDataWithSeed(123, 10000) - for i := 0; i < b.N; i++ { - for j, w := 0, 0; j < len(td.data); j += w { - w, _ = p.parseSequence(td.data[j:]) - } - } -} diff --git a/packages/tui/input/kitty.go b/packages/tui/input/kitty.go deleted file mode 100644 index 4da00b502..000000000 --- a/packages/tui/input/kitty.go +++ /dev/null @@ -1,353 +0,0 @@ -package input - -import ( - "unicode" - "unicode/utf8" - - "github.com/charmbracelet/x/ansi" - "github.com/charmbracelet/x/ansi/kitty" -) - -// KittyGraphicsEvent represents a Kitty Graphics response event. -// -// See https://sw.kovidgoyal.net/kitty/graphics-protocol/ -type KittyGraphicsEvent struct { - Options kitty.Options - Payload []byte -} - -// KittyEnhancementsEvent represents a Kitty enhancements event. -type KittyEnhancementsEvent int - -// Kitty keyboard enhancement constants. -// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement -const ( - KittyDisambiguateEscapeCodes KittyEnhancementsEvent = 1 << iota - KittyReportEventTypes - KittyReportAlternateKeys - KittyReportAllKeysAsEscapeCodes - KittyReportAssociatedText -) - -// Contains reports whether m contains the given enhancements. -func (e KittyEnhancementsEvent) Contains(enhancements KittyEnhancementsEvent) bool { - return e&enhancements == enhancements -} - -// Kitty Clipboard Control Sequences. -var kittyKeyMap = map[int]Key{ - ansi.BS: {Code: KeyBackspace}, - ansi.HT: {Code: KeyTab}, - ansi.CR: {Code: KeyEnter}, - ansi.ESC: {Code: KeyEscape}, - ansi.DEL: {Code: KeyBackspace}, - - 57344: {Code: KeyEscape}, - 57345: {Code: KeyEnter}, - 57346: {Code: KeyTab}, - 57347: {Code: KeyBackspace}, - 57348: {Code: KeyInsert}, - 57349: {Code: KeyDelete}, - 57350: {Code: KeyLeft}, - 57351: {Code: KeyRight}, - 57352: {Code: KeyUp}, - 57353: {Code: KeyDown}, - 57354: {Code: KeyPgUp}, - 57355: {Code: KeyPgDown}, - 57356: {Code: KeyHome}, - 57357: {Code: KeyEnd}, - 57358: {Code: KeyCapsLock}, - 57359: {Code: KeyScrollLock}, - 57360: {Code: KeyNumLock}, - 57361: {Code: KeyPrintScreen}, - 57362: {Code: KeyPause}, - 57363: {Code: KeyMenu}, - 57364: {Code: KeyF1}, - 57365: {Code: KeyF2}, - 57366: {Code: KeyF3}, - 57367: {Code: KeyF4}, - 57368: {Code: KeyF5}, - 57369: {Code: KeyF6}, - 57370: {Code: KeyF7}, - 57371: {Code: KeyF8}, - 57372: {Code: KeyF9}, - 57373: {Code: KeyF10}, - 57374: {Code: KeyF11}, - 57375: {Code: KeyF12}, - 57376: {Code: KeyF13}, - 57377: {Code: KeyF14}, - 57378: {Code: KeyF15}, - 57379: {Code: KeyF16}, - 57380: {Code: KeyF17}, - 57381: {Code: KeyF18}, - 57382: {Code: KeyF19}, - 57383: {Code: KeyF20}, - 57384: {Code: KeyF21}, - 57385: {Code: KeyF22}, - 57386: {Code: KeyF23}, - 57387: {Code: KeyF24}, - 57388: {Code: KeyF25}, - 57389: {Code: KeyF26}, - 57390: {Code: KeyF27}, - 57391: {Code: KeyF28}, - 57392: {Code: KeyF29}, - 57393: {Code: KeyF30}, - 57394: {Code: KeyF31}, - 57395: {Code: KeyF32}, - 57396: {Code: KeyF33}, - 57397: {Code: KeyF34}, - 57398: {Code: KeyF35}, - 57399: {Code: KeyKp0}, - 57400: {Code: KeyKp1}, - 57401: {Code: KeyKp2}, - 57402: {Code: KeyKp3}, - 57403: {Code: KeyKp4}, - 57404: {Code: KeyKp5}, - 57405: {Code: KeyKp6}, - 57406: {Code: KeyKp7}, - 57407: {Code: KeyKp8}, - 57408: {Code: KeyKp9}, - 57409: {Code: KeyKpDecimal}, - 57410: {Code: KeyKpDivide}, - 57411: {Code: KeyKpMultiply}, - 57412: {Code: KeyKpMinus}, - 57413: {Code: KeyKpPlus}, - 57414: {Code: KeyKpEnter}, - 57415: {Code: KeyKpEqual}, - 57416: {Code: KeyKpSep}, - 57417: {Code: KeyKpLeft}, - 57418: {Code: KeyKpRight}, - 57419: {Code: KeyKpUp}, - 57420: {Code: KeyKpDown}, - 57421: {Code: KeyKpPgUp}, - 57422: {Code: KeyKpPgDown}, - 57423: {Code: KeyKpHome}, - 57424: {Code: KeyKpEnd}, - 57425: {Code: KeyKpInsert}, - 57426: {Code: KeyKpDelete}, - 57427: {Code: KeyKpBegin}, - 57428: {Code: KeyMediaPlay}, - 57429: {Code: KeyMediaPause}, - 57430: {Code: KeyMediaPlayPause}, - 57431: {Code: KeyMediaReverse}, - 57432: {Code: KeyMediaStop}, - 57433: {Code: KeyMediaFastForward}, - 57434: {Code: KeyMediaRewind}, - 57435: {Code: KeyMediaNext}, - 57436: {Code: KeyMediaPrev}, - 57437: {Code: KeyMediaRecord}, - 57438: {Code: KeyLowerVol}, - 57439: {Code: KeyRaiseVol}, - 57440: {Code: KeyMute}, - 57441: {Code: KeyLeftShift}, - 57442: {Code: KeyLeftCtrl}, - 57443: {Code: KeyLeftAlt}, - 57444: {Code: KeyLeftSuper}, - 57445: {Code: KeyLeftHyper}, - 57446: {Code: KeyLeftMeta}, - 57447: {Code: KeyRightShift}, - 57448: {Code: KeyRightCtrl}, - 57449: {Code: KeyRightAlt}, - 57450: {Code: KeyRightSuper}, - 57451: {Code: KeyRightHyper}, - 57452: {Code: KeyRightMeta}, - 57453: {Code: KeyIsoLevel3Shift}, - 57454: {Code: KeyIsoLevel5Shift}, -} - -func init() { - // These are some faulty C0 mappings some terminals such as WezTerm have - // and doesn't follow the specs. - kittyKeyMap[ansi.NUL] = Key{Code: KeySpace, Mod: ModCtrl} - for i := ansi.SOH; i <= ansi.SUB; i++ { - if _, ok := kittyKeyMap[i]; !ok { - kittyKeyMap[i] = Key{Code: rune(i + 0x60), Mod: ModCtrl} - } - } - for i := ansi.FS; i <= ansi.US; i++ { - if _, ok := kittyKeyMap[i]; !ok { - kittyKeyMap[i] = Key{Code: rune(i + 0x40), Mod: ModCtrl} - } - } -} - -const ( - kittyShift = 1 << iota - kittyAlt - kittyCtrl - kittySuper - kittyHyper - kittyMeta - kittyCapsLock - kittyNumLock -) - -func fromKittyMod(mod int) KeyMod { - var m KeyMod - if mod&kittyShift != 0 { - m |= ModShift - } - if mod&kittyAlt != 0 { - m |= ModAlt - } - if mod&kittyCtrl != 0 { - m |= ModCtrl - } - if mod&kittySuper != 0 { - m |= ModSuper - } - if mod&kittyHyper != 0 { - m |= ModHyper - } - if mod&kittyMeta != 0 { - m |= ModMeta - } - if mod&kittyCapsLock != 0 { - m |= ModCapsLock - } - if mod&kittyNumLock != 0 { - m |= ModNumLock - } - return m -} - -// parseKittyKeyboard parses a Kitty Keyboard Protocol sequence. -// -// In `CSI u`, this is parsed as: -// -// CSI codepoint ; modifiers u -// codepoint: ASCII Dec value -// -// The Kitty Keyboard Protocol extends this with optional components that can be -// enabled progressively. The full sequence is parsed as: -// -// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u -// -// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/ -func parseKittyKeyboard(params ansi.Params) (Event Event) { - var isRelease bool - var key Key - - // The index of parameters separated by semicolons ';'. Sub parameters are - // separated by colons ':'. - var paramIdx int - var sudIdx int // The sub parameter index - for _, p := range params { - // Kitty Keyboard Protocol has 3 optional components. - switch paramIdx { - case 0: - switch sudIdx { - case 0: - var foundKey bool - code := p.Param(1) // CSI u has a default value of 1 - key, foundKey = kittyKeyMap[code] - if !foundKey { - r := rune(code) - if !utf8.ValidRune(r) { - r = utf8.RuneError - } - - key.Code = r - } - - case 2: - // shifted key + base key - if b := rune(p.Param(1)); unicode.IsPrint(b) { - // XXX: When alternate key reporting is enabled, the protocol - // can return 3 things, the unicode codepoint of the key, - // the shifted codepoint of the key, and the standard - // PC-101 key layout codepoint. - // This is useful to create an unambiguous mapping of keys - // when using a different language layout. - key.BaseCode = b - } - fallthrough - - case 1: - // shifted key - if s := rune(p.Param(1)); unicode.IsPrint(s) { - // XXX: We swap keys here because we want the shifted key - // to be the Rune that is returned by the event. - // For example, shift+a should produce "A" not "a". - // In such a case, we set AltRune to the original key "a" - // and Rune to "A". - key.ShiftedCode = s - } - } - case 1: - switch sudIdx { - case 0: - mod := p.Param(1) - if mod > 1 { - key.Mod = fromKittyMod(mod - 1) - if key.Mod > ModShift { - // XXX: We need to clear the text if we have a modifier key - // other than a [ModShift] key. - key.Text = "" - } - } - - case 1: - switch p.Param(1) { - case 2: - key.IsRepeat = true - case 3: - isRelease = true - } - case 2: - } - case 2: - if code := p.Param(0); code != 0 { - key.Text += string(rune(code)) - } - } - - sudIdx++ - if !p.HasMore() { - paramIdx++ - sudIdx = 0 - } - } - - //nolint:nestif - if len(key.Text) == 0 && unicode.IsPrint(key.Code) && - (key.Mod <= ModShift || key.Mod == ModCapsLock || key.Mod == ModShift|ModCapsLock) { - if key.Mod == 0 { - key.Text = string(key.Code) - } else { - desiredCase := unicode.ToLower - if key.Mod.Contains(ModShift) || key.Mod.Contains(ModCapsLock) { - desiredCase = unicode.ToUpper - } - if key.ShiftedCode != 0 { - key.Text = string(key.ShiftedCode) - } else { - key.Text = string(desiredCase(key.Code)) - } - } - } - - if isRelease { - return KeyReleaseEvent(key) - } - - return KeyPressEvent(key) -} - -// parseKittyKeyboardExt parses a Kitty Keyboard Protocol sequence extensions -// for non CSI u sequences. This includes things like CSI A, SS3 A and others, -// and CSI ~. -func parseKittyKeyboardExt(params ansi.Params, k KeyPressEvent) Event { - // Handle Kitty keyboard protocol - if len(params) > 2 && // We have at least 3 parameters - params[0].Param(1) == 1 && // The first parameter is 1 (defaults to 1) - params[1].HasMore() { // The second parameter is a subparameter (separated by a ":") - switch params[2].Param(1) { // The third parameter is the event type (defaults to 1) - case 2: - k.IsRepeat = true - case 3: - return KeyReleaseEvent(k) - } - } - return k -} diff --git a/packages/tui/input/mod.go b/packages/tui/input/mod.go deleted file mode 100644 index c00762769..000000000 --- a/packages/tui/input/mod.go +++ /dev/null @@ -1,37 +0,0 @@ -package input - -// KeyMod represents modifier keys. -type KeyMod int - -// Modifier keys. -const ( - ModShift KeyMod = 1 << iota - ModAlt - ModCtrl - ModMeta - - // These modifiers are used with the Kitty protocol. - // XXX: Meta and Super are swapped in the Kitty protocol, - // this is to preserve compatibility with XTerm modifiers. - - ModHyper - ModSuper // Windows/Command keys - - // These are key lock states. - - ModCapsLock - ModNumLock - ModScrollLock // Defined in Windows API only -) - -// Contains reports whether m contains the given modifiers. -// -// Example: -// -// m := ModAlt | ModCtrl -// m.Contains(ModCtrl) // true -// m.Contains(ModAlt | ModCtrl) // true -// m.Contains(ModAlt | ModCtrl | ModShift) // false -func (m KeyMod) Contains(mods KeyMod) bool { - return m&mods == mods -} diff --git a/packages/tui/input/mode.go b/packages/tui/input/mode.go deleted file mode 100644 index ea1ba571d..000000000 --- a/packages/tui/input/mode.go +++ /dev/null @@ -1,14 +0,0 @@ -package input - -import "github.com/charmbracelet/x/ansi" - -// ModeReportEvent is a message that represents a mode report event (DECRPM). -// -// See: https://vt100.net/docs/vt510-rm/DECRPM.html -type ModeReportEvent struct { - // Mode is the mode number. - Mode ansi.Mode - - // Value is the mode value. - Value ansi.ModeSetting -} diff --git a/packages/tui/input/mouse.go b/packages/tui/input/mouse.go deleted file mode 100644 index d97eb72ed..000000000 --- a/packages/tui/input/mouse.go +++ /dev/null @@ -1,292 +0,0 @@ -package input - -import ( - "fmt" - - "github.com/charmbracelet/x/ansi" -) - -// MouseButton represents the button that was pressed during a mouse message. -type MouseButton = ansi.MouseButton - -// Mouse event buttons -// -// This is based on X11 mouse button codes. -// -// 1 = left button -// 2 = middle button (pressing the scroll wheel) -// 3 = right button -// 4 = turn scroll wheel up -// 5 = turn scroll wheel down -// 6 = push scroll wheel left -// 7 = push scroll wheel right -// 8 = 4th button (aka browser backward button) -// 9 = 5th button (aka browser forward button) -// 10 -// 11 -// -// Other buttons are not supported. -const ( - MouseNone = ansi.MouseNone - MouseLeft = ansi.MouseLeft - MouseMiddle = ansi.MouseMiddle - MouseRight = ansi.MouseRight - MouseWheelUp = ansi.MouseWheelUp - MouseWheelDown = ansi.MouseWheelDown - MouseWheelLeft = ansi.MouseWheelLeft - MouseWheelRight = ansi.MouseWheelRight - MouseBackward = ansi.MouseBackward - MouseForward = ansi.MouseForward - MouseButton10 = ansi.MouseButton10 - MouseButton11 = ansi.MouseButton11 -) - -// MouseEvent represents a mouse message. This is a generic mouse message that -// can represent any kind of mouse event. -type MouseEvent interface { - fmt.Stringer - - // Mouse returns the underlying mouse event. - Mouse() Mouse -} - -// Mouse represents a Mouse message. Use [MouseEvent] to represent all mouse -// messages. -// -// The X and Y coordinates are zero-based, with (0,0) being the upper left -// corner of the terminal. -// -// // Catch all mouse events -// switch Event := Event.(type) { -// case MouseEvent: -// m := Event.Mouse() -// fmt.Println("Mouse event:", m.X, m.Y, m) -// } -// -// // Only catch mouse click events -// switch Event := Event.(type) { -// case MouseClickEvent: -// fmt.Println("Mouse click event:", Event.X, Event.Y, Event) -// } -type Mouse struct { - X, Y int - Button MouseButton - Mod KeyMod -} - -// String returns a string representation of the mouse message. -func (m Mouse) String() (s string) { - if m.Mod.Contains(ModCtrl) { - s += "ctrl+" - } - if m.Mod.Contains(ModAlt) { - s += "alt+" - } - if m.Mod.Contains(ModShift) { - s += "shift+" - } - - str := m.Button.String() - if str == "" { - s += "unknown" - } else if str != "none" { // motion events don't have a button - s += str - } - - return s -} - -// MouseClickEvent represents a mouse button click event. -type MouseClickEvent Mouse - -// String returns a string representation of the mouse click event. -func (e MouseClickEvent) String() string { - return Mouse(e).String() -} - -// Mouse returns the underlying mouse event. This is a convenience method and -// syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse -// event to [Mouse]. -func (e MouseClickEvent) Mouse() Mouse { - return Mouse(e) -} - -// MouseReleaseEvent represents a mouse button release event. -type MouseReleaseEvent Mouse - -// String returns a string representation of the mouse release event. -func (e MouseReleaseEvent) String() string { - return Mouse(e).String() -} - -// Mouse returns the underlying mouse event. This is a convenience method and -// syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse -// event to [Mouse]. -func (e MouseReleaseEvent) Mouse() Mouse { - return Mouse(e) -} - -// MouseWheelEvent represents a mouse wheel message event. -type MouseWheelEvent Mouse - -// String returns a string representation of the mouse wheel event. -func (e MouseWheelEvent) String() string { - return Mouse(e).String() -} - -// Mouse returns the underlying mouse event. This is a convenience method and -// syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse -// event to [Mouse]. -func (e MouseWheelEvent) Mouse() Mouse { - return Mouse(e) -} - -// MouseMotionEvent represents a mouse motion event. -type MouseMotionEvent Mouse - -// String returns a string representation of the mouse motion event. -func (e MouseMotionEvent) String() string { - m := Mouse(e) - if m.Button != 0 { - return m.String() + "+motion" - } - return m.String() + "motion" -} - -// Mouse returns the underlying mouse event. This is a convenience method and -// syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse -// event to [Mouse]. -func (e MouseMotionEvent) Mouse() Mouse { - return Mouse(e) -} - -// Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events -// look like: -// -// ESC [ < Cb ; Cx ; Cy (M or m) -// -// where: -// -// Cb is the encoded button code -// Cx is the x-coordinate of the mouse -// Cy is the y-coordinate of the mouse -// M is for button press, m is for button release -// -// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates -func parseSGRMouseEvent(cmd ansi.Cmd, params ansi.Params) Event { - x, _, ok := params.Param(1, 1) - if !ok { - x = 1 - } - y, _, ok := params.Param(2, 1) - if !ok { - y = 1 - } - release := cmd.Final() == 'm' - b, _, _ := params.Param(0, 0) - mod, btn, _, isMotion := parseMouseButton(b) - - // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). - x-- - y-- - - m := Mouse{X: x, Y: y, Button: btn, Mod: mod} - - // Wheel buttons don't have release events - // Motion can be reported as a release event in some terminals (Windows Terminal) - if isWheel(m.Button) { - return MouseWheelEvent(m) - } else if !isMotion && release { - return MouseReleaseEvent(m) - } else if isMotion { - return MouseMotionEvent(m) - } - return MouseClickEvent(m) -} - -const x10MouseByteOffset = 32 - -// Parse X10-encoded mouse events; the simplest kind. The last release of X10 -// was December 1986, by the way. The original X10 mouse protocol limits the Cx -// and Cy coordinates to 223 (=255-032). -// -// X10 mouse events look like: -// -// ESC [M Cb Cx Cy -// -// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking -func parseX10MouseEvent(buf []byte) Event { - v := buf[3:6] - b := int(v[0]) - if b >= x10MouseByteOffset { - // XXX: b < 32 should be impossible, but we're being defensive. - b -= x10MouseByteOffset - } - - mod, btn, isRelease, isMotion := parseMouseButton(b) - - // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). - x := int(v[1]) - x10MouseByteOffset - 1 - y := int(v[2]) - x10MouseByteOffset - 1 - - m := Mouse{X: x, Y: y, Button: btn, Mod: mod} - if isWheel(m.Button) { - return MouseWheelEvent(m) - } else if isMotion { - return MouseMotionEvent(m) - } else if isRelease { - return MouseReleaseEvent(m) - } - return MouseClickEvent(m) -} - -// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates -func parseMouseButton(b int) (mod KeyMod, btn MouseButton, isRelease bool, isMotion bool) { - // mouse bit shifts - const ( - bitShift = 0b0000_0100 - bitAlt = 0b0000_1000 - bitCtrl = 0b0001_0000 - bitMotion = 0b0010_0000 - bitWheel = 0b0100_0000 - bitAdd = 0b1000_0000 // additional buttons 8-11 - - bitsMask = 0b0000_0011 - ) - - // Modifiers - if b&bitAlt != 0 { - mod |= ModAlt - } - if b&bitCtrl != 0 { - mod |= ModCtrl - } - if b&bitShift != 0 { - mod |= ModShift - } - - if b&bitAdd != 0 { - btn = MouseBackward + MouseButton(b&bitsMask) - } else if b&bitWheel != 0 { - btn = MouseWheelUp + MouseButton(b&bitsMask) - } else { - btn = MouseLeft + MouseButton(b&bitsMask) - // X10 reports a button release as 0b0000_0011 (3) - if b&bitsMask == bitsMask { - btn = MouseNone - isRelease = true - } - } - - // Motion bit doesn't get reported for wheel events. - if b&bitMotion != 0 && !isWheel(btn) { - isMotion = true - } - - return //nolint:nakedret -} - -// isWheel returns true if the mouse event is a wheel event. -func isWheel(btn MouseButton) bool { - return btn >= MouseWheelUp && btn <= MouseWheelRight -} diff --git a/packages/tui/input/mouse_test.go b/packages/tui/input/mouse_test.go deleted file mode 100644 index d55e41480..000000000 --- a/packages/tui/input/mouse_test.go +++ /dev/null @@ -1,481 +0,0 @@ -package input - -import ( - "fmt" - "testing" - - "github.com/charmbracelet/x/ansi" - "github.com/charmbracelet/x/ansi/parser" -) - -func TestMouseEvent_String(t *testing.T) { - tt := []struct { - name string - event Event - expected string - }{ - { - name: "unknown", - event: MouseClickEvent{Button: MouseButton(0xff)}, - expected: "unknown", - }, - { - name: "left", - event: MouseClickEvent{Button: MouseLeft}, - expected: "left", - }, - { - name: "right", - event: MouseClickEvent{Button: MouseRight}, - expected: "right", - }, - { - name: "middle", - event: MouseClickEvent{Button: MouseMiddle}, - expected: "middle", - }, - { - name: "release", - event: MouseReleaseEvent{Button: MouseNone}, - expected: "", - }, - { - name: "wheelup", - event: MouseWheelEvent{Button: MouseWheelUp}, - expected: "wheelup", - }, - { - name: "wheeldown", - event: MouseWheelEvent{Button: MouseWheelDown}, - expected: "wheeldown", - }, - { - name: "wheelleft", - event: MouseWheelEvent{Button: MouseWheelLeft}, - expected: "wheelleft", - }, - { - name: "wheelright", - event: MouseWheelEvent{Button: MouseWheelRight}, - expected: "wheelright", - }, - { - name: "motion", - event: MouseMotionEvent{Button: MouseNone}, - expected: "motion", - }, - { - name: "shift+left", - event: MouseReleaseEvent{Button: MouseLeft, Mod: ModShift}, - expected: "shift+left", - }, - { - name: "shift+left", event: MouseClickEvent{Button: MouseLeft, Mod: ModShift}, - expected: "shift+left", - }, - { - name: "ctrl+shift+left", - event: MouseClickEvent{Button: MouseLeft, Mod: ModCtrl | ModShift}, - expected: "ctrl+shift+left", - }, - { - name: "alt+left", - event: MouseClickEvent{Button: MouseLeft, Mod: ModAlt}, - expected: "alt+left", - }, - { - name: "ctrl+left", - event: MouseClickEvent{Button: MouseLeft, Mod: ModCtrl}, - expected: "ctrl+left", - }, - { - name: "ctrl+alt+left", - event: MouseClickEvent{Button: MouseLeft, Mod: ModAlt | ModCtrl}, - expected: "ctrl+alt+left", - }, - { - name: "ctrl+alt+shift+left", - event: MouseClickEvent{Button: MouseLeft, Mod: ModAlt | ModCtrl | ModShift}, - expected: "ctrl+alt+shift+left", - }, - { - name: "ignore coordinates", - event: MouseClickEvent{X: 100, Y: 200, Button: MouseLeft}, - expected: "left", - }, - { - name: "broken type", - event: MouseClickEvent{Button: MouseButton(120)}, - expected: "unknown", - }, - } - - for i := range tt { - tc := tt[i] - - t.Run(tc.name, func(t *testing.T) { - actual := fmt.Sprint(tc.event) - - if tc.expected != actual { - t.Fatalf("expected %q but got %q", - tc.expected, - actual, - ) - } - }) - } -} - -func TestParseX10MouseDownEvent(t *testing.T) { - encode := func(b byte, x, y int) []byte { - return []byte{ - '\x1b', - '[', - 'M', - byte(32) + b, - byte(x + 32 + 1), - byte(y + 32 + 1), - } - } - - tt := []struct { - name string - buf []byte - expected Event - }{ - // Position. - { - name: "zero position", - buf: encode(0b0000_0000, 0, 0), - expected: MouseClickEvent{X: 0, Y: 0, Button: MouseLeft}, - }, - { - name: "max position", - buf: encode(0b0000_0000, 222, 222), // Because 255 (max int8) - 32 - 1. - expected: MouseClickEvent{X: 222, Y: 222, Button: MouseLeft}, - }, - // Simple. - { - name: "left", - buf: encode(0b0000_0000, 32, 16), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseLeft}, - }, - { - name: "left in motion", - buf: encode(0b0010_0000, 32, 16), - expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseLeft}, - }, - { - name: "middle", - buf: encode(0b0000_0001, 32, 16), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseMiddle}, - }, - { - name: "middle in motion", - buf: encode(0b0010_0001, 32, 16), - expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseMiddle}, - }, - { - name: "right", - buf: encode(0b0000_0010, 32, 16), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseRight}, - }, - { - name: "right in motion", - buf: encode(0b0010_0010, 32, 16), - expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseRight}, - }, - { - name: "motion", - buf: encode(0b0010_0011, 32, 16), - expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseNone}, - }, - { - name: "wheel up", - buf: encode(0b0100_0000, 32, 16), - expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelUp}, - }, - { - name: "wheel down", - buf: encode(0b0100_0001, 32, 16), - expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelDown}, - }, - { - name: "wheel left", - buf: encode(0b0100_0010, 32, 16), - expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelLeft}, - }, - { - name: "wheel right", - buf: encode(0b0100_0011, 32, 16), - expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelRight}, - }, - { - name: "release", - buf: encode(0b0000_0011, 32, 16), - expected: MouseReleaseEvent{X: 32, Y: 16, Button: MouseNone}, - }, - { - name: "backward", - buf: encode(0b1000_0000, 32, 16), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseBackward}, - }, - { - name: "forward", - buf: encode(0b1000_0001, 32, 16), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseForward}, - }, - { - name: "button 10", - buf: encode(0b1000_0010, 32, 16), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseButton10}, - }, - { - name: "button 11", - buf: encode(0b1000_0011, 32, 16), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseButton11}, - }, - // Combinations. - { - name: "alt+right", - buf: encode(0b0000_1010, 32, 16), - expected: MouseClickEvent{X: 32, Y: 16, Mod: ModAlt, Button: MouseRight}, - }, - { - name: "ctrl+right", - buf: encode(0b0001_0010, 32, 16), - expected: MouseClickEvent{X: 32, Y: 16, Mod: ModCtrl, Button: MouseRight}, - }, - { - name: "left in motion", - buf: encode(0b0010_0000, 32, 16), - expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseLeft}, - }, - { - name: "alt+right in motion", - buf: encode(0b0010_1010, 32, 16), - expected: MouseMotionEvent{X: 32, Y: 16, Mod: ModAlt, Button: MouseRight}, - }, - { - name: "ctrl+right in motion", - buf: encode(0b0011_0010, 32, 16), - expected: MouseMotionEvent{X: 32, Y: 16, Mod: ModCtrl, Button: MouseRight}, - }, - { - name: "ctrl+alt+right", - buf: encode(0b0001_1010, 32, 16), - expected: MouseClickEvent{X: 32, Y: 16, Mod: ModAlt | ModCtrl, Button: MouseRight}, - }, - { - name: "ctrl+wheel up", - buf: encode(0b0101_0000, 32, 16), - expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModCtrl, Button: MouseWheelUp}, - }, - { - name: "alt+wheel down", - buf: encode(0b0100_1001, 32, 16), - expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModAlt, Button: MouseWheelDown}, - }, - { - name: "ctrl+alt+wheel down", - buf: encode(0b0101_1001, 32, 16), - expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModAlt | ModCtrl, Button: MouseWheelDown}, - }, - // Overflow position. - { - name: "overflow position", - buf: encode(0b0010_0000, 250, 223), // Because 255 (max int8) - 32 - 1. - expected: MouseMotionEvent{X: -6, Y: -33, Button: MouseLeft}, - }, - } - - for i := range tt { - tc := tt[i] - - t.Run(tc.name, func(t *testing.T) { - actual := parseX10MouseEvent(tc.buf) - - if tc.expected != actual { - t.Fatalf("expected %#v but got %#v", - tc.expected, - actual, - ) - } - }) - } -} - -func TestParseSGRMouseEvent(t *testing.T) { - type csiSequence struct { - params []ansi.Param - cmd ansi.Cmd - } - encode := func(b, x, y int, r bool) *csiSequence { - re := 'M' - if r { - re = 'm' - } - return &csiSequence{ - params: []ansi.Param{ - ansi.Param(b), - ansi.Param(x + 1), - ansi.Param(y + 1), - }, - cmd: ansi.Cmd(re) | ('<' << parser.PrefixShift), - } - } - - tt := []struct { - name string - buf *csiSequence - expected Event - }{ - // Position. - { - name: "zero position", - buf: encode(0, 0, 0, false), - expected: MouseClickEvent{X: 0, Y: 0, Button: MouseLeft}, - }, - { - name: "225 position", - buf: encode(0, 225, 225, false), - expected: MouseClickEvent{X: 225, Y: 225, Button: MouseLeft}, - }, - // Simple. - { - name: "left", - buf: encode(0, 32, 16, false), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseLeft}, - }, - { - name: "left in motion", - buf: encode(32, 32, 16, false), - expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseLeft}, - }, - { - name: "left", - buf: encode(0, 32, 16, true), - expected: MouseReleaseEvent{X: 32, Y: 16, Button: MouseLeft}, - }, - { - name: "middle", - buf: encode(1, 32, 16, false), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseMiddle}, - }, - { - name: "middle in motion", - buf: encode(33, 32, 16, false), - expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseMiddle}, - }, - { - name: "middle", - buf: encode(1, 32, 16, true), - expected: MouseReleaseEvent{X: 32, Y: 16, Button: MouseMiddle}, - }, - { - name: "right", - buf: encode(2, 32, 16, false), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseRight}, - }, - { - name: "right", - buf: encode(2, 32, 16, true), - expected: MouseReleaseEvent{X: 32, Y: 16, Button: MouseRight}, - }, - { - name: "motion", - buf: encode(35, 32, 16, false), - expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseNone}, - }, - { - name: "wheel up", - buf: encode(64, 32, 16, false), - expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelUp}, - }, - { - name: "wheel down", - buf: encode(65, 32, 16, false), - expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelDown}, - }, - { - name: "wheel left", - buf: encode(66, 32, 16, false), - expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelLeft}, - }, - { - name: "wheel right", - buf: encode(67, 32, 16, false), - expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelRight}, - }, - { - name: "backward", - buf: encode(128, 32, 16, false), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseBackward}, - }, - { - name: "backward in motion", - buf: encode(160, 32, 16, false), - expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseBackward}, - }, - { - name: "forward", - buf: encode(129, 32, 16, false), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseForward}, - }, - { - name: "forward in motion", - buf: encode(161, 32, 16, false), - expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseForward}, - }, - // Combinations. - { - name: "alt+right", - buf: encode(10, 32, 16, false), - expected: MouseClickEvent{X: 32, Y: 16, Mod: ModAlt, Button: MouseRight}, - }, - { - name: "ctrl+right", - buf: encode(18, 32, 16, false), - expected: MouseClickEvent{X: 32, Y: 16, Mod: ModCtrl, Button: MouseRight}, - }, - { - name: "ctrl+alt+right", - buf: encode(26, 32, 16, false), - expected: MouseClickEvent{X: 32, Y: 16, Mod: ModAlt | ModCtrl, Button: MouseRight}, - }, - { - name: "alt+wheel", - buf: encode(73, 32, 16, false), - expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModAlt, Button: MouseWheelDown}, - }, - { - name: "ctrl+wheel", - buf: encode(81, 32, 16, false), - expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModCtrl, Button: MouseWheelDown}, - }, - { - name: "ctrl+alt+wheel", - buf: encode(89, 32, 16, false), - expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModAlt | ModCtrl, Button: MouseWheelDown}, - }, - { - name: "ctrl+alt+shift+wheel", - buf: encode(93, 32, 16, false), - expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModAlt | ModShift | ModCtrl, Button: MouseWheelDown}, - }, - } - - for i := range tt { - tc := tt[i] - - t.Run(tc.name, func(t *testing.T) { - actual := parseSGRMouseEvent(tc.buf.cmd, tc.buf.params) - if tc.expected != actual { - t.Fatalf("expected %#v but got %#v", - tc.expected, - actual, - ) - } - }) - } -} diff --git a/packages/tui/input/parse.go b/packages/tui/input/parse.go deleted file mode 100644 index ad8e21849..000000000 --- a/packages/tui/input/parse.go +++ /dev/null @@ -1,1030 +0,0 @@ -package input - -import ( - "bytes" - "encoding/base64" - "slices" - "strings" - "unicode" - "unicode/utf8" - - "github.com/charmbracelet/x/ansi" - "github.com/charmbracelet/x/ansi/parser" - "github.com/rivo/uniseg" -) - -// Flags to control the behavior of the parser. -const ( - // When this flag is set, the driver will treat both Ctrl+Space and Ctrl+@ - // as the same key sequence. - // - // Historically, the ANSI specs generate NUL (0x00) on both the Ctrl+Space - // and Ctrl+@ key sequences. This flag allows the driver to treat both as - // the same key sequence. - FlagCtrlAt = 1 << iota - - // When this flag is set, the driver will treat the Tab key and Ctrl+I as - // the same key sequence. - // - // Historically, the ANSI specs generate HT (0x09) on both the Tab key and - // Ctrl+I. This flag allows the driver to treat both as the same key - // sequence. - FlagCtrlI - - // When this flag is set, the driver will treat the Enter key and Ctrl+M as - // the same key sequence. - // - // Historically, the ANSI specs generate CR (0x0D) on both the Enter key - // and Ctrl+M. This flag allows the driver to treat both as the same key. - FlagCtrlM - - // When this flag is set, the driver will treat Escape and Ctrl+[ as - // the same key sequence. - // - // Historically, the ANSI specs generate ESC (0x1B) on both the Escape key - // and Ctrl+[. This flag allows the driver to treat both as the same key - // sequence. - FlagCtrlOpenBracket - - // When this flag is set, the driver will send a BS (0x08 byte) character - // instead of a DEL (0x7F byte) character when the Backspace key is - // pressed. - // - // The VT100 terminal has both a Backspace and a Delete key. The VT220 - // terminal dropped the Backspace key and replaced it with the Delete key. - // Both terminals send a DEL character when the Delete key is pressed. - // Modern terminals and PCs later readded the Delete key but used a - // different key sequence, and the Backspace key was standardized to send a - // DEL character. - FlagBackspace - - // When this flag is set, the driver will recognize the Find key instead of - // treating it as a Home key. - // - // The Find key was part of the VT220 keyboard, and is no longer used in - // modern day PCs. - FlagFind - - // When this flag is set, the driver will recognize the Select key instead - // of treating it as a End key. - // - // The Symbol key was part of the VT220 keyboard, and is no longer used in - // modern day PCs. - FlagSelect - - // When this flag is set, the driver will use Terminfo databases to - // overwrite the default key sequences. - FlagTerminfo - - // When this flag is set, the driver will preserve function keys (F13-F63) - // as symbols. - // - // Since these keys are not part of today's standard 20th century keyboard, - // we treat them as F1-F12 modifier keys i.e. ctrl/shift/alt + Fn combos. - // Key definitions come from Terminfo, this flag is only useful when - // FlagTerminfo is not set. - FlagFKeys - - // When this flag is set, the driver will enable mouse mode on Windows. - // This is only useful on Windows and has no effect on other platforms. - FlagMouseMode -) - -// Parser is a parser for input escape sequences. -type Parser struct { - flags int -} - -// NewParser returns a new input parser. This is a low-level parser that parses -// escape sequences into human-readable events. -// This differs from [ansi.Parser] and [ansi.DecodeSequence] in which it -// recognizes incorrect sequences that some terminals may send. -// -// For instance, the X10 mouse protocol sends a `CSI M` sequence followed by 3 -// bytes. If the parser doesn't recognize the 3 bytes, they might be echoed to -// the terminal output causing a mess. -// -// Another example is how URxvt sends invalid sequences for modified keys using -// invalid CSI final characters like '$'. -// -// Use flags to control the behavior of ambiguous key sequences. -func NewParser(flags int) *Parser { - return &Parser{flags: flags} -} - -// parseSequence finds the first recognized event sequence and returns it along -// with its length. -// -// It will return zero and nil no sequence is recognized or when the buffer is -// empty. If a sequence is not supported, an UnknownEvent is returned. -func (p *Parser) parseSequence(buf []byte) (n int, Event Event) { - if len(buf) == 0 { - return 0, nil - } - - switch b := buf[0]; b { - case ansi.ESC: - if len(buf) == 1 { - // Escape key - return 1, KeyPressEvent{Code: KeyEscape} - } - - switch bPrime := buf[1]; bPrime { - case 'O': // Esc-prefixed SS3 - return p.parseSs3(buf) - case 'P': // Esc-prefixed DCS - return p.parseDcs(buf) - case '[': // Esc-prefixed CSI - return p.parseCsi(buf) - case ']': // Esc-prefixed OSC - return p.parseOsc(buf) - case '_': // Esc-prefixed APC - return p.parseApc(buf) - case '^': // Esc-prefixed PM - return p.parseStTerminated(ansi.PM, '^', nil)(buf) - case 'X': // Esc-prefixed SOS - return p.parseStTerminated(ansi.SOS, 'X', nil)(buf) - default: - n, e := p.parseSequence(buf[1:]) - if k, ok := e.(KeyPressEvent); ok { - k.Text = "" - k.Mod |= ModAlt - return n + 1, k - } - - // Not a key sequence, nor an alt modified key sequence. In that - // case, just report a single escape key. - return 1, KeyPressEvent{Code: KeyEscape} - } - case ansi.SS3: - return p.parseSs3(buf) - case ansi.DCS: - return p.parseDcs(buf) - case ansi.CSI: - return p.parseCsi(buf) - case ansi.OSC: - return p.parseOsc(buf) - case ansi.APC: - return p.parseApc(buf) - case ansi.PM: - return p.parseStTerminated(ansi.PM, '^', nil)(buf) - case ansi.SOS: - return p.parseStTerminated(ansi.SOS, 'X', nil)(buf) - default: - if b <= ansi.US || b == ansi.DEL || b == ansi.SP { - return 1, p.parseControl(b) - } else if b >= ansi.PAD && b <= ansi.APC { - // C1 control code - // UTF-8 never starts with a C1 control code - // Encode these as Ctrl+Alt+<code - 0x40> - code := rune(b) - 0x40 - return 1, KeyPressEvent{Code: code, Mod: ModCtrl | ModAlt} - } - return p.parseUtf8(buf) - } -} - -func (p *Parser) parseCsi(b []byte) (int, Event) { - if len(b) == 2 && b[0] == ansi.ESC { - // short cut if this is an alt+[ key - return 2, KeyPressEvent{Text: string(rune(b[1])), Mod: ModAlt} - } - - var cmd ansi.Cmd - var params [parser.MaxParamsSize]ansi.Param - var paramsLen int - - var i int - if b[i] == ansi.CSI || b[i] == ansi.ESC { - i++ - } - if i < len(b) && b[i-1] == ansi.ESC && b[i] == '[' { - i++ - } - - // Initial CSI byte - if i < len(b) && b[i] >= '<' && b[i] <= '?' { - cmd |= ansi.Cmd(b[i]) << parser.PrefixShift - } - - // Scan parameter bytes in the range 0x30-0x3F - var j int - for j = 0; i < len(b) && paramsLen < len(params) && b[i] >= 0x30 && b[i] <= 0x3F; i, j = i+1, j+1 { - if b[i] >= '0' && b[i] <= '9' { - if params[paramsLen] == parser.MissingParam { - params[paramsLen] = 0 - } - params[paramsLen] *= 10 - params[paramsLen] += ansi.Param(b[i]) - '0' - } - if b[i] == ':' { - params[paramsLen] |= parser.HasMoreFlag - } - if b[i] == ';' || b[i] == ':' { - paramsLen++ - if paramsLen < len(params) { - // Don't overflow the params slice - params[paramsLen] = parser.MissingParam - } - } - } - - if j > 0 && paramsLen < len(params) { - // has parameters - paramsLen++ - } - - // Scan intermediate bytes in the range 0x20-0x2F - var intermed byte - for ; i < len(b) && b[i] >= 0x20 && b[i] <= 0x2F; i++ { - intermed = b[i] - } - - // Set the intermediate byte - cmd |= ansi.Cmd(intermed) << parser.IntermedShift - - // Scan final byte in the range 0x40-0x7E - if i >= len(b) { - // Incomplete sequence - return 0, nil - } - if b[i] < 0x40 || b[i] > 0x7E { - // Special case for URxvt keys - // CSI <number> $ is an invalid sequence, but URxvt uses it for - // shift modified keys. - if b[i-1] == '$' { - n, ev := p.parseCsi(append(b[:i-1], '~')) - if k, ok := ev.(KeyPressEvent); ok { - k.Mod |= ModShift - return n, k - } - } - return i, UnknownEvent(b[:i-1]) - } - - // Add the final byte - cmd |= ansi.Cmd(b[i]) - i++ - - pa := ansi.Params(params[:paramsLen]) - switch cmd { - case 'y' | '?'<<parser.PrefixShift | '$'<<parser.IntermedShift: - // Report Mode (DECRPM) - mode, _, ok := pa.Param(0, -1) - if !ok || mode == -1 { - break - } - value, _, ok := pa.Param(1, -1) - if !ok || value == -1 { - break - } - return i, ModeReportEvent{Mode: ansi.DECMode(mode), Value: ansi.ModeSetting(value)} - case 'c' | '?'<<parser.PrefixShift: - // Primary Device Attributes - return i, parsePrimaryDevAttrs(pa) - case 'u' | '?'<<parser.PrefixShift: - // Kitty keyboard flags - flags, _, ok := pa.Param(0, -1) - if !ok || flags == -1 { - break - } - return i, KittyEnhancementsEvent(flags) - case 'R' | '?'<<parser.PrefixShift: - // This report may return a third parameter representing the page - // number, but we don't really need it. - row, _, ok := pa.Param(0, 1) - if !ok { - break - } - col, _, ok := pa.Param(1, 1) - if !ok { - break - } - return i, CursorPositionEvent{Y: row - 1, X: col - 1} - case 'm' | '<'<<parser.PrefixShift, 'M' | '<'<<parser.PrefixShift: - // Handle SGR mouse - if paramsLen >= 3 { - pa = pa[:3] - return i, parseSGRMouseEvent(cmd, pa) - } - case 'm' | '>'<<parser.PrefixShift: - // XTerm modifyOtherKeys - mok, _, ok := pa.Param(0, 0) - if !ok || mok != 4 { - break - } - val, _, ok := pa.Param(1, -1) - if !ok || val == -1 { - break - } - return i, ModifyOtherKeysEvent(val) //nolint:gosec - case 'I': - return i, FocusEvent{} - case 'O': - return i, BlurEvent{} - case 'R': - // Cursor position report OR modified F3 - row, _, rok := pa.Param(0, 1) - col, _, cok := pa.Param(1, 1) - if paramsLen == 2 && rok && cok { - m := CursorPositionEvent{Y: row - 1, X: col - 1} - if row == 1 && col-1 <= int(ModMeta|ModShift|ModAlt|ModCtrl) { - // XXX: We cannot differentiate between cursor position report and - // CSI 1 ; <mod> R (which is modified F3) when the cursor is at the - // row 1. In this case, we report both messages. - // - // For a non ambiguous cursor position report, use - // [ansi.RequestExtendedCursorPosition] (DECXCPR) instead. - return i, MultiEvent{KeyPressEvent{Code: KeyF3, Mod: KeyMod(col - 1)}, m} - } - - return i, m - } - - if paramsLen != 0 { - break - } - - // Unmodified key F3 (CSI R) - fallthrough - case 'a', 'b', 'c', 'd', 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'S', 'Z': - var k KeyPressEvent - switch cmd { - case 'a', 'b', 'c', 'd': - k = KeyPressEvent{Code: KeyUp + rune(cmd-'a'), Mod: ModShift} - case 'A', 'B', 'C', 'D': - k = KeyPressEvent{Code: KeyUp + rune(cmd-'A')} - case 'E': - k = KeyPressEvent{Code: KeyBegin} - case 'F': - k = KeyPressEvent{Code: KeyEnd} - case 'H': - k = KeyPressEvent{Code: KeyHome} - case 'P', 'Q', 'R', 'S': - k = KeyPressEvent{Code: KeyF1 + rune(cmd-'P')} - case 'Z': - k = KeyPressEvent{Code: KeyTab, Mod: ModShift} - } - id, _, _ := pa.Param(0, 1) - if id == 0 { - id = 1 - } - mod, _, _ := pa.Param(1, 1) - if mod == 0 { - mod = 1 - } - if paramsLen > 1 && id == 1 && mod != -1 { - // CSI 1 ; <modifiers> A - k.Mod |= KeyMod(mod - 1) - } - // Don't forget to handle Kitty keyboard protocol - return i, parseKittyKeyboardExt(pa, k) - case 'M': - // Handle X10 mouse - if i+2 >= len(b) { - // Incomplete sequence - return 0, nil - } - // PERFORMANCE: Do not use append here, as it will allocate a new slice - // for every mouse event. Instead, pass a sub-slice of the original - // buffer. - return i + 3, parseX10MouseEvent(b[i-1 : i+3]) - case 'y' | '$'<<parser.IntermedShift: - // Report Mode (DECRPM) - mode, _, ok := pa.Param(0, -1) - if !ok || mode == -1 { - break - } - val, _, ok := pa.Param(1, -1) - if !ok || val == -1 { - break - } - return i, ModeReportEvent{Mode: ansi.ANSIMode(mode), Value: ansi.ModeSetting(val)} - case 'u': - // Kitty keyboard protocol & CSI u (fixterms) - if paramsLen == 0 { - return i, UnknownEvent(b[:i]) - } - return i, parseKittyKeyboard(pa) - case '_': - // Win32 Input Mode - if paramsLen != 6 { - return i, UnknownEvent(b[:i]) - } - - vrc, _, _ := pa.Param(5, 0) - rc := uint16(vrc) //nolint:gosec - if rc == 0 { - rc = 1 - } - - vk, _, _ := pa.Param(0, 0) - sc, _, _ := pa.Param(1, 0) - uc, _, _ := pa.Param(2, 0) - kd, _, _ := pa.Param(3, 0) - cs, _, _ := pa.Param(4, 0) - event := p.parseWin32InputKeyEvent( - nil, - uint16(vk), //nolint:gosec // Vk wVirtualKeyCode - uint16(sc), //nolint:gosec // Sc wVirtualScanCode - rune(uc), // Uc UnicodeChar - kd == 1, // Kd bKeyDown - uint32(cs), //nolint:gosec // Cs dwControlKeyState - rc, // Rc wRepeatCount - ) - - if event == nil { - return i, UnknownEvent(b[:]) - } - - return i, event - case '@', '^', '~': - if paramsLen == 0 { - return i, UnknownEvent(b[:i]) - } - - param, _, _ := pa.Param(0, 0) - switch cmd { - case '~': - switch param { - case 27: - // XTerm modifyOtherKeys 2 - if paramsLen != 3 { - return i, UnknownEvent(b[:i]) - } - return i, parseXTermModifyOtherKeys(pa) - case 200: - // bracketed-paste start - return i, PasteStartEvent{} - case 201: - // bracketed-paste end - return i, PasteEndEvent{} - } - } - - switch param { - case 1, 2, 3, 4, 5, 6, 7, 8, - 11, 12, 13, 14, 15, - 17, 18, 19, 20, 21, - 23, 24, 25, 26, - 28, 29, 31, 32, 33, 34: - var k KeyPressEvent - switch param { - case 1: - if p.flags&FlagFind != 0 { - k = KeyPressEvent{Code: KeyFind} - } else { - k = KeyPressEvent{Code: KeyHome} - } - case 2: - k = KeyPressEvent{Code: KeyInsert} - case 3: - k = KeyPressEvent{Code: KeyDelete} - case 4: - if p.flags&FlagSelect != 0 { - k = KeyPressEvent{Code: KeySelect} - } else { - k = KeyPressEvent{Code: KeyEnd} - } - case 5: - k = KeyPressEvent{Code: KeyPgUp} - case 6: - k = KeyPressEvent{Code: KeyPgDown} - case 7: - k = KeyPressEvent{Code: KeyHome} - case 8: - k = KeyPressEvent{Code: KeyEnd} - case 11, 12, 13, 14, 15: - k = KeyPressEvent{Code: KeyF1 + rune(param-11)} - case 17, 18, 19, 20, 21: - k = KeyPressEvent{Code: KeyF6 + rune(param-17)} - case 23, 24, 25, 26: - k = KeyPressEvent{Code: KeyF11 + rune(param-23)} - case 28, 29: - k = KeyPressEvent{Code: KeyF15 + rune(param-28)} - case 31, 32, 33, 34: - k = KeyPressEvent{Code: KeyF17 + rune(param-31)} - } - - // modifiers - mod, _, _ := pa.Param(1, -1) - if paramsLen > 1 && mod != -1 { - k.Mod |= KeyMod(mod - 1) - } - - // Handle URxvt weird keys - switch cmd { - case '~': - // Don't forget to handle Kitty keyboard protocol - return i, parseKittyKeyboardExt(pa, k) - case '^': - k.Mod |= ModCtrl - case '@': - k.Mod |= ModCtrl | ModShift - } - - return i, k - } - - case 't': - param, _, ok := pa.Param(0, 0) - if !ok { - break - } - - var winop WindowOpEvent - winop.Op = param - for j := 1; j < paramsLen; j++ { - val, _, ok := pa.Param(j, 0) - if ok { - winop.Args = append(winop.Args, val) - } - } - - return i, winop - } - return i, UnknownEvent(b[:i]) -} - -// parseSs3 parses a SS3 sequence. -// See https://vt100.net/docs/vt220-rm/chapter4.html#S4.4.4.2 -func (p *Parser) parseSs3(b []byte) (int, Event) { - if len(b) == 2 && b[0] == ansi.ESC { - // short cut if this is an alt+O key - return 2, KeyPressEvent{Code: rune(b[1]), Mod: ModAlt} - } - - var i int - if b[i] == ansi.SS3 || b[i] == ansi.ESC { - i++ - } - if i < len(b) && b[i-1] == ansi.ESC && b[i] == 'O' { - i++ - } - - // Scan numbers from 0-9 - var mod int - for ; i < len(b) && b[i] >= '0' && b[i] <= '9'; i++ { - mod *= 10 - mod += int(b[i]) - '0' - } - - // Scan a GL character - // A GL character is a single byte in the range 0x21-0x7E - // See https://vt100.net/docs/vt220-rm/chapter2.html#S2.3.2 - if i >= len(b) { - // Incomplete sequence - return 0, nil - } - if b[i] < 0x21 || b[i] > 0x7E { - return i, UnknownEvent(b[:i]) - } - - // GL character(s) - gl := b[i] - i++ - - var k KeyPressEvent - switch gl { - case 'a', 'b', 'c', 'd': - k = KeyPressEvent{Code: KeyUp + rune(gl-'a'), Mod: ModCtrl} - case 'A', 'B', 'C', 'D': - k = KeyPressEvent{Code: KeyUp + rune(gl-'A')} - case 'E': - k = KeyPressEvent{Code: KeyBegin} - case 'F': - k = KeyPressEvent{Code: KeyEnd} - case 'H': - k = KeyPressEvent{Code: KeyHome} - case 'P', 'Q', 'R', 'S': - k = KeyPressEvent{Code: KeyF1 + rune(gl-'P')} - case 'M': - k = KeyPressEvent{Code: KeyKpEnter} - case 'X': - k = KeyPressEvent{Code: KeyKpEqual} - case 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y': - k = KeyPressEvent{Code: KeyKpMultiply + rune(gl-'j')} - default: - return i, UnknownEvent(b[:i]) - } - - // Handle weird SS3 <modifier> Func - if mod > 0 { - k.Mod |= KeyMod(mod - 1) - } - - return i, k -} - -func (p *Parser) parseOsc(b []byte) (int, Event) { - defaultKey := func() KeyPressEvent { - return KeyPressEvent{Code: rune(b[1]), Mod: ModAlt} - } - if len(b) == 2 && b[0] == ansi.ESC { - // short cut if this is an alt+] key - return 2, defaultKey() - } - - var i int - if b[i] == ansi.OSC || b[i] == ansi.ESC { - i++ - } - if i < len(b) && b[i-1] == ansi.ESC && b[i] == ']' { - i++ - } - - // Parse OSC command - // An OSC sequence is terminated by a BEL, ESC, or ST character - var start, end int - cmd := -1 - for ; i < len(b) && b[i] >= '0' && b[i] <= '9'; i++ { - if cmd == -1 { - cmd = 0 - } else { - cmd *= 10 - } - cmd += int(b[i]) - '0' - } - - if i < len(b) && b[i] == ';' { - // mark the start of the sequence data - i++ - start = i - } - - for ; i < len(b); i++ { - // advance to the end of the sequence - if slices.Contains([]byte{ansi.BEL, ansi.ESC, ansi.ST, ansi.CAN, ansi.SUB}, b[i]) { - break - } - } - - if i >= len(b) { - // Incomplete sequence - return 0, nil - } - - end = i // end of the sequence data - i++ - - // Check 7-bit ST (string terminator) character - switch b[i-1] { - case ansi.CAN, ansi.SUB: - return i, UnknownEvent(b[:i]) - case ansi.ESC: - if i >= len(b) || b[i] != '\\' { - if cmd == -1 || (start == 0 && end == 2) { - return 2, defaultKey() - } - - // If we don't have a valid ST terminator, then this is a - // cancelled sequence and should be ignored. - return i, UnknownEvent(b[:i]) - } - - i++ - } - - if end <= start { - return i, UnknownEvent(b[:i]) - } - - // PERFORMANCE: Only allocate the data string if we know we have a handler - // for the command. This avoids allocations for unknown OSC sequences that - // can be sent in high frequency by trackpads. - switch cmd { - case 10, 11, 12: - data := string(b[start:end]) - color := ansi.XParseColor(data) - switch cmd { - case 10: - return i, ForegroundColorEvent{color} - case 11: - return i, BackgroundColorEvent{color} - case 12: - return i, CursorColorEvent{color} - } - case 52: - data := string(b[start:end]) - parts := strings.Split(data, ";") - if len(parts) == 0 { - return i, ClipboardEvent{} - } - if len(parts) != 2 || len(parts[0]) < 1 { - break - } - - b64 := parts[1] - bts, err := base64.StdEncoding.DecodeString(b64) - if err != nil { - break - } - - sel := ClipboardSelection(parts[0][0]) //nolint:unconvert - return i, ClipboardEvent{Selection: sel, Content: string(bts)} - } - - return i, UnknownEvent(b[:i]) -} - -// parseStTerminated parses a control sequence that gets terminated by a ST character. -func (p *Parser) parseStTerminated( - intro8, intro7 byte, - fn func([]byte) Event, -) func([]byte) (int, Event) { - defaultKey := func(b []byte) (int, Event) { - switch intro8 { - case ansi.SOS: - return 2, KeyPressEvent{Code: 'x', Mod: ModShift | ModAlt} - case ansi.PM, ansi.APC: - return 2, KeyPressEvent{Code: rune(b[1]), Mod: ModAlt} - } - return 0, nil - } - return func(b []byte) (int, Event) { - if len(b) == 2 && b[0] == ansi.ESC { - return defaultKey(b) - } - - var i int - if b[i] == intro8 || b[i] == ansi.ESC { - i++ - } - if i < len(b) && b[i-1] == ansi.ESC && b[i] == intro7 { - i++ - } - - // Scan control sequence - // Most common control sequence is terminated by a ST character - // ST is a 7-bit string terminator character is (ESC \) - start := i - for ; i < len(b); i++ { - if slices.Contains([]byte{ansi.ESC, ansi.ST, ansi.CAN, ansi.SUB}, b[i]) { - break - } - } - - if i >= len(b) { - // Incomplete sequence - return 0, nil - } - - end := i // end of the sequence data - i++ - - // Check 7-bit ST (string terminator) character - switch b[i-1] { - case ansi.CAN, ansi.SUB: - return i, UnknownEvent(b[:i]) - case ansi.ESC: - if i >= len(b) || b[i] != '\\' { - if start == end { - return defaultKey(b) - } - - // If we don't have a valid ST terminator, then this is a - // cancelled sequence and should be ignored. - return i, UnknownEvent(b[:i]) - } - - i++ - } - - // Call the function to parse the sequence and return the result - if fn != nil { - if e := fn(b[start:end]); e != nil { - return i, e - } - } - - return i, UnknownEvent(b[:i]) - } -} - -func (p *Parser) parseDcs(b []byte) (int, Event) { - if len(b) == 2 && b[0] == ansi.ESC { - // short cut if this is an alt+P key - return 2, KeyPressEvent{Code: 'p', Mod: ModShift | ModAlt} - } - - var params [16]ansi.Param - var paramsLen int - var cmd ansi.Cmd - - // DCS sequences are introduced by DCS (0x90) or ESC P (0x1b 0x50) - var i int - if b[i] == ansi.DCS || b[i] == ansi.ESC { - i++ - } - if i < len(b) && b[i-1] == ansi.ESC && b[i] == 'P' { - i++ - } - - // initial DCS byte - if i < len(b) && b[i] >= '<' && b[i] <= '?' { - cmd |= ansi.Cmd(b[i]) << parser.PrefixShift - } - - // Scan parameter bytes in the range 0x30-0x3F - var j int - for j = 0; i < len(b) && paramsLen < len(params) && b[i] >= 0x30 && b[i] <= 0x3F; i, j = i+1, j+1 { - if b[i] >= '0' && b[i] <= '9' { - if params[paramsLen] == parser.MissingParam { - params[paramsLen] = 0 - } - params[paramsLen] *= 10 - params[paramsLen] += ansi.Param(b[i]) - '0' - } - if b[i] == ':' { - params[paramsLen] |= parser.HasMoreFlag - } - if b[i] == ';' || b[i] == ':' { - paramsLen++ - if paramsLen < len(params) { - // Don't overflow the params slice - params[paramsLen] = parser.MissingParam - } - } - } - - if j > 0 && paramsLen < len(params) { - // has parameters - paramsLen++ - } - - // Scan intermediate bytes in the range 0x20-0x2F - var intermed byte - for j := 0; i < len(b) && b[i] >= 0x20 && b[i] <= 0x2F; i, j = i+1, j+1 { - intermed = b[i] - } - - // set intermediate byte - cmd |= ansi.Cmd(intermed) << parser.IntermedShift - - // Scan final byte in the range 0x40-0x7E - if i >= len(b) { - // Incomplete sequence - return 0, nil - } - if b[i] < 0x40 || b[i] > 0x7E { - return i, UnknownEvent(b[:i]) - } - - // Add the final byte - cmd |= ansi.Cmd(b[i]) - i++ - - start := i // start of the sequence data - for ; i < len(b); i++ { - if b[i] == ansi.ST || b[i] == ansi.ESC { - break - } - } - - if i >= len(b) { - // Incomplete sequence - return 0, nil - } - - end := i // end of the sequence data - i++ - - // Check 7-bit ST (string terminator) character - if i < len(b) && b[i-1] == ansi.ESC && b[i] == '\\' { - i++ - } - - pa := ansi.Params(params[:paramsLen]) - switch cmd { - case 'r' | '+'<<parser.IntermedShift: - // XTGETTCAP responses - param, _, _ := pa.Param(0, 0) - switch param { - case 1: // 1 means valid response, 0 means invalid response - tc := parseTermcap(b[start:end]) - // XXX: some terminals like KiTTY report invalid responses with - // their queries i.e. sending a query for "Tc" using "\x1bP+q5463\x1b\\" - // returns "\x1bP0+r5463\x1b\\". - // The specs says that invalid responses should be in the form of - // DCS 0 + r ST "\x1bP0+r\x1b\\" - // We ignore invalid responses and only send valid ones to the program. - // - // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands - return i, tc - } - case '|' | '>'<<parser.PrefixShift: - // XTVersion response - return i, TerminalVersionEvent(b[start:end]) - } - - return i, UnknownEvent(b[:i]) -} - -func (p *Parser) parseApc(b []byte) (int, Event) { - if len(b) == 2 && b[0] == ansi.ESC { - // short cut if this is an alt+_ key - return 2, KeyPressEvent{Code: rune(b[1]), Mod: ModAlt} - } - - // APC sequences are introduced by APC (0x9f) or ESC _ (0x1b 0x5f) - return p.parseStTerminated(ansi.APC, '_', func(b []byte) Event { - if len(b) == 0 { - return nil - } - - switch b[0] { - case 'G': // Kitty Graphics Protocol - var g KittyGraphicsEvent - parts := bytes.Split(b[1:], []byte{';'}) - g.Options.UnmarshalText(parts[0]) //nolint:errcheck,gosec - if len(parts) > 1 { - g.Payload = parts[1] - } - return g - } - - return nil - })(b) -} - -func (p *Parser) parseUtf8(b []byte) (int, Event) { - if len(b) == 0 { - return 0, nil - } - - c := b[0] - if c <= ansi.US || c == ansi.DEL || c == ansi.SP { - // Control codes get handled by parseControl - return 1, p.parseControl(c) - } else if c > ansi.US && c < ansi.DEL { - // ASCII printable characters - code := rune(c) - k := KeyPressEvent{Code: code, Text: string(code)} - if unicode.IsUpper(code) { - // Convert upper case letters to lower case + shift modifier - k.Code = unicode.ToLower(code) - k.ShiftedCode = code - k.Mod |= ModShift - } - - return 1, k - } - - code, _ := utf8.DecodeRune(b) - if code == utf8.RuneError { - return 1, UnknownEvent(b[0]) - } - - cluster, _, _, _ := uniseg.FirstGraphemeCluster(b, -1) - // PERFORMANCE: Use RuneCount to check for multi-rune graphemes instead of - // looping over the string representation. - if utf8.RuneCount(cluster) > 1 { - code = KeyExtended - } - - return len(cluster), KeyPressEvent{Code: code, Text: string(cluster)} -} - -func (p *Parser) parseControl(b byte) Event { - switch b { - case ansi.NUL: - if p.flags&FlagCtrlAt != 0 { - return KeyPressEvent{Code: '@', Mod: ModCtrl} - } - return KeyPressEvent{Code: KeySpace, Mod: ModCtrl} - case ansi.BS: - return KeyPressEvent{Code: 'h', Mod: ModCtrl} - case ansi.HT: - if p.flags&FlagCtrlI != 0 { - return KeyPressEvent{Code: 'i', Mod: ModCtrl} - } - return KeyPressEvent{Code: KeyTab} - case ansi.CR: - if p.flags&FlagCtrlM != 0 { - return KeyPressEvent{Code: 'm', Mod: ModCtrl} - } - return KeyPressEvent{Code: KeyEnter} - case ansi.ESC: - if p.flags&FlagCtrlOpenBracket != 0 { - return KeyPressEvent{Code: '[', Mod: ModCtrl} - } - return KeyPressEvent{Code: KeyEscape} - case ansi.DEL: - if p.flags&FlagBackspace != 0 { - return KeyPressEvent{Code: KeyDelete} - } - return KeyPressEvent{Code: KeyBackspace} - case ansi.SP: - return KeyPressEvent{Code: KeySpace, Text: " "} - default: - if b >= ansi.SOH && b <= ansi.SUB { - // Use lower case letters for control codes - code := rune(b + 0x60) - return KeyPressEvent{Code: code, Mod: ModCtrl} - } else if b >= ansi.FS && b <= ansi.US { - code := rune(b + 0x40) - return KeyPressEvent{Code: code, Mod: ModCtrl} - } - return UnknownEvent(b) - } -} diff --git a/packages/tui/input/parse_test.go b/packages/tui/input/parse_test.go deleted file mode 100644 index dc892e0cd..000000000 --- a/packages/tui/input/parse_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package input - -import ( - "image/color" - "reflect" - "testing" - - "github.com/charmbracelet/x/ansi" -) - -func TestParseSequence_Events(t *testing.T) { - input := []byte("\x1b\x1b[Ztest\x00\x1b]10;rgb:1234/1234/1234\x07\x1b[27;2;27~\x1b[?1049;2$y\x1b[4;1$y") - want := []Event{ - KeyPressEvent{Code: KeyTab, Mod: ModShift | ModAlt}, - KeyPressEvent{Code: 't', Text: "t"}, - KeyPressEvent{Code: 'e', Text: "e"}, - KeyPressEvent{Code: 's', Text: "s"}, - KeyPressEvent{Code: 't', Text: "t"}, - KeyPressEvent{Code: KeySpace, Mod: ModCtrl}, - ForegroundColorEvent{color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff}}, - KeyPressEvent{Code: KeyEscape, Mod: ModShift}, - ModeReportEvent{Mode: ansi.AltScreenSaveCursorMode, Value: ansi.ModeReset}, - ModeReportEvent{Mode: ansi.InsertReplaceMode, Value: ansi.ModeSet}, - } - - var p Parser - for i := 0; len(input) != 0; i++ { - if i >= len(want) { - t.Fatalf("reached end of want events") - } - n, got := p.parseSequence(input) - if !reflect.DeepEqual(got, want[i]) { - t.Errorf("got %#v (%T), want %#v (%T)", got, got, want[i], want[i]) - } - input = input[n:] - } -} - -func BenchmarkParseSequence(b *testing.B) { - var p Parser - input := []byte("\x1b\x1b[Ztest\x00\x1b]10;1234/1234/1234\x07\x1b[27;2;27~") - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - p.parseSequence(input) - } -} diff --git a/packages/tui/input/paste.go b/packages/tui/input/paste.go deleted file mode 100644 index 4e8fe68c9..000000000 --- a/packages/tui/input/paste.go +++ /dev/null @@ -1,13 +0,0 @@ -package input - -// PasteEvent is an message that is emitted when a terminal receives pasted text -// using bracketed-paste. -type PasteEvent string - -// PasteStartEvent is an message that is emitted when the terminal starts the -// bracketed-paste text. -type PasteStartEvent struct{} - -// PasteEndEvent is an message that is emitted when the terminal ends the -// bracketed-paste text. -type PasteEndEvent struct{} diff --git a/packages/tui/input/table.go b/packages/tui/input/table.go deleted file mode 100644 index 7e81fde38..000000000 --- a/packages/tui/input/table.go +++ /dev/null @@ -1,389 +0,0 @@ -package input - -import ( - "maps" - "strconv" - - "github.com/charmbracelet/x/ansi" -) - -// buildKeysTable builds a table of key sequences and their corresponding key -// events based on the VT100/VT200, XTerm, and Urxvt terminal specs. -func buildKeysTable(flags int, term string) map[string]Key { - nul := Key{Code: KeySpace, Mod: ModCtrl} // ctrl+@ or ctrl+space - if flags&FlagCtrlAt != 0 { - nul = Key{Code: '@', Mod: ModCtrl} - } - - tab := Key{Code: KeyTab} // ctrl+i or tab - if flags&FlagCtrlI != 0 { - tab = Key{Code: 'i', Mod: ModCtrl} - } - - enter := Key{Code: KeyEnter} // ctrl+m or enter - if flags&FlagCtrlM != 0 { - enter = Key{Code: 'm', Mod: ModCtrl} - } - - esc := Key{Code: KeyEscape} // ctrl+[ or escape - if flags&FlagCtrlOpenBracket != 0 { - esc = Key{Code: '[', Mod: ModCtrl} // ctrl+[ or escape - } - - del := Key{Code: KeyBackspace} - if flags&FlagBackspace != 0 { - del.Code = KeyDelete - } - - find := Key{Code: KeyHome} - if flags&FlagFind != 0 { - find.Code = KeyFind - } - - sel := Key{Code: KeyEnd} - if flags&FlagSelect != 0 { - sel.Code = KeySelect - } - - // The following is a table of key sequences and their corresponding key - // events based on the VT100/VT200 terminal specs. - // - // See: https://vt100.net/docs/vt100-ug/chapter3.html#S3.2 - // See: https://vt100.net/docs/vt220-rm/chapter3.html - // - // XXX: These keys may be overwritten by other options like XTerm or - // Terminfo. - table := map[string]Key{ - // C0 control characters - string(byte(ansi.NUL)): nul, - string(byte(ansi.SOH)): {Code: 'a', Mod: ModCtrl}, - string(byte(ansi.STX)): {Code: 'b', Mod: ModCtrl}, - string(byte(ansi.ETX)): {Code: 'c', Mod: ModCtrl}, - string(byte(ansi.EOT)): {Code: 'd', Mod: ModCtrl}, - string(byte(ansi.ENQ)): {Code: 'e', Mod: ModCtrl}, - string(byte(ansi.ACK)): {Code: 'f', Mod: ModCtrl}, - string(byte(ansi.BEL)): {Code: 'g', Mod: ModCtrl}, - string(byte(ansi.BS)): {Code: 'h', Mod: ModCtrl}, - string(byte(ansi.HT)): tab, - string(byte(ansi.LF)): {Code: 'j', Mod: ModCtrl}, - string(byte(ansi.VT)): {Code: 'k', Mod: ModCtrl}, - string(byte(ansi.FF)): {Code: 'l', Mod: ModCtrl}, - string(byte(ansi.CR)): enter, - string(byte(ansi.SO)): {Code: 'n', Mod: ModCtrl}, - string(byte(ansi.SI)): {Code: 'o', Mod: ModCtrl}, - string(byte(ansi.DLE)): {Code: 'p', Mod: ModCtrl}, - string(byte(ansi.DC1)): {Code: 'q', Mod: ModCtrl}, - string(byte(ansi.DC2)): {Code: 'r', Mod: ModCtrl}, - string(byte(ansi.DC3)): {Code: 's', Mod: ModCtrl}, - string(byte(ansi.DC4)): {Code: 't', Mod: ModCtrl}, - string(byte(ansi.NAK)): {Code: 'u', Mod: ModCtrl}, - string(byte(ansi.SYN)): {Code: 'v', Mod: ModCtrl}, - string(byte(ansi.ETB)): {Code: 'w', Mod: ModCtrl}, - string(byte(ansi.CAN)): {Code: 'x', Mod: ModCtrl}, - string(byte(ansi.EM)): {Code: 'y', Mod: ModCtrl}, - string(byte(ansi.SUB)): {Code: 'z', Mod: ModCtrl}, - string(byte(ansi.ESC)): esc, - string(byte(ansi.FS)): {Code: '\\', Mod: ModCtrl}, - string(byte(ansi.GS)): {Code: ']', Mod: ModCtrl}, - string(byte(ansi.RS)): {Code: '^', Mod: ModCtrl}, - string(byte(ansi.US)): {Code: '_', Mod: ModCtrl}, - - // Special keys in G0 - string(byte(ansi.SP)): {Code: KeySpace, Text: " "}, - string(byte(ansi.DEL)): del, - - // Special keys - - "\x1b[Z": {Code: KeyTab, Mod: ModShift}, - - "\x1b[1~": find, - "\x1b[2~": {Code: KeyInsert}, - "\x1b[3~": {Code: KeyDelete}, - "\x1b[4~": sel, - "\x1b[5~": {Code: KeyPgUp}, - "\x1b[6~": {Code: KeyPgDown}, - "\x1b[7~": {Code: KeyHome}, - "\x1b[8~": {Code: KeyEnd}, - - // Normal mode - "\x1b[A": {Code: KeyUp}, - "\x1b[B": {Code: KeyDown}, - "\x1b[C": {Code: KeyRight}, - "\x1b[D": {Code: KeyLeft}, - "\x1b[E": {Code: KeyBegin}, - "\x1b[F": {Code: KeyEnd}, - "\x1b[H": {Code: KeyHome}, - "\x1b[P": {Code: KeyF1}, - "\x1b[Q": {Code: KeyF2}, - "\x1b[R": {Code: KeyF3}, - "\x1b[S": {Code: KeyF4}, - - // Application Cursor Key Mode (DECCKM) - "\x1bOA": {Code: KeyUp}, - "\x1bOB": {Code: KeyDown}, - "\x1bOC": {Code: KeyRight}, - "\x1bOD": {Code: KeyLeft}, - "\x1bOE": {Code: KeyBegin}, - "\x1bOF": {Code: KeyEnd}, - "\x1bOH": {Code: KeyHome}, - "\x1bOP": {Code: KeyF1}, - "\x1bOQ": {Code: KeyF2}, - "\x1bOR": {Code: KeyF3}, - "\x1bOS": {Code: KeyF4}, - - // Keypad Application Mode (DECKPAM) - - "\x1bOM": {Code: KeyKpEnter}, - "\x1bOX": {Code: KeyKpEqual}, - "\x1bOj": {Code: KeyKpMultiply}, - "\x1bOk": {Code: KeyKpPlus}, - "\x1bOl": {Code: KeyKpComma}, - "\x1bOm": {Code: KeyKpMinus}, - "\x1bOn": {Code: KeyKpDecimal}, - "\x1bOo": {Code: KeyKpDivide}, - "\x1bOp": {Code: KeyKp0}, - "\x1bOq": {Code: KeyKp1}, - "\x1bOr": {Code: KeyKp2}, - "\x1bOs": {Code: KeyKp3}, - "\x1bOt": {Code: KeyKp4}, - "\x1bOu": {Code: KeyKp5}, - "\x1bOv": {Code: KeyKp6}, - "\x1bOw": {Code: KeyKp7}, - "\x1bOx": {Code: KeyKp8}, - "\x1bOy": {Code: KeyKp9}, - - // Function keys - - "\x1b[11~": {Code: KeyF1}, - "\x1b[12~": {Code: KeyF2}, - "\x1b[13~": {Code: KeyF3}, - "\x1b[14~": {Code: KeyF4}, - "\x1b[15~": {Code: KeyF5}, - "\x1b[17~": {Code: KeyF6}, - "\x1b[18~": {Code: KeyF7}, - "\x1b[19~": {Code: KeyF8}, - "\x1b[20~": {Code: KeyF9}, - "\x1b[21~": {Code: KeyF10}, - "\x1b[23~": {Code: KeyF11}, - "\x1b[24~": {Code: KeyF12}, - "\x1b[25~": {Code: KeyF13}, - "\x1b[26~": {Code: KeyF14}, - "\x1b[28~": {Code: KeyF15}, - "\x1b[29~": {Code: KeyF16}, - "\x1b[31~": {Code: KeyF17}, - "\x1b[32~": {Code: KeyF18}, - "\x1b[33~": {Code: KeyF19}, - "\x1b[34~": {Code: KeyF20}, - } - - // CSI ~ sequence keys - csiTildeKeys := map[string]Key{ - "1": find, "2": {Code: KeyInsert}, - "3": {Code: KeyDelete}, "4": sel, - "5": {Code: KeyPgUp}, "6": {Code: KeyPgDown}, - "7": {Code: KeyHome}, "8": {Code: KeyEnd}, - // There are no 9 and 10 keys - "11": {Code: KeyF1}, "12": {Code: KeyF2}, - "13": {Code: KeyF3}, "14": {Code: KeyF4}, - "15": {Code: KeyF5}, "17": {Code: KeyF6}, - "18": {Code: KeyF7}, "19": {Code: KeyF8}, - "20": {Code: KeyF9}, "21": {Code: KeyF10}, - "23": {Code: KeyF11}, "24": {Code: KeyF12}, - "25": {Code: KeyF13}, "26": {Code: KeyF14}, - "28": {Code: KeyF15}, "29": {Code: KeyF16}, - "31": {Code: KeyF17}, "32": {Code: KeyF18}, - "33": {Code: KeyF19}, "34": {Code: KeyF20}, - } - - // URxvt keys - // See https://manpages.ubuntu.com/manpages/trusty/man7/urxvt.7.html#key%20codes - table["\x1b[a"] = Key{Code: KeyUp, Mod: ModShift} - table["\x1b[b"] = Key{Code: KeyDown, Mod: ModShift} - table["\x1b[c"] = Key{Code: KeyRight, Mod: ModShift} - table["\x1b[d"] = Key{Code: KeyLeft, Mod: ModShift} - table["\x1bOa"] = Key{Code: KeyUp, Mod: ModCtrl} - table["\x1bOb"] = Key{Code: KeyDown, Mod: ModCtrl} - table["\x1bOc"] = Key{Code: KeyRight, Mod: ModCtrl} - table["\x1bOd"] = Key{Code: KeyLeft, Mod: ModCtrl} - //nolint:godox - // TODO: investigate if shift-ctrl arrow keys collide with DECCKM keys i.e. - // "\x1bOA", "\x1bOB", "\x1bOC", "\x1bOD" - - // URxvt modifier CSI ~ keys - for k, v := range csiTildeKeys { - key := v - // Normal (no modifier) already defined part of VT100/VT200 - // Shift modifier - key.Mod = ModShift - table["\x1b["+k+"$"] = key - // Ctrl modifier - key.Mod = ModCtrl - table["\x1b["+k+"^"] = key - // Shift-Ctrl modifier - key.Mod = ModShift | ModCtrl - table["\x1b["+k+"@"] = key - } - - // URxvt F keys - // Note: Shift + F1-F10 generates F11-F20. - // This means Shift + F1 and Shift + F2 will generate F11 and F12, the same - // applies to Ctrl + Shift F1 & F2. - // - // P.S. Don't like this? Blame URxvt, configure your terminal to use - // different escapes like XTerm, or switch to a better terminal ¯\_(ツ)_/¯ - // - // See https://manpages.ubuntu.com/manpages/trusty/man7/urxvt.7.html#key%20codes - table["\x1b[23$"] = Key{Code: KeyF11, Mod: ModShift} - table["\x1b[24$"] = Key{Code: KeyF12, Mod: ModShift} - table["\x1b[25$"] = Key{Code: KeyF13, Mod: ModShift} - table["\x1b[26$"] = Key{Code: KeyF14, Mod: ModShift} - table["\x1b[28$"] = Key{Code: KeyF15, Mod: ModShift} - table["\x1b[29$"] = Key{Code: KeyF16, Mod: ModShift} - table["\x1b[31$"] = Key{Code: KeyF17, Mod: ModShift} - table["\x1b[32$"] = Key{Code: KeyF18, Mod: ModShift} - table["\x1b[33$"] = Key{Code: KeyF19, Mod: ModShift} - table["\x1b[34$"] = Key{Code: KeyF20, Mod: ModShift} - table["\x1b[11^"] = Key{Code: KeyF1, Mod: ModCtrl} - table["\x1b[12^"] = Key{Code: KeyF2, Mod: ModCtrl} - table["\x1b[13^"] = Key{Code: KeyF3, Mod: ModCtrl} - table["\x1b[14^"] = Key{Code: KeyF4, Mod: ModCtrl} - table["\x1b[15^"] = Key{Code: KeyF5, Mod: ModCtrl} - table["\x1b[17^"] = Key{Code: KeyF6, Mod: ModCtrl} - table["\x1b[18^"] = Key{Code: KeyF7, Mod: ModCtrl} - table["\x1b[19^"] = Key{Code: KeyF8, Mod: ModCtrl} - table["\x1b[20^"] = Key{Code: KeyF9, Mod: ModCtrl} - table["\x1b[21^"] = Key{Code: KeyF10, Mod: ModCtrl} - table["\x1b[23^"] = Key{Code: KeyF11, Mod: ModCtrl} - table["\x1b[24^"] = Key{Code: KeyF12, Mod: ModCtrl} - table["\x1b[25^"] = Key{Code: KeyF13, Mod: ModCtrl} - table["\x1b[26^"] = Key{Code: KeyF14, Mod: ModCtrl} - table["\x1b[28^"] = Key{Code: KeyF15, Mod: ModCtrl} - table["\x1b[29^"] = Key{Code: KeyF16, Mod: ModCtrl} - table["\x1b[31^"] = Key{Code: KeyF17, Mod: ModCtrl} - table["\x1b[32^"] = Key{Code: KeyF18, Mod: ModCtrl} - table["\x1b[33^"] = Key{Code: KeyF19, Mod: ModCtrl} - table["\x1b[34^"] = Key{Code: KeyF20, Mod: ModCtrl} - table["\x1b[23@"] = Key{Code: KeyF11, Mod: ModShift | ModCtrl} - table["\x1b[24@"] = Key{Code: KeyF12, Mod: ModShift | ModCtrl} - table["\x1b[25@"] = Key{Code: KeyF13, Mod: ModShift | ModCtrl} - table["\x1b[26@"] = Key{Code: KeyF14, Mod: ModShift | ModCtrl} - table["\x1b[28@"] = Key{Code: KeyF15, Mod: ModShift | ModCtrl} - table["\x1b[29@"] = Key{Code: KeyF16, Mod: ModShift | ModCtrl} - table["\x1b[31@"] = Key{Code: KeyF17, Mod: ModShift | ModCtrl} - table["\x1b[32@"] = Key{Code: KeyF18, Mod: ModShift | ModCtrl} - table["\x1b[33@"] = Key{Code: KeyF19, Mod: ModShift | ModCtrl} - table["\x1b[34@"] = Key{Code: KeyF20, Mod: ModShift | ModCtrl} - - // Register Alt + <key> combinations - // XXX: this must come after URxvt but before XTerm keys to register URxvt - // keys with alt modifier - tmap := map[string]Key{} - for seq, key := range table { - key := key - key.Mod |= ModAlt - key.Text = "" // Clear runes - tmap["\x1b"+seq] = key - } - maps.Copy(table, tmap) - - // XTerm modifiers - // These are offset by 1 to be compatible with our Mod type. - // See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-PC-Style-Function-Keys - modifiers := []KeyMod{ - ModShift, // 1 - ModAlt, // 2 - ModShift | ModAlt, // 3 - ModCtrl, // 4 - ModShift | ModCtrl, // 5 - ModAlt | ModCtrl, // 6 - ModShift | ModAlt | ModCtrl, // 7 - ModMeta, // 8 - ModMeta | ModShift, // 9 - ModMeta | ModAlt, // 10 - ModMeta | ModShift | ModAlt, // 11 - ModMeta | ModCtrl, // 12 - ModMeta | ModShift | ModCtrl, // 13 - ModMeta | ModAlt | ModCtrl, // 14 - ModMeta | ModShift | ModAlt | ModCtrl, // 15 - } - - // SS3 keypad function keys - ss3FuncKeys := map[string]Key{ - // These are defined in XTerm - // Taken from Foot keymap.h and XTerm modifyOtherKeys - // https://codeberg.org/dnkl/foot/src/branch/master/keymap.h - "M": {Code: KeyKpEnter}, "X": {Code: KeyKpEqual}, - "j": {Code: KeyKpMultiply}, "k": {Code: KeyKpPlus}, - "l": {Code: KeyKpComma}, "m": {Code: KeyKpMinus}, - "n": {Code: KeyKpDecimal}, "o": {Code: KeyKpDivide}, - "p": {Code: KeyKp0}, "q": {Code: KeyKp1}, - "r": {Code: KeyKp2}, "s": {Code: KeyKp3}, - "t": {Code: KeyKp4}, "u": {Code: KeyKp5}, - "v": {Code: KeyKp6}, "w": {Code: KeyKp7}, - "x": {Code: KeyKp8}, "y": {Code: KeyKp9}, - } - - // XTerm keys - csiFuncKeys := map[string]Key{ - "A": {Code: KeyUp}, "B": {Code: KeyDown}, - "C": {Code: KeyRight}, "D": {Code: KeyLeft}, - "E": {Code: KeyBegin}, "F": {Code: KeyEnd}, - "H": {Code: KeyHome}, "P": {Code: KeyF1}, - "Q": {Code: KeyF2}, "R": {Code: KeyF3}, - "S": {Code: KeyF4}, - } - - // CSI 27 ; <modifier> ; <code> ~ keys defined in XTerm modifyOtherKeys - modifyOtherKeys := map[int]Key{ - ansi.BS: {Code: KeyBackspace}, - ansi.HT: {Code: KeyTab}, - ansi.CR: {Code: KeyEnter}, - ansi.ESC: {Code: KeyEscape}, - ansi.DEL: {Code: KeyBackspace}, - } - - for _, m := range modifiers { - // XTerm modifier offset +1 - xtermMod := strconv.Itoa(int(m) + 1) - - // CSI 1 ; <modifier> <func> - for k, v := range csiFuncKeys { - // Functions always have a leading 1 param - seq := "\x1b[1;" + xtermMod + k - key := v - key.Mod = m - table[seq] = key - } - // SS3 <modifier> <func> - for k, v := range ss3FuncKeys { - seq := "\x1bO" + xtermMod + k - key := v - key.Mod = m - table[seq] = key - } - // CSI <number> ; <modifier> ~ - for k, v := range csiTildeKeys { - seq := "\x1b[" + k + ";" + xtermMod + "~" - key := v - key.Mod = m - table[seq] = key - } - // CSI 27 ; <modifier> ; <code> ~ - for k, v := range modifyOtherKeys { - code := strconv.Itoa(k) - seq := "\x1b[27;" + xtermMod + ";" + code + "~" - key := v - key.Mod = m - table[seq] = key - } - } - - // Register terminfo keys - // XXX: this might override keys already registered in table - if flags&FlagTerminfo != 0 { - titable := buildTerminfoKeys(flags, term) - maps.Copy(table, titable) - } - - return table -} diff --git a/packages/tui/input/termcap.go b/packages/tui/input/termcap.go deleted file mode 100644 index 3502189ff..000000000 --- a/packages/tui/input/termcap.go +++ /dev/null @@ -1,54 +0,0 @@ -package input - -import ( - "bytes" - "encoding/hex" - "strings" -) - -// CapabilityEvent represents a Termcap/Terminfo response event. Termcap -// responses are generated by the terminal in response to RequestTermcap -// (XTGETTCAP) requests. -// -// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands -type CapabilityEvent string - -func parseTermcap(data []byte) CapabilityEvent { - // XTGETTCAP - if len(data) == 0 { - return CapabilityEvent("") - } - - var tc strings.Builder - split := bytes.Split(data, []byte{';'}) - for _, s := range split { - parts := bytes.SplitN(s, []byte{'='}, 2) - if len(parts) == 0 { - return CapabilityEvent("") - } - - name, err := hex.DecodeString(string(parts[0])) - if err != nil || len(name) == 0 { - continue - } - - var value []byte - if len(parts) > 1 { - value, err = hex.DecodeString(string(parts[1])) - if err != nil { - continue - } - } - - if tc.Len() > 0 { - tc.WriteByte(';') - } - tc.WriteString(string(name)) - if len(value) > 0 { - tc.WriteByte('=') - tc.WriteString(string(value)) - } - } - - return CapabilityEvent(tc.String()) -} diff --git a/packages/tui/input/terminfo.go b/packages/tui/input/terminfo.go deleted file mode 100644 index a54da2c3a..000000000 --- a/packages/tui/input/terminfo.go +++ /dev/null @@ -1,277 +0,0 @@ -package input - -import ( - "strings" - - "github.com/xo/terminfo" -) - -func buildTerminfoKeys(flags int, term string) map[string]Key { - table := make(map[string]Key) - ti, _ := terminfo.Load(term) - if ti == nil { - return table - } - - tiTable := defaultTerminfoKeys(flags) - - // Default keys - for name, seq := range ti.StringCapsShort() { - if !strings.HasPrefix(name, "k") || len(seq) == 0 { - continue - } - - if k, ok := tiTable[name]; ok { - table[string(seq)] = k - } - } - - // Extended keys - for name, seq := range ti.ExtStringCapsShort() { - if !strings.HasPrefix(name, "k") || len(seq) == 0 { - continue - } - - if k, ok := tiTable[name]; ok { - table[string(seq)] = k - } - } - - return table -} - -// This returns a map of terminfo keys to key events. It's a mix of ncurses -// terminfo default and user-defined key capabilities. -// Upper-case caps that are defined in the default terminfo database are -// - kNXT -// - kPRV -// - kHOM -// - kEND -// - kDC -// - kIC -// - kLFT -// - kRIT -// -// See https://man7.org/linux/man-pages/man5/terminfo.5.html -// See https://github.com/mirror/ncurses/blob/master/include/Caps-ncurses -func defaultTerminfoKeys(flags int) map[string]Key { - keys := map[string]Key{ - "kcuu1": {Code: KeyUp}, - "kUP": {Code: KeyUp, Mod: ModShift}, - "kUP3": {Code: KeyUp, Mod: ModAlt}, - "kUP4": {Code: KeyUp, Mod: ModShift | ModAlt}, - "kUP5": {Code: KeyUp, Mod: ModCtrl}, - "kUP6": {Code: KeyUp, Mod: ModShift | ModCtrl}, - "kUP7": {Code: KeyUp, Mod: ModAlt | ModCtrl}, - "kUP8": {Code: KeyUp, Mod: ModShift | ModAlt | ModCtrl}, - "kcud1": {Code: KeyDown}, - "kDN": {Code: KeyDown, Mod: ModShift}, - "kDN3": {Code: KeyDown, Mod: ModAlt}, - "kDN4": {Code: KeyDown, Mod: ModShift | ModAlt}, - "kDN5": {Code: KeyDown, Mod: ModCtrl}, - "kDN7": {Code: KeyDown, Mod: ModAlt | ModCtrl}, - "kDN6": {Code: KeyDown, Mod: ModShift | ModCtrl}, - "kDN8": {Code: KeyDown, Mod: ModShift | ModAlt | ModCtrl}, - "kcub1": {Code: KeyLeft}, - "kLFT": {Code: KeyLeft, Mod: ModShift}, - "kLFT3": {Code: KeyLeft, Mod: ModAlt}, - "kLFT4": {Code: KeyLeft, Mod: ModShift | ModAlt}, - "kLFT5": {Code: KeyLeft, Mod: ModCtrl}, - "kLFT6": {Code: KeyLeft, Mod: ModShift | ModCtrl}, - "kLFT7": {Code: KeyLeft, Mod: ModAlt | ModCtrl}, - "kLFT8": {Code: KeyLeft, Mod: ModShift | ModAlt | ModCtrl}, - "kcuf1": {Code: KeyRight}, - "kRIT": {Code: KeyRight, Mod: ModShift}, - "kRIT3": {Code: KeyRight, Mod: ModAlt}, - "kRIT4": {Code: KeyRight, Mod: ModShift | ModAlt}, - "kRIT5": {Code: KeyRight, Mod: ModCtrl}, - "kRIT6": {Code: KeyRight, Mod: ModShift | ModCtrl}, - "kRIT7": {Code: KeyRight, Mod: ModAlt | ModCtrl}, - "kRIT8": {Code: KeyRight, Mod: ModShift | ModAlt | ModCtrl}, - "kich1": {Code: KeyInsert}, - "kIC": {Code: KeyInsert, Mod: ModShift}, - "kIC3": {Code: KeyInsert, Mod: ModAlt}, - "kIC4": {Code: KeyInsert, Mod: ModShift | ModAlt}, - "kIC5": {Code: KeyInsert, Mod: ModCtrl}, - "kIC6": {Code: KeyInsert, Mod: ModShift | ModCtrl}, - "kIC7": {Code: KeyInsert, Mod: ModAlt | ModCtrl}, - "kIC8": {Code: KeyInsert, Mod: ModShift | ModAlt | ModCtrl}, - "kdch1": {Code: KeyDelete}, - "kDC": {Code: KeyDelete, Mod: ModShift}, - "kDC3": {Code: KeyDelete, Mod: ModAlt}, - "kDC4": {Code: KeyDelete, Mod: ModShift | ModAlt}, - "kDC5": {Code: KeyDelete, Mod: ModCtrl}, - "kDC6": {Code: KeyDelete, Mod: ModShift | ModCtrl}, - "kDC7": {Code: KeyDelete, Mod: ModAlt | ModCtrl}, - "kDC8": {Code: KeyDelete, Mod: ModShift | ModAlt | ModCtrl}, - "khome": {Code: KeyHome}, - "kHOM": {Code: KeyHome, Mod: ModShift}, - "kHOM3": {Code: KeyHome, Mod: ModAlt}, - "kHOM4": {Code: KeyHome, Mod: ModShift | ModAlt}, - "kHOM5": {Code: KeyHome, Mod: ModCtrl}, - "kHOM6": {Code: KeyHome, Mod: ModShift | ModCtrl}, - "kHOM7": {Code: KeyHome, Mod: ModAlt | ModCtrl}, - "kHOM8": {Code: KeyHome, Mod: ModShift | ModAlt | ModCtrl}, - "kend": {Code: KeyEnd}, - "kEND": {Code: KeyEnd, Mod: ModShift}, - "kEND3": {Code: KeyEnd, Mod: ModAlt}, - "kEND4": {Code: KeyEnd, Mod: ModShift | ModAlt}, - "kEND5": {Code: KeyEnd, Mod: ModCtrl}, - "kEND6": {Code: KeyEnd, Mod: ModShift | ModCtrl}, - "kEND7": {Code: KeyEnd, Mod: ModAlt | ModCtrl}, - "kEND8": {Code: KeyEnd, Mod: ModShift | ModAlt | ModCtrl}, - "kpp": {Code: KeyPgUp}, - "kprv": {Code: KeyPgUp}, - "kPRV": {Code: KeyPgUp, Mod: ModShift}, - "kPRV3": {Code: KeyPgUp, Mod: ModAlt}, - "kPRV4": {Code: KeyPgUp, Mod: ModShift | ModAlt}, - "kPRV5": {Code: KeyPgUp, Mod: ModCtrl}, - "kPRV6": {Code: KeyPgUp, Mod: ModShift | ModCtrl}, - "kPRV7": {Code: KeyPgUp, Mod: ModAlt | ModCtrl}, - "kPRV8": {Code: KeyPgUp, Mod: ModShift | ModAlt | ModCtrl}, - "knp": {Code: KeyPgDown}, - "knxt": {Code: KeyPgDown}, - "kNXT": {Code: KeyPgDown, Mod: ModShift}, - "kNXT3": {Code: KeyPgDown, Mod: ModAlt}, - "kNXT4": {Code: KeyPgDown, Mod: ModShift | ModAlt}, - "kNXT5": {Code: KeyPgDown, Mod: ModCtrl}, - "kNXT6": {Code: KeyPgDown, Mod: ModShift | ModCtrl}, - "kNXT7": {Code: KeyPgDown, Mod: ModAlt | ModCtrl}, - "kNXT8": {Code: KeyPgDown, Mod: ModShift | ModAlt | ModCtrl}, - - "kbs": {Code: KeyBackspace}, - "kcbt": {Code: KeyTab, Mod: ModShift}, - - // Function keys - // This only includes the first 12 function keys. The rest are treated - // as modifiers of the first 12. - // Take a look at XTerm modifyFunctionKeys - // - // XXX: To use unambiguous function keys, use fixterms or kitty clipboard. - // - // See https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyFunctionKeys - // See https://invisible-island.net/xterm/terminfo.html - - "kf1": {Code: KeyF1}, - "kf2": {Code: KeyF2}, - "kf3": {Code: KeyF3}, - "kf4": {Code: KeyF4}, - "kf5": {Code: KeyF5}, - "kf6": {Code: KeyF6}, - "kf7": {Code: KeyF7}, - "kf8": {Code: KeyF8}, - "kf9": {Code: KeyF9}, - "kf10": {Code: KeyF10}, - "kf11": {Code: KeyF11}, - "kf12": {Code: KeyF12}, - "kf13": {Code: KeyF1, Mod: ModShift}, - "kf14": {Code: KeyF2, Mod: ModShift}, - "kf15": {Code: KeyF3, Mod: ModShift}, - "kf16": {Code: KeyF4, Mod: ModShift}, - "kf17": {Code: KeyF5, Mod: ModShift}, - "kf18": {Code: KeyF6, Mod: ModShift}, - "kf19": {Code: KeyF7, Mod: ModShift}, - "kf20": {Code: KeyF8, Mod: ModShift}, - "kf21": {Code: KeyF9, Mod: ModShift}, - "kf22": {Code: KeyF10, Mod: ModShift}, - "kf23": {Code: KeyF11, Mod: ModShift}, - "kf24": {Code: KeyF12, Mod: ModShift}, - "kf25": {Code: KeyF1, Mod: ModCtrl}, - "kf26": {Code: KeyF2, Mod: ModCtrl}, - "kf27": {Code: KeyF3, Mod: ModCtrl}, - "kf28": {Code: KeyF4, Mod: ModCtrl}, - "kf29": {Code: KeyF5, Mod: ModCtrl}, - "kf30": {Code: KeyF6, Mod: ModCtrl}, - "kf31": {Code: KeyF7, Mod: ModCtrl}, - "kf32": {Code: KeyF8, Mod: ModCtrl}, - "kf33": {Code: KeyF9, Mod: ModCtrl}, - "kf34": {Code: KeyF10, Mod: ModCtrl}, - "kf35": {Code: KeyF11, Mod: ModCtrl}, - "kf36": {Code: KeyF12, Mod: ModCtrl}, - "kf37": {Code: KeyF1, Mod: ModShift | ModCtrl}, - "kf38": {Code: KeyF2, Mod: ModShift | ModCtrl}, - "kf39": {Code: KeyF3, Mod: ModShift | ModCtrl}, - "kf40": {Code: KeyF4, Mod: ModShift | ModCtrl}, - "kf41": {Code: KeyF5, Mod: ModShift | ModCtrl}, - "kf42": {Code: KeyF6, Mod: ModShift | ModCtrl}, - "kf43": {Code: KeyF7, Mod: ModShift | ModCtrl}, - "kf44": {Code: KeyF8, Mod: ModShift | ModCtrl}, - "kf45": {Code: KeyF9, Mod: ModShift | ModCtrl}, - "kf46": {Code: KeyF10, Mod: ModShift | ModCtrl}, - "kf47": {Code: KeyF11, Mod: ModShift | ModCtrl}, - "kf48": {Code: KeyF12, Mod: ModShift | ModCtrl}, - "kf49": {Code: KeyF1, Mod: ModAlt}, - "kf50": {Code: KeyF2, Mod: ModAlt}, - "kf51": {Code: KeyF3, Mod: ModAlt}, - "kf52": {Code: KeyF4, Mod: ModAlt}, - "kf53": {Code: KeyF5, Mod: ModAlt}, - "kf54": {Code: KeyF6, Mod: ModAlt}, - "kf55": {Code: KeyF7, Mod: ModAlt}, - "kf56": {Code: KeyF8, Mod: ModAlt}, - "kf57": {Code: KeyF9, Mod: ModAlt}, - "kf58": {Code: KeyF10, Mod: ModAlt}, - "kf59": {Code: KeyF11, Mod: ModAlt}, - "kf60": {Code: KeyF12, Mod: ModAlt}, - "kf61": {Code: KeyF1, Mod: ModShift | ModAlt}, - "kf62": {Code: KeyF2, Mod: ModShift | ModAlt}, - "kf63": {Code: KeyF3, Mod: ModShift | ModAlt}, - } - - // Preserve F keys from F13 to F63 instead of using them for F-keys - // modifiers. - if flags&FlagFKeys != 0 { - keys["kf13"] = Key{Code: KeyF13} - keys["kf14"] = Key{Code: KeyF14} - keys["kf15"] = Key{Code: KeyF15} - keys["kf16"] = Key{Code: KeyF16} - keys["kf17"] = Key{Code: KeyF17} - keys["kf18"] = Key{Code: KeyF18} - keys["kf19"] = Key{Code: KeyF19} - keys["kf20"] = Key{Code: KeyF20} - keys["kf21"] = Key{Code: KeyF21} - keys["kf22"] = Key{Code: KeyF22} - keys["kf23"] = Key{Code: KeyF23} - keys["kf24"] = Key{Code: KeyF24} - keys["kf25"] = Key{Code: KeyF25} - keys["kf26"] = Key{Code: KeyF26} - keys["kf27"] = Key{Code: KeyF27} - keys["kf28"] = Key{Code: KeyF28} - keys["kf29"] = Key{Code: KeyF29} - keys["kf30"] = Key{Code: KeyF30} - keys["kf31"] = Key{Code: KeyF31} - keys["kf32"] = Key{Code: KeyF32} - keys["kf33"] = Key{Code: KeyF33} - keys["kf34"] = Key{Code: KeyF34} - keys["kf35"] = Key{Code: KeyF35} - keys["kf36"] = Key{Code: KeyF36} - keys["kf37"] = Key{Code: KeyF37} - keys["kf38"] = Key{Code: KeyF38} - keys["kf39"] = Key{Code: KeyF39} - keys["kf40"] = Key{Code: KeyF40} - keys["kf41"] = Key{Code: KeyF41} - keys["kf42"] = Key{Code: KeyF42} - keys["kf43"] = Key{Code: KeyF43} - keys["kf44"] = Key{Code: KeyF44} - keys["kf45"] = Key{Code: KeyF45} - keys["kf46"] = Key{Code: KeyF46} - keys["kf47"] = Key{Code: KeyF47} - keys["kf48"] = Key{Code: KeyF48} - keys["kf49"] = Key{Code: KeyF49} - keys["kf50"] = Key{Code: KeyF50} - keys["kf51"] = Key{Code: KeyF51} - keys["kf52"] = Key{Code: KeyF52} - keys["kf53"] = Key{Code: KeyF53} - keys["kf54"] = Key{Code: KeyF54} - keys["kf55"] = Key{Code: KeyF55} - keys["kf56"] = Key{Code: KeyF56} - keys["kf57"] = Key{Code: KeyF57} - keys["kf58"] = Key{Code: KeyF58} - keys["kf59"] = Key{Code: KeyF59} - keys["kf60"] = Key{Code: KeyF60} - keys["kf61"] = Key{Code: KeyF61} - keys["kf62"] = Key{Code: KeyF62} - keys["kf63"] = Key{Code: KeyF63} - } - - return keys -} diff --git a/packages/tui/input/xterm.go b/packages/tui/input/xterm.go deleted file mode 100644 index b3bbc3083..000000000 --- a/packages/tui/input/xterm.go +++ /dev/null @@ -1,47 +0,0 @@ -package input - -import ( - "github.com/charmbracelet/x/ansi" -) - -func parseXTermModifyOtherKeys(params ansi.Params) Event { - // XTerm modify other keys starts with ESC [ 27 ; <modifier> ; <code> ~ - xmod, _, _ := params.Param(1, 1) - xrune, _, _ := params.Param(2, 1) - mod := KeyMod(xmod - 1) - r := rune(xrune) - - switch r { - case ansi.BS: - return KeyPressEvent{Mod: mod, Code: KeyBackspace} - case ansi.HT: - return KeyPressEvent{Mod: mod, Code: KeyTab} - case ansi.CR: - return KeyPressEvent{Mod: mod, Code: KeyEnter} - case ansi.ESC: - return KeyPressEvent{Mod: mod, Code: KeyEscape} - case ansi.DEL: - return KeyPressEvent{Mod: mod, Code: KeyBackspace} - } - - // CSI 27 ; <modifier> ; <code> ~ keys defined in XTerm modifyOtherKeys - k := KeyPressEvent{Code: r, Mod: mod} - if k.Mod <= ModShift { - k.Text = string(r) - } - - return k -} - -// TerminalVersionEvent is a message that represents the terminal version. -type TerminalVersionEvent string - -// ModifyOtherKeysEvent represents a modifyOtherKeys event. -// -// 0: disable -// 1: enable mode 1 -// 2: enable mode 2 -// -// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ -// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys -type ModifyOtherKeysEvent uint8 |
