Skip to content

Commit b5b6a04

Browse files
feat: add MouseStateService for mouse event protocol dispatch and encoding
Implement mousestate.go with protocol definitions (NONE, X10, VT200, DRAG, ANY), encoding functions (DEFAULT, SGR, SGR_PIXELS), and TriggerMouseEvent on Terminal that reads DecPrivateModes to dispatch mouse events as escape sequences via the data event. Fixes #29 Co-authored-by: Ona <no-reply@ona.com>
1 parent 5f78656 commit b5b6a04

3 files changed

Lines changed: 943 additions & 14 deletions

File tree

mousestate.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package xterm
2+
3+
// Ported from xterm.js src/common/services/MouseStateService.ts.
4+
// Provides mouse tracking reports with different protocols and encodings.
5+
6+
import "fmt"
7+
8+
// mouseModShift is the modifier bit for Shift in mouse event codes.
9+
const mouseModShift = 4
10+
11+
// mouseModAlt is the modifier bit for Alt in mouse event codes.
12+
const mouseModAlt = 8
13+
14+
// mouseModCtrl is the modifier bit for Ctrl in mouse event codes.
15+
const mouseModCtrl = 16
16+
17+
// coreMouseProtocol defines which events a mouse tracking mode accepts
18+
// and a restrict function that filters/modifies events.
19+
type coreMouseProtocol struct {
20+
events CoreMouseEventType
21+
restrict func(e *CoreMouseEvent) bool
22+
}
23+
24+
// defaultProtocols maps tracking mode names to their protocol definitions.
25+
var defaultProtocols = map[string]coreMouseProtocol{
26+
// NONE: no events reported.
27+
"NONE": {
28+
events: MouseEventNone,
29+
restrict: func(e *CoreMouseEvent) bool {
30+
return false
31+
},
32+
},
33+
// X10: mousedown only, no modifiers.
34+
"X10": {
35+
events: MouseEventDown,
36+
restrict: func(e *CoreMouseEvent) bool {
37+
if e.Button == MouseButtonWheel || e.Action != MouseActionDown {
38+
return false
39+
}
40+
e.Ctrl = false
41+
e.Alt = false
42+
e.Shift = false
43+
return true
44+
},
45+
},
46+
// VT200: mousedown, mouseup, wheel. All modifiers.
47+
"VT200": {
48+
events: MouseEventDown | MouseEventUp | MouseEventWheel,
49+
restrict: func(e *CoreMouseEvent) bool {
50+
if e.Action == MouseActionMove {
51+
return false
52+
}
53+
return true
54+
},
55+
},
56+
// DRAG: mousedown, mouseup, wheel, drag. All modifiers.
57+
"DRAG": {
58+
events: MouseEventDown | MouseEventUp | MouseEventWheel | MouseEventDrag,
59+
restrict: func(e *CoreMouseEvent) bool {
60+
if e.Action == MouseActionMove && e.Button == MouseButtonNone {
61+
return false
62+
}
63+
return true
64+
},
65+
},
66+
// ANY: all mouse events. All modifiers.
67+
"ANY": {
68+
events: MouseEventDown | MouseEventUp | MouseEventWheel | MouseEventDrag | MouseEventMove,
69+
restrict: func(e *CoreMouseEvent) bool {
70+
return true
71+
},
72+
},
73+
}
74+
75+
// mouseEventCode computes the numeric event code for a mouse event.
76+
// isSGR controls whether button release can report the actual button.
77+
func mouseEventCode(e *CoreMouseEvent, isSGR bool) int {
78+
code := 0
79+
if e.Ctrl {
80+
code |= mouseModCtrl
81+
}
82+
if e.Shift {
83+
code |= mouseModShift
84+
}
85+
if e.Alt {
86+
code |= mouseModAlt
87+
}
88+
89+
if e.Button == MouseButtonWheel {
90+
code |= 64
91+
code |= int(e.Action)
92+
} else {
93+
code |= int(e.Button) & 3
94+
if int(e.Button)&4 != 0 {
95+
code |= 64
96+
}
97+
if int(e.Button)&8 != 0 {
98+
code |= 128
99+
}
100+
if e.Action == MouseActionMove {
101+
code |= int(MouseActionMove)
102+
} else if e.Action == MouseActionUp && !isSGR {
103+
// Only SGR can report button on release; others use NONE.
104+
code |= int(MouseButtonNone)
105+
}
106+
}
107+
return code
108+
}
109+
110+
// CoreMouseEncoding is a function that encodes a CoreMouseEvent into an escape sequence.
111+
type CoreMouseEncoding func(e *CoreMouseEvent) string
112+
113+
// defaultEncodings maps encoding names to their encoding functions.
114+
var defaultEncodings = map[string]CoreMouseEncoding{
115+
// DEFAULT: CSI M Pb Px Py — single-byte encoding, values up to 223 (1-based).
116+
"DEFAULT": func(e *CoreMouseEvent) string {
117+
p0 := mouseEventCode(e, false) + 32
118+
p1 := e.Col + 32
119+
p2 := e.Row + 32
120+
if p0 > 255 || p1 > 255 || p2 > 255 {
121+
return ""
122+
}
123+
return fmt.Sprintf("\x1b[M%c%c%c", rune(p0), rune(p1), rune(p2))
124+
},
125+
// SGR: CSI < Pb ; Px ; Py M|m — no encoding limitation, reports button on release.
126+
"SGR": func(e *CoreMouseEvent) string {
127+
final := byte('M')
128+
if e.Action == MouseActionUp && e.Button != MouseButtonWheel {
129+
final = 'm'
130+
}
131+
return fmt.Sprintf("\x1b[<%d;%d;%d%c", mouseEventCode(e, true), e.Col, e.Row, final)
132+
},
133+
// SGR_PIXELS: like SGR but uses pixel coordinates (X, Y) instead of cell coordinates.
134+
"SGR_PIXELS": func(e *CoreMouseEvent) string {
135+
final := byte('M')
136+
if e.Action == MouseActionUp && e.Button != MouseButtonWheel {
137+
final = 'm'
138+
}
139+
return fmt.Sprintf("\x1b[<%d;%d;%d%c", mouseEventCode(e, true), e.X, e.Y, final)
140+
},
141+
}
142+
143+
// MouseStateService manages mouse tracking protocols and encodings.
144+
type MouseStateService struct {
145+
protocols map[string]coreMouseProtocol
146+
encodings map[string]CoreMouseEncoding
147+
activeProtocol string
148+
activeEncoding string
149+
150+
OnProtocolChangeEmitter EventEmitter[CoreMouseEventType]
151+
}
152+
153+
// NewMouseStateService creates a MouseStateService with default protocols and encodings.
154+
func NewMouseStateService() *MouseStateService {
155+
m := &MouseStateService{
156+
protocols: make(map[string]coreMouseProtocol),
157+
encodings: make(map[string]CoreMouseEncoding),
158+
}
159+
for name, p := range defaultProtocols {
160+
m.protocols[name] = p
161+
}
162+
for name, enc := range defaultEncodings {
163+
m.encodings[name] = enc
164+
}
165+
m.Reset()
166+
return m
167+
}
168+
169+
// ActiveProtocol returns the name of the active mouse tracking protocol.
170+
func (m *MouseStateService) ActiveProtocol() string {
171+
return m.activeProtocol
172+
}
173+
174+
// SetActiveProtocol sets the active mouse tracking protocol by name.
175+
func (m *MouseStateService) SetActiveProtocol(name string) {
176+
p, ok := m.protocols[name]
177+
if !ok {
178+
return
179+
}
180+
m.activeProtocol = name
181+
m.OnProtocolChangeEmitter.Fire(p.events)
182+
}
183+
184+
// ActiveEncoding returns the name of the active mouse encoding.
185+
func (m *MouseStateService) ActiveEncoding() string {
186+
return m.activeEncoding
187+
}
188+
189+
// SetActiveEncoding sets the active mouse encoding by name.
190+
func (m *MouseStateService) SetActiveEncoding(name string) {
191+
if _, ok := m.encodings[name]; !ok {
192+
return
193+
}
194+
m.activeEncoding = name
195+
}
196+
197+
// AreMouseEventsActive returns true if the active protocol accepts any events.
198+
func (m *MouseStateService) AreMouseEventsActive() bool {
199+
return m.protocols[m.activeProtocol].events != 0
200+
}
201+
202+
// Reset restores the default protocol (NONE) and encoding (DEFAULT).
203+
func (m *MouseStateService) Reset() {
204+
m.SetActiveProtocol("NONE")
205+
m.SetActiveEncoding("DEFAULT")
206+
}
207+
208+
// TriggerMouseEvent applies the active protocol's filter and encoding to the event.
209+
// Returns the encoded escape sequence and true if the event was accepted,
210+
// or ("", false) if the protocol rejected it.
211+
func (m *MouseStateService) TriggerMouseEvent(e CoreMouseEvent) (string, bool) {
212+
proto := m.protocols[m.activeProtocol]
213+
if proto.events == MouseEventNone {
214+
return "", false
215+
}
216+
if !proto.restrict(&e) {
217+
return "", false
218+
}
219+
encoded := m.encodings[m.activeEncoding](&e)
220+
if encoded == "" {
221+
return "", false
222+
}
223+
return encoded, true
224+
}
225+
226+
// Dispose cleans up event emitters.
227+
func (m *MouseStateService) Dispose() {
228+
m.OnProtocolChangeEmitter.Dispose()
229+
}

0 commit comments

Comments
 (0)