diff --git a/ansi/mouse.go b/ansi/mouse.go index bae52b71..09dfe62b 100644 --- a/ansi/mouse.go +++ b/ansi/mouse.go @@ -4,6 +4,134 @@ import ( "fmt" ) +// MouseButton represents the button that was pressed during a mouse message. +type MouseButton byte + +// Mouse event buttons +// +// This is based on X11 mouse button codes. +// +// 1 = left button +// 2 = middle button (pressing the scroll wheel) +// 3 = right button +// 4 = turn scroll wheel up +// 5 = turn scroll wheel down +// 6 = push scroll wheel left +// 7 = push scroll wheel right +// 8 = 4th button (aka browser backward button) +// 9 = 5th button (aka browser forward button) +// 10 +// 11 +// +// Other buttons are not supported. +const ( + MouseNone MouseButton = iota + MouseLeft + MouseMiddle + MouseRight + MouseWheelUp + MouseWheelDown + MouseWheelLeft + MouseWheelRight + MouseBackward + MouseForward + MouseButton10 + MouseButton11 + + MouseRelease = MouseNone +) + +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", + MouseButton10: "button10", + MouseButton11: "button11", +} + +// String returns a string representation of the mouse button. +func (b MouseButton) String() string { + return mouseButtons[b] +} + +// Button returns a byte representing a mouse button. +// The button is a bitmask of the following leftmost values: +// +// - The first two bits are the button number: +// 0 = left button, wheel up, or button no. 8 aka (backwards) +// 1 = middle button, wheel down, or button no. 9 aka (forwards) +// 2 = right button, wheel left, or button no. 10 +// 3 = release event, wheel right, or button no. 11 +// +// - The third bit indicates whether the shift key was pressed. +// +// - The fourth bit indicates the alt key was pressed. +// +// - The fifth bit indicates the control key was pressed. +// +// - The sixth bit indicates motion events. Combined with button number 3, i.e. +// release event, it represents a drag event. +// +// - The seventh bit indicates a wheel event. +// +// - The eighth bit indicates additional buttons. +// +// If button is [MouseRelease], and motion is false, this returns a release +// event. If button is undefined, this function returns 0xff. +func (b MouseButton) Button(motion, shift, alt, ctrl bool) (m byte) { + // mouse bit shifts + const ( + bitShift = 0b0000_0100 + bitAlt = 0b0000_1000 + bitCtrl = 0b0001_0000 + bitMotion = 0b0010_0000 + bitWheel = 0b0100_0000 + bitAdd = 0b1000_0000 // additional buttons 8-11 + + bitsMask = 0b0000_0011 + ) + + if b == MouseRelease { + m = bitsMask + } else if b >= MouseLeft && b <= MouseRight { + m = byte(b - MouseLeft) + } else if b >= MouseWheelUp && b <= MouseWheelRight { + m = byte(b - MouseWheelUp) + m |= bitWheel + } else if b >= MouseBackward && b <= MouseButton11 { + m = byte(b - MouseBackward) + m |= bitAdd + } else { + m = 0xff // invalid button + } + + if shift { + m |= bitShift + } + if alt { + m |= bitAlt + } + if ctrl { + m |= bitCtrl + } + if motion { + m |= bitMotion + } + + return +} + +// x10Offset is the offset for X10 mouse events. +// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking +const x10Offset = 32 + // MouseX10 returns an escape sequence representing a mouse event in X10 mode. // Note that this requires the terminal support X10 mouse modes. // @@ -11,7 +139,6 @@ import ( // // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking func MouseX10(b byte, x, y int) string { - const x10Offset = 32 return "\x1b[M" + string(b+x10Offset) + string(byte(x)+x10Offset+1) + string(byte(y)+x10Offset+1) } @@ -22,9 +149,9 @@ func MouseX10(b byte, x, y int) string { // // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking func MouseSgr(b byte, x, y int, release bool) string { - s := "M" + s := 'M' if release { - s = "m" + s = 'm' } if x < 0 { x = -x @@ -32,5 +159,5 @@ func MouseSgr(b byte, x, y int, release bool) string { if y < 0 { y = -y } - return fmt.Sprintf("\x1b[<%d;%d;%d%s", b, x+1, y+1, s) + return fmt.Sprintf("\x1b[<%d;%d;%d%c", b, x+1, y+1, s) } diff --git a/ansi/mouse_test.go b/ansi/mouse_test.go new file mode 100644 index 00000000..35abfc08 --- /dev/null +++ b/ansi/mouse_test.go @@ -0,0 +1,237 @@ +package ansi + +import ( + "fmt" + "testing" +) + +func TestMouseButton(t *testing.T) { + type test struct { + name string + btn MouseButton + motion, shift, alt, ctrl bool + want byte + } + + cases := []test{ + { + name: "mouse release", + btn: MouseRelease, + want: 0b0000_0011, + }, + { + name: "mouse left", + btn: MouseLeft, + want: 0b0000_0000, + }, + { + name: "mouse right", + btn: MouseRight, + want: 0b0000_0010, + }, + { + name: "mouse wheel up", + btn: MouseWheelUp, + want: 0b0100_0000, + }, + { + name: "mouse wheel right", + btn: MouseWheelRight, + want: 0b0100_0011, + }, + { + name: "mouse backward", + btn: MouseBackward, + want: 0b1000_0000, + }, + { + name: "mouse forward", + btn: MouseForward, + want: 0b1000_0001, + }, + { + name: "mouse button 10", + btn: MouseButton10, + want: 0b1000_0010, + }, + { + name: "mouse button 11", + btn: MouseButton11, + want: 0b1000_0011, + }, + { + name: "mouse middle with motion", + btn: MouseMiddle, + motion: true, + want: 0b0010_0001, + }, + { + name: "mouse middle with shift", + btn: MouseMiddle, + shift: true, + want: 0b0000_0101, + }, + { + name: "mouse middle with motion and alt", + btn: MouseMiddle, + motion: true, + alt: true, + want: 0b0010_1001, + }, + { + name: "mouse right with shift, alt, and ctrl", + btn: MouseRight, + shift: true, + alt: true, + ctrl: true, + want: 0b0001_1110, + }, + { + name: "mouse button 10 with motion, shift, alt, and ctrl", + btn: MouseButton10, + motion: true, + shift: true, + alt: true, + ctrl: true, + want: 0b1011_1110, + }, + { + name: "mouse left with motion, shift, and ctrl", + btn: MouseLeft, + motion: true, + shift: true, + ctrl: true, + want: 0b0011_0100, + }, + { + name: "invalid mouse button", + btn: MouseButton(0xff), + want: 0b1111_1111, + }, + { + name: "mouse wheel down with motion", + btn: MouseWheelDown, + motion: true, + want: 0b0110_0001, + }, + { + name: "mouse wheel down with shift and ctrl", + btn: MouseWheelDown, + shift: true, + ctrl: true, + want: 0b0101_0101, + }, + { + name: "mouse wheel left with alt", + btn: MouseWheelLeft, + alt: true, + want: 0b0100_1010, + }, + { + name: "mouse middle with all modifiers", + btn: MouseMiddle, + motion: true, + shift: true, + alt: true, + ctrl: true, + want: 0b0011_1101, + }, + } + + for i, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.btn.Button(tc.motion, tc.shift, tc.alt, tc.ctrl) + if got != tc.want { + t.Errorf("test %d: got %08b; want %08b", i+1, got, tc.want) + } + }) + } +} + +func TestMouseSgr(t *testing.T) { + type test struct { + name string + btn byte + x, y int + release bool + } + + cases := []test{ + { + name: "mouse left", + btn: MouseLeft.Button(false, false, false, false), + x: 0, + y: 0, + }, + { + name: "wheel down", + btn: MouseWheelDown.Button(false, false, false, false), + x: 1, + y: 10, + }, + { + name: "mouse right with shift, alt, and ctrl", + btn: MouseRight.Button(false, true, true, true), + x: 10, + y: 1, + }, + { + name: "mouse release", + btn: MouseRelease.Button(false, false, false, false), + x: 5, + y: 5, + release: true, + }, + { + name: "mouse button 10 with motion, shift, alt, and ctrl", + btn: MouseButton10.Button(true, true, true, true), + x: 10, + y: 10, + }, + { + name: "mouse wheel up with motion", + btn: MouseWheelUp.Button(true, false, false, false), + x: 15, + y: 15, + }, + { + name: "mouse middle with all modifiers", + btn: MouseMiddle.Button(true, true, true, true), + x: 20, + y: 20, + }, + { + name: "mouse wheel left at max coordinates", + btn: MouseWheelLeft.Button(false, false, false, false), + x: 223, + y: 223, + }, + { + name: "mouse forward release", + btn: MouseForward.Button(false, false, false, false), + x: 100, + y: 100, + release: true, + }, + { + name: "mouse backward with shift and ctrl", + btn: MouseBackward.Button(false, true, false, true), + x: 50, + y: 50, + }, + } + + for i, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + m := MouseSgr(tc.btn, tc.x, tc.y, tc.release) + action := 'M' + if tc.release { + action = 'm' + } + want := fmt.Sprintf("\x1b[<%d;%d;%d%c", tc.btn, tc.x+1, tc.y+1, action) + if m != want { + t.Errorf("test %d: got %q; want %q", i+1, m, want) + } + }) + } +} diff --git a/go.work.sum b/go.work.sum index 10dc7ff9..a7f7546b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,5 +1,6 @@ github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/x/ansi v0.5.0/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/input v0.2.0/go.mod h1:KUSFIS6uQymtnr5lHVSOK9j8RvwTD4YHnWnzJUYnd/M= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= diff --git a/input/cancelreader_windows.go b/input/cancelreader_windows.go index 8e502119..20e9a71a 100644 --- a/input/cancelreader_windows.go +++ b/input/cancelreader_windows.go @@ -10,7 +10,7 @@ import ( "sync" "time" - "github.com/erikgeiser/coninput" + xwindows "github.com/charmbracelet/x/windows" "github.com/muesli/cancelreader" "golang.org/x/sys/windows" ) @@ -43,11 +43,16 @@ func newCancelreader(r io.Reader) (cancelreader.CancelReader, error) { return fallback(r) } - conin, err := coninput.NewStdinHandle() + conin, err := windows.GetStdHandle(windows.STD_INPUT_HANDLE) if err != nil { return fallback(r) } + // Discard any pending input events. + if err := xwindows.FlushConsoleInputBuffer(conin); err != nil { + return fallback(r) + } + originalMode, err := prepareConsole(conin, windows.ENABLE_MOUSE_INPUT, windows.ENABLE_WINDOW_INPUT, @@ -136,7 +141,10 @@ func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32, return 0, fmt.Errorf("get console mode: %w", err) } - newMode := coninput.AddInputModes(0, modes...) + var newMode uint32 + for _, mode := range modes { + newMode |= mode + } err = windows.SetConsoleMode(input, newMode) if err != nil { @@ -210,7 +218,7 @@ func (r overlappedReader) Read(data []byte) (int, error) { err = windows.GetOverlappedResult(windows.Handle(r), &overlapped, &n, true) if err != nil { - return int(n), nil + return int(n), err } return int(n), nil diff --git a/input/clipboard.go b/input/clipboard.go index c1c9fbeb..451238f4 100644 --- a/input/clipboard.go +++ b/input/clipboard.go @@ -1,9 +1,23 @@ package input -// ClipboardEvent is a clipboard read event. -type ClipboardEvent string +// ClipboardSelection represents a clipboard selection. The most common +// clipboard selections are "system" and "primary" and selections. +type ClipboardSelection byte -// String returns the string representation of the clipboard event. +// Clipboard selections. +const ( + SystemClipboard = ClipboardSelection('c') + PrimaryClipboard = ClipboardSelection('p') +) + +// ClipboardEvent is a clipboard read message event. This message is emitted when +// a terminal receives an OSC52 clipboard read message event. +type ClipboardEvent struct { + Content string + Selection ClipboardSelection +} + +// String returns the string representation of the clipboard message. func (e ClipboardEvent) String() string { - return string(e) + return e.Content } diff --git a/input/color.go b/input/color.go index 309e8c9d..7120bcf2 100644 --- a/input/color.go +++ b/input/color.go @@ -3,34 +3,54 @@ package input import ( "fmt" "image/color" - "strconv" - "strings" + "math" ) -// ForegroundColorEvent represents a foreground color change event. +// ForegroundColorEvent represents a foreground color event. This event is +// emitted when the terminal requests the terminal foreground color using +// [ansi.RequestForegroundColor]. type ForegroundColorEvent struct{ color.Color } -// String implements fmt.Stringer. +// String returns the hex representation of the color. func (e ForegroundColorEvent) String() string { - return colorToHex(e) + return colorToHex(e.Color) +} + +// IsDark returns whether the color is dark. +func (e ForegroundColorEvent) IsDark() bool { + return isDarkColor(e.Color) } -// BackgroundColorEvent represents a background color change event. +// BackgroundColorEvent represents a background color event. This event is +// emitted when the terminal requests the terminal background color using +// [ansi.RequestBackgroundColor]. type BackgroundColorEvent struct{ color.Color } -// String implements fmt.Stringer. +// String returns the hex representation of the color. func (e BackgroundColorEvent) String() string { return colorToHex(e) } -// CursorColorEvent represents a cursor color change event. +// IsDark returns whether the color is dark. +func (e BackgroundColorEvent) IsDark() bool { + return isDarkColor(e.Color) +} + +// CursorColorEvent represents a cursor color change event. This event is +// emitted when the program requests the terminal cursor color using +// [ansi.RequestCursorColor]. type CursorColorEvent struct{ color.Color } -// String implements fmt.Stringer. +// String returns the hex representation of the color. func (e CursorColorEvent) String() string { return colorToHex(e) } +// IsDark returns whether the color is dark. +func (e CursorColorEvent) IsDark() bool { + return isDarkColor(e) +} + type shiftable interface { ~uint | ~uint16 | ~uint32 | ~uint64 } @@ -43,35 +63,75 @@ func shift[T shiftable](x T) T { } func colorToHex(c color.Color) string { + if c == nil { + return "" + } r, g, b, _ := c.RGBA() return fmt.Sprintf("#%02x%02x%02x", shift(r), shift(g), shift(b)) } -func xParseColor(s string) color.Color { - switch { - case strings.HasPrefix(s, "rgb:"): - parts := strings.Split(s[4:], "/") - if len(parts) != 3 { - return color.Black - } +func getMaxMin(a, b, c float64) (max, min float64) { //nolint:predeclared + // TODO: use go1.21 min/max functions + if a > b { + max = a + min = b + } else { + max = b + min = a + } + if c > max { + max = c + } else if c < min { + min = c + } + return max, min +} - r, _ := strconv.ParseUint(parts[0], 16, 32) - g, _ := strconv.ParseUint(parts[1], 16, 32) - b, _ := strconv.ParseUint(parts[2], 16, 32) +func round(x float64) float64 { + return math.Round(x*1000) / 1000 +} - 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 +// rgbToHSL converts an RGB triple to an HSL triple. +func rgbToHSL(r, g, b uint8) (h, s, l float64) { + // convert uint32 pre-multiplied value to uint8 + // The r,g,b values are divided by 255 to change the range from 0..255 to 0..1: + Rnot := float64(r) / 255 + Gnot := float64(g) / 255 + Bnot := float64(b) / 255 + Cmax, Cmin := getMaxMin(Rnot, Gnot, Bnot) + Δ := Cmax - Cmin + // Lightness calculation: + l = (Cmax + Cmin) / 2 + // Hue and Saturation Calculation: + if Δ == 0 { + h = 0 + s = 0 + } else { + switch Cmax { + case Rnot: + h = 60 * (math.Mod((Gnot-Bnot)/Δ, 6)) + case Gnot: + h = 60 * (((Bnot - Rnot) / Δ) + 2) + case Bnot: + h = 60 * (((Rnot - Gnot) / Δ) + 4) + } + if h < 0 { + h += 360 } - 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) + s = Δ / (1 - math.Abs((2*l)-1)) + } - return color.RGBA{uint8(shift(r)), uint8(shift(g)), uint8(shift(b)), uint8(shift(a))} + return h, round(s), round(l) +} + +// isDarkColor returns whether the given color is dark. +func isDarkColor(c color.Color) bool { + if c == nil { + return true } - return color.Black + + r, g, b, _ := c.RGBA() + _, _, l := rgbToHSL(uint8(r>>8), uint8(g>>8), uint8(b>>8)) //nolint:gosec + return l < 0.5 } diff --git a/input/cursor.go b/input/cursor.go index c5d0fbb1..cf4e973d 100644 --- a/input/cursor.go +++ b/input/cursor.go @@ -1,10 +1,7 @@ package input -// CursorPositionEvent represents a cursor position event. -type CursorPositionEvent struct { - // Row is the row number. - Row int +import "image" - // Column is the column number. - Column int -} +// CursorPositionEvent represents a cursor position event. Where X is the +// zero-based column and Y is the zero-based row. +type CursorPositionEvent image.Point diff --git a/input/da1.go b/input/da1.go index 7fb2f308..fd96eeeb 100644 --- a/input/da1.go +++ b/input/da1.go @@ -2,15 +2,16 @@ package input import "github.com/charmbracelet/x/ansi" -// PrimaryDeviceAttributesEvent represents a primary device attributes event. -type PrimaryDeviceAttributesEvent []uint +// PrimaryDeviceAttributesEvent is an event that represents the terminal +// primary device attributes. +type PrimaryDeviceAttributesEvent []int func parsePrimaryDevAttrs(csi *ansi.CsiSequence) Event { // Primary Device Attributes da1 := make(PrimaryDeviceAttributesEvent, len(csi.Params)) for i, p := range csi.Params { - if !ansi.Parameter(p).HasMore() { - da1[i] = uint(p) + if !p.HasMore() { + da1[i] = p.Param(0) } } return da1 diff --git a/input/driver.go b/input/driver.go index 5492b348..10a35d92 100644 --- a/input/driver.go +++ b/input/driver.go @@ -3,16 +3,33 @@ package input import ( "bytes" "io" + "log" "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 { +// win32InputState is a state machine for parsing key events from the Windows +// Console API into escape sequences and utf8 runes, and keeps track of the last +// control key state to determine modifier key changes. It also keeps track of +// the last mouse button state and window size changes to determine which mouse +// buttons were released and to prevent multiple size events from firing. +// +//nolint:unused +type win32InputState struct { + ansiBuf [256]byte + ansiIdx int + utf16Buf [2]rune + utf16Half bool + lastCks uint32 // the last control key state for the previous event + lastMouseBtns uint32 // the last mouse button state for the previous event + lastWinsizeX, lastWinsizeY int16 // the last window size for the previous event to prevent multiple size events from firing +} + +// Reader represents an input event reader. It reads input events and parses +// escape sequences from the terminal input buffer and translates them into +// human-readable events. +type Reader struct { rd cancelreader.CancelReader table map[string]Key // table is a lookup table for key sequences. @@ -24,46 +41,56 @@ type Driver struct { 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 + // keyState keeps track of the current Windows Console API key events state. + // It is used to decode ANSI escape sequences and utf16 sequences. + keyState win32InputState //nolint:unused - flags int // control the behavior of the driver. + parser Parser + trace bool // trace enables input tracing and logging. } -// 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) +// NewReader returns a new input event reader. The reader reads input events +// from the terminal and parses escape sequences into human-readable events. It +// supports reading Terminfo databases. See [Parser] for more information. +// +// Example: +// +// r, _ := input.NewReader(os.Stdin, os.Getenv("TERM"), 0) +// defer r.Close() +// events, _ := r.ReadEvents() +// for _, ev := range events { +// log.Printf("%v", ev) +// } +func NewReader(r io.Reader, termType string, flags int) (*Reader, error) { + d := new(Reader) cr, err := newCancelreader(r) if err != nil { return nil, err } d.rd = cr - d.table = buildKeysTable(flags, term) - d.term = term - d.flags = flags + d.table = buildKeysTable(flags, termType) + d.term = termType + d.parser.flags = flags return d, nil } +// Read implements [io.Reader]. +func (d *Reader) Read(p []byte) (int, error) { + return d.rd.Read(p) +} + // Cancel cancels the underlying reader. -func (d *Driver) Cancel() bool { +func (d *Reader) Cancel() bool { return d.rd.Cancel() } // Close closes the underlying reader. -func (d *Driver) Close() error { +func (d *Reader) Close() error { return d.rd.Close() } -func (d *Driver) readEvents() (e []Event, err error) { +func (d *Reader) readEvents() (Events []Event, err error) { nb, err := d.rd.Read(d.buf[:]) if err != nil { return nil, err @@ -74,14 +101,17 @@ func (d *Driver) readEvents() (e []Event, err error) { // Lookup table first if bytes.HasPrefix(buf, []byte{'\x1b'}) { if k, ok := d.table[string(buf)]; ok { - e = append(e, KeyPressEvent(k)) + Events = append(Events, KeyPressEvent(k)) return } } var i int for i < len(buf) { - nb, ev := ParseSequence(buf[i:]) + nb, ev := d.parser.parseSequence(buf[i:]) + if d.trace { + log.Printf("input: %q", buf[i:i+nb]) + } // Handle bracketed-paste if d.paste != nil { @@ -93,7 +123,7 @@ func (d *Driver) readEvents() (e []Event, err error) { } switch ev.(type) { - case UnknownCsiEvent, UnknownSs3Event, UnknownEvent: + case UnknownEvent: // 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 = KeyPressEvent(k) @@ -111,16 +141,16 @@ func (d *Driver) readEvents() (e []Event, err error) { d.paste = d.paste[w:] } d.paste = nil // reset the buffer - e = append(e, PasteEvent(paste)) + Events = append(Events, PasteEvent(paste)) case nil: i++ continue } if mevs, ok := ev.(MultiEvent); ok { - e = append(e, []Event(mevs)...) + Events = append(Events, []Event(mevs)...) } else { - e = append(e, ev) + Events = append(Events, ev) } i += nb } diff --git a/input/driver_other.go b/input/driver_other.go index 7092aa31..fd3df06c 100644 --- a/input/driver_other.go +++ b/input/driver_other.go @@ -6,6 +6,12 @@ package input // ReadEvents reads input events from the terminal. // // It reads the events available in the input buffer and returns them. -func (d *Driver) ReadEvents() ([]Event, error) { +func (d *Reader) ReadEvents() ([]Event, error) { return d.readEvents() } + +// parseWin32InputKeyEvent parses a Win32 input key events. This function is +// only available on Windows. +func (p *Parser) parseWin32InputKeyEvent(*win32InputState, uint16, uint16, rune, bool, uint32, uint16) Event { + return nil +} diff --git a/input/driver_test.go b/input/driver_test.go index 7b875779..affdf5b8 100644 --- a/input/driver_test.go +++ b/input/driver_test.go @@ -9,7 +9,7 @@ import ( 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) + drv, err := NewReader(rdr, "dumb", 0) if err != nil { b.Fatalf("could not create driver: %v", err) } diff --git a/input/driver_windows.go b/input/driver_windows.go index 5d1910ab..d997803f 100644 --- a/input/driver_windows.go +++ b/input/driver_windows.go @@ -6,18 +6,21 @@ package input import ( "errors" "fmt" + "strings" + "unicode" "unicode/utf16" + "unicode/utf8" "github.com/charmbracelet/x/ansi" - "github.com/erikgeiser/coninput" + xwindows "github.com/charmbracelet/x/windows" "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() ([]Event, error) { - events, err := d.handleConInput(coninput.ReadConsoleInput) +func (d *Reader) ReadEvents() ([]Event, error) { + events, err := d.handleConInput(readConsoleInput) if errors.Is(err, errNotConInputReader) { return d.readEvents() } @@ -26,8 +29,8 @@ func (d *Driver) ReadEvents() ([]Event, error) { var errNotConInputReader = fmt.Errorf("handleConInput: not a conInputReader") -func (d *Driver) handleConInput( - finput func(windows.Handle, []coninput.InputRecord) (uint32, error), +func (d *Reader) handleConInput( + finput func(windows.Handle, []xwindows.InputRecord) (uint32, error), ) ([]Event, error) { cc, ok := d.rd.(*conInputReader) if !ok { @@ -36,7 +39,7 @@ func (d *Driver) handleConInput( // read up to 256 events, this is to allow for sequences events reported as // key events. - var events [256]coninput.InputRecord + var events [256]xwindows.InputRecord _, err := finput(cc.conin, events[:]) if err != nil { return nil, fmt.Errorf("read coninput events: %w", err) @@ -44,150 +47,52 @@ func (d *Driver) handleConInput( var evs []Event 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 []Event) []Event { - var newEvents []Event - start, end := -1, -1 - -loop: - for i, e := range events { - switch e := e.(type) { - case KeyPressEvent: - switch e.Rune { - case ansi.ESC, ansi.CSI, ansi.OSC, ansi.DCS, ansi.APC: - // start of a sequence - if start == -1 { - start = i - } + if e := d.parser.parseConInputEvent(event, &d.keyState); e != nil { + if multi, ok := e.(MultiEvent); ok { + evs = append(evs, multi...) + } else { + evs = append(evs, e) } - 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 KeyPressEvent: - seq = append(seq, byte(e.Rune)) - } - } - - n, seqevent := ParseSequence(seq) - switch seqevent.(type) { - case UnknownEvent: - // 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 + return evs, nil } -func parseConInputEvent(event coninput.InputRecord, ps *coninput.ButtonState, ws *coninput.WindowBufferSizeEventRecord) Event { - 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 KeyPressEvent: - key = Key(event) - case KeyReleaseEvent: - 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 key.Rune == 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 KeyPressEvent(key) - } - - return KeyReleaseEvent(key) - - case coninput.WindowBufferSizeEventRecord: - if e != *ws { - *ws = e +func (p *Parser) parseConInputEvent(event xwindows.InputRecord, keyState *win32InputState) Event { + switch event.EventType { + case xwindows.KEY_EVENT: + kevent := event.KeyEvent() + return p.parseWin32InputKeyEvent(keyState, kevent.VirtualKeyCode, kevent.VirtualScanCode, + kevent.Char, kevent.KeyDown, kevent.ControlKeyState, kevent.RepeatCount) + + case xwindows.WINDOW_BUFFER_SIZE_EVENT: + wevent := event.WindowBufferSizeEvent() + if wevent.Size.X != keyState.lastWinsizeX || wevent.Size.Y != keyState.lastWinsizeY { + keyState.lastWinsizeX, keyState.lastWinsizeY = wevent.Size.X, wevent.Size.Y return WindowSizeEvent{ - Width: int(e.Size.X), - Height: int(e.Size.Y), + Width: int(wevent.Size.X), + Height: int(wevent.Size.Y), } } - case coninput.MouseEventRecord: - mevent := mouseEvent(*ps, e) - *ps = e.ButtonState - return mevent - case coninput.FocusEventRecord, coninput.MenuEventRecord: + case xwindows.MOUSE_EVENT: + mevent := event.MouseEvent() + Event := mouseEvent(keyState.lastMouseBtns, mevent) + keyState.lastMouseBtns = mevent.ButtonState + return Event + case xwindows.FOCUS_EVENT: + fevent := event.FocusEvent() + if fevent.SetFocus { + return FocusEvent{} + } + return BlurEvent{} + case xwindows.MENU_EVENT: // ignore } return nil } -func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, isRelease bool) { +func mouseEventButton(p, s uint32) (button MouseButton, isRelease bool) { btn := p ^ s if btn&s == 0 { isRelease = true @@ -195,69 +100,72 @@ func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, isRelease if btn == 0 { switch { - case s&coninput.FROM_LEFT_1ST_BUTTON_PRESSED > 0: + case s&xwindows.FROM_LEFT_1ST_BUTTON_PRESSED > 0: button = MouseLeft - case s&coninput.FROM_LEFT_2ND_BUTTON_PRESSED > 0: + case s&xwindows.FROM_LEFT_2ND_BUTTON_PRESSED > 0: button = MouseMiddle - case s&coninput.RIGHTMOST_BUTTON_PRESSED > 0: + case s&xwindows.RIGHTMOST_BUTTON_PRESSED > 0: button = MouseRight - case s&coninput.FROM_LEFT_3RD_BUTTON_PRESSED > 0: + case s&xwindows.FROM_LEFT_3RD_BUTTON_PRESSED > 0: button = MouseBackward - case s&coninput.FROM_LEFT_4TH_BUTTON_PRESSED > 0: + case s&xwindows.FROM_LEFT_4TH_BUTTON_PRESSED > 0: button = MouseForward } return } switch btn { - case coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button + case xwindows.FROM_LEFT_1ST_BUTTON_PRESSED: // left button button = MouseLeft - case coninput.RIGHTMOST_BUTTON_PRESSED: // right button + case xwindows.RIGHTMOST_BUTTON_PRESSED: // right button button = MouseRight - case coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button + case xwindows.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button button = MouseMiddle - case coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward) + case xwindows.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward) button = MouseBackward - case coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward) + case xwindows.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward) button = MouseForward } return } -func mouseEvent(p coninput.ButtonState, e coninput.MouseEventRecord) (ev Event) { +func mouseEvent(p uint32, e xwindows.MouseEventRecord) (ev Event) { var mod KeyMod var isRelease bool - if e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED) { + if e.ControlKeyState&(xwindows.LEFT_ALT_PRESSED|xwindows.RIGHT_ALT_PRESSED) != 0 { mod |= ModAlt } - if e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED) { + if e.ControlKeyState&(xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_CTRL_PRESSED) != 0 { mod |= ModCtrl } - if e.ControlKeyState.Contains(coninput.SHIFT_PRESSED) { + if e.ControlKeyState&(xwindows.SHIFT_PRESSED) != 0 { mod |= ModShift } + m := Mouse{ X: int(e.MousePositon.X), Y: int(e.MousePositon.Y), Mod: mod, } + + wheelDirection := int16(highWord(e.ButtonState)) //nolint:gosec switch e.EventFlags { - case coninput.CLICK, coninput.DOUBLE_CLICK: + case xwindows.CLICK, xwindows.DOUBLE_CLICK: m.Button, isRelease = mouseEventButton(p, e.ButtonState) - case coninput.MOUSE_WHEELED: - if e.WheelDirection > 0 { + case xwindows.MOUSE_WHEELED: + if wheelDirection > 0 { m.Button = MouseWheelUp } else { m.Button = MouseWheelDown } - case coninput.MOUSE_HWHEELED: - if e.WheelDirection > 0 { + case xwindows.MOUSE_HWHEELED: + if wheelDirection > 0 { m.Button = MouseWheelRight } else { m.Button = MouseWheelLeft } - case coninput.MOUSE_MOVED: + case xwindows.MOUSE_MOVED: m.Button, _ = mouseEventButton(p, e.ButtonState) return MouseMotionEvent(m) } @@ -270,3 +178,396 @@ func mouseEvent(p coninput.ButtonState, e coninput.MouseEventRecord) (ev Event) return MouseClickEvent(m) } + +func highWord(data uint32) uint16 { + return uint16((data & 0xFFFF0000) >> 16) //nolint:gosec +} + +func readConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecord) (uint32, error) { + if len(inputRecords) == 0 { + return 0, fmt.Errorf("size of input record buffer cannot be zero") + } + + var read uint32 + + err := xwindows.ReadConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read) //nolint:gosec + + return read, err +} + +//nolint:unused +func peekConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecord) (uint32, error) { + if len(inputRecords) == 0 { + return 0, fmt.Errorf("size of input record buffer cannot be zero") + } + + var read uint32 + + err := xwindows.PeekConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read) //nolint:gosec + + return read, err +} + +// parseWin32InputKeyEvent parses a single key event from either the Windows +// Console API or win32-input-mode events. When state is nil, it means this is +// an event from win32-input-mode. Otherwise, it's a key event from the Windows +// Console API and needs a state to decode ANSI escape sequences and utf16 +// runes. +func (p *Parser) parseWin32InputKeyEvent(state *win32InputState, vkc uint16, _ uint16, r rune, keyDown bool, cks uint32, repeatCount uint16) (Event Event) { + defer func() { + // Respect the repeat count. + if repeatCount > 1 { + var multi MultiEvent + for i := 0; i < int(repeatCount); i++ { + multi = append(multi, Event) + } + Event = multi + } + }() + if state != nil { + defer func() { + state.lastCks = cks + }() + } + + var utf8Buf [utf8.UTFMax]byte + var key Key + if state != nil && state.utf16Half { + state.utf16Half = false + state.utf16Buf[1] = r + codepoint := utf16.DecodeRune(state.utf16Buf[0], state.utf16Buf[1]) + rw := utf8.EncodeRune(utf8Buf[:], codepoint) + r, _ = utf8.DecodeRune(utf8Buf[:rw]) + key.Code = r + key.Text = string(r) + key.Mod = translateControlKeyState(cks) + key = ensureKeyCase(key, cks) + if keyDown { + return KeyPressEvent(key) + } + return KeyReleaseEvent(key) + } + + var baseCode rune + switch { + case vkc == 0: + // Zero means this event is either an escape code or a unicode + // codepoint. + if state != nil && state.ansiIdx == 0 && r != ansi.ESC { + // This is a unicode codepoint. + baseCode = r + break + } + + if state != nil { + // Collect ANSI escape code. + state.ansiBuf[state.ansiIdx] = byte(r) + state.ansiIdx++ + if state.ansiIdx <= 2 { + // We haven't received enough bytes to determine if this is an + // ANSI escape code. + return nil + } + + n, Event := p.parseSequence(state.ansiBuf[:state.ansiIdx]) + if n == 0 { + return nil + } + + if _, ok := Event.(UnknownEvent); ok { + return nil + } + + state.ansiIdx = 0 + return Event + } + case vkc == xwindows.VK_BACK: + baseCode = KeyBackspace + case vkc == xwindows.VK_TAB: + baseCode = KeyTab + case vkc == xwindows.VK_RETURN: + baseCode = KeyEnter + case vkc == xwindows.VK_SHIFT: + if cks&xwindows.SHIFT_PRESSED != 0 { + if cks&xwindows.ENHANCED_KEY != 0 { + baseCode = KeyRightShift + } else { + baseCode = KeyLeftShift + } + } else if state != nil { + if state.lastCks&xwindows.SHIFT_PRESSED != 0 { + if state.lastCks&xwindows.ENHANCED_KEY != 0 { + baseCode = KeyRightShift + } else { + baseCode = KeyLeftShift + } + } + } + case vkc == xwindows.VK_CONTROL: + if cks&xwindows.LEFT_CTRL_PRESSED != 0 { + baseCode = KeyLeftCtrl + } else if cks&xwindows.RIGHT_CTRL_PRESSED != 0 { + baseCode = KeyRightCtrl + } else if state != nil { + if state.lastCks&xwindows.LEFT_CTRL_PRESSED != 0 { + baseCode = KeyLeftCtrl + } else if state.lastCks&xwindows.RIGHT_CTRL_PRESSED != 0 { + baseCode = KeyRightCtrl + } + } + case vkc == xwindows.VK_MENU: + if cks&xwindows.LEFT_ALT_PRESSED != 0 { + baseCode = KeyLeftAlt + } else if cks&xwindows.RIGHT_ALT_PRESSED != 0 { + baseCode = KeyRightAlt + } else if state != nil { + if state.lastCks&xwindows.LEFT_ALT_PRESSED != 0 { + baseCode = KeyLeftAlt + } else if state.lastCks&xwindows.RIGHT_ALT_PRESSED != 0 { + baseCode = KeyRightAlt + } + } + case vkc == xwindows.VK_PAUSE: + baseCode = KeyPause + case vkc == xwindows.VK_CAPITAL: + baseCode = KeyCapsLock + case vkc == xwindows.VK_ESCAPE: + baseCode = KeyEscape + case vkc == xwindows.VK_SPACE: + baseCode = KeySpace + case vkc == xwindows.VK_PRIOR: + baseCode = KeyPgUp + case vkc == xwindows.VK_NEXT: + baseCode = KeyPgDown + case vkc == xwindows.VK_END: + baseCode = KeyEnd + case vkc == xwindows.VK_HOME: + baseCode = KeyHome + case vkc == xwindows.VK_LEFT: + baseCode = KeyLeft + case vkc == xwindows.VK_UP: + baseCode = KeyUp + case vkc == xwindows.VK_RIGHT: + baseCode = KeyRight + case vkc == xwindows.VK_DOWN: + baseCode = KeyDown + case vkc == xwindows.VK_SELECT: + baseCode = KeySelect + case vkc == xwindows.VK_SNAPSHOT: + baseCode = KeyPrintScreen + case vkc == xwindows.VK_INSERT: + baseCode = KeyInsert + case vkc == xwindows.VK_DELETE: + baseCode = KeyDelete + case vkc >= '0' && vkc <= '9': + baseCode = rune(vkc) + case vkc >= 'A' && vkc <= 'Z': + // Convert to lowercase. + baseCode = rune(vkc) + 32 + case vkc == xwindows.VK_LWIN: + baseCode = KeyLeftSuper + case vkc == xwindows.VK_RWIN: + baseCode = KeyRightSuper + case vkc == xwindows.VK_APPS: + baseCode = KeyMenu + case vkc >= xwindows.VK_NUMPAD0 && vkc <= xwindows.VK_NUMPAD9: + baseCode = rune(vkc-xwindows.VK_NUMPAD0) + KeyKp0 + case vkc == xwindows.VK_MULTIPLY: + baseCode = KeyKpMultiply + case vkc == xwindows.VK_ADD: + baseCode = KeyKpPlus + case vkc == xwindows.VK_SEPARATOR: + baseCode = KeyKpComma + case vkc == xwindows.VK_SUBTRACT: + baseCode = KeyKpMinus + case vkc == xwindows.VK_DECIMAL: + baseCode = KeyKpDecimal + case vkc == xwindows.VK_DIVIDE: + baseCode = KeyKpDivide + case vkc >= xwindows.VK_F1 && vkc <= xwindows.VK_F24: + baseCode = rune(vkc-xwindows.VK_F1) + KeyF1 + case vkc == xwindows.VK_NUMLOCK: + baseCode = KeyNumLock + case vkc == xwindows.VK_SCROLL: + baseCode = KeyScrollLock + case vkc == xwindows.VK_LSHIFT: + baseCode = KeyLeftShift + case vkc == xwindows.VK_RSHIFT: + baseCode = KeyRightShift + case vkc == xwindows.VK_LCONTROL: + baseCode = KeyLeftCtrl + case vkc == xwindows.VK_RCONTROL: + baseCode = KeyRightCtrl + case vkc == xwindows.VK_LMENU: + baseCode = KeyLeftAlt + case vkc == xwindows.VK_RMENU: + baseCode = KeyRightAlt + case vkc == xwindows.VK_VOLUME_MUTE: + baseCode = KeyMute + case vkc == xwindows.VK_VOLUME_DOWN: + baseCode = KeyLowerVol + case vkc == xwindows.VK_VOLUME_UP: + baseCode = KeyRaiseVol + case vkc == xwindows.VK_MEDIA_NEXT_TRACK: + baseCode = KeyMediaNext + case vkc == xwindows.VK_MEDIA_PREV_TRACK: + baseCode = KeyMediaPrev + case vkc == xwindows.VK_MEDIA_STOP: + baseCode = KeyMediaStop + case vkc == xwindows.VK_MEDIA_PLAY_PAUSE: + baseCode = KeyMediaPlayPause + case vkc == xwindows.VK_OEM_1: + baseCode = ';' + case vkc == xwindows.VK_OEM_PLUS: + baseCode = '+' + case vkc == xwindows.VK_OEM_COMMA: + baseCode = ',' + case vkc == xwindows.VK_OEM_MINUS: + baseCode = '-' + case vkc == xwindows.VK_OEM_PERIOD: + baseCode = '.' + case vkc == xwindows.VK_OEM_2: + baseCode = '/' + case vkc == xwindows.VK_OEM_3: + baseCode = '`' + case vkc == xwindows.VK_OEM_4: + baseCode = '[' + case vkc == xwindows.VK_OEM_5: + baseCode = '\\' + case vkc == xwindows.VK_OEM_6: + baseCode = ']' + case vkc == xwindows.VK_OEM_7: + baseCode = '\'' + } + + if utf16.IsSurrogate(r) { + if state != nil { + state.utf16Buf[0] = r + state.utf16Half = true + } + return nil + } + + // AltGr is left ctrl + right alt. On non-US keyboards, this is used to type + // special characters and produce printable events. + // XXX: Should this be a KeyMod? + altGr := cks&(xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_ALT_PRESSED) == xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_ALT_PRESSED + + var text string + keyCode := baseCode + if !unicode.IsControl(r) { + rw := utf8.EncodeRune(utf8Buf[:], r) + keyCode, _ = utf8.DecodeRune(utf8Buf[:rw]) + if cks == xwindows.NO_CONTROL_KEY || + cks == xwindows.SHIFT_PRESSED || + cks == xwindows.CAPSLOCK_ON || + altGr { + // If the control key state is 0, shift is pressed, or caps lock + // then the key event is a printable event i.e. [text] is not empty. + text = string(keyCode) + } + } + + key.Code = keyCode + key.Text = text + key.Mod = translateControlKeyState(cks) + key.BaseCode = baseCode + key = ensureKeyCase(key, cks) + if keyDown { + return KeyPressEvent(key) + } + + return KeyReleaseEvent(key) +} + +// ensureKeyCase ensures that the key's text is in the correct case based on the +// control key state. +func ensureKeyCase(key Key, cks uint32) Key { + if len(key.Text) == 0 { + return key + } + + hasShift := cks&xwindows.SHIFT_PRESSED != 0 + hasCaps := cks&xwindows.CAPSLOCK_ON != 0 + if hasShift || hasCaps { + if unicode.IsLower(key.Code) { + key.ShiftedCode = unicode.ToUpper(key.Code) + key.Text = string(key.ShiftedCode) + } + } else { + if unicode.IsUpper(key.Code) { + key.ShiftedCode = unicode.ToLower(key.Code) + key.Text = string(key.ShiftedCode) + } + } + + return key +} + +// translateControlKeyState translates the control key state from the Windows +// Console API into a Mod bitmask. +func translateControlKeyState(cks uint32) (m KeyMod) { + if cks&xwindows.LEFT_CTRL_PRESSED != 0 || cks&xwindows.RIGHT_CTRL_PRESSED != 0 { + m |= ModCtrl + } + if cks&xwindows.LEFT_ALT_PRESSED != 0 || cks&xwindows.RIGHT_ALT_PRESSED != 0 { + m |= ModAlt + } + if cks&xwindows.SHIFT_PRESSED != 0 { + m |= ModShift + } + if cks&xwindows.CAPSLOCK_ON != 0 { + m |= ModCapsLock + } + if cks&xwindows.NUMLOCK_ON != 0 { + m |= ModNumLock + } + if cks&xwindows.SCROLLLOCK_ON != 0 { + m |= ModScrollLock + } + return +} + +//nolint:unused +func keyEventString(vkc, sc uint16, r rune, keyDown bool, cks uint32, repeatCount uint16) string { + var s strings.Builder + s.WriteString("vkc: ") + s.WriteString(fmt.Sprintf("%d, 0x%02x", vkc, vkc)) + s.WriteString(", sc: ") + s.WriteString(fmt.Sprintf("%d, 0x%02x", sc, sc)) + s.WriteString(", r: ") + s.WriteString(fmt.Sprintf("%q", r)) + s.WriteString(", down: ") + s.WriteString(fmt.Sprintf("%v", keyDown)) + s.WriteString(", cks: [") + if cks&xwindows.LEFT_ALT_PRESSED != 0 { + s.WriteString("left alt, ") + } + if cks&xwindows.RIGHT_ALT_PRESSED != 0 { + s.WriteString("right alt, ") + } + if cks&xwindows.LEFT_CTRL_PRESSED != 0 { + s.WriteString("left ctrl, ") + } + if cks&xwindows.RIGHT_CTRL_PRESSED != 0 { + s.WriteString("right ctrl, ") + } + if cks&xwindows.SHIFT_PRESSED != 0 { + s.WriteString("shift, ") + } + if cks&xwindows.CAPSLOCK_ON != 0 { + s.WriteString("caps lock, ") + } + if cks&xwindows.NUMLOCK_ON != 0 { + s.WriteString("num lock, ") + } + if cks&xwindows.SCROLLLOCK_ON != 0 { + s.WriteString("scroll lock, ") + } + if cks&xwindows.ENHANCED_KEY != 0 { + s.WriteString("enhanced key, ") + } + s.WriteString("], repeat count: ") + s.WriteString(fmt.Sprintf("%d", repeatCount)) + return s.String() +} diff --git a/input/driver_windows_test.go b/input/driver_windows_test.go new file mode 100644 index 00000000..ff55caee --- /dev/null +++ b/input/driver_windows_test.go @@ -0,0 +1,265 @@ +package input + +import ( + "encoding/binary" + "image/color" + "reflect" + "testing" + "unicode/utf16" + + "github.com/charmbracelet/x/ansi" + xwindows "github.com/charmbracelet/x/windows" + "golang.org/x/sys/windows" +) + +func TestWindowsInputEvents(t *testing.T) { + cases := []struct { + name string + events []xwindows.InputRecord + expected []Event + sequence bool // indicates that the input events are ANSI sequence or utf16 + }{ + { + name: "single key event", + events: []xwindows.InputRecord{ + encodeKeyEvent(xwindows.KeyEventRecord{ + KeyDown: true, + Char: 'a', + VirtualKeyCode: 'A', + }), + }, + expected: []Event{KeyPressEvent{Code: 'a', BaseCode: 'a', Text: "a"}}, + }, + { + name: "single key event with control key", + events: []xwindows.InputRecord{ + encodeKeyEvent(xwindows.KeyEventRecord{ + KeyDown: true, + Char: 'a', + VirtualKeyCode: 'A', + ControlKeyState: xwindows.LEFT_CTRL_PRESSED, + }), + }, + expected: []Event{KeyPressEvent{Code: 'a', BaseCode: 'a', Mod: ModCtrl}}, + }, + { + name: "escape alt key event", + events: []xwindows.InputRecord{ + encodeKeyEvent(xwindows.KeyEventRecord{ + KeyDown: true, + Char: ansi.ESC, + VirtualKeyCode: ansi.ESC, + ControlKeyState: xwindows.LEFT_ALT_PRESSED, + }), + }, + expected: []Event{KeyPressEvent{Code: ansi.ESC, BaseCode: ansi.ESC, Mod: ModAlt}}, + }, + { + name: "single shifted key event", + events: []xwindows.InputRecord{ + encodeKeyEvent(xwindows.KeyEventRecord{ + KeyDown: true, + Char: 'A', + VirtualKeyCode: 'A', + ControlKeyState: xwindows.SHIFT_PRESSED, + }), + }, + expected: []Event{KeyPressEvent{Code: 'A', BaseCode: 'a', Text: "A", Mod: ModShift}}, + }, + { + name: "utf16 rune", + events: encodeUtf16Rune('😊'), // smiley emoji '😊' + expected: []Event{ + KeyPressEvent{Code: '😊', Text: "😊"}, + }, + sequence: true, + }, + { + name: "background color response", + events: encodeSequence("\x1b]11;rgb:ff/ff/ff\x07"), + expected: []Event{BackgroundColorEvent{Color: color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}}}, + sequence: true, + }, + { + name: "simple mouse event", + events: []xwindows.InputRecord{ + encodeMouseEvent(xwindows.MouseEventRecord{ + MousePositon: windows.Coord{X: 10, Y: 20}, + ButtonState: xwindows.FROM_LEFT_1ST_BUTTON_PRESSED, + EventFlags: xwindows.CLICK, + }), + encodeMouseEvent(xwindows.MouseEventRecord{ + MousePositon: windows.Coord{X: 10, Y: 20}, + EventFlags: xwindows.CLICK, + }), + }, + expected: []Event{ + MouseClickEvent{Button: MouseLeft, X: 10, Y: 20}, + MouseReleaseEvent{Button: MouseLeft, X: 10, Y: 20}, + }, + }, + { + name: "focus event", + events: []xwindows.InputRecord{ + encodeFocusEvent(xwindows.FocusEventRecord{ + SetFocus: true, + }), + encodeFocusEvent(xwindows.FocusEventRecord{ + SetFocus: false, + }), + }, + expected: []Event{ + FocusEvent{}, + BlurEvent{}, + }, + }, + { + name: "window size event", + events: []xwindows.InputRecord{ + encodeWindowBufferSizeEvent(xwindows.WindowBufferSizeRecord{ + Size: windows.Coord{X: 10, Y: 20}, + }), + }, + expected: []Event{ + WindowSizeEvent{Width: 10, Height: 20}, + }, + }, + } + + // p is the parser to parse the input events + var p Parser + + // keep track of the state of the driver to handle ANSI sequences and utf16 + var state win32InputState + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.sequence { + var Event Event + for _, ev := range tc.events { + if ev.EventType != xwindows.KEY_EVENT { + t.Fatalf("expected key event, got %v", ev.EventType) + } + + key := ev.KeyEvent() + Event = p.parseWin32InputKeyEvent(&state, key.VirtualKeyCode, key.VirtualScanCode, key.Char, key.KeyDown, key.ControlKeyState, key.RepeatCount) + } + if len(tc.expected) != 1 { + t.Fatalf("expected 1 event, got %d", len(tc.expected)) + } + if !reflect.DeepEqual(Event, tc.expected[0]) { + t.Errorf("expected %v, got %v", tc.expected[0], Event) + } + } else { + if len(tc.events) != len(tc.expected) { + t.Fatalf("expected %d events, got %d", len(tc.expected), len(tc.events)) + } + for j, ev := range tc.events { + Event := p.parseConInputEvent(ev, &state) + if !reflect.DeepEqual(Event, tc.expected[j]) { + t.Errorf("expected %#v, got %#v", tc.expected[j], Event) + } + } + } + }) + } +} + +func boolToUint32(b bool) uint32 { + if b { + return 1 + } + return 0 +} + +func encodeMenuEvent(menu xwindows.MenuEventRecord) xwindows.InputRecord { + var bts [16]byte + binary.LittleEndian.PutUint32(bts[0:4], menu.CommandID) + return xwindows.InputRecord{ + EventType: xwindows.MENU_EVENT, + Event: bts, + } +} + +func encodeWindowBufferSizeEvent(size xwindows.WindowBufferSizeRecord) xwindows.InputRecord { + var bts [16]byte + binary.LittleEndian.PutUint16(bts[0:2], uint16(size.Size.X)) + binary.LittleEndian.PutUint16(bts[2:4], uint16(size.Size.Y)) + return xwindows.InputRecord{ + EventType: xwindows.WINDOW_BUFFER_SIZE_EVENT, + Event: bts, + } +} + +func encodeFocusEvent(focus xwindows.FocusEventRecord) xwindows.InputRecord { + var bts [16]byte + if focus.SetFocus { + bts[0] = 1 + } + return xwindows.InputRecord{ + EventType: xwindows.FOCUS_EVENT, + Event: bts, + } +} + +func encodeMouseEvent(mouse xwindows.MouseEventRecord) xwindows.InputRecord { + var bts [16]byte + binary.LittleEndian.PutUint16(bts[0:2], uint16(mouse.MousePositon.X)) + binary.LittleEndian.PutUint16(bts[2:4], uint16(mouse.MousePositon.Y)) + binary.LittleEndian.PutUint32(bts[4:8], mouse.ButtonState) + binary.LittleEndian.PutUint32(bts[8:12], mouse.ControlKeyState) + binary.LittleEndian.PutUint32(bts[12:16], mouse.EventFlags) + return xwindows.InputRecord{ + EventType: xwindows.MOUSE_EVENT, + Event: bts, + } +} + +func encodeKeyEvent(key xwindows.KeyEventRecord) xwindows.InputRecord { + var bts [16]byte + binary.LittleEndian.PutUint32(bts[0:4], boolToUint32(key.KeyDown)) + binary.LittleEndian.PutUint16(bts[4:6], key.RepeatCount) + binary.LittleEndian.PutUint16(bts[6:8], key.VirtualKeyCode) + binary.LittleEndian.PutUint16(bts[8:10], key.VirtualScanCode) + binary.LittleEndian.PutUint16(bts[10:12], uint16(key.Char)) + binary.LittleEndian.PutUint32(bts[12:16], key.ControlKeyState) + return xwindows.InputRecord{ + EventType: xwindows.KEY_EVENT, + Event: bts, + } +} + +// encodeSequence encodes a string of ANSI escape sequences into a slice of +// Windows input key records. +func encodeSequence(s string) (evs []xwindows.InputRecord) { + var state byte + for len(s) > 0 { + seq, _, n, newState := ansi.DecodeSequence(s, state, nil) + for i := 0; i < n; i++ { + evs = append(evs, encodeKeyEvent(xwindows.KeyEventRecord{ + KeyDown: true, + Char: rune(seq[i]), + })) + } + state = newState + s = s[n:] + } + return +} + +func encodeUtf16Rune(r rune) []xwindows.InputRecord { + r1, r2 := utf16.EncodeRune(r) + return encodeUtf16Pair(r1, r2) +} + +func encodeUtf16Pair(r1, r2 rune) []xwindows.InputRecord { + return []xwindows.InputRecord{ + encodeKeyEvent(xwindows.KeyEventRecord{ + KeyDown: true, + Char: r1, + }), + encodeKeyEvent(xwindows.KeyEventRecord{ + KeyDown: true, + Char: r2, + }), + } +} diff --git a/input/focus.go b/input/focus.go index dd445acc..796d95f6 100644 --- a/input/focus.go +++ b/input/focus.go @@ -1,13 +1,9 @@ package input -// FocusEvent represents a focus event. +// FocusEvent represents a terminal focus event. +// This occurs when the terminal gains focus. type FocusEvent struct{} -// String implements fmt.Stringer. -func (FocusEvent) String() string { return "focus" } - -// BlurEvent represents a blur event. +// BlurEvent represents a terminal blur event. +// This occurs when the terminal loses focus. type BlurEvent struct{} - -// String implements fmt.Stringer. -func (BlurEvent) String() string { return "blur" } diff --git a/input/focus_test.go b/input/focus_test.go index 435a77ff..2d35e476 100644 --- a/input/focus_test.go +++ b/input/focus_test.go @@ -5,7 +5,8 @@ import ( ) func TestFocus(t *testing.T) { - _, e := ParseSequence([]byte("\x1b[I")) + var p Parser + _, e := p.parseSequence([]byte("\x1b[I")) switch e.(type) { case FocusEvent: // ok @@ -15,7 +16,8 @@ func TestFocus(t *testing.T) { } func TestBlur(t *testing.T) { - _, e := ParseSequence([]byte("\x1b[O")) + var p Parser + _, e := p.parseSequence([]byte("\x1b[O")) switch e.(type) { case BlurEvent: // ok diff --git a/input/go.mod b/input/go.mod index ba9c7ab0..27f34f8f 100644 --- a/input/go.mod +++ b/input/go.mod @@ -3,15 +3,15 @@ module github.com/charmbracelet/x/input go 1.18 require ( - github.com/charmbracelet/x/ansi v0.5.3-0.20241204155720-fa6b43c98350 - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f + github.com/charmbracelet/x/ansi v0.5.3-0.20241209212528-0eec74ecaa6f + github.com/charmbracelet/x/windows v0.2.0 github.com/muesli/cancelreader v0.2.2 + github.com/rivo/uniseg v0.4.7 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e golang.org/x/sys v0.28.0 ) require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect ) diff --git a/input/go.sum b/input/go.sum index 0603ffe5..473abd00 100644 --- a/input/go.sum +++ b/input/go.sum @@ -1,9 +1,7 @@ -github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= -github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/ansi v0.5.3-0.20241204155720-fa6b43c98350 h1:LEZviFqdD7htj5oKO6o053ko6s1H+LAiOd0nbKZa1cE= -github.com/charmbracelet/x/ansi v0.5.3-0.20241204155720-fa6b43c98350/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= -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/charmbracelet/x/ansi v0.5.3-0.20241209212528-0eec74ecaa6f h1:TGyWqNyb5ML8uqdEXVSyYxrH9HfPpBxgmHyroYLYHu8= +github.com/charmbracelet/x/ansi v0.5.3-0.20241209212528-0eec74ecaa6f/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= +github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw= +github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s= 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/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -14,6 +12,5 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/input/input.go b/input/input.go index b29df6b5..02a5d960 100644 --- a/input/input.go +++ b/input/input.go @@ -5,39 +5,21 @@ import ( "strings" ) -var ( - // ErrUnknownEvent is returned when an unknown event is encountered. - ErrUnknownEvent = fmt.Errorf("unknown event") - - // ErrEmpty is returned when the event buffer is empty. - ErrEmpty = fmt.Errorf("empty event buffer") -) - -// Event represents a terminal input event. +// Event represents a terminal event. type Event interface{} // UnknownEvent represents an unknown event. type UnknownEvent string -// String implements fmt.Stringer. +// String returns a string representation of the unknown event. func (e UnknownEvent) String() string { return fmt.Sprintf("%q", string(e)) } -// WindowSizeEvent represents a window resize event. -type WindowSizeEvent struct { - Width, Height int -} - -// String implements fmt.Stringer. -func (e WindowSizeEvent) String() string { - return fmt.Sprintf("resize: %dx%d", e.Width, e.Height) -} - -// MultiEvent represents multiple events. +// MultiEvent represents multiple messages event. type MultiEvent []Event -// String implements fmt.Stringer. +// String returns a string representation of the multiple messages event. func (e MultiEvent) String() string { var sb strings.Builder for _, ev := range e { @@ -45,3 +27,12 @@ func (e MultiEvent) String() string { } return sb.String() } + +// WindowSizeEvent is used to report the terminal size. Note that Windows does +// not have support for reporting resizes via SIGWINCH signals and relies on +// the Windows Console API to report window size changes. See [newCancelreader] +// and [conInputReader] for more information. +type WindowSizeEvent struct { + Width int + Height int +} diff --git a/input/key.go b/input/key.go index 31617340..17a76162 100644 --- a/input/key.go +++ b/input/key.go @@ -1,33 +1,32 @@ package input -// KeySym is a keyboard symbol. -type KeySym int +import ( + "fmt" + "strings" + "unicode" -// Symbol constants. -const ( - KeyNone KeySym = iota - - // Special names in C0 - - KeyBackspace - KeyTab - KeyEnter - KeyEscape + "github.com/charmbracelet/x/ansi" +) - // Special names in G0 +const ( + // KeyExtended is a special key code used to signify that a key event + // contains multiple runes. + KeyExtended = unicode.MaxRune + 1 +) - KeySpace - KeyDelete +// Special key symbols. +const ( // Special keys - KeyUp + KeyUp rune = KeyExtended + iota + 1 KeyDown KeyRight KeyLeft KeyBegin KeyFind KeyInsert + KeyDelete KeySelect KeyPgUp KeyPgDown @@ -175,41 +174,131 @@ const ( KeyRightMeta KeyIsoLevel3Shift KeyIsoLevel5Shift + + // Special names in C0 + + KeyBackspace = rune(ansi.DEL) + KeyTab = rune(ansi.HT) + KeyEnter = rune(ansi.CR) + KeyReturn = KeyEnter + KeyEscape = rune(ansi.ESC) + KeyEsc = KeyEscape + + // Special names in G0 + + KeySpace = rune(ansi.SP) ) -// Key represents a key event. +// KeyPressEvent represents a key press event. +type KeyPressEvent Key + +// String implements [fmt.Stringer] and is quite useful for matching key +// events. For details, on what this returns see [Key.String]. +func (k KeyPressEvent) String() string { + return Key(k).String() +} + +// Key returns the underlying key event. This is a syntactic sugar for casting +// the key event to a [Key]. +func (k KeyPressEvent) Key() Key { + return Key(k) +} + +// KeyReleaseEvent represents a key release event. +type KeyReleaseEvent Key + +// String implements [fmt.Stringer] and is quite useful for matching key +// events. For details, on what this returns see [Key.String]. +func (k KeyReleaseEvent) String() string { + return Key(k).String() +} + +// Key returns the underlying key event. This is a convenience method and +// syntactic sugar to satisfy the [KeyEvent] interface, and cast the key event to +// [Key]. +func (k KeyReleaseEvent) Key() Key { + return Key(k) +} + +// KeyEvent represents a key event. This can be either a key press or a key +// release event. +type KeyEvent interface { + fmt.Stringer + + // Key returns the underlying key event. + Key() Key +} + +// Key represents a Key press or release event. It contains information about +// the Key pressed, like the runes, the type of Key, and the modifiers pressed. +// There are a couple general patterns you could use to check for key presses +// or releases: +// +// // Switch on the string representation of the key (shorter) +// switch ev := ev.(type) { +// case KeyPressEvent: +// switch ev.String() { +// case "enter": +// fmt.Println("you pressed enter!") +// case "a": +// fmt.Println("you pressed a!") +// } +// } +// +// // Switch on the key type (more foolproof) +// switch ev := ev.(type) { +// case KeyEvent: +// // catch both KeyPressEvent and KeyReleaseEvent +// switch key := ev.Key(); key.Code { +// case KeyEnter: +// fmt.Println("you pressed enter!") +// default: +// switch key.Text { +// case "a": +// fmt.Println("you pressed a!") +// } +// } +// } +// +// Note that [Key.Text] will be empty for special keys like [KeyEnter], +// [KeyTab], and for keys that don't represent printable characters like key +// combos with modifier keys. In other words, [Key.Text] is populated only for +// keys that represent printable characters shifted or unshifted (like 'a', +// 'A', '1', '!', etc.). type Key struct { - // Sym is a special key, like enter, tab, backspace, and so on. - Sym KeySym + // Text contains the actual characters received. This usually the same as + // [Key.Code]. When [Key.Text] is non-empty, it indicates that the key + // pressed represents printable character(s). + Text string + + // Mod represents modifier keys, like [ModCtrl], [ModAlt], and so on. + Mod KeyMod - // Rune is the actual character received. If the user presses shift+a, the - // Rune will be 'A'. - Rune rune + // Code represents the key pressed. This is usually a special key like + // [KeyTab], [KeyEnter], [KeyF1], or a printable character like 'a'. + Code 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'. + // ShiftedCode is the actual, shifted key pressed by the user. For example, + // if the user presses shift+a, or caps lock is on, [Key.ShiftedCode] will + // be 'A' and [Key.Code] will be 'a'. // - // In the case of non-latin keyboards, like Arabic, AltRune is the + // In the case of non-latin keyboards, like Arabic, [Key.ShiftedCode] is the // unshifted key on the keyboard. // // This is only available with the Kitty Keyboard Protocol or the Windows // Console API. - AltRune rune + ShiftedCode 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. + // BaseCode is the key pressed according to the standard PC-101 key layout. + // On international keyboards, this is the key that would be pressed if the + // keyboard was set to US PC-101 layout. // - // For example, if the user presses 'q' on a French AZERTY keyboard, the - // baseRune will be 'q'. + // For example, if the user presses 'q' on a French AZERTY keyboard, + // [Key.BaseCode] will be 'q'. // // This is only available with the Kitty Keyboard Protocol or the Windows // Console API. - baseRune rune - - // Mod is a modifier key, like ctrl, alt, and so on. - Mod KeyMod + BaseCode rune // IsRepeat indicates whether the key is being held down and sending events // repeatedly. @@ -219,25 +308,7 @@ type Key struct { IsRepeat bool } -// KeyPressEvent represents a key press event. -type KeyPressEvent Key - -// String implements fmt.Stringer and is quite useful for matching key -// events. For details, on what this returns see [Key.String]. -func (k KeyPressEvent) String() string { - return Key(k).String() -} - -// KeyReleaseEvent represents a key release event. -type KeyReleaseEvent Key - -// String implements fmt.Stringer and is quite useful for matching complex key -// events. For details, on what this returns see [Key.String]. -func (k KeyReleaseEvent) String() string { - return Key(k).String() -} - -// String implements fmt.Stringer and is used to convert a key to a string. +// 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. @@ -253,60 +324,53 @@ func (k KeyReleaseEvent) String() string { // 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+" + var sb strings.Builder + if k.Mod.Contains(ModCtrl) && k.Code != KeyLeftCtrl && k.Code != KeyRightCtrl { + sb.WriteString("ctrl+") } - if k.Mod.HasAlt() && k.Sym != KeyLeftAlt && k.Sym != KeyRightAlt { - s += "alt+" + if k.Mod.Contains(ModAlt) && k.Code != KeyLeftAlt && k.Code != KeyRightAlt { + sb.WriteString("alt+") } - if k.Mod.HasShift() && k.Sym != KeyLeftShift && k.Sym != KeyRightShift { - s += "shift+" + if k.Mod.Contains(ModShift) && k.Code != KeyLeftShift && k.Code != KeyRightShift { + sb.WriteString("shift+") } - if k.Mod.HasMeta() && k.Sym != KeyLeftMeta && k.Sym != KeyRightMeta { - s += "meta+" + if k.Mod.Contains(ModMeta) && k.Code != KeyLeftMeta && k.Code != KeyRightMeta { + sb.WriteString("meta+") } - if k.Mod.HasHyper() && k.Sym != KeyLeftHyper && k.Sym != KeyRightHyper { - s += "hyper+" + if k.Mod.Contains(ModHyper) && k.Code != KeyLeftHyper && k.Code != KeyRightHyper { + sb.WriteString("hyper+") } - if k.Mod.HasSuper() && k.Sym != KeyLeftSuper && k.Sym != KeyRightSuper { - s += "super+" + if k.Mod.Contains(ModSuper) && k.Code != KeyLeftSuper && k.Code != KeyRightSuper { + sb.WriteString("super+") } - runeStr := func(r rune) string { - // Space is the only invisible printable character. - if r == ' ' { - return "space" - } - return string(r) - } - 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 k.Rune != 0 { - // Else, just print the rune. - s += runeStr(k.Rune) + if kt, ok := keyTypeString[k.Code]; ok { + sb.WriteString(kt) } else { - s += k.Sym.String() - } - return s -} + code := k.Code + if k.BaseCode != 0 { + // If a [Key.BaseCode] is present, use it to represent a key using the standard + // PC-101 key layout. + code = k.BaseCode + } -// 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" + switch code { + case KeySpace: + // Space is the only invisible printable character. + sb.WriteString("space") + case KeyExtended: + // Write the actual text of the key when the key contains multiple + // runes. + sb.WriteString(k.Text) + default: + sb.WriteRune(code) + } } - return s + + return sb.String() } -var keySymString = map[KeySym]string{ +var keyTypeString = map[rune]string{ KeyEnter: "enter", KeyTab: "tab", KeyBackspace: "backspace", diff --git a/input/key_test.go b/input/key_test.go index 48bb6c7d..60144e71 100644 --- a/input/key_test.go +++ b/input/key_test.go @@ -9,6 +9,7 @@ import ( "io" "math/rand" "reflect" + "regexp" "runtime" "sort" "strings" @@ -23,52 +24,49 @@ var sequences = buildKeysTable(FlagTerminfo, "dumb") func TestKeyString(t *testing.T) { t.Run("alt+space", func(t *testing.T) { - k := KeyPressEvent{Sym: KeySpace, Rune: ' ', Mod: ModAlt} + k := KeyPressEvent{Code: KeySpace, Text: " ", Mod: ModAlt} if got := k.String(); got != "alt+space" { t.Fatalf(`expected a "alt+space ", got %q`, got) } }) t.Run("runes", func(t *testing.T) { - k := KeyPressEvent{Rune: 'a'} + k := KeyPressEvent{Code: 'a', Text: "a"} if got := k.String(); got != "a" { t.Fatalf(`expected an "a", got %q`, got) } }) t.Run("invalid", func(t *testing.T) { - k := KeyPressEvent{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 != "space" { - t.Fatalf(`expected a "space", got %q`, got) - } - }) - - t.Run("invalid", func(t *testing.T) { - if got := KeySym(99999).String(); got != "unknown" { + k := KeyPressEvent{Code: 99999} + if got := k.String(); got != "𘚟" { t.Fatalf(`expected a "unknown", got %q`, got) } }) } type seqTest struct { - seq []byte - msgs []Event + seq []byte + Events []Event } +var f3CurPosRegexp = regexp.MustCompile(`\x1b\[1;(\d+)R`) + // buildBaseSeqTests returns sequence tests that are valid for the // detectSequence() function. func buildBaseSeqTests() []seqTest { td := []seqTest{} for seq, key := range sequences { - td = append(td, seqTest{[]byte(seq), []Event{KeyPressEvent(key)}}) + k := KeyPressEvent(key) + st := seqTest{seq: []byte(seq), Events: []Event{k}} + + // XXX: This is a special case to handle F3 key sequence and cursor + // position report having the same sequence. See [parseCsi] for more + // information. + if f3CurPosRegexp.MatchString(seq) { + st.Events = []Event{k, CursorPositionEvent{Y: 0, X: int(key.Mod)}} + } + td = append(td, st) } // Additional special cases. @@ -77,21 +75,21 @@ func buildBaseSeqTests() []seqTest { seqTest{ []byte{'\x1b', '[', '-', '-', '-', '-', 'X'}, []Event{ - UnknownCsiEvent([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'}), + UnknownEvent([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'}), }, }, // A lone space character. seqTest{ []byte{' '}, []Event{ - KeyPressEvent{Sym: KeySpace, Rune: ' '}, + KeyPressEvent{Code: KeySpace, Text: " "}, }, }, // An escape character with the alt modifier. seqTest{ []byte{'\x1b', ' '}, []Event{ - KeyPressEvent{Sym: KeySpace, Rune: ' ', Mod: ModAlt}, + KeyPressEvent{Code: KeySpace, Mod: ModAlt}, }, }, ) @@ -101,6 +99,118 @@ func buildBaseSeqTests() []seqTest { func TestParseSequence(t *testing.T) { td := buildBaseSeqTests() td = append(td, + // Xterm modifyOtherKeys CSI 27 ; ; ~ + seqTest{ + []byte("\x1b[27;3;20320~"), + []Event{KeyPressEvent{Code: '你', Mod: ModAlt}}, + }, + seqTest{ + []byte("\x1b[27;3;65~"), + []Event{KeyPressEvent{Code: 'A', Mod: ModAlt}}, + }, + seqTest{ + []byte("\x1b[27;3;8~"), + []Event{KeyPressEvent{Code: KeyBackspace, Mod: ModAlt}}, + }, + seqTest{ + []byte("\x1b[27;3;27~"), + []Event{KeyPressEvent{Code: KeyEscape, Mod: ModAlt}}, + }, + seqTest{ + []byte("\x1b[27;3;127~"), + []Event{KeyPressEvent{Code: KeyBackspace, Mod: ModAlt}}, + }, + + // Kitty keyboard / CSI u (fixterms) + seqTest{ + []byte("\x1b[1B"), + []Event{KeyPressEvent{Code: KeyDown}}, + }, + seqTest{ + []byte("\x1b[1;B"), + []Event{KeyPressEvent{Code: KeyDown}}, + }, + seqTest{ + []byte("\x1b[1;4B"), + []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyDown}}, + }, + seqTest{ + []byte("\x1b[1;4:1B"), + []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyDown}}, + }, + seqTest{ + []byte("\x1b[1;4:2B"), + []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyDown, IsRepeat: true}}, + }, + seqTest{ + []byte("\x1b[1;4:3B"), + []Event{KeyReleaseEvent{Mod: ModShift | ModAlt, Code: KeyDown}}, + }, + seqTest{ + []byte("\x1b[8~"), + []Event{KeyPressEvent{Code: KeyEnd}}, + }, + seqTest{ + []byte("\x1b[8;~"), + []Event{KeyPressEvent{Code: KeyEnd}}, + }, + seqTest{ + []byte("\x1b[8;10~"), + []Event{KeyPressEvent{Mod: ModShift | ModMeta, Code: KeyEnd}}, + }, + seqTest{ + []byte("\x1b[27;4u"), + []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyEscape}}, + }, + seqTest{ + []byte("\x1b[127;4u"), + []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyBackspace}}, + }, + seqTest{ + []byte("\x1b[57358;4u"), + []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyCapsLock}}, + }, + seqTest{ + []byte("\x1b[9;2u"), + []Event{KeyPressEvent{Mod: ModShift, Code: KeyTab}}, + }, + seqTest{ + []byte("\x1b[195;u"), + []Event{KeyPressEvent{Text: "Ã", Code: 'Ã'}}, + }, + seqTest{ + []byte("\x1b[20320;2u"), + []Event{KeyPressEvent{Text: "你", Mod: ModShift, Code: '你'}}, + }, + seqTest{ + []byte("\x1b[195;:1u"), + []Event{KeyPressEvent{Text: "Ã", Code: 'Ã'}}, + }, + seqTest{ + []byte("\x1b[195;2:3u"), + []Event{KeyReleaseEvent{Code: 'Ã', Text: "Ã", Mod: ModShift}}, + }, + seqTest{ + []byte("\x1b[195;2:2u"), + []Event{KeyPressEvent{Code: 'Ã', Text: "Ã", IsRepeat: true, Mod: ModShift}}, + }, + seqTest{ + []byte("\x1b[195;2:1u"), + []Event{KeyPressEvent{Code: 'Ã', Text: "Ã", Mod: ModShift}}, + }, + seqTest{ + []byte("\x1b[195;2:3u"), + []Event{KeyReleaseEvent{Code: 'Ã', Text: "Ã", Mod: ModShift}}, + }, + seqTest{ + []byte("\x1b[97;2;65u"), + []Event{KeyPressEvent{Code: 'a', Text: "A", Mod: ModShift}}, + }, + seqTest{ + []byte("\x1b[97;;229u"), + []Event{KeyPressEvent{Code: 'a', Text: "å"}}, + }, + // focus/blur seqTest{ []byte{'\x1b', '[', 'I'}, @@ -132,72 +242,72 @@ func TestParseSequence(t *testing.T) { seqTest{ []byte{'a'}, []Event{ - KeyPressEvent{Rune: 'a'}, + KeyPressEvent{Code: 'a', Text: "a"}, }, }, seqTest{ []byte{'\x1b', 'a'}, []Event{ - KeyPressEvent{Rune: 'a', Mod: ModAlt}, + KeyPressEvent{Code: 'a', Mod: ModAlt}, }, }, seqTest{ []byte{'a', 'a', 'a'}, []Event{ - KeyPressEvent{Rune: 'a'}, - KeyPressEvent{Rune: 'a'}, - KeyPressEvent{Rune: 'a'}, + KeyPressEvent{Code: 'a', Text: "a"}, + KeyPressEvent{Code: 'a', Text: "a"}, + KeyPressEvent{Code: 'a', Text: "a"}, }, }, // Multi-byte rune. seqTest{ []byte("☃"), []Event{ - KeyPressEvent{Rune: '☃'}, + KeyPressEvent{Code: '☃', Text: "☃"}, }, }, seqTest{ []byte("\x1b☃"), []Event{ - KeyPressEvent{Rune: '☃', Mod: ModAlt}, + KeyPressEvent{Code: '☃', Mod: ModAlt}, }, }, - // Standalone control chacters. + // Standalone control characters. seqTest{ []byte{'\x1b'}, []Event{ - KeyPressEvent{Sym: KeyEscape}, + KeyPressEvent{Code: KeyEscape}, }, }, seqTest{ []byte{ansi.SOH}, []Event{ - KeyPressEvent{Rune: 'a', Mod: ModCtrl}, + KeyPressEvent{Code: 'a', Mod: ModCtrl}, }, }, seqTest{ []byte{'\x1b', ansi.SOH}, []Event{ - KeyPressEvent{Rune: 'a', Mod: ModCtrl | ModAlt}, + KeyPressEvent{Code: 'a', Mod: ModCtrl | ModAlt}, }, }, seqTest{ []byte{ansi.NUL}, []Event{ - KeyPressEvent{Rune: ' ', Sym: KeySpace, Mod: ModCtrl}, + KeyPressEvent{Code: KeySpace, Mod: ModCtrl}, }, }, seqTest{ []byte{'\x1b', ansi.NUL}, []Event{ - KeyPressEvent{Rune: ' ', Sym: KeySpace, Mod: ModCtrl | ModAlt}, + KeyPressEvent{Code: KeySpace, Mod: ModCtrl | ModAlt}, }, }, // C1 control characters. seqTest{ []byte{'\x80'}, []Event{ - KeyPressEvent{Rune: 0x80 - '@', Mod: ModCtrl | ModAlt}, + KeyPressEvent{Code: rune(0x80 - '@'), Mod: ModCtrl | ModAlt}, }, }, ) @@ -213,17 +323,23 @@ func TestParseSequence(t *testing.T) { }) } + var p Parser for _, tc := range td { t.Run(fmt.Sprintf("%q", string(tc.seq)), func(t *testing.T) { var events []Event buf := tc.seq for len(buf) > 0 { - width, msg := ParseSequence(buf) - events = append(events, msg) + width, Event := p.parseSequence(buf) + switch Event := Event.(type) { + case MultiEvent: + events = append(events, Event...) + default: + events = append(events, Event) + } buf = buf[width:] } - if !reflect.DeepEqual(tc.msgs, events) { - t.Errorf("\nexpected event:\n %#v\ngot:\n %#v", tc.msgs, events) + if !reflect.DeepEqual(tc.Events, events) { + t.Errorf("\nexpected event for %q:\n %#v\ngot:\n %#v", tc.seq, tc.Events, events) } }) } @@ -232,15 +348,15 @@ func TestParseSequence(t *testing.T) { func TestReadLongInput(t *testing.T) { expect := make([]Event, 1000) for i := 0; i < 1000; i++ { - expect[i] = KeyPressEvent{Rune: 'a'} + expect[i] = KeyPressEvent{Code: 'a', Text: "a"} } input := strings.Repeat("a", 1000) - drv, err := NewDriver(strings.NewReader(input), "dumb", 0) + drv, err := NewReader(strings.NewReader(input), "dumb", 0) if err != nil { t.Fatalf("unexpected input driver error: %v", err) } - var msgs []Event + var Events []Event for { events, err := drv.ReadEvents() if err == io.EOF { @@ -249,11 +365,11 @@ func TestReadLongInput(t *testing.T) { if err != nil { t.Fatalf("unexpected input error: %v", err) } - msgs = append(msgs, events...) + Events = append(Events, events...) } - if !reflect.DeepEqual(expect, msgs) { - t.Errorf("unexpected messages, expected:\n %+v\ngot:\n %+v", expect, msgs) + if !reflect.DeepEqual(expect, Events) { + t.Errorf("unexpected messages, expected:\n %+v\ngot:\n %+v", expect, Events) } } @@ -268,70 +384,70 @@ func TestReadInput(t *testing.T) { "a", []byte{'a'}, []Event{ - KeyPressEvent{Sym: KeyNone, Rune: 'a'}, + KeyPressEvent{Code: 'a', Text: "a"}, }, }, { "space", []byte{' '}, []Event{ - KeyPressEvent{Sym: KeySpace, Rune: ' '}, + KeyPressEvent{Code: KeySpace, Text: " "}, }, }, { "a alt+a", []byte{'a', '\x1b', 'a'}, []Event{ - KeyPressEvent{Sym: KeyNone, Rune: 'a'}, - KeyPressEvent{Sym: KeyNone, Rune: 'a', Mod: ModAlt}, + KeyPressEvent{Code: 'a', Text: "a"}, + KeyPressEvent{Code: 'a', Mod: ModAlt}, }, }, { "a alt+a a", []byte{'a', '\x1b', 'a', 'a'}, []Event{ - KeyPressEvent{Sym: KeyNone, Rune: 'a'}, - KeyPressEvent{Sym: KeyNone, Rune: 'a', Mod: ModAlt}, - KeyPressEvent{Sym: KeyNone, Rune: 'a'}, + KeyPressEvent{Code: 'a', Text: "a"}, + KeyPressEvent{Code: 'a', Mod: ModAlt}, + KeyPressEvent{Code: 'a', Text: "a"}, }, }, { "ctrl+a", []byte{byte(ansi.SOH)}, []Event{ - KeyPressEvent{Rune: 'a', Mod: ModCtrl}, + KeyPressEvent{Code: 'a', Mod: ModCtrl}, }, }, { "ctrl+a ctrl+b", []byte{byte(ansi.SOH), byte(ansi.STX)}, []Event{ - KeyPressEvent{Rune: 'a', Mod: ModCtrl}, - KeyPressEvent{Rune: 'b', Mod: ModCtrl}, + KeyPressEvent{Code: 'a', Mod: ModCtrl}, + KeyPressEvent{Code: 'b', Mod: ModCtrl}, }, }, { "alt+a", []byte{byte(0x1b), 'a'}, []Event{ - KeyPressEvent{Sym: KeyNone, Mod: ModAlt, Rune: 'a'}, + KeyPressEvent{Code: 'a', Mod: ModAlt}, }, }, { "a b c d", []byte{'a', 'b', 'c', 'd'}, []Event{ - KeyPressEvent{Rune: 'a'}, - KeyPressEvent{Rune: 'b'}, - KeyPressEvent{Rune: 'c'}, - KeyPressEvent{Rune: 'd'}, + KeyPressEvent{Code: 'a', Text: "a"}, + KeyPressEvent{Code: 'b', Text: "b"}, + KeyPressEvent{Code: 'c', Text: "c"}, + KeyPressEvent{Code: 'd', Text: "d"}, }, }, { "up", []byte("\x1b[A"), []Event{ - KeyPressEvent{Sym: KeyUp}, + KeyPressEvent{Code: KeyUp}, }, }, { @@ -356,90 +472,90 @@ func TestReadInput(t *testing.T) { "shift+tab", []byte{'\x1b', '[', 'Z'}, []Event{ - KeyPressEvent{Sym: KeyTab, Mod: ModShift}, + KeyPressEvent{Code: KeyTab, Mod: ModShift}, }, }, { "enter", []byte{'\r'}, - []Event{KeyPressEvent{Sym: KeyEnter}}, + []Event{KeyPressEvent{Code: KeyEnter}}, }, { "alt+enter", []byte{'\x1b', '\r'}, []Event{ - KeyPressEvent{Sym: KeyEnter, Mod: ModAlt}, + KeyPressEvent{Code: KeyEnter, Mod: ModAlt}, }, }, { "insert", []byte{'\x1b', '[', '2', '~'}, []Event{ - KeyPressEvent{Sym: KeyInsert}, + KeyPressEvent{Code: KeyInsert}, }, }, { "ctrl+alt+a", []byte{'\x1b', byte(ansi.SOH)}, []Event{ - KeyPressEvent{Rune: 'a', Mod: ModCtrl | ModAlt}, + KeyPressEvent{Code: 'a', Mod: ModCtrl | ModAlt}, }, }, { "CSI?----X?", []byte{'\x1b', '[', '-', '-', '-', '-', 'X'}, - []Event{UnknownCsiEvent([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'})}, + []Event{UnknownEvent([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'})}, }, // Powershell sequences. { "up", []byte{'\x1b', 'O', 'A'}, - []Event{KeyPressEvent{Sym: KeyUp}}, + []Event{KeyPressEvent{Code: KeyUp}}, }, { "down", []byte{'\x1b', 'O', 'B'}, - []Event{KeyPressEvent{Sym: KeyDown}}, + []Event{KeyPressEvent{Code: KeyDown}}, }, { "right", []byte{'\x1b', 'O', 'C'}, - []Event{KeyPressEvent{Sym: KeyRight}}, + []Event{KeyPressEvent{Code: KeyRight}}, }, { "left", []byte{'\x1b', 'O', 'D'}, - []Event{KeyPressEvent{Sym: KeyLeft}}, + []Event{KeyPressEvent{Code: KeyLeft}}, }, { "alt+enter", []byte{'\x1b', '\x0d'}, - []Event{KeyPressEvent{Sym: KeyEnter, Mod: ModAlt}}, + []Event{KeyPressEvent{Code: KeyEnter, Mod: ModAlt}}, }, { "alt+backspace", []byte{'\x1b', '\x7f'}, - []Event{KeyPressEvent{Sym: KeyBackspace, Mod: ModAlt}}, + []Event{KeyPressEvent{Code: KeyBackspace, Mod: ModAlt}}, }, { "ctrl+space", []byte{'\x00'}, - []Event{KeyPressEvent{Sym: KeySpace, Rune: ' ', Mod: ModCtrl}}, + []Event{KeyPressEvent{Code: KeySpace, Mod: ModCtrl}}, }, { "ctrl+alt+space", []byte{'\x1b', '\x00'}, - []Event{KeyPressEvent{Sym: KeySpace, Rune: ' ', Mod: ModCtrl | ModAlt}}, + []Event{KeyPressEvent{Code: KeySpace, Mod: ModCtrl | ModAlt}}, }, { "esc", []byte{'\x1b'}, - []Event{KeyPressEvent{Sym: KeyEscape}}, + []Event{KeyPressEvent{Code: KeyEscape}}, }, { "alt+esc", []byte{'\x1b', '\x1b'}, - []Event{KeyPressEvent{Sym: KeyEscape, Mod: ModAlt}}, + []Event{KeyPressEvent{Code: KeyEscape, Mod: ModAlt}}, }, { "a b o", @@ -453,7 +569,7 @@ func TestReadInput(t *testing.T) { PasteStartEvent{}, PasteEvent("a b"), PasteEndEvent{}, - KeyPressEvent{Sym: KeyNone, Rune: 'o'}, + KeyPressEvent{Code: 'o', Text: "o"}, }, }, { @@ -480,35 +596,35 @@ func TestReadInput(t *testing.T) { "a ?0xfe? b", []byte{'a', '\xfe', ' ', 'b'}, []Event{ - KeyPressEvent{Sym: KeyNone, Rune: 'a'}, + KeyPressEvent{Code: 'a', Text: "a"}, UnknownEvent(rune(0xfe)), - KeyPressEvent{Sym: KeySpace, Rune: ' '}, - KeyPressEvent{Sym: KeyNone, Rune: 'b'}, + KeyPressEvent{Code: KeySpace, Text: " "}, + KeyPressEvent{Code: 'b', Text: "b"}, }, }, } for i, td := range testData { t.Run(fmt.Sprintf("%d: %s", i, td.keyname), func(t *testing.T) { - msgs := testReadInputs(t, bytes.NewReader(td.in)) + Events := testReadInputs(t, bytes.NewReader(td.in)) var buf strings.Builder - for i, msg := range msgs { + for i, Event := range Events { if i > 0 { buf.WriteByte(' ') } - if s, ok := msg.(fmt.Stringer); ok { + if s, ok := Event.(fmt.Stringer); ok { buf.WriteString(s.String()) } else { - fmt.Fprintf(&buf, "%#v:%T", msg, msg) + fmt.Fprintf(&buf, "%#v:%T", Event, Event) } } - if len(msgs) != len(td.out) { - 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 len(Events) != len(td.out) { + t.Fatalf("unexpected message list length: got %d, expected %d\n got: %#v\n expected: %#v\n", len(Events), len(td.out), Events, td.out) } - if !reflect.DeepEqual(td.out, msgs) { - t.Fatalf("expected:\n%#v\ngot:\n%#v", td.out, msgs) + if !reflect.DeepEqual(td.out, Events) { + t.Fatalf("expected:\n%#v\ngot:\n%#v", td.out, Events) } }) } @@ -528,13 +644,13 @@ func testReadInputs(t *testing.T, input io.Reader) []Event { } }() - dr, err := NewDriver(input, "dumb", 0) + dr, err := NewReader(input, "dumb", 0) if err != nil { t.Fatalf("unexpected input driver error: %v", err) } // The messages we're consuming. - msgsC := make(chan Event) + EventsC := make(chan Event) // Start the reader in the background. wg.Add(1) @@ -545,30 +661,30 @@ func testReadInputs(t *testing.T, input io.Reader) []Event { out: for _, ev := range events { select { - case msgsC <- ev: + case EventsC <- ev: case <-ctx.Done(): break out } } - msgsC <- nil + EventsC <- nil }() - var msgs []Event + var Events []Event loop: for { select { - case msg := <-msgsC: - if msg == nil { + case Event := <-EventsC: + if Event == nil { // end of input marker for the test. break loop } - msgs = append(msgs, msg) + Events = append(Events, Event) case <-time.After(2 * time.Second): t.Errorf("timeout waiting for input event") break loop } } - return msgs + return Events } // randTest defines the test input and expected output for a sequence @@ -661,6 +777,7 @@ func genRandomDataWithSeed(s int64, length int) randTest { } func FuzzParseSequence(f *testing.F) { + var p Parser for seq := range sequences { f.Add(seq) } @@ -669,7 +786,7 @@ func FuzzParseSequence(f *testing.F) { 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)) + n, _ := p.parseSequence([]byte(seq)) if n == 0 && seq != "" { t.Errorf("expected a non-zero width for %q", seq) } @@ -679,10 +796,11 @@ func FuzzParseSequence(f *testing.F) { // BenchmarkDetectSequenceMap benchmarks the map-based sequence // detector. func BenchmarkDetectSequenceMap(b *testing.B) { + var p Parser td := genRandomDataWithSeed(123, 10000) for i := 0; i < b.N; i++ { for j, w := 0, 0; j < len(td.data); j += w { - w, _ = ParseSequence(td.data[j:]) + w, _ = p.parseSequence(td.data[j:]) } } } diff --git a/input/kitty.go b/input/kitty.go index 21466477..13b7c997 100644 --- a/input/kitty.go +++ b/input/kitty.go @@ -5,156 +5,161 @@ import ( "unicode/utf8" "github.com/charmbracelet/x/ansi" - "github.com/charmbracelet/x/ansi/parser" ) -// KittyKeyboardEvent represents Kitty keyboard progressive enhancement flags. -type KittyKeyboardEvent int +// KittyEnhancementsEvent represents a Kitty enhancements event. +type KittyEnhancementsEvent int -// IsDisambiguateEscapeCodes returns true if the DisambiguateEscapeCodes flag is set. -func (e KittyKeyboardEvent) IsDisambiguateEscapeCodes() bool { - return e&ansi.KittyDisambiguateEscapeCodes != 0 -} - -// IsReportEventTypes returns true if the ReportEventTypes flag is set. -func (e KittyKeyboardEvent) IsReportEventTypes() bool { - return e&ansi.KittyReportEventTypes != 0 -} +// Kitty keyboard enhancement constants. +// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +const ( + KittyDisambiguateEscapeCodes KittyEnhancementsEvent = 1 << iota + KittyReportEventTypes + KittyReportAlternateKeys + KittyReportAllKeysAsEscapeCodes + KittyReportAssociatedText +) -// IsReportAlternateKeys returns true if the ReportAlternateKeys flag is set. -func (e KittyKeyboardEvent) IsReportAlternateKeys() bool { - return e&ansi.KittyReportAlternateKeys != 0 +// Contains reports whether m contains the given enhancements. +func (e KittyEnhancementsEvent) Contains(enhancements KittyEnhancementsEvent) bool { + return e&enhancements == enhancements } -// IsReportAllKeys returns true if the ReportAllKeys flag is set. -func (e KittyKeyboardEvent) IsReportAllKeys() bool { - return e&ansi.KittyReportAllKeysAsEscapeCodes != 0 -} +// Kitty Clipboard Control Sequences +var kittyKeyMap = map[int]Key{ + ansi.BS: {Code: KeyBackspace}, + ansi.HT: {Code: KeyTab}, + ansi.CR: {Code: KeyEnter}, + ansi.ESC: {Code: KeyEscape}, + ansi.DEL: {Code: KeyBackspace}, -// IsReportAssociatedKeys returns true if the ReportAssociatedKeys flag is set. -func (e KittyKeyboardEvent) IsReportAssociatedKeys() bool { - return e&ansi.KittyReportAssociatedKeys != 0 + 57344: {Code: KeyEscape}, + 57345: {Code: KeyEnter}, + 57346: {Code: KeyTab}, + 57347: {Code: KeyBackspace}, + 57348: {Code: KeyInsert}, + 57349: {Code: KeyDelete}, + 57350: {Code: KeyLeft}, + 57351: {Code: KeyRight}, + 57352: {Code: KeyUp}, + 57353: {Code: KeyDown}, + 57354: {Code: KeyPgUp}, + 57355: {Code: KeyPgDown}, + 57356: {Code: KeyHome}, + 57357: {Code: KeyEnd}, + 57358: {Code: KeyCapsLock}, + 57359: {Code: KeyScrollLock}, + 57360: {Code: KeyNumLock}, + 57361: {Code: KeyPrintScreen}, + 57362: {Code: KeyPause}, + 57363: {Code: KeyMenu}, + 57364: {Code: KeyF1}, + 57365: {Code: KeyF2}, + 57366: {Code: KeyF3}, + 57367: {Code: KeyF4}, + 57368: {Code: KeyF5}, + 57369: {Code: KeyF6}, + 57370: {Code: KeyF7}, + 57371: {Code: KeyF8}, + 57372: {Code: KeyF9}, + 57373: {Code: KeyF10}, + 57374: {Code: KeyF11}, + 57375: {Code: KeyF12}, + 57376: {Code: KeyF13}, + 57377: {Code: KeyF14}, + 57378: {Code: KeyF15}, + 57379: {Code: KeyF16}, + 57380: {Code: KeyF17}, + 57381: {Code: KeyF18}, + 57382: {Code: KeyF19}, + 57383: {Code: KeyF20}, + 57384: {Code: KeyF21}, + 57385: {Code: KeyF22}, + 57386: {Code: KeyF23}, + 57387: {Code: KeyF24}, + 57388: {Code: KeyF25}, + 57389: {Code: KeyF26}, + 57390: {Code: KeyF27}, + 57391: {Code: KeyF28}, + 57392: {Code: KeyF29}, + 57393: {Code: KeyF30}, + 57394: {Code: KeyF31}, + 57395: {Code: KeyF32}, + 57396: {Code: KeyF33}, + 57397: {Code: KeyF34}, + 57398: {Code: KeyF35}, + 57399: {Code: KeyKp0}, + 57400: {Code: KeyKp1}, + 57401: {Code: KeyKp2}, + 57402: {Code: KeyKp3}, + 57403: {Code: KeyKp4}, + 57404: {Code: KeyKp5}, + 57405: {Code: KeyKp6}, + 57406: {Code: KeyKp7}, + 57407: {Code: KeyKp8}, + 57408: {Code: KeyKp9}, + 57409: {Code: KeyKpDecimal}, + 57410: {Code: KeyKpDivide}, + 57411: {Code: KeyKpMultiply}, + 57412: {Code: KeyKpMinus}, + 57413: {Code: KeyKpPlus}, + 57414: {Code: KeyKpEnter}, + 57415: {Code: KeyKpEqual}, + 57416: {Code: KeyKpSep}, + 57417: {Code: KeyKpLeft}, + 57418: {Code: KeyKpRight}, + 57419: {Code: KeyKpUp}, + 57420: {Code: KeyKpDown}, + 57421: {Code: KeyKpPgUp}, + 57422: {Code: KeyKpPgDown}, + 57423: {Code: KeyKpHome}, + 57424: {Code: KeyKpEnd}, + 57425: {Code: KeyKpInsert}, + 57426: {Code: KeyKpDelete}, + 57427: {Code: KeyKpBegin}, + 57428: {Code: KeyMediaPlay}, + 57429: {Code: KeyMediaPause}, + 57430: {Code: KeyMediaPlayPause}, + 57431: {Code: KeyMediaReverse}, + 57432: {Code: KeyMediaStop}, + 57433: {Code: KeyMediaFastForward}, + 57434: {Code: KeyMediaRewind}, + 57435: {Code: KeyMediaNext}, + 57436: {Code: KeyMediaPrev}, + 57437: {Code: KeyMediaRecord}, + 57438: {Code: KeyLowerVol}, + 57439: {Code: KeyRaiseVol}, + 57440: {Code: KeyMute}, + 57441: {Code: KeyLeftShift}, + 57442: {Code: KeyLeftCtrl}, + 57443: {Code: KeyLeftAlt}, + 57444: {Code: KeyLeftSuper}, + 57445: {Code: KeyLeftHyper}, + 57446: {Code: KeyLeftMeta}, + 57447: {Code: KeyRightShift}, + 57448: {Code: KeyRightCtrl}, + 57449: {Code: KeyRightAlt}, + 57450: {Code: KeyRightSuper}, + 57451: {Code: KeyRightHyper}, + 57452: {Code: KeyRightMeta}, + 57453: {Code: KeyIsoLevel3Shift}, + 57454: {Code: KeyIsoLevel5Shift}, } -// 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, +func init() { + // These are some faulty C0 mappings some terminals such as WezTerm have + // and doesn't follow the specs. + kittyKeyMap[ansi.NUL] = Key{Code: KeySpace, Mod: ModCtrl} + for i := ansi.SOH; i <= ansi.SUB; i++ { + if _, ok := kittyKeyMap[i]; !ok { + kittyKeyMap[i] = Key{Code: rune(i + 0x60), Mod: ModCtrl} + } + } + for i := ansi.FS; i <= ansi.US; i++ { + if _, ok := kittyKeyMap[i]; !ok { + kittyKeyMap[i] = Key{Code: rune(i + 0x40), Mod: ModCtrl} + } + } } const ( @@ -210,78 +215,129 @@ func fromKittyMod(mod int) KeyMod { // 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) Event { +func parseKittyKeyboard(csi *ansi.CsiSequence) (Event Event) { var isRelease bool - key := Key{} + var key Key - pparams := make([]int, 0, len(csi.Params)) + // The index of parameters separated by semicolons ';'. Sub parameters are + // separated by colons ':'. + var paramIdx int + var sudIdx int // The sub parameter index for _, p := range csi.Params { - pparams = append(pparams, int(p)) - } - if params := parser.Subparams(pparams, 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 - } + // Kitty Keyboard Protocol has 3 optional components. + switch paramIdx { + case 0: + switch sudIdx { + case 0: + var foundKey bool + code := p.Param(1) // CSI u has a default value of 1 + key, foundKey = kittyKeyMap[code] + if !foundKey { + r := rune(code) + if !utf8.ValidRune(r) { + r = utf8.RuneError + } - key.Rune = r + key.Code = r + } - // alternate key reporting - switch len(params) { - case 3: + case 2: // shifted key + base key - if b := rune(params[2]); unicode.IsPrint(b) { + if b := rune(p.Param(1)); unicode.IsPrint(b) { // XXX: When alternate key reporting is enabled, the protocol // can return 3 things, the unicode codepoint of the key, // the shifted codepoint of the key, and the standard // PC-101 key layout codepoint. // This is useful to create an unambiguous mapping of keys // when using a different language layout. - key.baseRune = b + key.BaseCode = b } fallthrough - case 2: + + case 1: // shifted key - if s := rune(params[1]); unicode.IsPrint(s) { + if s := rune(p.Param(1)); unicode.IsPrint(s) { // XXX: We swap keys here because we want the shifted key // to be the Rune that is returned by the event. // For example, shift+a should produce "A" not "a". // In such a case, we set AltRune to the original key "a" // and Rune to "A". - key.AltRune = key.Rune - key.Rune = s + key.ShiftedCode = s + } + } + case 1: + switch sudIdx { + case 0: + mod := p.Param(1) + if mod > 1 { + key.Mod = fromKittyMod(mod - 1) + if key.Mod > ModShift { + // XXX: We need to clear the text if we have a modifier key + // other than a [ModShift] key. + key.Text = "" + } + } + + case 1: + switch p.Param(1) { + case 2: + key.IsRepeat = true + case 3: + isRelease = true } + case 2: + } + case 2: + if code := p.Param(0); code != 0 { + key.Text += string(rune(code)) } } - } - if params := parser.Subparams(pparams, 1); len(params) > 0 { - mod := params[0] - if mod > 1 { - key.Mod = fromKittyMod(mod - 1) + sudIdx++ + if !p.HasMore() { + paramIdx++ + sudIdx = 0 } - if len(params) > 1 { - switch params[1] { - case 2: - key.IsRepeat = true - case 3: - isRelease = true + } + + if len(key.Text) == 0 && unicode.IsPrint(key.Code) && + (key.Mod <= ModShift || key.Mod == ModCapsLock) { + if key.Mod == 0 { + key.Text = string(key.Code) + } else { + desiredCase := unicode.ToLower + if key.Mod == ModShift || key.Mod == ModCapsLock { + desiredCase = unicode.ToUpper + } + if key.ShiftedCode != 0 { + key.Text = string(key.ShiftedCode) + } else { + key.Text = string(desiredCase(key.Code)) } } } - // 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 KeyReleaseEvent(key) } + return KeyPressEvent(key) } + +// parseKittyKeyboardExt parses a Kitty Keyboard Protocol sequence extensions +// for non CSI u sequences. This includes things like CSI A, SS3 A and others, +// and CSI ~. +func parseKittyKeyboardExt(csi *ansi.CsiSequence, k KeyPressEvent) Event { + // Handle Kitty keyboard protocol + if len(csi.Params) > 2 && // We have at least 3 parameters + csi.Params[0].Param(1) == 1 && // The first parameter is 1 (defaults to 1) + csi.Params[1].HasMore() { // The second parameter is a subparameter (separated by a ":") + switch csi.Params[2].Param(1) { // The third parameter is the event type (defaults to 1) + case 2: + k.IsRepeat = true + case 3: + return KeyReleaseEvent(k) + } + } + return k +} diff --git a/input/mod.go b/input/mod.go index 3c91aa59..c0076276 100644 --- a/input/mod.go +++ b/input/mod.go @@ -1,7 +1,7 @@ package input // KeyMod represents modifier keys. -type KeyMod uint16 +type KeyMod int // Modifier keys. const ( @@ -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/input/mode.go b/input/mode.go index e838e681..ea1ba571 100644 --- a/input/mode.go +++ b/input/mode.go @@ -1,12 +1,14 @@ package input -// ReportModeEvent represents a report mode event for sequence DECRPM. +import "github.com/charmbracelet/x/ansi" + +// ModeReportEvent is a message that represents a mode report event (DECRPM). // // See: https://vt100.net/docs/vt510-rm/DECRPM.html -type ReportModeEvent struct { +type ModeReportEvent struct { // Mode is the mode number. - Mode int + Mode ansi.Mode // Value is the mode value. - Value int + Value ansi.ModeSetting } diff --git a/input/mouse.go b/input/mouse.go index dcdc192a..c8c14f4a 100644 --- a/input/mouse.go +++ b/input/mouse.go @@ -1,13 +1,13 @@ package input import ( - "regexp" + "fmt" "github.com/charmbracelet/x/ansi" ) -// MouseButton represents the button that was pressed during a mouse event. -type MouseButton byte +// MouseButton represents the button that was pressed during a mouse message. +type MouseButton = ansi.MouseButton // Mouse event buttons // @@ -27,56 +27,67 @@ type MouseButton byte // // Other buttons are not supported. const ( - MouseNone MouseButton = iota - MouseLeft - MouseMiddle - MouseRight - MouseWheelUp - MouseWheelDown - MouseWheelLeft - MouseWheelRight - MouseBackward - MouseForward - MouseExtra1 - MouseExtra2 + MouseNone = ansi.MouseNone + MouseLeft = ansi.MouseLeft + MouseMiddle = ansi.MouseMiddle + MouseRight = ansi.MouseRight + MouseWheelUp = ansi.MouseWheelUp + MouseWheelDown = ansi.MouseWheelDown + MouseWheelLeft = ansi.MouseWheelLeft + MouseWheelRight = ansi.MouseWheelRight + MouseBackward = ansi.MouseBackward + MouseForward = ansi.MouseForward + MouseButton10 = ansi.MouseButton10 + MouseButton11 = ansi.MouseButton11 ) -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", +// MouseEvent represents a mouse message. This is a generic mouse message that +// can represent any kind of mouse event. +type MouseEvent interface { + fmt.Stringer + + // Mouse returns the underlying mouse event. + Mouse() Mouse } -// Mouse represents a Mouse event. +// Mouse represents a Mouse message. Use [MouseEvent] to represent all mouse +// messages. +// +// The X and Y coordinates are zero-based, with (0,0) being the upper left +// corner of the terminal. +// +// // Catch all mouse events +// switch Event := Event.(type) { +// case MouseEvent: +// m := Event.Mouse() +// fmt.Println("Mouse event:", m.X, m.Y, m) +// } +// +// // Only catch mouse click events +// switch Event := Event.(type) { +// case MouseClickEvent: +// fmt.Println("Mouse click event:", Event.X, Event.Y, Event) +// } type Mouse struct { X, Y int Button MouseButton Mod KeyMod } -// String implements fmt.Stringer. +// 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+" } - str, ok := mouseButtons[m.Button] - if !ok { + str := m.Button.String() + if str == "" { s += "unknown" } else if str != "none" { // motion events don't have a button s += str @@ -88,31 +99,52 @@ func (m Mouse) String() (s string) { // MouseClickEvent represents a mouse button click event. type MouseClickEvent Mouse -// String implements fmt.Stringer. +// String returns a string representation of the mouse click event. func (e MouseClickEvent) String() string { return Mouse(e).String() } +// Mouse returns the underlying mouse event. This is a convenience method and +// syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse +// event to [Mouse]. +func (e MouseClickEvent) Mouse() Mouse { + return Mouse(e) +} + // MouseReleaseEvent represents a mouse button release event. type MouseReleaseEvent Mouse -// String implements fmt.Stringer. +// String returns a string representation of the mouse release event. func (e MouseReleaseEvent) String() string { return Mouse(e).String() } -// MouseWheelEvent represents a mouse wheel event. +// Mouse returns the underlying mouse event. This is a convenience method and +// syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse +// event to [Mouse]. +func (e MouseReleaseEvent) Mouse() Mouse { + return Mouse(e) +} + +// MouseWheelEvent represents a mouse wheel message event. type MouseWheelEvent Mouse -// String implements fmt.Stringer. +// String returns a string representation of the mouse wheel event. func (e MouseWheelEvent) String() string { return Mouse(e).String() } +// Mouse returns the underlying mouse event. This is a convenience method and +// syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse +// event to [Mouse]. +func (e MouseWheelEvent) Mouse() Mouse { + return Mouse(e) +} + // MouseMotionEvent represents a mouse motion event. type MouseMotionEvent Mouse -// String implements fmt.Stringer. +// String returns a string representation of the mouse motion event. func (e MouseMotionEvent) String() string { m := Mouse(e) if m.Button != 0 { @@ -121,7 +153,12 @@ func (e MouseMotionEvent) String() string { return m.String() + "motion" } -var mouseSGRRegex = regexp.MustCompile(`(\d+);(\d+);(\d+)([Mm])`) +// Mouse returns the underlying mouse event. This is a convenience method and +// syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse +// event to [Mouse]. +func (e MouseMotionEvent) Mouse() Mouse { + return Mouse(e) +} // Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events // look like: @@ -137,10 +174,16 @@ var mouseSGRRegex = regexp.MustCompile(`(\d+);(\d+);(\d+)([Mm])`) // // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates func parseSGRMouseEvent(csi *ansi.CsiSequence) Event { - b, _ := csi.Param(0, -1) - x, _ := csi.Param(1, -1) - y, _ := csi.Param(2, -1) + x, ok := csi.Param(1, 1) + if !ok { + x = 1 + } + y, ok := csi.Param(2, 1) + if !ok { + y = 1 + } release := csi.Command() == 'm' + b, _ := csi.Param(0, 0) mod, btn, _, isMotion := parseMouseButton(b) // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). diff --git a/input/mouse_test.go b/input/mouse_test.go index 7d8750f5..b9c50ff6 100644 --- a/input/mouse_test.go +++ b/input/mouse_test.go @@ -228,12 +228,12 @@ func TestParseX10MouseDownEvent(t *testing.T) { { name: "button 10", buf: encode(0b1000_0010, 32, 16), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseExtra1}, + expected: MouseClickEvent{X: 32, Y: 16, Button: MouseButton10}, }, { name: "button 11", buf: encode(0b1000_0011, 32, 16), - expected: MouseClickEvent{X: 32, Y: 16, Button: MouseExtra2}, + expected: MouseClickEvent{X: 32, Y: 16, Button: MouseButton11}, }, // Combinations. { @@ -317,7 +317,7 @@ func TestParseSGRMouseEvent(t *testing.T) { ansi.Parameter(x + 1), ansi.Parameter(y + 1), }, - Cmd: ansi.Command(int(re) | ('<' << parser.MarkerShift)), + Cmd: ansi.Command(re) | ('<' << parser.MarkerShift), } } diff --git a/input/parse.go b/input/parse.go index 172dd487..4a0a6062 100644 --- a/input/parse.go +++ b/input/parse.go @@ -3,11 +3,12 @@ package input import ( "encoding/base64" "strings" + "unicode" "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. @@ -83,20 +84,36 @@ const ( FlagFKeys ) -var flags int +// Parser is a parser for input escape sequences. +type Parser struct { + flags int +} -// SetFlags sets the flags for the parser. -// This will control the behavior of ParseSequence. -func SetFlags(f int) { - flags = f +// NewParser returns a new input parser. This is a low-level parser that parses +// escape sequences into human-readable events. +// This differs from [ansi.Parser] and [ansi.DecodeSequence] in which it +// recognizes incorrect sequences that some terminals may send. +// +// For instance, the X10 mouse protocol sends a `CSI M` sequence followed by 3 +// bytes. If the parser doesn't recognize the 3 bytes, they might be echoed to +// the terminal output causing a mess. +// +// Another example is how URxvt sends invalid sequences for modified keys using +// invalid CSI final characters like '$'. +// +// Use flags to control the behavior of ambiguous key sequences. +func NewParser(flags int) *Parser { + return &Parser{flags: flags} } -// ParseSequence finds the first recognized event sequence and returns it along +// 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, e Event) { +// +// TODO: Use [ansi.DecodeSequence] instead of this parser. +func (p *Parser) parseSequence(buf []byte) (n int, Event Event) { if len(buf) == 0 { return 0, nil } @@ -105,58 +122,60 @@ func ParseSequence(buf []byte) (n int, e Event) { case ansi.ESC: if len(buf) == 1 { // Escape key - return 1, KeyPressEvent{Sym: KeyEscape} + return 1, KeyPressEvent{Code: KeyEscape} } - switch b := buf[1]; b { + switch bPrime := buf[1]; bPrime { case 'O': // Esc-prefixed SS3 - return parseSs3(buf) + return p.parseSs3(buf) case 'P': // Esc-prefixed DCS - return parseDcs(buf) + return p.parseDcs(buf) case '[': // Esc-prefixed CSI - return parseCsi(buf) + return p.parseCsi(buf) case ']': // Esc-prefixed OSC - return parseOsc(buf) + return p.parseOsc(buf) case '_': // Esc-prefixed APC - return parseApc(buf) + return p.parseApc(buf) default: - n, e := ParseSequence(buf[1:]) - if k, ok := e.(KeyPressEvent); ok && !k.Mod.HasAlt() { + n, e := p.parseSequence(buf[1:]) + if k, ok := e.(KeyPressEvent); ok { + k.Text = "" k.Mod |= ModAlt return n + 1, k } // Not a key sequence, nor an alt modified key sequence. In that // case, just report a single escape key. - return 1, KeyPressEvent{Sym: KeyEscape} + return 1, KeyPressEvent{Code: KeyEscape} } case ansi.SS3: - return parseSs3(buf) + return p.parseSs3(buf) case ansi.DCS: - return parseDcs(buf) + return p.parseDcs(buf) case ansi.CSI: - return parseCsi(buf) + return p.parseCsi(buf) case ansi.OSC: - return parseOsc(buf) + return p.parseOsc(buf) case ansi.APC: - return parseApc(buf) + return p.parseApc(buf) default: if b <= ansi.US || b == ansi.DEL || b == ansi.SP { - return 1, parseControl(b) + return 1, p.parseControl(b) } else if b >= ansi.PAD && b <= ansi.APC { // C1 control code // UTF-8 never starts with a C1 control code // Encode these as Ctrl+Alt+ - return 1, KeyPressEvent{Rune: rune(b) - 0x40, Mod: ModCtrl | ModAlt} + code := rune(b) - 0x40 + return 1, KeyPressEvent{Code: code, Mod: ModCtrl | ModAlt} } - return parseUtf8(buf) + return p.parseUtf8(buf) } } -func parseCsi(b []byte) (int, Event) { +func (p *Parser) parseCsi(b []byte) (int, Event) { if len(b) == 2 && b[0] == ansi.ESC { // short cut if this is an alt+[ key - return 2, KeyPressEvent{Rune: rune(b[1]), Mod: ModAlt} + return 2, KeyPressEvent{Text: string(rune(b[1])), Mod: ModAlt} } var csi ansi.CsiSequence @@ -173,7 +192,7 @@ func parseCsi(b []byte) (int, Event) { // Initial CSI byte if i < len(b) && b[i] >= '<' && b[i] <= '?' { - csi.Cmd = ansi.Command(int(csi.Cmd) | int(b[i])<= len(b) || b[i] < 0x40 || b[i] > 0x7E { @@ -218,7 +237,7 @@ func parseCsi(b []byte) (int, Event) { // CSI $ is an invalid sequence, but URxvt uses it for // shift modified keys. if b[i-1] == '$' { - n, ev := parseCsi(append(b[:i-1], '~')) + n, ev := p.parseCsi(append(b[:i-1], '~')) if k, ok := ev.(KeyPressEvent); ok { k.Mod |= ModShift return n, k @@ -228,187 +247,189 @@ func parseCsi(b []byte) (int, Event) { } // Add the final byte - csi.Cmd = ansi.Command(int(csi.Cmd) | int(b[i])) + csi.Cmd |= ansi.Command(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, UnknownCsiEvent(b[:i]) - } - mode, _ := csi.Param(0, 0) - value, _ := csi.Param(1, 0) - return i, ReportModeEvent{Mode: mode, Value: value} - } - case 'c': - // Primary Device Attributes - return i, parsePrimaryDevAttrs(&csi) - case 'u': - // Kitty keyboard flags - param, _ := csi.Param(0, -1) - if param != -1 { - return i, KittyKeyboardEvent(param) - } - case 'R': - // This report may return a third parameter representing the page - // number, but we don't really need it. - if paramsLen >= 2 { - row, _ := csi.Param(0, 0) - col, _ := csi.Param(1, 0) - return i, CursorPositionEvent{Row: row, Column: col} - } + switch cmd := csi.Cmd; cmd { + case 'y' | '?'<': - switch cmd { - case 'm': - // XTerm modifyOtherKeys - p0, _ := csi.Param(0, 0) - if paramsLen != 2 || p0 != 4 { - return i, UnknownCsiEvent(b[:i]) - } - - p1, _ := csi.Param(1, 0) - return i, ModifyOtherKeysEvent(p1) - default: - return i, UnknownCsiEvent(b[:i]) + case 'm' | '>'< R (which is modified F3) when the cursor is at the + // row 1. In this case, we report both messages. + // + // For a non ambiguous cursor position report, use + // [ansi.RequestExtendedCursorPosition] (DECXCPR) instead. + return i, MultiEvent{KeyPressEvent{Code: KeyF3, Mod: KeyMod(col - 1)}, m} + } + + return i, m } - // 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. - row, _ := csi.Param(0, 1) - col, _ := csi.Param(1, 1) - if row != 1 { - return i, CursorPositionEvent{Row: row, Column: col} + if paramsLen != 0 { + break } + // Unmodified key F3 (CSI R) fallthrough case 'a', 'b', 'c', 'd', 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'S', 'Z': var k KeyPressEvent switch cmd { case 'a', 'b', 'c', 'd': - k = KeyPressEvent{Sym: KeyUp + KeySym(cmd-'a'), Mod: ModShift} + k = KeyPressEvent{Code: KeyUp + rune(cmd-'a'), Mod: ModShift} case 'A', 'B', 'C', 'D': - k = KeyPressEvent{Sym: KeyUp + KeySym(cmd-'A')} + k = KeyPressEvent{Code: KeyUp + rune(cmd-'A')} case 'E': - k = KeyPressEvent{Sym: KeyBegin} + k = KeyPressEvent{Code: KeyBegin} case 'F': - k = KeyPressEvent{Sym: KeyEnd} + k = KeyPressEvent{Code: KeyEnd} case 'H': - k = KeyPressEvent{Sym: KeyHome} + k = KeyPressEvent{Code: KeyHome} case 'P', 'Q', 'R', 'S': - k = KeyPressEvent{Sym: KeyF1 + KeySym(cmd-'P')} + k = KeyPressEvent{Code: KeyF1 + rune(cmd-'P')} case 'Z': - k = KeyPressEvent{Sym: KeyTab, Mod: ModShift} + k = KeyPressEvent{Code: KeyTab, Mod: ModShift} + } + id, _ := csi.Param(0, 1) + if id == 0 { + id = 1 + } + mod, _ := csi.Param(1, 1) + if mod == 0 { + mod = 1 } - p0, _ := csi.Param(0, 0) - if paramsLen > 1 && p0 == 1 { + if paramsLen > 1 && id == 1 && mod != -1 { // CSI 1 ; A - p1, _ := csi.Param(1, 0) - if paramsLen > 1 { - k.Mod |= KeyMod(p1 - 1) - } + k.Mod |= KeyMod(mod - 1) } - return i, k + // Don't forget to handle Kitty keyboard protocol + return i, parseKittyKeyboardExt(&csi, k) case 'M': // Handle X10 mouse if i+3 > len(b) { - return i, UnknownCsiEvent(b[:i]) + return i, UnknownEvent(b[:i]) } return i + 3, parseX10MouseEvent(append(b[:i], b[i:i+3]...)) - case 'y': + case 'y' | '$'< 1 { - mod, _ := csi.Param(1, 0) + mod, _ := csi.Param(1, -1) + if paramsLen > 1 && mod != -1 { k.Mod |= KeyMod(mod - 1) } // Handle URxvt weird keys switch cmd { + case '~': + // Don't forget to handle Kitty keyboard protocol + return i, parseKittyKeyboardExt(&csi, k) case '^': k.Mod |= ModCtrl case '@': @@ -483,15 +505,15 @@ func parseCsi(b []byte) (int, Event) { return i, k } } - return i, UnknownCsiEvent(b[:i]) + return i, UnknownEvent(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, Event) { +func (p *Parser) parseSs3(b []byte) (int, Event) { if len(b) == 2 && b[0] == ansi.ESC { // short cut if this is an alt+O key - return 2, KeyPressEvent{Rune: rune(b[1]), Mod: ModAlt} + return 2, KeyPressEvent{Code: rune(b[1]), Mod: ModAlt} } var i int @@ -523,25 +545,25 @@ func parseSs3(b []byte) (int, Event) { var k KeyPressEvent switch gl { case 'a', 'b', 'c', 'd': - k = KeyPressEvent{Sym: KeyUp + KeySym(gl-'a'), Mod: ModCtrl} + k = KeyPressEvent{Code: KeyUp + rune(gl-'a'), Mod: ModCtrl} case 'A', 'B', 'C', 'D': - k = KeyPressEvent{Sym: KeyUp + KeySym(gl-'A')} + k = KeyPressEvent{Code: KeyUp + rune(gl-'A')} case 'E': - k = KeyPressEvent{Sym: KeyBegin} + k = KeyPressEvent{Code: KeyBegin} case 'F': - k = KeyPressEvent{Sym: KeyEnd} + k = KeyPressEvent{Code: KeyEnd} case 'H': - k = KeyPressEvent{Sym: KeyHome} + k = KeyPressEvent{Code: KeyHome} case 'P', 'Q', 'R', 'S': - k = KeyPressEvent{Sym: KeyF1 + KeySym(gl-'P')} + k = KeyPressEvent{Code: KeyF1 + rune(gl-'P')} case 'M': - k = KeyPressEvent{Sym: KeyKpEnter} + k = KeyPressEvent{Code: KeyKpEnter} case 'X': - k = KeyPressEvent{Sym: KeyKpEqual} + k = KeyPressEvent{Code: KeyKpEqual} case 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y': - k = KeyPressEvent{Sym: KeyKpMultiply + KeySym(gl-'j')} + k = KeyPressEvent{Code: KeyKpMultiply + rune(gl-'j')} default: - return i, UnknownSs3Event(b[:i]) + return i, UnknownEvent(b[:i]) } // Handle weird SS3 Func @@ -552,10 +574,10 @@ func parseSs3(b []byte) (int, Event) { return i, k } -func parseOsc(b []byte) (int, Event) { +func (p *Parser) parseOsc(b []byte) (int, Event) { if len(b) == 2 && b[0] == ansi.ESC { // short cut if this is an alt+] key - return 2, KeyPressEvent{Rune: rune(b[1]), Mod: ModAlt} + return 2, KeyPressEvent{Code: rune(b[1]), Mod: ModAlt} } var i int @@ -605,35 +627,41 @@ func parseOsc(b []byte) (int, Event) { } if end <= start { - return i, UnknownOscEvent(b[:i]) + return i, UnknownEvent(b[:i]) } data := string(b[start:end]) switch cmd { case 10: - return i, ForegroundColorEvent{xParseColor(data)} + return i, ForegroundColorEvent{ansi.XParseColor(data)} case 11: - return i, BackgroundColorEvent{xParseColor(data)} + return i, BackgroundColorEvent{ansi.XParseColor(data)} case 12: - return i, CursorColorEvent{xParseColor(data)} + return i, CursorColorEvent{ansi.XParseColor(data)} case 52: parts := strings.Split(data, ";") if len(parts) == 0 { - return i, ClipboardEvent("") + return i, ClipboardEvent{} } - b64 := parts[len(parts)-1] + if len(parts) != 2 || len(parts[0]) < 1 { + break + } + + b64 := parts[1] bts, err := base64.StdEncoding.DecodeString(b64) if err != nil { - return i, ClipboardEvent("") + break } - return i, ClipboardEvent(bts) - default: - return i, UnknownOscEvent(b[:i]) + + sel := ClipboardSelection(parts[0][0]) + return i, ClipboardEvent{Selection: sel, Content: string(bts)} } + + return i, UnknownEvent(b[:i]) } // parseStTerminated parses a control sequence that gets terminated by a ST character. -func parseStTerminated(intro8, intro7 byte) func([]byte) (int, Event) { +func (p *Parser) parseStTerminated(intro8, intro7 byte) func([]byte) (int, Event) { return func(b []byte) (int, Event) { var i int if b[i] == intro8 || b[i] == ansi.ESC { @@ -651,14 +679,7 @@ func parseStTerminated(intro8, intro7 byte) func([]byte) (int, Event) { } if i >= len(b) { - switch intro8 { - case ansi.DCS: - return i, UnknownDcsEvent(b[:i]) - case ansi.APC: - return i, UnknownApcEvent(b[:i]) - default: - return i, UnknownEvent(b[:i]) - } + return i, UnknownEvent(b[:i]) } i++ @@ -667,21 +688,14 @@ func parseStTerminated(intro8, intro7 byte) func([]byte) (int, Event) { i++ } - switch intro8 { - case ansi.DCS: - return i, UnknownDcsEvent(b[:i]) - case ansi.APC: - return i, UnknownApcEvent(b[:i]) - default: - return i, UnknownEvent(b[:i]) - } + return i, UnknownEvent(b[:i]) } } -func parseDcs(b []byte) (int, Event) { +func (p *Parser) parseDcs(b []byte) (int, Event) { if len(b) == 2 && b[0] == ansi.ESC { // short cut if this is an alt+P key - return 2, KeyPressEvent{Rune: rune(b[1]), Mod: ModAlt} + return 2, KeyPressEvent{Code: rune(b[1]), Mod: ModAlt} } var params [16]ansi.Parameter @@ -699,7 +713,7 @@ func parseDcs(b []byte) (int, Event) { // initial DCS byte if i < len(b) && b[i] >= '<' && b[i] <= '?' { - dcs.Cmd = ansi.Command(int(dcs.Cmd) | int(b[i])<= len(b) || b[i] < 0x40 || b[i] > 0x7E { @@ -744,7 +758,7 @@ func parseDcs(b []byte) (int, Event) { } // Add the final byte - dcs.Cmd = ansi.Command(int(dcs.Cmd) | int(b[i])) + dcs.Cmd |= ansi.Command(b[i]) i++ start := i // start of the sequence data @@ -767,88 +781,121 @@ func parseDcs(b []byte) (int, Event) { } dcs.Params = params[:paramsLen] - switch cmd := dcs.Command(); cmd { - case 'r': - switch dcs.Intermediate() { - case '+': - // XTGETTCAP responses - switch param, _ := dcs.Param(0, -1); 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 - tc.IsValid = param == 1 - return i, tc - } - } - } - - return i, UnknownDcsEvent(b[:i]) + switch cmd := dcs.Cmd; cmd { + case 'r' | '+'<'< ansi.US && c < ansi.DEL { + // ASCII printable characters + code := rune(c) + k := KeyPressEvent{Code: code, Text: string(code)} + if unicode.IsUpper(code) { + // Convert upper case letters to lower case + shift modifier + k.Code = unicode.ToLower(code) + k.ShiftedCode = code + k.Mod |= ModShift + } + + return 1, k + } + + code, _ := utf8.DecodeRune(b) + if code == utf8.RuneError { return 1, UnknownEvent(b[0]) } - return rw, KeyPressEvent{Rune: r} + + cluster, _, _, _ := uniseg.FirstGraphemeCluster(b, -1) + text := string(cluster) + for i := range text { + if i > 0 { + // Use [KeyExtended] for multi-rune graphemes + code = KeyExtended + break + } + } + + return len(cluster), KeyPressEvent{Code: code, Text: text} } -func parseControl(b byte) Event { +func (p *Parser) parseControl(b byte) Event { switch b { case ansi.NUL: - if flags&FlagCtrlAt != 0 { - return KeyPressEvent{Rune: '@', Mod: ModCtrl} + if p.flags&FlagCtrlAt != 0 { + return KeyPressEvent{Code: '@', Mod: ModCtrl} } - return KeyPressEvent{Rune: ' ', Sym: KeySpace, Mod: ModCtrl} + return KeyPressEvent{Code: KeySpace, Mod: ModCtrl} case ansi.BS: - return KeyPressEvent{Rune: 'h', Mod: ModCtrl} + return KeyPressEvent{Code: 'h', Mod: ModCtrl} case ansi.HT: - if flags&FlagCtrlI != 0 { - return KeyPressEvent{Rune: 'i', Mod: ModCtrl} + if p.flags&FlagCtrlI != 0 { + return KeyPressEvent{Code: 'i', Mod: ModCtrl} } - return KeyPressEvent{Sym: KeyTab} + return KeyPressEvent{Code: KeyTab} case ansi.CR: - if flags&FlagCtrlM != 0 { - return KeyPressEvent{Rune: 'm', Mod: ModCtrl} + if p.flags&FlagCtrlM != 0 { + return KeyPressEvent{Code: 'm', Mod: ModCtrl} } - return KeyPressEvent{Sym: KeyEnter} + return KeyPressEvent{Code: KeyEnter} case ansi.ESC: - if flags&FlagCtrlOpenBracket != 0 { - return KeyPressEvent{Rune: '[', Mod: ModCtrl} + if p.flags&FlagCtrlOpenBracket != 0 { + return KeyPressEvent{Code: '[', Mod: ModCtrl} } - return KeyPressEvent{Sym: KeyEscape} + return KeyPressEvent{Code: KeyEscape} case ansi.DEL: - if flags&FlagBackspace != 0 { - return KeyPressEvent{Sym: KeyDelete} + if p.flags&FlagBackspace != 0 { + return KeyPressEvent{Code: KeyDelete} } - return KeyPressEvent{Sym: KeyBackspace} + return KeyPressEvent{Code: KeyBackspace} case ansi.SP: - return KeyPressEvent{Sym: KeySpace, Rune: ' '} + return KeyPressEvent{Code: KeySpace, Text: " "} default: if b >= ansi.SOH && b <= ansi.SUB { // Use lower case letters for control codes - return KeyPressEvent{Rune: rune(b + 0x60), Mod: ModCtrl} + code := rune(b + 0x60) + return KeyPressEvent{Code: code, Mod: ModCtrl} } else if b >= ansi.FS && b <= ansi.US { - return KeyPressEvent{Rune: rune(b + 0x40), Mod: ModCtrl} + code := rune(b + 0x40) + return KeyPressEvent{Code: code, Mod: ModCtrl} } return UnknownEvent(b) } diff --git a/input/parse_test.go b/input/parse_test.go index 0758fd19..dc892e0c 100644 --- a/input/parse_test.go +++ b/input/parse_test.go @@ -2,39 +2,46 @@ package input import ( "image/color" + "reflect" "testing" + + "github.com/charmbracelet/x/ansi" ) func TestParseSequence_Events(t *testing.T) { - input := []byte("\x1b\x1b[Ztest\x00\x1b]10;rgb:1234/1234/1234\x07\x1b[27;2;27~\x1b[?1049;2$y") + input := []byte("\x1b\x1b[Ztest\x00\x1b]10;rgb:1234/1234/1234\x07\x1b[27;2;27~\x1b[?1049;2$y\x1b[4;1$y") want := []Event{ - KeyPressEvent{Sym: KeyTab, Mod: ModShift | ModAlt}, - KeyPressEvent{Rune: 't'}, - KeyPressEvent{Rune: 'e'}, - KeyPressEvent{Rune: 's'}, - KeyPressEvent{Rune: 't'}, - KeyPressEvent{Rune: ' ', Sym: KeySpace, Mod: ModCtrl}, + KeyPressEvent{Code: KeyTab, Mod: ModShift | ModAlt}, + KeyPressEvent{Code: 't', Text: "t"}, + KeyPressEvent{Code: 'e', Text: "e"}, + KeyPressEvent{Code: 's', Text: "s"}, + KeyPressEvent{Code: 't', Text: "t"}, + KeyPressEvent{Code: KeySpace, Mod: ModCtrl}, ForegroundColorEvent{color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff}}, - KeyPressEvent{Sym: KeyEscape, Mod: ModShift}, - ReportModeEvent{Mode: 1049, Value: 2}, + KeyPressEvent{Code: KeyEscape, Mod: ModShift}, + ModeReportEvent{Mode: ansi.AltScreenSaveCursorMode, Value: ansi.ModeReset}, + ModeReportEvent{Mode: ansi.InsertReplaceMode, Value: ansi.ModeSet}, } + + var p Parser for i := 0; len(input) != 0; i++ { if i >= len(want) { t.Fatalf("reached end of want events") } - n, got := ParseSequence(input) - if got != want[i] { - t.Errorf("got %v (%T), want %v (%T)", got, got, want[i], want[i]) + n, got := p.parseSequence(input) + if !reflect.DeepEqual(got, want[i]) { + t.Errorf("got %#v (%T), want %#v (%T)", got, got, want[i], want[i]) } input = input[n:] } } func BenchmarkParseSequence(b *testing.B) { + var p Parser input := []byte("\x1b\x1b[Ztest\x00\x1b]10;1234/1234/1234\x07\x1b[27;2;27~") b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - ParseSequence(input) + p.parseSequence(input) } } diff --git a/input/paste.go b/input/paste.go index 079d3cb2..455597f8 100644 --- a/input/paste.go +++ b/input/paste.go @@ -1,12 +1,13 @@ package input -// PasteEvent is an event that is emitted when a terminal receives pasted text +// PasteEvent is an message that is emitted when a terminal receives pasted text // using bracketed-paste. type PasteEvent string -// PasteStartEvent is an event that is emitted when a terminal enters -// bracketed-paste mode. +// PasteStartEvent is an message that is emitted when the terminal starts the +// bracketed-paste text type PasteStartEvent struct{} -// PasteEvent is an event that is emitted when a terminal receives pasted text. +// PasteEndEvent is an message that is emitted when the terminal ends the +// bracketed-paste text. type PasteEndEvent struct{} diff --git a/input/seq.go b/input/seq.go deleted file mode 100644 index 460e12b8..00000000 --- a/input/seq.go +++ /dev/null @@ -1,45 +0,0 @@ -package input - -import ( - "fmt" -) - -// UnknownCsiEvent represents an unknown CSI sequence event. -type UnknownCsiEvent string - -// String implements fmt.Stringer. -func (e UnknownCsiEvent) String() string { - return fmt.Sprintf("%q", string(e)) -} - -// UnknownOscEvent represents an unknown OSC sequence event. -type UnknownOscEvent string - -// String implements fmt.Stringer. -func (e UnknownOscEvent) String() string { - return fmt.Sprintf("%q", string(e)) -} - -// UnknownDcsEvent represents an unknown DCS sequence event. -type UnknownDcsEvent string - -// String implements fmt.Stringer. -func (e UnknownDcsEvent) String() string { - return fmt.Sprintf("%q", string(e)) -} - -// UnknownApcEvent represents an unknown APC sequence event. -type UnknownApcEvent string - -// String implements fmt.Stringer. -func (e UnknownApcEvent) String() string { - return fmt.Sprintf("%q", string(e)) -} - -// UnknownSs3Event represents an unknown SS3 sequence event. -type UnknownSs3Event string - -// String implements fmt.Stringer. -func (e UnknownSs3Event) String() string { - return fmt.Sprintf("%q", string(e)) -} diff --git a/input/table.go b/input/table.go index 022d1adc..ab8cb5fd 100644 --- a/input/table.go +++ b/input/table.go @@ -6,40 +6,43 @@ import ( "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{Rune: ' ', Sym: KeySpace, Mod: ModCtrl} // ctrl+@ or ctrl+space + nul := Key{Code: KeySpace, Mod: ModCtrl} // ctrl+@ or ctrl+space if flags&FlagCtrlAt != 0 { - nul = Key{Rune: '@', Mod: ModCtrl} + nul = Key{Code: '@', Mod: ModCtrl} } - tab := Key{Sym: KeyTab} // ctrl+i or tab + tab := Key{Code: KeyTab} // ctrl+i or tab if flags&FlagCtrlI != 0 { - tab = Key{Rune: 'i', Mod: ModCtrl} + tab = Key{Code: 'i', Mod: ModCtrl} } - enter := Key{Sym: KeyEnter} // ctrl+m or enter + enter := Key{Code: KeyEnter} // ctrl+m or enter if flags&FlagCtrlM != 0 { - enter = Key{Rune: 'm', Mod: ModCtrl} + enter = Key{Code: 'm', Mod: ModCtrl} } - esc := Key{Sym: KeyEscape} // ctrl+[ or escape + esc := Key{Code: KeyEscape} // ctrl+[ or escape if flags&FlagCtrlOpenBracket != 0 { - esc = Key{Rune: '[', Mod: ModCtrl} // ctrl+[ or escape + esc = Key{Code: '[', Mod: ModCtrl} // ctrl+[ or escape } - del := Key{Sym: KeyBackspace} + del := Key{Code: KeyBackspace} if flags&FlagBackspace != 0 { - del.Sym = KeyDelete + del.Code = KeyDelete } - find := Key{Sym: KeyHome} + find := Key{Code: KeyHome} if flags&FlagFind != 0 { - find.Sym = KeyFind + find.Code = KeyFind } - sel := Key{Sym: KeyEnd} + sel := Key{Code: KeyEnd} if flags&FlagSelect != 0 { - sel.Sym = KeySelect + sel.Code = KeySelect } // The following is a table of key sequences and their corresponding key @@ -53,155 +56,155 @@ func buildKeysTable(flags int, term string) map[string]Key { table := map[string]Key{ // C0 control characters string(byte(ansi.NUL)): nul, - string(byte(ansi.SOH)): {Rune: 'a', Mod: ModCtrl}, - string(byte(ansi.STX)): {Rune: 'b', Mod: ModCtrl}, - string(byte(ansi.ETX)): {Rune: 'c', Mod: ModCtrl}, - string(byte(ansi.EOT)): {Rune: 'd', Mod: ModCtrl}, - string(byte(ansi.ENQ)): {Rune: 'e', Mod: ModCtrl}, - string(byte(ansi.ACK)): {Rune: 'f', Mod: ModCtrl}, - string(byte(ansi.BEL)): {Rune: 'g', Mod: ModCtrl}, - string(byte(ansi.BS)): {Rune: 'h', Mod: ModCtrl}, + string(byte(ansi.SOH)): {Code: 'a', Mod: ModCtrl}, + string(byte(ansi.STX)): {Code: 'b', Mod: ModCtrl}, + string(byte(ansi.ETX)): {Code: 'c', Mod: ModCtrl}, + string(byte(ansi.EOT)): {Code: 'd', Mod: ModCtrl}, + string(byte(ansi.ENQ)): {Code: 'e', Mod: ModCtrl}, + string(byte(ansi.ACK)): {Code: 'f', Mod: ModCtrl}, + string(byte(ansi.BEL)): {Code: 'g', Mod: ModCtrl}, + string(byte(ansi.BS)): {Code: 'h', Mod: ModCtrl}, string(byte(ansi.HT)): tab, - string(byte(ansi.LF)): {Rune: 'j', Mod: ModCtrl}, - string(byte(ansi.VT)): {Rune: 'k', Mod: ModCtrl}, - string(byte(ansi.FF)): {Rune: 'l', Mod: ModCtrl}, + string(byte(ansi.LF)): {Code: 'j', Mod: ModCtrl}, + string(byte(ansi.VT)): {Code: 'k', Mod: ModCtrl}, + string(byte(ansi.FF)): {Code: 'l', Mod: ModCtrl}, string(byte(ansi.CR)): enter, - string(byte(ansi.SO)): {Rune: 'n', Mod: ModCtrl}, - string(byte(ansi.SI)): {Rune: 'o', Mod: ModCtrl}, - string(byte(ansi.DLE)): {Rune: 'p', Mod: ModCtrl}, - string(byte(ansi.DC1)): {Rune: 'q', Mod: ModCtrl}, - string(byte(ansi.DC2)): {Rune: 'r', Mod: ModCtrl}, - string(byte(ansi.DC3)): {Rune: 's', Mod: ModCtrl}, - string(byte(ansi.DC4)): {Rune: 't', Mod: ModCtrl}, - string(byte(ansi.NAK)): {Rune: 'u', Mod: ModCtrl}, - string(byte(ansi.SYN)): {Rune: 'v', Mod: ModCtrl}, - string(byte(ansi.ETB)): {Rune: 'w', Mod: ModCtrl}, - string(byte(ansi.CAN)): {Rune: 'x', Mod: ModCtrl}, - string(byte(ansi.EM)): {Rune: 'y', Mod: ModCtrl}, - string(byte(ansi.SUB)): {Rune: 'z', Mod: ModCtrl}, + string(byte(ansi.SO)): {Code: 'n', Mod: ModCtrl}, + string(byte(ansi.SI)): {Code: 'o', Mod: ModCtrl}, + string(byte(ansi.DLE)): {Code: 'p', Mod: ModCtrl}, + string(byte(ansi.DC1)): {Code: 'q', Mod: ModCtrl}, + string(byte(ansi.DC2)): {Code: 'r', Mod: ModCtrl}, + string(byte(ansi.DC3)): {Code: 's', Mod: ModCtrl}, + string(byte(ansi.DC4)): {Code: 't', Mod: ModCtrl}, + string(byte(ansi.NAK)): {Code: 'u', Mod: ModCtrl}, + string(byte(ansi.SYN)): {Code: 'v', Mod: ModCtrl}, + string(byte(ansi.ETB)): {Code: 'w', Mod: ModCtrl}, + string(byte(ansi.CAN)): {Code: 'x', Mod: ModCtrl}, + string(byte(ansi.EM)): {Code: 'y', Mod: ModCtrl}, + string(byte(ansi.SUB)): {Code: 'z', Mod: ModCtrl}, string(byte(ansi.ESC)): esc, - string(byte(ansi.FS)): {Rune: '\\', Mod: ModCtrl}, - string(byte(ansi.GS)): {Rune: ']', Mod: ModCtrl}, - string(byte(ansi.RS)): {Rune: '^', Mod: ModCtrl}, - string(byte(ansi.US)): {Rune: '_', Mod: ModCtrl}, + string(byte(ansi.FS)): {Code: '\\', Mod: ModCtrl}, + string(byte(ansi.GS)): {Code: ']', Mod: ModCtrl}, + string(byte(ansi.RS)): {Code: '^', Mod: ModCtrl}, + string(byte(ansi.US)): {Code: '_', Mod: ModCtrl}, // Special keys in G0 - string(byte(ansi.SP)): {Sym: KeySpace, Rune: ' '}, + string(byte(ansi.SP)): {Code: KeySpace, Text: " "}, string(byte(ansi.DEL)): del, // Special keys - "\x1b[Z": {Sym: KeyTab, Mod: ModShift}, + "\x1b[Z": {Code: KeyTab, Mod: ModShift}, "\x1b[1~": find, - "\x1b[2~": {Sym: KeyInsert}, - "\x1b[3~": {Sym: KeyDelete}, + "\x1b[2~": {Code: KeyInsert}, + "\x1b[3~": {Code: KeyDelete}, "\x1b[4~": sel, - "\x1b[5~": {Sym: KeyPgUp}, - "\x1b[6~": {Sym: KeyPgDown}, - "\x1b[7~": {Sym: KeyHome}, - "\x1b[8~": {Sym: KeyEnd}, + "\x1b[5~": {Code: KeyPgUp}, + "\x1b[6~": {Code: KeyPgDown}, + "\x1b[7~": {Code: KeyHome}, + "\x1b[8~": {Code: 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": {Code: KeyUp}, + "\x1b[B": {Code: KeyDown}, + "\x1b[C": {Code: KeyRight}, + "\x1b[D": {Code: KeyLeft}, + "\x1b[E": {Code: KeyBegin}, + "\x1b[F": {Code: KeyEnd}, + "\x1b[H": {Code: KeyHome}, + "\x1b[P": {Code: KeyF1}, + "\x1b[Q": {Code: KeyF2}, + "\x1b[R": {Code: KeyF3}, + "\x1b[S": {Code: KeyF4}, // Application Cursor Key Mode (DECCKM) - "\x1bOA": {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": {Code: KeyUp}, + "\x1bOB": {Code: KeyDown}, + "\x1bOC": {Code: KeyRight}, + "\x1bOD": {Code: KeyLeft}, + "\x1bOE": {Code: KeyBegin}, + "\x1bOF": {Code: KeyEnd}, + "\x1bOH": {Code: KeyHome}, + "\x1bOP": {Code: KeyF1}, + "\x1bOQ": {Code: KeyF2}, + "\x1bOR": {Code: KeyF3}, + "\x1bOS": {Code: KeyF4}, // Keypad Application Mode (DECKPAM) - "\x1bOM": {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": {Code: KeyKpEnter}, + "\x1bOX": {Code: KeyKpEqual}, + "\x1bOj": {Code: KeyKpMultiply}, + "\x1bOk": {Code: KeyKpPlus}, + "\x1bOl": {Code: KeyKpComma}, + "\x1bOm": {Code: KeyKpMinus}, + "\x1bOn": {Code: KeyKpDecimal}, + "\x1bOo": {Code: KeyKpDivide}, + "\x1bOp": {Code: KeyKp0}, + "\x1bOq": {Code: KeyKp1}, + "\x1bOr": {Code: KeyKp2}, + "\x1bOs": {Code: KeyKp3}, + "\x1bOt": {Code: KeyKp4}, + "\x1bOu": {Code: KeyKp5}, + "\x1bOv": {Code: KeyKp6}, + "\x1bOw": {Code: KeyKp7}, + "\x1bOx": {Code: KeyKp8}, + "\x1bOy": {Code: KeyKp9}, // Function keys - "\x1b[11~": {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~": {Code: KeyF1}, + "\x1b[12~": {Code: KeyF2}, + "\x1b[13~": {Code: KeyF3}, + "\x1b[14~": {Code: KeyF4}, + "\x1b[15~": {Code: KeyF5}, + "\x1b[17~": {Code: KeyF6}, + "\x1b[18~": {Code: KeyF7}, + "\x1b[19~": {Code: KeyF8}, + "\x1b[20~": {Code: KeyF9}, + "\x1b[21~": {Code: KeyF10}, + "\x1b[23~": {Code: KeyF11}, + "\x1b[24~": {Code: KeyF12}, + "\x1b[25~": {Code: KeyF13}, + "\x1b[26~": {Code: KeyF14}, + "\x1b[28~": {Code: KeyF15}, + "\x1b[29~": {Code: KeyF16}, + "\x1b[31~": {Code: KeyF17}, + "\x1b[32~": {Code: KeyF18}, + "\x1b[33~": {Code: KeyF19}, + "\x1b[34~": {Code: KeyF20}, } // CSI ~ sequence keys csiTildeKeys := map[string]Key{ - "1": find, "2": {Sym: KeyInsert}, - "3": {Sym: KeyDelete}, "4": sel, - "5": {Sym: KeyPgUp}, "6": {Sym: KeyPgDown}, - "7": {Sym: KeyHome}, "8": {Sym: KeyEnd}, + "1": find, "2": {Code: KeyInsert}, + "3": {Code: KeyDelete}, "4": sel, + "5": {Code: KeyPgUp}, "6": {Code: KeyPgDown}, + "7": {Code: KeyHome}, "8": {Code: KeyEnd}, // There are no 9 and 10 keys - "11": {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": {Code: KeyF1}, "12": {Code: KeyF2}, + "13": {Code: KeyF3}, "14": {Code: KeyF4}, + "15": {Code: KeyF5}, "17": {Code: KeyF6}, + "18": {Code: KeyF7}, "19": {Code: KeyF8}, + "20": {Code: KeyF9}, "21": {Code: KeyF10}, + "23": {Code: KeyF11}, "24": {Code: KeyF12}, + "25": {Code: KeyF13}, "26": {Code: KeyF14}, + "28": {Code: KeyF15}, "29": {Code: KeyF16}, + "31": {Code: KeyF17}, "32": {Code: KeyF18}, + "33": {Code: KeyF19}, "34": {Code: KeyF20}, } // URxvt keys // See https://manpages.ubuntu.com/manpages/trusty/man7/urxvt.7.html#key%20codes - table["\x1b[a"] = Key{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{Code: KeyUp, Mod: ModShift} + table["\x1b[b"] = Key{Code: KeyDown, Mod: ModShift} + table["\x1b[c"] = Key{Code: KeyRight, Mod: ModShift} + table["\x1b[d"] = Key{Code: KeyLeft, Mod: ModShift} + table["\x1bOa"] = Key{Code: KeyUp, Mod: ModCtrl} + table["\x1bOb"] = Key{Code: KeyDown, Mod: ModCtrl} + table["\x1bOc"] = Key{Code: KeyRight, Mod: ModCtrl} + table["\x1bOd"] = Key{Code: KeyLeft, Mod: ModCtrl} // TODO: invistigate if shift-ctrl arrow keys collide with DECCKM keys i.e. // "\x1bOA", "\x1bOB", "\x1bOC", "\x1bOD" @@ -229,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{Code: KeyF11, Mod: ModShift} + table["\x1b[24$"] = Key{Code: KeyF12, Mod: ModShift} + table["\x1b[25$"] = Key{Code: KeyF13, Mod: ModShift} + table["\x1b[26$"] = Key{Code: KeyF14, Mod: ModShift} + table["\x1b[28$"] = Key{Code: KeyF15, Mod: ModShift} + table["\x1b[29$"] = Key{Code: KeyF16, Mod: ModShift} + table["\x1b[31$"] = Key{Code: KeyF17, Mod: ModShift} + table["\x1b[32$"] = Key{Code: KeyF18, Mod: ModShift} + table["\x1b[33$"] = Key{Code: KeyF19, Mod: ModShift} + table["\x1b[34$"] = Key{Code: KeyF20, Mod: ModShift} + table["\x1b[11^"] = Key{Code: KeyF1, Mod: ModCtrl} + table["\x1b[12^"] = Key{Code: KeyF2, Mod: ModCtrl} + table["\x1b[13^"] = Key{Code: KeyF3, Mod: ModCtrl} + table["\x1b[14^"] = Key{Code: KeyF4, Mod: ModCtrl} + table["\x1b[15^"] = Key{Code: KeyF5, Mod: ModCtrl} + table["\x1b[17^"] = Key{Code: KeyF6, Mod: ModCtrl} + table["\x1b[18^"] = Key{Code: KeyF7, Mod: ModCtrl} + table["\x1b[19^"] = Key{Code: KeyF8, Mod: ModCtrl} + table["\x1b[20^"] = Key{Code: KeyF9, Mod: ModCtrl} + table["\x1b[21^"] = Key{Code: KeyF10, Mod: ModCtrl} + table["\x1b[23^"] = Key{Code: KeyF11, Mod: ModCtrl} + table["\x1b[24^"] = Key{Code: KeyF12, Mod: ModCtrl} + table["\x1b[25^"] = Key{Code: KeyF13, Mod: ModCtrl} + table["\x1b[26^"] = Key{Code: KeyF14, Mod: ModCtrl} + table["\x1b[28^"] = Key{Code: KeyF15, Mod: ModCtrl} + table["\x1b[29^"] = Key{Code: KeyF16, Mod: ModCtrl} + table["\x1b[31^"] = Key{Code: KeyF17, Mod: ModCtrl} + table["\x1b[32^"] = Key{Code: KeyF18, Mod: ModCtrl} + table["\x1b[33^"] = Key{Code: KeyF19, Mod: ModCtrl} + table["\x1b[34^"] = Key{Code: KeyF20, Mod: ModCtrl} + table["\x1b[23@"] = Key{Code: KeyF11, Mod: ModShift | ModCtrl} + table["\x1b[24@"] = Key{Code: KeyF12, Mod: ModShift | ModCtrl} + table["\x1b[25@"] = Key{Code: KeyF13, Mod: ModShift | ModCtrl} + table["\x1b[26@"] = Key{Code: KeyF14, Mod: ModShift | ModCtrl} + table["\x1b[28@"] = Key{Code: KeyF15, Mod: ModShift | ModCtrl} + table["\x1b[29@"] = Key{Code: KeyF16, Mod: ModShift | ModCtrl} + table["\x1b[31@"] = Key{Code: KeyF17, Mod: ModShift | ModCtrl} + table["\x1b[32@"] = Key{Code: KeyF18, Mod: ModShift | ModCtrl} + table["\x1b[33@"] = Key{Code: KeyF19, Mod: ModShift | ModCtrl} + table["\x1b[34@"] = Key{Code: KeyF20, Mod: ModShift | ModCtrl} // Register Alt + combinations // XXX: this must come after URxvt but before XTerm keys to register URxvt @@ -277,6 +280,7 @@ func buildKeysTable(flags int, term string) map[string]Key { for seq, key := range table { key := key key.Mod |= ModAlt + key.Text = "" // Clear runes tmap["\x1b"+seq] = key } for seq, key := range tmap { @@ -309,34 +313,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": {Code: KeyKpEnter}, "X": {Code: KeyKpEqual}, + "j": {Code: KeyKpMultiply}, "k": {Code: KeyKpPlus}, + "l": {Code: KeyKpComma}, "m": {Code: KeyKpMinus}, + "n": {Code: KeyKpDecimal}, "o": {Code: KeyKpDivide}, + "p": {Code: KeyKp0}, "q": {Code: KeyKp1}, + "r": {Code: KeyKp2}, "s": {Code: KeyKp3}, + "t": {Code: KeyKp4}, "u": {Code: KeyKp5}, + "v": {Code: KeyKp6}, "w": {Code: KeyKp7}, + "x": {Code: KeyKp8}, "y": {Code: KeyKp9}, } // XTerm keys csiFuncKeys := map[string]Key{ - "A": {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": {Code: KeyUp}, "B": {Code: KeyDown}, + "C": {Code: KeyRight}, "D": {Code: KeyLeft}, + "E": {Code: KeyBegin}, "F": {Code: KeyEnd}, + "H": {Code: KeyHome}, "P": {Code: KeyF1}, + "Q": {Code: KeyF2}, "R": {Code: KeyF3}, + "S": {Code: KeyF4}, } // CSI 27 ; ; ~ 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: {Code: KeyBackspace}, + ansi.HT: {Code: KeyTab}, + ansi.CR: {Code: KeyEnter}, + ansi.ESC: {Code: KeyEscape}, + ansi.DEL: {Code: KeyBackspace}, } for _, m := range modifiers { diff --git a/input/termcap.go b/input/termcap.go index 82045b1d..3502189f 100644 --- a/input/termcap.go +++ b/input/termcap.go @@ -3,30 +3,28 @@ package input import ( "bytes" "encoding/hex" + "strings" ) -// TermcapEvent represents a Termcap response event. Termcap responses are -// generated by the terminal in response to RequestTermcap (XTGETTCAP) -// requests. +// CapabilityEvent represents a Termcap/Terminfo response event. Termcap +// responses are generated by the terminal in response to RequestTermcap +// (XTGETTCAP) requests. // // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands -type TermcapEvent struct { - Values map[string]string - IsValid bool -} +type CapabilityEvent string -func parseTermcap(data []byte) TermcapEvent { +func parseTermcap(data []byte) CapabilityEvent { // XTGETTCAP if len(data) == 0 { - return TermcapEvent{} + return CapabilityEvent("") } - tc := TermcapEvent{Values: make(map[string]string)} + var tc strings.Builder split := bytes.Split(data, []byte{';'}) for _, s := range split { parts := bytes.SplitN(s, []byte{'='}, 2) if len(parts) == 0 { - return TermcapEvent{} + return CapabilityEvent("") } name, err := hex.DecodeString(string(parts[0])) @@ -42,8 +40,15 @@ func parseTermcap(data []byte) TermcapEvent { } } - tc.Values[string(name)] = string(value) + if tc.Len() > 0 { + tc.WriteByte(';') + } + tc.WriteString(string(name)) + if len(value) > 0 { + tc.WriteByte('=') + tc.WriteString(string(value)) + } } - return tc + return CapabilityEvent(tc.String()) } diff --git a/input/terminfo.go b/input/terminfo.go index d5a702af..a54da2c3 100644 --- a/input/terminfo.go +++ b/input/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": {Code: KeyUp}, + "kUP": {Code: KeyUp, Mod: ModShift}, + "kUP3": {Code: KeyUp, Mod: ModAlt}, + "kUP4": {Code: KeyUp, Mod: ModShift | ModAlt}, + "kUP5": {Code: KeyUp, Mod: ModCtrl}, + "kUP6": {Code: KeyUp, Mod: ModShift | ModCtrl}, + "kUP7": {Code: KeyUp, Mod: ModAlt | ModCtrl}, + "kUP8": {Code: KeyUp, Mod: ModShift | ModAlt | ModCtrl}, + "kcud1": {Code: KeyDown}, + "kDN": {Code: KeyDown, Mod: ModShift}, + "kDN3": {Code: KeyDown, Mod: ModAlt}, + "kDN4": {Code: KeyDown, Mod: ModShift | ModAlt}, + "kDN5": {Code: KeyDown, Mod: ModCtrl}, + "kDN7": {Code: KeyDown, Mod: ModAlt | ModCtrl}, + "kDN6": {Code: KeyDown, Mod: ModShift | ModCtrl}, + "kDN8": {Code: KeyDown, Mod: ModShift | ModAlt | ModCtrl}, + "kcub1": {Code: KeyLeft}, + "kLFT": {Code: KeyLeft, Mod: ModShift}, + "kLFT3": {Code: KeyLeft, Mod: ModAlt}, + "kLFT4": {Code: KeyLeft, Mod: ModShift | ModAlt}, + "kLFT5": {Code: KeyLeft, Mod: ModCtrl}, + "kLFT6": {Code: KeyLeft, Mod: ModShift | ModCtrl}, + "kLFT7": {Code: KeyLeft, Mod: ModAlt | ModCtrl}, + "kLFT8": {Code: KeyLeft, Mod: ModShift | ModAlt | ModCtrl}, + "kcuf1": {Code: KeyRight}, + "kRIT": {Code: KeyRight, Mod: ModShift}, + "kRIT3": {Code: KeyRight, Mod: ModAlt}, + "kRIT4": {Code: KeyRight, Mod: ModShift | ModAlt}, + "kRIT5": {Code: KeyRight, Mod: ModCtrl}, + "kRIT6": {Code: KeyRight, Mod: ModShift | ModCtrl}, + "kRIT7": {Code: KeyRight, Mod: ModAlt | ModCtrl}, + "kRIT8": {Code: KeyRight, Mod: ModShift | ModAlt | ModCtrl}, + "kich1": {Code: KeyInsert}, + "kIC": {Code: KeyInsert, Mod: ModShift}, + "kIC3": {Code: KeyInsert, Mod: ModAlt}, + "kIC4": {Code: KeyInsert, Mod: ModShift | ModAlt}, + "kIC5": {Code: KeyInsert, Mod: ModCtrl}, + "kIC6": {Code: KeyInsert, Mod: ModShift | ModCtrl}, + "kIC7": {Code: KeyInsert, Mod: ModAlt | ModCtrl}, + "kIC8": {Code: KeyInsert, Mod: ModShift | ModAlt | ModCtrl}, + "kdch1": {Code: KeyDelete}, + "kDC": {Code: KeyDelete, Mod: ModShift}, + "kDC3": {Code: KeyDelete, Mod: ModAlt}, + "kDC4": {Code: KeyDelete, Mod: ModShift | ModAlt}, + "kDC5": {Code: KeyDelete, Mod: ModCtrl}, + "kDC6": {Code: KeyDelete, Mod: ModShift | ModCtrl}, + "kDC7": {Code: KeyDelete, Mod: ModAlt | ModCtrl}, + "kDC8": {Code: KeyDelete, Mod: ModShift | ModAlt | ModCtrl}, + "khome": {Code: KeyHome}, + "kHOM": {Code: KeyHome, Mod: ModShift}, + "kHOM3": {Code: KeyHome, Mod: ModAlt}, + "kHOM4": {Code: KeyHome, Mod: ModShift | ModAlt}, + "kHOM5": {Code: KeyHome, Mod: ModCtrl}, + "kHOM6": {Code: KeyHome, Mod: ModShift | ModCtrl}, + "kHOM7": {Code: KeyHome, Mod: ModAlt | ModCtrl}, + "kHOM8": {Code: KeyHome, Mod: ModShift | ModAlt | ModCtrl}, + "kend": {Code: KeyEnd}, + "kEND": {Code: KeyEnd, Mod: ModShift}, + "kEND3": {Code: KeyEnd, Mod: ModAlt}, + "kEND4": {Code: KeyEnd, Mod: ModShift | ModAlt}, + "kEND5": {Code: KeyEnd, Mod: ModCtrl}, + "kEND6": {Code: KeyEnd, Mod: ModShift | ModCtrl}, + "kEND7": {Code: KeyEnd, Mod: ModAlt | ModCtrl}, + "kEND8": {Code: KeyEnd, Mod: ModShift | ModAlt | ModCtrl}, + "kpp": {Code: KeyPgUp}, + "kprv": {Code: KeyPgUp}, + "kPRV": {Code: KeyPgUp, Mod: ModShift}, + "kPRV3": {Code: KeyPgUp, Mod: ModAlt}, + "kPRV4": {Code: KeyPgUp, Mod: ModShift | ModAlt}, + "kPRV5": {Code: KeyPgUp, Mod: ModCtrl}, + "kPRV6": {Code: KeyPgUp, Mod: ModShift | ModCtrl}, + "kPRV7": {Code: KeyPgUp, Mod: ModAlt | ModCtrl}, + "kPRV8": {Code: KeyPgUp, Mod: ModShift | ModAlt | ModCtrl}, + "knp": {Code: KeyPgDown}, + "knxt": {Code: KeyPgDown}, + "kNXT": {Code: KeyPgDown, Mod: ModShift}, + "kNXT3": {Code: KeyPgDown, Mod: ModAlt}, + "kNXT4": {Code: KeyPgDown, Mod: ModShift | ModAlt}, + "kNXT5": {Code: KeyPgDown, Mod: ModCtrl}, + "kNXT6": {Code: KeyPgDown, Mod: ModShift | ModCtrl}, + "kNXT7": {Code: KeyPgDown, Mod: ModAlt | ModCtrl}, + "kNXT8": {Code: KeyPgDown, Mod: ModShift | ModAlt | ModCtrl}, - "kbs": {Sym: KeyBackspace}, - "kcbt": {Sym: KeyTab, Mod: ModShift}, + "kbs": {Code: KeyBackspace}, + "kcbt": {Code: 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": {Code: KeyF1}, + "kf2": {Code: KeyF2}, + "kf3": {Code: KeyF3}, + "kf4": {Code: KeyF4}, + "kf5": {Code: KeyF5}, + "kf6": {Code: KeyF6}, + "kf7": {Code: KeyF7}, + "kf8": {Code: KeyF8}, + "kf9": {Code: KeyF9}, + "kf10": {Code: KeyF10}, + "kf11": {Code: KeyF11}, + "kf12": {Code: KeyF12}, + "kf13": {Code: KeyF1, Mod: ModShift}, + "kf14": {Code: KeyF2, Mod: ModShift}, + "kf15": {Code: KeyF3, Mod: ModShift}, + "kf16": {Code: KeyF4, Mod: ModShift}, + "kf17": {Code: KeyF5, Mod: ModShift}, + "kf18": {Code: KeyF6, Mod: ModShift}, + "kf19": {Code: KeyF7, Mod: ModShift}, + "kf20": {Code: KeyF8, Mod: ModShift}, + "kf21": {Code: KeyF9, Mod: ModShift}, + "kf22": {Code: KeyF10, Mod: ModShift}, + "kf23": {Code: KeyF11, Mod: ModShift}, + "kf24": {Code: KeyF12, Mod: ModShift}, + "kf25": {Code: KeyF1, Mod: ModCtrl}, + "kf26": {Code: KeyF2, Mod: ModCtrl}, + "kf27": {Code: KeyF3, Mod: ModCtrl}, + "kf28": {Code: KeyF4, Mod: ModCtrl}, + "kf29": {Code: KeyF5, Mod: ModCtrl}, + "kf30": {Code: KeyF6, Mod: ModCtrl}, + "kf31": {Code: KeyF7, Mod: ModCtrl}, + "kf32": {Code: KeyF8, Mod: ModCtrl}, + "kf33": {Code: KeyF9, Mod: ModCtrl}, + "kf34": {Code: KeyF10, Mod: ModCtrl}, + "kf35": {Code: KeyF11, Mod: ModCtrl}, + "kf36": {Code: KeyF12, Mod: ModCtrl}, + "kf37": {Code: KeyF1, Mod: ModShift | ModCtrl}, + "kf38": {Code: KeyF2, Mod: ModShift | ModCtrl}, + "kf39": {Code: KeyF3, Mod: ModShift | ModCtrl}, + "kf40": {Code: KeyF4, Mod: ModShift | ModCtrl}, + "kf41": {Code: KeyF5, Mod: ModShift | ModCtrl}, + "kf42": {Code: KeyF6, Mod: ModShift | ModCtrl}, + "kf43": {Code: KeyF7, Mod: ModShift | ModCtrl}, + "kf44": {Code: KeyF8, Mod: ModShift | ModCtrl}, + "kf45": {Code: KeyF9, Mod: ModShift | ModCtrl}, + "kf46": {Code: KeyF10, Mod: ModShift | ModCtrl}, + "kf47": {Code: KeyF11, Mod: ModShift | ModCtrl}, + "kf48": {Code: KeyF12, Mod: ModShift | ModCtrl}, + "kf49": {Code: KeyF1, Mod: ModAlt}, + "kf50": {Code: KeyF2, Mod: ModAlt}, + "kf51": {Code: KeyF3, Mod: ModAlt}, + "kf52": {Code: KeyF4, Mod: ModAlt}, + "kf53": {Code: KeyF5, Mod: ModAlt}, + "kf54": {Code: KeyF6, Mod: ModAlt}, + "kf55": {Code: KeyF7, Mod: ModAlt}, + "kf56": {Code: KeyF8, Mod: ModAlt}, + "kf57": {Code: KeyF9, Mod: ModAlt}, + "kf58": {Code: KeyF10, Mod: ModAlt}, + "kf59": {Code: KeyF11, Mod: ModAlt}, + "kf60": {Code: KeyF12, Mod: ModAlt}, + "kf61": {Code: KeyF1, Mod: ModShift | ModAlt}, + "kf62": {Code: KeyF2, Mod: ModShift | ModAlt}, + "kf63": {Code: KeyF3, Mod: ModShift | ModAlt}, } // Preserve F keys from F13 to F63 instead of using them for F-keys // modifiers. if flags&FlagFKeys != 0 { - keys["kf13"] = Key{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{Code: KeyF13} + keys["kf14"] = Key{Code: KeyF14} + keys["kf15"] = Key{Code: KeyF15} + keys["kf16"] = Key{Code: KeyF16} + keys["kf17"] = Key{Code: KeyF17} + keys["kf18"] = Key{Code: KeyF18} + keys["kf19"] = Key{Code: KeyF19} + keys["kf20"] = Key{Code: KeyF20} + keys["kf21"] = Key{Code: KeyF21} + keys["kf22"] = Key{Code: KeyF22} + keys["kf23"] = Key{Code: KeyF23} + keys["kf24"] = Key{Code: KeyF24} + keys["kf25"] = Key{Code: KeyF25} + keys["kf26"] = Key{Code: KeyF26} + keys["kf27"] = Key{Code: KeyF27} + keys["kf28"] = Key{Code: KeyF28} + keys["kf29"] = Key{Code: KeyF29} + keys["kf30"] = Key{Code: KeyF30} + keys["kf31"] = Key{Code: KeyF31} + keys["kf32"] = Key{Code: KeyF32} + keys["kf33"] = Key{Code: KeyF33} + keys["kf34"] = Key{Code: KeyF34} + keys["kf35"] = Key{Code: KeyF35} + keys["kf36"] = Key{Code: KeyF36} + keys["kf37"] = Key{Code: KeyF37} + keys["kf38"] = Key{Code: KeyF38} + keys["kf39"] = Key{Code: KeyF39} + keys["kf40"] = Key{Code: KeyF40} + keys["kf41"] = Key{Code: KeyF41} + keys["kf42"] = Key{Code: KeyF42} + keys["kf43"] = Key{Code: KeyF43} + keys["kf44"] = Key{Code: KeyF44} + keys["kf45"] = Key{Code: KeyF45} + keys["kf46"] = Key{Code: KeyF46} + keys["kf47"] = Key{Code: KeyF47} + keys["kf48"] = Key{Code: KeyF48} + keys["kf49"] = Key{Code: KeyF49} + keys["kf50"] = Key{Code: KeyF50} + keys["kf51"] = Key{Code: KeyF51} + keys["kf52"] = Key{Code: KeyF52} + keys["kf53"] = Key{Code: KeyF53} + keys["kf54"] = Key{Code: KeyF54} + keys["kf55"] = Key{Code: KeyF55} + keys["kf56"] = Key{Code: KeyF56} + keys["kf57"] = Key{Code: KeyF57} + keys["kf58"] = Key{Code: KeyF58} + keys["kf59"] = Key{Code: KeyF59} + keys["kf60"] = Key{Code: KeyF60} + keys["kf61"] = Key{Code: KeyF61} + keys["kf62"] = Key{Code: KeyF62} + keys["kf63"] = Key{Code: KeyF63} } return keys diff --git a/input/win32input.go b/input/win32input.go deleted file mode 100644 index a8d5f4b2..00000000 --- a/input/win32input.go +++ /dev/null @@ -1,240 +0,0 @@ -package input - -import ( - "unicode" - - "github.com/erikgeiser/coninput" -) - -func parseWin32InputKeyEvent(vkc coninput.VirtualKeyCode, _ coninput.VirtualKeyCode, r rune, keyDown bool, cks coninput.ControlKeyState, repeatCount uint16) Event { - 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{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 Event = KeyPressEvent(key) - key.IsRepeat = repeatCount > 1 - if !keyDown { - e = KeyReleaseEvent(key) - } - - if repeatCount <= 1 { - return e - } - - var kevents []Event - for i := 0; i < int(repeatCount); i++ { - kevents = append(kevents, e) - } - - return MultiEvent(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, 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: {Rune: '['}, - // TODO: add more keys -} - -func vkCtrlRune(k Key, r rune, kc coninput.VirtualKeyCode) Key { - switch r { - case '@': - k.Rune = '@' - case '\x01': - k.Rune = 'a' - case '\x02': - k.Rune = 'b' - case '\x03': - k.Rune = 'c' - case '\x04': - k.Rune = 'd' - case '\x05': - k.Rune = 'e' - case '\x06': - k.Rune = 'f' - case '\a': - k.Rune = 'g' - case '\b': - k.Rune = 'h' - case '\t': - k.Rune = 'i' - case '\n': - k.Rune = 'j' - case '\v': - k.Rune = 'k' - case '\f': - k.Rune = 'l' - case '\r': - k.Rune = 'm' - case '\x0e': - k.Rune = 'n' - case '\x0f': - k.Rune = 'o' - case '\x10': - k.Rune = 'p' - case '\x11': - k.Rune = 'q' - case '\x12': - k.Rune = 'r' - case '\x13': - k.Rune = 's' - case '\x14': - k.Rune = 't' - case '\x15': - k.Rune = 'u' - case '\x16': - k.Rune = 'v' - case '\x17': - k.Rune = 'w' - case '\x18': - k.Rune = 'x' - case '\x19': - k.Rune = 'y' - case '\x1a': - k.Rune = 'z' - case '\x1b': - k.Rune = ']' - case '\x1c': - k.Rune = '\\' - case '\x1f': - k.Rune = '_' - } - - switch kc { - case coninput.VK_OEM_4: - k.Rune = '[' - } - - // https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes - if k.Rune == 0 && - (kc >= 0x30 && kc <= 0x39) || - (kc >= 0x41 && kc <= 0x5a) { - k.Rune = rune(kc) - } - - return k -} diff --git a/input/xterm.go b/input/xterm.go index 3a66a35c..a48bbc92 100644 --- a/input/xterm.go +++ b/input/xterm.go @@ -6,31 +6,36 @@ import ( func parseXTermModifyOtherKeys(csi *ansi.CsiSequence) Event { // XTerm modify other keys starts with ESC [ 27 ; ; ~ - m, _ := csi.Param(1, 0) - mod := KeyMod(m - 1) - p, _ := csi.Param(2, 0) - r := rune(p) + xmod, _ := csi.Param(1, 1) + xrune, _ := csi.Param(2, 1) + mod := KeyMod(xmod - 1) + r := rune(xrune) switch r { case ansi.BS: - return KeyPressEvent{Mod: mod, Sym: KeyBackspace} + return KeyPressEvent{Mod: mod, Code: KeyBackspace} case ansi.HT: - return KeyPressEvent{Mod: mod, Sym: KeyTab} + return KeyPressEvent{Mod: mod, Code: KeyTab} case ansi.CR: - return KeyPressEvent{Mod: mod, Sym: KeyEnter} + return KeyPressEvent{Mod: mod, Code: KeyEnter} case ansi.ESC: - return KeyPressEvent{Mod: mod, Sym: KeyEscape} + return KeyPressEvent{Mod: mod, Code: KeyEscape} case ansi.DEL: - return KeyPressEvent{Mod: mod, Sym: KeyBackspace} + return KeyPressEvent{Mod: mod, Code: KeyBackspace} } // CSI 27 ; ; ~ keys defined in XTerm modifyOtherKeys - return KeyPressEvent{ - Mod: mod, - Rune: r, + k := KeyPressEvent{Code: r, Mod: mod} + if k.Mod <= ModShift { + k.Text = string(r) } + + return k } +// TerminalVersionEvent is a message that represents the terminal version. +type TerminalVersionEvent string + // ModifyOtherKeysEvent represents a modifyOtherKeys event. // // 0: disable