From bf8af4a87190b8d897c0e4a32e5cd2250771f272 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 27 May 2026 16:20:45 -0700 Subject: [PATCH] Toggle enable-mouse-events cursor while alt is held for mouseEventsRequireAlt. Extract AltMouseCursorController to bind DOM listeners only when the option and mouse protocol are active. Co-authored-by: Cursor --- src/browser/services/MouseService.test.ts | 20 +++++- src/browser/services/MouseService.ts | 75 ++++++++++++++++++++++- 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/browser/services/MouseService.test.ts b/src/browser/services/MouseService.test.ts index 4afa0336f..df3983572 100644 --- a/src/browser/services/MouseService.test.ts +++ b/src/browser/services/MouseService.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import { assert } from 'chai'; -import { MouseService } from 'browser/services/MouseService'; +import { AltMouseCursorController, MouseService } from 'browser/services/MouseService'; import { MouseStateService } from 'common/services/MouseStateService'; import { CoreMouseAction, CoreMouseButton } from 'common/Types'; import { IBufferService, ICoreService, ILogService, IOptionsService } from 'common/services/Services'; @@ -332,4 +332,22 @@ describe('MouseService mouseEventsRequireAlt', () => { assert.isTrue(sent); assert.deepEqual(reports, ['\x1b[<0;1;1M']); }); + + it('should toggle enable-mouse-events class when alt modifier changes', () => { + const element = createTestMouseTargetElement(); + const altMouseCursor = new AltMouseCursorController(element, { + addEventListener: () => {}, + removeEventListener: () => {} + } as any, () => true); + + const sync = (altHeld: boolean) => altMouseCursor.syncFromModifier({ + getModifierState: (key: string) => key === 'Alt' && altHeld + } as KeyboardEvent); + + assert.isFalse(element.classList.contains('enable-mouse-events')); + sync(true); + assert.isTrue(element.classList.contains('enable-mouse-events')); + sync(false); + assert.isFalse(element.classList.contains('enable-mouse-events')); + }); }); diff --git a/src/browser/services/MouseService.ts b/src/browser/services/MouseService.ts index b90015139..64fe75ffe 100644 --- a/src/browser/services/MouseService.ts +++ b/src/browser/services/MouseService.ts @@ -7,7 +7,7 @@ import { addDisposableListener } from 'browser/Dom'; import { IBufferService, IMouseStateService, ICoreService, ILogService, IOptionsService } from 'common/services/Services'; import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, ICoreMouseEvent, IDisposable } from 'common/Types'; import { C0 } from 'common/data/EscapeSequences'; -import { toDisposable } from 'common/Lifecycle'; +import { DisposableStore, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services'; import { Gesture, EventType as GestureEventType, IGestureEvent } from 'browser/scrollable/touch'; @@ -25,6 +25,7 @@ export class MouseService implements IMouseService { private _lastEvent: ICoreMouseEvent | null = null; private _wheelPartialScroll: number = 0; private _touchScrollAccumulator: number = 0; + private _altMouseCursor: AltMouseCursorController | undefined; constructor( @IRenderService private readonly _renderService: IRenderService, @@ -63,11 +64,19 @@ export class MouseService implements IMouseService { mousedrag: (ev: Event) => this._handleMouseDrag(ctx, ev as MouseEvent), mousemove: (ev: Event) => this._handleMouseMove(ctx, ev as MouseEvent) }; + this._altMouseCursor = new AltMouseCursorController( + element, + document, + () => this._mouseStateService.areMouseEventsActive + && !!this._optionsService.rawOptions.mouseEventsRequireAlt + ); + register(this._altMouseCursor); register(this._mouseStateService.onProtocolChange(events => { this._handleProtocolChange(ctx, eventListeners, events); })); register(this._optionsService.onSpecificOptionChange('mouseEventsRequireAlt', () => { this._syncMouseModeState(element); + this._altMouseCursor?.sync(); })); // force initial onProtocolChange so we dont miss early mouse requests this._mouseStateService.activeProtocol = this._mouseStateService.activeProtocol; @@ -372,7 +381,7 @@ export class MouseService implements IMouseService { private _syncMouseModeState(element: HTMLElement): void { if (this._mouseStateService.areMouseEventsActive) { if (this._optionsService.rawOptions.mouseEventsRequireAlt) { - element.classList.remove('enable-mouse-events'); + this._altMouseCursor?.resetClass(); this._selectionService.enable(); } else { element.classList.add('enable-mouse-events'); @@ -396,6 +405,7 @@ export class MouseService implements IMouseService { this._logService.debug('Unbinding from mouse events.'); } this._syncMouseModeState(element); + this._altMouseCursor?.sync(); // add/remove handlers from requestedEvents if (!(events & CoreMouseEventType.MOVE)) { @@ -565,3 +575,64 @@ export class MouseService implements IMouseService { } } + +/** + * Toggles `enable-mouse-events` on the terminal element while alt is held when + * `mouseEventsRequireAlt` is active. DOM listeners are only registered while active. + */ +export class AltMouseCursorController implements IDisposable { + private readonly _listeners = new MutableDisposable(); + + constructor( + private readonly _element: HTMLElement, + private readonly _document: Document, + private readonly _isActive: () => boolean + ) { + } + + public dispose(): void { + this._listeners.dispose(); + } + + public sync(): void { + this._listeners.clear(); + + if (!this._isActive()) { + return; + } + + const store = new DisposableStore(); + const syncFromModifier = (ev: KeyboardEvent | MouseEvent) => this.syncFromModifier(ev); + store.add(addDisposableListener(this._document, 'keydown', syncFromModifier)); + store.add(addDisposableListener(this._document, 'keyup', syncFromModifier)); + store.add(addDisposableListener(this._element, 'mousemove', syncFromModifier)); + const targetWindow = this._element.ownerDocument?.defaultView; + if (targetWindow) { + store.add(addDisposableListener(targetWindow, 'blur', () => { + if (this._isActive()) { + this.resetClass(); + } + })); + } + this._listeners.value = store; + } + + public resetClass(): void { + this._updateClass(false); + } + + public syncFromModifier(ev: KeyboardEvent | MouseEvent): void { + if (!this._isActive()) { + return; + } + this._updateClass(ev.getModifierState('Alt')); + } + + private _updateClass(altHeld: boolean): void { + if (altHeld) { + this._element.classList.add('enable-mouse-events'); + } else { + this._element.classList.remove('enable-mouse-events'); + } + } +}