summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/input
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-11-02 18:43:17 -0500
committerDax Raad <[email protected]>2025-11-02 18:43:33 -0500
commitf68374ad2223ddc213bdea9519ca6a699819ee0e (patch)
tree04f0fe21b8e12cd62d7274961bb0cff64f966f40 /packages/tui/input
parent5e86c9b7916f75c7ad227b80eab18c7c54fc8ffe (diff)
downloadopencode-f68374ad2223ddc213bdea9519ca6a699819ee0e.tar.gz
opencode-f68374ad2223ddc213bdea9519ca6a699819ee0e.zip
DELETE GO BUBBLETEA CRAP HOORAY
Diffstat (limited to 'packages/tui/input')
-rw-r--r--packages/tui/input/cancelreader_other.go14
-rw-r--r--packages/tui/input/cancelreader_windows.go143
-rw-r--r--packages/tui/input/clipboard.go25
-rw-r--r--packages/tui/input/color.go136
-rw-r--r--packages/tui/input/cursor.go7
-rw-r--r--packages/tui/input/da1.go18
-rw-r--r--packages/tui/input/doc.go6
-rw-r--r--packages/tui/input/driver.go192
-rw-r--r--packages/tui/input/driver_other.go17
-rw-r--r--packages/tui/input/driver_test.go25
-rw-r--r--packages/tui/input/driver_windows.go642
-rw-r--r--packages/tui/input/driver_windows_test.go271
-rw-r--r--packages/tui/input/focus.go9
-rw-r--r--packages/tui/input/focus_test.go27
-rw-r--r--packages/tui/input/go.mod18
-rw-r--r--packages/tui/input/go.sum19
-rw-r--r--packages/tui/input/input.go45
-rw-r--r--packages/tui/input/key.go574
-rw-r--r--packages/tui/input/key_test.go880
-rw-r--r--packages/tui/input/kitty.go353
-rw-r--r--packages/tui/input/mod.go37
-rw-r--r--packages/tui/input/mode.go14
-rw-r--r--packages/tui/input/mouse.go292
-rw-r--r--packages/tui/input/mouse_test.go481
-rw-r--r--packages/tui/input/parse.go1030
-rw-r--r--packages/tui/input/parse_test.go47
-rw-r--r--packages/tui/input/paste.go13
-rw-r--r--packages/tui/input/table.go389
-rw-r--r--packages/tui/input/termcap.go54
-rw-r--r--packages/tui/input/terminfo.go277
-rw-r--r--packages/tui/input/xterm.go47
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