Skip to content

Commit

Permalink
feat(mouse): deprecated MouseEventType and support additional button
Browse files Browse the repository at this point in the history
This includes wheel left/right, and buttons 8 through 11
  • Loading branch information
aymanbagabas committed Nov 14, 2022
1 parent 7cff054 commit 2cb61de
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 125 deletions.
284 changes: 218 additions & 66 deletions mouse.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package tea
import (
"bytes"
"errors"
"regexp"
"strconv"
"strings"
)

const x10ByteOffset = 32
Expand All @@ -16,13 +18,24 @@ type MouseMsg MouseEvent
// MouseEvent represents a mouse event, which could be a click, a scroll wheel
// movement, a cursor movement, or a combination.
type MouseEvent struct {
X int
Y int
Type MouseEventType
Shift bool
Alt bool
Ctrl bool
Release bool // true if the mouse button was released (SGR only)
X int
Y int
Shift bool
Alt bool
Ctrl bool
Action MouseAction
Button MouseButton

// Deprecated: Use MouseAction & MouseButton instead.
Type MouseEventType

isSGR bool
}

// IsWheel returns true if the mouse event is a wheel event.
func (m MouseEvent) IsWheel() bool {
return m.Button == MouseButtonWheelUp || m.Button == MouseButtonWheelDown ||
m.Button == MouseButtonWheelLeft || m.Button == MouseButtonWheelRight
}

// String returns a string representation of a mouse event.
Expand All @@ -36,18 +49,99 @@ func (m MouseEvent) String() (s string) {
if m.Shift {
s += "shift+"
}
s += mouseEventTypes[m.Type]
// Only SGR mouse events report button releases.
if m.Release {
s += " release"

if m.isSGR {
if m.Button == MouseButtonNone && m.Action == MouseActionMotion {
s += mouseActions[m.Action]
} else if m.IsWheel() {
s += mouseButtons[m.Button]
} else {
s += mouseButtons[m.Button]
s += " "
s += mouseActions[m.Action]
}
} else {
s += mouseEventTypes[m.Type]
}

return s
}

// MouseAction represents the action that occurred during a mouse event.
type MouseAction int

// Mouse event actions
const (
MouseActionPress MouseAction = iota
MouseActionRelease
MouseActionMotion
)

var mouseActions = map[MouseAction]string{
MouseActionPress: "press",
MouseActionRelease: "release",
MouseActionMotion: "motion",
}

// MouseButton represents the button that was pressed during a mouse event.
type MouseButton int

// 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)
//
// Other buttons are not supported.
const (
MouseButtonNone MouseButton = iota
MouseButtonLeft
MouseButtonMiddle
MouseButtonRight
MouseButtonWheelUp
MouseButtonWheelDown
MouseButtonWheelLeft
MouseButtonWheelRight
MouseButtonBackward
MouseButtonForward
MouseButton10
MouseButton11

MouseButtonUnknown
)

var mouseButtons = map[MouseButton]string{
MouseButtonNone: "none",
MouseButtonLeft: "left",
MouseButtonMiddle: "middle",
MouseButtonRight: "right",
MouseButtonWheelUp: "wheel up",
MouseButtonWheelDown: "wheel down",
MouseButtonWheelLeft: "wheel left",
MouseButtonWheelRight: "wheel right",
MouseButtonBackward: "backward",
MouseButtonForward: "forward",
MouseButton10: "button 10",
MouseButton11: "button 11",
MouseButtonUnknown: "unknown",
}

// MouseEventType indicates the type of mouse event occurring.
//
// Deprecated: Use MouseAction & MouseButton instead.
type MouseEventType int

// Mouse event types.
//
// Deprecated: Use MouseAction & MouseButton instead.
const (
MouseUnknown MouseEventType = iota
MouseLeft
Expand All @@ -56,35 +150,54 @@ const (
MouseRelease // mouse button release (X10 only)
MouseWheelUp
MouseWheelDown
MouseWheelLeft
MouseWheelRight
MouseBackward
MouseForward
MouseBtn10
MouseBtn11
MouseMotion
)

var mouseEventTypes = map[MouseEventType]string{
MouseUnknown: "unknown",
MouseLeft: "left",
MouseRight: "right",
MouseMiddle: "middle",
MouseRelease: "release",
MouseWheelUp: "wheel up",
MouseWheelDown: "wheel down",
MouseMotion: "motion",
MouseUnknown: "unknown",
MouseLeft: "left",
MouseRight: "right",
MouseMiddle: "middle",
MouseRelease: "release",
MouseWheelUp: "wheel up",
MouseWheelDown: "wheel down",
MouseWheelLeft: "wheel left",
MouseWheelRight: "wheel right",
MouseBackward: "backward",
MouseForward: "forward",
MouseBtn10: "button 10",
MouseBtn11: "button 11",
MouseMotion: "motion",
}

var (
mouseX10Seq = []byte("\x1b[M")
mouseSGRSeq = []byte("\x1b[<")
)

func parseMouseEvents(buf []byte) ([]MouseEvent, error) {
if len(buf) == 0 {
return nil, errors.New("empty buffer")
}

switch {
case bytes.Contains(buf, []byte("\x1b[<")):
return parseSGRMouseEvents(buf)
case bytes.Contains(buf, []byte("\x1b[M")):
case bytes.Contains(buf, mouseSGRSeq):
return parseSGRMouseEvents(string(buf))
case bytes.Contains(buf, mouseX10Seq):
return parseX10MouseEvents(buf)
}

return nil, errors.New("not a mouse event")
}

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

// parseSGRMouseEvents parses SGR extended mouse events. SGR mouse events look
// like:
//
Expand All @@ -98,41 +211,46 @@ func parseMouseEvents(buf []byte) ([]MouseEvent, error) {
// M is for button press, m is for button release
//
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
func parseSGRMouseEvents(buf []byte) ([]MouseEvent, error) {
var r []MouseEvent
func parseSGRMouseEvents(buf string) ([]MouseEvent, error) {
var ev []MouseEvent

seq := []byte("\x1b[<")
if !bytes.Contains(buf, seq) {
seq := string(mouseSGRSeq)
if !strings.Contains(buf, seq) {
return nil, errors.New("not a SGR mouse event")
}

for _, v := range bytes.Split(buf, seq) {
for _, v := range strings.Split(buf, seq) {
if len(v) == 0 {
continue
}

e := bytes.Split(v, []byte(";"))
if len(e) != 3 {
matches := mouseSGRRegex.FindStringSubmatch(v)
if len(matches) != 5 {
return nil, errors.New("not a SGR mouse event")
}

b, _ := strconv.Atoi(string(e[0]))
b, _ := strconv.Atoi(matches[1])
px := matches[2]
py := matches[3]
release := matches[4] == "m"
m := parseMouseButton(b, true)
m.Release = e[2][len(e[2])-1] == 'm'
// Wheel buttons don't have release events
// Motion can be reported as a release event in some terminals (Windows Terminal)
if m.Action != MouseActionMotion && !m.IsWheel() && release {
m.Action = MouseActionRelease
}

px := e[1]
py := e[2][:len(e[2])-1]
x, _ := strconv.Atoi(string(px))
y, _ := strconv.Atoi(string(py))
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

r = append(r, m)
ev = append(ev, m)
}

return r, nil
return ev, nil
}

// Parse X10-encoded mouse events; the simplest kind. The last release of X10
Expand All @@ -147,7 +265,7 @@ func parseSGRMouseEvents(buf []byte) ([]MouseEvent, error) {
func parseX10MouseEvents(buf []byte) ([]MouseEvent, error) {
var r []MouseEvent

seq := []byte("\x1b[M")
seq := mouseX10Seq
if !bytes.Contains(buf, seq) {
return r, errors.New("not an X10 mouse event")
}
Expand All @@ -172,8 +290,10 @@ func parseX10MouseEvents(buf []byte) ([]MouseEvent, error) {
return r, nil
}

// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
func parseMouseButton(b int, isSGR bool) MouseEvent {
var m MouseEvent
m.isSGR = isSGR
e := b
if !isSGR {
e -= x10ByteOffset
Expand All @@ -185,43 +305,75 @@ func parseMouseButton(b int, isSGR bool) MouseEvent {
bitCtrl = 0b0001_0000
bitMotion = 0b0010_0000
bitWheel = 0b0100_0000
bitAdd = 0b1000_0000 // additional buttons 8-11

bitsMask = 0b0000_0011

bitsLeft = 0b0000_0000
bitsMiddle = 0b0000_0001
bitsRight = 0b0000_0010
bitsRelease = 0b0000_0011

bitsWheelUp = 0b0000_0000
bitsWheelDown = 0b0000_0001
)

if e&bitWheel != 0 {
// Check the low two bits.
switch e & bitsMask {
case bitsWheelUp:
m.Type = MouseWheelUp
case bitsWheelDown:
m.Type = MouseWheelDown
}
if e&bitAdd != 0 {
m.Button = MouseButtonBackward + MouseButton(e&bitsMask)
} else if e&bitWheel != 0 {
m.Button = MouseButtonWheelUp + MouseButton(e&bitsMask)
} else {
// Check the low two bits.
// We do not separate clicking and dragging.
switch e & bitsMask {
case bitsLeft:
m.Button = MouseButtonLeft + MouseButton(e&bitsMask)
// X10 reports a button release as 0b0000_0011 (3)
if e&bitsMask == bitsMask {
m.Action = MouseActionRelease
m.Button = MouseButtonNone
}
}
// Motion bit doesn't get reported for wheel events but we check for it
// anyway in case of a faulty terminal emulator.
if e&bitMotion != 0 && !m.IsWheel() {
m.Action = MouseActionMotion
}

// backward compatibility
switch {
case m.Button == MouseButtonLeft && m.Action == MouseActionPress:
m.Type = MouseLeft
case m.Button == MouseButtonMiddle && m.Action == MouseActionPress:
m.Type = MouseMiddle
case m.Button == MouseButtonRight && m.Action == MouseActionPress:
m.Type = MouseRight
case m.Button == MouseButtonNone && m.Action == MouseActionRelease:
m.Type = MouseRelease
case m.Button == MouseButtonWheelUp && m.Action == MouseActionPress:
m.Type = MouseWheelUp
case m.Button == MouseButtonWheelDown && m.Action == MouseActionPress:
m.Type = MouseWheelDown
case m.Button == MouseButtonWheelLeft && m.Action == MouseActionPress:
m.Type = MouseWheelLeft
case m.Button == MouseButtonWheelRight && m.Action == MouseActionPress:
m.Type = MouseWheelRight
case m.Button == MouseButtonBackward && m.Action == MouseActionPress:
m.Type = MouseBackward
case m.Button == MouseButtonForward && m.Action == MouseActionPress:
m.Type = MouseForward
case m.Button == MouseButton10 && m.Action == MouseActionPress:
m.Type = MouseBtn10
case m.Button == MouseButton11 && m.Action == MouseActionPress:
m.Type = MouseBtn11
case m.Action == MouseActionMotion:
m.Type = MouseMotion
switch m.Button {
case MouseButtonLeft:
m.Type = MouseLeft
case bitsMiddle:
case MouseButtonMiddle:
m.Type = MouseMiddle
case bitsRight:
case MouseButtonRight:
m.Type = MouseRight
case bitsRelease:
if e&bitMotion != 0 {
m.Type = MouseMotion
} else {
m.Type = MouseRelease
}
case MouseButtonBackward:
m.Type = MouseBackward
case MouseButtonForward:
m.Type = MouseForward
case MouseButton10:
m.Type = MouseBtn10
case MouseButton11:
m.Type = MouseBtn11
}
default:
m.Type = MouseUnknown
}

m.Alt = e&bitAlt != 0
Expand Down
Loading

0 comments on commit 2cb61de

Please sign in to comment.