|
| 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