main
Raw Download raw file
  1package tea
  2
  3import "strconv"
  4
  5// MouseMsg contains information about a mouse event and are sent to a programs
  6// update function when mouse activity occurs. Note that the mouse must first
  7// be enabled in order for the mouse events to be received.
  8type MouseMsg MouseEvent
  9
 10// String returns a string representation of a mouse event.
 11func (m MouseMsg) String() string {
 12	return MouseEvent(m).String()
 13}
 14
 15// MouseEvent represents a mouse event, which could be a click, a scroll wheel
 16// movement, a cursor movement, or a combination.
 17type MouseEvent struct {
 18	X      int
 19	Y      int
 20	Shift  bool
 21	Alt    bool
 22	Ctrl   bool
 23	Action MouseAction
 24	Button MouseButton
 25
 26	// Deprecated: Use MouseAction & MouseButton instead.
 27	Type MouseEventType
 28}
 29
 30// IsWheel returns true if the mouse event is a wheel event.
 31func (m MouseEvent) IsWheel() bool {
 32	return m.Button == MouseButtonWheelUp || m.Button == MouseButtonWheelDown ||
 33		m.Button == MouseButtonWheelLeft || m.Button == MouseButtonWheelRight
 34}
 35
 36// String returns a string representation of a mouse event.
 37func (m MouseEvent) String() (s string) {
 38	if m.Ctrl {
 39		s += "ctrl+"
 40	}
 41	if m.Alt {
 42		s += "alt+"
 43	}
 44	if m.Shift {
 45		s += "shift+"
 46	}
 47
 48	if m.Button == MouseButtonNone { //nolint:nestif
 49		if m.Action == MouseActionMotion || m.Action == MouseActionRelease {
 50			s += mouseActions[m.Action]
 51		} else {
 52			s += "unknown"
 53		}
 54	} else if m.IsWheel() {
 55		s += mouseButtons[m.Button]
 56	} else {
 57		btn := mouseButtons[m.Button]
 58		if btn != "" {
 59			s += btn
 60		}
 61		act := mouseActions[m.Action]
 62		if act != "" {
 63			s += " " + act
 64		}
 65	}
 66
 67	return s
 68}
 69
 70// MouseAction represents the action that occurred during a mouse event.
 71type MouseAction int
 72
 73// Mouse event actions.
 74const (
 75	MouseActionPress MouseAction = iota
 76	MouseActionRelease
 77	MouseActionMotion
 78)
 79
 80var mouseActions = map[MouseAction]string{
 81	MouseActionPress:   "press",
 82	MouseActionRelease: "release",
 83	MouseActionMotion:  "motion",
 84}
 85
 86// MouseButton represents the button that was pressed during a mouse event.
 87type MouseButton int
 88
 89// Mouse event buttons
 90//
 91// This is based on X11 mouse button codes.
 92//
 93//	1 = left button
 94//	2 = middle button (pressing the scroll wheel)
 95//	3 = right button
 96//	4 = turn scroll wheel up
 97//	5 = turn scroll wheel down
 98//	6 = push scroll wheel left
 99//	7 = push scroll wheel right
100//	8 = 4th button (aka browser backward button)
101//	9 = 5th button (aka browser forward button)
102//	10
103//	11
104//
105// Other buttons are not supported.
106const (
107	MouseButtonNone MouseButton = iota
108	MouseButtonLeft
109	MouseButtonMiddle
110	MouseButtonRight
111	MouseButtonWheelUp
112	MouseButtonWheelDown
113	MouseButtonWheelLeft
114	MouseButtonWheelRight
115	MouseButtonBackward
116	MouseButtonForward
117	MouseButton10
118	MouseButton11
119)
120
121var mouseButtons = map[MouseButton]string{
122	MouseButtonNone:       "none",
123	MouseButtonLeft:       "left",
124	MouseButtonMiddle:     "middle",
125	MouseButtonRight:      "right",
126	MouseButtonWheelUp:    "wheel up",
127	MouseButtonWheelDown:  "wheel down",
128	MouseButtonWheelLeft:  "wheel left",
129	MouseButtonWheelRight: "wheel right",
130	MouseButtonBackward:   "backward",
131	MouseButtonForward:    "forward",
132	MouseButton10:         "button 10",
133	MouseButton11:         "button 11",
134}
135
136// MouseEventType indicates the type of mouse event occurring.
137//
138// Deprecated: Use MouseAction & MouseButton instead.
139type MouseEventType int
140
141// Mouse event types.
142//
143// Deprecated: Use MouseAction & MouseButton instead.
144const (
145	MouseUnknown MouseEventType = iota
146	MouseLeft
147	MouseRight
148	MouseMiddle
149	MouseRelease // mouse button release (X10 only)
150	MouseWheelUp
151	MouseWheelDown
152	MouseWheelLeft
153	MouseWheelRight
154	MouseBackward
155	MouseForward
156	MouseMotion
157)
158
159// Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events
160// look like:
161//
162//	ESC [ < Cb ; Cx ; Cy (M or m)
163//
164// where:
165//
166//	Cb is the encoded button code
167//	Cx is the x-coordinate of the mouse
168//	Cy is the y-coordinate of the mouse
169//	M is for button press, m is for button release
170//
171// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
172func parseSGRMouseEvent(buf []byte) MouseEvent {
173	str := string(buf[3:])
174	matches := mouseSGRRegex.FindStringSubmatch(str)
175	if len(matches) != 5 { //nolint:gomnd
176		// Unreachable, we already checked the regex in `detectOneMsg`.
177		panic("invalid mouse event")
178	}
179
180	b, _ := strconv.Atoi(matches[1])
181	px := matches[2]
182	py := matches[3]
183	release := matches[4] == "m"
184	m := parseMouseButton(b, true)
185
186	// Wheel buttons don't have release events
187	// Motion can be reported as a release event in some terminals (Windows Terminal)
188	if m.Action != MouseActionMotion && !m.IsWheel() && release {
189		m.Action = MouseActionRelease
190		m.Type = MouseRelease
191	}
192
193	x, _ := strconv.Atoi(px)
194	y, _ := strconv.Atoi(py)
195
196	// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
197	m.X = x - 1
198	m.Y = y - 1
199
200	return m
201}
202
203const x10MouseByteOffset = 32
204
205// Parse X10-encoded mouse events; the simplest kind. The last release of X10
206// was December 1986, by the way. The original X10 mouse protocol limits the Cx
207// and Cy coordinates to 223 (=255-032).
208//
209// X10 mouse events look like:
210//
211//	ESC [M Cb Cx Cy
212//
213// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
214func parseX10MouseEvent(buf []byte) MouseEvent {
215	v := buf[3:6]
216	m := parseMouseButton(int(v[0]), false)
217
218	// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
219	m.X = int(v[1]) - x10MouseByteOffset - 1
220	m.Y = int(v[2]) - x10MouseByteOffset - 1
221
222	return m
223}
224
225// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
226func parseMouseButton(b int, isSGR bool) MouseEvent {
227	var m MouseEvent
228	e := b
229	if !isSGR {
230		e -= x10MouseByteOffset
231	}
232
233	const (
234		bitShift  = 0b0000_0100
235		bitAlt    = 0b0000_1000
236		bitCtrl   = 0b0001_0000
237		bitMotion = 0b0010_0000
238		bitWheel  = 0b0100_0000
239		bitAdd    = 0b1000_0000 // additional buttons 8-11
240
241		bitsMask = 0b0000_0011
242	)
243
244	if e&bitAdd != 0 {
245		m.Button = MouseButtonBackward + MouseButton(e&bitsMask)
246	} else if e&bitWheel != 0 {
247		m.Button = MouseButtonWheelUp + MouseButton(e&bitsMask)
248	} else {
249		m.Button = MouseButtonLeft + MouseButton(e&bitsMask)
250		// X10 reports a button release as 0b0000_0011 (3)
251		if e&bitsMask == bitsMask {
252			m.Action = MouseActionRelease
253			m.Button = MouseButtonNone
254		}
255	}
256
257	// Motion bit doesn't get reported for wheel events.
258	if e&bitMotion != 0 && !m.IsWheel() {
259		m.Action = MouseActionMotion
260	}
261
262	// Modifiers
263	m.Alt = e&bitAlt != 0
264	m.Ctrl = e&bitCtrl != 0
265	m.Shift = e&bitShift != 0
266
267	// backward compatibility
268	switch {
269	case m.Button == MouseButtonLeft && m.Action == MouseActionPress:
270		m.Type = MouseLeft
271	case m.Button == MouseButtonMiddle && m.Action == MouseActionPress:
272		m.Type = MouseMiddle
273	case m.Button == MouseButtonRight && m.Action == MouseActionPress:
274		m.Type = MouseRight
275	case m.Button == MouseButtonNone && m.Action == MouseActionRelease:
276		m.Type = MouseRelease
277	case m.Button == MouseButtonWheelUp && m.Action == MouseActionPress:
278		m.Type = MouseWheelUp
279	case m.Button == MouseButtonWheelDown && m.Action == MouseActionPress:
280		m.Type = MouseWheelDown
281	case m.Button == MouseButtonWheelLeft && m.Action == MouseActionPress:
282		m.Type = MouseWheelLeft
283	case m.Button == MouseButtonWheelRight && m.Action == MouseActionPress:
284		m.Type = MouseWheelRight
285	case m.Button == MouseButtonBackward && m.Action == MouseActionPress:
286		m.Type = MouseBackward
287	case m.Button == MouseButtonForward && m.Action == MouseActionPress:
288		m.Type = MouseForward
289	case m.Action == MouseActionMotion:
290		m.Type = MouseMotion
291		switch m.Button { //nolint:exhaustive
292		case MouseButtonLeft:
293			m.Type = MouseLeft
294		case MouseButtonMiddle:
295			m.Type = MouseMiddle
296		case MouseButtonRight:
297			m.Type = MouseRight
298		case MouseButtonBackward:
299			m.Type = MouseBackward
300		case MouseButtonForward:
301			m.Type = MouseForward
302		}
303	default:
304		m.Type = MouseUnknown
305	}
306
307	return m
308}