@xterm/xterm 6.1.0-beta.183 → 6.1.0-beta.184

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xterm/xterm",
3
3
  "description": "Full xterm terminal, in your browser",
4
- "version": "6.1.0-beta.183",
4
+ "version": "6.1.0-beta.184",
5
5
  "main": "lib/xterm.js",
6
6
  "module": "lib/xterm.mjs",
7
7
  "style": "css/xterm.css",
@@ -119,5 +119,5 @@
119
119
  "ws": "^8.2.3",
120
120
  "xterm-benchmark": "^0.3.1"
121
121
  },
122
- "commit": "73adfdfd56b411b6abcc162ba48cb9fbd071aaee"
122
+ "commit": "1400398abbba1c6849eff034d82da556ea7753d1"
123
123
  }
@@ -36,16 +36,17 @@ import { CharSizeService } from 'browser/services/CharSizeService';
36
36
  import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
37
37
  import { CoreBrowserService } from 'browser/services/CoreBrowserService';
38
38
  import { LinkProviderService } from 'browser/services/LinkProviderService';
39
+ import { MouseCoordsService } from 'browser/services/MouseCoordsService';
39
40
  import { MouseService } from 'browser/services/MouseService';
40
41
  import { RenderService } from 'browser/services/RenderService';
41
42
  import { SelectionService } from 'browser/services/SelectionService';
42
- import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
43
+ import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseCoordsService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
43
44
  import { ThemeService } from 'browser/services/ThemeService';
44
45
  import { KeyboardService } from 'browser/services/KeyboardService';
45
46
  import { channels, color, rgb } from 'common/Color';
46
47
  import { CoreTerminal } from 'common/CoreTerminal';
47
48
  import * as Browser from 'common/Platform';
48
- import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
49
+ import { ColorRequestType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
49
50
  import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
50
51
  import { IBuffer } from 'common/buffer/Types';
51
52
  import { C0, C1ESCAPED } from 'common/data/EscapeSequences';
@@ -87,6 +88,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
87
88
  // Optional browser services
88
89
  private _charSizeService: ICharSizeService | undefined;
89
90
  private _coreBrowserService: ICoreBrowserService | undefined;
91
+ private _mouseCoordsService: IMouseCoordsService | undefined;
90
92
  private _mouseService: IMouseService | undefined;
91
93
  private _renderService: IRenderService | undefined;
92
94
  private _themeService: IThemeService | undefined;
@@ -543,8 +545,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
543
545
  this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
544
546
  this._helperContainer.appendChild(this._compositionView);
545
547
 
546
- this._mouseService = this._instantiationService.createInstance(MouseService);
547
- this._instantiationService.setService(IMouseService, this._mouseService);
548
+ this._mouseCoordsService = this._instantiationService.createInstance(MouseCoordsService);
549
+ this._instantiationService.setService(IMouseCoordsService, this._mouseCoordsService);
548
550
 
549
551
  const linkifier = this._linkifier.value = this._register(this._instantiationService.createInstance(Linkifier, this.screenElement));
550
552
 
@@ -579,6 +581,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
579
581
  linkifier
580
582
  ));
581
583
  this._instantiationService.setService(ISelectionService, this._selectionService);
584
+ this._mouseService = this._instantiationService.createInstance(MouseService);
585
+ this._mouseService.setCustomWheelEventHandler(this._customWheelEventHandler);
586
+ this._instantiationService.setService(IMouseService, this._mouseService);
582
587
  this._register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
583
588
  this._register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
584
589
  this._register(this._selectionService.onRequestRedraw(e => this._renderService!.handleSelectionChanged(e.start, e.end, e.columnSelectMode)));
@@ -638,285 +643,17 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
638
643
 
639
644
  // Listen for mouse events and translate
640
645
  // them into terminal mouse protocols.
641
- this.bindMouse();
646
+ this._mouseService.bindMouse({
647
+ element: this.element!,
648
+ screenElement: this.screenElement!,
649
+ document: this._document!
650
+ }, disposable => this._register(disposable), () => this.focus());
642
651
  }
643
652
 
644
653
  private _createRenderer(): IRenderer {
645
654
  return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier!);
646
655
  }
647
656
 
648
- /**
649
- * Bind certain mouse events to the terminal.
650
- * By default only 3 button + wheel up/down is ativated. For higher buttons
651
- * no mouse report will be created. Typically the standard actions will be active.
652
- *
653
- * There are several reasons not to enable support for higher buttons/wheel:
654
- * - Button 4 and 5 are typically used for history back and forward navigation,
655
- * there is no straight forward way to supress/intercept those standard actions.
656
- * - Support for higher buttons does not work in some platform/browser combinations.
657
- * - Left/right wheel was not tested.
658
- * - Emulators vary in mouse button support, typically only 3 buttons and
659
- * wheel up/down work reliable.
660
- *
661
- * TODO: Move mouse event code into its own file.
662
- */
663
- public bindMouse(): void {
664
- const self = this;
665
- const el = this.element!;
666
-
667
- // send event to CoreMouseService
668
- function sendEvent(ev: MouseEvent | WheelEvent): boolean {
669
- // Get mouse coordinates
670
- const pos = self._mouseService?.getMouseReportCoords(ev, self.screenElement!);
671
- if (!pos) {
672
- return false;
673
- }
674
-
675
- let but: CoreMouseButton;
676
- let action: CoreMouseAction | undefined;
677
- switch ((ev as any).overrideType || ev.type) {
678
- case 'mousemove':
679
- action = CoreMouseAction.MOVE;
680
- if (ev.buttons === undefined) {
681
- // buttons is not supported on macOS, try to get a value from button instead
682
- but = CoreMouseButton.NONE;
683
- if (ev.button !== undefined) {
684
- but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
685
- }
686
- } else {
687
- // according to MDN buttons only reports up to button 5 (AUX2)
688
- but = ev.buttons & 1 ? CoreMouseButton.LEFT :
689
- ev.buttons & 4 ? CoreMouseButton.MIDDLE :
690
- ev.buttons & 2 ? CoreMouseButton.RIGHT :
691
- CoreMouseButton.NONE; // fallback to NONE
692
- }
693
- break;
694
- case 'mouseup':
695
- action = CoreMouseAction.UP;
696
- but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
697
- break;
698
- case 'mousedown':
699
- action = CoreMouseAction.DOWN;
700
- but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
701
- break;
702
- case 'wheel':
703
- if (self._customWheelEventHandler && self._customWheelEventHandler(ev as WheelEvent) === false) {
704
- return false;
705
- }
706
- const deltaY = (ev as WheelEvent).deltaY;
707
- if (deltaY === 0) {
708
- return false;
709
- }
710
- const lines = self.coreMouseService.consumeWheelEvent(
711
- ev as WheelEvent,
712
- self._renderService?.dimensions?.device?.cell?.height,
713
- self._coreBrowserService?.dpr
714
- );
715
- if (lines === 0) {
716
- return false;
717
- }
718
- action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
719
- but = CoreMouseButton.WHEEL;
720
- break;
721
- default:
722
- // dont handle other event types by accident
723
- return false;
724
- }
725
-
726
- // exit if we cannot determine valid button/action values
727
- // do nothing for higher buttons than wheel
728
- if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) {
729
- return false;
730
- }
731
-
732
- return self.coreMouseService.triggerMouseEvent({
733
- col: pos.col,
734
- row: pos.row,
735
- x: pos.x,
736
- y: pos.y,
737
- button: but,
738
- action,
739
- ctrl: ev.ctrlKey,
740
- alt: ev.altKey,
741
- shift: ev.shiftKey
742
- });
743
- }
744
-
745
- /**
746
- * Event listener state handling.
747
- * We listen to the onProtocolChange event of CoreMouseService and put
748
- * requested listeners in `requestedEvents`. With this the listeners
749
- * have all bits to do the event listener juggling.
750
- * Note: 'mousedown' currently is "always on" and not managed
751
- * by onProtocolChange.
752
- */
753
- const requestedEvents: { [key: string]: ((ev: MouseEvent | WheelEvent) => void) | null } = {
754
- mouseup: null,
755
- wheel: null,
756
- mousedrag: null,
757
- mousemove: null
758
- };
759
- const eventListeners: { [key: string]: (ev: any) => void | boolean } = {
760
- mouseup: (ev: MouseEvent) => {
761
- sendEvent(ev);
762
- if (!ev.buttons) {
763
- // if no other button is held remove global handlers
764
- this._document!.removeEventListener('mouseup', requestedEvents.mouseup!);
765
- if (requestedEvents.mousedrag) {
766
- this._document!.removeEventListener('mousemove', requestedEvents.mousedrag);
767
- }
768
- }
769
- },
770
- wheel: (ev: WheelEvent) => {
771
- sendEvent(ev);
772
- ev.preventDefault();
773
- ev.stopPropagation();
774
- return false;
775
- },
776
- mousedrag: (ev: MouseEvent) => {
777
- // deal only with move while a button is held
778
- if (ev.buttons) {
779
- sendEvent(ev);
780
- }
781
- },
782
- mousemove: (ev: MouseEvent) => {
783
- // deal only with move without any button
784
- if (!ev.buttons) {
785
- sendEvent(ev);
786
- }
787
- }
788
- };
789
- this._register(this.coreMouseService.onProtocolChange(events => {
790
- // apply global changes on events
791
- if (events) {
792
- if (this.optionsService.rawOptions.logLevel === 'debug') {
793
- this._logService.debug('Binding to mouse events:', this.coreMouseService.explainEvents(events));
794
- }
795
- this.element!.classList.add('enable-mouse-events');
796
- this._selectionService!.disable();
797
- } else {
798
- this._logService.debug('Unbinding from mouse events.');
799
- this.element!.classList.remove('enable-mouse-events');
800
- this._selectionService!.enable();
801
- }
802
-
803
- // add/remove handlers from requestedEvents
804
-
805
- if (!(events & CoreMouseEventType.MOVE)) {
806
- el.removeEventListener('mousemove', requestedEvents.mousemove!);
807
- requestedEvents.mousemove = null;
808
- } else if (!requestedEvents.mousemove) {
809
- el.addEventListener('mousemove', eventListeners.mousemove);
810
- requestedEvents.mousemove = eventListeners.mousemove;
811
- }
812
-
813
- if (!(events & CoreMouseEventType.WHEEL)) {
814
- el.removeEventListener('wheel', requestedEvents.wheel!);
815
- requestedEvents.wheel = null;
816
- } else if (!requestedEvents.wheel) {
817
- el.addEventListener('wheel', eventListeners.wheel, { passive: false });
818
- requestedEvents.wheel = eventListeners.wheel;
819
- }
820
-
821
- if (!(events & CoreMouseEventType.UP)) {
822
- this._document!.removeEventListener('mouseup', requestedEvents.mouseup!);
823
- requestedEvents.mouseup = null;
824
- } else {
825
- requestedEvents.mouseup ??= eventListeners.mouseup;
826
- }
827
-
828
- if (!(events & CoreMouseEventType.DRAG)) {
829
- this._document!.removeEventListener('mousemove', requestedEvents.mousedrag!);
830
- requestedEvents.mousedrag = null;
831
- } else {
832
- requestedEvents.mousedrag ??= eventListeners.mousedrag;
833
- }
834
- }));
835
- // force initial onProtocolChange so we dont miss early mouse requests
836
- this.coreMouseService.activeProtocol = this.coreMouseService.activeProtocol;
837
-
838
- // Ensure document-level listeners are removed on dispose
839
- this._register(toDisposable(() => {
840
- if (requestedEvents.mouseup) {
841
- this._document!.removeEventListener('mouseup', requestedEvents.mouseup);
842
- }
843
- if (requestedEvents.mousedrag) {
844
- this._document!.removeEventListener('mousemove', requestedEvents.mousedrag);
845
- }
846
- }));
847
-
848
- /**
849
- * "Always on" event listeners.
850
- */
851
- this._register(addDisposableListener(el, 'mousedown', (ev: MouseEvent) => {
852
- ev.preventDefault();
853
- this.focus();
854
-
855
- // Don't send the mouse button to the pty if mouse events are disabled or
856
- // if the selection manager is having selection forced (ie. a modifier is
857
- // held).
858
- if (!this.coreMouseService.areMouseEventsActive || this._selectionService!.shouldForceSelection(ev)) {
859
- return;
860
- }
861
-
862
- sendEvent(ev);
863
-
864
- // Register additional global handlers which should keep reporting outside
865
- // of the terminal element.
866
- // Note: Other emulators also do this for 'mousedown' while a button
867
- // is held, we currently limit 'mousedown' to the terminal only.
868
- if (requestedEvents.mouseup) {
869
- this._document!.addEventListener('mouseup', requestedEvents.mouseup);
870
- }
871
- if (requestedEvents.mousedrag) {
872
- this._document!.addEventListener('mousemove', requestedEvents.mousedrag);
873
- }
874
- }));
875
-
876
- this._register(addDisposableListener(el, 'wheel', (ev: WheelEvent) => {
877
- // do nothing, if app side handles wheel itself
878
- if (requestedEvents.wheel) return;
879
-
880
- if (this._customWheelEventHandler && this._customWheelEventHandler(ev) === false) {
881
- return false;
882
- }
883
-
884
- if (!this.buffer.hasScrollback) {
885
- // Convert wheel events into up/down events when the buffer does not have scrollback, this
886
- // enables scrolling in apps hosted in the alt buffer such as vim or tmux even when mouse
887
- // events are not enabled.
888
- // This used implementation used get the actual lines/partial lines scrolled from the
889
- // viewport but since moving to the new viewport implementation has been simplified to
890
- // simply send a single up or down sequence.
891
-
892
- // Do nothing if there's no vertical scroll
893
- const deltaY = (ev as WheelEvent).deltaY;
894
- if (deltaY === 0) {
895
- return false;
896
- }
897
-
898
- const lines = self.coreMouseService.consumeWheelEvent(
899
- ev as WheelEvent,
900
- self._renderService?.dimensions?.device?.cell?.height,
901
- self._coreBrowserService?.dpr
902
- );
903
- if (lines === 0) {
904
- ev.preventDefault();
905
- ev.stopPropagation();
906
- return false;
907
- }
908
-
909
- // Construct and send sequences
910
- const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
911
- this.coreService.triggerDataEvent(sequence, true);
912
- ev.preventDefault();
913
- ev.stopPropagation();
914
- return false;
915
- }
916
- }, { passive: false }));
917
- }
918
-
919
-
920
657
  /**
921
658
  * Tells the renderer to refresh terminal content between two rows (inclusive) at the next
922
659
  * opportunity.
@@ -991,6 +728,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
991
728
 
992
729
  public attachCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler): void {
993
730
  this._customWheelEventHandler = customWheelEventHandler;
731
+ this._mouseService?.setCustomWheelEventHandler(customWheelEventHandler);
994
732
  }
995
733
 
996
734
  public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
@@ -7,7 +7,7 @@ import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifie
7
7
  import { Disposable, dispose, toDisposable } from 'common/Lifecycle';
8
8
  import { IDisposable } from 'common/Types';
9
9
  import { IBufferService } from 'common/services/Services';
10
- import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
10
+ import { ILinkProviderService, IMouseCoordsService, IRenderService } from './services/Services';
11
11
  import { Emitter } from 'common/Event';
12
12
  import { addDisposableListener } from 'browser/Dom';
13
13
 
@@ -30,7 +30,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
30
30
 
31
31
  constructor(
32
32
  private readonly _element: HTMLElement,
33
- @IMouseService private readonly _mouseService: IMouseService,
33
+ @IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
34
34
  @IRenderService private readonly _renderService: IRenderService,
35
35
  @IBufferService private readonly _bufferService: IBufferService,
36
36
  @ILinkProviderService private readonly _linkProviderService: ILinkProviderService
@@ -60,7 +60,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
60
60
  private _handleMouseMove(event: MouseEvent): void {
61
61
  this._lastMouseEvent = event;
62
62
 
63
- const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
63
+ const position = this._positionFromMouseEvent(event, this._element);
64
64
  if (!position) {
65
65
  return;
66
66
  }
@@ -222,7 +222,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
222
222
  return;
223
223
  }
224
224
 
225
- const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
225
+ const position = this._positionFromMouseEvent(event, this._element);
226
226
  if (!position) {
227
227
  return;
228
228
  }
@@ -251,7 +251,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
251
251
  return;
252
252
  }
253
253
 
254
- const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService);
254
+ const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element);
255
255
 
256
256
  if (!position) {
257
257
  return;
@@ -312,7 +312,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
312
312
  this._clearCurrentLink(start, end);
313
313
  if (this._lastMouseEvent) {
314
314
  // re-eval previously active link after changes
315
- const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
315
+ const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element);
316
316
  if (position) {
317
317
  this._askForLink(position, false);
318
318
  }
@@ -378,8 +378,8 @@ export class Linkifier extends Disposable implements ILinkifier2 {
378
378
  * Get the buffer position from a mouse event
379
379
  * @param event
380
380
  */
381
- private _positionFromMouseEvent(event: MouseEvent, element: HTMLElement, mouseService: IMouseService): IBufferCellPosition | undefined {
382
- const coords = mouseService.getCoords(event, element, this._bufferService.cols, this._bufferService.rows);
381
+ private _positionFromMouseEvent(event: MouseEvent, element: HTMLElement): IBufferCellPosition | undefined {
382
+ const coords = this._mouseCoordsService.getCoords(event, element, this._bufferService.cols, this._bufferService.rows);
383
383
  if (!coords) {
384
384
  return;
385
385
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Copyright (c) 2026 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import { getWindow } from 'browser/Dom';
7
+ import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse';
8
+ import { ICharSizeService, IMouseCoordsService, IRenderService } from 'browser/services/Services';
9
+
10
+ export class MouseCoordsService implements IMouseCoordsService {
11
+ public serviceBrand: undefined;
12
+
13
+ constructor(
14
+ @ICharSizeService private readonly _charSizeService: ICharSizeService,
15
+ @IRenderService private readonly _renderService: IRenderService
16
+ ) {
17
+ }
18
+
19
+ public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
20
+ return getCoords(
21
+ window,
22
+ event,
23
+ element,
24
+ colCount,
25
+ rowCount,
26
+ this._charSizeService.hasValidSize,
27
+ this._renderService.dimensions.css.cell.width,
28
+ this._renderService.dimensions.css.cell.height,
29
+ isSelection
30
+ );
31
+ }
32
+
33
+ public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
34
+ const coords = getCoordsRelativeToElement(getWindow(element), event, element);
35
+ if (!this._charSizeService.hasValidSize) {
36
+ return undefined;
37
+ }
38
+ coords[0] = Math.min(Math.max(coords[0], 0), this._renderService.dimensions.css.canvas.width - 1);
39
+ coords[1] = Math.min(Math.max(coords[1], 0), this._renderService.dimensions.css.canvas.height - 1);
40
+ return {
41
+ col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width),
42
+ row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height),
43
+ x: Math.floor(coords[0]),
44
+ y: Math.floor(coords[1])
45
+ };
46
+ }
47
+ }