Skip to content

Commit

Permalink
feat(input): add mouse support
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed Feb 14, 2024
1 parent 7e83172 commit 7bfa6a7
Show file tree
Hide file tree
Showing 5 changed files with 436 additions and 220 deletions.
11 changes: 5 additions & 6 deletions exp/term/input/ansi/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const (

// driver represents a terminal ANSI input driver.
type driver struct {
table map[string]input.Key
table map[string]input.KeyEvent
rd *bufio.Reader
term string
flags int
Expand Down Expand Up @@ -247,12 +247,11 @@ func (d *driver) parseCsi(i int, p []byte, alt bool) (n int, e input.Event, err
n++
seq += string(p[i])

// Handle X10 mouse
if seq == "\x1b[M" && i+3 < len(p) {
btn := int(p[i+1] - 32)
x := int(p[i+2] - 32)
y := int(p[i+3] - 32)
return n + 3, input.MouseEvent{X: x, Y: y, Btn: input.Button(btn)}, nil
// Handle X10 mouse
return n + 3, parseX10MouseEvent(append([]byte(seq), p[i+1:i+3]...)), nil
} else if seq[2] == '<' && (seq[len(seq)-1] == 'm' || seq[len(seq)-1] == 'M') {
return n, parseSGRMouseEvent([]byte(seq)), nil
}

k, ok := d.table[seq]
Expand Down
126 changes: 126 additions & 0 deletions exp/term/input/ansi/mouse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package ansi

import (
"regexp"
"strconv"

"github.com/charmbracelet/x/exp/term/input"
)

var mouseSGRRegex = regexp.MustCompile(`(\d+);(\d+);(\d+)([Mm])`)

// Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events
// look like:
//
// ESC [ < Cb ; Cx ; Cy (M or m)
//
// where:
//
// Cb is the encoded button code
// Cx is the x-coordinate of the mouse
// Cy is the y-coordinate of the mouse
// M is for button press, m is for button release
//
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
func parseSGRMouseEvent(buf []byte) input.MouseEvent {
str := string(buf[3:])
matches := mouseSGRRegex.FindStringSubmatch(str)
if len(matches) != 5 {
// Unreachable, we already checked the regex in `detectOneMsg`.
panic("invalid mouse event")
}

b, _ := strconv.Atoi(matches[1])
px := matches[2]
py := matches[3]
release := matches[4] == "m"
m := parseMouseButton(b, true)

// Wheel buttons don't have release events
// Motion can be reported as a release event in some terminals (Windows Terminal)
if m.Action != input.MouseActionMotion && !m.IsWheel() && release {
m.Action = input.MouseActionRelease
}

x, _ := strconv.Atoi(px)
y, _ := strconv.Atoi(py)

// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
m.X = x - 1
m.Y = y - 1

return m
}

const x10MouseByteOffset = 32

// Parse X10-encoded mouse events; the simplest kind. The last release of X10
// was December 1986, by the way. The original X10 mouse protocol limits the Cx
// and Cy coordinates to 223 (=255-032).
//
// X10 mouse events look like:
//
// ESC [M Cb Cx Cy
//
// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
func parseX10MouseEvent(buf []byte) input.MouseEvent {
v := buf[3:6]
m := parseMouseButton(int(v[0]), false)

// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
m.X = int(v[1]) - x10MouseByteOffset - 1
m.Y = int(v[2]) - x10MouseByteOffset - 1

return m
}

// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
func parseMouseButton(b int, isSGR bool) input.MouseEvent {
var m input.MouseEvent
e := b
if !isSGR {
e -= x10MouseByteOffset
}

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 e&bitAdd != 0 {
m.Button = input.MouseButtonBackward + input.MouseButton(e&bitsMask)
} else if e&bitWheel != 0 {
m.Button = input.MouseButtonWheelUp + input.MouseButton(e&bitsMask)
} else {
m.Button = input.MouseButtonLeft + input.MouseButton(e&bitsMask)
// X10 reports a button release as 0b0000_0011 (3)
if e&bitsMask == bitsMask {
m.Action = input.MouseActionRelease
m.Button = input.MouseButtonNone
}
}

// Motion bit doesn't get reported for wheel events.
if e&bitMotion != 0 && !m.IsWheel() {
m.Action = input.MouseActionMotion
}

// Modifiers
if e&bitAlt != 0 {
m.Mod |= input.Alt
}
if e&bitCtrl != 0 {
m.Mod |= input.Ctrl
}
if e&bitShift != 0 {
m.Mod |= input.Shift
}

return m
}
168 changes: 84 additions & 84 deletions exp/term/input/ansi/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,62 +6,62 @@ import (
)

func (d *driver) registerKeys(flags int) {
nul := input.Key{Rune: '@', Mod: input.Ctrl} // ctrl+@ or ctrl+space
nul := input.KeyEvent{Rune: '@', Mod: input.Ctrl} // ctrl+@ or ctrl+space
if flags&Fctrlsp != 0 {
if flags&Fspacesym != 0 {
nul.Rune = 0
nul.Sym = input.Space
nul.Sym = input.KeySpace
} else {
nul.Rune = ' '
}
}

tab := input.Key{Rune: 'i', Mod: input.Ctrl} // ctrl+i or tab
tab := input.KeyEvent{Rune: 'i', Mod: input.Ctrl} // ctrl+i or tab
if flags&Ftabsym != 0 {
tab.Rune = 0
tab.Mod = 0
tab.Sym = input.Tab
tab.Sym = input.KeyTab
}

enter := input.Key{Rune: 'm', Mod: input.Ctrl} // ctrl+m or enter
enter := input.KeyEvent{Rune: 'm', Mod: input.Ctrl} // ctrl+m or enter
if flags&Fentersym != 0 {
enter.Rune = 0
enter.Mod = 0
enter.Sym = input.Enter
enter.Sym = input.KeyEnter
}

esc := input.Key{Rune: '[', Mod: input.Ctrl} // ctrl+[ or escape
esc := input.KeyEvent{Rune: '[', Mod: input.Ctrl} // ctrl+[ or escape
if flags&Fescsym != 0 {
esc.Rune = 0
esc.Mod = 0
esc.Sym = input.Escape
esc.Sym = input.KeyEscape
}

sp := input.Key{Rune: ' '}
sp := input.KeyEvent{Rune: ' '}
if flags&Fspacesym != 0 {
sp.Rune = 0
sp.Sym = input.Space
sp.Sym = input.KeySpace
}

del := input.Key{Sym: input.Delete}
del := input.KeyEvent{Sym: input.KeyDelete}
if flags&Fdelbackspace != 0 {
del.Sym = input.Backspace
del.Sym = input.KeyBackspace
}

find := input.Key{Sym: input.Find}
find := input.KeyEvent{Sym: input.KeyFind}
if flags&Ffindhome != 0 {
find.Sym = input.Home
find.Sym = input.KeyHome
}

select_ := input.Key{Sym: input.Select}
select_ := input.KeyEvent{Sym: input.KeySelect}
if flags&Fselectend != 0 {
select_.Sym = input.End
select_.Sym = input.KeyEnd
}

// See: https://vt100.net/docs/vt100-ug/chapter3.html#S3.2
// See: https://vt100.net/docs/vt220-rm/chapter3.html
// See: https://vt100.net/docs/vt510-rm/chapter8.html
d.table = map[string]input.Key{
d.table = map[string]input.KeyEvent{
// C0 control characters
string(ansi.NUL): nul,
string(ansi.SOH): {Rune: 'a', Mod: input.Ctrl},
Expand Down Expand Up @@ -102,85 +102,85 @@ func (d *driver) registerKeys(flags int) {

// Special keys

"\x1b[Z": {Sym: input.Tab, Mod: input.Shift},
"\x1b[Z": {Sym: input.KeyTab, Mod: input.Shift},

"\x1b[1~": find,
"\x1b[2~": {Sym: input.Insert},
"\x1b[3~": {Sym: input.Delete},
"\x1b[2~": {Sym: input.KeyInsert},
"\x1b[3~": {Sym: input.KeyDelete},
"\x1b[4~": select_,
"\x1b[5~": {Sym: input.PgUp},
"\x1b[6~": {Sym: input.PgDown},
"\x1b[7~": {Sym: input.Home},
"\x1b[8~": {Sym: input.End},
"\x1b[5~": {Sym: input.KeyPgUp},
"\x1b[6~": {Sym: input.KeyPgDown},
"\x1b[7~": {Sym: input.KeyHome},
"\x1b[8~": {Sym: input.KeyEnd},

// Normal mode
"\x1b[A": {Sym: input.Up},
"\x1b[B": {Sym: input.Down},
"\x1b[C": {Sym: input.Right},
"\x1b[D": {Sym: input.Left},
"\x1b[E": {Sym: input.Begin},
"\x1b[F": {Sym: input.End},
"\x1b[H": {Sym: input.Home},
"\x1b[P": {Sym: input.F1},
"\x1b[Q": {Sym: input.F2},
"\x1b[R": {Sym: input.F3},
"\x1b[S": {Sym: input.F4},
"\x1b[A": {Sym: input.KeyUp},
"\x1b[B": {Sym: input.KeyDown},
"\x1b[C": {Sym: input.KeyRight},
"\x1b[D": {Sym: input.KeyLeft},
"\x1b[E": {Sym: input.KeyBegin},
"\x1b[F": {Sym: input.KeyEnd},
"\x1b[H": {Sym: input.KeyHome},
"\x1b[P": {Sym: input.KeyF1},
"\x1b[Q": {Sym: input.KeyF2},
"\x1b[R": {Sym: input.KeyF3},
"\x1b[S": {Sym: input.KeyF4},

// Application Cursor Key Mode (DECCKM)
"\x1bOA": {Sym: input.Up},
"\x1bOB": {Sym: input.Down},
"\x1bOC": {Sym: input.Right},
"\x1bOD": {Sym: input.Left},
"\x1bOE": {Sym: input.Begin},
"\x1bOF": {Sym: input.End},
"\x1bOH": {Sym: input.Home},
"\x1bOP": {Sym: input.F1},
"\x1bOQ": {Sym: input.F2},
"\x1bOR": {Sym: input.F3},
"\x1bOS": {Sym: input.F4},
"\x1bOA": {Sym: input.KeyUp},
"\x1bOB": {Sym: input.KeyDown},
"\x1bOC": {Sym: input.KeyRight},
"\x1bOD": {Sym: input.KeyLeft},
"\x1bOE": {Sym: input.KeyBegin},
"\x1bOF": {Sym: input.KeyEnd},
"\x1bOH": {Sym: input.KeyHome},
"\x1bOP": {Sym: input.KeyF1},
"\x1bOQ": {Sym: input.KeyF2},
"\x1bOR": {Sym: input.KeyF3},
"\x1bOS": {Sym: input.KeyF4},

// Keypad Application Mode (DECKPAM)

"\x1bOM": {Sym: input.KpEnter},
"\x1bOX": {Sym: input.KpEqual},
"\x1bOj": {Sym: input.KpMul},
"\x1bOk": {Sym: input.KpPlus},
"\x1bOl": {Sym: input.KpComma},
"\x1bOm": {Sym: input.KpMinus},
"\x1bOn": {Sym: input.KpPeriod},
"\x1bOo": {Sym: input.KpDiv},
"\x1bOp": {Sym: input.Kp0},
"\x1bOq": {Sym: input.Kp1},
"\x1bOr": {Sym: input.Kp2},
"\x1bOs": {Sym: input.Kp3},
"\x1bOt": {Sym: input.Kp4},
"\x1bOu": {Sym: input.Kp5},
"\x1bOv": {Sym: input.Kp6},
"\x1bOw": {Sym: input.Kp7},
"\x1bOx": {Sym: input.Kp8},
"\x1bOy": {Sym: input.Kp9},
"\x1bOM": {Sym: input.KeyKpEnter},
"\x1bOX": {Sym: input.KeyKpEqual},
"\x1bOj": {Sym: input.KeyKpMul},
"\x1bOk": {Sym: input.KeyKpPlus},
"\x1bOl": {Sym: input.KeyKpComma},
"\x1bOm": {Sym: input.KeyKpMinus},
"\x1bOn": {Sym: input.KeyKpPeriod},
"\x1bOo": {Sym: input.KeyKpDiv},
"\x1bOp": {Sym: input.KeyKp0},
"\x1bOq": {Sym: input.KeyKp1},
"\x1bOr": {Sym: input.KeyKp2},
"\x1bOs": {Sym: input.KeyKp3},
"\x1bOt": {Sym: input.KeyKp4},
"\x1bOu": {Sym: input.KeyKp5},
"\x1bOv": {Sym: input.KeyKp6},
"\x1bOw": {Sym: input.KeyKp7},
"\x1bOx": {Sym: input.KeyKp8},
"\x1bOy": {Sym: input.KeyKp9},

// Function keys

"\x1b[11~": {Sym: input.F1},
"\x1b[12~": {Sym: input.F2},
"\x1b[13~": {Sym: input.F3},
"\x1b[14~": {Sym: input.F4},
"\x1b[15~": {Sym: input.F5},
"\x1b[17~": {Sym: input.F6},
"\x1b[18~": {Sym: input.F7},
"\x1b[19~": {Sym: input.F8},
"\x1b[20~": {Sym: input.F9},
"\x1b[21~": {Sym: input.F10},
"\x1b[23~": {Sym: input.F11},
"\x1b[24~": {Sym: input.F12},
"\x1b[25~": {Sym: input.F13},
"\x1b[26~": {Sym: input.F14},
"\x1b[28~": {Sym: input.F15},
"\x1b[29~": {Sym: input.F16},
"\x1b[31~": {Sym: input.F17},
"\x1b[32~": {Sym: input.F18},
"\x1b[33~": {Sym: input.F19},
"\x1b[34~": {Sym: input.F20},
"\x1b[11~": {Sym: input.KeyF1},
"\x1b[12~": {Sym: input.KeyF2},
"\x1b[13~": {Sym: input.KeyF3},
"\x1b[14~": {Sym: input.KeyF4},
"\x1b[15~": {Sym: input.KeyF5},
"\x1b[17~": {Sym: input.KeyF6},
"\x1b[18~": {Sym: input.KeyF7},
"\x1b[19~": {Sym: input.KeyF8},
"\x1b[20~": {Sym: input.KeyF9},
"\x1b[21~": {Sym: input.KeyF10},
"\x1b[23~": {Sym: input.KeyF11},
"\x1b[24~": {Sym: input.KeyF12},
"\x1b[25~": {Sym: input.KeyF13},
"\x1b[26~": {Sym: input.KeyF14},
"\x1b[28~": {Sym: input.KeyF15},
"\x1b[29~": {Sym: input.KeyF16},
"\x1b[31~": {Sym: input.KeyF17},
"\x1b[32~": {Sym: input.KeyF18},
"\x1b[33~": {Sym: input.KeyF19},
"\x1b[34~": {Sym: input.KeyF20},
}
}
Loading

0 comments on commit 7bfa6a7

Please sign in to comment.