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 <cursoragent@cursor.com>
This commit is contained in:
Daniel Imms
2026-05-27 16:20:45 -07:00
co-authored by Cursor
parent f8e9a81d0e
commit bf8af4a871
2 changed files with 92 additions and 3 deletions
+19 -1
View File
@@ -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'));
});
});
+73 -2
View File
@@ -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<IDisposable>();
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');
}
}
}