| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- package input
- import (
- "fmt"
- "github.com/charmbracelet/x/ansi"
- )
- // MouseButton represents the button that was pressed during a mouse message.
- type MouseButton = ansi.MouseButton
- // Mouse event buttons
- //
- // This is based on X11 mouse button codes.
- //
- // 1 = left button
- // 2 = middle button (pressing the scroll wheel)
- // 3 = right button
- // 4 = turn scroll wheel up
- // 5 = turn scroll wheel down
- // 6 = push scroll wheel left
- // 7 = push scroll wheel right
- // 8 = 4th button (aka browser backward button)
- // 9 = 5th button (aka browser forward button)
- // 10
- // 11
- //
- // Other buttons are not supported.
- const (
- MouseNone = ansi.MouseNone
- MouseLeft = ansi.MouseLeft
- MouseMiddle = ansi.MouseMiddle
- MouseRight = ansi.MouseRight
- MouseWheelUp = ansi.MouseWheelUp
- MouseWheelDown = ansi.MouseWheelDown
- MouseWheelLeft = ansi.MouseWheelLeft
- MouseWheelRight = ansi.MouseWheelRight
- MouseBackward = ansi.MouseBackward
- MouseForward = ansi.MouseForward
- MouseButton10 = ansi.MouseButton10
- MouseButton11 = ansi.MouseButton11
- )
- // MouseEvent represents a mouse message. This is a generic mouse message that
- // can represent any kind of mouse event.
- type MouseEvent interface {
- fmt.Stringer
- // Mouse returns the underlying mouse event.
- Mouse() Mouse
- }
- // Mouse represents a Mouse message. Use [MouseEvent] to represent all mouse
- // messages.
- //
- // The X and Y coordinates are zero-based, with (0,0) being the upper left
- // corner of the terminal.
- //
- // // Catch all mouse events
- // switch Event := Event.(type) {
- // case MouseEvent:
- // m := Event.Mouse()
- // fmt.Println("Mouse event:", m.X, m.Y, m)
- // }
- //
- // // Only catch mouse click events
- // switch Event := Event.(type) {
- // case MouseClickEvent:
- // fmt.Println("Mouse click event:", Event.X, Event.Y, Event)
- // }
- type Mouse struct {
- X, Y int
- Button MouseButton
- Mod KeyMod
- }
- // String returns a string representation of the mouse message.
- func (m Mouse) String() (s string) {
- if m.Mod.Contains(ModCtrl) {
- s += "ctrl+"
- }
- if m.Mod.Contains(ModAlt) {
- s += "alt+"
- }
- if m.Mod.Contains(ModShift) {
- s += "shift+"
- }
- str := m.Button.String()
- if str == "" {
- s += "unknown"
- } else if str != "none" { // motion events don't have a button
- s += str
- }
- return s
- }
- // MouseClickEvent represents a mouse button click event.
- type MouseClickEvent Mouse
- // String returns a string representation of the mouse click event.
- func (e MouseClickEvent) String() string {
- return Mouse(e).String()
- }
- // Mouse returns the underlying mouse event. This is a convenience method and
- // syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse
- // event to [Mouse].
- func (e MouseClickEvent) Mouse() Mouse {
- return Mouse(e)
- }
- // MouseReleaseEvent represents a mouse button release event.
- type MouseReleaseEvent Mouse
- // String returns a string representation of the mouse release event.
- func (e MouseReleaseEvent) String() string {
- return Mouse(e).String()
- }
- // Mouse returns the underlying mouse event. This is a convenience method and
- // syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse
- // event to [Mouse].
- func (e MouseReleaseEvent) Mouse() Mouse {
- return Mouse(e)
- }
- // MouseWheelEvent represents a mouse wheel message event.
- type MouseWheelEvent Mouse
- // String returns a string representation of the mouse wheel event.
- func (e MouseWheelEvent) String() string {
- return Mouse(e).String()
- }
- // Mouse returns the underlying mouse event. This is a convenience method and
- // syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse
- // event to [Mouse].
- func (e MouseWheelEvent) Mouse() Mouse {
- return Mouse(e)
- }
- // MouseMotionEvent represents a mouse motion event.
- type MouseMotionEvent Mouse
- // String returns a string representation of the mouse motion event.
- func (e MouseMotionEvent) String() string {
- m := Mouse(e)
- if m.Button != 0 {
- return m.String() + "+motion"
- }
- return m.String() + "motion"
- }
- // Mouse returns the underlying mouse event. This is a convenience method and
- // syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse
- // event to [Mouse].
- func (e MouseMotionEvent) Mouse() Mouse {
- return Mouse(e)
- }
- // Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events
- // look like:
- //
- // ESC [ < Cb ; Cx ; Cy (M or m)
- //
- // where:
- //
- // Cb is the encoded button code
- // Cx is the x-coordinate of the mouse
- // Cy is the y-coordinate of the mouse
- // M is for button press, m is for button release
- //
- // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
- func parseSGRMouseEvent(cmd ansi.Cmd, params ansi.Params) Event {
- x, _, ok := params.Param(1, 1)
- if !ok {
- x = 1
- }
- y, _, ok := params.Param(2, 1)
- if !ok {
- y = 1
- }
- release := cmd.Final() == 'm'
- b, _, _ := params.Param(0, 0)
- mod, btn, _, isMotion := parseMouseButton(b)
- // (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
- x--
- y--
- m := Mouse{X: x, Y: y, Button: btn, Mod: mod}
- // Wheel buttons don't have release events
- // Motion can be reported as a release event in some terminals (Windows Terminal)
- if isWheel(m.Button) {
- return MouseWheelEvent(m)
- } else if !isMotion && release {
- return MouseReleaseEvent(m)
- } else if isMotion {
- return MouseMotionEvent(m)
- }
- return MouseClickEvent(m)
- }
- const x10MouseByteOffset = 32
- // Parse X10-encoded mouse events; the simplest kind. The last release of X10
- // was December 1986, by the way. The original X10 mouse protocol limits the Cx
- // and Cy coordinates to 223 (=255-032).
- //
- // X10 mouse events look like:
- //
- // ESC [M Cb Cx Cy
- //
- // See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
- func parseX10MouseEvent(buf []byte) Event {
- v := buf[3:6]
- b := int(v[0])
- if b >= x10MouseByteOffset {
- // XXX: b < 32 should be impossible, but we're being defensive.
- b -= x10MouseByteOffset
- }
- mod, btn, isRelease, isMotion := parseMouseButton(b)
- // (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
- x := int(v[1]) - x10MouseByteOffset - 1
- y := int(v[2]) - x10MouseByteOffset - 1
- m := Mouse{X: x, Y: y, Button: btn, Mod: mod}
- if isWheel(m.Button) {
- return MouseWheelEvent(m)
- } else if isMotion {
- return MouseMotionEvent(m)
- } else if isRelease {
- return MouseReleaseEvent(m)
- }
- return MouseClickEvent(m)
- }
- // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
- func parseMouseButton(b int) (mod KeyMod, btn MouseButton, isRelease bool, isMotion bool) {
- // mouse bit shifts
- const (
- bitShift = 0b0000_0100
- bitAlt = 0b0000_1000
- bitCtrl = 0b0001_0000
- bitMotion = 0b0010_0000
- bitWheel = 0b0100_0000
- bitAdd = 0b1000_0000 // additional buttons 8-11
- bitsMask = 0b0000_0011
- )
- // Modifiers
- if b&bitAlt != 0 {
- mod |= ModAlt
- }
- if b&bitCtrl != 0 {
- mod |= ModCtrl
- }
- if b&bitShift != 0 {
- mod |= ModShift
- }
- if b&bitAdd != 0 {
- btn = MouseBackward + MouseButton(b&bitsMask)
- } else if b&bitWheel != 0 {
- btn = MouseWheelUp + MouseButton(b&bitsMask)
- } else {
- btn = MouseLeft + MouseButton(b&bitsMask)
- // X10 reports a button release as 0b0000_0011 (3)
- if b&bitsMask == bitsMask {
- btn = MouseNone
- isRelease = true
- }
- }
- // Motion bit doesn't get reported for wheel events.
- if b&bitMotion != 0 && !isWheel(btn) {
- isMotion = true
- }
- return //nolint:nakedret
- }
- // isWheel returns true if the mouse event is a wheel event.
- func isWheel(btn MouseButton) bool {
- return btn >= MouseWheelUp && btn <= MouseWheelRight
- }
|