From 13ffcad06c58001658d5c0d2ef58990da39183e0 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 12 Aug 2024 17:04:04 -0400 Subject: [PATCH 01/18] refactor: reimplement the event loop with a sequence parser Currently, Bubble Tea uses a simple lookup table to detect input events. Here, we're introducing an actual input sequence parser instead of simply using a lookup table. This will allow Bubble Tea programs to read all sorts of input events such Kitty keyboard, background color, mode report, and all sorts of ANSI sequence input events. Supersedes: https://github.com/charmbracelet/bubbletea/pull/1079 Supersedes: https://github.com/charmbracelet/bubbletea/issues/1014 Related: https://github.com/charmbracelet/bubbletea/issues/869 Related: https://github.com/charmbracelet/bubbletea/issues/163 Related: https://github.com/charmbracelet/bubbletea/issues/918 Related: https://github.com/charmbracelet/bubbletea/issues/850 Related: https://github.com/charmbracelet/bubbletea/issues/207 --- inputreader_other.go => cancelreader_other.go | 2 +- cancelreader_windows.go | 217 +++ clipboard.go | 9 + color.go | 77 ++ cursor.go | 10 + da1.go | 18 + driver.go | 129 ++ driver_other.go | 11 + driver_test.go | 25 + driver_windows.go | 272 ++++ examples/go.mod | 8 +- examples/go.sum | 13 +- focus.go | 9 + focus_test.go | 25 + go.mod | 4 +- go.sum | 4 - input.go | 26 + inputreader_windows.go | 107 -- key.go | 1063 ++++++--------- key_deprecated.go | 259 ++++ key_other.go | 13 - key_sequences.go | 119 -- key_test.go | 564 ++++---- key_windows.go | 351 ----- kitty.go | 281 ++++ mod.go | 70 + mode.go | 12 + mouse.go | 368 +++-- mouse_deprecated.go | 162 +++ mouse_test.go | 1178 +++++------------ parse.go | 828 ++++++++++++ parse_test.go | 41 + paste.go | 13 + table.go | 391 ++++++ tea.go | 31 +- termcap.go | 54 + terminfo.go | 277 ++++ tty.go | 154 ++- tutorials/go.mod | 8 +- tutorials/go.sum | 16 +- win32input.go | 240 ++++ xterm.go | 40 + 42 files changed, 4888 insertions(+), 2611 deletions(-) rename inputreader_other.go => cancelreader_other.go (66%) create mode 100644 cancelreader_windows.go create mode 100644 clipboard.go create mode 100644 color.go create mode 100644 cursor.go create mode 100644 da1.go create mode 100644 driver.go create mode 100644 driver_other.go create mode 100644 driver_test.go create mode 100644 driver_windows.go create mode 100644 focus.go create mode 100644 focus_test.go create mode 100644 input.go delete mode 100644 inputreader_windows.go create mode 100644 key_deprecated.go delete mode 100644 key_other.go delete mode 100644 key_sequences.go delete mode 100644 key_windows.go create mode 100644 kitty.go create mode 100644 mod.go create mode 100644 mode.go create mode 100644 mouse_deprecated.go create mode 100644 parse.go create mode 100644 parse_test.go create mode 100644 paste.go create mode 100644 table.go create mode 100644 termcap.go create mode 100644 terminfo.go create mode 100644 win32input.go create mode 100644 xterm.go diff --git a/inputreader_other.go b/cancelreader_other.go similarity index 66% rename from inputreader_other.go rename to cancelreader_other.go index 8e63a87dc6..5c6e277cb9 100644 --- a/inputreader_other.go +++ b/cancelreader_other.go @@ -9,6 +9,6 @@ import ( "github.com/muesli/cancelreader" ) -func newInputReader(r io.Reader) (cancelreader.CancelReader, error) { +func newCancelreader(r io.Reader) (cancelreader.CancelReader, error) { return cancelreader.NewReader(r) } diff --git a/cancelreader_windows.go b/cancelreader_windows.go new file mode 100644 index 0000000000..15bc3f6254 --- /dev/null +++ b/cancelreader_windows.go @@ -0,0 +1,217 @@ +//go:build windows +// +build windows + +package tea + +import ( + "fmt" + "io" + "os" + "sync" + "time" + + "github.com/erikgeiser/coninput" + "github.com/muesli/cancelreader" + "golang.org/x/sys/windows" +) + +type conInputReader struct { + cancelMixin + + conin windows.Handle + cancelEvent windows.Handle + + originalMode uint32 + + // blockingReadSignal is used to signal that a blocking read is in progress. + blockingReadSignal chan struct{} +} + +var _ cancelreader.CancelReader = &conInputReader{} + +func newCancelreader(r io.Reader) (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 := coninput.NewStdinHandle() + if err != nil { + return fallback(r) + } + + originalMode, err := prepareConsole(conin, + windows.ENABLE_MOUSE_INPUT, + windows.ENABLE_WINDOW_INPUT, + windows.ENABLE_EXTENDED_FLAGS, + ) + if err != nil { + return nil, fmt.Errorf("failed to prepare console input: %w", err) + } + + cancelEvent, err := windows.CreateEvent(nil, 0, 0, nil) + if err != nil { + return nil, fmt.Errorf("create stop event: %w", err) + } + + return &conInputReader{ + conin: conin, + cancelEvent: cancelEvent, + originalMode: originalMode, + blockingReadSignal: make(chan struct{}, 1), + }, nil +} + +// Cancel implements cancelreader.CancelReader. +func (r *conInputReader) Cancel() bool { + r.setCanceled() + + select { + case r.blockingReadSignal <- struct{}{}: + err := windows.SetEvent(r.cancelEvent) + if err != nil { + return false + } + <-r.blockingReadSignal + case <-time.After(100 * time.Millisecond): + // Read() hangs in a GetOverlappedResult which is likely due to + // WaitForMultipleObjects returning without input being available + // so we cannot cancel this ongoing read. + return false + } + + return true +} + +// Close implements cancelreader.CancelReader. +func (r *conInputReader) Close() error { + err := windows.CloseHandle(r.cancelEvent) + if err != nil { + return fmt.Errorf("closing cancel event handle: %w", err) + } + + 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) (n int, err error) { + if r.isCanceled() { + return 0, cancelreader.ErrCanceled + } + + err = waitForInput(r.conin, r.cancelEvent) + if err != nil { + return 0, err + } + + if r.isCanceled() { + return 0, cancelreader.ErrCanceled + } + + r.blockingReadSignal <- struct{}{} + n, err = overlappedReader(r.conin).Read(data) + <-r.blockingReadSignal + + return +} + +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) + } + + newMode := coninput.AddInputModes(0, modes...) + + err = windows.SetConsoleMode(input, newMode) + if err != nil { + return 0, fmt.Errorf("set console mode: %w", err) + } + + return originalMode, nil +} + +func waitForInput(conin, cancel windows.Handle) error { + event, err := windows.WaitForMultipleObjects([]windows.Handle{conin, cancel}, false, windows.INFINITE) + switch { + case windows.WAIT_OBJECT_0 <= event && event < windows.WAIT_OBJECT_0+2: + if event == windows.WAIT_OBJECT_0+1 { + return cancelreader.ErrCanceled + } + + if event == windows.WAIT_OBJECT_0 { + return nil + } + + return fmt.Errorf("unexpected wait object is ready: %d", event-windows.WAIT_OBJECT_0) + case windows.WAIT_ABANDONED <= event && event < windows.WAIT_ABANDONED+2: + return fmt.Errorf("abandoned") + case event == uint32(windows.WAIT_TIMEOUT): + return fmt.Errorf("timeout") + case event == windows.WAIT_FAILED: + return fmt.Errorf("failed") + default: + return fmt.Errorf("unexpected error: %w", err) + } +} + +// 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 +} + +type overlappedReader windows.Handle + +// Read performs an overlapping read fom a windows.Handle. +func (r overlappedReader) Read(data []byte) (int, error) { + hevent, err := windows.CreateEvent(nil, 0, 0, nil) + if err != nil { + return 0, fmt.Errorf("create event: %w", err) + } + + overlapped := windows.Overlapped{HEvent: hevent} + + var n uint32 + + err = windows.ReadFile(windows.Handle(r), data, &n, &overlapped) + if err != nil && err != windows.ERROR_IO_PENDING { + return int(n), err + } + + err = windows.GetOverlappedResult(windows.Handle(r), &overlapped, &n, true) + if err != nil { + return int(n), nil + } + + return int(n), nil +} diff --git a/clipboard.go b/clipboard.go new file mode 100644 index 0000000000..619844485a --- /dev/null +++ b/clipboard.go @@ -0,0 +1,9 @@ +package tea + +// ClipboardEvent is a clipboard read event. +type ClipboardEvent string + +// String returns the string representation of the clipboard event. +func (e ClipboardEvent) String() string { + return string(e) +} diff --git a/color.go b/color.go new file mode 100644 index 0000000000..20dad064f3 --- /dev/null +++ b/color.go @@ -0,0 +1,77 @@ +package tea + +import ( + "fmt" + "image/color" + "strconv" + "strings" +) + +// ForegroundColorEvent represents a foreground color change event. +type ForegroundColorEvent struct{ color.Color } + +// String implements fmt.Stringer. +func (e ForegroundColorEvent) String() string { + return colorToHex(e) +} + +// BackgroundColorEvent represents a background color change event. +type BackgroundColorEvent struct{ color.Color } + +// String implements fmt.Stringer. +func (e BackgroundColorEvent) String() string { + return colorToHex(e) +} + +// CursorColorEvent represents a cursor color change event. +type CursorColorEvent struct{ color.Color } + +// String implements fmt.Stringer. +func (e CursorColorEvent) String() string { + return colorToHex(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 { + r, g, b, _ := c.RGBA() + return fmt.Sprintf("#%02x%02x%02x", shift(r), shift(g), shift(b)) +} + +func xParseColor(s string) color.Color { + switch { + case strings.HasPrefix(s, "rgb:"): + parts := strings.Split(s[4:], "/") + if len(parts) != 3 { + return color.Black + } + + r, _ := strconv.ParseUint(parts[0], 16, 32) + g, _ := strconv.ParseUint(parts[1], 16, 32) + b, _ := strconv.ParseUint(parts[2], 16, 32) + + return color.RGBA{uint8(shift(r)), uint8(shift(g)), uint8(shift(b)), 255} + case strings.HasPrefix(s, "rgba:"): + parts := strings.Split(s[5:], "/") + if len(parts) != 4 { + return color.Black + } + + r, _ := strconv.ParseUint(parts[0], 16, 32) + g, _ := strconv.ParseUint(parts[1], 16, 32) + b, _ := strconv.ParseUint(parts[2], 16, 32) + a, _ := strconv.ParseUint(parts[3], 16, 32) + + return color.RGBA{uint8(shift(r)), uint8(shift(g)), uint8(shift(b)), uint8(shift(a))} + } + return color.Black +} diff --git a/cursor.go b/cursor.go new file mode 100644 index 0000000000..b231d46fa4 --- /dev/null +++ b/cursor.go @@ -0,0 +1,10 @@ +package tea + +// CursorPositionEvent represents a cursor position event. +type CursorPositionEvent struct { + // Row is the row number. + Row int + + // Column is the column number. + Column int +} diff --git a/da1.go b/da1.go new file mode 100644 index 0000000000..02cf89157f --- /dev/null +++ b/da1.go @@ -0,0 +1,18 @@ +package tea + +import "github.com/charmbracelet/x/ansi" + +// PrimaryDeviceAttributesMsg represents a primary device attributes message. +type PrimaryDeviceAttributesMsg []uint + +func parsePrimaryDevAttrs(csi *ansi.CsiSequence) Msg { + // Primary Device Attributes + da1 := make(PrimaryDeviceAttributesMsg, len(csi.Params)) + csi.Range(func(i int, p int, hasMore bool) bool { + if !hasMore { + da1[i] = uint(p) + } + return true + }) + return da1 +} diff --git a/driver.go b/driver.go new file mode 100644 index 0000000000..b31a7c2d7e --- /dev/null +++ b/driver.go @@ -0,0 +1,129 @@ +package tea + +import ( + "bytes" + "io" + "unicode/utf8" + + "github.com/erikgeiser/coninput" + "github.com/muesli/cancelreader" +) + +// driver represents an ANSI terminal input driver. +// It reads input events and parses ANSI sequences from the terminal input +// buffer. +type driver struct { + rd cancelreader.CancelReader + table map[string]Key // table is a lookup table for key sequences. + + term string // term is the terminal name $TERM. + + // paste is the bracketed paste mode buffer. + // When nil, bracketed paste mode is disabled. + paste []byte + + buf [256]byte // do we need a larger buffer? + + // prevMouseState keeps track of the previous mouse state to determine mouse + // up button events. + prevMouseState coninput.ButtonState // nolint: unused + + // lastWinsizeEvent keeps track of the last window size event to prevent + // multiple size events from firing. + lastWinsizeEvent coninput.WindowBufferSizeEventRecord // nolint: unused + + flags int // control the behavior of the driver. +} + +// newDriver returns a new ANSI input driver. +// This driver uses ANSI control codes compatible with VT100/VT200 terminals, +// and XTerm. It supports reading Terminfo databases to overwrite the default +// key sequences. +func newDriver(r io.Reader, term string, flags int) (*driver, error) { + d := new(driver) + cr, err := newCancelreader(r) + if err != nil { + return nil, err + } + + d.rd = cr + d.table = buildKeysTable(flags, term) + d.term = term + d.flags = flags + return d, nil +} + +// Cancel cancels the underlying reader. +func (d *driver) Cancel() bool { + return d.rd.Cancel() +} + +// Close closes the underlying reader. +func (d *driver) Close() error { + return d.rd.Close() +} + +func (d *driver) readEvents() (msgs []Msg, err error) { + nb, err := d.rd.Read(d.buf[:]) + if err != nil { + return nil, err + } + + buf := d.buf[:nb] + + // Lookup table first + if bytes.HasPrefix(buf, []byte{'\x1b'}) { + if k, ok := d.table[string(buf)]; ok { + msgs = append(msgs, KeyPressMsg(k)) + return + } + } + + var i int + for i < len(buf) { + nb, ev := parseSequence(buf[i:]) + + // Handle bracketed-paste + if d.paste != nil { + if _, ok := ev.(PasteEndMsg); !ok { + d.paste = append(d.paste, buf[i]) + i++ + continue + } + } + + switch ev.(type) { + case UnknownMsg: + // If the sequence is not recognized by the parser, try looking it up. + if k, ok := d.table[string(buf[i:i+nb])]; ok { + ev = KeyPressMsg(k) + } + case PasteStartMsg: + d.paste = []byte{} + case PasteEndMsg: + // Decode the captured data into runes. + 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 // reset the buffer + msgs = append(msgs, PasteMsg(paste)) + case nil: + i++ + continue + } + + if mevs, ok := ev.(multiMsg); ok { + msgs = append(msgs, []Msg(mevs)...) + } else { + msgs = append(msgs, ev) + } + i += nb + } + + return +} diff --git a/driver_other.go b/driver_other.go new file mode 100644 index 0000000000..8e67ec1e4f --- /dev/null +++ b/driver_other.go @@ -0,0 +1,11 @@ +//go:build !windows +// +build !windows + +package tea + +// ReadEvents reads input events from the terminal. +// +// It reads the events available in the input buffer and returns them. +func (d *driver) ReadEvents() ([]Msg, error) { + return d.readEvents() +} diff --git a/driver_test.go b/driver_test.go new file mode 100644 index 0000000000..214d536510 --- /dev/null +++ b/driver_test.go @@ -0,0 +1,25 @@ +package tea + +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 := newDriver(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/driver_windows.go b/driver_windows.go new file mode 100644 index 0000000000..0f257edcff --- /dev/null +++ b/driver_windows.go @@ -0,0 +1,272 @@ +//go:build windows +// +build windows + +package tea + +import ( + "errors" + "fmt" + "unicode/utf16" + + "github.com/charmbracelet/x/ansi" + "github.com/erikgeiser/coninput" + "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 *driver) ReadEvents() ([]Msg, error) { + events, err := d.handleConInput(coninput.ReadConsoleInput) + if errors.Is(err, errNotConInputReader) { + return d.readEvents() + } + return events, err +} + +var errNotConInputReader = fmt.Errorf("handleConInput: not a conInputReader") + +func (d *driver) handleConInput( + finput func(windows.Handle, []coninput.InputRecord) (uint32, error), +) ([]Msg, error) { + cc, ok := d.rd.(*conInputReader) + if !ok { + return nil, errNotConInputReader + } + + // read up to 256 events, this is to allow for sequences events reported as + // key events. + var events [256]coninput.InputRecord + _, err := finput(cc.conin, events[:]) + if err != nil { + return nil, fmt.Errorf("read coninput events: %w", err) + } + + var evs []Msg + for _, event := range events { + if e := parseConInputEvent(event, &d.prevMouseState, &d.lastWinsizeEvent); e != nil { + evs = append(evs, e) + } + } + + return d.detectConInputQuerySequences(evs), nil +} + +// Using ConInput API, Windows Terminal responds to sequence query events with +// KEY_EVENT_RECORDs so we need to collect them and parse them as a single +// sequence. +// Is this a hack? +func (d *driver) detectConInputQuerySequences(events []Msg) []Msg { + var newEvents []Msg + start, end := -1, -1 + +loop: + for i, e := range events { + switch e := e.(type) { + case KeyPressMsg: + switch e.Rune() { + case ansi.ESC, ansi.CSI, ansi.OSC, ansi.DCS, ansi.APC: + // start of a sequence + if start == -1 { + start = i + } + } + default: + break loop + } + end = i + } + + if start == -1 || end <= start { + return events + } + + var seq []byte + for i := start; i <= end; i++ { + switch e := events[i].(type) { + case KeyPressMsg: + seq = append(seq, byte(e.Rune())) + } + } + + n, seqevent := parseSequence(seq) + switch seqevent.(type) { + case UnknownMsg: + // We're not interested in unknown events + default: + if start+n > len(events) { + return events + } + newEvents = events[:start] + newEvents = append(newEvents, seqevent) + newEvents = append(newEvents, events[start+n:]...) + return d.detectConInputQuerySequences(newEvents) + } + + return events +} + +func parseConInputEvent(event coninput.InputRecord, ps *coninput.ButtonState, ws *coninput.WindowBufferSizeEventRecord) Msg { + switch e := event.Unwrap().(type) { + case coninput.KeyEventRecord: + event := parseWin32InputKeyEvent(e.VirtualKeyCode, e.VirtualScanCode, + e.Char, e.KeyDown, e.ControlKeyState, e.RepeatCount) + + var key Key + switch event := event.(type) { + case KeyPressMsg: + key = Key(event) + case KeyReleaseMsg: + key = Key(event) + default: + return nil + } + + // If the key is not printable, return the event as is + // (e.g. function keys, arrows, etc.) + // Otherwise, try to translate it to a rune based on the active keyboard + // layout. + if len(key.Runes) == 0 { + return event + } + + // Always use US layout for translation + // This is to follow the behavior of the Kitty Keyboard base layout + // feature :eye_roll: + // https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-language-pack-default-values?view=windows-11 + const usLayout = 0x409 + + // Translate key to rune + var keyState [256]byte + var utf16Buf [16]uint16 + const dontChangeKernelKeyboardLayout = 0x4 + ret := windows.ToUnicodeEx( + uint32(e.VirtualKeyCode), + uint32(e.VirtualScanCode), + &keyState[0], + &utf16Buf[0], + int32(len(utf16Buf)), + dontChangeKernelKeyboardLayout, + usLayout, + ) + + // -1 indicates a dead key + // 0 indicates no translation for this key + if ret < 1 { + return event + } + + runes := utf16.Decode(utf16Buf[:ret]) + if len(runes) != 1 { + // Key doesn't translate to a single rune + return event + } + + key.baseRune = runes[0] + if e.KeyDown { + return KeyPressMsg(key) + } + + return KeyReleaseMsg(key) + + case coninput.WindowBufferSizeEventRecord: + if e != *ws { + *ws = e + return WindowSizeMsg{ + Width: int(e.Size.X), + Height: int(e.Size.Y), + } + } + case coninput.MouseEventRecord: + mevent := mouseEvent(*ps, e) + *ps = e.ButtonState + return mevent + case coninput.FocusEventRecord, coninput.MenuEventRecord: + // ignore + } + return nil +} + +func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, isRelease bool) { + btn := p ^ s + if btn&s == 0 { + isRelease = true + } + + if btn == 0 { + switch { + case s&coninput.FROM_LEFT_1ST_BUTTON_PRESSED > 0: + button = MouseLeft + case s&coninput.FROM_LEFT_2ND_BUTTON_PRESSED > 0: + button = MouseMiddle + case s&coninput.RIGHTMOST_BUTTON_PRESSED > 0: + button = MouseRight + case s&coninput.FROM_LEFT_3RD_BUTTON_PRESSED > 0: + button = MouseBackward + case s&coninput.FROM_LEFT_4TH_BUTTON_PRESSED > 0: + button = MouseForward + } + return + } + + switch btn { + case coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button + button = MouseLeft + case coninput.RIGHTMOST_BUTTON_PRESSED: // right button + button = MouseRight + case coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button + button = MouseMiddle + case coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward) + button = MouseBackward + case coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward) + button = MouseForward + } + + return +} + +func mouseEvent(p coninput.ButtonState, e coninput.MouseEventRecord) (ev Msg) { + var mod KeyMod + var isRelease bool + if e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED) { + mod |= ModAlt + } + if e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED) { + mod |= ModCtrl + } + if e.ControlKeyState.Contains(coninput.SHIFT_PRESSED) { + mod |= ModShift + } + m := Mouse{ + X: int(e.MousePositon.X), + Y: int(e.MousePositon.Y), + Mod: mod, + } + switch e.EventFlags { + case coninput.CLICK, coninput.DOUBLE_CLICK: + m.Button, isRelease = mouseEventButton(p, e.ButtonState) + case coninput.MOUSE_WHEELED: + if e.WheelDirection > 0 { + m.Button = MouseWheelUp + } else { + m.Button = MouseWheelDown + } + case coninput.MOUSE_HWHEELED: + if e.WheelDirection > 0 { + m.Button = MouseWheelRight + } else { + m.Button = MouseWheelLeft + } + case coninput.MOUSE_MOVED: + m.Button, _ = mouseEventButton(p, e.ButtonState) + return MouseMotionMsg(m) + } + + if isWheel(m.Button) { + return MouseWheelMsg(m) + } else if isRelease { + return MouseReleaseMsg(m) + } + + return MouseClickMsg(m) +} diff --git a/examples/go.mod b/examples/go.mod index b33f5c9bda..40598a5479 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -1,6 +1,8 @@ module examples -go 1.18 +go 1.21 + +toolchain go1.22.5 require ( github.com/charmbracelet/bubbles v0.18.0 @@ -29,7 +31,6 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gorilla/css v1.0.1 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect @@ -43,9 +44,8 @@ require ( github.com/yuin/goldmark-emoji v1.0.3 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect + golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect ) replace github.com/charmbracelet/bubbletea => ../ diff --git a/examples/go.sum b/examples/go.sum index 0ab3f68368..75ebdfe9b3 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,7 +1,9 @@ github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -41,13 +43,13 @@ github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776/go.mod h1:9wvnDu3YOf github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 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-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -75,15 +77,14 @@ github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRla github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= diff --git a/focus.go b/focus.go new file mode 100644 index 0000000000..4d34bea6f8 --- /dev/null +++ b/focus.go @@ -0,0 +1,9 @@ +package tea + +// FocusMsg represents a terminal focus message. +// This occurs when the terminal gains focus. +type FocusMsg struct{} + +// BlurMsg represents a terminal blur message. +// This occurs when the terminal loses focus. +type BlurMsg struct{} diff --git a/focus_test.go b/focus_test.go new file mode 100644 index 0000000000..2871c3af1d --- /dev/null +++ b/focus_test.go @@ -0,0 +1,25 @@ +package tea + +import ( + "testing" +) + +func TestFocus(t *testing.T) { + _, e := parseSequence([]byte("\x1b[I")) + switch e.(type) { + case FocusMsg: + // ok + default: + t.Error("invalid sequence") + } +} + +func TestBlur(t *testing.T) { + _, e := parseSequence([]byte("\x1b[O")) + switch e.(type) { + case BlurMsg: + // ok + default: + t.Error("invalid sequence") + } +} diff --git a/go.mod b/go.mod index 837102c6a9..34bcfa4f9a 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( github.com/charmbracelet/x/ansi v0.1.4 github.com/charmbracelet/x/term v0.1.1 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f - github.com/mattn/go-localereader v0.0.1 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 github.com/muesli/cancelreader v0.2.2 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e golang.org/x/sync v0.8.0 golang.org/x/sys v0.24.0 ) @@ -18,6 +18,4 @@ require ( github.com/charmbracelet/x/windows v0.1.0 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index d07e6adb09..afe8f921d0 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wp github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= @@ -27,5 +25,3 @@ golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/input.go b/input.go new file mode 100644 index 0000000000..69ac277b26 --- /dev/null +++ b/input.go @@ -0,0 +1,26 @@ +package tea + +import ( + "fmt" + "strings" +) + +// UnknownMsg represents an unknown message. +type UnknownMsg string + +// String returns a string representation of the unknown message. +func (e UnknownMsg) String() string { + return fmt.Sprintf("%q", string(e)) +} + +// multiMsg represents multiple messages event. +type multiMsg []Msg + +// String returns a string representation of the multiple messages event. +func (e multiMsg) String() string { + var sb strings.Builder + for _, ev := range e { + sb.WriteString(fmt.Sprintf("%v\n", ev)) + } + return sb.String() +} diff --git a/inputreader_windows.go b/inputreader_windows.go deleted file mode 100644 index 449df4790c..0000000000 --- a/inputreader_windows.go +++ /dev/null @@ -1,107 +0,0 @@ -//go:build windows -// +build windows - -package tea - -import ( - "fmt" - "io" - "os" - "sync" - - "github.com/charmbracelet/x/term" - "github.com/erikgeiser/coninput" - "github.com/muesli/cancelreader" - "golang.org/x/sys/windows" -) - -type conInputReader struct { - cancelMixin - - conin windows.Handle - - originalMode uint32 -} - -var _ cancelreader.CancelReader = &conInputReader{} - -func newInputReader(r io.Reader) (cancelreader.CancelReader, error) { - fallback := func(io.Reader) (cancelreader.CancelReader, error) { - return cancelreader.NewReader(r) - } - if f, ok := r.(term.File); !ok || f.Fd() != os.Stdin.Fd() { - return fallback(r) - } - - conin, err := coninput.NewStdinHandle() - if err != nil { - return fallback(r) - } - - originalMode, err := prepareConsole(conin, - windows.ENABLE_MOUSE_INPUT, - windows.ENABLE_WINDOW_INPUT, - windows.ENABLE_EXTENDED_FLAGS, - ) - 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.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 (*conInputReader) Read(_ []byte) (n int, err error) { - return 0, 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) - } - - newMode := coninput.AddInputModes(0, modes...) - - 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 -} diff --git a/key.go b/key.go index 89a588aec0..94d9b8aefd 100644 --- a/key.go +++ b/key.go @@ -1,240 +1,77 @@ package tea -import ( - "context" - "fmt" - "io" - "regexp" - "strings" - "unicode/utf8" -) +// KeySym is a keyboard symbol. +type KeySym int -// KeyMsg contains information about a keypress. KeyMsgs are always sent to -// the program's update function. There are a couple general patterns you could -// use to check for keypresses: -// -// // Switch on the string representation of the key (shorter) -// switch msg := msg.(type) { -// case KeyMsg: -// switch msg.String() { -// case "enter": -// fmt.Println("you pressed enter!") -// case "a": -// fmt.Println("you pressed a!") -// } -// } -// -// // Switch on the key type (more foolproof) -// switch msg := msg.(type) { -// case KeyMsg: -// switch msg.Type { -// case KeyEnter: -// fmt.Println("you pressed enter!") -// case KeyRunes: -// switch string(msg.Runes) { -// case "a": -// fmt.Println("you pressed a!") -// } -// } -// } -// -// Note that Key.Runes will always contain at least one character, so you can -// always safely call Key.Runes[0]. In most cases Key.Runes will only contain -// one character, though certain input method editors (most notably Chinese -// IMEs) can input multiple runes at once. -type KeyMsg Key - -// String returns a string representation for a key message. It's safe (and -// encouraged) for use in key comparison. -func (k KeyMsg) String() (str string) { - return Key(k).String() -} - -// Key contains information about a keypress. -type Key struct { - Type KeyType - Runes []rune - Alt bool - Paste bool -} +// Key Symbol constants. +const ( + KeyNone KeySym = iota -// String returns a friendly string representation for a key. It's safe (and -// encouraged) for use in key comparison. -// -// k := Key{Type: KeyEnter} -// fmt.Println(k) -// // Output: enter -func (k Key) String() (str string) { - var buf strings.Builder - if k.Alt { - buf.WriteString("alt+") - } - if k.Type == KeyRunes { - if k.Paste { - // Note: bubbles/keys bindings currently do string compares to - // recognize shortcuts. Since pasted text should never activate - // shortcuts, we need to ensure that the binding code doesn't - // match Key events that result from pastes. We achieve this - // here by enclosing pastes in '[...]' so that the string - // comparison in Matches() fails in that case. - buf.WriteByte('[') - } - buf.WriteString(string(k.Runes)) - if k.Paste { - buf.WriteByte(']') - } - return buf.String() - } else if s, ok := keyNames[k.Type]; ok { - buf.WriteString(s) - return buf.String() - } - return "" -} + // Special names in C0 -// KeyType indicates the key pressed, such as KeyEnter or KeyBreak or KeyCtrlC. -// All other keys will be type KeyRunes. To get the rune value, check the Rune -// method on a Key struct, or use the Key.String() method: -// -// k := Key{Type: KeyRunes, Runes: []rune{'a'}, Alt: true} -// if k.Type == KeyRunes { -// -// fmt.Println(k.Runes) -// // Output: a -// -// fmt.Println(k.String()) -// // Output: alt+a -// -// } -type KeyType int + KeyBackspace + KeyTab + KeyEnter + KeyEscape -func (k KeyType) String() (str string) { - if s, ok := keyNames[k]; ok { - return s - } - return "" -} + // Special names in G0 -// Control keys. We could do this with an iota, but the values are very -// specific, so we set the values explicitly to avoid any confusion. -// -// See also: -// https://en.wikipedia.org/wiki/C0_and_C1_control_codes -const ( - keyNUL KeyType = 0 // null, \0 - keySOH KeyType = 1 // start of heading - keySTX KeyType = 2 // start of text - keyETX KeyType = 3 // break, ctrl+c - keyEOT KeyType = 4 // end of transmission - keyENQ KeyType = 5 // enquiry - keyACK KeyType = 6 // acknowledge - keyBEL KeyType = 7 // bell, \a - keyBS KeyType = 8 // backspace - keyHT KeyType = 9 // horizontal tabulation, \t - keyLF KeyType = 10 // line feed, \n - keyVT KeyType = 11 // vertical tabulation \v - keyFF KeyType = 12 // form feed \f - keyCR KeyType = 13 // carriage return, \r - keySO KeyType = 14 // shift out - keySI KeyType = 15 // shift in - keyDLE KeyType = 16 // data link escape - keyDC1 KeyType = 17 // device control one - keyDC2 KeyType = 18 // device control two - keyDC3 KeyType = 19 // device control three - keyDC4 KeyType = 20 // device control four - keyNAK KeyType = 21 // negative acknowledge - keySYN KeyType = 22 // synchronous idle - keyETB KeyType = 23 // end of transmission block - keyCAN KeyType = 24 // cancel - keyEM KeyType = 25 // end of medium - keySUB KeyType = 26 // substitution - keyESC KeyType = 27 // escape, \e - keyFS KeyType = 28 // file separator - keyGS KeyType = 29 // group separator - keyRS KeyType = 30 // record separator - keyUS KeyType = 31 // unit separator - keyDEL KeyType = 127 // delete. on most systems this is mapped to backspace, I hear -) + KeySpace + KeyDelete -// Control key aliases. -const ( - KeyNull KeyType = keyNUL - KeyBreak KeyType = keyETX - KeyEnter KeyType = keyCR - KeyBackspace KeyType = keyDEL - KeyTab KeyType = keyHT - KeyEsc KeyType = keyESC - KeyEscape KeyType = keyESC - - KeyCtrlAt KeyType = keyNUL // ctrl+@ - KeyCtrlA KeyType = keySOH - KeyCtrlB KeyType = keySTX - KeyCtrlC KeyType = keyETX - KeyCtrlD KeyType = keyEOT - KeyCtrlE KeyType = keyENQ - KeyCtrlF KeyType = keyACK - KeyCtrlG KeyType = keyBEL - KeyCtrlH KeyType = keyBS - KeyCtrlI KeyType = keyHT - KeyCtrlJ KeyType = keyLF - KeyCtrlK KeyType = keyVT - KeyCtrlL KeyType = keyFF - KeyCtrlM KeyType = keyCR - KeyCtrlN KeyType = keySO - KeyCtrlO KeyType = keySI - KeyCtrlP KeyType = keyDLE - KeyCtrlQ KeyType = keyDC1 - KeyCtrlR KeyType = keyDC2 - KeyCtrlS KeyType = keyDC3 - KeyCtrlT KeyType = keyDC4 - KeyCtrlU KeyType = keyNAK - KeyCtrlV KeyType = keySYN - KeyCtrlW KeyType = keyETB - KeyCtrlX KeyType = keyCAN - KeyCtrlY KeyType = keyEM - KeyCtrlZ KeyType = keySUB - KeyCtrlOpenBracket KeyType = keyESC // ctrl+[ - KeyCtrlBackslash KeyType = keyFS // ctrl+\ - KeyCtrlCloseBracket KeyType = keyGS // ctrl+] - KeyCtrlCaret KeyType = keyRS // ctrl+^ - KeyCtrlUnderscore KeyType = keyUS // ctrl+_ - KeyCtrlQuestionMark KeyType = keyDEL // ctrl+? -) + // Special keys -// Other keys. -const ( - KeyRunes KeyType = -(iota + 1) KeyUp KeyDown KeyRight KeyLeft - KeyShiftTab - KeyHome - KeyEnd + KeyBegin + KeyFind + KeyInsert + KeySelect KeyPgUp KeyPgDown - KeyCtrlPgUp - KeyCtrlPgDown - KeyDelete - KeyInsert - KeySpace - KeyCtrlUp - KeyCtrlDown - KeyCtrlRight - KeyCtrlLeft - KeyCtrlHome - KeyCtrlEnd - KeyShiftUp - KeyShiftDown - KeyShiftRight - KeyShiftLeft - KeyShiftHome - KeyShiftEnd - KeyCtrlShiftUp - KeyCtrlShiftDown - KeyCtrlShiftLeft - KeyCtrlShiftRight - KeyCtrlShiftHome - KeyCtrlShiftEnd + KeyHome + KeyEnd + + // Keypad keys + + KeyKpEnter + KeyKpEqual + KeyKpMultiply + KeyKpPlus + KeyKpComma + KeyKpMinus + KeyKpDecimal + KeyKpDivide + KeyKp0 + KeyKp1 + KeyKp2 + KeyKp3 + KeyKp4 + KeyKp5 + KeyKp6 + KeyKp7 + KeyKp8 + KeyKp9 + + // 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 @@ -255,454 +92,394 @@ const ( 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 + + // 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 ) -// Mappings for control keys and other special keys to friendly consts. -var keyNames = map[KeyType]string{ - // Control keys. - keyNUL: "ctrl+@", // also ctrl+` (that's ctrl+backtick) - keySOH: "ctrl+a", - keySTX: "ctrl+b", - keyETX: "ctrl+c", - keyEOT: "ctrl+d", - keyENQ: "ctrl+e", - keyACK: "ctrl+f", - keyBEL: "ctrl+g", - keyBS: "ctrl+h", - keyHT: "tab", // also ctrl+i - keyLF: "ctrl+j", - keyVT: "ctrl+k", - keyFF: "ctrl+l", - keyCR: "enter", - keySO: "ctrl+n", - keySI: "ctrl+o", - keyDLE: "ctrl+p", - keyDC1: "ctrl+q", - keyDC2: "ctrl+r", - keyDC3: "ctrl+s", - keyDC4: "ctrl+t", - keyNAK: "ctrl+u", - keySYN: "ctrl+v", - keyETB: "ctrl+w", - keyCAN: "ctrl+x", - keyEM: "ctrl+y", - keySUB: "ctrl+z", - keyESC: "esc", - keyFS: "ctrl+\\", - keyGS: "ctrl+]", - keyRS: "ctrl+^", - keyUS: "ctrl+_", - keyDEL: "backspace", - - // Other keys. - KeyRunes: "runes", - KeyUp: "up", - KeyDown: "down", - KeyRight: "right", - KeySpace: " ", // for backwards compatibility - KeyLeft: "left", - KeyShiftTab: "shift+tab", - KeyHome: "home", - KeyEnd: "end", - KeyCtrlHome: "ctrl+home", - KeyCtrlEnd: "ctrl+end", - KeyShiftHome: "shift+home", - KeyShiftEnd: "shift+end", - KeyCtrlShiftHome: "ctrl+shift+home", - KeyCtrlShiftEnd: "ctrl+shift+end", - KeyPgUp: "pgup", - KeyPgDown: "pgdown", - KeyCtrlPgUp: "ctrl+pgup", - KeyCtrlPgDown: "ctrl+pgdown", - KeyDelete: "delete", - KeyInsert: "insert", - KeyCtrlUp: "ctrl+up", - KeyCtrlDown: "ctrl+down", - KeyCtrlRight: "ctrl+right", - KeyCtrlLeft: "ctrl+left", - KeyShiftUp: "shift+up", - KeyShiftDown: "shift+down", - KeyShiftRight: "shift+right", - KeyShiftLeft: "shift+left", - KeyCtrlShiftUp: "ctrl+shift+up", - KeyCtrlShiftDown: "ctrl+shift+down", - KeyCtrlShiftLeft: "ctrl+shift+left", - KeyCtrlShiftRight: "ctrl+shift+right", - 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", -} +// Key represents a key event. +type Key struct { + // Sym is a special key, like enter, tab, backspace, and so on. + Sym KeySym -// Sequence mappings. -var sequences = map[string]Key{ - // Arrow keys - "\x1b[A": {Type: KeyUp}, - "\x1b[B": {Type: KeyDown}, - "\x1b[C": {Type: KeyRight}, - "\x1b[D": {Type: KeyLeft}, - "\x1b[1;2A": {Type: KeyShiftUp}, - "\x1b[1;2B": {Type: KeyShiftDown}, - "\x1b[1;2C": {Type: KeyShiftRight}, - "\x1b[1;2D": {Type: KeyShiftLeft}, - "\x1b[OA": {Type: KeyShiftUp}, // DECCKM - "\x1b[OB": {Type: KeyShiftDown}, // DECCKM - "\x1b[OC": {Type: KeyShiftRight}, // DECCKM - "\x1b[OD": {Type: KeyShiftLeft}, // DECCKM - "\x1b[a": {Type: KeyShiftUp}, // urxvt - "\x1b[b": {Type: KeyShiftDown}, // urxvt - "\x1b[c": {Type: KeyShiftRight}, // urxvt - "\x1b[d": {Type: KeyShiftLeft}, // urxvt - "\x1b[1;3A": {Type: KeyUp, Alt: true}, - "\x1b[1;3B": {Type: KeyDown, Alt: true}, - "\x1b[1;3C": {Type: KeyRight, Alt: true}, - "\x1b[1;3D": {Type: KeyLeft, Alt: true}, - - "\x1b[1;4A": {Type: KeyShiftUp, Alt: true}, - "\x1b[1;4B": {Type: KeyShiftDown, Alt: true}, - "\x1b[1;4C": {Type: KeyShiftRight, Alt: true}, - "\x1b[1;4D": {Type: KeyShiftLeft, Alt: true}, - - "\x1b[1;5A": {Type: KeyCtrlUp}, - "\x1b[1;5B": {Type: KeyCtrlDown}, - "\x1b[1;5C": {Type: KeyCtrlRight}, - "\x1b[1;5D": {Type: KeyCtrlLeft}, - "\x1b[Oa": {Type: KeyCtrlUp, Alt: true}, // urxvt - "\x1b[Ob": {Type: KeyCtrlDown, Alt: true}, // urxvt - "\x1b[Oc": {Type: KeyCtrlRight, Alt: true}, // urxvt - "\x1b[Od": {Type: KeyCtrlLeft, Alt: true}, // urxvt - "\x1b[1;6A": {Type: KeyCtrlShiftUp}, - "\x1b[1;6B": {Type: KeyCtrlShiftDown}, - "\x1b[1;6C": {Type: KeyCtrlShiftRight}, - "\x1b[1;6D": {Type: KeyCtrlShiftLeft}, - "\x1b[1;7A": {Type: KeyCtrlUp, Alt: true}, - "\x1b[1;7B": {Type: KeyCtrlDown, Alt: true}, - "\x1b[1;7C": {Type: KeyCtrlRight, Alt: true}, - "\x1b[1;7D": {Type: KeyCtrlLeft, Alt: true}, - "\x1b[1;8A": {Type: KeyCtrlShiftUp, Alt: true}, - "\x1b[1;8B": {Type: KeyCtrlShiftDown, Alt: true}, - "\x1b[1;8C": {Type: KeyCtrlShiftRight, Alt: true}, - "\x1b[1;8D": {Type: KeyCtrlShiftLeft, Alt: true}, - - // Miscellaneous keys - "\x1b[Z": {Type: KeyShiftTab}, - - "\x1b[2~": {Type: KeyInsert}, - "\x1b[3;2~": {Type: KeyInsert, Alt: true}, - - "\x1b[3~": {Type: KeyDelete}, - "\x1b[3;3~": {Type: KeyDelete, Alt: true}, - - "\x1b[5~": {Type: KeyPgUp}, - "\x1b[5;3~": {Type: KeyPgUp, Alt: true}, - "\x1b[5;5~": {Type: KeyCtrlPgUp}, - "\x1b[5^": {Type: KeyCtrlPgUp}, // urxvt - "\x1b[5;7~": {Type: KeyCtrlPgUp, Alt: true}, - - "\x1b[6~": {Type: KeyPgDown}, - "\x1b[6;3~": {Type: KeyPgDown, Alt: true}, - "\x1b[6;5~": {Type: KeyCtrlPgDown}, - "\x1b[6^": {Type: KeyCtrlPgDown}, // urxvt - "\x1b[6;7~": {Type: KeyCtrlPgDown, Alt: true}, - - "\x1b[1~": {Type: KeyHome}, - "\x1b[H": {Type: KeyHome}, // xterm, lxterm - "\x1b[1;3H": {Type: KeyHome, Alt: true}, // xterm, lxterm - "\x1b[1;5H": {Type: KeyCtrlHome}, // xterm, lxterm - "\x1b[1;7H": {Type: KeyCtrlHome, Alt: true}, // xterm, lxterm - "\x1b[1;2H": {Type: KeyShiftHome}, // xterm, lxterm - "\x1b[1;4H": {Type: KeyShiftHome, Alt: true}, // xterm, lxterm - "\x1b[1;6H": {Type: KeyCtrlShiftHome}, // xterm, lxterm - "\x1b[1;8H": {Type: KeyCtrlShiftHome, Alt: true}, // xterm, lxterm - - "\x1b[4~": {Type: KeyEnd}, - "\x1b[F": {Type: KeyEnd}, // xterm, lxterm - "\x1b[1;3F": {Type: KeyEnd, Alt: true}, // xterm, lxterm - "\x1b[1;5F": {Type: KeyCtrlEnd}, // xterm, lxterm - "\x1b[1;7F": {Type: KeyCtrlEnd, Alt: true}, // xterm, lxterm - "\x1b[1;2F": {Type: KeyShiftEnd}, // xterm, lxterm - "\x1b[1;4F": {Type: KeyShiftEnd, Alt: true}, // xterm, lxterm - "\x1b[1;6F": {Type: KeyCtrlShiftEnd}, // xterm, lxterm - "\x1b[1;8F": {Type: KeyCtrlShiftEnd, Alt: true}, // xterm, lxterm - - "\x1b[7~": {Type: KeyHome}, // urxvt - "\x1b[7^": {Type: KeyCtrlHome}, // urxvt - "\x1b[7$": {Type: KeyShiftHome}, // urxvt - "\x1b[7@": {Type: KeyCtrlShiftHome}, // urxvt - - "\x1b[8~": {Type: KeyEnd}, // urxvt - "\x1b[8^": {Type: KeyCtrlEnd}, // urxvt - "\x1b[8$": {Type: KeyShiftEnd}, // urxvt - "\x1b[8@": {Type: KeyCtrlShiftEnd}, // urxvt - - // Function keys, Linux console - "\x1b[[A": {Type: KeyF1}, // linux console - "\x1b[[B": {Type: KeyF2}, // linux console - "\x1b[[C": {Type: KeyF3}, // linux console - "\x1b[[D": {Type: KeyF4}, // linux console - "\x1b[[E": {Type: KeyF5}, // linux console - - // Function keys, X11 - "\x1bOP": {Type: KeyF1}, // vt100, xterm - "\x1bOQ": {Type: KeyF2}, // vt100, xterm - "\x1bOR": {Type: KeyF3}, // vt100, xterm - "\x1bOS": {Type: KeyF4}, // vt100, xterm - - "\x1b[1;3P": {Type: KeyF1, Alt: true}, // vt100, xterm - "\x1b[1;3Q": {Type: KeyF2, Alt: true}, // vt100, xterm - "\x1b[1;3R": {Type: KeyF3, Alt: true}, // vt100, xterm - "\x1b[1;3S": {Type: KeyF4, Alt: true}, // vt100, xterm - - "\x1b[11~": {Type: KeyF1}, // urxvt - "\x1b[12~": {Type: KeyF2}, // urxvt - "\x1b[13~": {Type: KeyF3}, // urxvt - "\x1b[14~": {Type: KeyF4}, // urxvt - - "\x1b[15~": {Type: KeyF5}, // vt100, xterm, also urxvt - - "\x1b[15;3~": {Type: KeyF5, Alt: true}, // vt100, xterm, also urxvt - - "\x1b[17~": {Type: KeyF6}, // vt100, xterm, also urxvt - "\x1b[18~": {Type: KeyF7}, // vt100, xterm, also urxvt - "\x1b[19~": {Type: KeyF8}, // vt100, xterm, also urxvt - "\x1b[20~": {Type: KeyF9}, // vt100, xterm, also urxvt - "\x1b[21~": {Type: KeyF10}, // vt100, xterm, also urxvt - - "\x1b[17;3~": {Type: KeyF6, Alt: true}, // vt100, xterm - "\x1b[18;3~": {Type: KeyF7, Alt: true}, // vt100, xterm - "\x1b[19;3~": {Type: KeyF8, Alt: true}, // vt100, xterm - "\x1b[20;3~": {Type: KeyF9, Alt: true}, // vt100, xterm - "\x1b[21;3~": {Type: KeyF10, Alt: true}, // vt100, xterm - - "\x1b[23~": {Type: KeyF11}, // vt100, xterm, also urxvt - "\x1b[24~": {Type: KeyF12}, // vt100, xterm, also urxvt - - "\x1b[23;3~": {Type: KeyF11, Alt: true}, // vt100, xterm - "\x1b[24;3~": {Type: KeyF12, Alt: true}, // vt100, xterm - - "\x1b[1;2P": {Type: KeyF13}, - "\x1b[1;2Q": {Type: KeyF14}, - - "\x1b[25~": {Type: KeyF13}, // vt100, xterm, also urxvt - "\x1b[26~": {Type: KeyF14}, // vt100, xterm, also urxvt - - "\x1b[25;3~": {Type: KeyF13, Alt: true}, // vt100, xterm - "\x1b[26;3~": {Type: KeyF14, Alt: true}, // vt100, xterm - - "\x1b[1;2R": {Type: KeyF15}, - "\x1b[1;2S": {Type: KeyF16}, - - "\x1b[28~": {Type: KeyF15}, // vt100, xterm, also urxvt - "\x1b[29~": {Type: KeyF16}, // vt100, xterm, also urxvt - - "\x1b[28;3~": {Type: KeyF15, Alt: true}, // vt100, xterm - "\x1b[29;3~": {Type: KeyF16, Alt: true}, // vt100, xterm - - "\x1b[15;2~": {Type: KeyF17}, - "\x1b[17;2~": {Type: KeyF18}, - "\x1b[18;2~": {Type: KeyF19}, - "\x1b[19;2~": {Type: KeyF20}, - - "\x1b[31~": {Type: KeyF17}, - "\x1b[32~": {Type: KeyF18}, - "\x1b[33~": {Type: KeyF19}, - "\x1b[34~": {Type: KeyF20}, - - // Powershell sequences. - "\x1bOA": {Type: KeyUp, Alt: false}, - "\x1bOB": {Type: KeyDown, Alt: false}, - "\x1bOC": {Type: KeyRight, Alt: false}, - "\x1bOD": {Type: KeyLeft, Alt: false}, + // Runes contains the actual characters received. This usually has a length + // of 1. Use [Rune()] to get the first key rune received. If the user + // presses shift+a, the Runes will be `[]rune{'A'}`. + Runes []rune + + // altRune is the actual, unshifted key pressed by the user. For example, + // if the user presses shift+a, or caps lock is on, the altRune will be + // 'a'. + // + // In the case of non-latin keyboards, like Arabic, altRune is the + // unshifted key on the keyboard. + // + // This is only available with the Kitty Keyboard Protocol or the Windows + // Console API. + altRune rune + + // baseRune is the key pressed according to the standard PC-101 key layout. + // On internaltional keyboards, this is the key that would be pressed if + // the keyboard was set to US layout. + // + // For example, if the user presses 'q' on a French AZERTY keyboard, the + // baseRune will be 'q'. + // + // This is only available with the Kitty Keyboard Protocol or the Windows + // Console API. + baseRune rune + + // Mod is a modifier key, like ctrl, alt, and so on. + Mod KeyMod + + // 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 } -// unknownInputByteMsg is reported by the input reader when an invalid -// utf-8 byte is detected on the input. Currently, it is not handled -// further by bubbletea. However, having this event makes it possible -// to troubleshoot invalid inputs. -type unknownInputByteMsg byte +// KeyPressMsg represents a key press message. +type KeyPressMsg Key -func (u unknownInputByteMsg) String() string { - return fmt.Sprintf("?%#02x?", int(u)) +// String implements fmt.Stringer and is quite useful for matching key +// events. For details, on what this returns see [Key.String]. +func (k KeyPressMsg) String() string { + return Key(k).String() } -// unknownCSISequenceMsg is reported by the input reader when an -// unrecognized CSI sequence is detected on the input. Currently, it -// is not handled further by bubbletea. However, having this event -// makes it possible to troubleshoot invalid inputs. -type unknownCSISequenceMsg []byte - -func (u unknownCSISequenceMsg) String() string { - return fmt.Sprintf("?CSI%+v?", []byte(u)[2:]) +// Rune returns the first rune in the Runes field. If the Runes field is empty, +// it returns 0. +func (k KeyPressMsg) Rune() rune { + return Key(k).Rune() } -var spaceRunes = []rune{' '} +// KeyReleaseMsg represents a key release message. +type KeyReleaseMsg Key -// readAnsiInputs reads keypress and mouse inputs from a TTY and produces messages -// containing information about the key or mouse events accordingly. -func readAnsiInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { - var buf [256]byte +// String implements fmt.Stringer and is quite useful for matching complex key +// events. For details, on what this returns see [Key.String]. +func (k KeyReleaseMsg) String() string { + return Key(k).String() +} - var leftOverFromPrevIteration []byte -loop: - for { - // Read and block. - numBytes, err := input.Read(buf[:]) - if err != nil { - return fmt.Errorf("error reading input: %w", err) - } - b := buf[:numBytes] - if leftOverFromPrevIteration != nil { - b = append(leftOverFromPrevIteration, b...) - } +// Rune returns the first rune in the Runes field. If the Runes field is empty, +// it returns 0. +func (k KeyReleaseMsg) Rune() rune { + return Key(k).Rune() +} - // If we had a short read (numBytes < len(buf)), we're sure that - // the end of this read is an event boundary, so there is no doubt - // if we are encountering the end of the buffer while parsing a message. - // However, if we've succeeded in filling up the buffer, there may - // be more data in the OS buffer ready to be read in, to complete - // the last message in the input. In that case, we will retry with - // the left over data in the next iteration. - canHaveMoreData := numBytes == len(buf) - - var i, w int - for i, w = 0, 0; i < len(b); i += w { - var msg Msg - w, msg = detectOneMsg(b[i:], canHaveMoreData) - if w == 0 { - // Expecting more bytes beyond the current buffer. Try waiting - // for more input. - leftOverFromPrevIteration = make([]byte, 0, len(b[i:])+len(buf)) - leftOverFromPrevIteration = append(leftOverFromPrevIteration, b[i:]...) - continue loop - } - - select { - case msgs <- msg: - case <-ctx.Done(): - err := ctx.Err() - if err != nil { - err = fmt.Errorf("found context error while reading input: %w", err) - } - return err - } - } - leftOverFromPrevIteration = nil +// Rune returns the first rune in the Runes field. If the Runes field is empty, +// it returns 0. +func (k Key) Rune() rune { + if len(k.Runes) == 0 { + return 0 } + return k.Runes[0] } -var ( - unknownCSIRe = regexp.MustCompile(`^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]`) - mouseSGRRegex = regexp.MustCompile(`(\d+);(\d+);(\d+)([Mm])`) -) - -func detectOneMsg(b []byte, canHaveMoreData bool) (w int, msg Msg) { - // Detect mouse events. - // X10 mouse events have a length of 6 bytes - const mouseEventX10Len = 6 - if len(b) >= mouseEventX10Len && b[0] == '\x1b' && b[1] == '[' { - switch b[2] { - case 'M': - return mouseEventX10Len, MouseMsg(parseX10MouseEvent(b)) - case '<': - if matchIndices := mouseSGRRegex.FindSubmatchIndex(b[3:]); matchIndices != nil { - // SGR mouse events length is the length of the match plus the length of the escape sequence - mouseEventSGRLen := matchIndices[1] + 3 //nolint:gomnd - return mouseEventSGRLen, MouseMsg(parseSGRMouseEvent(b)) - } - } +// String implements fmt.Stringer and is used to convert a key to a string. +// 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) String() string { + var s string + if k.Mod.HasCtrl() && k.Sym != KeyLeftCtrl && k.Sym != KeyRightCtrl { + s += "ctrl+" } - - // Detect bracketed paste. - var foundbp bool - foundbp, w, msg = detectBracketedPaste(b) - if foundbp { - return w, msg + if k.Mod.HasAlt() && k.Sym != KeyLeftAlt && k.Sym != KeyRightAlt { + s += "alt+" } - - // Detect escape sequence and control characters other than NUL, - // possibly with an escape character in front to mark the Alt - // modifier. - var foundSeq bool - foundSeq, w, msg = detectSequence(b) - if foundSeq { - return w, msg + if k.Mod.HasShift() && k.Sym != KeyLeftShift && k.Sym != KeyRightShift { + s += "shift+" } - - // No non-NUL control character or escape sequence. - // If we are seeing at least an escape character, remember it for later below. - alt := false - i := 0 - if b[0] == '\x1b' { - alt = true - i++ + if k.Mod.HasMeta() && k.Sym != KeyLeftMeta && k.Sym != KeyRightMeta { + s += "meta+" } - - // Are we seeing a standalone NUL? This is not handled by detectSequence(). - if i < len(b) && b[i] == 0 { - return i + 1, KeyMsg{Type: keyNUL, Alt: alt} + if k.Mod.HasHyper() && k.Sym != KeyLeftHyper && k.Sym != KeyRightHyper { + s += "hyper+" + } + if k.Mod.HasSuper() && k.Sym != KeyLeftSuper && k.Sym != KeyRightSuper { + s += "super+" } - // Find the longest sequence of runes that are not control - // characters from this point. - var runes []rune - for rw := 0; i < len(b); i += rw { - var r rune - r, rw = utf8.DecodeRune(b[i:]) - if r == utf8.RuneError || r <= rune(keyUS) || r == rune(keyDEL) || r == ' ' { - // Rune errors are handled below; control characters and spaces will - // be handled by detectSequence in the next call to detectOneMsg. - break - } - runes = append(runes, r) - if alt { - // We only support a single rune after an escape alt modifier. - i += rw - break + runeStr := func(r rune) string { + // Space is the only invisible printable character. + if r == ' ' { + return "space" } + return string(r) } - if i >= len(b) && canHaveMoreData { - // We have encountered the end of the input buffer. Alas, we can't - // be sure whether the data in the remainder of the buffer is - // complete (maybe there was a short read). Instead of sending anything - // dumb to the message channel, do a short read. The outer loop will - // handle this case by extending the buffer as necessary. - return 0, nil - } - - // If we found at least one rune, we report the bunch of them as - // a single KeyRunes or KeySpace event. - if len(runes) > 0 { - k := Key{Type: KeyRunes, Runes: runes, Alt: alt} - if len(runes) == 1 && runes[0] == ' ' { - k.Type = KeySpace + if k.baseRune != 0 { + // If a baseRune is present, use it to represent a key using the standard + // PC-101 key layout. + s += runeStr(k.baseRune) + } else if k.altRune != 0 { + // Otherwise, use the AltRune aka the non-shifted one if present. + s += runeStr(k.altRune) + } else if len(k.Runes) > 0 { + // Else, just print the rune. + if len(k.Runes) > 1 { + s += string(k.Runes) + } else { + s += runeStr(k.Rune()) } - return i, KeyMsg(k) + } else { + s += k.Sym.String() } + return s +} - // We didn't find an escape sequence, nor a valid rune. Was this a - // lone escape character at the end of the input? - if alt && len(b) == 1 { - return 1, KeyMsg(Key{Type: KeyEscape}) +// String implements fmt.Stringer and prints the string representation of a of +// a Symbol key. +func (k KeySym) String() string { + s, ok := keySymString[k] + if !ok { + return "unknown" } + return s +} - // The character at the current position is neither an escape - // sequence, a valid rune start or a sole escape character. Report - // it as an invalid byte. - return 1, unknownInputByteMsg(b[0]) +var keySymString = map[KeySym]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/key_deprecated.go b/key_deprecated.go new file mode 100644 index 0000000000..f63b002583 --- /dev/null +++ b/key_deprecated.go @@ -0,0 +1,259 @@ +package tea + +import ( + "strings" +) + +// KeyMsg contains information about a keypress. KeyMsgs are always sent to +// the program's update function. There are a couple general patterns you could +// use to check for keypresses: +// +// // Switch on the string representation of the key (shorter) +// switch msg := msg.(type) { +// case KeyMsg: +// switch msg.String() { +// case "enter": +// fmt.Println("you pressed enter!") +// case "a": +// fmt.Println("you pressed a!") +// } +// } +// +// // Switch on the key type (more foolproof) +// switch msg := msg.(type) { +// case KeyMsg: +// switch msg.Type { +// case KeyEnter: +// fmt.Println("you pressed enter!") +// case KeyRunes: +// switch string(msg.Runes) { +// case "a": +// fmt.Println("you pressed a!") +// } +// } +// } +// +// Note that Key.Runes will always contain at least one character, so you can +// always safely call Key.Runes[0]. In most cases Key.Runes will only contain +// one character, though certain input method editors (most notably Chinese +// IMEs) can input multiple runes at once. +// +// Deprecated: KeyMsg is deprecated in favor of KeyPressMsg and KeyReleaseMsg. +type KeyMsg struct { + Type KeyType + Runes []rune + Alt bool + Paste bool +} + +// String returns a friendly string representation for a key. It's safe (and +// encouraged) for use in key comparison. +// +// k := Key{Type: KeyEnter} +// fmt.Println(k) +// // Output: enter +func (k KeyMsg) String() (str string) { + var buf strings.Builder + if k.Alt { + buf.WriteString("alt+") + } + if k.Type == KeyRunes { + if k.Paste { + // Note: bubbles/keys bindings currently do string compares to + // recognize shortcuts. Since pasted text should never activate + // shortcuts, we need to ensure that the binding code doesn't + // match Key events that result from pastes. We achieve this + // here by enclosing pastes in '[...]' so that the string + // comparison in Matches() fails in that case. + buf.WriteByte('[') + } + buf.WriteString(string(k.Runes)) + if k.Paste { + buf.WriteByte(']') + } + return buf.String() + } else if s, ok := keyNames[k.Type]; ok { + buf.WriteString(s) + return buf.String() + } + return "" +} + +// KeyType indicates the key pressed, such as KeyEnter or KeyBreak or KeyCtrlC. +// All other keys will be type KeyRunes. To get the rune value, check the Rune +// method on a Key struct, or use the Key.String() method: +// +// k := Key{Type: KeyRunes, Runes: []rune{'a'}, Alt: true} +// if k.Type == KeyRunes { +// +// fmt.Println(k.Runes) +// // Output: a +// +// fmt.Println(k.String()) +// // Output: alt+a +// +// } +type KeyType = KeySym + +// Control key aliases. +const ( + KeyNull KeyType = -iota - 1 + KeyBreak + + KeyCtrlAt // ctrl+@ + KeyCtrlA + KeyCtrlB + KeyCtrlC + KeyCtrlD + KeyCtrlE + KeyCtrlF + KeyCtrlG + KeyCtrlH + KeyCtrlI + KeyCtrlJ + KeyCtrlK + KeyCtrlL + KeyCtrlM + KeyCtrlN + KeyCtrlO + KeyCtrlP + KeyCtrlQ + KeyCtrlR + KeyCtrlS + KeyCtrlT + KeyCtrlU + KeyCtrlV + KeyCtrlW + KeyCtrlX + KeyCtrlY + KeyCtrlZ + KeyCtrlOpenBracket // ctrl+[ + KeyCtrlBackslash // ctrl+\ + KeyCtrlCloseBracket // ctrl+] + KeyCtrlCaret // ctrl+^ + KeyCtrlUnderscore // ctrl+_ + KeyCtrlQuestionMark // ctrl+? + KeyCtrlUp + KeyCtrlDown + KeyCtrlRight + KeyCtrlLeft + KeyCtrlPgUp + KeyCtrlPgDown + KeyCtrlHome + KeyCtrlEnd + + KeyShiftTab + KeyShiftUp + KeyShiftDown + KeyShiftRight + KeyShiftLeft + KeyShiftHome + KeyShiftEnd + + KeyCtrlShiftUp + KeyCtrlShiftDown + KeyCtrlShiftLeft + KeyCtrlShiftRight + KeyCtrlShiftHome + KeyCtrlShiftEnd + + // Deprecated: Use KeyEscape instead. + KeyEsc = KeyEscape + + // Deprecated: Use KeyNone instead. + KeyRunes = KeyNone +) + +// Mappings for control keys and other special keys to friendly consts. +var keyNames = map[KeyType]string{ + // Control keys. + KeyCtrlAt: "ctrl+@", // also ctrl+` (that's ctrl+backtick) + KeyCtrlA: "ctrl+a", + KeyCtrlB: "ctrl+b", + KeyCtrlC: "ctrl+c", + KeyCtrlD: "ctrl+d", + KeyCtrlE: "ctrl+e", + KeyCtrlF: "ctrl+f", + KeyCtrlG: "ctrl+g", + KeyCtrlH: "ctrl+h", + KeyTab: "tab", // also ctrl+i + KeyCtrlJ: "ctrl+j", + KeyCtrlK: "ctrl+k", + KeyCtrlL: "ctrl+l", + KeyEnter: "enter", + KeyCtrlN: "ctrl+n", + KeyCtrlO: "ctrl+o", + KeyCtrlP: "ctrl+p", + KeyCtrlQ: "ctrl+q", + KeyCtrlR: "ctrl+r", + KeyCtrlS: "ctrl+s", + KeyCtrlT: "ctrl+t", + KeyCtrlU: "ctrl+u", + KeyCtrlV: "ctrl+v", + KeyCtrlW: "ctrl+w", + KeyCtrlX: "ctrl+x", + KeyCtrlY: "ctrl+y", + KeyCtrlZ: "ctrl+z", + KeyEscape: "esc", + KeyCtrlOpenBracket: "ctrl+[", + KeyCtrlBackslash: "ctrl+\\", + KeyCtrlCloseBracket: "ctrl+]", + KeyCtrlCaret: "ctrl+^", + KeyCtrlUnderscore: "ctrl+_", + KeyBackspace: "backspace", + + // Other keys. + KeyRunes: "runes", + KeyUp: "up", + KeyDown: "down", + KeyRight: "right", + KeySpace: " ", // for backwards compatibility + KeyLeft: "left", + KeyShiftTab: "shift+tab", + KeyHome: "home", + KeyEnd: "end", + KeyCtrlHome: "ctrl+home", + KeyCtrlEnd: "ctrl+end", + KeyShiftHome: "shift+home", + KeyShiftEnd: "shift+end", + KeyCtrlShiftHome: "ctrl+shift+home", + KeyCtrlShiftEnd: "ctrl+shift+end", + KeyPgUp: "pgup", + KeyPgDown: "pgdown", + KeyCtrlPgUp: "ctrl+pgup", + KeyCtrlPgDown: "ctrl+pgdown", + KeyDelete: "delete", + KeyInsert: "insert", + KeyCtrlUp: "ctrl+up", + KeyCtrlDown: "ctrl+down", + KeyCtrlRight: "ctrl+right", + KeyCtrlLeft: "ctrl+left", + KeyShiftUp: "shift+up", + KeyShiftDown: "shift+down", + KeyShiftRight: "shift+right", + KeyShiftLeft: "shift+left", + KeyCtrlShiftUp: "ctrl+shift+up", + KeyCtrlShiftDown: "ctrl+shift+down", + KeyCtrlShiftLeft: "ctrl+shift+left", + KeyCtrlShiftRight: "ctrl+shift+right", + 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", +} diff --git a/key_other.go b/key_other.go deleted file mode 100644 index b8c46082f8..0000000000 --- a/key_other.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build !windows -// +build !windows - -package tea - -import ( - "context" - "io" -) - -func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { - return readAnsiInputs(ctx, msgs, input) -} diff --git a/key_sequences.go b/key_sequences.go deleted file mode 100644 index 4ba0f79e34..0000000000 --- a/key_sequences.go +++ /dev/null @@ -1,119 +0,0 @@ -package tea - -import ( - "bytes" - "sort" - "unicode/utf8" -) - -// extSequences is used by the map-based algorithm below. It contains -// the sequences plus their alternatives with an escape character -// prefixed, plus the control chars, plus the space. -// It does not contain the NUL character, which is handled specially -// by detectOneMsg. -var extSequences = func() map[string]Key { - s := map[string]Key{} - for seq, key := range sequences { - key := key - s[seq] = key - if !key.Alt { - key.Alt = true - s["\x1b"+seq] = key - } - } - for i := keyNUL + 1; i <= keyDEL; i++ { - if i == keyESC { - continue - } - s[string([]byte{byte(i)})] = Key{Type: i} - s[string([]byte{'\x1b', byte(i)})] = Key{Type: i, Alt: true} - if i == keyUS { - i = keyDEL - 1 - } - } - s[" "] = Key{Type: KeySpace, Runes: spaceRunes} - s["\x1b "] = Key{Type: KeySpace, Alt: true, Runes: spaceRunes} - s["\x1b\x1b"] = Key{Type: KeyEscape, Alt: true} - return s -}() - -// seqLengths is the sizes of valid sequences, starting with the -// largest size. -var seqLengths = func() []int { - sizes := map[int]struct{}{} - for seq := range extSequences { - sizes[len(seq)] = struct{}{} - } - lsizes := make([]int, 0, len(sizes)) - for sz := range sizes { - lsizes = append(lsizes, sz) - } - sort.Slice(lsizes, func(i, j int) bool { return lsizes[i] > lsizes[j] }) - return lsizes -}() - -// detectSequence uses a longest prefix match over the input -// sequence and a hash map. -func detectSequence(input []byte) (hasSeq bool, width int, msg Msg) { - seqs := extSequences - for _, sz := range seqLengths { - if sz > len(input) { - continue - } - prefix := input[:sz] - key, ok := seqs[string(prefix)] - if ok { - return true, sz, KeyMsg(key) - } - } - // Is this an unknown CSI sequence? - if loc := unknownCSIRe.FindIndex(input); loc != nil { - return true, loc[1], unknownCSISequenceMsg(input[:loc[1]]) - } - - return false, 0, nil -} - -// detectBracketedPaste detects an input pasted while bracketed -// paste mode was enabled. -// -// Note: this function is a no-op if bracketed paste was not enabled -// on the terminal, since in that case we'd never see this -// particular escape sequence. -func detectBracketedPaste(input []byte) (hasBp bool, width int, msg Msg) { - // Detect the start sequence. - const bpStart = "\x1b[200~" - if len(input) < len(bpStart) || string(input[:len(bpStart)]) != bpStart { - return false, 0, nil - } - - // Skip over the start sequence. - input = input[len(bpStart):] - - // If we saw the start sequence, then we must have an end sequence - // as well. Find it. - const bpEnd = "\x1b[201~" - idx := bytes.Index(input, []byte(bpEnd)) - inputLen := len(bpStart) + idx + len(bpEnd) - if idx == -1 { - // We have encountered the end of the input buffer without seeing - // the marker for the end of the bracketed paste. - // Tell the outer loop we have done a short read and we want more. - return true, 0, nil - } - - // The paste is everything in-between. - paste := input[:idx] - - // All there is in-between is runes, not to be interpreted further. - k := Key{Type: KeyRunes, Paste: true} - for len(paste) > 0 { - r, w := utf8.DecodeRune(paste) - if r != utf8.RuneError { - k.Runes = append(k.Runes, r) - } - paste = paste[w:] - } - - return true, inputLen, KeyMsg(k) -} diff --git a/key_test.go b/key_test.go index 67b0c50ed5..f8fcd524cc 100644 --- a/key_test.go +++ b/key_test.go @@ -15,53 +15,52 @@ import ( "sync" "testing" "time" + + "github.com/charmbracelet/x/ansi" ) +var sequences = buildKeysTable(_FlagTerminfo, "dumb") + func TestKeyString(t *testing.T) { t.Run("alt+space", func(t *testing.T) { - if got := KeyMsg(Key{ - Type: KeySpace, - Alt: true, - }).String(); got != "alt+ " { - t.Fatalf(`expected a "alt+ ", got %q`, got) + k := KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}, 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) { - if got := KeyMsg(Key{ - Type: KeyRunes, - Runes: []rune{'a'}, - }).String(); got != "a" { + k := KeyPressMsg{Runes: []rune{'a'}} + if got := k.String(); got != "a" { t.Fatalf(`expected an "a", got %q`, got) } }) t.Run("invalid", func(t *testing.T) { - if got := KeyMsg(Key{ - Type: KeyType(99999), - }).String(); got != "" { - t.Fatalf(`expected a "", got %q`, got) + k := KeyPressMsg{Sym: 99999} + if got := k.String(); got != "unknown" { + t.Fatalf(`expected a "unknown", got %q`, got) } }) } func TestKeyTypeString(t *testing.T) { t.Run("space", func(t *testing.T) { - if got := KeySpace.String(); got != " " { - t.Fatalf(`expected a " ", got %q`, got) + if got := KeySpace.String(); got != "space" { + t.Fatalf(`expected a "space", got %q`, got) } }) t.Run("invalid", func(t *testing.T) { - if got := KeyType(99999).String(); got != "" { - t.Fatalf(`expected a "", got %q`, got) + if got := KeySym(99999).String(); got != "unknown" { + t.Fatalf(`expected a "unknown", got %q`, got) } }) } type seqTest struct { - seq []byte - msg Msg + seq []byte + msgs []Msg } // buildBaseSeqTests returns sequence tests that are valid for the @@ -69,25 +68,7 @@ type seqTest struct { func buildBaseSeqTests() []seqTest { td := []seqTest{} for seq, key := range sequences { - key := key - td = append(td, seqTest{[]byte(seq), KeyMsg(key)}) - if !key.Alt { - key.Alt = true - td = append(td, seqTest{[]byte("\x1b" + seq), KeyMsg(key)}) - } - } - // Add all the control characters. - for i := keyNUL + 1; i <= keyDEL; i++ { - if i == keyESC { - // Not handled in detectSequence(), so not part of the base test - // suite. - continue - } - td = append(td, seqTest{[]byte{byte(i)}, KeyMsg{Type: i}}) - td = append(td, seqTest{[]byte{'\x1b', byte(i)}, KeyMsg{Type: i, Alt: true}}) - if i == keyUS { - i = keyDEL - 1 - } + td = append(td, seqTest{[]byte(seq), []Msg{KeyPressMsg(key)}}) } // Additional special cases. @@ -95,102 +76,129 @@ func buildBaseSeqTests() []seqTest { // Unrecognized CSI sequence. seqTest{ []byte{'\x1b', '[', '-', '-', '-', '-', 'X'}, - unknownCSISequenceMsg([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'}), + []Msg{ + UnknownMsg([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'}), + }, }, // A lone space character. seqTest{ []byte{' '}, - KeyMsg{Type: KeySpace, Runes: []rune(" ")}, + []Msg{ + KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}}, + }, }, // An escape character with the alt modifier. seqTest{ []byte{'\x1b', ' '}, - KeyMsg{Type: KeySpace, Runes: []rune(" "), Alt: true}, + []Msg{ + KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}, Mod: ModAlt}, + }, }, ) return td } -func TestDetectSequence(t *testing.T) { +func TestParseSequence(t *testing.T) { td := buildBaseSeqTests() - for _, tc := range td { - t.Run(fmt.Sprintf("%q", string(tc.seq)), func(t *testing.T) { - hasSeq, width, msg := detectSequence(tc.seq) - if !hasSeq { - t.Fatalf("no sequence found") - } - if width != len(tc.seq) { - t.Errorf("parser did not consume the entire input: got %d, expected %d", width, len(tc.seq)) - } - if !reflect.DeepEqual(tc.msg, msg) { - t.Errorf("expected event %#v (%T), got %#v (%T)", tc.msg, tc.msg, msg, msg) - } - }) - } -} - -func TestDetectOneMsg(t *testing.T) { - td := buildBaseSeqTests() - // Add tests for the inputs that detectOneMsg() can parse, but - // detectSequence() cannot. td = append(td, + // focus/blur + seqTest{ + []byte{'\x1b', '[', 'I'}, + []Msg{ + FocusMsg{}, + }, + }, + seqTest{ + []byte{'\x1b', '[', 'O'}, + []Msg{ + BlurMsg{}, + }, + }, // Mouse event. seqTest{ []byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)}, - MouseMsg{X: 32, Y: 16, Type: MouseWheelUp, Button: MouseButtonWheelUp, Action: MouseActionPress}, + []Msg{ + MouseWheelMsg{X: 32, Y: 16, Button: MouseWheelUp}, + }, }, // SGR Mouse event. seqTest{ []byte("\x1b[<0;33;17M"), - MouseMsg{X: 32, Y: 16, Type: MouseLeft, Button: MouseButtonLeft, Action: MouseActionPress}, + []Msg{ + MouseClickMsg{X: 32, Y: 16, Button: MouseLeft}, + }, }, // Runes. seqTest{ []byte{'a'}, - KeyMsg{Type: KeyRunes, Runes: []rune("a")}, + []Msg{ + KeyPressMsg{Runes: []rune{'a'}}, + }, }, seqTest{ []byte{'\x1b', 'a'}, - KeyMsg{Type: KeyRunes, Runes: []rune("a"), Alt: true}, + []Msg{ + KeyPressMsg{Runes: []rune{'a'}, Mod: ModAlt}, + }, }, seqTest{ []byte{'a', 'a', 'a'}, - KeyMsg{Type: KeyRunes, Runes: []rune("aaa")}, + []Msg{ + KeyPressMsg{Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'a'}}, + }, }, // Multi-byte rune. seqTest{ []byte("☃"), - KeyMsg{Type: KeyRunes, Runes: []rune("☃")}, + []Msg{ + KeyPressMsg{Runes: []rune{'☃'}}, + }, }, seqTest{ []byte("\x1b☃"), - KeyMsg{Type: KeyRunes, Runes: []rune("☃"), Alt: true}, + []Msg{ + KeyPressMsg{Runes: []rune{'☃'}, Mod: ModAlt}, + }, }, // Standalone control chacters. seqTest{ []byte{'\x1b'}, - KeyMsg{Type: KeyEscape}, + []Msg{ + KeyPressMsg{Sym: KeyEscape}, + }, }, seqTest{ - []byte{byte(keySOH)}, - KeyMsg{Type: KeyCtrlA}, + []byte{ansi.SOH}, + []Msg{ + KeyPressMsg{Runes: []rune{'a'}, Mod: ModCtrl}, + }, }, seqTest{ - []byte{'\x1b', byte(keySOH)}, - KeyMsg{Type: KeyCtrlA, Alt: true}, + []byte{'\x1b', ansi.SOH}, + []Msg{ + KeyPressMsg{Runes: []rune{'a'}, Mod: ModCtrl | ModAlt}, + }, }, seqTest{ - []byte{byte(keyNUL)}, - KeyMsg{Type: KeyCtrlAt}, + []byte{ansi.NUL}, + []Msg{ + KeyPressMsg{Runes: []rune{' '}, Sym: KeySpace, Mod: ModCtrl}, + }, }, seqTest{ - []byte{'\x1b', byte(keyNUL)}, - KeyMsg{Type: KeyCtrlAt, Alt: true}, + []byte{'\x1b', ansi.NUL}, + []Msg{ + KeyPressMsg{Runes: []rune{' '}, Sym: KeySpace, Mod: ModCtrl | ModAlt}, + }, }, - // Invalid characters. + // C1 control characters. seqTest{ []byte{'\x80'}, - unknownInputByteMsg(0x80), + []Msg{ + KeyPressMsg{Runes: []rune{0x80 - '@'}, Mod: ModCtrl | ModAlt}, + }, }, ) @@ -199,39 +207,53 @@ func TestDetectOneMsg(t *testing.T) { // This is incorrect, but it makes our test fail if we try it out. td = append(td, seqTest{ []byte{'\xfe'}, - unknownInputByteMsg(0xfe), + []Msg{ + UnknownMsg(rune(0xfe)), + }, }) } for _, tc := range td { t.Run(fmt.Sprintf("%q", string(tc.seq)), func(t *testing.T) { - width, msg := detectOneMsg(tc.seq, false /* canHaveMoreData */) - if width != len(tc.seq) { - t.Errorf("parser did not consume the entire input: got %d, expected %d", width, len(tc.seq)) + var events []Msg + buf := tc.seq + for len(buf) > 0 { + width, msg := parseSequence(buf) + events = append(events, msg) + buf = buf[width:] } - if !reflect.DeepEqual(tc.msg, msg) { - t.Errorf("expected event %#v (%T), got %#v (%T)", tc.msg, tc.msg, msg, msg) + if !reflect.DeepEqual(tc.msgs, events) { + t.Errorf("\nexpected event:\n %#v\ngot:\n %#v", tc.msgs, events) } }) } } func TestReadLongInput(t *testing.T) { - input := strings.Repeat("a", 1000) - msgs := testReadInputs(t, bytes.NewReader([]byte(input))) - if len(msgs) != 1 { - t.Errorf("expected 1 messages, got %d", len(msgs)) + expect := make([]Msg, 1000) + for i := 0; i < 1000; i++ { + expect[i] = KeyPressMsg{Runes: []rune{'a'}} } - km := msgs[0] - k := Key(km.(KeyMsg)) - if k.Type != KeyRunes { - t.Errorf("expected key runes, got %d", k.Type) + input := strings.Repeat("a", 1000) + drv, err := newDriver(strings.NewReader(input), "dumb", 0) + if err != nil { + t.Fatalf("unexpected input driver error: %v", err) } - if len(k.Runes) != 1000 || !reflect.DeepEqual(k.Runes, []rune(input)) { - t.Errorf("unexpected runes: %+v", k) + + var msgs []Msg + for { + events, err := drv.ReadEvents() + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("unexpected input error: %v", err) + } + msgs = append(msgs, events...) } - if k.Alt { - t.Errorf("unexpected alt") + + if !reflect.DeepEqual(expect, msgs) { + t.Errorf("unexpected messages, expected:\n %+v\ngot:\n %+v", expect, msgs) } } @@ -242,199 +264,185 @@ func TestReadInput(t *testing.T) { out []Msg } testData := []test{ - {"a", + { + "a", []byte{'a'}, []Msg{ - KeyMsg{ - Type: KeyRunes, - Runes: []rune{'a'}, - }, + KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}}, }, }, - {" ", + { + "space", []byte{' '}, []Msg{ - KeyMsg{ - Type: KeySpace, - Runes: []rune{' '}, - }, + KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}}, }, }, - {"a alt+a", + { + "a alt+a", []byte{'a', '\x1b', 'a'}, []Msg{ - KeyMsg{Type: KeyRunes, Runes: []rune{'a'}}, - KeyMsg{Type: KeyRunes, Runes: []rune{'a'}, Alt: true}, + KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}}, + KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}, Mod: ModAlt}, }, }, - {"a alt+a a", + { + "a alt+a a", []byte{'a', '\x1b', 'a', 'a'}, []Msg{ - KeyMsg{Type: KeyRunes, Runes: []rune{'a'}}, - KeyMsg{Type: KeyRunes, Runes: []rune{'a'}, Alt: true}, - KeyMsg{Type: KeyRunes, Runes: []rune{'a'}}, + KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}}, + KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}, Mod: ModAlt}, + KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}}, }, }, - {"ctrl+a", - []byte{byte(keySOH)}, + { + "ctrl+a", + []byte{byte(ansi.SOH)}, []Msg{ - KeyMsg{ - Type: KeyCtrlA, - }, + KeyPressMsg{Runes: []rune{'a'}, Mod: ModCtrl}, }, }, - {"ctrl+a ctrl+b", - []byte{byte(keySOH), byte(keySTX)}, + { + "ctrl+a ctrl+b", + []byte{byte(ansi.SOH), byte(ansi.STX)}, []Msg{ - KeyMsg{Type: KeyCtrlA}, - KeyMsg{Type: KeyCtrlB}, + KeyPressMsg{Runes: []rune{'a'}, Mod: ModCtrl}, + KeyPressMsg{Runes: []rune{'b'}, Mod: ModCtrl}, }, }, - {"alt+a", + { + "alt+a", []byte{byte(0x1b), 'a'}, []Msg{ - KeyMsg{ - Type: KeyRunes, - Alt: true, - Runes: []rune{'a'}, - }, + KeyPressMsg{Sym: KeyNone, Mod: ModAlt, Runes: []rune{'a'}}, }, }, - {"abcd", + { + "a b c d", []byte{'a', 'b', 'c', 'd'}, []Msg{ - KeyMsg{ - Type: KeyRunes, - Runes: []rune{'a', 'b', 'c', 'd'}, - }, + KeyPressMsg{Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'b'}}, + KeyPressMsg{Runes: []rune{'c'}}, + KeyPressMsg{Runes: []rune{'d'}}, }, }, - {"up", + { + "up", []byte("\x1b[A"), []Msg{ - KeyMsg{ - Type: KeyUp, - }, + KeyPressMsg{Sym: KeyUp}, }, }, - {"wheel up", + { + "wheel up", []byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)}, []Msg{ - MouseMsg{ - X: 32, - Y: 16, - Type: MouseWheelUp, - Button: MouseButtonWheelUp, - Action: MouseActionPress, - }, + MouseWheelMsg{X: 32, Y: 16, Button: MouseWheelUp}, }, }, - {"left motion release", + { + "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), }, []Msg{ - MouseMsg(MouseEvent{ - X: 32, - Y: 16, - Type: MouseLeft, - Button: MouseButtonLeft, - Action: MouseActionMotion, - }), - MouseMsg(MouseEvent{ - X: 64, - Y: 32, - Type: MouseRelease, - Button: MouseButtonNone, - Action: MouseActionRelease, - }), + MouseMotionMsg{X: 32, Y: 16, Button: MouseLeft}, + MouseReleaseMsg{X: 64, Y: 32, Button: MouseNone}, }, }, - {"shift+tab", + { + "shift+tab", []byte{'\x1b', '[', 'Z'}, []Msg{ - KeyMsg{ - Type: KeyShiftTab, - }, + KeyPressMsg{Sym: KeyTab, Mod: ModShift}, }, }, - {"enter", + { + "enter", []byte{'\r'}, - []Msg{KeyMsg{Type: KeyEnter}}, + []Msg{KeyPressMsg{Sym: KeyEnter}}, }, - {"alt+enter", + { + "alt+enter", []byte{'\x1b', '\r'}, []Msg{ - KeyMsg{ - Type: KeyEnter, - Alt: true, - }, + KeyPressMsg{Sym: KeyEnter, Mod: ModAlt}, }, }, - {"insert", + { + "insert", []byte{'\x1b', '[', '2', '~'}, []Msg{ - KeyMsg{ - Type: KeyInsert, - }, + KeyPressMsg{Sym: KeyInsert}, }, }, - {"alt+ctrl+a", - []byte{'\x1b', byte(keySOH)}, + { + "ctrl+alt+a", + []byte{'\x1b', byte(ansi.SOH)}, []Msg{ - KeyMsg{ - Type: KeyCtrlA, - Alt: true, - }, + KeyPressMsg{Runes: []rune{'a'}, Mod: ModCtrl | ModAlt}, }, }, - {"?CSI[45 45 45 45 88]?", + { + "CSI?----X?", []byte{'\x1b', '[', '-', '-', '-', '-', 'X'}, - []Msg{unknownCSISequenceMsg([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'})}, + []Msg{UnknownMsg([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'})}, }, // Powershell sequences. - {"up", + { + "up", []byte{'\x1b', 'O', 'A'}, - []Msg{KeyMsg{Type: KeyUp}}, + []Msg{KeyPressMsg{Sym: KeyUp}}, }, - {"down", + { + "down", []byte{'\x1b', 'O', 'B'}, - []Msg{KeyMsg{Type: KeyDown}}, + []Msg{KeyPressMsg{Sym: KeyDown}}, }, - {"right", + { + "right", []byte{'\x1b', 'O', 'C'}, - []Msg{KeyMsg{Type: KeyRight}}, + []Msg{KeyPressMsg{Sym: KeyRight}}, }, - {"left", + { + "left", []byte{'\x1b', 'O', 'D'}, - []Msg{KeyMsg{Type: KeyLeft}}, + []Msg{KeyPressMsg{Sym: KeyLeft}}, }, - {"alt+enter", + { + "alt+enter", []byte{'\x1b', '\x0d'}, - []Msg{KeyMsg{Type: KeyEnter, Alt: true}}, + []Msg{KeyPressMsg{Sym: KeyEnter, Mod: ModAlt}}, }, - {"alt+backspace", + { + "alt+backspace", []byte{'\x1b', '\x7f'}, - []Msg{KeyMsg{Type: KeyBackspace, Alt: true}}, + []Msg{KeyPressMsg{Sym: KeyBackspace, Mod: ModAlt}}, }, - {"ctrl+@", + { + "ctrl+space", []byte{'\x00'}, - []Msg{KeyMsg{Type: KeyCtrlAt}}, + []Msg{KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}, Mod: ModCtrl}}, }, - {"alt+ctrl+@", + { + "ctrl+alt+space", []byte{'\x1b', '\x00'}, - []Msg{KeyMsg{Type: KeyCtrlAt, Alt: true}}, + []Msg{KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}, Mod: ModCtrl | ModAlt}}, }, - {"esc", + { + "esc", []byte{'\x1b'}, - []Msg{KeyMsg{Type: KeyEsc}}, + []Msg{KeyPressMsg{Sym: KeyEscape}}, }, - {"alt+esc", + { + "alt+esc", []byte{'\x1b', '\x1b'}, - []Msg{KeyMsg{Type: KeyEsc, Alt: true}}, + []Msg{KeyPressMsg{Sym: KeyEscape, Mod: ModAlt}}, }, - {"[a b] o", + { + "a b o", []byte{ '\x1b', '[', '2', '0', '0', '~', 'a', ' ', 'b', @@ -442,38 +450,42 @@ func TestReadInput(t *testing.T) { 'o', }, []Msg{ - KeyMsg{Type: KeyRunes, Runes: []rune("a b"), Paste: true}, - KeyMsg{Type: KeyRunes, Runes: []rune("o")}, + PasteStartMsg{}, + PasteMsg("a b"), + PasteEndMsg{}, + KeyPressMsg{Sym: KeyNone, Runes: []rune{'o'}}, }, }, - {"[a\x03\nb]", + { + "a\x03\nb", []byte{ '\x1b', '[', '2', '0', '0', '~', 'a', '\x03', '\n', 'b', - '\x1b', '[', '2', '0', '1', '~'}, + '\x1b', '[', '2', '0', '1', '~', + }, []Msg{ - KeyMsg{Type: KeyRunes, Runes: []rune("a\x03\nb"), Paste: true}, + PasteStartMsg{}, + PasteMsg("a\x03\nb"), + PasteEndMsg{}, + }, + }, + { + "?0xfe?", + []byte{'\xfe'}, + []Msg{ + UnknownMsg(rune(0xfe)), + }, + }, + { + "a ?0xfe? b", + []byte{'a', '\xfe', ' ', 'b'}, + []Msg{ + KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}}, + UnknownMsg(rune(0xfe)), + KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}}, + KeyPressMsg{Sym: KeyNone, Runes: []rune{'b'}}, }, }, - } - 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. - testData = append(testData, - test{"?0xfe?", - []byte{'\xfe'}, - []Msg{unknownInputByteMsg(0xfe)}, - }, - test{"a ?0xfe? b", - []byte{'a', '\xfe', ' ', 'b'}, - []Msg{ - KeyMsg{Type: KeyRunes, Runes: []rune{'a'}}, - unknownInputByteMsg(0xfe), - KeyMsg{Type: KeySpace, Runes: []rune{' '}}, - KeyMsg{Type: KeyRunes, Runes: []rune{'b'}}, - }, - }, - ) } for i, td := range testData { @@ -491,13 +503,8 @@ func TestReadInput(t *testing.T) { } } - title := buf.String() - if title != td.keyname { - t.Errorf("expected message titles:\n %s\ngot:\n %s", td.keyname, title) - } - if len(msgs) != len(td.out) { - t.Fatalf("unexpected message list length: got %d, expected %d\n%#v", len(msgs), len(td.out), msgs) + t.Fatalf("unexpected message list length: got %d, expected %d\n got: %#v\n expected: %#v\n", len(msgs), len(td.out), msgs, td.out) } if !reflect.DeepEqual(td.out, msgs) { @@ -521,6 +528,11 @@ func testReadInputs(t *testing.T, input io.Reader) []Msg { } }() + dr, err := newDriver(input, "dumb", 0) + if err != nil { + t.Fatalf("unexpected input driver error: %v", err) + } + // The messages we're consuming. msgsC := make(chan Msg) @@ -528,7 +540,16 @@ func testReadInputs(t *testing.T, input io.Reader) []Msg { wg.Add(1) go func() { defer wg.Done() - inputErr = readAnsiInputs(ctx, msgsC, input) + var events []Msg + events, inputErr = dr.ReadEvents() + out: + for _, ev := range events { + select { + case msgsC <- ev: + case <-ctx.Done(): + break out + } + } msgsC <- nil }() @@ -612,14 +633,14 @@ func genRandomDataWithSeed(s int64, length int) randTest { res.data = append(res.data, '\x1b') } res.data = append(res.data, 1) - res.names = append(res.names, prefix+"ctrl+a") + 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.HasPrefix(s.name, "alt+") { + if strings.Contains(s.name, "alt+") || strings.Contains(s.name, "meta+") { esclen = 0 prefix = "" alt = 0 @@ -628,58 +649,31 @@ func genRandomDataWithSeed(s int64, length int) randTest { res.data = append(res.data, '\x1b') } res.data = append(res.data, s.seq...) - res.names = append(res.names, prefix+s.name) + 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 } -// TestDetectRandomSequencesLex checks that the lex-generated sequence -// detector works over concatenations of random sequences. -func TestDetectRandomSequencesLex(t *testing.T) { - runTestDetectSequence(t, detectSequence) -} - -func runTestDetectSequence( - t *testing.T, detectSequence func(input []byte) (hasSeq bool, width int, msg Msg), -) { - for i := 0; i < 10; i++ { - t.Run("", func(t *testing.T) { - td := genRandomData(func(s int64) { t.Logf("using random seed: %d", s) }, 1000) - - t.Logf("%#v", td) - - // tn is the event number in td. - // i is the cursor in the input data. - // w is the length of the last sequence detected. - for tn, i, w := 0, 0, 0; i < len(td.data); tn, i = tn+1, i+w { - hasSequence, width, msg := detectSequence(td.data[i:]) - if !hasSequence { - t.Fatalf("at %d (ev %d): failed to find sequence", i, tn) - } - if width != td.lengths[tn] { - t.Errorf("at %d (ev %d): expected width %d, got %d", i, tn, td.lengths[tn], width) - } - w = width - - s, ok := msg.(fmt.Stringer) - if !ok { - t.Errorf("at %d (ev %d): expected stringer event, got %T", i, tn, msg) - } else { - if td.names[tn] != s.String() { - t.Errorf("at %d (ev %d): expected event %q, got %q", i, tn, td.names[tn], s.String()) - } - } - } - }) +func FuzzParseSequence(f *testing.F) { + for seq := range sequences { + f.Add(seq) } -} - -// TestDetectRandomSequencesMap checks that the map-based sequence -// detector works over concatenations of random sequences. -func TestDetectRandomSequencesMap(t *testing.T) { - runTestDetectSequence(t, detectSequence) + 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, _ := parseSequence([]byte(seq)) + if n == 0 && seq != "" { + t.Errorf("expected a non-zero width for %q", seq) + } + }) } // BenchmarkDetectSequenceMap benchmarks the map-based sequence @@ -688,7 +682,7 @@ func BenchmarkDetectSequenceMap(b *testing.B) { td := genRandomDataWithSeed(123, 10000) for i := 0; i < b.N; i++ { for j, w := 0, 0; j < len(td.data); j += w { - _, w, _ = detectSequence(td.data[j:]) + w, _ = parseSequence(td.data[j:]) } } } diff --git a/key_windows.go b/key_windows.go deleted file mode 100644 index b693efd655..0000000000 --- a/key_windows.go +++ /dev/null @@ -1,351 +0,0 @@ -//go:build windows -// +build windows - -package tea - -import ( - "context" - "fmt" - "io" - - "github.com/erikgeiser/coninput" - localereader "github.com/mattn/go-localereader" - "golang.org/x/sys/windows" -) - -func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { - if coninReader, ok := input.(*conInputReader); ok { - return readConInputs(ctx, msgs, coninReader.conin) - } - - return readAnsiInputs(ctx, msgs, localereader.NewReader(input)) -} - -func readConInputs(ctx context.Context, msgsch chan<- Msg, con windows.Handle) error { - var ps coninput.ButtonState // keep track of previous mouse state - var ws coninput.WindowBufferSizeEventRecord // keep track of the last window size event - for { - events, err := coninput.ReadNConsoleInputs(con, 16) - if err != nil { - return fmt.Errorf("read coninput events: %w", err) - } - - for _, event := range events { - var msgs []Msg - switch e := event.Unwrap().(type) { - case coninput.KeyEventRecord: - if !e.KeyDown || e.VirtualKeyCode == coninput.VK_SHIFT { - continue - } - - for i := 0; i < int(e.RepeatCount); i++ { - eventKeyType := keyType(e) - var runes []rune - - // Add the character only if the key type is an actual character and not a control sequence. - // This mimics the behavior in readAnsiInputs where the character is also removed. - // We don't need to handle KeySpace here. See the comment in keyType(). - if eventKeyType == KeyRunes { - runes = []rune{e.Char} - } - - msgs = append(msgs, KeyMsg{ - Type: eventKeyType, - Runes: runes, - Alt: e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED), - }) - } - case coninput.WindowBufferSizeEventRecord: - if e != ws { - ws = e - msgs = append(msgs, WindowSizeMsg{ - Width: int(e.Size.X), - Height: int(e.Size.Y), - }) - } - case coninput.MouseEventRecord: - event := mouseEvent(ps, e) - if event.Type != MouseUnknown { - msgs = append(msgs, event) - } - ps = e.ButtonState - case coninput.FocusEventRecord, coninput.MenuEventRecord: - // ignore - default: // unknown event - continue - } - - // Send all messages to the channel - for _, msg := range msgs { - select { - case msgsch <- msg: - case <-ctx.Done(): - err := ctx.Err() - if err != nil { - return fmt.Errorf("coninput context error: %w", err) - } - return err - } - } - } - } -} - -func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, action MouseAction) { - btn := p ^ s - action = MouseActionPress - if btn&s == 0 { - action = MouseActionRelease - } - - if btn == 0 { - switch { - case s&coninput.FROM_LEFT_1ST_BUTTON_PRESSED > 0: - button = MouseButtonLeft - case s&coninput.FROM_LEFT_2ND_BUTTON_PRESSED > 0: - button = MouseButtonMiddle - case s&coninput.RIGHTMOST_BUTTON_PRESSED > 0: - button = MouseButtonRight - case s&coninput.FROM_LEFT_3RD_BUTTON_PRESSED > 0: - button = MouseButtonBackward - case s&coninput.FROM_LEFT_4TH_BUTTON_PRESSED > 0: - button = MouseButtonForward - } - return - } - - switch { - case btn == coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button - button = MouseButtonLeft - case btn == coninput.RIGHTMOST_BUTTON_PRESSED: // right button - button = MouseButtonRight - case btn == coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button - button = MouseButtonMiddle - case btn == coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward) - button = MouseButtonBackward - case btn == coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward) - button = MouseButtonForward - } - - return button, action -} - -func mouseEvent(p coninput.ButtonState, e coninput.MouseEventRecord) MouseMsg { - ev := MouseMsg{ - X: int(e.MousePositon.X), - Y: int(e.MousePositon.Y), - Alt: e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED), - Ctrl: e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED), - Shift: e.ControlKeyState.Contains(coninput.SHIFT_PRESSED), - } - switch e.EventFlags { - case coninput.CLICK, coninput.DOUBLE_CLICK: - ev.Button, ev.Action = mouseEventButton(p, e.ButtonState) - if ev.Action == MouseActionRelease { - ev.Type = MouseRelease - } - switch ev.Button { - case MouseButtonLeft: - ev.Type = MouseLeft - case MouseButtonMiddle: - ev.Type = MouseMiddle - case MouseButtonRight: - ev.Type = MouseRight - case MouseButtonBackward: - ev.Type = MouseBackward - case MouseButtonForward: - ev.Type = MouseForward - } - case coninput.MOUSE_WHEELED: - if e.WheelDirection > 0 { - ev.Button = MouseButtonWheelUp - ev.Type = MouseWheelUp - } else { - ev.Button = MouseButtonWheelDown - ev.Type = MouseWheelDown - } - case coninput.MOUSE_HWHEELED: - if e.WheelDirection > 0 { - ev.Button = MouseButtonWheelRight - ev.Type = MouseWheelRight - } else { - ev.Button = MouseButtonWheelLeft - ev.Type = MouseWheelLeft - } - case coninput.MOUSE_MOVED: - ev.Button, _ = mouseEventButton(p, e.ButtonState) - ev.Action = MouseActionMotion - ev.Type = MouseMotion - } - - return ev -} - -func keyType(e coninput.KeyEventRecord) KeyType { - code := e.VirtualKeyCode - - shiftPressed := e.ControlKeyState.Contains(coninput.SHIFT_PRESSED) - ctrlPressed := e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED) - - switch code { - case coninput.VK_RETURN: - return KeyEnter - case coninput.VK_BACK: - return KeyBackspace - case coninput.VK_TAB: - if shiftPressed { - return KeyShiftTab - } - return KeyTab - case coninput.VK_SPACE: - return KeyRunes // this could be KeySpace but on unix space also produces KeyRunes - case coninput.VK_ESCAPE: - return KeyEscape - case coninput.VK_UP: - switch { - case shiftPressed && ctrlPressed: - return KeyCtrlShiftUp - case shiftPressed: - return KeyShiftUp - case ctrlPressed: - return KeyCtrlUp - default: - return KeyUp - } - case coninput.VK_DOWN: - switch { - case shiftPressed && ctrlPressed: - return KeyCtrlShiftDown - case shiftPressed: - return KeyShiftDown - case ctrlPressed: - return KeyCtrlDown - default: - return KeyDown - } - case coninput.VK_RIGHT: - switch { - case shiftPressed && ctrlPressed: - return KeyCtrlShiftRight - case shiftPressed: - return KeyShiftRight - case ctrlPressed: - return KeyCtrlRight - default: - return KeyRight - } - case coninput.VK_LEFT: - switch { - case shiftPressed && ctrlPressed: - return KeyCtrlShiftLeft - case shiftPressed: - return KeyShiftLeft - case ctrlPressed: - return KeyCtrlLeft - default: - return KeyLeft - } - case coninput.VK_HOME: - switch { - case shiftPressed && ctrlPressed: - return KeyCtrlShiftHome - case shiftPressed: - return KeyShiftHome - case ctrlPressed: - return KeyCtrlHome - default: - return KeyHome - } - case coninput.VK_END: - switch { - case shiftPressed && ctrlPressed: - return KeyCtrlShiftEnd - case shiftPressed: - return KeyShiftEnd - case ctrlPressed: - return KeyCtrlEnd - default: - return KeyEnd - } - case coninput.VK_PRIOR: - return KeyPgUp - case coninput.VK_NEXT: - return KeyPgDown - case coninput.VK_DELETE: - return KeyDelete - default: - if e.ControlKeyState&(coninput.LEFT_CTRL_PRESSED|coninput.RIGHT_CTRL_PRESSED) == 0 { - return KeyRunes - } - - switch e.Char { - case '@': - return KeyCtrlAt - case '\x01': - return KeyCtrlA - case '\x02': - return KeyCtrlB - case '\x03': - return KeyCtrlC - case '\x04': - return KeyCtrlD - case '\x05': - return KeyCtrlE - case '\x06': - return KeyCtrlF - case '\a': - return KeyCtrlG - case '\b': - return KeyCtrlH - case '\t': - return KeyCtrlI - case '\n': - return KeyCtrlJ - case '\v': - return KeyCtrlK - case '\f': - return KeyCtrlL - case '\r': - return KeyCtrlM - case '\x0e': - return KeyCtrlN - case '\x0f': - return KeyCtrlO - case '\x10': - return KeyCtrlP - case '\x11': - return KeyCtrlQ - case '\x12': - return KeyCtrlR - case '\x13': - return KeyCtrlS - case '\x14': - return KeyCtrlT - case '\x15': - return KeyCtrlU - case '\x16': - return KeyCtrlV - case '\x17': - return KeyCtrlW - case '\x18': - return KeyCtrlX - case '\x19': - return KeyCtrlY - case '\x1a': - return KeyCtrlZ - case '\x1b': - return KeyCtrlCloseBracket - case '\x1c': - return KeyCtrlBackslash - case '\x1f': - return KeyCtrlUnderscore - } - - switch code { - case coninput.VK_OEM_4: - return KeyCtrlOpenBracket - } - - return KeyRunes - } -} diff --git a/kitty.go b/kitty.go new file mode 100644 index 0000000000..5365e29518 --- /dev/null +++ b/kitty.go @@ -0,0 +1,281 @@ +package tea + +import ( + "unicode" + "unicode/utf8" + + "github.com/charmbracelet/x/ansi" +) + +// KittyKeyboardMsg represents Kitty keyboard progressive enhancement flags message. +type KittyKeyboardMsg int + +// IsDisambiguateEscapeCodes returns true if the DisambiguateEscapeCodes flag is set. +func (e KittyKeyboardMsg) IsDisambiguateEscapeCodes() bool { + return e&ansi.KittyDisambiguateEscapeCodes != 0 +} + +// IsReportEventTypes returns true if the ReportEventTypes flag is set. +func (e KittyKeyboardMsg) IsReportEventTypes() bool { + return e&ansi.KittyReportEventTypes != 0 +} + +// IsReportAlternateKeys returns true if the ReportAlternateKeys flag is set. +func (e KittyKeyboardMsg) IsReportAlternateKeys() bool { + return e&ansi.KittyReportAlternateKeys != 0 +} + +// IsReportAllKeys returns true if the ReportAllKeys flag is set. +func (e KittyKeyboardMsg) IsReportAllKeys() bool { + return e&ansi.KittyReportAllKeys != 0 +} + +// IsReportAssociatedKeys returns true if the ReportAssociatedKeys flag is set. +func (e KittyKeyboardMsg) IsReportAssociatedKeys() bool { + return e&ansi.KittyReportAssociatedKeys != 0 +} + +// Kitty Clipboard Control Sequences +var kittyKeyMap = map[int]KeySym{ + ansi.BS: KeyBackspace, + ansi.HT: KeyTab, + ansi.CR: KeyEnter, + ansi.ESC: KeyEscape, + ansi.DEL: KeyBackspace, + + 57344: KeyEscape, + 57345: KeyEnter, + 57346: KeyTab, + 57347: KeyBackspace, + 57348: KeyInsert, + 57349: KeyDelete, + 57350: KeyLeft, + 57351: KeyRight, + 57352: KeyUp, + 57353: KeyDown, + 57354: KeyPgUp, + 57355: KeyPgDown, + 57356: KeyHome, + 57357: KeyEnd, + 57358: KeyCapsLock, + 57359: KeyScrollLock, + 57360: KeyNumLock, + 57361: KeyPrintScreen, + 57362: KeyPause, + 57363: KeyMenu, + 57364: KeyF1, + 57365: KeyF2, + 57366: KeyF3, + 57367: KeyF4, + 57368: KeyF5, + 57369: KeyF6, + 57370: KeyF7, + 57371: KeyF8, + 57372: KeyF9, + 57373: KeyF10, + 57374: KeyF11, + 57375: KeyF12, + 57376: KeyF13, + 57377: KeyF14, + 57378: KeyF15, + 57379: KeyF16, + 57380: KeyF17, + 57381: KeyF18, + 57382: KeyF19, + 57383: KeyF20, + 57384: KeyF21, + 57385: KeyF22, + 57386: KeyF23, + 57387: KeyF24, + 57388: KeyF25, + 57389: KeyF26, + 57390: KeyF27, + 57391: KeyF28, + 57392: KeyF29, + 57393: KeyF30, + 57394: KeyF31, + 57395: KeyF32, + 57396: KeyF33, + 57397: KeyF34, + 57398: KeyF35, + 57399: KeyKp0, + 57400: KeyKp1, + 57401: KeyKp2, + 57402: KeyKp3, + 57403: KeyKp4, + 57404: KeyKp5, + 57405: KeyKp6, + 57406: KeyKp7, + 57407: KeyKp8, + 57408: KeyKp9, + 57409: KeyKpDecimal, + 57410: KeyKpDivide, + 57411: KeyKpMultiply, + 57412: KeyKpMinus, + 57413: KeyKpPlus, + 57414: KeyKpEnter, + 57415: KeyKpEqual, + 57416: KeyKpSep, + 57417: KeyKpLeft, + 57418: KeyKpRight, + 57419: KeyKpUp, + 57420: KeyKpDown, + 57421: KeyKpPgUp, + 57422: KeyKpPgDown, + 57423: KeyKpHome, + 57424: KeyKpEnd, + 57425: KeyKpInsert, + 57426: KeyKpDelete, + 57427: KeyKpBegin, + 57428: KeyMediaPlay, + 57429: KeyMediaPause, + 57430: KeyMediaPlayPause, + 57431: KeyMediaReverse, + 57432: KeyMediaStop, + 57433: KeyMediaFastForward, + 57434: KeyMediaRewind, + 57435: KeyMediaNext, + 57436: KeyMediaPrev, + 57437: KeyMediaRecord, + 57438: KeyLowerVol, + 57439: KeyRaiseVol, + 57440: KeyMute, + 57441: KeyLeftShift, + 57442: KeyLeftCtrl, + 57443: KeyLeftAlt, + 57444: KeyLeftSuper, + 57445: KeyLeftHyper, + 57446: KeyLeftMeta, + 57447: KeyRightShift, + 57448: KeyRightCtrl, + 57449: KeyRightAlt, + 57450: KeyRightSuper, + 57451: KeyRightHyper, + 57452: KeyRightMeta, + 57453: KeyIsoLevel3Shift, + 57454: KeyIsoLevel5Shift, +} + +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(csi *ansi.CsiSequence) Msg { + var isRelease bool + key := Key{} + + if params := csi.Subparams(0); len(params) > 0 { + code := params[0] + if sym, ok := kittyKeyMap[code]; ok { + key.Sym = sym + } else { + r := rune(code) + if !utf8.ValidRune(r) { + r = utf8.RuneError + } + + key.Runes = []rune{r} + + // alternate key reporting + switch len(params) { + case 3: + // shifted key + base key + if b := rune(params[2]); 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.baseRune = b + } + fallthrough + case 2: + // shifted key + if s := rune(params[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.altRune = key.Rune() + key.Runes = []rune{s} + } + } + } + } + if params := csi.Subparams(1); len(params) > 0 { + mod := params[0] + if mod > 1 { + key.Mod = fromKittyMod(mod - 1) + } + if len(params) > 1 { + switch params[1] { + case 2: + key.IsRepeat = true + case 3: + isRelease = true + } + } + } + // TODO: Associated keys are not support yet. + // if params := csi.Subparams(2); len(params) > 0 { + // r := rune(params[0]) + // if unicode.IsPrint(r) { + // key.AltRune = r + // } + // } + if isRelease { + return KeyReleaseMsg(key) + } + return KeyPressMsg(key) +} diff --git a/mod.go b/mod.go new file mode 100644 index 0000000000..a93ec72df4 --- /dev/null +++ b/mod.go @@ -0,0 +1,70 @@ +package tea + +// KeyMod represents modifier keys. +type KeyMod uint16 + +// 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 +) + +// HasShift reports whether the Shift modifier is set. +func (m KeyMod) HasShift() bool { + return m&ModShift != 0 +} + +// HasAlt reports whether the Alt modifier is set. +func (m KeyMod) HasAlt() bool { + return m&ModAlt != 0 +} + +// HasCtrl reports whether the Ctrl modifier is set. +func (m KeyMod) HasCtrl() bool { + return m&ModCtrl != 0 +} + +// HasMeta reports whether the Meta modifier is set. +func (m KeyMod) HasMeta() bool { + return m&ModMeta != 0 +} + +// HasHyper reports whether the Hyper modifier is set. +func (m KeyMod) HasHyper() bool { + return m&ModHyper != 0 +} + +// HasSuper reports whether the Super modifier is set. +func (m KeyMod) HasSuper() bool { + return m&ModSuper != 0 +} + +// HasCapsLock reports whether the CapsLock key is enabled. +func (m KeyMod) HasCapsLock() bool { + return m&ModCapsLock != 0 +} + +// HasNumLock reports whether the NumLock key is enabled. +func (m KeyMod) HasNumLock() bool { + return m&ModNumLock != 0 +} + +// HasScrollLock reports whether the ScrollLock key is enabled. +func (m KeyMod) HasScrollLock() bool { + return m&ModScrollLock != 0 +} diff --git a/mode.go b/mode.go new file mode 100644 index 0000000000..bee0e3b3ef --- /dev/null +++ b/mode.go @@ -0,0 +1,12 @@ +package tea + +// ReportModeEvent represents a report mode event for sequence DECRPM. +// +// See: https://vt100.net/docs/vt510-rm/DECRPM.html +type ReportModeEvent struct { + // Mode is the mode number. + Mode int + + // Value is the mode value. + Value int +} diff --git a/mouse.go b/mouse.go index 6ec51cc0c0..710770d2bb 100644 --- a/mouse.go +++ b/mouse.go @@ -1,89 +1,10 @@ package tea -import "strconv" - -// MouseMsg contains information about a mouse event and are sent to a programs -// update function when mouse activity occurs. Note that the mouse must first -// be enabled in order for the mouse events to be received. -type MouseMsg MouseEvent - -// String returns a string representation of a mouse event. -func (m MouseMsg) String() string { - return MouseEvent(m).String() -} - -// MouseEvent represents a mouse event, which could be a click, a scroll wheel -// movement, a cursor movement, or a combination. -type MouseEvent struct { - X int - Y int - Shift bool - Alt bool - Ctrl bool - Action MouseAction - Button MouseButton - - // Deprecated: Use MouseAction & MouseButton instead. - Type MouseEventType -} - -// IsWheel returns true if the mouse event is a wheel event. -func (m MouseEvent) IsWheel() bool { - return m.Button == MouseButtonWheelUp || m.Button == MouseButtonWheelDown || - m.Button == MouseButtonWheelLeft || m.Button == MouseButtonWheelRight -} - -// String returns a string representation of a mouse event. -func (m MouseEvent) String() (s string) { - if m.Ctrl { - s += "ctrl+" - } - if m.Alt { - s += "alt+" - } - if m.Shift { - s += "shift+" - } - - if m.Button == MouseButtonNone { //nolint:nestif - if m.Action == MouseActionMotion || m.Action == MouseActionRelease { - s += mouseActions[m.Action] - } else { - s += "unknown" - } - } else if m.IsWheel() { - s += mouseButtons[m.Button] - } else { - btn := mouseButtons[m.Button] - if btn != "" { - s += btn - } - act := mouseActions[m.Action] - if act != "" { - s += " " + act - } - } - - return s -} - -// MouseAction represents the action that occurred during a mouse event. -type MouseAction int - -// Mouse event actions. -const ( - MouseActionPress MouseAction = iota - MouseActionRelease - MouseActionMotion +import ( + "github.com/charmbracelet/x/ansi" ) -var mouseActions = map[MouseAction]string{ - MouseActionPress: "press", - MouseActionRelease: "release", - MouseActionMotion: "motion", -} - -// MouseButton represents the button that was pressed during a mouse event. +// MouseButton represents the button that was pressed during a mouse message. type MouseButton int // Mouse event buttons @@ -104,58 +25,100 @@ type MouseButton int // // Other buttons are not supported. const ( - MouseButtonNone MouseButton = iota - MouseButtonLeft - MouseButtonMiddle - MouseButtonRight - MouseButtonWheelUp - MouseButtonWheelDown - MouseButtonWheelLeft - MouseButtonWheelRight - MouseButtonBackward - MouseButtonForward - MouseButton10 - MouseButton11 -) - -var mouseButtons = map[MouseButton]string{ - MouseButtonNone: "none", - MouseButtonLeft: "left", - MouseButtonMiddle: "middle", - MouseButtonRight: "right", - MouseButtonWheelUp: "wheel up", - MouseButtonWheelDown: "wheel down", - MouseButtonWheelLeft: "wheel left", - MouseButtonWheelRight: "wheel right", - MouseButtonBackward: "backward", - MouseButtonForward: "forward", - MouseButton10: "button 10", - MouseButton11: "button 11", -} - -// MouseEventType indicates the type of mouse event occurring. -// -// Deprecated: Use MouseAction & MouseButton instead. -type MouseEventType int - -// Mouse event types. -// -// Deprecated: Use MouseAction & MouseButton instead. -const ( - MouseUnknown MouseEventType = iota + MouseNone MouseButton = iota MouseLeft - MouseRight MouseMiddle - MouseRelease // mouse button release (X10 only) + MouseRight MouseWheelUp MouseWheelDown MouseWheelLeft MouseWheelRight MouseBackward MouseForward - MouseMotion + MouseExtra1 + MouseExtra2 ) +var mouseButtons = map[MouseButton]string{ + MouseNone: "none", + MouseLeft: "left", + MouseMiddle: "middle", + MouseRight: "right", + MouseWheelUp: "wheelup", + MouseWheelDown: "wheeldown", + MouseWheelLeft: "wheelleft", + MouseWheelRight: "wheelright", + MouseBackward: "backward", + MouseForward: "forward", + MouseExtra1: "button10", + MouseExtra2: "button11", +} + +// Mouse represents a mouse message. +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.HasCtrl() { + s += "ctrl+" + } + if m.Mod.HasAlt() { + s += "alt+" + } + if m.Mod.HasShift() { + s += "shift+" + } + + str, ok := mouseButtons[m.Button] + if !ok { + s += "unknown" + } else if str != "none" { // motion events don't have a button + s += str + } + + return s +} + +// MouseClickMsg represents a mouse button click message. +type MouseClickMsg Mouse + +// String returns a string representation of the mouse click message. +func (e MouseClickMsg) String() string { + return Mouse(e).String() +} + +// MouseReleaseMsg represents a mouse button release message. +type MouseReleaseMsg Mouse + +// String returns a string representation of the mouse release message. +func (e MouseReleaseMsg) String() string { + return Mouse(e).String() +} + +// MouseWheelMsg represents a mouse wheel message event. +type MouseWheelMsg Mouse + +// String returns a string representation of the mouse wheel message. +func (e MouseWheelMsg) String() string { + return Mouse(e).String() +} + +// MouseMotionMsg represents a mouse motion message. +type MouseMotionMsg Mouse + +// String returns a string representation of the mouse motion message. +func (e MouseMotionMsg) String() string { + m := Mouse(e) + if m.Button != 0 { + return m.String() + "+motion" + } + return m.String() + "motion" +} + // Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events // look like: // @@ -169,35 +132,28 @@ const ( // M is for button press, m is for button release // // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates -func parseSGRMouseEvent(buf []byte) MouseEvent { - str := string(buf[3:]) - matches := mouseSGRRegex.FindStringSubmatch(str) - if len(matches) != 5 { //nolint:gomnd - // Unreachable, we already checked the regex in `detectOneMsg`. - panic("invalid mouse event") - } +func parseSGRMouseEvent(csi *ansi.CsiSequence) Msg { + x := csi.Param(1) + y := csi.Param(2) + release := csi.Command() == 'm' + mod, btn, _, isMotion := parseMouseButton(csi.Param(0)) + + // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). + x-- + y-- - b, _ := strconv.Atoi(matches[1]) - px := matches[2] - py := matches[3] - release := matches[4] == "m" - m := parseMouseButton(b, true) + 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 m.Action != MouseActionMotion && !m.IsWheel() && release { - m.Action = MouseActionRelease - m.Type = MouseRelease + if isWheel(m.Button) { + return MouseWheelMsg(m) + } else if !isMotion && release { + return MouseReleaseMsg(m) + } else if isMotion { + return MouseMotionMsg(m) } - - x, _ := strconv.Atoi(px) - y, _ := strconv.Atoi(py) - - // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). - m.X = x - 1 - m.Y = y - 1 - - return m + return MouseClickMsg(m) } const x10MouseByteOffset = 32 @@ -211,25 +167,34 @@ const x10MouseByteOffset = 32 // ESC [M Cb Cx Cy // // See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking -func parseX10MouseEvent(buf []byte) MouseEvent { +func parseX10MouseEvent(buf []byte) Msg { v := buf[3:6] - m := parseMouseButton(int(v[0]), false) + b := int(v[0]) + if b >= x10MouseByteOffset { + // XXX: b < 32 should be impossible, but we're being defensive. + b -= x10MouseByteOffset + } - // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). - m.X = int(v[1]) - x10MouseByteOffset - 1 - m.Y = int(v[2]) - x10MouseByteOffset - 1 + mod, btn, isRelease, isMotion := parseMouseButton(b) - return m + // (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 MouseWheelMsg(m) + } else if isMotion { + return MouseMotionMsg(m) + } else if isRelease { + return MouseReleaseMsg(m) + } + return MouseClickMsg(m) } // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates -func parseMouseButton(b int, isSGR bool) MouseEvent { - var m MouseEvent - e := b - if !isSGR { - e -= x10MouseByteOffset - } - +func parseMouseButton(b int) (mod KeyMod, btn MouseButton, isRelease bool, isMotion bool) { + // mouse bit shifts const ( bitShift = 0b0000_0100 bitAlt = 0b0000_1000 @@ -241,68 +206,39 @@ func parseMouseButton(b int, isSGR bool) MouseEvent { bitsMask = 0b0000_0011 ) - if e&bitAdd != 0 { - m.Button = MouseButtonBackward + MouseButton(e&bitsMask) - } else if e&bitWheel != 0 { - m.Button = MouseButtonWheelUp + MouseButton(e&bitsMask) + // 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 { - m.Button = MouseButtonLeft + MouseButton(e&bitsMask) + btn = MouseLeft + MouseButton(b&bitsMask) // X10 reports a button release as 0b0000_0011 (3) - if e&bitsMask == bitsMask { - m.Action = MouseActionRelease - m.Button = MouseButtonNone + if b&bitsMask == bitsMask { + btn = MouseNone + isRelease = true } } // Motion bit doesn't get reported for wheel events. - if e&bitMotion != 0 && !m.IsWheel() { - m.Action = MouseActionMotion + if b&bitMotion != 0 && !isWheel(btn) { + isMotion = true } - // Modifiers - m.Alt = e&bitAlt != 0 - m.Ctrl = e&bitCtrl != 0 - m.Shift = e&bitShift != 0 - - // backward compatibility - switch { - case m.Button == MouseButtonLeft && m.Action == MouseActionPress: - m.Type = MouseLeft - case m.Button == MouseButtonMiddle && m.Action == MouseActionPress: - m.Type = MouseMiddle - case m.Button == MouseButtonRight && m.Action == MouseActionPress: - m.Type = MouseRight - case m.Button == MouseButtonNone && m.Action == MouseActionRelease: - m.Type = MouseRelease - case m.Button == MouseButtonWheelUp && m.Action == MouseActionPress: - m.Type = MouseWheelUp - case m.Button == MouseButtonWheelDown && m.Action == MouseActionPress: - m.Type = MouseWheelDown - case m.Button == MouseButtonWheelLeft && m.Action == MouseActionPress: - m.Type = MouseWheelLeft - case m.Button == MouseButtonWheelRight && m.Action == MouseActionPress: - m.Type = MouseWheelRight - case m.Button == MouseButtonBackward && m.Action == MouseActionPress: - m.Type = MouseBackward - case m.Button == MouseButtonForward && m.Action == MouseActionPress: - m.Type = MouseForward - case m.Action == MouseActionMotion: - m.Type = MouseMotion - switch m.Button { //nolint:exhaustive - case MouseButtonLeft: - m.Type = MouseLeft - case MouseButtonMiddle: - m.Type = MouseMiddle - case MouseButtonRight: - m.Type = MouseRight - case MouseButtonBackward: - m.Type = MouseBackward - case MouseButtonForward: - m.Type = MouseForward - } - default: - m.Type = MouseUnknown - } + return +} - return m +// isWheel returns true if the mouse event is a wheel event. +func isWheel(btn MouseButton) bool { + return btn >= MouseWheelUp && btn <= MouseWheelRight } diff --git a/mouse_deprecated.go b/mouse_deprecated.go new file mode 100644 index 0000000000..fb2c41da6c --- /dev/null +++ b/mouse_deprecated.go @@ -0,0 +1,162 @@ +package tea + +// MouseMsg contains information about a mouse event and are sent to a programs +// update function when mouse activity occurs. Note that the mouse must first +// be enabled in order for the mouse events to be received. +// +// Deprecated in favor of MouseClickMsg, MouseReleaseMsg, MouseWheelMsg, and +// MouseMotionMsg. +type MouseMsg struct { + X int + Y int + Shift bool + Alt bool + Ctrl bool + Action MouseAction + Button MouseButton + Type MouseEventType +} + +// MouseEvent represents a mouse event. +// +// Deprecated: Use Mouse. +type MouseEvent = MouseMsg + +// IsWheel returns true if the mouse event is a wheel event. +func (m MouseMsg) IsWheel() bool { + return m.Button == MouseButtonWheelUp || m.Button == MouseButtonWheelDown || + m.Button == MouseButtonWheelLeft || m.Button == MouseButtonWheelRight +} + +// String returns a string representation of a mouse event. +func (m MouseMsg) String() (s string) { + if m.Ctrl { + s += "ctrl+" + } + if m.Alt { + s += "alt+" + } + if m.Shift { + s += "shift+" + } + + if m.Button == MouseButtonNone { //nolint:nestif + if m.Action == MouseActionMotion || m.Action == MouseActionRelease { + s += mouseMsgActions[m.Action] + } else { + s += "unknown" + } + } else if m.IsWheel() { + s += mouseMsgButtons[m.Button] + } else { + btn := mouseMsgButtons[m.Button] + if btn != "" { + s += btn + } + act := mouseMsgActions[m.Action] + if act != "" { + s += " " + act + } + } + + return s +} + +// MouseAction represents the action that occurred during a mouse event. +// +// Deprecated: Use MouseClickMsg, MouseReleaseMsg, MouseWheelMsg, and +// MouseMotionMsg. +type MouseAction int + +// Mouse event actions. +// +// Deprecated in favor of MouseClickMsg, MouseReleaseMsg, MouseWheelMsg, and +// MouseMotionMsg. +const ( + MouseActionPress MouseAction = iota + MouseActionRelease + MouseActionMotion +) + +var mouseMsgActions = map[MouseAction]string{ + MouseActionPress: "press", + MouseActionRelease: "release", + MouseActionMotion: "motion", +} + +// 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. +// +// Deprecated: Use MouseNone, MouseLeft, etc. +const ( + MouseButtonNone = MouseNone + MouseButtonLeft = MouseLeft + MouseButtonMiddle = MouseMiddle + MouseButtonRight = MouseRight + MouseButtonWheelUp = MouseWheelUp + MouseButtonWheelDown = MouseWheelDown + MouseButtonWheelLeft = MouseWheelLeft + MouseButtonWheelRight = MouseWheelRight + MouseButtonBackward = MouseBackward + MouseButtonForward = MouseForward + MouseButton10 = MouseExtra1 + MouseButton11 = MouseExtra2 +) + +// Deprecated: Use mouseButtons. +var mouseMsgButtons = map[MouseButton]string{ + MouseButtonNone: "none", + MouseButtonLeft: "left", + MouseButtonMiddle: "middle", + MouseButtonRight: "right", + MouseButtonWheelUp: "wheel up", + MouseButtonWheelDown: "wheel down", + MouseButtonWheelLeft: "wheel left", + MouseButtonWheelRight: "wheel right", + MouseButtonBackward: "backward", + MouseButtonForward: "forward", + MouseButton10: "button 10", + MouseButton11: "button 11", +} + +// MouseEventType indicates the type of mouse event occurring. +// +// Deprecated: Use MouseButton. +type MouseEventType = MouseButton + +// Mouse event types. +// +// Deprecated in favor of MouseReleaseMsg and MouseMotionMsg. +const ( + MouseUnknown = MouseNone + + MouseRelease MouseEventType = -iota // mouse button release (X10 only) + MouseMotion +) + +// toMouseMsg converts a mouse event to a mouse message. +func toMouseMsg(m Mouse) MouseMsg { + return MouseMsg{ + X: m.X, + Y: m.Y, + Shift: m.Mod.HasShift(), + Alt: m.Mod.HasAlt(), + Ctrl: m.Mod.HasCtrl(), + Button: m.Button, + } +} diff --git a/mouse_test.go b/mouse_test.go index 30f6ee364b..69b2730d1f 100644 --- a/mouse_test.go +++ b/mouse_test.go @@ -3,205 +3,118 @@ package tea 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 MouseEvent + event Msg expected string }{ { - name: "unknown", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonNone, - Type: MouseUnknown, - }, + name: "unknown", + event: MouseClickMsg{Button: MouseButton(0xff)}, expected: "unknown", }, { - name: "left", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonLeft, - Type: MouseLeft, - }, - expected: "left press", - }, - { - name: "right", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonRight, - Type: MouseRight, - }, - expected: "right press", - }, - { - name: "middle", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonMiddle, - Type: MouseMiddle, - }, - expected: "middle press", - }, - { - name: "release", - event: MouseEvent{ - Action: MouseActionRelease, - Button: MouseButtonNone, - Type: MouseRelease, - }, - expected: "release", - }, - { - name: "wheel up", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonWheelUp, - Type: MouseWheelUp, - }, - expected: "wheel up", - }, - { - name: "wheel down", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonWheelDown, - Type: MouseWheelDown, - }, - expected: "wheel down", - }, - { - name: "wheel left", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonWheelLeft, - Type: MouseWheelLeft, - }, - expected: "wheel left", - }, - { - name: "wheel right", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonWheelRight, - Type: MouseWheelRight, - }, - expected: "wheel right", - }, - { - name: "motion", - event: MouseEvent{ - Action: MouseActionMotion, - Button: MouseButtonNone, - Type: MouseMotion, - }, - expected: "motion", + name: "left", + event: MouseClickMsg{Button: MouseLeft}, + expected: "left", }, { - name: "shift+left release", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionRelease, - Button: MouseButtonLeft, - Shift: true, - }, - expected: "shift+left release", - }, - { - name: "shift+left", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - Shift: true, - }, - expected: "shift+left press", - }, - { - name: "ctrl+shift+left", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - Shift: true, - Ctrl: true, - }, - expected: "ctrl+shift+left press", - }, - { - name: "alt+left", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - Alt: true, - }, - expected: "alt+left press", - }, - { - name: "ctrl+left", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - Ctrl: true, - }, - expected: "ctrl+left press", - }, - { - name: "ctrl+alt+left", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - Alt: true, - Ctrl: true, - }, - expected: "ctrl+alt+left press", - }, - { - name: "ctrl+alt+shift+left", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - Alt: true, - Ctrl: true, - Shift: true, - }, - expected: "ctrl+alt+shift+left press", - }, - { - name: "ignore coordinates", - event: MouseEvent{ - X: 100, - Y: 200, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, - expected: "left press", - }, - { - name: "broken type", - event: MouseEvent{ - Type: MouseEventType(-100), - Action: MouseAction(-110), - Button: MouseButton(-120), - }, + name: "right", + event: MouseClickMsg{Button: MouseRight}, + expected: "right", + }, + { + name: "middle", + event: MouseClickMsg{Button: MouseMiddle}, + expected: "middle", + }, + { + name: "release", + event: MouseReleaseMsg{Button: MouseNone}, expected: "", }, + { + name: "wheelup", + event: MouseWheelMsg{Button: MouseWheelUp}, + expected: "wheelup", + }, + { + name: "wheeldown", + event: MouseWheelMsg{Button: MouseWheelDown}, + expected: "wheeldown", + }, + { + name: "wheelleft", + event: MouseWheelMsg{Button: MouseWheelLeft}, + expected: "wheelleft", + }, + { + name: "wheelright", + event: MouseWheelMsg{Button: MouseWheelRight}, + expected: "wheelright", + }, + { + name: "motion", + event: MouseMotionMsg{Button: MouseNone}, + expected: "motion", + }, + { + name: "shift+left", + event: MouseReleaseMsg{Button: MouseLeft, Mod: ModShift}, + expected: "shift+left", + }, + { + name: "shift+left", event: MouseClickMsg{Button: MouseLeft, Mod: ModShift}, + expected: "shift+left", + }, + { + name: "ctrl+shift+left", + event: MouseClickMsg{Button: MouseLeft, Mod: ModCtrl | ModShift}, + expected: "ctrl+shift+left", + }, + { + name: "alt+left", + event: MouseClickMsg{Button: MouseLeft, Mod: ModAlt}, + expected: "alt+left", + }, + { + name: "ctrl+left", + event: MouseClickMsg{Button: MouseLeft, Mod: ModCtrl}, + expected: "ctrl+left", + }, + { + name: "ctrl+alt+left", + event: MouseClickMsg{Button: MouseLeft, Mod: ModAlt | ModCtrl}, + expected: "ctrl+alt+left", + }, + { + name: "ctrl+alt+shift+left", + event: MouseClickMsg{Button: MouseLeft, Mod: ModAlt | ModCtrl | ModShift}, + expected: "ctrl+alt+shift+left", + }, + { + name: "ignore coordinates", + event: MouseClickMsg{X: 100, Y: 200, Button: MouseLeft}, + expected: "left", + }, + { + name: "broken type", + event: MouseClickMsg{Button: MouseButton(120)}, + expected: "unknown", + }, } for i := range tt { tc := tt[i] t.Run(tc.name, func(t *testing.T) { - actual := tc.event.String() + actual := fmt.Sprint(tc.event) if tc.expected != actual { t.Fatalf("expected %q but got %q", @@ -213,7 +126,7 @@ func TestMouseEvent_String(t *testing.T) { } } -func TestParseX10MouseEvent(t *testing.T) { +func TestParseX10MouseDownEvent(t *testing.T) { encode := func(b byte, x, y int) []byte { return []byte{ '\x1b', @@ -228,330 +141,151 @@ func TestParseX10MouseEvent(t *testing.T) { tt := []struct { name string buf []byte - expected MouseEvent + expected Msg }{ // Position. { - name: "zero position", - buf: encode(0b0000_0000, 0, 0), - expected: MouseEvent{ - X: 0, - Y: 0, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, - }, - { - name: "max position", - buf: encode(0b0000_0000, 222, 222), // Because 255 (max int8) - 32 - 1. - expected: MouseEvent{ - X: 222, - Y: 222, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, + name: "zero position", + buf: encode(0b0000_0000, 0, 0), + expected: MouseClickMsg{X: 0, Y: 0, Button: MouseLeft}, + }, + { + name: "max position", + buf: encode(0b0000_0000, 222, 222), // Because 255 (max int8) - 32 - 1. + expected: MouseClickMsg{X: 222, Y: 222, Button: MouseLeft}, }, // Simple. { - name: "left", - buf: encode(0b0000_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, - }, - { - name: "left in motion", - buf: encode(0b0010_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseLeft, - Action: MouseActionMotion, - Button: MouseButtonLeft, - }, - }, - { - name: "middle", - buf: encode(0b0000_0001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMiddle, - Action: MouseActionPress, - Button: MouseButtonMiddle, - }, - }, - { - name: "middle in motion", - buf: encode(0b0010_0001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMiddle, - Action: MouseActionMotion, - Button: MouseButtonMiddle, - }, - }, - { - name: "right", - buf: encode(0b0000_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "right in motion", - buf: encode(0b0010_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRight, - Action: MouseActionMotion, - Button: MouseButtonRight, - }, - }, - { - name: "motion", - buf: encode(0b0010_0011, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMotion, - Action: MouseActionMotion, - Button: MouseButtonNone, - }, - }, - { - name: "wheel up", - buf: encode(0b0100_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelUp, - Action: MouseActionPress, - Button: MouseButtonWheelUp, - }, - }, - { - name: "wheel down", - buf: encode(0b0100_0001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - { - name: "wheel left", - buf: encode(0b0100_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelLeft, - Action: MouseActionPress, - Button: MouseButtonWheelLeft, - }, - }, - { - name: "wheel right", - buf: encode(0b0100_0011, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelRight, - Action: MouseActionPress, - Button: MouseButtonWheelRight, - }, - }, - { - name: "release", - buf: encode(0b0000_0011, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRelease, - Action: MouseActionRelease, - Button: MouseButtonNone, - }, - }, - { - name: "backward", - buf: encode(0b1000_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseBackward, - Action: MouseActionPress, - Button: MouseButtonBackward, - }, - }, - { - name: "forward", - buf: encode(0b1000_0001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseForward, - Action: MouseActionPress, - Button: MouseButtonForward, - }, - }, - { - name: "button 10", - buf: encode(0b1000_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseUnknown, - Action: MouseActionPress, - Button: MouseButton10, - }, - }, - { - name: "button 11", - buf: encode(0b1000_0011, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseUnknown, - Action: MouseActionPress, - Button: MouseButton11, - }, + name: "left", + buf: encode(0b0000_0000, 32, 16), + expected: MouseClickMsg{X: 32, Y: 16, Button: MouseLeft}, + }, + { + name: "left in motion", + buf: encode(0b0010_0000, 32, 16), + expected: MouseMotionMsg{X: 32, Y: 16, Button: MouseLeft}, + }, + { + name: "middle", + buf: encode(0b0000_0001, 32, 16), + expected: MouseClickMsg{X: 32, Y: 16, Button: MouseMiddle}, + }, + { + name: "middle in motion", + buf: encode(0b0010_0001, 32, 16), + expected: MouseMotionMsg{X: 32, Y: 16, Button: MouseMiddle}, + }, + { + name: "right", + buf: encode(0b0000_0010, 32, 16), + expected: MouseClickMsg{X: 32, Y: 16, Button: MouseRight}, + }, + { + name: "right in motion", + buf: encode(0b0010_0010, 32, 16), + expected: MouseMotionMsg{X: 32, Y: 16, Button: MouseRight}, + }, + { + name: "motion", + buf: encode(0b0010_0011, 32, 16), + expected: MouseMotionMsg{X: 32, Y: 16, Button: MouseNone}, + }, + { + name: "wheel up", + buf: encode(0b0100_0000, 32, 16), + expected: MouseWheelMsg{X: 32, Y: 16, Button: MouseWheelUp}, + }, + { + name: "wheel down", + buf: encode(0b0100_0001, 32, 16), + expected: MouseWheelMsg{X: 32, Y: 16, Button: MouseWheelDown}, + }, + { + name: "wheel left", + buf: encode(0b0100_0010, 32, 16), + expected: MouseWheelMsg{X: 32, Y: 16, Button: MouseWheelLeft}, + }, + { + name: "wheel right", + buf: encode(0b0100_0011, 32, 16), + expected: MouseWheelMsg{X: 32, Y: 16, Button: MouseWheelRight}, + }, + { + name: "release", + buf: encode(0b0000_0011, 32, 16), + expected: MouseReleaseMsg{X: 32, Y: 16, Button: MouseNone}, + }, + { + name: "backward", + buf: encode(0b1000_0000, 32, 16), + expected: MouseClickMsg{X: 32, Y: 16, Button: MouseBackward}, + }, + { + name: "forward", + buf: encode(0b1000_0001, 32, 16), + expected: MouseClickMsg{X: 32, Y: 16, Button: MouseForward}, + }, + { + name: "button 10", + buf: encode(0b1000_0010, 32, 16), + expected: MouseClickMsg{X: 32, Y: 16, Button: MouseExtra1}, + }, + { + name: "button 11", + buf: encode(0b1000_0011, 32, 16), + expected: MouseClickMsg{X: 32, Y: 16, Button: MouseExtra2}, }, // Combinations. { - name: "alt+right", - buf: encode(0b0000_1010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "ctrl+right", - buf: encode(0b0001_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Ctrl: true, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "left in motion", - buf: encode(0b0010_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: false, - Type: MouseLeft, - Action: MouseActionMotion, - Button: MouseButtonLeft, - }, - }, - { - name: "alt+right in motion", - buf: encode(0b0010_1010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Type: MouseRight, - Action: MouseActionMotion, - Button: MouseButtonRight, - }, - }, - { - name: "ctrl+right in motion", - buf: encode(0b0011_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Ctrl: true, - Type: MouseRight, - Action: MouseActionMotion, - Button: MouseButtonRight, - }, - }, - { - name: "ctrl+alt+right", - buf: encode(0b0001_1010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Ctrl: true, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "ctrl+wheel up", - buf: encode(0b0101_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Ctrl: true, - Type: MouseWheelUp, - Action: MouseActionPress, - Button: MouseButtonWheelUp, - }, - }, - { - name: "alt+wheel down", - buf: encode(0b0100_1001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - { - name: "ctrl+alt+wheel down", - buf: encode(0b0101_1001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Ctrl: true, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, + name: "alt+right", + buf: encode(0b0000_1010, 32, 16), + expected: MouseClickMsg{X: 32, Y: 16, Mod: ModAlt, Button: MouseRight}, + }, + { + name: "ctrl+right", + buf: encode(0b0001_0010, 32, 16), + expected: MouseClickMsg{X: 32, Y: 16, Mod: ModCtrl, Button: MouseRight}, + }, + { + name: "left in motion", + buf: encode(0b0010_0000, 32, 16), + expected: MouseMotionMsg{X: 32, Y: 16, Button: MouseLeft}, + }, + { + name: "alt+right in motion", + buf: encode(0b0010_1010, 32, 16), + expected: MouseMotionMsg{X: 32, Y: 16, Mod: ModAlt, Button: MouseRight}, + }, + { + name: "ctrl+right in motion", + buf: encode(0b0011_0010, 32, 16), + expected: MouseMotionMsg{X: 32, Y: 16, Mod: ModCtrl, Button: MouseRight}, + }, + { + name: "ctrl+alt+right", + buf: encode(0b0001_1010, 32, 16), + expected: MouseClickMsg{X: 32, Y: 16, Mod: ModAlt | ModCtrl, Button: MouseRight}, + }, + { + name: "ctrl+wheel up", + buf: encode(0b0101_0000, 32, 16), + expected: MouseWheelMsg{X: 32, Y: 16, Mod: ModCtrl, Button: MouseWheelUp}, + }, + { + name: "alt+wheel down", + buf: encode(0b0100_1001, 32, 16), + expected: MouseWheelMsg{X: 32, Y: 16, Mod: ModAlt, Button: MouseWheelDown}, + }, + { + name: "ctrl+alt+wheel down", + buf: encode(0b0101_1001, 32, 16), + expected: MouseWheelMsg{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: MouseEvent{ - X: -6, - Y: -33, - Type: MouseLeft, - Action: MouseActionMotion, - Button: MouseButtonLeft, - }, + name: "overflow position", + buf: encode(0b0010_0000, 250, 223), // Because 255 (max int8) - 32 - 1. + expected: MouseMotionMsg{X: -6, Y: -33, Button: MouseLeft}, }, } @@ -571,355 +305,155 @@ func TestParseX10MouseEvent(t *testing.T) { } } -// func TestParseX10MouseEvent_error(t *testing.T) { -// tt := []struct { -// name string -// buf []byte -// }{ -// { -// name: "empty buf", -// buf: nil, -// }, -// { -// name: "wrong high bit", -// buf: []byte("\x1a[M@A1"), -// }, -// { -// name: "short buf", -// buf: []byte("\x1b[M@A"), -// }, -// { -// name: "long buf", -// buf: []byte("\x1b[M@A11"), -// }, -// } -// -// for i := range tt { -// tc := tt[i] -// -// t.Run(tc.name, func(t *testing.T) { -// _, err := parseX10MouseEvent(tc.buf) -// -// if err == nil { -// t.Fatalf("expected error but got nil") -// } -// }) -// } -// } - func TestParseSGRMouseEvent(t *testing.T) { - encode := func(b, x, y int, r bool) []byte { + encode := func(b, x, y int, r bool) *ansi.CsiSequence { re := 'M' if r { re = 'm' } - return []byte(fmt.Sprintf("\x1b[<%d;%d;%d%c", b, x+1, y+1, re)) + return &ansi.CsiSequence{ + Params: []int{b, x + 1, y + 1}, + Cmd: int(re) | ('<' << parser.MarkerShift), + } } tt := []struct { name string - buf []byte - expected MouseEvent + buf *ansi.CsiSequence + expected Msg }{ // Position. { - name: "zero position", - buf: encode(0, 0, 0, false), - expected: MouseEvent{ - X: 0, - Y: 0, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, - }, - { - name: "225 position", - buf: encode(0, 225, 225, false), - expected: MouseEvent{ - X: 225, - Y: 225, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, + name: "zero position", + buf: encode(0, 0, 0, false), + expected: MouseClickMsg{X: 0, Y: 0, Button: MouseLeft}, + }, + { + name: "225 position", + buf: encode(0, 225, 225, false), + expected: MouseClickMsg{X: 225, Y: 225, Button: MouseLeft}, }, // Simple. { - name: "left", - buf: encode(0, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, - }, - { - name: "left in motion", - buf: encode(32, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseLeft, - Action: MouseActionMotion, - Button: MouseButtonLeft, - }, - }, - { - name: "left release", - buf: encode(0, 32, 16, true), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRelease, - Action: MouseActionRelease, - Button: MouseButtonLeft, - }, - }, - { - name: "middle", - buf: encode(1, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMiddle, - Action: MouseActionPress, - Button: MouseButtonMiddle, - }, - }, - { - name: "middle in motion", - buf: encode(33, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMiddle, - Action: MouseActionMotion, - Button: MouseButtonMiddle, - }, - }, - { - name: "middle release", - buf: encode(1, 32, 16, true), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRelease, - Action: MouseActionRelease, - Button: MouseButtonMiddle, - }, - }, - { - name: "right", - buf: encode(2, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "right release", - buf: encode(2, 32, 16, true), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRelease, - Action: MouseActionRelease, - Button: MouseButtonRight, - }, - }, - { - name: "motion", - buf: encode(35, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMotion, - Action: MouseActionMotion, - Button: MouseButtonNone, - }, - }, - { - name: "wheel up", - buf: encode(64, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelUp, - Action: MouseActionPress, - Button: MouseButtonWheelUp, - }, - }, - { - name: "wheel down", - buf: encode(65, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - { - name: "wheel left", - buf: encode(66, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelLeft, - Action: MouseActionPress, - Button: MouseButtonWheelLeft, - }, - }, - { - name: "wheel right", - buf: encode(67, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelRight, - Action: MouseActionPress, - Button: MouseButtonWheelRight, - }, - }, - { - name: "backward", - buf: encode(128, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseBackward, - Action: MouseActionPress, - Button: MouseButtonBackward, - }, - }, - { - name: "backward in motion", - buf: encode(160, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseBackward, - Action: MouseActionMotion, - Button: MouseButtonBackward, - }, - }, - { - name: "forward", - buf: encode(129, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseForward, - Action: MouseActionPress, - Button: MouseButtonForward, - }, - }, - { - name: "forward in motion", - buf: encode(161, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseForward, - Action: MouseActionMotion, - Button: MouseButtonForward, - }, + name: "left", + buf: encode(0, 32, 16, false), + expected: MouseClickMsg{X: 32, Y: 16, Button: MouseLeft}, + }, + { + name: "left in motion", + buf: encode(32, 32, 16, false), + expected: MouseMotionMsg{X: 32, Y: 16, Button: MouseLeft}, + }, + { + name: "left", + buf: encode(0, 32, 16, true), + expected: MouseReleaseMsg{X: 32, Y: 16, Button: MouseLeft}, + }, + { + name: "middle", + buf: encode(1, 32, 16, false), + expected: MouseClickMsg{X: 32, Y: 16, Button: MouseMiddle}, + }, + { + name: "middle in motion", + buf: encode(33, 32, 16, false), + expected: MouseMotionMsg{X: 32, Y: 16, Button: MouseMiddle}, + }, + { + name: "middle", + buf: encode(1, 32, 16, true), + expected: MouseReleaseMsg{X: 32, Y: 16, Button: MouseMiddle}, + }, + { + name: "right", + buf: encode(2, 32, 16, false), + expected: MouseClickMsg{X: 32, Y: 16, Button: MouseRight}, + }, + { + name: "right", + buf: encode(2, 32, 16, true), + expected: MouseReleaseMsg{X: 32, Y: 16, Button: MouseRight}, + }, + { + name: "motion", + buf: encode(35, 32, 16, false), + expected: MouseMotionMsg{X: 32, Y: 16, Button: MouseNone}, + }, + { + name: "wheel up", + buf: encode(64, 32, 16, false), + expected: MouseWheelMsg{X: 32, Y: 16, Button: MouseWheelUp}, + }, + { + name: "wheel down", + buf: encode(65, 32, 16, false), + expected: MouseWheelMsg{X: 32, Y: 16, Button: MouseWheelDown}, + }, + { + name: "wheel left", + buf: encode(66, 32, 16, false), + expected: MouseWheelMsg{X: 32, Y: 16, Button: MouseWheelLeft}, + }, + { + name: "wheel right", + buf: encode(67, 32, 16, false), + expected: MouseWheelMsg{X: 32, Y: 16, Button: MouseWheelRight}, + }, + { + name: "backward", + buf: encode(128, 32, 16, false), + expected: MouseClickMsg{X: 32, Y: 16, Button: MouseBackward}, + }, + { + name: "backward in motion", + buf: encode(160, 32, 16, false), + expected: MouseMotionMsg{X: 32, Y: 16, Button: MouseBackward}, + }, + { + name: "forward", + buf: encode(129, 32, 16, false), + expected: MouseClickMsg{X: 32, Y: 16, Button: MouseForward}, + }, + { + name: "forward in motion", + buf: encode(161, 32, 16, false), + expected: MouseMotionMsg{X: 32, Y: 16, Button: MouseForward}, }, // Combinations. { - name: "alt+right", - buf: encode(10, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "ctrl+right", - buf: encode(18, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Ctrl: true, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "ctrl+alt+right", - buf: encode(26, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Ctrl: true, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "alt+wheel press", - buf: encode(73, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - { - name: "ctrl+wheel press", - buf: encode(81, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Ctrl: true, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - { - name: "ctrl+alt+wheel press", - buf: encode(89, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Ctrl: true, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - { - name: "ctrl+alt+shift+wheel press", - buf: encode(93, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Shift: true, - Alt: true, - Ctrl: true, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, + name: "alt+right", + buf: encode(10, 32, 16, false), + expected: MouseClickMsg{X: 32, Y: 16, Mod: ModAlt, Button: MouseRight}, + }, + { + name: "ctrl+right", + buf: encode(18, 32, 16, false), + expected: MouseClickMsg{X: 32, Y: 16, Mod: ModCtrl, Button: MouseRight}, + }, + { + name: "ctrl+alt+right", + buf: encode(26, 32, 16, false), + expected: MouseClickMsg{X: 32, Y: 16, Mod: ModAlt | ModCtrl, Button: MouseRight}, + }, + { + name: "alt+wheel", + buf: encode(73, 32, 16, false), + expected: MouseWheelMsg{X: 32, Y: 16, Mod: ModAlt, Button: MouseWheelDown}, + }, + { + name: "ctrl+wheel", + buf: encode(81, 32, 16, false), + expected: MouseWheelMsg{X: 32, Y: 16, Mod: ModCtrl, Button: MouseWheelDown}, + }, + { + name: "ctrl+alt+wheel", + buf: encode(89, 32, 16, false), + expected: MouseWheelMsg{X: 32, Y: 16, Mod: ModAlt | ModCtrl, Button: MouseWheelDown}, + }, + { + name: "ctrl+alt+shift+wheel", + buf: encode(93, 32, 16, false), + expected: MouseWheelMsg{X: 32, Y: 16, Mod: ModAlt | ModShift | ModCtrl, Button: MouseWheelDown}, }, } diff --git a/parse.go b/parse.go new file mode 100644 index 0000000000..407faf491d --- /dev/null +++ b/parse.go @@ -0,0 +1,828 @@ +package tea + +import ( + "encoding/base64" + "strings" + "unicode/utf8" + + "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/ansi/parser" + "github.com/erikgeiser/coninput" +) + +// Flags to control the behavior of the parser. +// TODO: Should these be exported? +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 +) + +var flags int + +// setFlags sets the flags for the parser. +// This will control the behavior of ParseSequence. +// +//nolint:unused +func setFlags(f int) { + flags = f +} + +// 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 parseSequence(buf []byte) (n int, msg Msg) { + if len(buf) == 0 { + return 0, nil + } + + switch b := buf[0]; b { + case ansi.ESC: + if len(buf) == 1 { + // Escape key + return 1, KeyPressMsg{Sym: KeyEscape} + } + + switch b := buf[1]; b { + case 'O': // Esc-prefixed SS3 + return parseSs3(buf) + case 'P': // Esc-prefixed DCS + return parseDcs(buf) + case '[': // Esc-prefixed CSI + return parseCsi(buf) + case ']': // Esc-prefixed OSC + return parseOsc(buf) + case '_': // Esc-prefixed APC + return parseApc(buf) + default: + n, e := parseSequence(buf[1:]) + if k, ok := e.(KeyPressMsg); ok && !k.Mod.HasAlt() { + 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, KeyPressMsg{Sym: KeyEscape} + } + case ansi.SS3: + return parseSs3(buf) + case ansi.DCS: + return parseDcs(buf) + case ansi.CSI: + return parseCsi(buf) + case ansi.OSC: + return parseOsc(buf) + case ansi.APC: + return parseApc(buf) + default: + if b <= ansi.US || b == ansi.DEL || b == ansi.SP { + return 1, 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+ + return 1, KeyPressMsg{Runes: []rune{rune(b) - 0x40}, Mod: ModCtrl | ModAlt} + } + return parseUtf8(buf) + } +} + +func parseCsi(b []byte) (int, Msg) { + if len(b) == 2 && b[0] == ansi.ESC { + // short cut if this is an alt+[ key + return 2, KeyPressMsg{Runes: []rune{rune(b[1])}, Mod: ModAlt} + } + + var csi ansi.CsiSequence + var params [parser.MaxParamsSize]int + 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] <= '?' { + csi.Cmd |= int(b[i]) << parser.MarkerShift + } + + // 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] += int(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 + csi.Cmd |= int(intermed) << parser.IntermedShift + + // Scan final byte in the range 0x40-0x7E + if i >= len(b) || b[i] < 0x40 || b[i] > 0x7E { + // Special case for URxvt keys + // CSI $ is an invalid sequence, but URxvt uses it for + // shift modified keys. + if b[i-1] == '$' { + n, ev := parseCsi(append(b[:i-1], '~')) + if k, ok := ev.(KeyPressMsg); ok { + k.Mod |= ModShift + return n, k + } + } + return i, UnknownMsg(b[:i-1]) + } + + // Add the final byte + csi.Cmd |= int(b[i]) + i++ + + csi.Params = params[:paramsLen] + marker, cmd := csi.Marker(), csi.Command() + switch marker { + case '?': + switch cmd { + case 'y': + switch intermed { + case '$': + // Report Mode (DECRPM) + if paramsLen != 2 { + return i, UnknownMsg(b[:i]) + } + return i, ReportModeEvent{Mode: csi.Param(0), Value: csi.Param(1)} + } + case 'c': + // Primary Device Attributes + return i, parsePrimaryDevAttrs(&csi) + case 'u': + // Kitty keyboard flags + if param := csi.Param(0); param != -1 { + return i, KittyKeyboardMsg(param) + } + case 'R': + // This report may return a third parameter representing the page + // number, but we don't really need it. + if paramsLen >= 2 { + return i, CursorPositionEvent{Row: csi.Param(0), Column: csi.Param(1)} + } + } + return i, UnknownMsg(b[:i]) + case '<': + switch cmd { + case 'm', 'M': + // Handle SGR mouse + if paramsLen != 3 { + return i, UnknownMsg(b[:i]) + } + return i, parseSGRMouseEvent(&csi) + default: + return i, UnknownMsg(b[:i]) + } + case '>': + switch cmd { + case 'm': + // XTerm modifyOtherKeys + if paramsLen != 2 || csi.Param(0) != 4 { + return i, UnknownMsg(b[:i]) + } + + return i, ModifyOtherKeysEvent(csi.Param(1)) + default: + return i, UnknownMsg(b[:i]) + } + case '=': + // We don't support any of these yet + return i, UnknownMsg(b[:i]) + } + + switch cmd := csi.Command(); cmd { + case 'I': + return i, FocusMsg{} + case 'O': + return i, BlurMsg{} + case 'R': + // Cursor position report OR modified F3 + if paramsLen == 0 { + return i, KeyPressMsg{Sym: KeyF3} + } else if paramsLen != 2 { + break + } + + // XXX: We cannot differentiate between cursor position report and + // CSI 1 ; R (which is modified F3) when the cursor is at the + // row 1. In this case, we report a modified F3 event since it's more + // likely to be the case than the cursor being at the first row. + // + // For a non ambiguous cursor position report, use + // [ansi.RequestExtendedCursorPosition] (DECXCPR) instead. + if csi.Param(0) != 1 { + return i, CursorPositionEvent{Row: csi.Param(0), Column: csi.Param(1)} + } + + fallthrough + case 'a', 'b', 'c', 'd', 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'S', 'Z': + var k KeyPressMsg + switch cmd { + case 'a', 'b', 'c', 'd': + k = KeyPressMsg{Sym: KeyUp + KeySym(cmd-'a'), Mod: ModShift} + case 'A', 'B', 'C', 'D': + k = KeyPressMsg{Sym: KeyUp + KeySym(cmd-'A')} + case 'E': + k = KeyPressMsg{Sym: KeyBegin} + case 'F': + k = KeyPressMsg{Sym: KeyEnd} + case 'H': + k = KeyPressMsg{Sym: KeyHome} + case 'P', 'Q', 'R', 'S': + k = KeyPressMsg{Sym: KeyF1 + KeySym(cmd-'P')} + case 'Z': + k = KeyPressMsg{Sym: KeyTab, Mod: ModShift} + } + if paramsLen > 1 && csi.Param(0) == 1 { + // CSI 1 ; A + if paramsLen > 1 { + k.Mod |= KeyMod(csi.Param(1) - 1) + } + } + return i, k + case 'M': + // Handle X10 mouse + if i+3 > len(b) { + return i, UnknownMsg(b[:i]) + } + return i + 3, parseX10MouseEvent(append(b[:i], b[i:i+3]...)) + case 'y': + // Report Mode (DECRPM) + if paramsLen != 2 { + return i, UnknownMsg(b[:i]) + } + return i, ReportModeEvent{Mode: csi.Param(0), Value: csi.Param(1)} + case 'u': + // Kitty keyboard protocol & CSI u (fixterms) + if paramsLen == 0 { + return i, UnknownMsg(b[:i]) + } + return i, parseKittyKeyboard(&csi) + case '_': + // Win32 Input Mode + if paramsLen != 6 { + return i, UnknownMsg(b[:i]) + } + + rc := uint16(csi.Param(5)) + if rc == 0 { + rc = 1 + } + + event := parseWin32InputKeyEvent( + coninput.VirtualKeyCode(csi.Param(0)), // Vk wVirtualKeyCode + coninput.VirtualKeyCode(csi.Param(1)), // Sc wVirtualScanCode + rune(csi.Param(2)), // Uc UnicodeChar + csi.Param(3) == 1, // Kd bKeyDown + coninput.ControlKeyState(csi.Param(4)), // Cs dwControlKeyState + rc, // Rc wRepeatCount + ) + + if event == nil { + return i, UnknownMsg(b[:]) + } + + return i, event + case '@', '^', '~': + if paramsLen == 0 { + return i, UnknownMsg(b[:i]) + } + + param := csi.Param(0) + switch cmd { + case '~': + switch param { + case 27: + // XTerm modifyOtherKeys 2 + if paramsLen != 3 { + return i, UnknownMsg(b[:i]) + } + return i, parseXTermModifyOtherKeys(&csi) + case 200: + // bracketed-paste start + return i, PasteStartMsg{} + case 201: + // bracketed-paste end + return i, PasteEndMsg{} + } + } + + switch param { + case 1, 2, 3, 4, 5, 6, 7, 8: + fallthrough + case 11, 12, 13, 14, 15: + fallthrough + case 17, 18, 19, 20, 21, 23, 24, 25, 26: + fallthrough + case 28, 29, 31, 32, 33, 34: + var k KeyPressMsg + switch param { + case 1: + if flags&_FlagFind != 0 { + k = KeyPressMsg{Sym: KeyFind} + } else { + k = KeyPressMsg{Sym: KeyHome} + } + case 2: + k = KeyPressMsg{Sym: KeyInsert} + case 3: + k = KeyPressMsg{Sym: KeyDelete} + case 4: + if flags&_FlagSelect != 0 { + k = KeyPressMsg{Sym: KeySelect} + } else { + k = KeyPressMsg{Sym: KeyEnd} + } + case 5: + k = KeyPressMsg{Sym: KeyPgUp} + case 6: + k = KeyPressMsg{Sym: KeyPgDown} + case 7: + k = KeyPressMsg{Sym: KeyHome} + case 8: + k = KeyPressMsg{Sym: KeyEnd} + case 11, 12, 13, 14, 15: + k = KeyPressMsg{Sym: KeyF1 + KeySym(param-11)} + case 17, 18, 19, 20, 21: + k = KeyPressMsg{Sym: KeyF6 + KeySym(param-17)} + case 23, 24, 25, 26: + k = KeyPressMsg{Sym: KeyF11 + KeySym(param-23)} + case 28, 29: + k = KeyPressMsg{Sym: KeyF15 + KeySym(param-28)} + case 31, 32, 33, 34: + k = KeyPressMsg{Sym: KeyF17 + KeySym(param-31)} + } + + // modifiers + if paramsLen > 1 { + k.Mod |= KeyMod(csi.Param(1) - 1) + } + + // Handle URxvt weird keys + switch cmd { + case '^': + k.Mod |= ModCtrl + case '@': + k.Mod |= ModCtrl | ModShift + } + + return i, k + } + } + return i, UnknownMsg(b[:i]) +} + +// parseSs3 parses a SS3 sequence. +// See https://vt100.net/docs/vt220-rm/chapter4.html#S4.4.4.2 +func parseSs3(b []byte) (int, Msg) { + if len(b) == 2 && b[0] == ansi.ESC { + // short cut if this is an alt+O key + return 2, KeyPressMsg{Runes: []rune{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) || b[i] < 0x21 || b[i] > 0x7E { + return i, UnknownMsg(b[:i]) + } + + // GL character(s) + gl := b[i] + i++ + + var k KeyPressMsg + switch gl { + case 'a', 'b', 'c', 'd': + k = KeyPressMsg{Sym: KeyUp + KeySym(gl-'a'), Mod: ModCtrl} + case 'A', 'B', 'C', 'D': + k = KeyPressMsg{Sym: KeyUp + KeySym(gl-'A')} + case 'E': + k = KeyPressMsg{Sym: KeyBegin} + case 'F': + k = KeyPressMsg{Sym: KeyEnd} + case 'H': + k = KeyPressMsg{Sym: KeyHome} + case 'P', 'Q', 'R', 'S': + k = KeyPressMsg{Sym: KeyF1 + KeySym(gl-'P')} + case 'M': + k = KeyPressMsg{Sym: KeyKpEnter} + case 'X': + k = KeyPressMsg{Sym: KeyKpEqual} + case 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y': + k = KeyPressMsg{Sym: KeyKpMultiply + KeySym(gl-'j')} + default: + return i, UnknownMsg(b[:i]) + } + + // Handle weird SS3 Func + if mod > 0 { + k.Mod |= KeyMod(mod - 1) + } + + return i, k +} + +func parseOsc(b []byte) (int, Msg) { + if len(b) == 2 && b[0] == ansi.ESC { + // short cut if this is an alt+] key + return 2, KeyPressMsg{Runes: []rune{rune(b[1])}, Mod: ModAlt} + } + + 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 b[i] == ansi.BEL || b[i] == ansi.ESC || b[i] == ansi.ST { + break + } + } + + if i >= len(b) { + return i, UnknownMsg(b[:i]) + } + + 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++ + } + + if end <= start { + return i, UnknownMsg(b[:i]) + } + + data := string(b[start:end]) + switch cmd { + case 10: + return i, ForegroundColorEvent{xParseColor(data)} + case 11: + return i, BackgroundColorEvent{xParseColor(data)} + case 12: + return i, CursorColorEvent{xParseColor(data)} + case 52: + parts := strings.Split(data, ";") + if len(parts) == 0 { + return i, ClipboardEvent("") + } + b64 := parts[len(parts)-1] + bts, err := base64.StdEncoding.DecodeString(b64) + if err != nil { + return i, ClipboardEvent("") + } + return i, ClipboardEvent(bts) + default: + return i, UnknownMsg(b[:i]) + } +} + +// parseStTerminated parses a control sequence that gets terminated by a ST character. +func parseStTerminated(intro8, intro7 byte) func([]byte) (int, Msg) { + return func(b []byte) (int, Msg) { + 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 \) + // nolint: revive + for ; i < len(b) && b[i] != ansi.ST && b[i] != ansi.ESC; i++ { + } + + if i >= len(b) { + return i, UnknownMsg(b[:i]) + } + i++ + + // Check 7-bit ST (string terminator) character + if i < len(b) && b[i-1] == ansi.ESC && b[i] == '\\' { + i++ + } + + return i, UnknownMsg(b[:i]) + } +} + +func parseDcs(b []byte) (int, Msg) { + if len(b) == 2 && b[0] == ansi.ESC { + // short cut if this is an alt+P key + return 2, KeyPressMsg{Runes: []rune{rune(b[1])}, Mod: ModAlt} + } + + var params [16]int + var paramsLen int + var dcs ansi.DcsSequence + + // 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] <= '?' { + dcs.Cmd |= int(b[i]) << parser.MarkerShift + } + + // 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] += int(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 + dcs.Cmd |= int(intermed) << parser.IntermedShift + + // Scan final byte in the range 0x40-0x7E + if i >= len(b) || b[i] < 0x40 || b[i] > 0x7E { + return i, UnknownMsg(b[:i]) + } + + // Add the final byte + dcs.Cmd |= int(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) { + return i, UnknownMsg(b[:i]) + } + + 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++ + } + + dcs.Params = params[:paramsLen] + switch cmd := dcs.Command(); cmd { + case 'r': + switch dcs.Intermediate() { + case '+': + // XTGETTCAP responses + switch param := dcs.Param(0); param { + case 0, 1: + 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\\" + // + // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands + return i, tc + } + } + } + + return i, UnknownMsg(b[:i]) +} + +func parseApc(b []byte) (int, Msg) { + if len(b) == 2 && b[0] == ansi.ESC { + // short cut if this is an alt+_ key + return 2, KeyPressMsg{Runes: []rune{rune(b[1])}, Mod: ModAlt} + } + + // APC sequences are introduced by APC (0x9f) or ESC _ (0x1b 0x5f) + return parseStTerminated(ansi.APC, '_')(b) +} + +func parseUtf8(b []byte) (int, Msg) { + r, rw := utf8.DecodeRune(b) + if r <= ansi.US || r == ansi.DEL || r == ansi.SP { + // Control codes get handled by parseControl + return 1, parseControl(byte(r)) + } else if r == utf8.RuneError { + return 1, UnknownMsg(b[0]) + } + return rw, KeyPressMsg{Runes: []rune{r}} +} + +func parseControl(b byte) Msg { + switch b { + case ansi.NUL: + if flags&_FlagCtrlAt != 0 { + return KeyPressMsg{Runes: []rune{'@'}, Mod: ModCtrl} + } + return KeyPressMsg{Runes: []rune{' '}, Sym: KeySpace, Mod: ModCtrl} + case ansi.BS: + return KeyPressMsg{Runes: []rune{'h'}, Mod: ModCtrl} + case ansi.HT: + if flags&_FlagCtrlI != 0 { + return KeyPressMsg{Runes: []rune{'i'}, Mod: ModCtrl} + } + return KeyPressMsg{Sym: KeyTab} + case ansi.CR: + if flags&_FlagCtrlM != 0 { + return KeyPressMsg{Runes: []rune{'m'}, Mod: ModCtrl} + } + return KeyPressMsg{Sym: KeyEnter} + case ansi.ESC: + if flags&_FlagCtrlOpenBracket != 0 { + return KeyPressMsg{Runes: []rune{'['}, Mod: ModCtrl} + } + return KeyPressMsg{Sym: KeyEscape} + case ansi.DEL: + if flags&_FlagBackspace != 0 { + return KeyPressMsg{Sym: KeyDelete} + } + return KeyPressMsg{Sym: KeyBackspace} + case ansi.SP: + return KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}} + default: + if b >= ansi.SOH && b <= ansi.SUB { + // Use lower case letters for control codes + return KeyPressMsg{Runes: []rune{rune(b + 0x60)}, Mod: ModCtrl} + } else if b >= ansi.FS && b <= ansi.US { + return KeyPressMsg{Runes: []rune{rune(b + 0x40)}, Mod: ModCtrl} + } + return UnknownMsg(b) + } +} diff --git a/parse_test.go b/parse_test.go new file mode 100644 index 0000000000..41b608dcaa --- /dev/null +++ b/parse_test.go @@ -0,0 +1,41 @@ +package tea + +import ( + "image/color" + "reflect" + "testing" +) + +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") + want := []Msg{ + KeyPressMsg{Sym: KeyTab, Mod: ModShift | ModAlt}, + KeyPressMsg{Runes: []rune{'t'}}, + KeyPressMsg{Runes: []rune{'e'}}, + KeyPressMsg{Runes: []rune{'s'}}, + KeyPressMsg{Runes: []rune{'t'}}, + KeyPressMsg{Runes: []rune{' '}, Sym: KeySpace, Mod: ModCtrl}, + ForegroundColorEvent{color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff}}, + KeyPressMsg{Sym: KeyEscape, Mod: ModShift}, + ReportModeEvent{Mode: 1049, Value: 2}, + } + for i := 0; len(input) != 0; i++ { + if i >= len(want) { + t.Fatalf("reached end of want events") + } + n, got := 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) { + 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++ { + parseSequence(input) + } +} diff --git a/paste.go b/paste.go new file mode 100644 index 0000000000..efb53d6a5f --- /dev/null +++ b/paste.go @@ -0,0 +1,13 @@ +package tea + +// PasteMsg is an message that is emitted when a terminal receives pasted text +// using bracketed-paste. +type PasteMsg string + +// PasteStartMsg is an message that is emitted when a terminal enters +// bracketed-paste mode. +type PasteStartMsg struct{} + +// PasteEvent is an message that is emitted when a terminal receives pasted +// text. +type PasteEndMsg struct{} diff --git a/table.go b/table.go new file mode 100644 index 0000000000..4b350643b2 --- /dev/null +++ b/table.go @@ -0,0 +1,391 @@ +package tea + +import ( + "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. +// TODO: Use flags? +func buildKeysTable(flags int, term string) map[string]Key { + nul := Key{Runes: []rune{' '}, Sym: KeySpace, Mod: ModCtrl} // ctrl+@ or ctrl+space + if flags&_FlagCtrlAt != 0 { + nul = Key{Runes: []rune{'@'}, Mod: ModCtrl} + } + + tab := Key{Sym: KeyTab} // ctrl+i or tab + if flags&_FlagCtrlI != 0 { + tab = Key{Runes: []rune{'i'}, Mod: ModCtrl} + } + + enter := Key{Sym: KeyEnter} // ctrl+m or enter + if flags&_FlagCtrlM != 0 { + enter = Key{Runes: []rune{'m'}, Mod: ModCtrl} + } + + esc := Key{Sym: KeyEscape} // ctrl+[ or escape + if flags&_FlagCtrlOpenBracket != 0 { + esc = Key{Runes: []rune{'['}, Mod: ModCtrl} // ctrl+[ or escape + } + + del := Key{Sym: KeyBackspace} + if flags&_FlagBackspace != 0 { + del.Sym = KeyDelete + } + + find := Key{Sym: KeyHome} + if flags&_FlagFind != 0 { + find.Sym = KeyFind + } + + sel := Key{Sym: KeyEnd} + if flags&_FlagSelect != 0 { + sel.Sym = 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)): {Runes: []rune{'a'}, Mod: ModCtrl}, + string(byte(ansi.STX)): {Runes: []rune{'b'}, Mod: ModCtrl}, + string(byte(ansi.ETX)): {Runes: []rune{'c'}, Mod: ModCtrl}, + string(byte(ansi.EOT)): {Runes: []rune{'d'}, Mod: ModCtrl}, + string(byte(ansi.ENQ)): {Runes: []rune{'e'}, Mod: ModCtrl}, + string(byte(ansi.ACK)): {Runes: []rune{'f'}, Mod: ModCtrl}, + string(byte(ansi.BEL)): {Runes: []rune{'g'}, Mod: ModCtrl}, + string(byte(ansi.BS)): {Runes: []rune{'h'}, Mod: ModCtrl}, + string(byte(ansi.HT)): tab, + string(byte(ansi.LF)): {Runes: []rune{'j'}, Mod: ModCtrl}, + string(byte(ansi.VT)): {Runes: []rune{'k'}, Mod: ModCtrl}, + string(byte(ansi.FF)): {Runes: []rune{'l'}, Mod: ModCtrl}, + string(byte(ansi.CR)): enter, + string(byte(ansi.SO)): {Runes: []rune{'n'}, Mod: ModCtrl}, + string(byte(ansi.SI)): {Runes: []rune{'o'}, Mod: ModCtrl}, + string(byte(ansi.DLE)): {Runes: []rune{'p'}, Mod: ModCtrl}, + string(byte(ansi.DC1)): {Runes: []rune{'q'}, Mod: ModCtrl}, + string(byte(ansi.DC2)): {Runes: []rune{'r'}, Mod: ModCtrl}, + string(byte(ansi.DC3)): {Runes: []rune{'s'}, Mod: ModCtrl}, + string(byte(ansi.DC4)): {Runes: []rune{'t'}, Mod: ModCtrl}, + string(byte(ansi.NAK)): {Runes: []rune{'u'}, Mod: ModCtrl}, + string(byte(ansi.SYN)): {Runes: []rune{'v'}, Mod: ModCtrl}, + string(byte(ansi.ETB)): {Runes: []rune{'w'}, Mod: ModCtrl}, + string(byte(ansi.CAN)): {Runes: []rune{'x'}, Mod: ModCtrl}, + string(byte(ansi.EM)): {Runes: []rune{'y'}, Mod: ModCtrl}, + string(byte(ansi.SUB)): {Runes: []rune{'z'}, Mod: ModCtrl}, + string(byte(ansi.ESC)): esc, + string(byte(ansi.FS)): {Runes: []rune{'\\'}, Mod: ModCtrl}, + string(byte(ansi.GS)): {Runes: []rune{']'}, Mod: ModCtrl}, + string(byte(ansi.RS)): {Runes: []rune{'^'}, Mod: ModCtrl}, + string(byte(ansi.US)): {Runes: []rune{'_'}, Mod: ModCtrl}, + + // Special keys in G0 + string(byte(ansi.SP)): {Sym: KeySpace, Runes: []rune{' '}}, + string(byte(ansi.DEL)): del, + + // Special keys + + "\x1b[Z": {Sym: KeyTab, Mod: ModShift}, + + "\x1b[1~": find, + "\x1b[2~": {Sym: KeyInsert}, + "\x1b[3~": {Sym: KeyDelete}, + "\x1b[4~": sel, + "\x1b[5~": {Sym: KeyPgUp}, + "\x1b[6~": {Sym: KeyPgDown}, + "\x1b[7~": {Sym: KeyHome}, + "\x1b[8~": {Sym: KeyEnd}, + + // Normal mode + "\x1b[A": {Sym: KeyUp}, + "\x1b[B": {Sym: KeyDown}, + "\x1b[C": {Sym: KeyRight}, + "\x1b[D": {Sym: KeyLeft}, + "\x1b[E": {Sym: KeyBegin}, + "\x1b[F": {Sym: KeyEnd}, + "\x1b[H": {Sym: KeyHome}, + "\x1b[P": {Sym: KeyF1}, + "\x1b[Q": {Sym: KeyF2}, + "\x1b[R": {Sym: KeyF3}, + "\x1b[S": {Sym: KeyF4}, + + // Application Cursor Key Mode (DECCKM) + "\x1bOA": {Sym: KeyUp}, + "\x1bOB": {Sym: KeyDown}, + "\x1bOC": {Sym: KeyRight}, + "\x1bOD": {Sym: KeyLeft}, + "\x1bOE": {Sym: KeyBegin}, + "\x1bOF": {Sym: KeyEnd}, + "\x1bOH": {Sym: KeyHome}, + "\x1bOP": {Sym: KeyF1}, + "\x1bOQ": {Sym: KeyF2}, + "\x1bOR": {Sym: KeyF3}, + "\x1bOS": {Sym: KeyF4}, + + // Keypad Application Mode (DECKPAM) + + "\x1bOM": {Sym: KeyKpEnter}, + "\x1bOX": {Sym: KeyKpEqual}, + "\x1bOj": {Sym: KeyKpMultiply}, + "\x1bOk": {Sym: KeyKpPlus}, + "\x1bOl": {Sym: KeyKpComma}, + "\x1bOm": {Sym: KeyKpMinus}, + "\x1bOn": {Sym: KeyKpDecimal}, + "\x1bOo": {Sym: KeyKpDivide}, + "\x1bOp": {Sym: KeyKp0}, + "\x1bOq": {Sym: KeyKp1}, + "\x1bOr": {Sym: KeyKp2}, + "\x1bOs": {Sym: KeyKp3}, + "\x1bOt": {Sym: KeyKp4}, + "\x1bOu": {Sym: KeyKp5}, + "\x1bOv": {Sym: KeyKp6}, + "\x1bOw": {Sym: KeyKp7}, + "\x1bOx": {Sym: KeyKp8}, + "\x1bOy": {Sym: KeyKp9}, + + // Function keys + + "\x1b[11~": {Sym: KeyF1}, + "\x1b[12~": {Sym: KeyF2}, + "\x1b[13~": {Sym: KeyF3}, + "\x1b[14~": {Sym: KeyF4}, + "\x1b[15~": {Sym: KeyF5}, + "\x1b[17~": {Sym: KeyF6}, + "\x1b[18~": {Sym: KeyF7}, + "\x1b[19~": {Sym: KeyF8}, + "\x1b[20~": {Sym: KeyF9}, + "\x1b[21~": {Sym: KeyF10}, + "\x1b[23~": {Sym: KeyF11}, + "\x1b[24~": {Sym: KeyF12}, + "\x1b[25~": {Sym: KeyF13}, + "\x1b[26~": {Sym: KeyF14}, + "\x1b[28~": {Sym: KeyF15}, + "\x1b[29~": {Sym: KeyF16}, + "\x1b[31~": {Sym: KeyF17}, + "\x1b[32~": {Sym: KeyF18}, + "\x1b[33~": {Sym: KeyF19}, + "\x1b[34~": {Sym: KeyF20}, + } + + // CSI ~ sequence keys + csiTildeKeys := map[string]Key{ + "1": find, "2": {Sym: KeyInsert}, + "3": {Sym: KeyDelete}, "4": sel, + "5": {Sym: KeyPgUp}, "6": {Sym: KeyPgDown}, + "7": {Sym: KeyHome}, "8": {Sym: KeyEnd}, + // There are no 9 and 10 keys + "11": {Sym: KeyF1}, "12": {Sym: KeyF2}, + "13": {Sym: KeyF3}, "14": {Sym: KeyF4}, + "15": {Sym: KeyF5}, "17": {Sym: KeyF6}, + "18": {Sym: KeyF7}, "19": {Sym: KeyF8}, + "20": {Sym: KeyF9}, "21": {Sym: KeyF10}, + "23": {Sym: KeyF11}, "24": {Sym: KeyF12}, + "25": {Sym: KeyF13}, "26": {Sym: KeyF14}, + "28": {Sym: KeyF15}, "29": {Sym: KeyF16}, + "31": {Sym: KeyF17}, "32": {Sym: KeyF18}, + "33": {Sym: KeyF19}, "34": {Sym: KeyF20}, + } + + // URxvt keys + // See https://manpages.ubuntu.com/manpages/trusty/man7/urxvt.7.html#key%20codes + table["\x1b[a"] = Key{Sym: KeyUp, Mod: ModShift} + table["\x1b[b"] = Key{Sym: KeyDown, Mod: ModShift} + table["\x1b[c"] = Key{Sym: KeyRight, Mod: ModShift} + table["\x1b[d"] = Key{Sym: KeyLeft, Mod: ModShift} + table["\x1bOa"] = Key{Sym: KeyUp, Mod: ModCtrl} + table["\x1bOb"] = Key{Sym: KeyDown, Mod: ModCtrl} + table["\x1bOc"] = Key{Sym: KeyRight, Mod: ModCtrl} + table["\x1bOd"] = Key{Sym: KeyLeft, Mod: ModCtrl} + // TODO: invistigate 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{Sym: KeyF11, Mod: ModShift} + table["\x1b[24$"] = Key{Sym: KeyF12, Mod: ModShift} + table["\x1b[25$"] = Key{Sym: KeyF13, Mod: ModShift} + table["\x1b[26$"] = Key{Sym: KeyF14, Mod: ModShift} + table["\x1b[28$"] = Key{Sym: KeyF15, Mod: ModShift} + table["\x1b[29$"] = Key{Sym: KeyF16, Mod: ModShift} + table["\x1b[31$"] = Key{Sym: KeyF17, Mod: ModShift} + table["\x1b[32$"] = Key{Sym: KeyF18, Mod: ModShift} + table["\x1b[33$"] = Key{Sym: KeyF19, Mod: ModShift} + table["\x1b[34$"] = Key{Sym: KeyF20, Mod: ModShift} + table["\x1b[11^"] = Key{Sym: KeyF1, Mod: ModCtrl} + table["\x1b[12^"] = Key{Sym: KeyF2, Mod: ModCtrl} + table["\x1b[13^"] = Key{Sym: KeyF3, Mod: ModCtrl} + table["\x1b[14^"] = Key{Sym: KeyF4, Mod: ModCtrl} + table["\x1b[15^"] = Key{Sym: KeyF5, Mod: ModCtrl} + table["\x1b[17^"] = Key{Sym: KeyF6, Mod: ModCtrl} + table["\x1b[18^"] = Key{Sym: KeyF7, Mod: ModCtrl} + table["\x1b[19^"] = Key{Sym: KeyF8, Mod: ModCtrl} + table["\x1b[20^"] = Key{Sym: KeyF9, Mod: ModCtrl} + table["\x1b[21^"] = Key{Sym: KeyF10, Mod: ModCtrl} + table["\x1b[23^"] = Key{Sym: KeyF11, Mod: ModCtrl} + table["\x1b[24^"] = Key{Sym: KeyF12, Mod: ModCtrl} + table["\x1b[25^"] = Key{Sym: KeyF13, Mod: ModCtrl} + table["\x1b[26^"] = Key{Sym: KeyF14, Mod: ModCtrl} + table["\x1b[28^"] = Key{Sym: KeyF15, Mod: ModCtrl} + table["\x1b[29^"] = Key{Sym: KeyF16, Mod: ModCtrl} + table["\x1b[31^"] = Key{Sym: KeyF17, Mod: ModCtrl} + table["\x1b[32^"] = Key{Sym: KeyF18, Mod: ModCtrl} + table["\x1b[33^"] = Key{Sym: KeyF19, Mod: ModCtrl} + table["\x1b[34^"] = Key{Sym: KeyF20, Mod: ModCtrl} + table["\x1b[23@"] = Key{Sym: KeyF11, Mod: ModShift | ModCtrl} + table["\x1b[24@"] = Key{Sym: KeyF12, Mod: ModShift | ModCtrl} + table["\x1b[25@"] = Key{Sym: KeyF13, Mod: ModShift | ModCtrl} + table["\x1b[26@"] = Key{Sym: KeyF14, Mod: ModShift | ModCtrl} + table["\x1b[28@"] = Key{Sym: KeyF15, Mod: ModShift | ModCtrl} + table["\x1b[29@"] = Key{Sym: KeyF16, Mod: ModShift | ModCtrl} + table["\x1b[31@"] = Key{Sym: KeyF17, Mod: ModShift | ModCtrl} + table["\x1b[32@"] = Key{Sym: KeyF18, Mod: ModShift | ModCtrl} + table["\x1b[33@"] = Key{Sym: KeyF19, Mod: ModShift | ModCtrl} + table["\x1b[34@"] = Key{Sym: KeyF20, Mod: ModShift | ModCtrl} + + // Register Alt + 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 + tmap["\x1b"+seq] = key + } + for seq, key := range tmap { + table[seq] = key + } + + // 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": {Sym: KeyKpEnter}, "X": {Sym: KeyKpEqual}, + "j": {Sym: KeyKpMultiply}, "k": {Sym: KeyKpPlus}, + "l": {Sym: KeyKpComma}, "m": {Sym: KeyKpMinus}, + "n": {Sym: KeyKpDecimal}, "o": {Sym: KeyKpDivide}, + "p": {Sym: KeyKp0}, "q": {Sym: KeyKp1}, + "r": {Sym: KeyKp2}, "s": {Sym: KeyKp3}, + "t": {Sym: KeyKp4}, "u": {Sym: KeyKp5}, + "v": {Sym: KeyKp6}, "w": {Sym: KeyKp7}, + "x": {Sym: KeyKp8}, "y": {Sym: KeyKp9}, + } + + // XTerm keys + csiFuncKeys := map[string]Key{ + "A": {Sym: KeyUp}, "B": {Sym: KeyDown}, + "C": {Sym: KeyRight}, "D": {Sym: KeyLeft}, + "E": {Sym: KeyBegin}, "F": {Sym: KeyEnd}, + "H": {Sym: KeyHome}, "P": {Sym: KeyF1}, + "Q": {Sym: KeyF2}, "R": {Sym: KeyF3}, + "S": {Sym: KeyF4}, + } + + // CSI 27 ; ; ~ keys defined in XTerm modifyOtherKeys + modifyOtherKeys := map[int]Key{ + ansi.BS: {Sym: KeyBackspace}, + ansi.HT: {Sym: KeyTab}, + ansi.CR: {Sym: KeyEnter}, + ansi.ESC: {Sym: KeyEscape}, + ansi.DEL: {Sym: KeyBackspace}, + } + + for _, m := range modifiers { + // XTerm modifier offset +1 + xtermMod := strconv.Itoa(int(m) + 1) + + // CSI 1 ; + 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 + for k, v := range ss3FuncKeys { + seq := "\x1bO" + xtermMod + k + key := v + key.Mod = m + table[seq] = key + } + // CSI ; ~ + for k, v := range csiTildeKeys { + seq := "\x1b[" + k + ";" + xtermMod + "~" + key := v + key.Mod = m + table[seq] = key + } + // CSI 27 ; ; ~ + 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) + for seq, key := range titable { + table[seq] = key + } + } + + return table +} diff --git a/tea.go b/tea.go index 62cd6415bb..c0105b320b 100644 --- a/tea.go +++ b/tea.go @@ -22,7 +22,6 @@ import ( "syscall" "github.com/charmbracelet/x/term" - "github.com/muesli/cancelreader" "golang.org/x/sync/errgroup" ) @@ -159,7 +158,7 @@ type Program struct { // ttyInput is null if input is not a TTY. ttyInput term.File previousTtyInputState *term.State - cancelReader cancelreader.CancelReader + inputReader *driver readLoopDone chan struct{} // was the altscreen active before releasing the terminal? @@ -543,11 +542,18 @@ func (p *Program) Run() (Model, error) { p.renderer.enableMouseSGRMode() } + // Init the input reader and initial model. + model := p.initialModel + if p.input != nil { + if err := p.initInputReader(); err != nil { + return model, err + } + } + // Start the renderer. p.renderer.start() // Initialize the program. - model := p.initialModel if initCmd := model.Init(); initCmd != nil { ch := make(chan struct{}) handlers.add(ch) @@ -565,13 +571,6 @@ func (p *Program) Run() (Model, error) { // Render the initial view. p.renderer.write(model.View()) - // Subscribe to user input. - if p.input != nil { - if err := p.initCancelReader(); err != nil { - return model, err - } - } - // Handle resize events. handlers.add(p.handleResize()) @@ -592,12 +591,12 @@ func (p *Program) Run() (Model, error) { p.cancel() // Check if the cancel reader has been setup before waiting and closing. - if p.cancelReader != nil { + if p.inputReader != nil { // Wait for input loop to finish. - if p.cancelReader.Cancel() { + if p.inputReader.Cancel() { p.waitForReadLoop() } - _ = p.cancelReader.Close() + _ = p.inputReader.Close() } // Wait for all handlers to finish. @@ -684,8 +683,8 @@ func (p *Program) shutdown(kill bool) { // reader. You can return control to the Program with RestoreTerminal. func (p *Program) ReleaseTerminal() error { atomic.StoreUint32(&p.ignoreSignals, 1) - if p.cancelReader != nil { - p.cancelReader.Cancel() + if p.inputReader != nil { + p.inputReader.Cancel() } p.waitForReadLoop() @@ -708,7 +707,7 @@ func (p *Program) RestoreTerminal() error { if err := p.initTerminal(); err != nil { return err } - if err := p.initCancelReader(); err != nil { + if err := p.initInputReader(); err != nil { return err } if p.altScreenWasActive { diff --git a/termcap.go b/termcap.go new file mode 100644 index 0000000000..b2d31911b8 --- /dev/null +++ b/termcap.go @@ -0,0 +1,54 @@ +package tea + +import ( + "bytes" + "encoding/hex" + "strings" +) + +// TermcapMsg represents a Termcap 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 TermcapMsg string + +func parseTermcap(data []byte) TermcapMsg { + // XTGETTCAP + if len(data) == 0 { + return TermcapMsg("") + } + + var tc strings.Builder + split := bytes.Split(data, []byte{';'}) + for _, s := range split { + parts := bytes.SplitN(s, []byte{'='}, 2) + if len(parts) == 0 { + return TermcapMsg("") + } + + 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 TermcapMsg(tc.String()) +} diff --git a/terminfo.go b/terminfo.go new file mode 100644 index 0000000000..eb9cb3c71d --- /dev/null +++ b/terminfo.go @@ -0,0 +1,277 @@ +package tea + +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": {Sym: KeyUp}, + "kUP": {Sym: KeyUp, Mod: ModShift}, + "kUP3": {Sym: KeyUp, Mod: ModAlt}, + "kUP4": {Sym: KeyUp, Mod: ModShift | ModAlt}, + "kUP5": {Sym: KeyUp, Mod: ModCtrl}, + "kUP6": {Sym: KeyUp, Mod: ModShift | ModCtrl}, + "kUP7": {Sym: KeyUp, Mod: ModAlt | ModCtrl}, + "kUP8": {Sym: KeyUp, Mod: ModShift | ModAlt | ModCtrl}, + "kcud1": {Sym: KeyDown}, + "kDN": {Sym: KeyDown, Mod: ModShift}, + "kDN3": {Sym: KeyDown, Mod: ModAlt}, + "kDN4": {Sym: KeyDown, Mod: ModShift | ModAlt}, + "kDN5": {Sym: KeyDown, Mod: ModCtrl}, + "kDN7": {Sym: KeyDown, Mod: ModAlt | ModCtrl}, + "kDN6": {Sym: KeyDown, Mod: ModShift | ModCtrl}, + "kDN8": {Sym: KeyDown, Mod: ModShift | ModAlt | ModCtrl}, + "kcub1": {Sym: KeyLeft}, + "kLFT": {Sym: KeyLeft, Mod: ModShift}, + "kLFT3": {Sym: KeyLeft, Mod: ModAlt}, + "kLFT4": {Sym: KeyLeft, Mod: ModShift | ModAlt}, + "kLFT5": {Sym: KeyLeft, Mod: ModCtrl}, + "kLFT6": {Sym: KeyLeft, Mod: ModShift | ModCtrl}, + "kLFT7": {Sym: KeyLeft, Mod: ModAlt | ModCtrl}, + "kLFT8": {Sym: KeyLeft, Mod: ModShift | ModAlt | ModCtrl}, + "kcuf1": {Sym: KeyRight}, + "kRIT": {Sym: KeyRight, Mod: ModShift}, + "kRIT3": {Sym: KeyRight, Mod: ModAlt}, + "kRIT4": {Sym: KeyRight, Mod: ModShift | ModAlt}, + "kRIT5": {Sym: KeyRight, Mod: ModCtrl}, + "kRIT6": {Sym: KeyRight, Mod: ModShift | ModCtrl}, + "kRIT7": {Sym: KeyRight, Mod: ModAlt | ModCtrl}, + "kRIT8": {Sym: KeyRight, Mod: ModShift | ModAlt | ModCtrl}, + "kich1": {Sym: KeyInsert}, + "kIC": {Sym: KeyInsert, Mod: ModShift}, + "kIC3": {Sym: KeyInsert, Mod: ModAlt}, + "kIC4": {Sym: KeyInsert, Mod: ModShift | ModAlt}, + "kIC5": {Sym: KeyInsert, Mod: ModCtrl}, + "kIC6": {Sym: KeyInsert, Mod: ModShift | ModCtrl}, + "kIC7": {Sym: KeyInsert, Mod: ModAlt | ModCtrl}, + "kIC8": {Sym: KeyInsert, Mod: ModShift | ModAlt | ModCtrl}, + "kdch1": {Sym: KeyDelete}, + "kDC": {Sym: KeyDelete, Mod: ModShift}, + "kDC3": {Sym: KeyDelete, Mod: ModAlt}, + "kDC4": {Sym: KeyDelete, Mod: ModShift | ModAlt}, + "kDC5": {Sym: KeyDelete, Mod: ModCtrl}, + "kDC6": {Sym: KeyDelete, Mod: ModShift | ModCtrl}, + "kDC7": {Sym: KeyDelete, Mod: ModAlt | ModCtrl}, + "kDC8": {Sym: KeyDelete, Mod: ModShift | ModAlt | ModCtrl}, + "khome": {Sym: KeyHome}, + "kHOM": {Sym: KeyHome, Mod: ModShift}, + "kHOM3": {Sym: KeyHome, Mod: ModAlt}, + "kHOM4": {Sym: KeyHome, Mod: ModShift | ModAlt}, + "kHOM5": {Sym: KeyHome, Mod: ModCtrl}, + "kHOM6": {Sym: KeyHome, Mod: ModShift | ModCtrl}, + "kHOM7": {Sym: KeyHome, Mod: ModAlt | ModCtrl}, + "kHOM8": {Sym: KeyHome, Mod: ModShift | ModAlt | ModCtrl}, + "kend": {Sym: KeyEnd}, + "kEND": {Sym: KeyEnd, Mod: ModShift}, + "kEND3": {Sym: KeyEnd, Mod: ModAlt}, + "kEND4": {Sym: KeyEnd, Mod: ModShift | ModAlt}, + "kEND5": {Sym: KeyEnd, Mod: ModCtrl}, + "kEND6": {Sym: KeyEnd, Mod: ModShift | ModCtrl}, + "kEND7": {Sym: KeyEnd, Mod: ModAlt | ModCtrl}, + "kEND8": {Sym: KeyEnd, Mod: ModShift | ModAlt | ModCtrl}, + "kpp": {Sym: KeyPgUp}, + "kprv": {Sym: KeyPgUp}, + "kPRV": {Sym: KeyPgUp, Mod: ModShift}, + "kPRV3": {Sym: KeyPgUp, Mod: ModAlt}, + "kPRV4": {Sym: KeyPgUp, Mod: ModShift | ModAlt}, + "kPRV5": {Sym: KeyPgUp, Mod: ModCtrl}, + "kPRV6": {Sym: KeyPgUp, Mod: ModShift | ModCtrl}, + "kPRV7": {Sym: KeyPgUp, Mod: ModAlt | ModCtrl}, + "kPRV8": {Sym: KeyPgUp, Mod: ModShift | ModAlt | ModCtrl}, + "knp": {Sym: KeyPgDown}, + "knxt": {Sym: KeyPgDown}, + "kNXT": {Sym: KeyPgDown, Mod: ModShift}, + "kNXT3": {Sym: KeyPgDown, Mod: ModAlt}, + "kNXT4": {Sym: KeyPgDown, Mod: ModShift | ModAlt}, + "kNXT5": {Sym: KeyPgDown, Mod: ModCtrl}, + "kNXT6": {Sym: KeyPgDown, Mod: ModShift | ModCtrl}, + "kNXT7": {Sym: KeyPgDown, Mod: ModAlt | ModCtrl}, + "kNXT8": {Sym: KeyPgDown, Mod: ModShift | ModAlt | ModCtrl}, + + "kbs": {Sym: KeyBackspace}, + "kcbt": {Sym: 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": {Sym: KeyF1}, + "kf2": {Sym: KeyF2}, + "kf3": {Sym: KeyF3}, + "kf4": {Sym: KeyF4}, + "kf5": {Sym: KeyF5}, + "kf6": {Sym: KeyF6}, + "kf7": {Sym: KeyF7}, + "kf8": {Sym: KeyF8}, + "kf9": {Sym: KeyF9}, + "kf10": {Sym: KeyF10}, + "kf11": {Sym: KeyF11}, + "kf12": {Sym: KeyF12}, + "kf13": {Sym: KeyF1, Mod: ModShift}, + "kf14": {Sym: KeyF2, Mod: ModShift}, + "kf15": {Sym: KeyF3, Mod: ModShift}, + "kf16": {Sym: KeyF4, Mod: ModShift}, + "kf17": {Sym: KeyF5, Mod: ModShift}, + "kf18": {Sym: KeyF6, Mod: ModShift}, + "kf19": {Sym: KeyF7, Mod: ModShift}, + "kf20": {Sym: KeyF8, Mod: ModShift}, + "kf21": {Sym: KeyF9, Mod: ModShift}, + "kf22": {Sym: KeyF10, Mod: ModShift}, + "kf23": {Sym: KeyF11, Mod: ModShift}, + "kf24": {Sym: KeyF12, Mod: ModShift}, + "kf25": {Sym: KeyF1, Mod: ModCtrl}, + "kf26": {Sym: KeyF2, Mod: ModCtrl}, + "kf27": {Sym: KeyF3, Mod: ModCtrl}, + "kf28": {Sym: KeyF4, Mod: ModCtrl}, + "kf29": {Sym: KeyF5, Mod: ModCtrl}, + "kf30": {Sym: KeyF6, Mod: ModCtrl}, + "kf31": {Sym: KeyF7, Mod: ModCtrl}, + "kf32": {Sym: KeyF8, Mod: ModCtrl}, + "kf33": {Sym: KeyF9, Mod: ModCtrl}, + "kf34": {Sym: KeyF10, Mod: ModCtrl}, + "kf35": {Sym: KeyF11, Mod: ModCtrl}, + "kf36": {Sym: KeyF12, Mod: ModCtrl}, + "kf37": {Sym: KeyF1, Mod: ModShift | ModCtrl}, + "kf38": {Sym: KeyF2, Mod: ModShift | ModCtrl}, + "kf39": {Sym: KeyF3, Mod: ModShift | ModCtrl}, + "kf40": {Sym: KeyF4, Mod: ModShift | ModCtrl}, + "kf41": {Sym: KeyF5, Mod: ModShift | ModCtrl}, + "kf42": {Sym: KeyF6, Mod: ModShift | ModCtrl}, + "kf43": {Sym: KeyF7, Mod: ModShift | ModCtrl}, + "kf44": {Sym: KeyF8, Mod: ModShift | ModCtrl}, + "kf45": {Sym: KeyF9, Mod: ModShift | ModCtrl}, + "kf46": {Sym: KeyF10, Mod: ModShift | ModCtrl}, + "kf47": {Sym: KeyF11, Mod: ModShift | ModCtrl}, + "kf48": {Sym: KeyF12, Mod: ModShift | ModCtrl}, + "kf49": {Sym: KeyF1, Mod: ModAlt}, + "kf50": {Sym: KeyF2, Mod: ModAlt}, + "kf51": {Sym: KeyF3, Mod: ModAlt}, + "kf52": {Sym: KeyF4, Mod: ModAlt}, + "kf53": {Sym: KeyF5, Mod: ModAlt}, + "kf54": {Sym: KeyF6, Mod: ModAlt}, + "kf55": {Sym: KeyF7, Mod: ModAlt}, + "kf56": {Sym: KeyF8, Mod: ModAlt}, + "kf57": {Sym: KeyF9, Mod: ModAlt}, + "kf58": {Sym: KeyF10, Mod: ModAlt}, + "kf59": {Sym: KeyF11, Mod: ModAlt}, + "kf60": {Sym: KeyF12, Mod: ModAlt}, + "kf61": {Sym: KeyF1, Mod: ModShift | ModAlt}, + "kf62": {Sym: KeyF2, Mod: ModShift | ModAlt}, + "kf63": {Sym: 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{Sym: KeyF13} + keys["kf14"] = Key{Sym: KeyF14} + keys["kf15"] = Key{Sym: KeyF15} + keys["kf16"] = Key{Sym: KeyF16} + keys["kf17"] = Key{Sym: KeyF17} + keys["kf18"] = Key{Sym: KeyF18} + keys["kf19"] = Key{Sym: KeyF19} + keys["kf20"] = Key{Sym: KeyF20} + keys["kf21"] = Key{Sym: KeyF21} + keys["kf22"] = Key{Sym: KeyF22} + keys["kf23"] = Key{Sym: KeyF23} + keys["kf24"] = Key{Sym: KeyF24} + keys["kf25"] = Key{Sym: KeyF25} + keys["kf26"] = Key{Sym: KeyF26} + keys["kf27"] = Key{Sym: KeyF27} + keys["kf28"] = Key{Sym: KeyF28} + keys["kf29"] = Key{Sym: KeyF29} + keys["kf30"] = Key{Sym: KeyF30} + keys["kf31"] = Key{Sym: KeyF31} + keys["kf32"] = Key{Sym: KeyF32} + keys["kf33"] = Key{Sym: KeyF33} + keys["kf34"] = Key{Sym: KeyF34} + keys["kf35"] = Key{Sym: KeyF35} + keys["kf36"] = Key{Sym: KeyF36} + keys["kf37"] = Key{Sym: KeyF37} + keys["kf38"] = Key{Sym: KeyF38} + keys["kf39"] = Key{Sym: KeyF39} + keys["kf40"] = Key{Sym: KeyF40} + keys["kf41"] = Key{Sym: KeyF41} + keys["kf42"] = Key{Sym: KeyF42} + keys["kf43"] = Key{Sym: KeyF43} + keys["kf44"] = Key{Sym: KeyF44} + keys["kf45"] = Key{Sym: KeyF45} + keys["kf46"] = Key{Sym: KeyF46} + keys["kf47"] = Key{Sym: KeyF47} + keys["kf48"] = Key{Sym: KeyF48} + keys["kf49"] = Key{Sym: KeyF49} + keys["kf50"] = Key{Sym: KeyF50} + keys["kf51"] = Key{Sym: KeyF51} + keys["kf52"] = Key{Sym: KeyF52} + keys["kf53"] = Key{Sym: KeyF53} + keys["kf54"] = Key{Sym: KeyF54} + keys["kf55"] = Key{Sym: KeyF55} + keys["kf56"] = Key{Sym: KeyF56} + keys["kf57"] = Key{Sym: KeyF57} + keys["kf58"] = Key{Sym: KeyF58} + keys["kf59"] = Key{Sym: KeyF59} + keys["kf60"] = Key{Sym: KeyF60} + keys["kf61"] = Key{Sym: KeyF61} + keys["kf62"] = Key{Sym: KeyF62} + keys["kf63"] = Key{Sym: KeyF63} + } + + return keys +} diff --git a/tty.go b/tty.go index ed469ad43c..7d4bdf332c 100644 --- a/tty.go +++ b/tty.go @@ -1,9 +1,11 @@ package tea import ( + "context" "errors" "fmt" "io" + "strings" "time" "github.com/charmbracelet/x/term" @@ -65,24 +67,164 @@ func (p *Program) restoreInput() error { return nil } -// initCancelReader (re)commences reading inputs. -func (p *Program) initCancelReader() error { - var err error - p.cancelReader, err = newInputReader(p.input) +// initInputReader (re)commences reading inputs. +func (p *Program) initInputReader() error { + var term string + for i := len(p.environ) - 1; i >= 0; i-- { + // We iterate backwards to find the last TERM variable set in the + // environment. This is because the last one is the one that will be + // used by the terminal. + parts := strings.SplitN(p.environ[i], "=", 2) + if len(parts) == 2 && parts[0] == "TERM" { + term = parts[1] + break + } + } + + // Initialize the input reader. + // This need to be done after the terminal has been initialized and set to + // raw mode. + // On Windows, this will change the console mode to enable mouse and window + // events. + var flags int // TODO: make configurable through environment variables? + drv, err := newDriver(p.input, term, flags) if err != nil { - return fmt.Errorf("error creating cancelreader: %w", err) + return err } + p.inputReader = drv p.readLoopDone = make(chan struct{}) go p.readLoop() return nil } +func readInputs(ctx context.Context, msgs chan<- Msg, reader *driver) error { + for { + events, err := reader.ReadEvents() + if err != nil { + return err + } + + for _, msg := range events { + incomingMsgs := []Msg{msg} + + // We need to translate new e types to deprecated ones to keep + // compatibility. + switch e := msg.(type) { + case PasteMsg: + var k KeyMsg + k.Paste = true + k.Runes = []rune(e) + incomingMsgs = append(incomingMsgs, k) + case KeyPressMsg: + k := KeyMsg{ + Alt: e.Mod.HasAlt(), + Runes: e.Runes, + Type: e.Sym, + } + + // Backwards compatibility for ctrl- and shift- keys + switch { + case e.Mod.HasCtrl() && e.Mod.HasShift(): + switch e.Sym { + case KeyUp, KeyDown, KeyRight, KeyLeft: + k.Runes = nil + k.Type = KeyCtrlShiftUp - e.Sym + KeyUp + case KeyHome, KeyEnd: + k.Runes = nil + k.Type = KeyCtrlShiftHome - e.Sym + KeyHome + } + case e.Mod.HasCtrl(): + switch e.Sym { + case KeyNone: // KeyRunes + switch r := e.Rune(); r { + case ' ': + k.Runes = nil + k.Type = KeyCtrlAt + case '[', '\\', ']', '^', '_': + k.Runes = nil + k.Type = KeyCtrlOpenBracket - KeyType(r) + '[' + case 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z': + k.Runes = nil + k.Type = KeyCtrlA - KeyType(r) + 'a' + case '?': + k.Runes = nil + k.Type = KeyCtrlQuestionMark + } + case KeyPgUp, KeyPgDown, KeyHome, KeyEnd: + k.Runes = nil + k.Type = KeyCtrlPgUp - e.Sym + KeyPgUp + case KeyUp, KeyDown, KeyRight, KeyLeft: + k.Runes = nil + k.Type = KeyCtrlUp - e.Sym + KeyUp + } + case e.Mod.HasShift(): + switch e.Sym { + case KeyTab: + k.Runes = nil + k.Type = KeyShiftTab + case KeyUp, KeyDown, KeyRight, KeyLeft: + k.Runes = nil + k.Type = KeyShiftUp - e.Sym + KeyUp + k.Runes = nil + case KeyHome, KeyEnd: + k.Runes = nil + k.Type = KeyShiftHome - e.Sym + KeyHome + } + } + + switch k.Type { + case KeyNone: // KeyRunes + if len(k.Runes) > 0 { + incomingMsgs = append(incomingMsgs, k) + } + default: + incomingMsgs = append(incomingMsgs, k) + } + case MouseClickMsg: + m := toMouseMsg(Mouse(e)) + m.Action = MouseActionPress + m.Type = e.Button + incomingMsgs = append(incomingMsgs, m) + case MouseReleaseMsg: + m := toMouseMsg(Mouse(e)) + m.Action = MouseActionRelease + m.Type = MouseRelease + incomingMsgs = append(incomingMsgs, m) + case MouseWheelMsg: + m := toMouseMsg(Mouse(e)) + m.Action = MouseActionPress + m.Type = e.Button + incomingMsgs = append(incomingMsgs, m) + case MouseMotionMsg: + m := toMouseMsg(Mouse(e)) + m.Action = MouseActionMotion + m.Type = MouseMotion + incomingMsgs = append(incomingMsgs, m) + } + + for _, m := range incomingMsgs { + select { + case msgs <- m: + case <-ctx.Done(): + err := ctx.Err() + if err != nil { + err = fmt.Errorf("found context error while reading input: %w", err) + } + return err + } + } + } + } +} + func (p *Program) readLoop() { defer close(p.readLoopDone) - err := readInputs(p.ctx, p.msgs, p.cancelReader) + err := readInputs(p.ctx, p.msgs, p.inputReader) if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) { select { case <-p.ctx.Done(): diff --git a/tutorials/go.mod b/tutorials/go.mod index 2b31c4f4f7..fa326824b2 100644 --- a/tutorials/go.mod +++ b/tutorials/go.mod @@ -5,20 +5,18 @@ go 1.18 require github.com/charmbracelet/bubbletea v0.25.0 require ( - github.com/charmbracelet/x/ansi v0.1.1 // indirect + github.com/charmbracelet/x/ansi v0.1.4 // indirect github.com/charmbracelet/x/input v0.1.0 // indirect github.com/charmbracelet/x/term v0.1.1 // indirect github.com/charmbracelet/x/windows v0.1.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect ) replace github.com/charmbracelet/bubbletea => ../ diff --git a/tutorials/go.sum b/tutorials/go.sum index 1c500b5377..afe8f921d0 100644 --- a/tutorials/go.sum +++ b/tutorials/go.sum @@ -1,5 +1,5 @@ -github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= -github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= +github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= @@ -8,8 +8,6 @@ github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wp github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= @@ -22,10 +20,8 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc 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-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/win32input.go b/win32input.go new file mode 100644 index 0000000000..5d8b9902c7 --- /dev/null +++ b/win32input.go @@ -0,0 +1,240 @@ +package tea + +import ( + "unicode" + + "github.com/erikgeiser/coninput" +) + +func parseWin32InputKeyEvent(vkc coninput.VirtualKeyCode, _ coninput.VirtualKeyCode, r rune, keyDown bool, cks coninput.ControlKeyState, repeatCount uint16) Msg { + var key Key + isCtrl := cks.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED) + switch vkc { + case coninput.VK_SHIFT: + // We currently ignore these keys when they are pressed alone. + return nil + case coninput.VK_MENU: + if cks.Contains(coninput.LEFT_ALT_PRESSED) { + key = Key{Sym: KeyLeftAlt} + } else if cks.Contains(coninput.RIGHT_ALT_PRESSED) { + key = Key{Sym: KeyRightAlt} + } else if !keyDown { + return nil + } + case coninput.VK_CONTROL: + if cks.Contains(coninput.LEFT_CTRL_PRESSED) { + key = Key{Sym: KeyLeftCtrl} + } else if cks.Contains(coninput.RIGHT_CTRL_PRESSED) { + key = Key{Sym: KeyRightCtrl} + } else if !keyDown { + return nil + } + case coninput.VK_CAPITAL: + key = Key{Sym: KeyCapsLock} + default: + var ok bool + key, ok = vkKeyEvent[vkc] + if !ok { + if isCtrl { + key = vkCtrlRune(key, r, vkc) + } else { + key = Key{Runes: []rune{r}} + } + } + } + + if isCtrl { + key.Mod |= ModCtrl + } + if cks.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED) { + key.Mod |= ModAlt + } + if cks.Contains(coninput.SHIFT_PRESSED) { + key.Mod |= ModShift + } + if cks.Contains(coninput.CAPSLOCK_ON) { + key.Mod |= ModCapsLock + } + if cks.Contains(coninput.NUMLOCK_ON) { + key.Mod |= ModNumLock + } + if cks.Contains(coninput.SCROLLLOCK_ON) { + key.Mod |= ModScrollLock + } + + // Use the unshifted key + if cks.Contains(coninput.SHIFT_PRESSED ^ coninput.CAPSLOCK_ON) { + key.altRune = unicode.ToUpper(key.Rune()) + } else { + key.altRune = unicode.ToLower(key.Rune()) + } + + var e Msg = KeyPressMsg(key) + key.IsRepeat = repeatCount > 1 + if !keyDown { + e = KeyReleaseMsg(key) + } + + if repeatCount <= 1 { + return e + } + + var kevents []Msg + for i := 0; i < int(repeatCount); i++ { + kevents = append(kevents, e) + } + + return multiMsg(kevents) +} + +var vkKeyEvent = map[coninput.VirtualKeyCode]Key{ + coninput.VK_RETURN: {Sym: KeyEnter}, + coninput.VK_BACK: {Sym: KeyBackspace}, + coninput.VK_TAB: {Sym: KeyTab}, + coninput.VK_ESCAPE: {Sym: KeyEscape}, + coninput.VK_SPACE: {Sym: KeySpace, Runes: []rune{' '}}, + coninput.VK_UP: {Sym: KeyUp}, + coninput.VK_DOWN: {Sym: KeyDown}, + coninput.VK_RIGHT: {Sym: KeyRight}, + coninput.VK_LEFT: {Sym: KeyLeft}, + coninput.VK_HOME: {Sym: KeyHome}, + coninput.VK_END: {Sym: KeyEnd}, + coninput.VK_PRIOR: {Sym: KeyPgUp}, + coninput.VK_NEXT: {Sym: KeyPgDown}, + coninput.VK_DELETE: {Sym: KeyDelete}, + coninput.VK_SELECT: {Sym: KeySelect}, + coninput.VK_SNAPSHOT: {Sym: KeyPrintScreen}, + coninput.VK_INSERT: {Sym: KeyInsert}, + coninput.VK_LWIN: {Sym: KeyLeftSuper}, + coninput.VK_RWIN: {Sym: KeyRightSuper}, + coninput.VK_APPS: {Sym: KeyMenu}, + coninput.VK_NUMPAD0: {Sym: KeyKp0}, + coninput.VK_NUMPAD1: {Sym: KeyKp1}, + coninput.VK_NUMPAD2: {Sym: KeyKp2}, + coninput.VK_NUMPAD3: {Sym: KeyKp3}, + coninput.VK_NUMPAD4: {Sym: KeyKp4}, + coninput.VK_NUMPAD5: {Sym: KeyKp5}, + coninput.VK_NUMPAD6: {Sym: KeyKp6}, + coninput.VK_NUMPAD7: {Sym: KeyKp7}, + coninput.VK_NUMPAD8: {Sym: KeyKp8}, + coninput.VK_NUMPAD9: {Sym: KeyKp9}, + coninput.VK_MULTIPLY: {Sym: KeyKpMultiply}, + coninput.VK_ADD: {Sym: KeyKpPlus}, + coninput.VK_SEPARATOR: {Sym: KeyKpComma}, + coninput.VK_SUBTRACT: {Sym: KeyKpMinus}, + coninput.VK_DECIMAL: {Sym: KeyKpDecimal}, + coninput.VK_DIVIDE: {Sym: KeyKpDivide}, + coninput.VK_F1: {Sym: KeyF1}, + coninput.VK_F2: {Sym: KeyF2}, + coninput.VK_F3: {Sym: KeyF3}, + coninput.VK_F4: {Sym: KeyF4}, + coninput.VK_F5: {Sym: KeyF5}, + coninput.VK_F6: {Sym: KeyF6}, + coninput.VK_F7: {Sym: KeyF7}, + coninput.VK_F8: {Sym: KeyF8}, + coninput.VK_F9: {Sym: KeyF9}, + coninput.VK_F10: {Sym: KeyF10}, + coninput.VK_F11: {Sym: KeyF11}, + coninput.VK_F12: {Sym: KeyF12}, + coninput.VK_F13: {Sym: KeyF13}, + coninput.VK_F14: {Sym: KeyF14}, + coninput.VK_F15: {Sym: KeyF15}, + coninput.VK_F16: {Sym: KeyF16}, + coninput.VK_F17: {Sym: KeyF17}, + coninput.VK_F18: {Sym: KeyF18}, + coninput.VK_F19: {Sym: KeyF19}, + coninput.VK_F20: {Sym: KeyF20}, + coninput.VK_F21: {Sym: KeyF21}, + coninput.VK_F22: {Sym: KeyF22}, + coninput.VK_F23: {Sym: KeyF23}, + coninput.VK_F24: {Sym: KeyF24}, + coninput.VK_NUMLOCK: {Sym: KeyNumLock}, + coninput.VK_SCROLL: {Sym: KeyScrollLock}, + coninput.VK_LSHIFT: {Sym: KeyLeftShift}, + coninput.VK_RSHIFT: {Sym: KeyRightShift}, + coninput.VK_LCONTROL: {Sym: KeyLeftCtrl}, + coninput.VK_RCONTROL: {Sym: KeyRightCtrl}, + coninput.VK_LMENU: {Sym: KeyLeftAlt}, + coninput.VK_RMENU: {Sym: KeyRightAlt}, + coninput.VK_OEM_4: {Runes: []rune{'['}}, + // TODO: add more keys +} + +func vkCtrlRune(k Key, r rune, kc coninput.VirtualKeyCode) Key { + switch r { + case '@': + k.Runes = []rune{'@'} + case '\x01': + k.Runes = []rune{'a'} + case '\x02': + k.Runes = []rune{'b'} + case '\x03': + k.Runes = []rune{'c'} + case '\x04': + k.Runes = []rune{'d'} + case '\x05': + k.Runes = []rune{'e'} + case '\x06': + k.Runes = []rune{'f'} + case '\a': + k.Runes = []rune{'g'} + case '\b': + k.Runes = []rune{'h'} + case '\t': + k.Runes = []rune{'i'} + case '\n': + k.Runes = []rune{'j'} + case '\v': + k.Runes = []rune{'k'} + case '\f': + k.Runes = []rune{'l'} + case '\r': + k.Runes = []rune{'m'} + case '\x0e': + k.Runes = []rune{'n'} + case '\x0f': + k.Runes = []rune{'o'} + case '\x10': + k.Runes = []rune{'p'} + case '\x11': + k.Runes = []rune{'q'} + case '\x12': + k.Runes = []rune{'r'} + case '\x13': + k.Runes = []rune{'s'} + case '\x14': + k.Runes = []rune{'t'} + case '\x15': + k.Runes = []rune{'u'} + case '\x16': + k.Runes = []rune{'v'} + case '\x17': + k.Runes = []rune{'w'} + case '\x18': + k.Runes = []rune{'x'} + case '\x19': + k.Runes = []rune{'y'} + case '\x1a': + k.Runes = []rune{'z'} + case '\x1b': + k.Runes = []rune{']'} + case '\x1c': + k.Runes = []rune{'\\'} + case '\x1f': + k.Runes = []rune{'_'} + } + + switch kc { + case coninput.VK_OEM_4: + k.Runes = []rune{'['} + } + + // https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes + if len(k.Runes) == 0 && + (kc >= 0x30 && kc <= 0x39) || + (kc >= 0x41 && kc <= 0x5a) { + k.Runes = []rune{rune(kc)} + } + + return k +} diff --git a/xterm.go b/xterm.go new file mode 100644 index 0000000000..ca2e37104e --- /dev/null +++ b/xterm.go @@ -0,0 +1,40 @@ +package tea + +import ( + "github.com/charmbracelet/x/ansi" +) + +func parseXTermModifyOtherKeys(csi *ansi.CsiSequence) Msg { + // XTerm modify other keys starts with ESC [ 27 ; ; ~ + mod := KeyMod(csi.Param(1) - 1) + r := rune(csi.Param(2)) + + switch r { + case ansi.BS: + return KeyPressMsg{Mod: mod, Sym: KeyBackspace} + case ansi.HT: + return KeyPressMsg{Mod: mod, Sym: KeyTab} + case ansi.CR: + return KeyPressMsg{Mod: mod, Sym: KeyEnter} + case ansi.ESC: + return KeyPressMsg{Mod: mod, Sym: KeyEscape} + case ansi.DEL: + return KeyPressMsg{Mod: mod, Sym: KeyBackspace} + } + + // CSI 27 ; ; ~ keys defined in XTerm modifyOtherKeys + return KeyPressMsg{ + Mod: mod, + Runes: []rune{r}, + } +} + +// 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 From 448eb829daa1d9d6ba14ecfffb3a1d9b011c5b8f Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 12 Aug 2024 17:33:57 -0400 Subject: [PATCH 02/18] fix(ci): skip CI for examples/tutorials running go1.18 --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fabbf102f3..7f82f8c43a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,16 +31,19 @@ jobs: run: go test ./... - name: Build examples + if: ${{ matrix.go-version != '~1.18' }} run: | go mod tidy go build -v ./... working-directory: ./examples - name: Test examples + if: ${{ matrix.go-version != '~1.18' }} run: go test -v ./... working-directory: ./examples - name: Build tutorials + if: ${{ matrix.go-version != '~1.18' }} run: | go mod tidy go build -v ./... From cb37f8847e86f7493c9d6dab3ab26531b65091c8 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 13 Aug 2024 11:00:57 -0400 Subject: [PATCH 03/18] fix: rename events to messages --- clipboard.go | 10 ++++++---- color.go | 29 +++++++++++++++++------------ cursor.go | 4 ++-- da1.go | 3 ++- mode.go | 4 ++-- parse.go | 22 +++++++++++----------- parse_test.go | 4 ++-- paste.go | 8 ++++---- xterm.go | 6 ++++-- 9 files changed, 50 insertions(+), 40 deletions(-) diff --git a/clipboard.go b/clipboard.go index 619844485a..9963b34b5e 100644 --- a/clipboard.go +++ b/clipboard.go @@ -1,9 +1,11 @@ package tea -// ClipboardEvent is a clipboard read event. -type ClipboardEvent string +// ClipboardMsg is a clipboard read message event. +// This message is emitted when a terminal receives an OSC52 clipboard read +// message event. +type ClipboardMsg string -// String returns the string representation of the clipboard event. -func (e ClipboardEvent) String() string { +// String returns the string representation of the clipboard message. +func (e ClipboardMsg) String() string { return string(e) } diff --git a/color.go b/color.go index 20dad064f3..0b49185205 100644 --- a/color.go +++ b/color.go @@ -7,27 +7,32 @@ import ( "strings" ) -// ForegroundColorEvent represents a foreground color change event. -type ForegroundColorEvent struct{ color.Color } +// ForegroundColorMsg represents a foreground color message. +// This message is emitted when the program requests the terminal foreground +// color. +type ForegroundColorMsg struct{ color.Color } -// String implements fmt.Stringer. -func (e ForegroundColorEvent) String() string { +// String returns the hex representation of the color. +func (e ForegroundColorMsg) String() string { return colorToHex(e) } -// BackgroundColorEvent represents a background color change event. -type BackgroundColorEvent struct{ color.Color } +// BackgroundColorMsg represents a background color message. +// This message is emitted when the program requests the terminal background +// color. +type BackgroundColorMsg struct{ color.Color } -// String implements fmt.Stringer. -func (e BackgroundColorEvent) String() string { +// String returns the hex representation of the color. +func (e BackgroundColorMsg) String() string { return colorToHex(e) } -// CursorColorEvent represents a cursor color change event. -type CursorColorEvent struct{ color.Color } +// CursorColorMsg represents a cursor color change message. +// This message is emitted when the program requests the terminal cursor color. +type CursorColorMsg struct{ color.Color } -// String implements fmt.Stringer. -func (e CursorColorEvent) String() string { +// String returns the hex representation of the color. +func (e CursorColorMsg) String() string { return colorToHex(e) } diff --git a/cursor.go b/cursor.go index b231d46fa4..17a580f327 100644 --- a/cursor.go +++ b/cursor.go @@ -1,7 +1,7 @@ package tea -// CursorPositionEvent represents a cursor position event. -type CursorPositionEvent struct { +// CursorPositionMsg is a message that represents the terminal cursor position. +type CursorPositionMsg struct { // Row is the row number. Row int diff --git a/da1.go b/da1.go index 02cf89157f..dc42ee617f 100644 --- a/da1.go +++ b/da1.go @@ -2,7 +2,8 @@ package tea import "github.com/charmbracelet/x/ansi" -// PrimaryDeviceAttributesMsg represents a primary device attributes message. +// PrimaryDeviceAttributesMsg is a message that represents the terminal primary +// device attributes. type PrimaryDeviceAttributesMsg []uint func parsePrimaryDevAttrs(csi *ansi.CsiSequence) Msg { diff --git a/mode.go b/mode.go index bee0e3b3ef..5dfcbd0621 100644 --- a/mode.go +++ b/mode.go @@ -1,9 +1,9 @@ package tea -// ReportModeEvent represents a report mode event for sequence DECRPM. +// ReportModeMsg is a message that represents a mode report event (DECRPM). // // See: https://vt100.net/docs/vt510-rm/DECRPM.html -type ReportModeEvent struct { +type ReportModeMsg struct { // Mode is the mode number. Mode int diff --git a/parse.go b/parse.go index 407faf491d..c3df4897e2 100644 --- a/parse.go +++ b/parse.go @@ -246,7 +246,7 @@ func parseCsi(b []byte) (int, Msg) { if paramsLen != 2 { return i, UnknownMsg(b[:i]) } - return i, ReportModeEvent{Mode: csi.Param(0), Value: csi.Param(1)} + return i, ReportModeMsg{Mode: csi.Param(0), Value: csi.Param(1)} } case 'c': // Primary Device Attributes @@ -260,7 +260,7 @@ func parseCsi(b []byte) (int, Msg) { // This report may return a third parameter representing the page // number, but we don't really need it. if paramsLen >= 2 { - return i, CursorPositionEvent{Row: csi.Param(0), Column: csi.Param(1)} + return i, CursorPositionMsg{Row: csi.Param(0), Column: csi.Param(1)} } } return i, UnknownMsg(b[:i]) @@ -283,7 +283,7 @@ func parseCsi(b []byte) (int, Msg) { return i, UnknownMsg(b[:i]) } - return i, ModifyOtherKeysEvent(csi.Param(1)) + return i, ModifyOtherKeysMsg(csi.Param(1)) default: return i, UnknownMsg(b[:i]) } @@ -313,7 +313,7 @@ func parseCsi(b []byte) (int, Msg) { // For a non ambiguous cursor position report, use // [ansi.RequestExtendedCursorPosition] (DECXCPR) instead. if csi.Param(0) != 1 { - return i, CursorPositionEvent{Row: csi.Param(0), Column: csi.Param(1)} + return i, CursorPositionMsg{Row: csi.Param(0), Column: csi.Param(1)} } fallthrough @@ -353,7 +353,7 @@ func parseCsi(b []byte) (int, Msg) { if paramsLen != 2 { return i, UnknownMsg(b[:i]) } - return i, ReportModeEvent{Mode: csi.Param(0), Value: csi.Param(1)} + return i, ReportModeMsg{Mode: csi.Param(0), Value: csi.Param(1)} case 'u': // Kitty keyboard protocol & CSI u (fixterms) if paramsLen == 0 { @@ -599,22 +599,22 @@ func parseOsc(b []byte) (int, Msg) { data := string(b[start:end]) switch cmd { case 10: - return i, ForegroundColorEvent{xParseColor(data)} + return i, ForegroundColorMsg{xParseColor(data)} case 11: - return i, BackgroundColorEvent{xParseColor(data)} + return i, BackgroundColorMsg{xParseColor(data)} case 12: - return i, CursorColorEvent{xParseColor(data)} + return i, CursorColorMsg{xParseColor(data)} case 52: parts := strings.Split(data, ";") if len(parts) == 0 { - return i, ClipboardEvent("") + return i, ClipboardMsg("") } b64 := parts[len(parts)-1] bts, err := base64.StdEncoding.DecodeString(b64) if err != nil { - return i, ClipboardEvent("") + return i, ClipboardMsg("") } - return i, ClipboardEvent(bts) + return i, ClipboardMsg(bts) default: return i, UnknownMsg(b[:i]) } diff --git a/parse_test.go b/parse_test.go index 41b608dcaa..8165d687ce 100644 --- a/parse_test.go +++ b/parse_test.go @@ -15,9 +15,9 @@ func TestParseSequence_Events(t *testing.T) { KeyPressMsg{Runes: []rune{'s'}}, KeyPressMsg{Runes: []rune{'t'}}, KeyPressMsg{Runes: []rune{' '}, Sym: KeySpace, Mod: ModCtrl}, - ForegroundColorEvent{color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff}}, + ForegroundColorMsg{color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff}}, KeyPressMsg{Sym: KeyEscape, Mod: ModShift}, - ReportModeEvent{Mode: 1049, Value: 2}, + ReportModeMsg{Mode: 1049, Value: 2}, } for i := 0; len(input) != 0; i++ { if i >= len(want) { diff --git a/paste.go b/paste.go index efb53d6a5f..575f995a90 100644 --- a/paste.go +++ b/paste.go @@ -4,10 +4,10 @@ package tea // using bracketed-paste. type PasteMsg string -// PasteStartMsg is an message that is emitted when a terminal enters -// bracketed-paste mode. +// PasteStartMsg is an message that is emitted when the terminal starts the +// bracketed-paste text type PasteStartMsg struct{} -// PasteEvent is an message that is emitted when a terminal receives pasted -// text. +// PasteEndMsg is an message that is emitted when the terminal ends the +// bracketed-paste text. type PasteEndMsg struct{} diff --git a/xterm.go b/xterm.go index ca2e37104e..0c5706f500 100644 --- a/xterm.go +++ b/xterm.go @@ -29,7 +29,9 @@ func parseXTermModifyOtherKeys(csi *ansi.CsiSequence) Msg { } } -// ModifyOtherKeysEvent represents a modifyOtherKeys event. +// ModifyOtherKeysMsg is a message that represents XTerm modifyOtherKeys +// report. Querying the terminal for the modifyOtherKeys mode will return a +// ModifyOtherKeysMsg message with the current mode set. // // 0: disable // 1: enable mode 1 @@ -37,4 +39,4 @@ func parseXTermModifyOtherKeys(csi *ansi.CsiSequence) Msg { // // 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 +type ModifyOtherKeysMsg uint8 From 7a49b33ec5b3d40ee00118a86f58e1a9cc7a9fae Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 13 Aug 2024 11:17:04 -0400 Subject: [PATCH 04/18] refactor: define renderer execute to write queries to the terminal This replaces enable/disable directives with exception to alt-screen and cursor visibility since those affect the renderer. --- nil_renderer.go | 10 +--------- nil_renderer_test.go | 5 +---- renderer.go | 32 ++------------------------------ screen.go | 12 +++++++----- tea.go | 32 +++++++++++++++++--------------- tty.go | 3 ++- 6 files changed, 30 insertions(+), 64 deletions(-) diff --git a/nil_renderer.go b/nil_renderer.go index f4a83b6bc4..f8e771bf24 100644 --- a/nil_renderer.go +++ b/nil_renderer.go @@ -13,13 +13,5 @@ func (n nilRenderer) enterAltScreen() {} func (n nilRenderer) exitAltScreen() {} func (n nilRenderer) showCursor() {} func (n nilRenderer) hideCursor() {} -func (n nilRenderer) enableMouseCellMotion() {} -func (n nilRenderer) disableMouseCellMotion() {} -func (n nilRenderer) enableMouseAllMotion() {} -func (n nilRenderer) disableMouseAllMotion() {} -func (n nilRenderer) enableBracketedPaste() {} -func (n nilRenderer) disableBracketedPaste() {} -func (n nilRenderer) enableMouseSGRMode() {} -func (n nilRenderer) disableMouseSGRMode() {} +func (n nilRenderer) execute(_ string) {} func (n nilRenderer) bracketedPasteActive() bool { return false } -func (n nilRenderer) setWindowTitle(_ string) {} diff --git a/nil_renderer_test.go b/nil_renderer_test.go index ab94e34d23..f3db19547c 100644 --- a/nil_renderer_test.go +++ b/nil_renderer_test.go @@ -17,8 +17,5 @@ func TestNilRenderer(t *testing.T) { r.clearScreen() r.showCursor() r.hideCursor() - r.enableMouseCellMotion() - r.disableMouseCellMotion() - r.enableMouseAllMotion() - r.disableMouseAllMotion() + r.execute("") } diff --git a/renderer.go b/renderer.go index de3936e73b..9727a9cfc2 100644 --- a/renderer.go +++ b/renderer.go @@ -36,40 +36,12 @@ type renderer interface { // Hide the cursor. hideCursor() - // enableMouseCellMotion enables mouse click, release, wheel and motion - // events if a mouse button is pressed (i.e., drag events). - enableMouseCellMotion() - - // disableMouseCellMotion disables Mouse Cell Motion tracking. - disableMouseCellMotion() - - // enableMouseAllMotion enables mouse click, release, wheel and motion - // events, regardless of whether a mouse button is pressed. Many modern - // terminals support this, but not all. - enableMouseAllMotion() - - // disableMouseAllMotion disables All Motion mouse tracking. - disableMouseAllMotion() - - // enableMouseSGRMode enables mouse extended mode (SGR). - enableMouseSGRMode() - - // disableMouseSGRMode disables mouse extended mode (SGR). - disableMouseSGRMode() - - // enableBracketedPaste enables bracketed paste, where characters - // inside the input are not interpreted when pasted as a whole. - enableBracketedPaste() - - // disableBracketedPaste disables bracketed paste. - disableBracketedPaste() - // bracketedPasteActive reports whether bracketed paste mode is // currently enabled. bracketedPasteActive() bool - // setWindowTitle sets the terminal window title. - setWindowTitle(string) + // execute writes a sequence to the terminal. + execute(string) } // repaintMsg forces a full repaint. diff --git a/screen.go b/screen.go index 6256c15920..1f52dc26c0 100644 --- a/screen.go +++ b/screen.go @@ -1,5 +1,7 @@ package tea +import "github.com/charmbracelet/x/ansi" + // WindowSizeMsg is used to report the terminal size. It's sent to Update once // initially and then on every terminal resize. Note that Windows does not // have support for reporting when resizes occur as it does not support the @@ -173,7 +175,7 @@ func (p *Program) ExitAltScreen() { // Deprecated: Use the WithMouseCellMotion ProgramOption instead. func (p *Program) EnableMouseCellMotion() { if p.renderer != nil { - p.renderer.enableMouseCellMotion() + p.renderer.execute(ansi.EnableMouseCellMotion) } else { p.startupOptions |= withMouseCellMotion } @@ -185,7 +187,7 @@ func (p *Program) EnableMouseCellMotion() { // Deprecated: The mouse will automatically be disabled when the program exits. func (p *Program) DisableMouseCellMotion() { if p.renderer != nil { - p.renderer.disableMouseCellMotion() + p.renderer.execute(ansi.DisableMouseCellMotion) } else { p.startupOptions &^= withMouseCellMotion } @@ -198,7 +200,7 @@ func (p *Program) DisableMouseCellMotion() { // Deprecated: Use the WithMouseAllMotion ProgramOption instead. func (p *Program) EnableMouseAllMotion() { if p.renderer != nil { - p.renderer.enableMouseAllMotion() + p.renderer.execute(ansi.EnableMouseAllMotion) } else { p.startupOptions |= withMouseAllMotion } @@ -210,7 +212,7 @@ func (p *Program) EnableMouseAllMotion() { // Deprecated: The mouse will automatically be disabled when the program exits. func (p *Program) DisableMouseAllMotion() { if p.renderer != nil { - p.renderer.disableMouseAllMotion() + p.renderer.execute(ansi.DisableMouseAllMotion) } else { p.startupOptions &^= withMouseAllMotion } @@ -221,7 +223,7 @@ func (p *Program) DisableMouseAllMotion() { // Deprecated: Use the SetWindowTitle command instead. func (p *Program) SetWindowTitle(title string) { if p.renderer != nil { - p.renderer.setWindowTitle(title) + p.renderer.execute(ansi.SetWindowTitle(title)) } else { p.startupTitle = title } diff --git a/tea.go b/tea.go index c0105b320b..f44056cbaa 100644 --- a/tea.go +++ b/tea.go @@ -21,6 +21,7 @@ import ( "sync/atomic" "syscall" + "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/term" "golang.org/x/sync/errgroup" ) @@ -320,9 +321,9 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} { } func (p *Program) disableMouse() { - p.renderer.disableMouseCellMotion() - p.renderer.disableMouseAllMotion() - p.renderer.disableMouseSGRMode() + p.renderer.execute(ansi.DisableMouseAllMotion) + p.renderer.execute(ansi.DisableMouseCellMotion) + p.renderer.execute(ansi.DisableMouseSgrExt) } // eventLoop is the central message loop. It receives and handles the default @@ -367,12 +368,12 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { case enableMouseCellMotionMsg, enableMouseAllMotionMsg: switch msg.(type) { case enableMouseCellMotionMsg: - p.renderer.enableMouseCellMotion() + p.renderer.execute(ansi.EnableMouseCellMotion) case enableMouseAllMotionMsg: - p.renderer.enableMouseAllMotion() + p.renderer.execute(ansi.EnableMouseAllMotion) } // mouse mode (1006) is a no-op if the terminal doesn't support it. - p.renderer.enableMouseSGRMode() + p.renderer.execute(ansi.EnableMouseSgrExt) case disableMouseMsg: p.disableMouse() @@ -384,10 +385,11 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { p.renderer.hideCursor() case enableBracketedPasteMsg: - p.renderer.enableBracketedPaste() + p.renderer.execute(ansi.EnableBracketedPaste) + p.renderer.execute(ansi.EnableMouseSgrExt) case disableBracketedPasteMsg: - p.renderer.disableBracketedPaste() + p.renderer.execute(ansi.DisableBracketedPaste) case execMsg: // NB: this blocks. @@ -526,20 +528,20 @@ func (p *Program) Run() (Model, error) { // Honor program startup options. if p.startupTitle != "" { - p.renderer.setWindowTitle(p.startupTitle) + p.renderer.execute(ansi.SetWindowTitle(p.startupTitle)) } if p.startupOptions&withAltScreen != 0 { p.renderer.enterAltScreen() } if p.startupOptions&withoutBracketedPaste == 0 { - p.renderer.enableBracketedPaste() + p.renderer.execute(ansi.EnableBracketedPaste) } if p.startupOptions&withMouseCellMotion != 0 { - p.renderer.enableMouseCellMotion() - p.renderer.enableMouseSGRMode() + p.renderer.execute(ansi.EnableMouseCellMotion) + p.renderer.execute(ansi.EnableMouseSgrExt) } else if p.startupOptions&withMouseAllMotion != 0 { - p.renderer.enableMouseAllMotion() - p.renderer.enableMouseSGRMode() + p.renderer.execute(ansi.EnableMouseAllMotion) + p.renderer.execute(ansi.EnableMouseSgrExt) } // Init the input reader and initial model. @@ -720,7 +722,7 @@ func (p *Program) RestoreTerminal() error { p.renderer.start() } if p.bpWasActive { - p.renderer.enableBracketedPaste() + p.renderer.execute(ansi.EnableBracketedPaste) } // If the output is a terminal, it may have been resized while another diff --git a/tty.go b/tty.go index 7d4bdf332c..b3fa80e788 100644 --- a/tty.go +++ b/tty.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/term" "github.com/muesli/cancelreader" ) @@ -37,7 +38,7 @@ func (p *Program) initTerminal() error { // Bubble Tea program. func (p *Program) restoreTerminalState() error { if p.renderer != nil { - p.renderer.disableBracketedPaste() + p.renderer.execute(ansi.DisableBracketedPaste) p.renderer.showCursor() p.disableMouse() From ea5460529abf017cb04a11f9b77482a21d7735a3 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 13 Aug 2024 11:21:11 -0400 Subject: [PATCH 05/18] feat: introduce ExecuteSequence to send the terminal arbitrary sequence --- renderer.go | 11 +++++++++++ tea.go | 3 +++ 2 files changed, 14 insertions(+) diff --git a/renderer.go b/renderer.go index 9727a9cfc2..6dd40edd38 100644 --- a/renderer.go +++ b/renderer.go @@ -46,3 +46,14 @@ type renderer interface { // repaintMsg forces a full repaint. type repaintMsg struct{} + +// executeSequenceMsg is a message that writes a sequence to the terminal. +type executeSequenceMsg string + +// ExecuteSequence is a command that writes a sequence to the terminal. Use +// this with extreme caution as it can mess up the terminal and your program. +func ExecuteSequence(seq string) Cmd { + return func() Msg { + return executeSequenceMsg(seq) + } +} diff --git a/tea.go b/tea.go index f44056cbaa..7195159755 100644 --- a/tea.go +++ b/tea.go @@ -391,6 +391,9 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { case disableBracketedPasteMsg: p.renderer.execute(ansi.DisableBracketedPaste) + case executeSequenceMsg: + p.renderer.execute(string(msg)) + case execMsg: // NB: this blocks. p.exec(msg.cmd, msg.fn) From 5cf8c29adcb2b524d79e90e96424f2d960b5136c Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 13 Aug 2024 11:37:18 -0400 Subject: [PATCH 06/18] feat(example): add example to query the terminal --- examples/query-term/main.go | 92 +++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 examples/query-term/main.go diff --git a/examples/query-term/main.go b/examples/query-term/main.go new file mode 100644 index 0000000000..bbd6fcd197 --- /dev/null +++ b/examples/query-term/main.go @@ -0,0 +1,92 @@ +// This example uses a textinput to send the terminal ANSI sequences to query +// it for capabilities. +package main + +import ( + "fmt" + "log" + "strconv" + "strings" + "unicode" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +func newModel() model { + ti := textinput.NewModel() + ti.Focus() + ti.CharLimit = 156 + ti.Width = 20 + return model{input: ti} +} + +type model struct { + input textinput.Model + err error +} + +func (m model) Init() tea.Cmd { + return textinput.Blink +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyPressMsg: + m.err = nil + switch msg.String() { + case "ctrl+c": + return m, tea.Quit + case "enter": + // Write the sequence to the terminal + val := m.input.Value() + val = "\"" + val + "\"" + + // unescape the sequence + seq, err := strconv.Unquote(val) + if err != nil { + m.err = err + return m, nil + } + + if !strings.HasPrefix(seq, "\x1b") { + m.err = fmt.Errorf("sequence is not an ANSI escape sequence") + return m, nil + } + + // write the sequence to the terminal + return m, tea.ExecuteSequence(seq) + } + default: + typ := strings.TrimPrefix(fmt.Sprintf("%T", msg), "tea.") + if len(typ) > 0 && unicode.IsUpper(rune(typ[0])) { + // Only log messages that are exported types + cmds = append(cmds, tea.Printf("Received message: %T\n", msg)) + } + } + + var cmd tea.Cmd + m.input, cmd = m.input.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m model) View() string { + var s strings.Builder + s.WriteString(m.input.View()) + if m.err != nil { + s.WriteString("\n\nError: " + m.err.Error()) + } + s.WriteString("\n\nPress ctrl+c to quit, enter to write the sequence to terminal") + return s.String() +} + +func main() { + p := tea.NewProgram(newModel()) + if _, err := p.Run(); err != nil { + log.Fatal(err) + } +} From 76fc19dcb2c6efab763eee624bb385b213779fda Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 13 Aug 2024 11:43:09 -0400 Subject: [PATCH 07/18] feat: detect key input grapheme runes --- parse.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/parse.go b/parse.go index c3df4897e2..c5cd73fd16 100644 --- a/parse.go +++ b/parse.go @@ -3,11 +3,11 @@ package tea import ( "encoding/base64" "strings" - "unicode/utf8" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/ansi/parser" "github.com/erikgeiser/coninput" + "github.com/rivo/uniseg" ) // Flags to control the behavior of the parser. @@ -775,14 +775,8 @@ func parseApc(b []byte) (int, Msg) { } func parseUtf8(b []byte) (int, Msg) { - r, rw := utf8.DecodeRune(b) - if r <= ansi.US || r == ansi.DEL || r == ansi.SP { - // Control codes get handled by parseControl - return 1, parseControl(byte(r)) - } else if r == utf8.RuneError { - return 1, UnknownMsg(b[0]) - } - return rw, KeyPressMsg{Runes: []rune{r}} + cluster, _, _, _ := uniseg.FirstGraphemeCluster(b, -1) + return len(cluster), KeyPressMsg{Runes: []rune(string(cluster))} } func parseControl(b byte) Msg { From 88a5cd2192a3b2cd9efa0c4407be337a999875bc Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 13 Aug 2024 13:04:17 -0400 Subject: [PATCH 08/18] fix: parse invalid utf8 sequences --- parse.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/parse.go b/parse.go index c5cd73fd16..3051a5204e 100644 --- a/parse.go +++ b/parse.go @@ -3,6 +3,7 @@ package tea import ( "encoding/base64" "strings" + "unicode/utf8" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/ansi/parser" @@ -775,6 +776,23 @@ func parseApc(b []byte) (int, Msg) { } func parseUtf8(b []byte) (int, Msg) { + 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, parseControl(c) + } else if c > ansi.US && c < ansi.DEL { + // ASCII printable characters + return 1, KeyPressMsg{Runes: []rune{rune(c)}} + } + + if r, _ := utf8.DecodeRune(b); r == utf8.RuneError { + return 1, UnknownMsg(b[0]) + } + cluster, _, _, _ := uniseg.FirstGraphemeCluster(b, -1) return len(cluster), KeyPressMsg{Runes: []rune(string(cluster))} } From 7567352f1ad55193b0b8be989a0eb1068da4a379 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 13 Aug 2024 13:06:51 -0400 Subject: [PATCH 09/18] fix: mouse sequence enable/disable order --- tea.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tea.go b/tea.go index 7195159755..6cd0a8ba59 100644 --- a/tea.go +++ b/tea.go @@ -321,8 +321,8 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} { } func (p *Program) disableMouse() { - p.renderer.execute(ansi.DisableMouseAllMotion) p.renderer.execute(ansi.DisableMouseCellMotion) + p.renderer.execute(ansi.DisableMouseAllMotion) p.renderer.execute(ansi.DisableMouseSgrExt) } @@ -386,7 +386,6 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { case enableBracketedPasteMsg: p.renderer.execute(ansi.EnableBracketedPaste) - p.renderer.execute(ansi.EnableMouseSgrExt) case disableBracketedPasteMsg: p.renderer.execute(ansi.DisableBracketedPaste) From 3981b80fce82bf800906767e302fadef7d455821 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 13 Aug 2024 14:32:44 -0400 Subject: [PATCH 10/18] refactor: remove tea.ExecuteSequence --- examples/query-term/main.go | 7 ++++++- renderer.go | 11 ----------- tea.go | 3 --- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/examples/query-term/main.go b/examples/query-term/main.go index bbd6fcd197..b42f36be95 100644 --- a/examples/query-term/main.go +++ b/examples/query-term/main.go @@ -4,7 +4,9 @@ package main import ( "fmt" + "io" "log" + "os" "strconv" "strings" "unicode" @@ -57,7 +59,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // write the sequence to the terminal - return m, tea.ExecuteSequence(seq) + return m, func() tea.Msg { + io.WriteString(os.Stdout, seq) + return nil + } } default: typ := strings.TrimPrefix(fmt.Sprintf("%T", msg), "tea.") diff --git a/renderer.go b/renderer.go index 6dd40edd38..9727a9cfc2 100644 --- a/renderer.go +++ b/renderer.go @@ -46,14 +46,3 @@ type renderer interface { // repaintMsg forces a full repaint. type repaintMsg struct{} - -// executeSequenceMsg is a message that writes a sequence to the terminal. -type executeSequenceMsg string - -// ExecuteSequence is a command that writes a sequence to the terminal. Use -// this with extreme caution as it can mess up the terminal and your program. -func ExecuteSequence(seq string) Cmd { - return func() Msg { - return executeSequenceMsg(seq) - } -} diff --git a/tea.go b/tea.go index 6cd0a8ba59..551f7830b6 100644 --- a/tea.go +++ b/tea.go @@ -390,9 +390,6 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { case disableBracketedPasteMsg: p.renderer.execute(ansi.DisableBracketedPaste) - case executeSequenceMsg: - p.renderer.execute(string(msg)) - case execMsg: // NB: this blocks. p.exec(msg.cmd, msg.fn) From 0dd62108ee8a28efa08d05328f6070cca7683474 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 13 Aug 2024 14:43:00 -0400 Subject: [PATCH 11/18] fix: initialize the terminal before the renderer We need to initialize the tty and the input handler before writing to it. --- tea.go | 30 ++++++++++++++++-------------- tty.go | 7 +------ 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/tea.go b/tea.go index 551f7830b6..f0b0d86ed0 100644 --- a/tea.go +++ b/tea.go @@ -514,17 +514,28 @@ func (p *Program) Run() (Model, error) { }() } + // Check if output is a TTY before entering raw mode, hiding the cursor and + // so on. + if err := p.initTerminal(); err != nil { + return p.initialModel, err + } + // If no renderer is set use the standard one. if p.renderer == nil { p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor), p.fps) } - // Check if output is a TTY before entering raw mode, hiding the cursor and - // so on. - if err := p.initTerminal(); err != nil { - return p.initialModel, err + // Init the input reader and initial model. + model := p.initialModel + if p.input != nil { + if err := p.initInputReader(); err != nil { + return model, err + } } + // Hide the cursor before starting the renderer. + p.renderer.hideCursor() + // Honor program startup options. if p.startupTitle != "" { p.renderer.execute(ansi.SetWindowTitle(p.startupTitle)) @@ -543,14 +554,6 @@ func (p *Program) Run() (Model, error) { p.renderer.execute(ansi.EnableMouseSgrExt) } - // Init the input reader and initial model. - model := p.initialModel - if p.input != nil { - if err := p.initInputReader(); err != nil { - return model, err - } - } - // Start the renderer. p.renderer.start() @@ -719,8 +722,7 @@ func (p *Program) RestoreTerminal() error { } if p.renderer != nil { p.renderer.start() - } - if p.bpWasActive { + p.renderer.hideCursor() p.renderer.execute(ansi.EnableBracketedPaste) } diff --git a/tty.go b/tty.go index b3fa80e788..7d552ab64c 100644 --- a/tty.go +++ b/tty.go @@ -26,12 +26,7 @@ func (p *Program) suspend() { } func (p *Program) initTerminal() error { - if err := p.initInput(); err != nil { - return err - } - - p.renderer.hideCursor() - return nil + return p.initInput() } // restoreTerminalState restores the terminal to the state prior to running the From c2c195c51afa9cc25a10564482117149aa33b8bf Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 13 Aug 2024 15:21:43 -0400 Subject: [PATCH 12/18] fix: special case modified f3 key and cursor pos report A modified F3 key press has the same sequence as cursor position report. We need to handle that by reporting both messages and letting the program determine which one to listen for. On most cases, programs don't care about modified F3 key press since terminals tend to have different sequences for it. --- key_test.go | 21 +++++++++++++++++++-- parse.go | 29 ++++++++++++++++------------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/key_test.go b/key_test.go index f8fcd524cc..5903b92d47 100644 --- a/key_test.go +++ b/key_test.go @@ -9,6 +9,7 @@ import ( "io" "math/rand" "reflect" + "regexp" "runtime" "sort" "strings" @@ -63,12 +64,23 @@ type seqTest struct { msgs []Msg } +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 { - td = append(td, seqTest{[]byte(seq), []Msg{KeyPressMsg(key)}}) + k := KeyPressMsg(key) + st := seqTest{seq: []byte(seq), msgs: []Msg{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.msgs = []Msg{k, CursorPositionMsg{Row: 1, Column: int(key.Mod) + 1}} + } + td = append(td, st) } // Additional special cases. @@ -219,7 +231,12 @@ func TestParseSequence(t *testing.T) { buf := tc.seq for len(buf) > 0 { width, msg := parseSequence(buf) - events = append(events, msg) + switch msg := msg.(type) { + case multiMsg: + events = append(events, msg...) + default: + events = append(events, msg) + } buf = buf[width:] } if !reflect.DeepEqual(tc.msgs, events) { diff --git a/parse.go b/parse.go index 3051a5204e..23bcbd76d1 100644 --- a/parse.go +++ b/parse.go @@ -300,23 +300,26 @@ func parseCsi(b []byte) (int, Msg) { return i, BlurMsg{} case 'R': // Cursor position report OR modified F3 - if paramsLen == 0 { - return i, KeyPressMsg{Sym: KeyF3} - } else if paramsLen != 2 { - break + if paramsLen == 2 { + m := CursorPositionMsg{Row: csi.Param(0), Column: csi.Param(1)} + if csi.Param(0) == 1 && csi.Param(1)-1 <= int(ModMeta|ModShift|ModAlt|ModCtrl) { + // XXX: We cannot differentiate between cursor position report and + // CSI 1 ; 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, multiMsg{KeyPressMsg{Sym: KeyF3, Mod: KeyMod(csi.Param(1) - 1)}, m} + } + + return i, m } - // XXX: We cannot differentiate between cursor position report and - // CSI 1 ; R (which is modified F3) when the cursor is at the - // row 1. In this case, we report a modified F3 event since it's more - // likely to be the case than the cursor being at the first row. - // - // For a non ambiguous cursor position report, use - // [ansi.RequestExtendedCursorPosition] (DECXCPR) instead. - if csi.Param(0) != 1 { - return i, CursorPositionMsg{Row: csi.Param(0), Column: csi.Param(1)} + if paramsLen != 0 { + return i, UnknownMsg(b[:i]) } + // Unmodified key F3 (CSI R) fallthrough case 'a', 'b', 'c', 'd', 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'S', 'Z': var k KeyPressMsg From 4247aac6d7eb6d8c74653311897e427272b5c0de Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 13 Aug 2024 15:34:47 -0400 Subject: [PATCH 13/18] refactor: flatten csi sequence parsing --- parse.go | 94 +++++++++++++++++++------------------------------------- 1 file changed, 32 insertions(+), 62 deletions(-) diff --git a/parse.go b/parse.go index 23bcbd76d1..844c6230b2 100644 --- a/parse.go +++ b/parse.go @@ -236,64 +236,36 @@ func parseCsi(b []byte) (int, Msg) { i++ csi.Params = params[:paramsLen] - marker, cmd := csi.Marker(), csi.Command() - switch marker { - case '?': - switch cmd { - case 'y': - switch intermed { - case '$': - // Report Mode (DECRPM) - if paramsLen != 2 { - return i, UnknownMsg(b[:i]) - } - return i, ReportModeMsg{Mode: csi.Param(0), Value: csi.Param(1)} - } - case 'c': - // Primary Device Attributes - return i, parsePrimaryDevAttrs(&csi) - case 'u': - // Kitty keyboard flags - if param := csi.Param(0); param != -1 { - return i, KittyKeyboardMsg(param) - } - case 'R': - // This report may return a third parameter representing the page - // number, but we don't really need it. - if paramsLen >= 2 { - return i, CursorPositionMsg{Row: csi.Param(0), Column: csi.Param(1)} - } - } - return i, UnknownMsg(b[:i]) - case '<': - switch cmd { - case 'm', 'M': - // Handle SGR mouse - if paramsLen != 3 { - return i, UnknownMsg(b[:i]) - } + switch cmd := csi.Cmd; cmd { + case 'y' | '?'<= 2 { + return i, CursorPositionMsg{Row: csi.Param(0), Column: csi.Param(1)} + } + case 'm' | '<'<': - switch cmd { - case 'm': - // XTerm modifyOtherKeys - if paramsLen != 2 || csi.Param(0) != 4 { - return i, UnknownMsg(b[:i]) - } - + case 'm' | '>'< Date: Tue, 13 Aug 2024 17:09:54 -0400 Subject: [PATCH 14/18] refactor: bracketed-paste active state Move tracking bp to the program instead of the renderer. The renderer doesn't need to know about the state of bp and gains nothing from that information. --- nil_renderer.go | 25 ++++++++------- renderer.go | 4 --- standard_renderer.go | 73 -------------------------------------------- tea.go | 7 +++-- tty.go | 1 + 5 files changed, 18 insertions(+), 92 deletions(-) diff --git a/nil_renderer.go b/nil_renderer.go index f8e771bf24..19ad1c5a2b 100644 --- a/nil_renderer.go +++ b/nil_renderer.go @@ -2,16 +2,15 @@ package tea type nilRenderer struct{} -func (n nilRenderer) start() {} -func (n nilRenderer) stop() {} -func (n nilRenderer) kill() {} -func (n nilRenderer) write(_ string) {} -func (n nilRenderer) repaint() {} -func (n nilRenderer) clearScreen() {} -func (n nilRenderer) altScreen() bool { return false } -func (n nilRenderer) enterAltScreen() {} -func (n nilRenderer) exitAltScreen() {} -func (n nilRenderer) showCursor() {} -func (n nilRenderer) hideCursor() {} -func (n nilRenderer) execute(_ string) {} -func (n nilRenderer) bracketedPasteActive() bool { return false } +func (nilRenderer) start() {} +func (nilRenderer) stop() {} +func (nilRenderer) kill() {} +func (nilRenderer) write(string) {} +func (nilRenderer) repaint() {} +func (nilRenderer) clearScreen() {} +func (nilRenderer) altScreen() bool { return false } +func (nilRenderer) enterAltScreen() {} +func (nilRenderer) exitAltScreen() {} +func (nilRenderer) showCursor() {} +func (nilRenderer) hideCursor() {} +func (nilRenderer) execute(string) {} diff --git a/renderer.go b/renderer.go index 9727a9cfc2..a15f5ff69d 100644 --- a/renderer.go +++ b/renderer.go @@ -36,10 +36,6 @@ type renderer interface { // Hide the cursor. hideCursor() - // bracketedPasteActive reports whether bracketed paste mode is - // currently enabled. - bracketedPasteActive() bool - // execute writes a sequence to the terminal. execute(string) } diff --git a/standard_renderer.go b/standard_renderer.go index f81920de01..284ad32a92 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -44,9 +44,6 @@ type standardRenderer struct { // essentially whether or not we're using the full size of the terminal altScreenActive bool - // whether or not we're currently using bracketed paste - bpActive bool - // renderer dimensions; usually the size of the window width int height int @@ -393,76 +390,6 @@ func (r *standardRenderer) hideCursor() { r.execute(ansi.HideCursor) } -func (r *standardRenderer) enableMouseCellMotion() { - r.mtx.Lock() - defer r.mtx.Unlock() - - r.execute(ansi.EnableMouseCellMotion) -} - -func (r *standardRenderer) disableMouseCellMotion() { - r.mtx.Lock() - defer r.mtx.Unlock() - - r.execute(ansi.DisableMouseCellMotion) -} - -func (r *standardRenderer) enableMouseAllMotion() { - r.mtx.Lock() - defer r.mtx.Unlock() - - r.execute(ansi.EnableMouseAllMotion) -} - -func (r *standardRenderer) disableMouseAllMotion() { - r.mtx.Lock() - defer r.mtx.Unlock() - - r.execute(ansi.DisableMouseAllMotion) -} - -func (r *standardRenderer) enableMouseSGRMode() { - r.mtx.Lock() - defer r.mtx.Unlock() - - r.execute(ansi.EnableMouseSgrExt) -} - -func (r *standardRenderer) disableMouseSGRMode() { - r.mtx.Lock() - defer r.mtx.Unlock() - - r.execute(ansi.DisableMouseSgrExt) -} - -func (r *standardRenderer) enableBracketedPaste() { - r.mtx.Lock() - defer r.mtx.Unlock() - - r.execute(ansi.EnableBracketedPaste) - r.bpActive = true -} - -func (r *standardRenderer) disableBracketedPaste() { - r.mtx.Lock() - defer r.mtx.Unlock() - - r.execute(ansi.DisableBracketedPaste) - r.bpActive = false -} - -func (r *standardRenderer) bracketedPasteActive() bool { - r.mtx.Lock() - defer r.mtx.Unlock() - - return r.bpActive -} - -// setWindowTitle sets the terminal window title. -func (r *standardRenderer) setWindowTitle(title string) { - r.execute(ansi.SetWindowTitle(title)) -} - // setIgnoredLines specifies lines not to be touched by the standard Bubble Tea // renderer. func (r *standardRenderer) setIgnoredLines(from int, to int) { diff --git a/tea.go b/tea.go index f0b0d86ed0..d9b95a6d2f 100644 --- a/tea.go +++ b/tea.go @@ -166,7 +166,7 @@ type Program struct { altScreenWasActive bool ignoreSignals uint32 - bpWasActive bool // was the bracketed paste mode active before releasing the terminal? + bpActive bool // was the bracketed paste mode active before releasing the terminal? filter func(Model, Msg) Msg @@ -386,9 +386,11 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { case enableBracketedPasteMsg: p.renderer.execute(ansi.EnableBracketedPaste) + p.bpActive = true case disableBracketedPasteMsg: p.renderer.execute(ansi.DisableBracketedPaste) + p.bpActive = false case execMsg: // NB: this blocks. @@ -545,6 +547,7 @@ func (p *Program) Run() (Model, error) { } if p.startupOptions&withoutBracketedPaste == 0 { p.renderer.execute(ansi.EnableBracketedPaste) + p.bpActive = true } if p.startupOptions&withMouseCellMotion != 0 { p.renderer.execute(ansi.EnableMouseCellMotion) @@ -696,7 +699,6 @@ func (p *Program) ReleaseTerminal() error { if p.renderer != nil { p.renderer.stop() p.altScreenWasActive = p.renderer.altScreen() - p.bpWasActive = p.renderer.bracketedPasteActive() } return p.restoreTerminalState() @@ -724,6 +726,7 @@ func (p *Program) RestoreTerminal() error { p.renderer.start() p.renderer.hideCursor() p.renderer.execute(ansi.EnableBracketedPaste) + p.bpActive = true } // If the output is a terminal, it may have been resized while another diff --git a/tty.go b/tty.go index 7d552ab64c..304bb29166 100644 --- a/tty.go +++ b/tty.go @@ -34,6 +34,7 @@ func (p *Program) initTerminal() error { func (p *Program) restoreTerminalState() error { if p.renderer != nil { p.renderer.execute(ansi.DisableBracketedPaste) + p.bpActive = false p.renderer.showCursor() p.disableMouse() From f2bdd36fb2feb874216cd4ee03b0fa9011258035 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 14 Aug 2024 16:56:00 -0400 Subject: [PATCH 15/18] fix: use KeyRunes to indicate text input --- key.go | 45 ++++++++++++++++++++++++++++++++++++++++++--- key_deprecated.go | 5 +---- key_test.go | 38 +++++++++++++++++++------------------- kitty.go | 4 ++++ parse.go | 9 ++++++--- parse_test.go | 10 +++++----- 6 files changed, 77 insertions(+), 34 deletions(-) diff --git a/key.go b/key.go index 94d9b8aefd..2d478681d1 100644 --- a/key.go +++ b/key.go @@ -3,9 +3,16 @@ package tea // KeySym is a keyboard symbol. type KeySym int -// Key Symbol constants. +// Special key symbols. const ( - KeyNone KeySym = iota + // KeyRunes indicates that the key represents rune(s), like 'a', 'b', 'c', + // and so on. + KeyRunes KeySym = iota - 1 + + // KeyNone indicates that this key is not a special key. Combined with + // `len(k.Runes) > 0`, this indicates that the key represents rune(s) with + // modifiers like [ModCtrl], [ModAlt], [ModShift], and so on. + KeyNone // Special names in C0 @@ -177,7 +184,39 @@ const ( KeyIsoLevel5Shift ) -// Key represents a key event. +// Key contains information about a key or release. Keys are always sent to the +// program's update function. 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 msg := msg.(type) { +// case KeyPressMsg: +// switch msg.String() { +// case "enter": +// fmt.Println("you pressed enter!") +// case "a": +// fmt.Println("you pressed a!") +// } +// } +// +// // Switch on the key type (more foolproof) +// switch msg := msg.(type) { +// case KeyReleaseMsg: +// switch msg.Sym { +// case KeyEnter: +// fmt.Println("you pressed enter!") +// case KeyRunes: +// switch string(msg.Runes) { +// case "a": +// fmt.Println("you pressed a!") +// } +// } +// } +// +// Note that Key.Runes will always contain at least one character, so you can +// always safely call Key.Runes[0]. In most cases Key.Runes will only contain +// one character, though certain input method editors (most notably Chinese +// IMEs) can input multiple runes at once. type Key struct { // Sym is a special key, like enter, tab, backspace, and so on. Sym KeySym diff --git a/key_deprecated.go b/key_deprecated.go index f63b002583..f40c2a4905 100644 --- a/key_deprecated.go +++ b/key_deprecated.go @@ -97,7 +97,7 @@ type KeyType = KeySym // Control key aliases. const ( - KeyNull KeyType = -iota - 1 + KeyNull KeyType = -iota - 10 KeyBreak KeyCtrlAt // ctrl+@ @@ -159,9 +159,6 @@ const ( // Deprecated: Use KeyEscape instead. KeyEsc = KeyEscape - - // Deprecated: Use KeyNone instead. - KeyRunes = KeyNone ) // Mappings for control keys and other special keys to friendly consts. diff --git a/key_test.go b/key_test.go index 5903b92d47..ed04dafe5c 100644 --- a/key_test.go +++ b/key_test.go @@ -144,7 +144,7 @@ func TestParseSequence(t *testing.T) { seqTest{ []byte{'a'}, []Msg{ - KeyPressMsg{Runes: []rune{'a'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, }, }, seqTest{ @@ -156,16 +156,16 @@ func TestParseSequence(t *testing.T) { seqTest{ []byte{'a', 'a', 'a'}, []Msg{ - KeyPressMsg{Runes: []rune{'a'}}, - KeyPressMsg{Runes: []rune{'a'}}, - KeyPressMsg{Runes: []rune{'a'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, }, }, // Multi-byte rune. seqTest{ []byte("☃"), []Msg{ - KeyPressMsg{Runes: []rune{'☃'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'☃'}}, }, }, seqTest{ @@ -249,7 +249,7 @@ func TestParseSequence(t *testing.T) { func TestReadLongInput(t *testing.T) { expect := make([]Msg, 1000) for i := 0; i < 1000; i++ { - expect[i] = KeyPressMsg{Runes: []rune{'a'}} + expect[i] = KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}} } input := strings.Repeat("a", 1000) drv, err := newDriver(strings.NewReader(input), "dumb", 0) @@ -285,7 +285,7 @@ func TestReadInput(t *testing.T) { "a", []byte{'a'}, []Msg{ - KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, }, }, { @@ -299,17 +299,17 @@ func TestReadInput(t *testing.T) { "a alt+a", []byte{'a', '\x1b', 'a'}, []Msg{ - KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}}, - KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}, Mod: ModAlt}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'a'}, Mod: ModAlt}, }, }, { "a alt+a a", []byte{'a', '\x1b', 'a', 'a'}, []Msg{ - KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}}, - KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}, Mod: ModAlt}, - KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'a'}, Mod: ModAlt}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, }, }, { @@ -338,10 +338,10 @@ func TestReadInput(t *testing.T) { "a b c d", []byte{'a', 'b', 'c', 'd'}, []Msg{ - KeyPressMsg{Runes: []rune{'a'}}, - KeyPressMsg{Runes: []rune{'b'}}, - KeyPressMsg{Runes: []rune{'c'}}, - KeyPressMsg{Runes: []rune{'d'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'b'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'c'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'d'}}, }, }, { @@ -470,7 +470,7 @@ func TestReadInput(t *testing.T) { PasteStartMsg{}, PasteMsg("a b"), PasteEndMsg{}, - KeyPressMsg{Sym: KeyNone, Runes: []rune{'o'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'o'}}, }, }, { @@ -497,10 +497,10 @@ func TestReadInput(t *testing.T) { "a ?0xfe? b", []byte{'a', '\xfe', ' ', 'b'}, []Msg{ - KeyPressMsg{Sym: KeyNone, Runes: []rune{'a'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, UnknownMsg(rune(0xfe)), KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}}, - KeyPressMsg{Sym: KeyNone, Runes: []rune{'b'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'b'}}, }, }, } diff --git a/kitty.go b/kitty.go index 5365e29518..aae61d2427 100644 --- a/kitty.go +++ b/kitty.go @@ -223,6 +223,7 @@ func parseKittyKeyboard(csi *ansi.CsiSequence) Msg { r = utf8.RuneError } + key.Sym = KeyRunes key.Runes = []rune{r} // alternate key reporting @@ -257,6 +258,9 @@ func parseKittyKeyboard(csi *ansi.CsiSequence) Msg { mod := params[0] if mod > 1 { key.Mod = fromKittyMod(mod - 1) + if key.Sym == KeyRunes { + key.Sym = KeyNone + } } if len(params) > 1 { switch params[1] { diff --git a/parse.go b/parse.go index 844c6230b2..a140f09c16 100644 --- a/parse.go +++ b/parse.go @@ -125,8 +125,11 @@ func parseSequence(buf []byte) (n int, msg Msg) { return parseApc(buf) default: n, e := parseSequence(buf[1:]) - if k, ok := e.(KeyPressMsg); ok && !k.Mod.HasAlt() { + if k, ok := e.(KeyPressMsg); ok { k.Mod |= ModAlt + if k.Sym == KeyRunes { + k.Sym = KeyNone + } return n + 1, k } @@ -759,7 +762,7 @@ func parseUtf8(b []byte) (int, Msg) { return 1, parseControl(c) } else if c > ansi.US && c < ansi.DEL { // ASCII printable characters - return 1, KeyPressMsg{Runes: []rune{rune(c)}} + return 1, KeyPressMsg{Sym: KeyRunes, Runes: []rune{rune(c)}} } if r, _ := utf8.DecodeRune(b); r == utf8.RuneError { @@ -767,7 +770,7 @@ func parseUtf8(b []byte) (int, Msg) { } cluster, _, _, _ := uniseg.FirstGraphemeCluster(b, -1) - return len(cluster), KeyPressMsg{Runes: []rune(string(cluster))} + return len(cluster), KeyPressMsg{Sym: KeyRunes, Runes: []rune(string(cluster))} } func parseControl(b byte) Msg { diff --git a/parse_test.go b/parse_test.go index 8165d687ce..1f3ccfc6d5 100644 --- a/parse_test.go +++ b/parse_test.go @@ -10,11 +10,11 @@ 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") want := []Msg{ KeyPressMsg{Sym: KeyTab, Mod: ModShift | ModAlt}, - KeyPressMsg{Runes: []rune{'t'}}, - KeyPressMsg{Runes: []rune{'e'}}, - KeyPressMsg{Runes: []rune{'s'}}, - KeyPressMsg{Runes: []rune{'t'}}, - KeyPressMsg{Runes: []rune{' '}, Sym: KeySpace, Mod: ModCtrl}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'t'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'e'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'s'}}, + KeyPressMsg{Sym: KeyRunes, Runes: []rune{'t'}}, + KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}, Mod: ModCtrl}, ForegroundColorMsg{color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff}}, KeyPressMsg{Sym: KeyEscape, Mod: ModShift}, ReportModeMsg{Mode: 1049, Value: 2}, From eb2eee4d987964dbfd3fd6df07e962aeef0567fd Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Thu, 15 Aug 2024 13:52:03 -0400 Subject: [PATCH 16/18] refactor: rename KeySym to KeyType and make KeyRunes the default --- key.go | 57 ++++--- key_deprecated.go | 16 -- key_test.go | 88 +++++------ kitty.go | 9 +- parse.go | 107 ++++++------- parse_test.go | 14 +- table.go | 320 ++++++++++++++++++------------------- terminfo.go | 396 +++++++++++++++++++++++----------------------- tty.go | 24 +-- win32input.go | 146 ++++++++--------- xterm.go | 10 +- 11 files changed, 585 insertions(+), 602 deletions(-) diff --git a/key.go b/key.go index 2d478681d1..9997321037 100644 --- a/key.go +++ b/key.go @@ -1,18 +1,26 @@ package tea -// KeySym is a keyboard symbol. -type KeySym int +// KeyType indicates whether the key is a special key or runes. Special +// keys are things like KeyEnter, KeyBackspace, and so on. Runes keys are just +// regular characters like 'a', '你', 'ض', '🦄', and so on. +// +// k := Key{Type: KeyRunes, Runes: []rune{'A'}, Mod: ModShift} +// if k.Type == KeyRunes { +// +// fmt.Println(k.Runes) +// // Output: A +// +// fmt.Println(k.String()) +// // Output: shift+a +// +// } +type KeyType int // Special key symbols. const ( // KeyRunes indicates that the key represents rune(s), like 'a', 'b', 'c', // and so on. - KeyRunes KeySym = iota - 1 - - // KeyNone indicates that this key is not a special key. Combined with - // `len(k.Runes) > 0`, this indicates that the key represents rune(s) with - // modifiers like [ModCtrl], [ModAlt], [ModShift], and so on. - KeyNone + KeyRunes KeyType = iota // Special names in C0 @@ -218,8 +226,8 @@ const ( // one character, though certain input method editors (most notably Chinese // IMEs) can input multiple runes at once. type Key struct { - // Sym is a special key, like enter, tab, backspace, and so on. - Sym KeySym + // Type is a special key, like enter, tab, backspace, and so on. + Type KeyType // Runes contains the actual characters received. This usually has a length // of 1. Use [Rune()] to get the first key rune received. If the user @@ -315,22 +323,22 @@ func (k Key) Rune() rune { // "shift+ctrl+alt+a". func (k Key) String() string { var s string - if k.Mod.HasCtrl() && k.Sym != KeyLeftCtrl && k.Sym != KeyRightCtrl { + if k.Mod.HasCtrl() && k.Type != KeyLeftCtrl && k.Type != KeyRightCtrl { s += "ctrl+" } - if k.Mod.HasAlt() && k.Sym != KeyLeftAlt && k.Sym != KeyRightAlt { + if k.Mod.HasAlt() && k.Type != KeyLeftAlt && k.Type != KeyRightAlt { s += "alt+" } - if k.Mod.HasShift() && k.Sym != KeyLeftShift && k.Sym != KeyRightShift { + if k.Mod.HasShift() && k.Type != KeyLeftShift && k.Type != KeyRightShift { s += "shift+" } - if k.Mod.HasMeta() && k.Sym != KeyLeftMeta && k.Sym != KeyRightMeta { + if k.Mod.HasMeta() && k.Type != KeyLeftMeta && k.Type != KeyRightMeta { s += "meta+" } - if k.Mod.HasHyper() && k.Sym != KeyLeftHyper && k.Sym != KeyRightHyper { + if k.Mod.HasHyper() && k.Type != KeyLeftHyper && k.Type != KeyRightHyper { s += "hyper+" } - if k.Mod.HasSuper() && k.Sym != KeyLeftSuper && k.Sym != KeyRightSuper { + if k.Mod.HasSuper() && k.Type != KeyLeftSuper && k.Type != KeyRightSuper { s += "super+" } @@ -356,22 +364,21 @@ func (k Key) String() string { s += runeStr(k.Rune()) } } else { - s += k.Sym.String() + s += k.Type.String() } return s } -// String implements fmt.Stringer and prints the string representation of a of -// a Symbol key. -func (k KeySym) String() string { - s, ok := keySymString[k] - if !ok { - return "unknown" +// String returns the string representation of the key type. +func (k KeyType) String() string { + if s, ok := keyTypeString[k]; ok { + return s } - return s + return "" } -var keySymString = map[KeySym]string{ +var keyTypeString = map[KeyType]string{ + KeyRunes: "runes", KeyEnter: "enter", KeyTab: "tab", KeyBackspace: "backspace", diff --git a/key_deprecated.go b/key_deprecated.go index f40c2a4905..ab1c9de1a8 100644 --- a/key_deprecated.go +++ b/key_deprecated.go @@ -79,22 +79,6 @@ func (k KeyMsg) String() (str string) { return "" } -// KeyType indicates the key pressed, such as KeyEnter or KeyBreak or KeyCtrlC. -// All other keys will be type KeyRunes. To get the rune value, check the Rune -// method on a Key struct, or use the Key.String() method: -// -// k := Key{Type: KeyRunes, Runes: []rune{'a'}, Alt: true} -// if k.Type == KeyRunes { -// -// fmt.Println(k.Runes) -// // Output: a -// -// fmt.Println(k.String()) -// // Output: alt+a -// -// } -type KeyType = KeySym - // Control key aliases. const ( KeyNull KeyType = -iota - 10 diff --git a/key_test.go b/key_test.go index ed04dafe5c..d46da05379 100644 --- a/key_test.go +++ b/key_test.go @@ -24,7 +24,7 @@ var sequences = buildKeysTable(_FlagTerminfo, "dumb") func TestKeyString(t *testing.T) { t.Run("alt+space", func(t *testing.T) { - k := KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}, Mod: ModAlt} + k := KeyPressMsg{Type: KeySpace, Runes: []rune{' '}, Mod: ModAlt} if got := k.String(); got != "alt+space" { t.Fatalf(`expected a "alt+space ", got %q`, got) } @@ -38,8 +38,8 @@ func TestKeyString(t *testing.T) { }) t.Run("invalid", func(t *testing.T) { - k := KeyPressMsg{Sym: 99999} - if got := k.String(); got != "unknown" { + k := KeyPressMsg{Type: 99999} + if got := k.String(); got != "" { t.Fatalf(`expected a "unknown", got %q`, got) } }) @@ -53,7 +53,7 @@ func TestKeyTypeString(t *testing.T) { }) t.Run("invalid", func(t *testing.T) { - if got := KeySym(99999).String(); got != "unknown" { + if got := KeyType(99999).String(); got != "" { t.Fatalf(`expected a "unknown", got %q`, got) } }) @@ -96,14 +96,14 @@ func buildBaseSeqTests() []seqTest { seqTest{ []byte{' '}, []Msg{ - KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}}, + KeyPressMsg{Type: KeySpace, Runes: []rune{' '}}, }, }, // An escape character with the alt modifier. seqTest{ []byte{'\x1b', ' '}, []Msg{ - KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}, Mod: ModAlt}, + KeyPressMsg{Type: KeySpace, Runes: []rune{' '}, Mod: ModAlt}, }, }, ) @@ -144,7 +144,7 @@ func TestParseSequence(t *testing.T) { seqTest{ []byte{'a'}, []Msg{ - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'a'}}, }, }, seqTest{ @@ -156,16 +156,16 @@ func TestParseSequence(t *testing.T) { seqTest{ []byte{'a', 'a', 'a'}, []Msg{ - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'a'}}, }, }, // Multi-byte rune. seqTest{ []byte("☃"), []Msg{ - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'☃'}}, + KeyPressMsg{Runes: []rune{'☃'}}, }, }, seqTest{ @@ -178,7 +178,7 @@ func TestParseSequence(t *testing.T) { seqTest{ []byte{'\x1b'}, []Msg{ - KeyPressMsg{Sym: KeyEscape}, + KeyPressMsg{Type: KeyEscape}, }, }, seqTest{ @@ -196,13 +196,13 @@ func TestParseSequence(t *testing.T) { seqTest{ []byte{ansi.NUL}, []Msg{ - KeyPressMsg{Runes: []rune{' '}, Sym: KeySpace, Mod: ModCtrl}, + KeyPressMsg{Runes: []rune{' '}, Type: KeySpace, Mod: ModCtrl}, }, }, seqTest{ []byte{'\x1b', ansi.NUL}, []Msg{ - KeyPressMsg{Runes: []rune{' '}, Sym: KeySpace, Mod: ModCtrl | ModAlt}, + KeyPressMsg{Runes: []rune{' '}, Type: KeySpace, Mod: ModCtrl | ModAlt}, }, }, // C1 control characters. @@ -249,7 +249,7 @@ func TestParseSequence(t *testing.T) { func TestReadLongInput(t *testing.T) { expect := make([]Msg, 1000) for i := 0; i < 1000; i++ { - expect[i] = KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}} + expect[i] = KeyPressMsg{Runes: []rune{'a'}} } input := strings.Repeat("a", 1000) drv, err := newDriver(strings.NewReader(input), "dumb", 0) @@ -285,21 +285,21 @@ func TestReadInput(t *testing.T) { "a", []byte{'a'}, []Msg{ - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'a'}}, }, }, { "space", []byte{' '}, []Msg{ - KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}}, + KeyPressMsg{Type: KeySpace, Runes: []rune{' '}}, }, }, { "a alt+a", []byte{'a', '\x1b', 'a'}, []Msg{ - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'a'}}, KeyPressMsg{Runes: []rune{'a'}, Mod: ModAlt}, }, }, @@ -307,9 +307,9 @@ func TestReadInput(t *testing.T) { "a alt+a a", []byte{'a', '\x1b', 'a', 'a'}, []Msg{ - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'a'}}, KeyPressMsg{Runes: []rune{'a'}, Mod: ModAlt}, - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'a'}}, }, }, { @@ -331,24 +331,24 @@ func TestReadInput(t *testing.T) { "alt+a", []byte{byte(0x1b), 'a'}, []Msg{ - KeyPressMsg{Sym: KeyNone, Mod: ModAlt, Runes: []rune{'a'}}, + KeyPressMsg{Mod: ModAlt, Runes: []rune{'a'}}, }, }, { "a b c d", []byte{'a', 'b', 'c', 'd'}, []Msg{ - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'b'}}, - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'c'}}, - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'d'}}, + KeyPressMsg{Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'b'}}, + KeyPressMsg{Runes: []rune{'c'}}, + KeyPressMsg{Runes: []rune{'d'}}, }, }, { "up", []byte("\x1b[A"), []Msg{ - KeyPressMsg{Sym: KeyUp}, + KeyPressMsg{Type: KeyUp}, }, }, { @@ -373,26 +373,26 @@ func TestReadInput(t *testing.T) { "shift+tab", []byte{'\x1b', '[', 'Z'}, []Msg{ - KeyPressMsg{Sym: KeyTab, Mod: ModShift}, + KeyPressMsg{Type: KeyTab, Mod: ModShift}, }, }, { "enter", []byte{'\r'}, - []Msg{KeyPressMsg{Sym: KeyEnter}}, + []Msg{KeyPressMsg{Type: KeyEnter}}, }, { "alt+enter", []byte{'\x1b', '\r'}, []Msg{ - KeyPressMsg{Sym: KeyEnter, Mod: ModAlt}, + KeyPressMsg{Type: KeyEnter, Mod: ModAlt}, }, }, { "insert", []byte{'\x1b', '[', '2', '~'}, []Msg{ - KeyPressMsg{Sym: KeyInsert}, + KeyPressMsg{Type: KeyInsert}, }, }, { @@ -411,52 +411,52 @@ func TestReadInput(t *testing.T) { { "up", []byte{'\x1b', 'O', 'A'}, - []Msg{KeyPressMsg{Sym: KeyUp}}, + []Msg{KeyPressMsg{Type: KeyUp}}, }, { "down", []byte{'\x1b', 'O', 'B'}, - []Msg{KeyPressMsg{Sym: KeyDown}}, + []Msg{KeyPressMsg{Type: KeyDown}}, }, { "right", []byte{'\x1b', 'O', 'C'}, - []Msg{KeyPressMsg{Sym: KeyRight}}, + []Msg{KeyPressMsg{Type: KeyRight}}, }, { "left", []byte{'\x1b', 'O', 'D'}, - []Msg{KeyPressMsg{Sym: KeyLeft}}, + []Msg{KeyPressMsg{Type: KeyLeft}}, }, { "alt+enter", []byte{'\x1b', '\x0d'}, - []Msg{KeyPressMsg{Sym: KeyEnter, Mod: ModAlt}}, + []Msg{KeyPressMsg{Type: KeyEnter, Mod: ModAlt}}, }, { "alt+backspace", []byte{'\x1b', '\x7f'}, - []Msg{KeyPressMsg{Sym: KeyBackspace, Mod: ModAlt}}, + []Msg{KeyPressMsg{Type: KeyBackspace, Mod: ModAlt}}, }, { "ctrl+space", []byte{'\x00'}, - []Msg{KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}, Mod: ModCtrl}}, + []Msg{KeyPressMsg{Type: KeySpace, Runes: []rune{' '}, Mod: ModCtrl}}, }, { "ctrl+alt+space", []byte{'\x1b', '\x00'}, - []Msg{KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}, Mod: ModCtrl | ModAlt}}, + []Msg{KeyPressMsg{Type: KeySpace, Runes: []rune{' '}, Mod: ModCtrl | ModAlt}}, }, { "esc", []byte{'\x1b'}, - []Msg{KeyPressMsg{Sym: KeyEscape}}, + []Msg{KeyPressMsg{Type: KeyEscape}}, }, { "alt+esc", []byte{'\x1b', '\x1b'}, - []Msg{KeyPressMsg{Sym: KeyEscape, Mod: ModAlt}}, + []Msg{KeyPressMsg{Type: KeyEscape, Mod: ModAlt}}, }, { "a b o", @@ -470,7 +470,7 @@ func TestReadInput(t *testing.T) { PasteStartMsg{}, PasteMsg("a b"), PasteEndMsg{}, - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'o'}}, + KeyPressMsg{Runes: []rune{'o'}}, }, }, { @@ -497,10 +497,10 @@ func TestReadInput(t *testing.T) { "a ?0xfe? b", []byte{'a', '\xfe', ' ', 'b'}, []Msg{ - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'a'}}, + KeyPressMsg{Runes: []rune{'a'}}, UnknownMsg(rune(0xfe)), - KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}}, - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'b'}}, + KeyPressMsg{Type: KeySpace, Runes: []rune{' '}}, + KeyPressMsg{Runes: []rune{'b'}}, }, }, } diff --git a/kitty.go b/kitty.go index aae61d2427..ff6279d78d 100644 --- a/kitty.go +++ b/kitty.go @@ -36,7 +36,7 @@ func (e KittyKeyboardMsg) IsReportAssociatedKeys() bool { } // Kitty Clipboard Control Sequences -var kittyKeyMap = map[int]KeySym{ +var kittyKeyMap = map[int]KeyType{ ansi.BS: KeyBackspace, ansi.HT: KeyTab, ansi.CR: KeyEnter, @@ -216,14 +216,14 @@ func parseKittyKeyboard(csi *ansi.CsiSequence) Msg { if params := csi.Subparams(0); len(params) > 0 { code := params[0] if sym, ok := kittyKeyMap[code]; ok { - key.Sym = sym + key.Type = sym } else { r := rune(code) if !utf8.ValidRune(r) { r = utf8.RuneError } - key.Sym = KeyRunes + key.Type = KeyRunes key.Runes = []rune{r} // alternate key reporting @@ -258,9 +258,6 @@ func parseKittyKeyboard(csi *ansi.CsiSequence) Msg { mod := params[0] if mod > 1 { key.Mod = fromKittyMod(mod - 1) - if key.Sym == KeyRunes { - key.Sym = KeyNone - } } if len(params) > 1 { switch params[1] { diff --git a/parse.go b/parse.go index a140f09c16..49eae924fa 100644 --- a/parse.go +++ b/parse.go @@ -109,7 +109,7 @@ func parseSequence(buf []byte) (n int, msg Msg) { case ansi.ESC: if len(buf) == 1 { // Escape key - return 1, KeyPressMsg{Sym: KeyEscape} + return 1, KeyPressMsg{Type: KeyEscape} } switch b := buf[1]; b { @@ -127,15 +127,12 @@ func parseSequence(buf []byte) (n int, msg Msg) { n, e := parseSequence(buf[1:]) if k, ok := e.(KeyPressMsg); ok { k.Mod |= ModAlt - if k.Sym == KeyRunes { - k.Sym = KeyNone - } 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, KeyPressMsg{Sym: KeyEscape} + return 1, KeyPressMsg{Type: KeyEscape} } case ansi.SS3: return parseSs3(buf) @@ -242,7 +239,7 @@ func parseCsi(b []byte) (int, Msg) { switch cmd := csi.Cmd; cmd { case 'y' | '?'<= 2 { + if paramsLen >= 2 && csi.Param(0) != -1 && csi.Param(1) != -1 { return i, CursorPositionMsg{Row: csi.Param(0), Column: csi.Param(1)} } case 'm' | '<'<'< 1 && csi.Param(0) == 1 { + if paramsLen > 1 && csi.Param(0) == 1 && csi.Param(1) != -1 { // CSI 1 ; A - if paramsLen > 1 { - k.Mod |= KeyMod(csi.Param(1) - 1) - } + k.Mod |= KeyMod(csi.Param(1) - 1) } return i, k case 'M': @@ -329,7 +324,7 @@ func parseCsi(b []byte) (int, Msg) { return i + 3, parseX10MouseEvent(append(b[:i], b[i:i+3]...)) case 'y': // Report Mode (DECRPM) - if paramsLen != 2 { + if paramsLen != 2 && csi.Param(0) != -1 && csi.Param(0) != -1 { return i, UnknownMsg(b[:i]) } return i, ReportModeMsg{Mode: csi.Param(0), Value: csi.Param(1)} @@ -398,42 +393,42 @@ func parseCsi(b []byte) (int, Msg) { switch param { case 1: if flags&_FlagFind != 0 { - k = KeyPressMsg{Sym: KeyFind} + k = KeyPressMsg{Type: KeyFind} } else { - k = KeyPressMsg{Sym: KeyHome} + k = KeyPressMsg{Type: KeyHome} } case 2: - k = KeyPressMsg{Sym: KeyInsert} + k = KeyPressMsg{Type: KeyInsert} case 3: - k = KeyPressMsg{Sym: KeyDelete} + k = KeyPressMsg{Type: KeyDelete} case 4: if flags&_FlagSelect != 0 { - k = KeyPressMsg{Sym: KeySelect} + k = KeyPressMsg{Type: KeySelect} } else { - k = KeyPressMsg{Sym: KeyEnd} + k = KeyPressMsg{Type: KeyEnd} } case 5: - k = KeyPressMsg{Sym: KeyPgUp} + k = KeyPressMsg{Type: KeyPgUp} case 6: - k = KeyPressMsg{Sym: KeyPgDown} + k = KeyPressMsg{Type: KeyPgDown} case 7: - k = KeyPressMsg{Sym: KeyHome} + k = KeyPressMsg{Type: KeyHome} case 8: - k = KeyPressMsg{Sym: KeyEnd} + k = KeyPressMsg{Type: KeyEnd} case 11, 12, 13, 14, 15: - k = KeyPressMsg{Sym: KeyF1 + KeySym(param-11)} + k = KeyPressMsg{Type: KeyF1 + KeyType(param-11)} case 17, 18, 19, 20, 21: - k = KeyPressMsg{Sym: KeyF6 + KeySym(param-17)} + k = KeyPressMsg{Type: KeyF6 + KeyType(param-17)} case 23, 24, 25, 26: - k = KeyPressMsg{Sym: KeyF11 + KeySym(param-23)} + k = KeyPressMsg{Type: KeyF11 + KeyType(param-23)} case 28, 29: - k = KeyPressMsg{Sym: KeyF15 + KeySym(param-28)} + k = KeyPressMsg{Type: KeyF15 + KeyType(param-28)} case 31, 32, 33, 34: - k = KeyPressMsg{Sym: KeyF17 + KeySym(param-31)} + k = KeyPressMsg{Type: KeyF17 + KeyType(param-31)} } // modifiers - if paramsLen > 1 { + if paramsLen > 1 && csi.Param(1) != -1 { k.Mod |= KeyMod(csi.Param(1) - 1) } @@ -488,23 +483,23 @@ func parseSs3(b []byte) (int, Msg) { var k KeyPressMsg switch gl { case 'a', 'b', 'c', 'd': - k = KeyPressMsg{Sym: KeyUp + KeySym(gl-'a'), Mod: ModCtrl} + k = KeyPressMsg{Type: KeyUp + KeyType(gl-'a'), Mod: ModCtrl} case 'A', 'B', 'C', 'D': - k = KeyPressMsg{Sym: KeyUp + KeySym(gl-'A')} + k = KeyPressMsg{Type: KeyUp + KeyType(gl-'A')} case 'E': - k = KeyPressMsg{Sym: KeyBegin} + k = KeyPressMsg{Type: KeyBegin} case 'F': - k = KeyPressMsg{Sym: KeyEnd} + k = KeyPressMsg{Type: KeyEnd} case 'H': - k = KeyPressMsg{Sym: KeyHome} + k = KeyPressMsg{Type: KeyHome} case 'P', 'Q', 'R', 'S': - k = KeyPressMsg{Sym: KeyF1 + KeySym(gl-'P')} + k = KeyPressMsg{Type: KeyF1 + KeyType(gl-'P')} case 'M': - k = KeyPressMsg{Sym: KeyKpEnter} + k = KeyPressMsg{Type: KeyKpEnter} case 'X': - k = KeyPressMsg{Sym: KeyKpEqual} + k = KeyPressMsg{Type: KeyKpEqual} case 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y': - k = KeyPressMsg{Sym: KeyKpMultiply + KeySym(gl-'j')} + k = KeyPressMsg{Type: KeyKpMultiply + KeyType(gl-'j')} default: return i, UnknownMsg(b[:i]) } @@ -762,7 +757,7 @@ func parseUtf8(b []byte) (int, Msg) { return 1, parseControl(c) } else if c > ansi.US && c < ansi.DEL { // ASCII printable characters - return 1, KeyPressMsg{Sym: KeyRunes, Runes: []rune{rune(c)}} + return 1, KeyPressMsg{Runes: []rune{rune(c)}} } if r, _ := utf8.DecodeRune(b); r == utf8.RuneError { @@ -770,7 +765,7 @@ func parseUtf8(b []byte) (int, Msg) { } cluster, _, _, _ := uniseg.FirstGraphemeCluster(b, -1) - return len(cluster), KeyPressMsg{Sym: KeyRunes, Runes: []rune(string(cluster))} + return len(cluster), KeyPressMsg{Runes: []rune(string(cluster))} } func parseControl(b byte) Msg { @@ -779,31 +774,31 @@ func parseControl(b byte) Msg { if flags&_FlagCtrlAt != 0 { return KeyPressMsg{Runes: []rune{'@'}, Mod: ModCtrl} } - return KeyPressMsg{Runes: []rune{' '}, Sym: KeySpace, Mod: ModCtrl} + return KeyPressMsg{Runes: []rune{' '}, Type: KeySpace, Mod: ModCtrl} case ansi.BS: return KeyPressMsg{Runes: []rune{'h'}, Mod: ModCtrl} case ansi.HT: if flags&_FlagCtrlI != 0 { return KeyPressMsg{Runes: []rune{'i'}, Mod: ModCtrl} } - return KeyPressMsg{Sym: KeyTab} + return KeyPressMsg{Type: KeyTab} case ansi.CR: if flags&_FlagCtrlM != 0 { return KeyPressMsg{Runes: []rune{'m'}, Mod: ModCtrl} } - return KeyPressMsg{Sym: KeyEnter} + return KeyPressMsg{Type: KeyEnter} case ansi.ESC: if flags&_FlagCtrlOpenBracket != 0 { return KeyPressMsg{Runes: []rune{'['}, Mod: ModCtrl} } - return KeyPressMsg{Sym: KeyEscape} + return KeyPressMsg{Type: KeyEscape} case ansi.DEL: if flags&_FlagBackspace != 0 { - return KeyPressMsg{Sym: KeyDelete} + return KeyPressMsg{Type: KeyDelete} } - return KeyPressMsg{Sym: KeyBackspace} + return KeyPressMsg{Type: KeyBackspace} case ansi.SP: - return KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}} + return KeyPressMsg{Type: KeySpace, Runes: []rune{' '}} default: if b >= ansi.SOH && b <= ansi.SUB { // Use lower case letters for control codes diff --git a/parse_test.go b/parse_test.go index 1f3ccfc6d5..6e8e0b6170 100644 --- a/parse_test.go +++ b/parse_test.go @@ -9,14 +9,14 @@ import ( 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") want := []Msg{ - KeyPressMsg{Sym: KeyTab, Mod: ModShift | ModAlt}, - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'t'}}, - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'e'}}, - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'s'}}, - KeyPressMsg{Sym: KeyRunes, Runes: []rune{'t'}}, - KeyPressMsg{Sym: KeySpace, Runes: []rune{' '}, Mod: ModCtrl}, + KeyPressMsg{Type: KeyTab, Mod: ModShift | ModAlt}, + KeyPressMsg{Type: KeyRunes, Runes: []rune{'t'}}, + KeyPressMsg{Type: KeyRunes, Runes: []rune{'e'}}, + KeyPressMsg{Type: KeyRunes, Runes: []rune{'s'}}, + KeyPressMsg{Type: KeyRunes, Runes: []rune{'t'}}, + KeyPressMsg{Type: KeySpace, Runes: []rune{' '}, Mod: ModCtrl}, ForegroundColorMsg{color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff}}, - KeyPressMsg{Sym: KeyEscape, Mod: ModShift}, + KeyPressMsg{Type: KeyEscape, Mod: ModShift}, ReportModeMsg{Mode: 1049, Value: 2}, } for i := 0; len(input) != 0; i++ { diff --git a/table.go b/table.go index 4b350643b2..678388cdd9 100644 --- a/table.go +++ b/table.go @@ -10,39 +10,39 @@ import ( // events based on the VT100/VT200, XTerm, and Urxvt terminal specs. // TODO: Use flags? func buildKeysTable(flags int, term string) map[string]Key { - nul := Key{Runes: []rune{' '}, Sym: KeySpace, Mod: ModCtrl} // ctrl+@ or ctrl+space + nul := Key{Runes: []rune{' '}, Type: KeySpace, Mod: ModCtrl} // ctrl+@ or ctrl+space if flags&_FlagCtrlAt != 0 { nul = Key{Runes: []rune{'@'}, Mod: ModCtrl} } - tab := Key{Sym: KeyTab} // ctrl+i or tab + tab := Key{Type: KeyTab} // ctrl+i or tab if flags&_FlagCtrlI != 0 { tab = Key{Runes: []rune{'i'}, Mod: ModCtrl} } - enter := Key{Sym: KeyEnter} // ctrl+m or enter + enter := Key{Type: KeyEnter} // ctrl+m or enter if flags&_FlagCtrlM != 0 { enter = Key{Runes: []rune{'m'}, Mod: ModCtrl} } - esc := Key{Sym: KeyEscape} // ctrl+[ or escape + esc := Key{Type: KeyEscape} // ctrl+[ or escape if flags&_FlagCtrlOpenBracket != 0 { esc = Key{Runes: []rune{'['}, Mod: ModCtrl} // ctrl+[ or escape } - del := Key{Sym: KeyBackspace} + del := Key{Type: KeyBackspace} if flags&_FlagBackspace != 0 { - del.Sym = KeyDelete + del.Type = KeyDelete } - find := Key{Sym: KeyHome} + find := Key{Type: KeyHome} if flags&_FlagFind != 0 { - find.Sym = KeyFind + find.Type = KeyFind } - sel := Key{Sym: KeyEnd} + sel := Key{Type: KeyEnd} if flags&_FlagSelect != 0 { - sel.Sym = KeySelect + sel.Type = KeySelect } // The following is a table of key sequences and their corresponding key @@ -89,122 +89,122 @@ func buildKeysTable(flags int, term string) map[string]Key { string(byte(ansi.US)): {Runes: []rune{'_'}, Mod: ModCtrl}, // Special keys in G0 - string(byte(ansi.SP)): {Sym: KeySpace, Runes: []rune{' '}}, + string(byte(ansi.SP)): {Type: KeySpace, Runes: []rune{' '}}, string(byte(ansi.DEL)): del, // Special keys - "\x1b[Z": {Sym: KeyTab, Mod: ModShift}, + "\x1b[Z": {Type: KeyTab, Mod: ModShift}, "\x1b[1~": find, - "\x1b[2~": {Sym: KeyInsert}, - "\x1b[3~": {Sym: KeyDelete}, + "\x1b[2~": {Type: KeyInsert}, + "\x1b[3~": {Type: KeyDelete}, "\x1b[4~": sel, - "\x1b[5~": {Sym: KeyPgUp}, - "\x1b[6~": {Sym: KeyPgDown}, - "\x1b[7~": {Sym: KeyHome}, - "\x1b[8~": {Sym: KeyEnd}, + "\x1b[5~": {Type: KeyPgUp}, + "\x1b[6~": {Type: KeyPgDown}, + "\x1b[7~": {Type: KeyHome}, + "\x1b[8~": {Type: KeyEnd}, // Normal mode - "\x1b[A": {Sym: KeyUp}, - "\x1b[B": {Sym: KeyDown}, - "\x1b[C": {Sym: KeyRight}, - "\x1b[D": {Sym: KeyLeft}, - "\x1b[E": {Sym: KeyBegin}, - "\x1b[F": {Sym: KeyEnd}, - "\x1b[H": {Sym: KeyHome}, - "\x1b[P": {Sym: KeyF1}, - "\x1b[Q": {Sym: KeyF2}, - "\x1b[R": {Sym: KeyF3}, - "\x1b[S": {Sym: KeyF4}, + "\x1b[A": {Type: KeyUp}, + "\x1b[B": {Type: KeyDown}, + "\x1b[C": {Type: KeyRight}, + "\x1b[D": {Type: KeyLeft}, + "\x1b[E": {Type: KeyBegin}, + "\x1b[F": {Type: KeyEnd}, + "\x1b[H": {Type: KeyHome}, + "\x1b[P": {Type: KeyF1}, + "\x1b[Q": {Type: KeyF2}, + "\x1b[R": {Type: KeyF3}, + "\x1b[S": {Type: KeyF4}, // Application Cursor Key Mode (DECCKM) - "\x1bOA": {Sym: KeyUp}, - "\x1bOB": {Sym: KeyDown}, - "\x1bOC": {Sym: KeyRight}, - "\x1bOD": {Sym: KeyLeft}, - "\x1bOE": {Sym: KeyBegin}, - "\x1bOF": {Sym: KeyEnd}, - "\x1bOH": {Sym: KeyHome}, - "\x1bOP": {Sym: KeyF1}, - "\x1bOQ": {Sym: KeyF2}, - "\x1bOR": {Sym: KeyF3}, - "\x1bOS": {Sym: KeyF4}, + "\x1bOA": {Type: KeyUp}, + "\x1bOB": {Type: KeyDown}, + "\x1bOC": {Type: KeyRight}, + "\x1bOD": {Type: KeyLeft}, + "\x1bOE": {Type: KeyBegin}, + "\x1bOF": {Type: KeyEnd}, + "\x1bOH": {Type: KeyHome}, + "\x1bOP": {Type: KeyF1}, + "\x1bOQ": {Type: KeyF2}, + "\x1bOR": {Type: KeyF3}, + "\x1bOS": {Type: KeyF4}, // Keypad Application Mode (DECKPAM) - "\x1bOM": {Sym: KeyKpEnter}, - "\x1bOX": {Sym: KeyKpEqual}, - "\x1bOj": {Sym: KeyKpMultiply}, - "\x1bOk": {Sym: KeyKpPlus}, - "\x1bOl": {Sym: KeyKpComma}, - "\x1bOm": {Sym: KeyKpMinus}, - "\x1bOn": {Sym: KeyKpDecimal}, - "\x1bOo": {Sym: KeyKpDivide}, - "\x1bOp": {Sym: KeyKp0}, - "\x1bOq": {Sym: KeyKp1}, - "\x1bOr": {Sym: KeyKp2}, - "\x1bOs": {Sym: KeyKp3}, - "\x1bOt": {Sym: KeyKp4}, - "\x1bOu": {Sym: KeyKp5}, - "\x1bOv": {Sym: KeyKp6}, - "\x1bOw": {Sym: KeyKp7}, - "\x1bOx": {Sym: KeyKp8}, - "\x1bOy": {Sym: KeyKp9}, + "\x1bOM": {Type: KeyKpEnter}, + "\x1bOX": {Type: KeyKpEqual}, + "\x1bOj": {Type: KeyKpMultiply}, + "\x1bOk": {Type: KeyKpPlus}, + "\x1bOl": {Type: KeyKpComma}, + "\x1bOm": {Type: KeyKpMinus}, + "\x1bOn": {Type: KeyKpDecimal}, + "\x1bOo": {Type: KeyKpDivide}, + "\x1bOp": {Type: KeyKp0}, + "\x1bOq": {Type: KeyKp1}, + "\x1bOr": {Type: KeyKp2}, + "\x1bOs": {Type: KeyKp3}, + "\x1bOt": {Type: KeyKp4}, + "\x1bOu": {Type: KeyKp5}, + "\x1bOv": {Type: KeyKp6}, + "\x1bOw": {Type: KeyKp7}, + "\x1bOx": {Type: KeyKp8}, + "\x1bOy": {Type: KeyKp9}, // Function keys - "\x1b[11~": {Sym: KeyF1}, - "\x1b[12~": {Sym: KeyF2}, - "\x1b[13~": {Sym: KeyF3}, - "\x1b[14~": {Sym: KeyF4}, - "\x1b[15~": {Sym: KeyF5}, - "\x1b[17~": {Sym: KeyF6}, - "\x1b[18~": {Sym: KeyF7}, - "\x1b[19~": {Sym: KeyF8}, - "\x1b[20~": {Sym: KeyF9}, - "\x1b[21~": {Sym: KeyF10}, - "\x1b[23~": {Sym: KeyF11}, - "\x1b[24~": {Sym: KeyF12}, - "\x1b[25~": {Sym: KeyF13}, - "\x1b[26~": {Sym: KeyF14}, - "\x1b[28~": {Sym: KeyF15}, - "\x1b[29~": {Sym: KeyF16}, - "\x1b[31~": {Sym: KeyF17}, - "\x1b[32~": {Sym: KeyF18}, - "\x1b[33~": {Sym: KeyF19}, - "\x1b[34~": {Sym: KeyF20}, + "\x1b[11~": {Type: KeyF1}, + "\x1b[12~": {Type: KeyF2}, + "\x1b[13~": {Type: KeyF3}, + "\x1b[14~": {Type: KeyF4}, + "\x1b[15~": {Type: KeyF5}, + "\x1b[17~": {Type: KeyF6}, + "\x1b[18~": {Type: KeyF7}, + "\x1b[19~": {Type: KeyF8}, + "\x1b[20~": {Type: KeyF9}, + "\x1b[21~": {Type: KeyF10}, + "\x1b[23~": {Type: KeyF11}, + "\x1b[24~": {Type: KeyF12}, + "\x1b[25~": {Type: KeyF13}, + "\x1b[26~": {Type: KeyF14}, + "\x1b[28~": {Type: KeyF15}, + "\x1b[29~": {Type: KeyF16}, + "\x1b[31~": {Type: KeyF17}, + "\x1b[32~": {Type: KeyF18}, + "\x1b[33~": {Type: KeyF19}, + "\x1b[34~": {Type: KeyF20}, } // CSI ~ sequence keys csiTildeKeys := map[string]Key{ - "1": find, "2": {Sym: KeyInsert}, - "3": {Sym: KeyDelete}, "4": sel, - "5": {Sym: KeyPgUp}, "6": {Sym: KeyPgDown}, - "7": {Sym: KeyHome}, "8": {Sym: KeyEnd}, + "1": find, "2": {Type: KeyInsert}, + "3": {Type: KeyDelete}, "4": sel, + "5": {Type: KeyPgUp}, "6": {Type: KeyPgDown}, + "7": {Type: KeyHome}, "8": {Type: KeyEnd}, // There are no 9 and 10 keys - "11": {Sym: KeyF1}, "12": {Sym: KeyF2}, - "13": {Sym: KeyF3}, "14": {Sym: KeyF4}, - "15": {Sym: KeyF5}, "17": {Sym: KeyF6}, - "18": {Sym: KeyF7}, "19": {Sym: KeyF8}, - "20": {Sym: KeyF9}, "21": {Sym: KeyF10}, - "23": {Sym: KeyF11}, "24": {Sym: KeyF12}, - "25": {Sym: KeyF13}, "26": {Sym: KeyF14}, - "28": {Sym: KeyF15}, "29": {Sym: KeyF16}, - "31": {Sym: KeyF17}, "32": {Sym: KeyF18}, - "33": {Sym: KeyF19}, "34": {Sym: KeyF20}, + "11": {Type: KeyF1}, "12": {Type: KeyF2}, + "13": {Type: KeyF3}, "14": {Type: KeyF4}, + "15": {Type: KeyF5}, "17": {Type: KeyF6}, + "18": {Type: KeyF7}, "19": {Type: KeyF8}, + "20": {Type: KeyF9}, "21": {Type: KeyF10}, + "23": {Type: KeyF11}, "24": {Type: KeyF12}, + "25": {Type: KeyF13}, "26": {Type: KeyF14}, + "28": {Type: KeyF15}, "29": {Type: KeyF16}, + "31": {Type: KeyF17}, "32": {Type: KeyF18}, + "33": {Type: KeyF19}, "34": {Type: KeyF20}, } // URxvt keys // See https://manpages.ubuntu.com/manpages/trusty/man7/urxvt.7.html#key%20codes - table["\x1b[a"] = Key{Sym: KeyUp, Mod: ModShift} - table["\x1b[b"] = Key{Sym: KeyDown, Mod: ModShift} - table["\x1b[c"] = Key{Sym: KeyRight, Mod: ModShift} - table["\x1b[d"] = Key{Sym: KeyLeft, Mod: ModShift} - table["\x1bOa"] = Key{Sym: KeyUp, Mod: ModCtrl} - table["\x1bOb"] = Key{Sym: KeyDown, Mod: ModCtrl} - table["\x1bOc"] = Key{Sym: KeyRight, Mod: ModCtrl} - table["\x1bOd"] = Key{Sym: KeyLeft, Mod: ModCtrl} + table["\x1b[a"] = Key{Type: KeyUp, Mod: ModShift} + table["\x1b[b"] = Key{Type: KeyDown, Mod: ModShift} + table["\x1b[c"] = Key{Type: KeyRight, Mod: ModShift} + table["\x1b[d"] = Key{Type: KeyLeft, Mod: ModShift} + table["\x1bOa"] = Key{Type: KeyUp, Mod: ModCtrl} + table["\x1bOb"] = Key{Type: KeyDown, Mod: ModCtrl} + table["\x1bOc"] = Key{Type: KeyRight, Mod: ModCtrl} + table["\x1bOd"] = Key{Type: KeyLeft, Mod: ModCtrl} // TODO: invistigate if shift-ctrl arrow keys collide with DECCKM keys i.e. // "\x1bOA", "\x1bOB", "\x1bOC", "\x1bOD" @@ -232,46 +232,46 @@ func buildKeysTable(flags int, term string) map[string]Key { // 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{Sym: KeyF11, Mod: ModShift} - table["\x1b[24$"] = Key{Sym: KeyF12, Mod: ModShift} - table["\x1b[25$"] = Key{Sym: KeyF13, Mod: ModShift} - table["\x1b[26$"] = Key{Sym: KeyF14, Mod: ModShift} - table["\x1b[28$"] = Key{Sym: KeyF15, Mod: ModShift} - table["\x1b[29$"] = Key{Sym: KeyF16, Mod: ModShift} - table["\x1b[31$"] = Key{Sym: KeyF17, Mod: ModShift} - table["\x1b[32$"] = Key{Sym: KeyF18, Mod: ModShift} - table["\x1b[33$"] = Key{Sym: KeyF19, Mod: ModShift} - table["\x1b[34$"] = Key{Sym: KeyF20, Mod: ModShift} - table["\x1b[11^"] = Key{Sym: KeyF1, Mod: ModCtrl} - table["\x1b[12^"] = Key{Sym: KeyF2, Mod: ModCtrl} - table["\x1b[13^"] = Key{Sym: KeyF3, Mod: ModCtrl} - table["\x1b[14^"] = Key{Sym: KeyF4, Mod: ModCtrl} - table["\x1b[15^"] = Key{Sym: KeyF5, Mod: ModCtrl} - table["\x1b[17^"] = Key{Sym: KeyF6, Mod: ModCtrl} - table["\x1b[18^"] = Key{Sym: KeyF7, Mod: ModCtrl} - table["\x1b[19^"] = Key{Sym: KeyF8, Mod: ModCtrl} - table["\x1b[20^"] = Key{Sym: KeyF9, Mod: ModCtrl} - table["\x1b[21^"] = Key{Sym: KeyF10, Mod: ModCtrl} - table["\x1b[23^"] = Key{Sym: KeyF11, Mod: ModCtrl} - table["\x1b[24^"] = Key{Sym: KeyF12, Mod: ModCtrl} - table["\x1b[25^"] = Key{Sym: KeyF13, Mod: ModCtrl} - table["\x1b[26^"] = Key{Sym: KeyF14, Mod: ModCtrl} - table["\x1b[28^"] = Key{Sym: KeyF15, Mod: ModCtrl} - table["\x1b[29^"] = Key{Sym: KeyF16, Mod: ModCtrl} - table["\x1b[31^"] = Key{Sym: KeyF17, Mod: ModCtrl} - table["\x1b[32^"] = Key{Sym: KeyF18, Mod: ModCtrl} - table["\x1b[33^"] = Key{Sym: KeyF19, Mod: ModCtrl} - table["\x1b[34^"] = Key{Sym: KeyF20, Mod: ModCtrl} - table["\x1b[23@"] = Key{Sym: KeyF11, Mod: ModShift | ModCtrl} - table["\x1b[24@"] = Key{Sym: KeyF12, Mod: ModShift | ModCtrl} - table["\x1b[25@"] = Key{Sym: KeyF13, Mod: ModShift | ModCtrl} - table["\x1b[26@"] = Key{Sym: KeyF14, Mod: ModShift | ModCtrl} - table["\x1b[28@"] = Key{Sym: KeyF15, Mod: ModShift | ModCtrl} - table["\x1b[29@"] = Key{Sym: KeyF16, Mod: ModShift | ModCtrl} - table["\x1b[31@"] = Key{Sym: KeyF17, Mod: ModShift | ModCtrl} - table["\x1b[32@"] = Key{Sym: KeyF18, Mod: ModShift | ModCtrl} - table["\x1b[33@"] = Key{Sym: KeyF19, Mod: ModShift | ModCtrl} - table["\x1b[34@"] = Key{Sym: KeyF20, Mod: ModShift | ModCtrl} + table["\x1b[23$"] = Key{Type: KeyF11, Mod: ModShift} + table["\x1b[24$"] = Key{Type: KeyF12, Mod: ModShift} + table["\x1b[25$"] = Key{Type: KeyF13, Mod: ModShift} + table["\x1b[26$"] = Key{Type: KeyF14, Mod: ModShift} + table["\x1b[28$"] = Key{Type: KeyF15, Mod: ModShift} + table["\x1b[29$"] = Key{Type: KeyF16, Mod: ModShift} + table["\x1b[31$"] = Key{Type: KeyF17, Mod: ModShift} + table["\x1b[32$"] = Key{Type: KeyF18, Mod: ModShift} + table["\x1b[33$"] = Key{Type: KeyF19, Mod: ModShift} + table["\x1b[34$"] = Key{Type: KeyF20, Mod: ModShift} + table["\x1b[11^"] = Key{Type: KeyF1, Mod: ModCtrl} + table["\x1b[12^"] = Key{Type: KeyF2, Mod: ModCtrl} + table["\x1b[13^"] = Key{Type: KeyF3, Mod: ModCtrl} + table["\x1b[14^"] = Key{Type: KeyF4, Mod: ModCtrl} + table["\x1b[15^"] = Key{Type: KeyF5, Mod: ModCtrl} + table["\x1b[17^"] = Key{Type: KeyF6, Mod: ModCtrl} + table["\x1b[18^"] = Key{Type: KeyF7, Mod: ModCtrl} + table["\x1b[19^"] = Key{Type: KeyF8, Mod: ModCtrl} + table["\x1b[20^"] = Key{Type: KeyF9, Mod: ModCtrl} + table["\x1b[21^"] = Key{Type: KeyF10, Mod: ModCtrl} + table["\x1b[23^"] = Key{Type: KeyF11, Mod: ModCtrl} + table["\x1b[24^"] = Key{Type: KeyF12, Mod: ModCtrl} + table["\x1b[25^"] = Key{Type: KeyF13, Mod: ModCtrl} + table["\x1b[26^"] = Key{Type: KeyF14, Mod: ModCtrl} + table["\x1b[28^"] = Key{Type: KeyF15, Mod: ModCtrl} + table["\x1b[29^"] = Key{Type: KeyF16, Mod: ModCtrl} + table["\x1b[31^"] = Key{Type: KeyF17, Mod: ModCtrl} + table["\x1b[32^"] = Key{Type: KeyF18, Mod: ModCtrl} + table["\x1b[33^"] = Key{Type: KeyF19, Mod: ModCtrl} + table["\x1b[34^"] = Key{Type: KeyF20, Mod: ModCtrl} + table["\x1b[23@"] = Key{Type: KeyF11, Mod: ModShift | ModCtrl} + table["\x1b[24@"] = Key{Type: KeyF12, Mod: ModShift | ModCtrl} + table["\x1b[25@"] = Key{Type: KeyF13, Mod: ModShift | ModCtrl} + table["\x1b[26@"] = Key{Type: KeyF14, Mod: ModShift | ModCtrl} + table["\x1b[28@"] = Key{Type: KeyF15, Mod: ModShift | ModCtrl} + table["\x1b[29@"] = Key{Type: KeyF16, Mod: ModShift | ModCtrl} + table["\x1b[31@"] = Key{Type: KeyF17, Mod: ModShift | ModCtrl} + table["\x1b[32@"] = Key{Type: KeyF18, Mod: ModShift | ModCtrl} + table["\x1b[33@"] = Key{Type: KeyF19, Mod: ModShift | ModCtrl} + table["\x1b[34@"] = Key{Type: KeyF20, Mod: ModShift | ModCtrl} // Register Alt + combinations // XXX: this must come after URxvt but before XTerm keys to register URxvt @@ -312,34 +312,34 @@ func buildKeysTable(flags int, term string) 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": {Sym: KeyKpEnter}, "X": {Sym: KeyKpEqual}, - "j": {Sym: KeyKpMultiply}, "k": {Sym: KeyKpPlus}, - "l": {Sym: KeyKpComma}, "m": {Sym: KeyKpMinus}, - "n": {Sym: KeyKpDecimal}, "o": {Sym: KeyKpDivide}, - "p": {Sym: KeyKp0}, "q": {Sym: KeyKp1}, - "r": {Sym: KeyKp2}, "s": {Sym: KeyKp3}, - "t": {Sym: KeyKp4}, "u": {Sym: KeyKp5}, - "v": {Sym: KeyKp6}, "w": {Sym: KeyKp7}, - "x": {Sym: KeyKp8}, "y": {Sym: KeyKp9}, + "M": {Type: KeyKpEnter}, "X": {Type: KeyKpEqual}, + "j": {Type: KeyKpMultiply}, "k": {Type: KeyKpPlus}, + "l": {Type: KeyKpComma}, "m": {Type: KeyKpMinus}, + "n": {Type: KeyKpDecimal}, "o": {Type: KeyKpDivide}, + "p": {Type: KeyKp0}, "q": {Type: KeyKp1}, + "r": {Type: KeyKp2}, "s": {Type: KeyKp3}, + "t": {Type: KeyKp4}, "u": {Type: KeyKp5}, + "v": {Type: KeyKp6}, "w": {Type: KeyKp7}, + "x": {Type: KeyKp8}, "y": {Type: KeyKp9}, } // XTerm keys csiFuncKeys := map[string]Key{ - "A": {Sym: KeyUp}, "B": {Sym: KeyDown}, - "C": {Sym: KeyRight}, "D": {Sym: KeyLeft}, - "E": {Sym: KeyBegin}, "F": {Sym: KeyEnd}, - "H": {Sym: KeyHome}, "P": {Sym: KeyF1}, - "Q": {Sym: KeyF2}, "R": {Sym: KeyF3}, - "S": {Sym: KeyF4}, + "A": {Type: KeyUp}, "B": {Type: KeyDown}, + "C": {Type: KeyRight}, "D": {Type: KeyLeft}, + "E": {Type: KeyBegin}, "F": {Type: KeyEnd}, + "H": {Type: KeyHome}, "P": {Type: KeyF1}, + "Q": {Type: KeyF2}, "R": {Type: KeyF3}, + "S": {Type: KeyF4}, } // CSI 27 ; ; ~ keys defined in XTerm modifyOtherKeys modifyOtherKeys := map[int]Key{ - ansi.BS: {Sym: KeyBackspace}, - ansi.HT: {Sym: KeyTab}, - ansi.CR: {Sym: KeyEnter}, - ansi.ESC: {Sym: KeyEscape}, - ansi.DEL: {Sym: KeyBackspace}, + ansi.BS: {Type: KeyBackspace}, + ansi.HT: {Type: KeyTab}, + ansi.CR: {Type: KeyEnter}, + ansi.ESC: {Type: KeyEscape}, + ansi.DEL: {Type: KeyBackspace}, } for _, m := range modifiers { diff --git a/terminfo.go b/terminfo.go index eb9cb3c71d..e4be0ddf3e 100644 --- a/terminfo.go +++ b/terminfo.go @@ -56,91 +56,91 @@ func buildTerminfoKeys(flags int, term string) map[string]Key { // See https://github.com/mirror/ncurses/blob/master/include/Caps-ncurses func defaultTerminfoKeys(flags int) map[string]Key { keys := map[string]Key{ - "kcuu1": {Sym: KeyUp}, - "kUP": {Sym: KeyUp, Mod: ModShift}, - "kUP3": {Sym: KeyUp, Mod: ModAlt}, - "kUP4": {Sym: KeyUp, Mod: ModShift | ModAlt}, - "kUP5": {Sym: KeyUp, Mod: ModCtrl}, - "kUP6": {Sym: KeyUp, Mod: ModShift | ModCtrl}, - "kUP7": {Sym: KeyUp, Mod: ModAlt | ModCtrl}, - "kUP8": {Sym: KeyUp, Mod: ModShift | ModAlt | ModCtrl}, - "kcud1": {Sym: KeyDown}, - "kDN": {Sym: KeyDown, Mod: ModShift}, - "kDN3": {Sym: KeyDown, Mod: ModAlt}, - "kDN4": {Sym: KeyDown, Mod: ModShift | ModAlt}, - "kDN5": {Sym: KeyDown, Mod: ModCtrl}, - "kDN7": {Sym: KeyDown, Mod: ModAlt | ModCtrl}, - "kDN6": {Sym: KeyDown, Mod: ModShift | ModCtrl}, - "kDN8": {Sym: KeyDown, Mod: ModShift | ModAlt | ModCtrl}, - "kcub1": {Sym: KeyLeft}, - "kLFT": {Sym: KeyLeft, Mod: ModShift}, - "kLFT3": {Sym: KeyLeft, Mod: ModAlt}, - "kLFT4": {Sym: KeyLeft, Mod: ModShift | ModAlt}, - "kLFT5": {Sym: KeyLeft, Mod: ModCtrl}, - "kLFT6": {Sym: KeyLeft, Mod: ModShift | ModCtrl}, - "kLFT7": {Sym: KeyLeft, Mod: ModAlt | ModCtrl}, - "kLFT8": {Sym: KeyLeft, Mod: ModShift | ModAlt | ModCtrl}, - "kcuf1": {Sym: KeyRight}, - "kRIT": {Sym: KeyRight, Mod: ModShift}, - "kRIT3": {Sym: KeyRight, Mod: ModAlt}, - "kRIT4": {Sym: KeyRight, Mod: ModShift | ModAlt}, - "kRIT5": {Sym: KeyRight, Mod: ModCtrl}, - "kRIT6": {Sym: KeyRight, Mod: ModShift | ModCtrl}, - "kRIT7": {Sym: KeyRight, Mod: ModAlt | ModCtrl}, - "kRIT8": {Sym: KeyRight, Mod: ModShift | ModAlt | ModCtrl}, - "kich1": {Sym: KeyInsert}, - "kIC": {Sym: KeyInsert, Mod: ModShift}, - "kIC3": {Sym: KeyInsert, Mod: ModAlt}, - "kIC4": {Sym: KeyInsert, Mod: ModShift | ModAlt}, - "kIC5": {Sym: KeyInsert, Mod: ModCtrl}, - "kIC6": {Sym: KeyInsert, Mod: ModShift | ModCtrl}, - "kIC7": {Sym: KeyInsert, Mod: ModAlt | ModCtrl}, - "kIC8": {Sym: KeyInsert, Mod: ModShift | ModAlt | ModCtrl}, - "kdch1": {Sym: KeyDelete}, - "kDC": {Sym: KeyDelete, Mod: ModShift}, - "kDC3": {Sym: KeyDelete, Mod: ModAlt}, - "kDC4": {Sym: KeyDelete, Mod: ModShift | ModAlt}, - "kDC5": {Sym: KeyDelete, Mod: ModCtrl}, - "kDC6": {Sym: KeyDelete, Mod: ModShift | ModCtrl}, - "kDC7": {Sym: KeyDelete, Mod: ModAlt | ModCtrl}, - "kDC8": {Sym: KeyDelete, Mod: ModShift | ModAlt | ModCtrl}, - "khome": {Sym: KeyHome}, - "kHOM": {Sym: KeyHome, Mod: ModShift}, - "kHOM3": {Sym: KeyHome, Mod: ModAlt}, - "kHOM4": {Sym: KeyHome, Mod: ModShift | ModAlt}, - "kHOM5": {Sym: KeyHome, Mod: ModCtrl}, - "kHOM6": {Sym: KeyHome, Mod: ModShift | ModCtrl}, - "kHOM7": {Sym: KeyHome, Mod: ModAlt | ModCtrl}, - "kHOM8": {Sym: KeyHome, Mod: ModShift | ModAlt | ModCtrl}, - "kend": {Sym: KeyEnd}, - "kEND": {Sym: KeyEnd, Mod: ModShift}, - "kEND3": {Sym: KeyEnd, Mod: ModAlt}, - "kEND4": {Sym: KeyEnd, Mod: ModShift | ModAlt}, - "kEND5": {Sym: KeyEnd, Mod: ModCtrl}, - "kEND6": {Sym: KeyEnd, Mod: ModShift | ModCtrl}, - "kEND7": {Sym: KeyEnd, Mod: ModAlt | ModCtrl}, - "kEND8": {Sym: KeyEnd, Mod: ModShift | ModAlt | ModCtrl}, - "kpp": {Sym: KeyPgUp}, - "kprv": {Sym: KeyPgUp}, - "kPRV": {Sym: KeyPgUp, Mod: ModShift}, - "kPRV3": {Sym: KeyPgUp, Mod: ModAlt}, - "kPRV4": {Sym: KeyPgUp, Mod: ModShift | ModAlt}, - "kPRV5": {Sym: KeyPgUp, Mod: ModCtrl}, - "kPRV6": {Sym: KeyPgUp, Mod: ModShift | ModCtrl}, - "kPRV7": {Sym: KeyPgUp, Mod: ModAlt | ModCtrl}, - "kPRV8": {Sym: KeyPgUp, Mod: ModShift | ModAlt | ModCtrl}, - "knp": {Sym: KeyPgDown}, - "knxt": {Sym: KeyPgDown}, - "kNXT": {Sym: KeyPgDown, Mod: ModShift}, - "kNXT3": {Sym: KeyPgDown, Mod: ModAlt}, - "kNXT4": {Sym: KeyPgDown, Mod: ModShift | ModAlt}, - "kNXT5": {Sym: KeyPgDown, Mod: ModCtrl}, - "kNXT6": {Sym: KeyPgDown, Mod: ModShift | ModCtrl}, - "kNXT7": {Sym: KeyPgDown, Mod: ModAlt | ModCtrl}, - "kNXT8": {Sym: KeyPgDown, Mod: ModShift | ModAlt | ModCtrl}, + "kcuu1": {Type: KeyUp}, + "kUP": {Type: KeyUp, Mod: ModShift}, + "kUP3": {Type: KeyUp, Mod: ModAlt}, + "kUP4": {Type: KeyUp, Mod: ModShift | ModAlt}, + "kUP5": {Type: KeyUp, Mod: ModCtrl}, + "kUP6": {Type: KeyUp, Mod: ModShift | ModCtrl}, + "kUP7": {Type: KeyUp, Mod: ModAlt | ModCtrl}, + "kUP8": {Type: KeyUp, Mod: ModShift | ModAlt | ModCtrl}, + "kcud1": {Type: KeyDown}, + "kDN": {Type: KeyDown, Mod: ModShift}, + "kDN3": {Type: KeyDown, Mod: ModAlt}, + "kDN4": {Type: KeyDown, Mod: ModShift | ModAlt}, + "kDN5": {Type: KeyDown, Mod: ModCtrl}, + "kDN7": {Type: KeyDown, Mod: ModAlt | ModCtrl}, + "kDN6": {Type: KeyDown, Mod: ModShift | ModCtrl}, + "kDN8": {Type: KeyDown, Mod: ModShift | ModAlt | ModCtrl}, + "kcub1": {Type: KeyLeft}, + "kLFT": {Type: KeyLeft, Mod: ModShift}, + "kLFT3": {Type: KeyLeft, Mod: ModAlt}, + "kLFT4": {Type: KeyLeft, Mod: ModShift | ModAlt}, + "kLFT5": {Type: KeyLeft, Mod: ModCtrl}, + "kLFT6": {Type: KeyLeft, Mod: ModShift | ModCtrl}, + "kLFT7": {Type: KeyLeft, Mod: ModAlt | ModCtrl}, + "kLFT8": {Type: KeyLeft, Mod: ModShift | ModAlt | ModCtrl}, + "kcuf1": {Type: KeyRight}, + "kRIT": {Type: KeyRight, Mod: ModShift}, + "kRIT3": {Type: KeyRight, Mod: ModAlt}, + "kRIT4": {Type: KeyRight, Mod: ModShift | ModAlt}, + "kRIT5": {Type: KeyRight, Mod: ModCtrl}, + "kRIT6": {Type: KeyRight, Mod: ModShift | ModCtrl}, + "kRIT7": {Type: KeyRight, Mod: ModAlt | ModCtrl}, + "kRIT8": {Type: KeyRight, Mod: ModShift | ModAlt | ModCtrl}, + "kich1": {Type: KeyInsert}, + "kIC": {Type: KeyInsert, Mod: ModShift}, + "kIC3": {Type: KeyInsert, Mod: ModAlt}, + "kIC4": {Type: KeyInsert, Mod: ModShift | ModAlt}, + "kIC5": {Type: KeyInsert, Mod: ModCtrl}, + "kIC6": {Type: KeyInsert, Mod: ModShift | ModCtrl}, + "kIC7": {Type: KeyInsert, Mod: ModAlt | ModCtrl}, + "kIC8": {Type: KeyInsert, Mod: ModShift | ModAlt | ModCtrl}, + "kdch1": {Type: KeyDelete}, + "kDC": {Type: KeyDelete, Mod: ModShift}, + "kDC3": {Type: KeyDelete, Mod: ModAlt}, + "kDC4": {Type: KeyDelete, Mod: ModShift | ModAlt}, + "kDC5": {Type: KeyDelete, Mod: ModCtrl}, + "kDC6": {Type: KeyDelete, Mod: ModShift | ModCtrl}, + "kDC7": {Type: KeyDelete, Mod: ModAlt | ModCtrl}, + "kDC8": {Type: KeyDelete, Mod: ModShift | ModAlt | ModCtrl}, + "khome": {Type: KeyHome}, + "kHOM": {Type: KeyHome, Mod: ModShift}, + "kHOM3": {Type: KeyHome, Mod: ModAlt}, + "kHOM4": {Type: KeyHome, Mod: ModShift | ModAlt}, + "kHOM5": {Type: KeyHome, Mod: ModCtrl}, + "kHOM6": {Type: KeyHome, Mod: ModShift | ModCtrl}, + "kHOM7": {Type: KeyHome, Mod: ModAlt | ModCtrl}, + "kHOM8": {Type: KeyHome, Mod: ModShift | ModAlt | ModCtrl}, + "kend": {Type: KeyEnd}, + "kEND": {Type: KeyEnd, Mod: ModShift}, + "kEND3": {Type: KeyEnd, Mod: ModAlt}, + "kEND4": {Type: KeyEnd, Mod: ModShift | ModAlt}, + "kEND5": {Type: KeyEnd, Mod: ModCtrl}, + "kEND6": {Type: KeyEnd, Mod: ModShift | ModCtrl}, + "kEND7": {Type: KeyEnd, Mod: ModAlt | ModCtrl}, + "kEND8": {Type: KeyEnd, Mod: ModShift | ModAlt | ModCtrl}, + "kpp": {Type: KeyPgUp}, + "kprv": {Type: KeyPgUp}, + "kPRV": {Type: KeyPgUp, Mod: ModShift}, + "kPRV3": {Type: KeyPgUp, Mod: ModAlt}, + "kPRV4": {Type: KeyPgUp, Mod: ModShift | ModAlt}, + "kPRV5": {Type: KeyPgUp, Mod: ModCtrl}, + "kPRV6": {Type: KeyPgUp, Mod: ModShift | ModCtrl}, + "kPRV7": {Type: KeyPgUp, Mod: ModAlt | ModCtrl}, + "kPRV8": {Type: KeyPgUp, Mod: ModShift | ModAlt | ModCtrl}, + "knp": {Type: KeyPgDown}, + "knxt": {Type: KeyPgDown}, + "kNXT": {Type: KeyPgDown, Mod: ModShift}, + "kNXT3": {Type: KeyPgDown, Mod: ModAlt}, + "kNXT4": {Type: KeyPgDown, Mod: ModShift | ModAlt}, + "kNXT5": {Type: KeyPgDown, Mod: ModCtrl}, + "kNXT6": {Type: KeyPgDown, Mod: ModShift | ModCtrl}, + "kNXT7": {Type: KeyPgDown, Mod: ModAlt | ModCtrl}, + "kNXT8": {Type: KeyPgDown, Mod: ModShift | ModAlt | ModCtrl}, - "kbs": {Sym: KeyBackspace}, - "kcbt": {Sym: KeyTab, Mod: ModShift}, + "kbs": {Type: KeyBackspace}, + "kcbt": {Type: KeyTab, Mod: ModShift}, // Function keys // This only includes the first 12 function keys. The rest are treated @@ -152,125 +152,125 @@ func defaultTerminfoKeys(flags int) map[string]Key { // See https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyFunctionKeys // See https://invisible-island.net/xterm/terminfo.html - "kf1": {Sym: KeyF1}, - "kf2": {Sym: KeyF2}, - "kf3": {Sym: KeyF3}, - "kf4": {Sym: KeyF4}, - "kf5": {Sym: KeyF5}, - "kf6": {Sym: KeyF6}, - "kf7": {Sym: KeyF7}, - "kf8": {Sym: KeyF8}, - "kf9": {Sym: KeyF9}, - "kf10": {Sym: KeyF10}, - "kf11": {Sym: KeyF11}, - "kf12": {Sym: KeyF12}, - "kf13": {Sym: KeyF1, Mod: ModShift}, - "kf14": {Sym: KeyF2, Mod: ModShift}, - "kf15": {Sym: KeyF3, Mod: ModShift}, - "kf16": {Sym: KeyF4, Mod: ModShift}, - "kf17": {Sym: KeyF5, Mod: ModShift}, - "kf18": {Sym: KeyF6, Mod: ModShift}, - "kf19": {Sym: KeyF7, Mod: ModShift}, - "kf20": {Sym: KeyF8, Mod: ModShift}, - "kf21": {Sym: KeyF9, Mod: ModShift}, - "kf22": {Sym: KeyF10, Mod: ModShift}, - "kf23": {Sym: KeyF11, Mod: ModShift}, - "kf24": {Sym: KeyF12, Mod: ModShift}, - "kf25": {Sym: KeyF1, Mod: ModCtrl}, - "kf26": {Sym: KeyF2, Mod: ModCtrl}, - "kf27": {Sym: KeyF3, Mod: ModCtrl}, - "kf28": {Sym: KeyF4, Mod: ModCtrl}, - "kf29": {Sym: KeyF5, Mod: ModCtrl}, - "kf30": {Sym: KeyF6, Mod: ModCtrl}, - "kf31": {Sym: KeyF7, Mod: ModCtrl}, - "kf32": {Sym: KeyF8, Mod: ModCtrl}, - "kf33": {Sym: KeyF9, Mod: ModCtrl}, - "kf34": {Sym: KeyF10, Mod: ModCtrl}, - "kf35": {Sym: KeyF11, Mod: ModCtrl}, - "kf36": {Sym: KeyF12, Mod: ModCtrl}, - "kf37": {Sym: KeyF1, Mod: ModShift | ModCtrl}, - "kf38": {Sym: KeyF2, Mod: ModShift | ModCtrl}, - "kf39": {Sym: KeyF3, Mod: ModShift | ModCtrl}, - "kf40": {Sym: KeyF4, Mod: ModShift | ModCtrl}, - "kf41": {Sym: KeyF5, Mod: ModShift | ModCtrl}, - "kf42": {Sym: KeyF6, Mod: ModShift | ModCtrl}, - "kf43": {Sym: KeyF7, Mod: ModShift | ModCtrl}, - "kf44": {Sym: KeyF8, Mod: ModShift | ModCtrl}, - "kf45": {Sym: KeyF9, Mod: ModShift | ModCtrl}, - "kf46": {Sym: KeyF10, Mod: ModShift | ModCtrl}, - "kf47": {Sym: KeyF11, Mod: ModShift | ModCtrl}, - "kf48": {Sym: KeyF12, Mod: ModShift | ModCtrl}, - "kf49": {Sym: KeyF1, Mod: ModAlt}, - "kf50": {Sym: KeyF2, Mod: ModAlt}, - "kf51": {Sym: KeyF3, Mod: ModAlt}, - "kf52": {Sym: KeyF4, Mod: ModAlt}, - "kf53": {Sym: KeyF5, Mod: ModAlt}, - "kf54": {Sym: KeyF6, Mod: ModAlt}, - "kf55": {Sym: KeyF7, Mod: ModAlt}, - "kf56": {Sym: KeyF8, Mod: ModAlt}, - "kf57": {Sym: KeyF9, Mod: ModAlt}, - "kf58": {Sym: KeyF10, Mod: ModAlt}, - "kf59": {Sym: KeyF11, Mod: ModAlt}, - "kf60": {Sym: KeyF12, Mod: ModAlt}, - "kf61": {Sym: KeyF1, Mod: ModShift | ModAlt}, - "kf62": {Sym: KeyF2, Mod: ModShift | ModAlt}, - "kf63": {Sym: KeyF3, Mod: ModShift | ModAlt}, + "kf1": {Type: KeyF1}, + "kf2": {Type: KeyF2}, + "kf3": {Type: KeyF3}, + "kf4": {Type: KeyF4}, + "kf5": {Type: KeyF5}, + "kf6": {Type: KeyF6}, + "kf7": {Type: KeyF7}, + "kf8": {Type: KeyF8}, + "kf9": {Type: KeyF9}, + "kf10": {Type: KeyF10}, + "kf11": {Type: KeyF11}, + "kf12": {Type: KeyF12}, + "kf13": {Type: KeyF1, Mod: ModShift}, + "kf14": {Type: KeyF2, Mod: ModShift}, + "kf15": {Type: KeyF3, Mod: ModShift}, + "kf16": {Type: KeyF4, Mod: ModShift}, + "kf17": {Type: KeyF5, Mod: ModShift}, + "kf18": {Type: KeyF6, Mod: ModShift}, + "kf19": {Type: KeyF7, Mod: ModShift}, + "kf20": {Type: KeyF8, Mod: ModShift}, + "kf21": {Type: KeyF9, Mod: ModShift}, + "kf22": {Type: KeyF10, Mod: ModShift}, + "kf23": {Type: KeyF11, Mod: ModShift}, + "kf24": {Type: KeyF12, Mod: ModShift}, + "kf25": {Type: KeyF1, Mod: ModCtrl}, + "kf26": {Type: KeyF2, Mod: ModCtrl}, + "kf27": {Type: KeyF3, Mod: ModCtrl}, + "kf28": {Type: KeyF4, Mod: ModCtrl}, + "kf29": {Type: KeyF5, Mod: ModCtrl}, + "kf30": {Type: KeyF6, Mod: ModCtrl}, + "kf31": {Type: KeyF7, Mod: ModCtrl}, + "kf32": {Type: KeyF8, Mod: ModCtrl}, + "kf33": {Type: KeyF9, Mod: ModCtrl}, + "kf34": {Type: KeyF10, Mod: ModCtrl}, + "kf35": {Type: KeyF11, Mod: ModCtrl}, + "kf36": {Type: KeyF12, Mod: ModCtrl}, + "kf37": {Type: KeyF1, Mod: ModShift | ModCtrl}, + "kf38": {Type: KeyF2, Mod: ModShift | ModCtrl}, + "kf39": {Type: KeyF3, Mod: ModShift | ModCtrl}, + "kf40": {Type: KeyF4, Mod: ModShift | ModCtrl}, + "kf41": {Type: KeyF5, Mod: ModShift | ModCtrl}, + "kf42": {Type: KeyF6, Mod: ModShift | ModCtrl}, + "kf43": {Type: KeyF7, Mod: ModShift | ModCtrl}, + "kf44": {Type: KeyF8, Mod: ModShift | ModCtrl}, + "kf45": {Type: KeyF9, Mod: ModShift | ModCtrl}, + "kf46": {Type: KeyF10, Mod: ModShift | ModCtrl}, + "kf47": {Type: KeyF11, Mod: ModShift | ModCtrl}, + "kf48": {Type: KeyF12, Mod: ModShift | ModCtrl}, + "kf49": {Type: KeyF1, Mod: ModAlt}, + "kf50": {Type: KeyF2, Mod: ModAlt}, + "kf51": {Type: KeyF3, Mod: ModAlt}, + "kf52": {Type: KeyF4, Mod: ModAlt}, + "kf53": {Type: KeyF5, Mod: ModAlt}, + "kf54": {Type: KeyF6, Mod: ModAlt}, + "kf55": {Type: KeyF7, Mod: ModAlt}, + "kf56": {Type: KeyF8, Mod: ModAlt}, + "kf57": {Type: KeyF9, Mod: ModAlt}, + "kf58": {Type: KeyF10, Mod: ModAlt}, + "kf59": {Type: KeyF11, Mod: ModAlt}, + "kf60": {Type: KeyF12, Mod: ModAlt}, + "kf61": {Type: KeyF1, Mod: ModShift | ModAlt}, + "kf62": {Type: KeyF2, Mod: ModShift | ModAlt}, + "kf63": {Type: 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{Sym: KeyF13} - keys["kf14"] = Key{Sym: KeyF14} - keys["kf15"] = Key{Sym: KeyF15} - keys["kf16"] = Key{Sym: KeyF16} - keys["kf17"] = Key{Sym: KeyF17} - keys["kf18"] = Key{Sym: KeyF18} - keys["kf19"] = Key{Sym: KeyF19} - keys["kf20"] = Key{Sym: KeyF20} - keys["kf21"] = Key{Sym: KeyF21} - keys["kf22"] = Key{Sym: KeyF22} - keys["kf23"] = Key{Sym: KeyF23} - keys["kf24"] = Key{Sym: KeyF24} - keys["kf25"] = Key{Sym: KeyF25} - keys["kf26"] = Key{Sym: KeyF26} - keys["kf27"] = Key{Sym: KeyF27} - keys["kf28"] = Key{Sym: KeyF28} - keys["kf29"] = Key{Sym: KeyF29} - keys["kf30"] = Key{Sym: KeyF30} - keys["kf31"] = Key{Sym: KeyF31} - keys["kf32"] = Key{Sym: KeyF32} - keys["kf33"] = Key{Sym: KeyF33} - keys["kf34"] = Key{Sym: KeyF34} - keys["kf35"] = Key{Sym: KeyF35} - keys["kf36"] = Key{Sym: KeyF36} - keys["kf37"] = Key{Sym: KeyF37} - keys["kf38"] = Key{Sym: KeyF38} - keys["kf39"] = Key{Sym: KeyF39} - keys["kf40"] = Key{Sym: KeyF40} - keys["kf41"] = Key{Sym: KeyF41} - keys["kf42"] = Key{Sym: KeyF42} - keys["kf43"] = Key{Sym: KeyF43} - keys["kf44"] = Key{Sym: KeyF44} - keys["kf45"] = Key{Sym: KeyF45} - keys["kf46"] = Key{Sym: KeyF46} - keys["kf47"] = Key{Sym: KeyF47} - keys["kf48"] = Key{Sym: KeyF48} - keys["kf49"] = Key{Sym: KeyF49} - keys["kf50"] = Key{Sym: KeyF50} - keys["kf51"] = Key{Sym: KeyF51} - keys["kf52"] = Key{Sym: KeyF52} - keys["kf53"] = Key{Sym: KeyF53} - keys["kf54"] = Key{Sym: KeyF54} - keys["kf55"] = Key{Sym: KeyF55} - keys["kf56"] = Key{Sym: KeyF56} - keys["kf57"] = Key{Sym: KeyF57} - keys["kf58"] = Key{Sym: KeyF58} - keys["kf59"] = Key{Sym: KeyF59} - keys["kf60"] = Key{Sym: KeyF60} - keys["kf61"] = Key{Sym: KeyF61} - keys["kf62"] = Key{Sym: KeyF62} - keys["kf63"] = Key{Sym: KeyF63} + keys["kf13"] = Key{Type: KeyF13} + keys["kf14"] = Key{Type: KeyF14} + keys["kf15"] = Key{Type: KeyF15} + keys["kf16"] = Key{Type: KeyF16} + keys["kf17"] = Key{Type: KeyF17} + keys["kf18"] = Key{Type: KeyF18} + keys["kf19"] = Key{Type: KeyF19} + keys["kf20"] = Key{Type: KeyF20} + keys["kf21"] = Key{Type: KeyF21} + keys["kf22"] = Key{Type: KeyF22} + keys["kf23"] = Key{Type: KeyF23} + keys["kf24"] = Key{Type: KeyF24} + keys["kf25"] = Key{Type: KeyF25} + keys["kf26"] = Key{Type: KeyF26} + keys["kf27"] = Key{Type: KeyF27} + keys["kf28"] = Key{Type: KeyF28} + keys["kf29"] = Key{Type: KeyF29} + keys["kf30"] = Key{Type: KeyF30} + keys["kf31"] = Key{Type: KeyF31} + keys["kf32"] = Key{Type: KeyF32} + keys["kf33"] = Key{Type: KeyF33} + keys["kf34"] = Key{Type: KeyF34} + keys["kf35"] = Key{Type: KeyF35} + keys["kf36"] = Key{Type: KeyF36} + keys["kf37"] = Key{Type: KeyF37} + keys["kf38"] = Key{Type: KeyF38} + keys["kf39"] = Key{Type: KeyF39} + keys["kf40"] = Key{Type: KeyF40} + keys["kf41"] = Key{Type: KeyF41} + keys["kf42"] = Key{Type: KeyF42} + keys["kf43"] = Key{Type: KeyF43} + keys["kf44"] = Key{Type: KeyF44} + keys["kf45"] = Key{Type: KeyF45} + keys["kf46"] = Key{Type: KeyF46} + keys["kf47"] = Key{Type: KeyF47} + keys["kf48"] = Key{Type: KeyF48} + keys["kf49"] = Key{Type: KeyF49} + keys["kf50"] = Key{Type: KeyF50} + keys["kf51"] = Key{Type: KeyF51} + keys["kf52"] = Key{Type: KeyF52} + keys["kf53"] = Key{Type: KeyF53} + keys["kf54"] = Key{Type: KeyF54} + keys["kf55"] = Key{Type: KeyF55} + keys["kf56"] = Key{Type: KeyF56} + keys["kf57"] = Key{Type: KeyF57} + keys["kf58"] = Key{Type: KeyF58} + keys["kf59"] = Key{Type: KeyF59} + keys["kf60"] = Key{Type: KeyF60} + keys["kf61"] = Key{Type: KeyF61} + keys["kf62"] = Key{Type: KeyF62} + keys["kf63"] = Key{Type: KeyF63} } return keys diff --git a/tty.go b/tty.go index 304bb29166..9d5612cfbb 100644 --- a/tty.go +++ b/tty.go @@ -118,23 +118,23 @@ func readInputs(ctx context.Context, msgs chan<- Msg, reader *driver) error { k := KeyMsg{ Alt: e.Mod.HasAlt(), Runes: e.Runes, - Type: e.Sym, + Type: e.Type, } // Backwards compatibility for ctrl- and shift- keys switch { case e.Mod.HasCtrl() && e.Mod.HasShift(): - switch e.Sym { + switch e.Type { case KeyUp, KeyDown, KeyRight, KeyLeft: k.Runes = nil - k.Type = KeyCtrlShiftUp - e.Sym + KeyUp + k.Type = KeyCtrlShiftUp - e.Type + KeyUp case KeyHome, KeyEnd: k.Runes = nil - k.Type = KeyCtrlShiftHome - e.Sym + KeyHome + k.Type = KeyCtrlShiftHome - e.Type + KeyHome } case e.Mod.HasCtrl(): - switch e.Sym { - case KeyNone: // KeyRunes + switch e.Type { + case KeyRunes: // KeyRunes switch r := e.Rune(); r { case ' ': k.Runes = nil @@ -153,28 +153,28 @@ func readInputs(ctx context.Context, msgs chan<- Msg, reader *driver) error { } case KeyPgUp, KeyPgDown, KeyHome, KeyEnd: k.Runes = nil - k.Type = KeyCtrlPgUp - e.Sym + KeyPgUp + k.Type = KeyCtrlPgUp - e.Type + KeyPgUp case KeyUp, KeyDown, KeyRight, KeyLeft: k.Runes = nil - k.Type = KeyCtrlUp - e.Sym + KeyUp + k.Type = KeyCtrlUp - e.Type + KeyUp } case e.Mod.HasShift(): - switch e.Sym { + switch e.Type { case KeyTab: k.Runes = nil k.Type = KeyShiftTab case KeyUp, KeyDown, KeyRight, KeyLeft: k.Runes = nil - k.Type = KeyShiftUp - e.Sym + KeyUp + k.Type = KeyShiftUp - e.Type + KeyUp k.Runes = nil case KeyHome, KeyEnd: k.Runes = nil - k.Type = KeyShiftHome - e.Sym + KeyHome + k.Type = KeyShiftHome - e.Type + KeyHome } } switch k.Type { - case KeyNone: // KeyRunes + case KeyRunes: // KeyRunes if len(k.Runes) > 0 { incomingMsgs = append(incomingMsgs, k) } diff --git a/win32input.go b/win32input.go index 5d8b9902c7..526f61aba0 100644 --- a/win32input.go +++ b/win32input.go @@ -15,22 +15,22 @@ func parseWin32InputKeyEvent(vkc coninput.VirtualKeyCode, _ coninput.VirtualKeyC return nil case coninput.VK_MENU: if cks.Contains(coninput.LEFT_ALT_PRESSED) { - key = Key{Sym: KeyLeftAlt} + key = Key{Type: KeyLeftAlt} } else if cks.Contains(coninput.RIGHT_ALT_PRESSED) { - key = Key{Sym: KeyRightAlt} + key = Key{Type: KeyRightAlt} } else if !keyDown { return nil } case coninput.VK_CONTROL: if cks.Contains(coninput.LEFT_CTRL_PRESSED) { - key = Key{Sym: KeyLeftCtrl} + key = Key{Type: KeyLeftCtrl} } else if cks.Contains(coninput.RIGHT_CTRL_PRESSED) { - key = Key{Sym: KeyRightCtrl} + key = Key{Type: KeyRightCtrl} } else if !keyDown { return nil } case coninput.VK_CAPITAL: - key = Key{Sym: KeyCapsLock} + key = Key{Type: KeyCapsLock} default: var ok bool key, ok = vkKeyEvent[vkc] @@ -88,74 +88,74 @@ func parseWin32InputKeyEvent(vkc coninput.VirtualKeyCode, _ coninput.VirtualKeyC } var vkKeyEvent = map[coninput.VirtualKeyCode]Key{ - coninput.VK_RETURN: {Sym: KeyEnter}, - coninput.VK_BACK: {Sym: KeyBackspace}, - coninput.VK_TAB: {Sym: KeyTab}, - coninput.VK_ESCAPE: {Sym: KeyEscape}, - coninput.VK_SPACE: {Sym: KeySpace, Runes: []rune{' '}}, - coninput.VK_UP: {Sym: KeyUp}, - coninput.VK_DOWN: {Sym: KeyDown}, - coninput.VK_RIGHT: {Sym: KeyRight}, - coninput.VK_LEFT: {Sym: KeyLeft}, - coninput.VK_HOME: {Sym: KeyHome}, - coninput.VK_END: {Sym: KeyEnd}, - coninput.VK_PRIOR: {Sym: KeyPgUp}, - coninput.VK_NEXT: {Sym: KeyPgDown}, - coninput.VK_DELETE: {Sym: KeyDelete}, - coninput.VK_SELECT: {Sym: KeySelect}, - coninput.VK_SNAPSHOT: {Sym: KeyPrintScreen}, - coninput.VK_INSERT: {Sym: KeyInsert}, - coninput.VK_LWIN: {Sym: KeyLeftSuper}, - coninput.VK_RWIN: {Sym: KeyRightSuper}, - coninput.VK_APPS: {Sym: KeyMenu}, - coninput.VK_NUMPAD0: {Sym: KeyKp0}, - coninput.VK_NUMPAD1: {Sym: KeyKp1}, - coninput.VK_NUMPAD2: {Sym: KeyKp2}, - coninput.VK_NUMPAD3: {Sym: KeyKp3}, - coninput.VK_NUMPAD4: {Sym: KeyKp4}, - coninput.VK_NUMPAD5: {Sym: KeyKp5}, - coninput.VK_NUMPAD6: {Sym: KeyKp6}, - coninput.VK_NUMPAD7: {Sym: KeyKp7}, - coninput.VK_NUMPAD8: {Sym: KeyKp8}, - coninput.VK_NUMPAD9: {Sym: KeyKp9}, - coninput.VK_MULTIPLY: {Sym: KeyKpMultiply}, - coninput.VK_ADD: {Sym: KeyKpPlus}, - coninput.VK_SEPARATOR: {Sym: KeyKpComma}, - coninput.VK_SUBTRACT: {Sym: KeyKpMinus}, - coninput.VK_DECIMAL: {Sym: KeyKpDecimal}, - coninput.VK_DIVIDE: {Sym: KeyKpDivide}, - coninput.VK_F1: {Sym: KeyF1}, - coninput.VK_F2: {Sym: KeyF2}, - coninput.VK_F3: {Sym: KeyF3}, - coninput.VK_F4: {Sym: KeyF4}, - coninput.VK_F5: {Sym: KeyF5}, - coninput.VK_F6: {Sym: KeyF6}, - coninput.VK_F7: {Sym: KeyF7}, - coninput.VK_F8: {Sym: KeyF8}, - coninput.VK_F9: {Sym: KeyF9}, - coninput.VK_F10: {Sym: KeyF10}, - coninput.VK_F11: {Sym: KeyF11}, - coninput.VK_F12: {Sym: KeyF12}, - coninput.VK_F13: {Sym: KeyF13}, - coninput.VK_F14: {Sym: KeyF14}, - coninput.VK_F15: {Sym: KeyF15}, - coninput.VK_F16: {Sym: KeyF16}, - coninput.VK_F17: {Sym: KeyF17}, - coninput.VK_F18: {Sym: KeyF18}, - coninput.VK_F19: {Sym: KeyF19}, - coninput.VK_F20: {Sym: KeyF20}, - coninput.VK_F21: {Sym: KeyF21}, - coninput.VK_F22: {Sym: KeyF22}, - coninput.VK_F23: {Sym: KeyF23}, - coninput.VK_F24: {Sym: KeyF24}, - coninput.VK_NUMLOCK: {Sym: KeyNumLock}, - coninput.VK_SCROLL: {Sym: KeyScrollLock}, - coninput.VK_LSHIFT: {Sym: KeyLeftShift}, - coninput.VK_RSHIFT: {Sym: KeyRightShift}, - coninput.VK_LCONTROL: {Sym: KeyLeftCtrl}, - coninput.VK_RCONTROL: {Sym: KeyRightCtrl}, - coninput.VK_LMENU: {Sym: KeyLeftAlt}, - coninput.VK_RMENU: {Sym: KeyRightAlt}, + coninput.VK_RETURN: {Type: KeyEnter}, + coninput.VK_BACK: {Type: KeyBackspace}, + coninput.VK_TAB: {Type: KeyTab}, + coninput.VK_ESCAPE: {Type: KeyEscape}, + coninput.VK_SPACE: {Type: KeySpace, Runes: []rune{' '}}, + coninput.VK_UP: {Type: KeyUp}, + coninput.VK_DOWN: {Type: KeyDown}, + coninput.VK_RIGHT: {Type: KeyRight}, + coninput.VK_LEFT: {Type: KeyLeft}, + coninput.VK_HOME: {Type: KeyHome}, + coninput.VK_END: {Type: KeyEnd}, + coninput.VK_PRIOR: {Type: KeyPgUp}, + coninput.VK_NEXT: {Type: KeyPgDown}, + coninput.VK_DELETE: {Type: KeyDelete}, + coninput.VK_SELECT: {Type: KeySelect}, + coninput.VK_SNAPSHOT: {Type: KeyPrintScreen}, + coninput.VK_INSERT: {Type: KeyInsert}, + coninput.VK_LWIN: {Type: KeyLeftSuper}, + coninput.VK_RWIN: {Type: KeyRightSuper}, + coninput.VK_APPS: {Type: KeyMenu}, + coninput.VK_NUMPAD0: {Type: KeyKp0}, + coninput.VK_NUMPAD1: {Type: KeyKp1}, + coninput.VK_NUMPAD2: {Type: KeyKp2}, + coninput.VK_NUMPAD3: {Type: KeyKp3}, + coninput.VK_NUMPAD4: {Type: KeyKp4}, + coninput.VK_NUMPAD5: {Type: KeyKp5}, + coninput.VK_NUMPAD6: {Type: KeyKp6}, + coninput.VK_NUMPAD7: {Type: KeyKp7}, + coninput.VK_NUMPAD8: {Type: KeyKp8}, + coninput.VK_NUMPAD9: {Type: KeyKp9}, + coninput.VK_MULTIPLY: {Type: KeyKpMultiply}, + coninput.VK_ADD: {Type: KeyKpPlus}, + coninput.VK_SEPARATOR: {Type: KeyKpComma}, + coninput.VK_SUBTRACT: {Type: KeyKpMinus}, + coninput.VK_DECIMAL: {Type: KeyKpDecimal}, + coninput.VK_DIVIDE: {Type: KeyKpDivide}, + coninput.VK_F1: {Type: KeyF1}, + coninput.VK_F2: {Type: KeyF2}, + coninput.VK_F3: {Type: KeyF3}, + coninput.VK_F4: {Type: KeyF4}, + coninput.VK_F5: {Type: KeyF5}, + coninput.VK_F6: {Type: KeyF6}, + coninput.VK_F7: {Type: KeyF7}, + coninput.VK_F8: {Type: KeyF8}, + coninput.VK_F9: {Type: KeyF9}, + coninput.VK_F10: {Type: KeyF10}, + coninput.VK_F11: {Type: KeyF11}, + coninput.VK_F12: {Type: KeyF12}, + coninput.VK_F13: {Type: KeyF13}, + coninput.VK_F14: {Type: KeyF14}, + coninput.VK_F15: {Type: KeyF15}, + coninput.VK_F16: {Type: KeyF16}, + coninput.VK_F17: {Type: KeyF17}, + coninput.VK_F18: {Type: KeyF18}, + coninput.VK_F19: {Type: KeyF19}, + coninput.VK_F20: {Type: KeyF20}, + coninput.VK_F21: {Type: KeyF21}, + coninput.VK_F22: {Type: KeyF22}, + coninput.VK_F23: {Type: KeyF23}, + coninput.VK_F24: {Type: KeyF24}, + coninput.VK_NUMLOCK: {Type: KeyNumLock}, + coninput.VK_SCROLL: {Type: KeyScrollLock}, + coninput.VK_LSHIFT: {Type: KeyLeftShift}, + coninput.VK_RSHIFT: {Type: KeyRightShift}, + coninput.VK_LCONTROL: {Type: KeyLeftCtrl}, + coninput.VK_RCONTROL: {Type: KeyRightCtrl}, + coninput.VK_LMENU: {Type: KeyLeftAlt}, + coninput.VK_RMENU: {Type: KeyRightAlt}, coninput.VK_OEM_4: {Runes: []rune{'['}}, // TODO: add more keys } diff --git a/xterm.go b/xterm.go index 0c5706f500..f09caeba52 100644 --- a/xterm.go +++ b/xterm.go @@ -11,15 +11,15 @@ func parseXTermModifyOtherKeys(csi *ansi.CsiSequence) Msg { switch r { case ansi.BS: - return KeyPressMsg{Mod: mod, Sym: KeyBackspace} + return KeyPressMsg{Mod: mod, Type: KeyBackspace} case ansi.HT: - return KeyPressMsg{Mod: mod, Sym: KeyTab} + return KeyPressMsg{Mod: mod, Type: KeyTab} case ansi.CR: - return KeyPressMsg{Mod: mod, Sym: KeyEnter} + return KeyPressMsg{Mod: mod, Type: KeyEnter} case ansi.ESC: - return KeyPressMsg{Mod: mod, Sym: KeyEscape} + return KeyPressMsg{Mod: mod, Type: KeyEscape} case ansi.DEL: - return KeyPressMsg{Mod: mod, Sym: KeyBackspace} + return KeyPressMsg{Mod: mod, Type: KeyBackspace} } // CSI 27 ; ; ~ keys defined in XTerm modifyOtherKeys From e0865cfed36c594d6be4ea783548d03d8f6efeca Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 16 Aug 2024 16:51:34 -0400 Subject: [PATCH 17/18] refactor: simplify key modifier matching Use bitmap matching to check if a modifier contains another --- key.go | 12 +++++----- mod.go | 53 +++++++++------------------------------------ mouse.go | 6 ++--- mouse_deprecated.go | 6 ++--- tty.go | 8 +++---- 5 files changed, 26 insertions(+), 59 deletions(-) diff --git a/key.go b/key.go index 9997321037..e8c9c3c06f 100644 --- a/key.go +++ b/key.go @@ -323,22 +323,22 @@ func (k Key) Rune() rune { // "shift+ctrl+alt+a". func (k Key) String() string { var s string - if k.Mod.HasCtrl() && k.Type != KeyLeftCtrl && k.Type != KeyRightCtrl { + if k.Mod.Contains(ModCtrl) && k.Type != KeyLeftCtrl && k.Type != KeyRightCtrl { s += "ctrl+" } - if k.Mod.HasAlt() && k.Type != KeyLeftAlt && k.Type != KeyRightAlt { + if k.Mod.Contains(ModAlt) && k.Type != KeyLeftAlt && k.Type != KeyRightAlt { s += "alt+" } - if k.Mod.HasShift() && k.Type != KeyLeftShift && k.Type != KeyRightShift { + if k.Mod.Contains(ModShift) && k.Type != KeyLeftShift && k.Type != KeyRightShift { s += "shift+" } - if k.Mod.HasMeta() && k.Type != KeyLeftMeta && k.Type != KeyRightMeta { + if k.Mod.Contains(ModMeta) && k.Type != KeyLeftMeta && k.Type != KeyRightMeta { s += "meta+" } - if k.Mod.HasHyper() && k.Type != KeyLeftHyper && k.Type != KeyRightHyper { + if k.Mod.Contains(ModHyper) && k.Type != KeyLeftHyper && k.Type != KeyRightHyper { s += "hyper+" } - if k.Mod.HasSuper() && k.Type != KeyLeftSuper && k.Type != KeyRightSuper { + if k.Mod.Contains(ModSuper) && k.Type != KeyLeftSuper && k.Type != KeyRightSuper { s += "super+" } diff --git a/mod.go b/mod.go index a93ec72df4..9bb43a0cb3 100644 --- a/mod.go +++ b/mod.go @@ -24,47 +24,14 @@ const ( ModScrollLock // Defined in Windows API only ) -// HasShift reports whether the Shift modifier is set. -func (m KeyMod) HasShift() bool { - return m&ModShift != 0 -} - -// HasAlt reports whether the Alt modifier is set. -func (m KeyMod) HasAlt() bool { - return m&ModAlt != 0 -} - -// HasCtrl reports whether the Ctrl modifier is set. -func (m KeyMod) HasCtrl() bool { - return m&ModCtrl != 0 -} - -// HasMeta reports whether the Meta modifier is set. -func (m KeyMod) HasMeta() bool { - return m&ModMeta != 0 -} - -// HasHyper reports whether the Hyper modifier is set. -func (m KeyMod) HasHyper() bool { - return m&ModHyper != 0 -} - -// HasSuper reports whether the Super modifier is set. -func (m KeyMod) HasSuper() bool { - return m&ModSuper != 0 -} - -// HasCapsLock reports whether the CapsLock key is enabled. -func (m KeyMod) HasCapsLock() bool { - return m&ModCapsLock != 0 -} - -// HasNumLock reports whether the NumLock key is enabled. -func (m KeyMod) HasNumLock() bool { - return m&ModNumLock != 0 -} - -// HasScrollLock reports whether the ScrollLock key is enabled. -func (m KeyMod) HasScrollLock() bool { - return m&ModScrollLock != 0 +// 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/mouse.go b/mouse.go index 710770d2bb..f3460fa8f8 100644 --- a/mouse.go +++ b/mouse.go @@ -63,13 +63,13 @@ type Mouse struct { // String returns a string representation of the mouse message. func (m Mouse) String() (s string) { - if m.Mod.HasCtrl() { + if m.Mod.Contains(ModCtrl) { s += "ctrl+" } - if m.Mod.HasAlt() { + if m.Mod.Contains(ModAlt) { s += "alt+" } - if m.Mod.HasShift() { + if m.Mod.Contains(ModShift) { s += "shift+" } diff --git a/mouse_deprecated.go b/mouse_deprecated.go index fb2c41da6c..714b63fd22 100644 --- a/mouse_deprecated.go +++ b/mouse_deprecated.go @@ -154,9 +154,9 @@ func toMouseMsg(m Mouse) MouseMsg { return MouseMsg{ X: m.X, Y: m.Y, - Shift: m.Mod.HasShift(), - Alt: m.Mod.HasAlt(), - Ctrl: m.Mod.HasCtrl(), + Shift: m.Mod.Contains(ModShift), + Alt: m.Mod.Contains(ModAlt), + Ctrl: m.Mod.Contains(ModCtrl), Button: m.Button, } } diff --git a/tty.go b/tty.go index 9d5612cfbb..9ec3d2e03f 100644 --- a/tty.go +++ b/tty.go @@ -116,14 +116,14 @@ func readInputs(ctx context.Context, msgs chan<- Msg, reader *driver) error { incomingMsgs = append(incomingMsgs, k) case KeyPressMsg: k := KeyMsg{ - Alt: e.Mod.HasAlt(), + Alt: e.Mod.Contains(ModAlt), Runes: e.Runes, Type: e.Type, } // Backwards compatibility for ctrl- and shift- keys switch { - case e.Mod.HasCtrl() && e.Mod.HasShift(): + case e.Mod.Contains(ModCtrl | ModShift): switch e.Type { case KeyUp, KeyDown, KeyRight, KeyLeft: k.Runes = nil @@ -132,7 +132,7 @@ func readInputs(ctx context.Context, msgs chan<- Msg, reader *driver) error { k.Runes = nil k.Type = KeyCtrlShiftHome - e.Type + KeyHome } - case e.Mod.HasCtrl(): + case e.Mod.Contains(ModCtrl): switch e.Type { case KeyRunes: // KeyRunes switch r := e.Rune(); r { @@ -158,7 +158,7 @@ func readInputs(ctx context.Context, msgs chan<- Msg, reader *driver) error { k.Runes = nil k.Type = KeyCtrlUp - e.Type + KeyUp } - case e.Mod.HasShift(): + case e.Mod.Contains(ModShift): switch e.Type { case KeyTab: k.Runes = nil From 3f5fb9a3f9d6b5f84330f78cd7879d0e40d5bbe9 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 19 Aug 2024 10:27:20 -0400 Subject: [PATCH 18/18] fix(lint): reorder key struct to fix fieldalignment --- key.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/key.go b/key.go index e8c9c3c06f..33679414f2 100644 --- a/key.go +++ b/key.go @@ -226,14 +226,14 @@ const ( // one character, though certain input method editors (most notably Chinese // IMEs) can input multiple runes at once. type Key struct { - // Type is a special key, like enter, tab, backspace, and so on. - Type KeyType - // Runes contains the actual characters received. This usually has a length // of 1. Use [Rune()] to get the first key rune received. If the user // presses shift+a, the Runes will be `[]rune{'A'}`. Runes []rune + // Type is a special key, like enter, tab, backspace, and so on. + Type KeyType + // altRune is the actual, unshifted key pressed by the user. For example, // if the user presses shift+a, or caps lock is on, the altRune will be // 'a'.