[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
5

Configure Feed

Select the types of activity you want to include in your feed.

handle mouse events

Charles Lowell (Mar 30, 2026, 12:33 PM -0500) cfcbca71 d6bef782

+119 -37
+48 -24
input.ts
··· 163 163 export type KeyEvent = KeyDown | KeyRepeat | KeyUp; 164 164 165 165 /** 166 - * A mouse button was pressed or released 166 + * A mouse button was pressed. 167 167 */ 168 - export interface MouseEvent extends KeyModifiers { 169 - type: "mouse"; 168 + export interface MouseDownEvent extends KeyModifiers { 169 + type: "mousedown"; 170 + /** 171 + * Which mouse button triggered this event. 172 + */ 173 + button: "left" | "right" | "middle"; 174 + 175 + /** 176 + * `x` coordinate of this event 177 + */ 178 + x: number; 179 + 180 + /** 181 + * `y` coordinate of this event 182 + */ 183 + y: number; 184 + } 185 + 186 + /** 187 + * A mouse button was released. 188 + */ 189 + export interface MouseUpEvent extends KeyModifiers { 190 + type: "mouseup"; 170 191 /** 171 192 * Which mouse button triggered this event. 172 193 * ··· 185 206 * `y` coordinate of this event 186 207 */ 187 208 y: number; 188 - 189 - /** 190 - * True if the event represents a button being released. 191 - */ 192 - release?: true; 193 209 } 194 210 195 211 /** 196 212 * Mouse movement while a button is held. 197 213 */ 198 - export interface DragEvent extends KeyModifiers { 199 - type: "drag"; 214 + export interface MouseMoveEvent extends KeyModifiers { 215 + type: "mousemove"; 200 216 /** 201 217 * Which mouse button is being held during the drag. 202 218 */ ··· 251 267 252 268 export type InputEvent = 253 269 | KeyEvent 254 - | MouseEvent 255 - | DragEvent 270 + | MouseDownEvent 271 + | MouseUpEvent 272 + | MouseMoveEvent 256 273 | WheelEvent 257 274 | ResizeEvent; 258 275 ··· 441 458 [KEY_SCROLL_LOCK, "ScrollLock"], 442 459 ]); 443 460 444 - const BUTTON_NAMES = new Map<number, MouseEvent["button"]>([ 461 + const BUTTON_NAMES = new Map<number, MouseUpEvent["button"]>([ 445 462 [KEY_MOUSE_LEFT, "left"], 446 463 [KEY_MOUSE_RIGHT, "right"], 447 464 [KEY_MOUSE_MIDDLE, "middle"], ··· 526 543 if (native.mod & MOD_MOTION) { 527 544 let button = BUTTON_NAMES.get(native.key) ?? "left"; 528 545 return { 529 - type: "drag" as const, 546 + type: "mousemove" as const, 530 547 button: button === "release" ? "left" as const : button, 531 548 x: native.x, 532 549 y: native.y, ··· 534 551 }; 535 552 } 536 553 let button = BUTTON_NAMES.get(native.key) ?? "left"; 537 - let m = mods(native); 538 - let ev: MouseEvent = { 539 - type: "mouse", 540 - button, 541 - x: native.x, 542 - y: native.y, 543 - ...m, 544 - }; 545 - if (native.mod & MOD_RELEASE) ev.release = true; 546 - return ev; 554 + if (native.mod & MOD_RELEASE) { 555 + return { 556 + type: "mouseup" as const, 557 + button, 558 + x: native.x, 559 + y: native.y, 560 + ...mods(native), 561 + }; 562 + } else { 563 + return { 564 + type: "mousedown" as const, 565 + button: button as MouseDownEvent["button"], 566 + x: native.x, 567 + y: native.y, 568 + ...mods(native), 569 + }; 570 + } 547 571 } 548 572 case EVENT_RESIZE: { 549 573 return { type: "resize", width: native.w, height: native.h };
+12 -3
src/input.c
··· 61 61 #define MOUSE_BUTTON_RELEASE 3 62 62 #define MOUSE_WHEEL_BIT 64 63 63 64 + static uint8_t mouse_mods(int b) { 65 + uint8_t m = 0; 66 + if (b & 4) m |= MOD_SHIFT; 67 + if (b & 8) m |= MOD_ALT; 68 + if (b & 16) m |= MOD_CTRL; 69 + if (b & 32) m |= MOD_MOTION; 70 + return m; 71 + } 72 + 64 73 static uint16_t mouse_button(int b) { 65 74 switch (b & MOUSE_BUTTON_MASK) { 66 75 case MOUSE_BUTTON_LEFT: ··· 94 103 int b = st->buf[3] - 0x20; 95 104 ev->type = EVENT_MOUSE; 96 105 ev->key = mouse_button(b); 97 - ev->mod = (b & 32) ? MOD_MOTION : 0; 106 + ev->mod = mouse_mods(b); 98 107 if (ev->key == KEY_MOUSE_RELEASE) 99 108 ev->mod |= MOD_RELEASE; 100 109 ev->x = ((uint8_t)st->buf[4]) - 0x21; ··· 139 148 140 149 ev->type = EVENT_MOUSE; 141 150 ev->key = mouse_button(num[0]); 142 - ev->mod = (num[0] & 32) ? MOD_MOTION : 0; 151 + ev->mod = mouse_mods(num[0]); 143 152 if (trail == 'm') 144 153 ev->mod |= MOD_RELEASE; 145 154 ev->x = (num[1] - 1 < 0) ? 0 : num[1] - 1; ··· 185 194 int b = num[0] - 0x20; 186 195 ev->type = EVENT_MOUSE; 187 196 ev->key = mouse_button(b); 188 - ev->mod = (b & 32) ? MOD_MOTION : 0; 197 + ev->mod = mouse_mods(b); 189 198 if (ev->key == KEY_MOUSE_RELEASE) 190 199 ev->mod |= MOD_RELEASE; 191 200 ev->x = (num[1] - 1 < 0) ? 0 : num[1] - 1;
+59 -10
test/input.test.ts
··· 160 160 let result = input.scan(str("\x1b[<0;35;12M")); 161 161 expect(result.events.length).toBe(1); 162 162 expect(result.events[0]).toMatchObject({ 163 - type: "mouse", 163 + type: "mousedown", 164 164 button: "left", 165 165 x: 34, 166 166 y: 11, ··· 171 171 let result = input.scan(str("\x1b[<0;10;5m")); 172 172 expect(result.events.length).toBe(1); 173 173 expect(result.events[0]).toMatchObject({ 174 - type: "mouse", 174 + type: "mouseup", 175 175 button: "left", 176 176 x: 9, 177 177 y: 4, 178 - release: true, 179 178 }); 180 179 }); 181 180 ··· 183 182 let result = input.scan(str("\x1b[<2;10;5m")); 184 183 expect(result.events.length).toBe(1); 185 184 expect(result.events[0]).toMatchObject({ 186 - type: "mouse", 185 + type: "mouseup", 187 186 button: "right", 188 187 x: 9, 189 188 y: 4, 190 - release: true, 191 189 }); 192 190 }); 193 191 ··· 195 193 let result = input.scan(str("\x1b[<1;10;5m")); 196 194 expect(result.events.length).toBe(1); 197 195 expect(result.events[0]).toMatchObject({ 198 - type: "mouse", 196 + type: "mouseup", 199 197 button: "middle", 200 198 x: 9, 201 199 y: 4, 202 - release: true, 203 200 }); 204 201 }); 205 202 ··· 208 205 let result = input.scan(bytes(0x1b, 0x5b, 0x4d, 0x23, 0x2b, 0x26)); 209 206 expect(result.events.length).toBe(1); 210 207 expect(result.events[0]).toMatchObject({ 211 - type: "mouse", 208 + type: "mouseup", 212 209 button: "release", 213 210 x: 10, 214 211 y: 5, 215 - release: true, 216 212 }); 217 213 }); 218 214 ··· 243 239 let result = input.scan(bytes(0x1b, 0x5b, 0x4d, 0x20, 0x2b, 0x26)); 244 240 expect(result.events.length).toBe(1); 245 241 expect(result.events[0]).toMatchObject({ 246 - type: "mouse", 242 + type: "mousedown", 247 243 button: "left", 248 244 x: 10, 249 245 y: 5, 246 + }); 247 + }); 248 + 249 + it("parses SGR mouse press with shift", () => { 250 + // btn=4 (shift bit) => left + shift 251 + let result = input.scan(str("\x1b[<4;10;5M")); 252 + expect(result.events.length).toBe(1); 253 + expect(result.events[0]).toMatchObject({ 254 + type: "mousedown", 255 + button: "left", 256 + x: 9, 257 + y: 4, 258 + shift: true, 259 + }); 260 + }); 261 + 262 + it("parses SGR mouse press with ctrl", () => { 263 + // btn=16 (ctrl bit) => left + ctrl 264 + let result = input.scan(str("\x1b[<16;10;5M")); 265 + expect(result.events.length).toBe(1); 266 + expect(result.events[0]).toMatchObject({ 267 + type: "mousedown", 268 + button: "left", 269 + x: 9, 270 + y: 4, 271 + ctrl: true, 272 + }); 273 + }); 274 + 275 + it("parses SGR mouse press with alt", () => { 276 + // btn=8 (alt bit) => left + alt 277 + let result = input.scan(str("\x1b[<8;10;5M")); 278 + expect(result.events.length).toBe(1); 279 + expect(result.events[0]).toMatchObject({ 280 + type: "mousedown", 281 + button: "left", 282 + x: 9, 283 + y: 4, 284 + alt: true, 285 + }); 286 + }); 287 + 288 + it("parses SGR mouse press with ctrl+shift", () => { 289 + // btn=20 (ctrl=16 + shift=4) => left + ctrl + shift 290 + let result = input.scan(str("\x1b[<20;10;5M")); 291 + expect(result.events.length).toBe(1); 292 + expect(result.events[0]).toMatchObject({ 293 + type: "mousedown", 294 + button: "left", 295 + x: 9, 296 + y: 4, 297 + ctrl: true, 298 + shift: true, 250 299 }); 251 300 }); 252 301 });