Add altClickForMouseEvents option for issue #5952

When enabled during mouse events mode, click/drag/move events require the alt
key while wheel events are unaffected. Selection stays enabled by default
and takes precedence over macOptionClickForcesSelection.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-05-27 20:13:31 +00:00
co-authored by Daniel Imms
parent 95d9224b8c
commit fbb2a9710f
8 changed files with 75 additions and 9 deletions
+4 -2
View File
@@ -619,8 +619,10 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
// apply mouse event classes set by escape codes before terminal was attached
if (this.mouseStateService.areMouseEventsActive) {
this._selectionService.disable();
this.element.classList.add('enable-mouse-events');
if (!this.options.altClickForMouseEvents) {
this._selectionService.disable();
this.element.classList.add('enable-mouse-events');
}
} else {
this._selectionService.enable();
}
+13 -2
View File
@@ -153,6 +153,13 @@ export class MouseService implements IMouseService {
return false;
}
if (but !== CoreMouseButton.WHEEL
&& this._optionsService.rawOptions.altClickForMouseEvents
&& this._mouseStateService.areMouseEventsActive
&& !ev.altKey) {
return false;
}
return this._triggerMouseEvent({
col: pos.col,
row: pos.row,
@@ -361,8 +368,12 @@ export class MouseService implements IMouseService {
if (this._optionsService.rawOptions.logLevel === 'debug') {
this._logService.debug('Binding to mouse events:', this._explainEvents(events));
}
element.classList.add('enable-mouse-events');
this._selectionService.disable();
if (this._optionsService.rawOptions.altClickForMouseEvents) {
this._selectionService.enable();
} else {
element.classList.add('enable-mouse-events');
this._selectionService.disable();
}
} else {
this._logService.debug('Unbinding from mouse events.');
element.classList.remove('enable-mouse-events');
+23 -4
View File
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { SelectionService, SelectionMode } from './SelectionService';
import { SelectionModel } from 'browser/selection/SelectionModel';
import { IBufferLine } from 'common/Types';
import { MockBufferService, MockOptionsService, MockCoreService, createCellData } from 'common/TestUtils.test';
import { MockBufferService, MockOptionsService, MockCoreService, MockMouseStateService, createCellData } from 'common/TestUtils.test';
import { BufferLine } from 'common/buffer/BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { IBufferService, IOptionsService } from 'common/services/Services';
@@ -22,9 +22,10 @@ class TestSelectionService extends SelectionService {
constructor(
bufferService: IBufferService,
optionsService: IOptionsService,
renderService: IRenderService
renderService: IRenderService,
public readonly mouseStateService: MockMouseStateService
) {
super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService, new MockCoreBrowserService());
super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, mouseStateService, renderService, new MockCoreBrowserService());
}
public get model(): SelectionModel { return this._model; }
@@ -45,16 +46,18 @@ describe('SelectionService', () => {
let buffer: IBuffer;
let bufferService: IBufferService;
let optionsService: IOptionsService;
let mouseStateService: MockMouseStateService;
let selectionService: TestSelectionService;
beforeEach(() => {
optionsService = new MockOptionsService();
mouseStateService = new MockMouseStateService();
bufferService = new MockBufferService(20, 20, optionsService);
buffer = bufferService.buffer;
const renderService = new MockRenderService();
renderService.dimensions.css.canvas.height = 10 * 20;
renderService.dimensions.css.canvas.width = 10 * 20;
selectionService = new TestSelectionService(bufferService, optionsService, renderService);
selectionService = new TestSelectionService(bufferService, optionsService, renderService, mouseStateService);
});
function stringToRow(text: string): IBufferLine {
@@ -499,5 +502,21 @@ describe('SelectionService', () => {
assert.isFalse(selectionService.areCoordsInSelection([2, 1], [2, 0], [2, 1]));
});
});
describe('shouldForceSelection', () => {
it('should force selection without alt when altClickForMouseEvents is enabled', () => {
optionsService.options.altClickForMouseEvents = true;
mouseStateService.areMouseEventsActive = true;
assert.isTrue(selectionService.shouldForceSelection({ altKey: false } as MouseEvent));
assert.isFalse(selectionService.shouldForceSelection({ altKey: true } as MouseEvent));
});
it('should take precedence over macOptionClickForcesSelection', () => {
optionsService.options.altClickForMouseEvents = true;
optionsService.options.macOptionClickForcesSelection = true;
mouseStateService.areMouseEventsActive = true;
assert.isFalse(selectionService.shouldForceSelection({ altKey: true } as MouseEvent));
});
});
});
+13 -1
View File
@@ -15,7 +15,7 @@ import { IBufferLine, ICellData, IDisposable } from 'common/Types';
import { getRangeLength } from 'common/buffer/BufferRange';
import { CellData } from 'common/buffer/CellData';
import { IBuffer } from 'common/buffer/Types';
import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services';
import { IBufferService, ICoreService, IMouseStateService, IOptionsService } from 'common/services/Services';
import { Emitter } from 'common/Event';
const enum Constants {
@@ -127,6 +127,7 @@ export class SelectionService extends Disposable implements ISelectionService {
@ICoreService private readonly _coreService: ICoreService,
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
@IOptionsService private readonly _optionsService: IOptionsService,
@IMouseStateService private readonly _mouseStateService: IMouseStateService,
@IRenderService private readonly _renderService: IRenderService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
) {
@@ -434,6 +435,10 @@ export class SelectionService extends Disposable implements ISelectionService {
* @param event The mouse event.
*/
public shouldForceSelection(event: MouseEvent): boolean {
if (this._optionsService.rawOptions.altClickForMouseEvents && this._mouseStateService.areMouseEventsActive) {
return !event.altKey;
}
if (Browser.isMac) {
return event.altKey && this._optionsService.rawOptions.macOptionClickForcesSelection;
}
@@ -458,6 +463,10 @@ export class SelectionService extends Disposable implements ISelectionService {
return;
}
if (this._optionsService.rawOptions.altClickForMouseEvents && this._mouseStateService.areMouseEventsActive && event.altKey) {
return;
}
// Allow selection when using a specific modifier key, even when disabled
if (!this._enabled) {
if (!this.shouldForceSelection(event)) {
@@ -596,6 +605,9 @@ export class SelectionService extends Disposable implements ISelectionService {
* @param event the mouse or keyboard event
*/
public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean {
if (this._optionsService.rawOptions.altClickForMouseEvents && this._mouseStateService.areMouseEventsActive) {
return false;
}
return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionClickForcesSelection);
}
+1
View File
@@ -52,6 +52,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
windowOptions: {},
windowsPty: {},
wordSeparator: ' ()[]{}\',"`',
altClickForMouseEvents: false,
altClickMovesCursor: true,
convertEol: false,
termName: 'xterm',
+1
View File
@@ -209,6 +209,7 @@ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'off';
export interface ITerminalOptions {
allowProposedApi?: boolean;
allowTransparency?: boolean;
altClickForMouseEvents?: boolean;
altClickMovesCursor?: boolean;
cols?: number;
convertEol?: boolean;
+10
View File
@@ -38,6 +38,16 @@ declare module '@xterm/headless' {
*/
altClickMovesCursor?: boolean;
/**
* When enabled and the terminal is in mouse events mode, mouse click, drag,
* and move events are only sent to the underlying application when the alt
* key is held. Wheel events are not affected. This allows normal text
* selection by default while still supporting application mouse interaction
* and scrolling when holding alt. When enabled, this takes precedence over
* `macOptionClickForcesSelection`.
*/
altClickForMouseEvents?: boolean;
/**
* When enabled the cursor will be set to the beginning of the next line
* with every new line. This is equivalent to sending '\r\n' for each '\n'.
+10
View File
@@ -45,6 +45,16 @@ declare module '@xterm/xterm' {
*/
altClickMovesCursor?: boolean;
/**
* When enabled and the terminal is in mouse events mode, mouse click, drag,
* and move events are only sent to the underlying application when the alt
* key is held. Wheel events are not affected. This allows normal text
* selection by default while still supporting application mouse interaction
* and scrolling when holding alt. When enabled, this takes precedence over
* `macOptionClickForcesSelection`.
*/
altClickForMouseEvents?: boolean;
/**
* When enabled the cursor will be set to the beginning of the next line
* with every new line. This is equivalent to sending `\r\n` for each `\n`.