@xterm/xterm 6.1.0-beta.19 → 6.1.0-beta.190

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.
Files changed (157) hide show
  1. package/README.md +60 -38
  2. package/css/xterm.css +29 -22
  3. package/lib/xterm.js +1 -1
  4. package/lib/xterm.js.map +1 -1
  5. package/lib/xterm.mjs +8 -34
  6. package/lib/xterm.mjs.map +4 -4
  7. package/package.json +25 -14
  8. package/src/browser/AccessibilityManager.ts +6 -3
  9. package/src/browser/Clipboard.ts +6 -3
  10. package/src/browser/CoreBrowserTerminal.ts +147 -318
  11. package/src/browser/Dom.ts +178 -0
  12. package/src/browser/Linkifier.ts +11 -11
  13. package/src/browser/OscLinkProvider.ts +3 -1
  14. package/src/browser/RenderDebouncer.ts +2 -2
  15. package/src/browser/TimeBasedDebouncer.ts +2 -2
  16. package/src/browser/Types.ts +12 -11
  17. package/src/browser/Viewport.ts +37 -20
  18. package/src/browser/decorations/BufferDecorationRenderer.ts +1 -1
  19. package/src/browser/decorations/OverviewRulerRenderer.ts +15 -16
  20. package/src/browser/input/CompositionHelper.ts +44 -8
  21. package/src/browser/public/Terminal.ts +25 -28
  22. package/src/browser/renderer/dom/DomRenderer.ts +128 -8
  23. package/src/browser/renderer/dom/DomRendererRowFactory.ts +19 -13
  24. package/src/browser/renderer/dom/WidthCache.ts +54 -52
  25. package/src/browser/renderer/shared/Constants.ts +7 -0
  26. package/src/browser/renderer/shared/TextBlinkStateManager.ts +97 -0
  27. package/src/browser/renderer/shared/Types.ts +8 -2
  28. package/src/browser/scrollable/abstractScrollbar.ts +300 -0
  29. package/src/browser/scrollable/fastDomNode.ts +126 -0
  30. package/src/browser/scrollable/globalPointerMoveMonitor.ts +90 -0
  31. package/src/browser/scrollable/horizontalScrollbar.ts +85 -0
  32. package/src/browser/scrollable/mouseEvent.ts +292 -0
  33. package/src/browser/scrollable/scrollable.ts +486 -0
  34. package/src/browser/scrollable/scrollableElement.ts +579 -0
  35. package/src/browser/scrollable/scrollableElementOptions.ts +161 -0
  36. package/src/browser/scrollable/scrollbarArrow.ts +110 -0
  37. package/src/browser/scrollable/scrollbarState.ts +246 -0
  38. package/src/browser/scrollable/scrollbarVisibilityController.ts +113 -0
  39. package/src/browser/scrollable/touch.ts +485 -0
  40. package/src/browser/scrollable/verticalScrollbar.ts +143 -0
  41. package/src/browser/scrollable/widget.ts +23 -0
  42. package/src/browser/services/CharSizeService.ts +2 -2
  43. package/src/browser/services/CoreBrowserService.ts +7 -5
  44. package/src/browser/services/KeyboardService.ts +67 -0
  45. package/src/browser/services/LinkProviderService.ts +1 -1
  46. package/src/browser/services/MouseCoordsService.ts +47 -0
  47. package/src/browser/services/MouseService.ts +518 -25
  48. package/src/browser/services/RenderService.ts +22 -15
  49. package/src/browser/services/SelectionService.ts +16 -8
  50. package/src/browser/services/Services.ts +40 -17
  51. package/src/browser/services/ThemeService.ts +2 -2
  52. package/src/common/Async.ts +105 -0
  53. package/src/common/CircularList.ts +2 -2
  54. package/src/common/Color.ts +8 -0
  55. package/src/common/CoreTerminal.ts +28 -18
  56. package/src/common/Event.ts +118 -0
  57. package/src/common/InputHandler.ts +256 -36
  58. package/src/common/Lifecycle.ts +113 -0
  59. package/src/common/Platform.ts +13 -3
  60. package/src/common/SortedList.ts +7 -3
  61. package/src/common/TaskQueue.ts +9 -3
  62. package/src/common/Types.ts +35 -15
  63. package/src/common/Version.ts +9 -0
  64. package/src/common/buffer/Buffer.ts +20 -14
  65. package/src/common/buffer/BufferLine.ts +4 -5
  66. package/src/common/buffer/BufferSet.ts +7 -6
  67. package/src/common/buffer/CellData.ts +57 -0
  68. package/src/common/buffer/Marker.ts +2 -2
  69. package/src/common/buffer/Types.ts +6 -2
  70. package/src/common/data/EscapeSequences.ts +71 -70
  71. package/src/common/input/Keyboard.ts +14 -7
  72. package/src/common/input/KittyKeyboard.ts +496 -0
  73. package/src/common/input/Win32InputMode.ts +297 -0
  74. package/src/common/input/WriteBuffer.ts +34 -2
  75. package/src/common/input/XParseColor.ts +2 -2
  76. package/src/common/parser/ApcParser.ts +245 -0
  77. package/src/common/parser/Constants.ts +22 -4
  78. package/src/common/parser/DcsParser.ts +5 -5
  79. package/src/common/parser/EscapeSequenceParser.ts +75 -22
  80. package/src/common/parser/OscParser.ts +5 -5
  81. package/src/common/parser/Types.ts +34 -1
  82. package/src/common/public/BufferLineApiView.ts +2 -2
  83. package/src/common/public/BufferNamespaceApi.ts +2 -2
  84. package/src/common/public/ParserApi.ts +3 -0
  85. package/src/common/services/BufferService.ts +8 -5
  86. package/src/common/services/CharsetService.ts +4 -0
  87. package/src/common/services/CoreService.ts +18 -4
  88. package/src/common/services/DecorationService.ts +24 -8
  89. package/src/common/services/LogService.ts +1 -31
  90. package/src/common/services/{CoreMouseService.ts → MouseStateService.ts} +21 -132
  91. package/src/common/services/OptionsService.ts +13 -4
  92. package/src/common/services/Services.ts +47 -40
  93. package/src/common/services/UnicodeService.ts +1 -1
  94. package/typings/xterm.d.ts +319 -35
  95. package/src/common/TypedArrayUtils.ts +0 -17
  96. package/src/vs/base/browser/browser.ts +0 -141
  97. package/src/vs/base/browser/canIUse.ts +0 -49
  98. package/src/vs/base/browser/dom.ts +0 -2369
  99. package/src/vs/base/browser/fastDomNode.ts +0 -316
  100. package/src/vs/base/browser/globalPointerMoveMonitor.ts +0 -112
  101. package/src/vs/base/browser/iframe.ts +0 -135
  102. package/src/vs/base/browser/keyboardEvent.ts +0 -213
  103. package/src/vs/base/browser/mouseEvent.ts +0 -229
  104. package/src/vs/base/browser/touch.ts +0 -372
  105. package/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts +0 -303
  106. package/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts +0 -114
  107. package/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +0 -720
  108. package/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts +0 -165
  109. package/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts +0 -114
  110. package/src/vs/base/browser/ui/scrollbar/scrollbarState.ts +0 -243
  111. package/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts +0 -118
  112. package/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts +0 -116
  113. package/src/vs/base/browser/ui/widget.ts +0 -57
  114. package/src/vs/base/browser/window.ts +0 -14
  115. package/src/vs/base/common/arrays.ts +0 -887
  116. package/src/vs/base/common/arraysFind.ts +0 -202
  117. package/src/vs/base/common/assert.ts +0 -71
  118. package/src/vs/base/common/async.ts +0 -1992
  119. package/src/vs/base/common/cancellation.ts +0 -148
  120. package/src/vs/base/common/charCode.ts +0 -450
  121. package/src/vs/base/common/collections.ts +0 -140
  122. package/src/vs/base/common/decorators.ts +0 -130
  123. package/src/vs/base/common/equals.ts +0 -146
  124. package/src/vs/base/common/errors.ts +0 -303
  125. package/src/vs/base/common/event.ts +0 -1778
  126. package/src/vs/base/common/functional.ts +0 -32
  127. package/src/vs/base/common/hash.ts +0 -316
  128. package/src/vs/base/common/iterator.ts +0 -159
  129. package/src/vs/base/common/keyCodes.ts +0 -526
  130. package/src/vs/base/common/keybindings.ts +0 -284
  131. package/src/vs/base/common/lazy.ts +0 -47
  132. package/src/vs/base/common/lifecycle.ts +0 -801
  133. package/src/vs/base/common/linkedList.ts +0 -142
  134. package/src/vs/base/common/map.ts +0 -202
  135. package/src/vs/base/common/numbers.ts +0 -98
  136. package/src/vs/base/common/observable.ts +0 -76
  137. package/src/vs/base/common/observableInternal/api.ts +0 -31
  138. package/src/vs/base/common/observableInternal/autorun.ts +0 -281
  139. package/src/vs/base/common/observableInternal/base.ts +0 -489
  140. package/src/vs/base/common/observableInternal/debugName.ts +0 -145
  141. package/src/vs/base/common/observableInternal/derived.ts +0 -428
  142. package/src/vs/base/common/observableInternal/lazyObservableValue.ts +0 -146
  143. package/src/vs/base/common/observableInternal/logging.ts +0 -328
  144. package/src/vs/base/common/observableInternal/promise.ts +0 -209
  145. package/src/vs/base/common/observableInternal/utils.ts +0 -610
  146. package/src/vs/base/common/platform.ts +0 -281
  147. package/src/vs/base/common/scrollable.ts +0 -522
  148. package/src/vs/base/common/sequence.ts +0 -34
  149. package/src/vs/base/common/stopwatch.ts +0 -43
  150. package/src/vs/base/common/strings.ts +0 -557
  151. package/src/vs/base/common/symbols.ts +0 -9
  152. package/src/vs/base/common/uint.ts +0 -59
  153. package/src/vs/patches/nls.ts +0 -90
  154. package/src/vs/typings/base-common.d.ts +0 -20
  155. package/src/vs/typings/require.d.ts +0 -42
  156. package/src/vs/typings/vscode-globals-nls.d.ts +0 -36
  157. package/src/vs/typings/vscode-globals-product.d.ts +0 -33
@@ -30,6 +30,12 @@ export class CompositionHelper {
30
30
  */
31
31
  private _compositionPosition: IPosition;
32
32
 
33
+ /**
34
+ * Text that existed after the composing range when composition started.
35
+ * This is used to avoid treating existing trailing text as new input.
36
+ */
37
+ private _compositionSuffix: string;
38
+
33
39
  /**
34
40
  * Whether a composition is in the process of being sent, setting this to false will cancel any
35
41
  * in-progress composition.
@@ -41,6 +47,11 @@ export class CompositionHelper {
41
47
  */
42
48
  private _dataAlreadySent: string;
43
49
 
50
+ /**
51
+ * The pending textarea change timer, if any.
52
+ */
53
+ private _textareaChangeTimer?: number;
54
+
44
55
  constructor(
45
56
  private readonly _textarea: HTMLTextAreaElement,
46
57
  private readonly _compositionView: HTMLElement,
@@ -52,6 +63,7 @@ export class CompositionHelper {
52
63
  this._isComposing = false;
53
64
  this._isSendingComposition = false;
54
65
  this._compositionPosition = { start: 0, end: 0 };
66
+ this._compositionSuffix = '';
55
67
  this._dataAlreadySent = '';
56
68
  }
57
69
 
@@ -60,7 +72,13 @@ export class CompositionHelper {
60
72
  */
61
73
  public compositionstart(): void {
62
74
  this._isComposing = true;
63
- this._compositionPosition.start = this._textarea.value.length;
75
+ // It's important to use the selection here instead of textarea length to avoid conflicts with
76
+ // screen reader mode
77
+ const start = this._textarea.selectionStart ?? this._textarea.value.length;
78
+ const end = this._textarea.selectionEnd ?? start;
79
+ this._compositionPosition.start = Math.min(start, end);
80
+ this._compositionPosition.end = Math.max(start, end);
81
+ this._compositionSuffix = this._textarea.value.substring(this._compositionPosition.end);
64
82
  this._compositionView.textContent = '';
65
83
  this._dataAlreadySent = '';
66
84
  this._compositionView.classList.add('active');
@@ -71,10 +89,13 @@ export class CompositionHelper {
71
89
  * @param ev The event.
72
90
  */
73
91
  public compositionupdate(ev: Pick<CompositionEvent, 'data'>): void {
74
- this._compositionView.textContent = ev.data;
92
+ // Mark text as LTR, direction=rtl is used in CSS so the end of the text is followed for long
93
+ // compositions
94
+ this._compositionView.textContent = `\u200E${ev.data}\u200E`;
75
95
  this.updateCompositionElements();
76
96
  setTimeout(() => {
77
- this._compositionPosition.end = this._textarea.value.length;
97
+ const end = this._textarea.selectionEnd ?? this._textarea.value.length;
98
+ this._compositionPosition.end = Math.max( this._compositionPosition.start, end);
78
99
  }, 0);
79
100
  }
80
101
 
@@ -141,6 +162,7 @@ export class CompositionHelper {
141
162
  start: this._compositionPosition.start,
142
163
  end: this._compositionPosition.end
143
164
  };
165
+ const currentCompositionSuffix = this._compositionSuffix;
144
166
 
145
167
  // Since composition* events happen before the changes take place in the textarea on most
146
168
  // browsers, use a setTimeout with 0ms time to allow the native compositionend event to
@@ -164,10 +186,14 @@ export class CompositionHelper {
164
186
  // if a new composition has started.
165
187
  input = this._textarea.value.substring(currentCompositionPosition.start, this._compositionPosition.start);
166
188
  } else {
167
- // Don't use the end position here in order to pick up any characters after the
168
- // composition has finished, for example when typing a non-composition character
169
- // (eg. 2) after a composition character.
170
- input = this._textarea.value.substring(currentCompositionPosition.start);
189
+ // Keep support for non-composition characters typed immediately after composition end
190
+ // while avoiding re-sending the trailing text that was already present
191
+ // before composition started.
192
+ const value = this._textarea.value;
193
+ const valueEnd = currentCompositionSuffix.length > 0 && value.endsWith(currentCompositionSuffix)
194
+ ? value.length - currentCompositionSuffix.length
195
+ : value.length;
196
+ input = value.substring(currentCompositionPosition.start, Math.max(currentCompositionPosition.start, valueEnd));
171
197
  }
172
198
  if (input.length > 0) {
173
199
  this._coreService.triggerDataEvent(input, true);
@@ -184,8 +210,12 @@ export class CompositionHelper {
184
210
  * IME is active.
185
211
  */
186
212
  private _handleAnyTextareaChanges(): void {
213
+ if (this._textareaChangeTimer) {
214
+ return;
215
+ }
187
216
  const oldValue = this._textarea.value;
188
- setTimeout(() => {
217
+ this._textareaChangeTimer = window.setTimeout(() => {
218
+ this._textareaChangeTimer = undefined;
189
219
  // Ignore if a composition has started since the timeout
190
220
  if (!this._isComposing) {
191
221
  const newValue = this._textarea.value;
@@ -230,6 +260,12 @@ export class CompositionHelper {
230
260
  this._compositionView.style.lineHeight = cellHeight + 'px';
231
261
  this._compositionView.style.fontFamily = this._optionsService.rawOptions.fontFamily;
232
262
  this._compositionView.style.fontSize = this._optionsService.rawOptions.fontSize + 'px';
263
+ // Limit the composition view width to the space between the cursor and
264
+ // the terminal's right edge, preventing it from overflowing the terminal.
265
+ const maxWidth = this._bufferService.cols * this._renderService.dimensions.css.cell.width - cursorLeft;
266
+ this._compositionView.style.maxWidth = maxWidth + 'px';
267
+ this._compositionView.style.overflow = 'hidden';
268
+ this._compositionView.style.direction = 'rtl';
233
269
  // Sync the textarea to the exact position of the composition view so the IME knows where the
234
270
  // text is.
235
271
  const compositionViewBounds = this._compositionView.getBoundingClientRect();
@@ -6,14 +6,14 @@
6
6
  import * as Strings from 'browser/LocalizableStrings';
7
7
  import { CoreBrowserTerminal as TerminalCore } from 'browser/CoreBrowserTerminal';
8
8
  import { IBufferRange, ITerminal } from 'browser/Types';
9
- import { Disposable } from 'vs/base/common/lifecycle';
9
+ import { Disposable } from 'common/Lifecycle';
10
10
  import { ITerminalOptions } from 'common/Types';
11
11
  import { AddonManager } from 'common/public/AddonManager';
12
12
  import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi';
13
13
  import { ParserApi } from 'common/public/ParserApi';
14
14
  import { UnicodeApi } from 'common/public/UnicodeApi';
15
- import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling } from '@xterm/xterm';
16
- import type { Event } from 'vs/base/common/event';
15
+ import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, IRenderDimensions, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling } from '@xterm/xterm';
16
+ import type { IEvent } from 'common/Event';
17
17
 
18
18
  /**
19
19
  * The set of options that only have an effect when set in the Terminal constructor.
@@ -68,25 +68,24 @@ export class Terminal extends Disposable implements ITerminalApi {
68
68
  }
69
69
  }
70
70
 
71
- public get onBell(): Event<void> { return this._core.onBell; }
72
- public get onBinary(): Event<string> { return this._core.onBinary; }
73
- public get onCursorMove(): Event<void> { return this._core.onCursorMove; }
74
- public get onData(): Event<string> { return this._core.onData; }
75
- public get onKey(): Event<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
76
- public get onLineFeed(): Event<void> { return this._core.onLineFeed; }
77
- public get onRender(): Event<{ start: number, end: number }> { return this._core.onRender; }
78
- public get onResize(): Event<{ cols: number, rows: number }> { return this._core.onResize; }
79
- public get onScroll(): Event<number> { return this._core.onScroll; }
80
- public get onSelectionChange(): Event<void> { return this._core.onSelectionChange; }
81
- public get onTitleChange(): Event<string> { return this._core.onTitleChange; }
82
- public get onWriteParsed(): Event<void> { return this._core.onWriteParsed; }
71
+ public get onBell(): IEvent<void> { return this._core.onBell; }
72
+ public get onBinary(): IEvent<string> { return this._core.onBinary; }
73
+ public get onCursorMove(): IEvent<void> { return this._core.onCursorMove; }
74
+ public get onData(): IEvent<string> { return this._core.onData; }
75
+ public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
76
+ public get onLineFeed(): IEvent<void> { return this._core.onLineFeed; }
77
+ public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }
78
+ public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
79
+ public get onScroll(): IEvent<number> { return this._core.onScroll; }
80
+ public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
81
+ public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
82
+ public get onWriteParsed(): IEvent<void> { return this._core.onWriteParsed; }
83
+ public get onDimensionsChange(): IEvent<IRenderDimensions> { return this._core.onDimensionsChange; }
83
84
 
84
85
  public get element(): HTMLElement | undefined { return this._core.element; }
86
+ public get screenElement(): HTMLElement | undefined { return this._core.screenElement; }
85
87
  public get parser(): IParser {
86
- if (!this._parser) {
87
- this._parser = new ParserApi(this._core);
88
- }
89
- return this._parser;
88
+ return this._parser ??= new ParserApi(this._core);
90
89
  }
91
90
  public get unicode(): IUnicodeHandling {
92
91
  this._checkProposedApi();
@@ -96,19 +95,15 @@ export class Terminal extends Disposable implements ITerminalApi {
96
95
  public get rows(): number { return this._core.rows; }
97
96
  public get cols(): number { return this._core.cols; }
98
97
  public get buffer(): IBufferNamespaceApi {
99
- if (!this._buffer) {
100
- this._buffer = this._register(new BufferNamespaceApi(this._core));
101
- }
102
- return this._buffer;
98
+ return this._buffer ??= this._register(new BufferNamespaceApi(this._core));
103
99
  }
104
100
  public get markers(): ReadonlyArray<IMarker> {
105
- this._checkProposedApi();
106
101
  return this._core.markers;
107
102
  }
108
103
  public get modes(): IModes {
109
104
  const m = this._core.coreService.decPrivateModes;
110
105
  let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none';
111
- switch (this._core.coreMouseService.activeProtocol) {
106
+ switch (this._core.mouseStateService.activeProtocol) {
112
107
  case 'X10': mouseTrackingMode = 'x10'; break;
113
108
  case 'VT200': mouseTrackingMode = 'vt200'; break;
114
109
  case 'DRAG': mouseTrackingMode = 'drag'; break;
@@ -123,10 +118,15 @@ export class Terminal extends Disposable implements ITerminalApi {
123
118
  originMode: m.origin,
124
119
  reverseWraparoundMode: m.reverseWraparound,
125
120
  sendFocusMode: m.sendFocus,
121
+ showCursor: !this._core.coreService.isCursorHidden,
126
122
  synchronizedOutputMode: m.synchronizedOutput,
123
+ win32InputMode: m.win32InputMode,
127
124
  wraparoundMode: m.wraparound
128
125
  };
129
126
  }
127
+ public get dimensions(): IRenderDimensions | undefined {
128
+ return this._core.dimensions;
129
+ }
130
130
  public get options(): Required<ITerminalOptions> {
131
131
  return this._publicOptions;
132
132
  }
@@ -161,11 +161,9 @@ export class Terminal extends Disposable implements ITerminalApi {
161
161
  return this._core.registerLinkProvider(linkProvider);
162
162
  }
163
163
  public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
164
- this._checkProposedApi();
165
164
  return this._core.registerCharacterJoiner(handler);
166
165
  }
167
166
  public deregisterCharacterJoiner(joinerId: number): void {
168
- this._checkProposedApi();
169
167
  this._core.deregisterCharacterJoiner(joinerId);
170
168
  }
171
169
  public registerMarker(cursorYOffset: number = 0): IMarker {
@@ -173,7 +171,6 @@ export class Terminal extends Disposable implements ITerminalApi {
173
171
  return this._core.registerMarker(cursorYOffset);
174
172
  }
175
173
  public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
176
- this._checkProposedApi();
177
174
  this._verifyPositiveIntegers(decorationOptions.x ?? 0, decorationOptions.width ?? 0, decorationOptions.height ?? 0);
178
175
  return this._core.registerDecoration(decorationOptions);
179
176
  }
@@ -5,16 +5,18 @@
5
5
 
6
6
  import { DomRendererRowFactory, RowCss } from 'browser/renderer/dom/DomRendererRowFactory';
7
7
  import { WidthCache } from 'browser/renderer/dom/WidthCache';
8
- import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
8
+ import { INVERTED_DEFAULT_COLOR, RendererConstants } from 'browser/renderer/shared/Constants';
9
9
  import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
10
10
  import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
11
+ import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager';
11
12
  import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ISelectionRenderModel } from 'browser/renderer/shared/Types';
12
13
  import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
13
14
  import { ILinkifier2, ILinkifierEvent, ITerminal, ReadonlyColorSet } from 'browser/Types';
14
15
  import { color } from 'common/Color';
15
- import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
16
+ import { Disposable, toDisposable } from 'common/Lifecycle';
16
17
  import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from 'common/services/Services';
17
- import { Emitter } from 'vs/base/common/event';
18
+ import { Emitter } from 'common/Event';
19
+ import { addDisposableListener } from 'browser/Dom';
18
20
 
19
21
 
20
22
  const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
@@ -23,6 +25,7 @@ const FG_CLASS_PREFIX = 'xterm-fg-';
23
25
  const BG_CLASS_PREFIX = 'xterm-bg-';
24
26
  const FOCUS_CLASS = 'xterm-focus';
25
27
  const SELECTION_CLASS = 'xterm-selection';
28
+ const CURSOR_BLINK_IDLE_CLASS = 'xterm-cursor-blink-idle';
26
29
 
27
30
  let nextTerminalId = 1;
28
31
 
@@ -42,10 +45,15 @@ export class DomRenderer extends Disposable implements IRenderer {
42
45
  private _selectionContainer: HTMLElement;
43
46
  private _widthCache: WidthCache;
44
47
  private _selectionRenderModel: ISelectionRenderModel = createSelectionRenderModel();
48
+ private _cursorBlinkStateManager: CursorBlinkStateManager;
49
+ private _textBlinkStateManager: TextBlinkStateManager;
50
+ private _rowHasBlinkingCells: boolean[] = [];
51
+ private _rowHasBlinkingCellsCount: number = 0;
45
52
 
46
53
  public dimensions: IRenderDimensions;
47
54
 
48
- public readonly onRequestRedraw = this._register(new Emitter<IRequestRedrawEvent>()).event;
55
+ private readonly _onRequestRedraw = this._register(new Emitter<IRequestRedrawEvent>());
56
+ public readonly onRequestRedraw = this._onRequestRedraw.event;
49
57
 
50
58
  constructor(
51
59
  private readonly _terminal: ITerminal,
@@ -89,6 +97,15 @@ export class DomRenderer extends Disposable implements IRenderer {
89
97
  this._register(this._linkifier2.onShowLinkUnderline(e => this._handleLinkHover(e)));
90
98
  this._register(this._linkifier2.onHideLinkUnderline(e => this._handleLinkLeave(e)));
91
99
 
100
+ this._cursorBlinkStateManager = new CursorBlinkStateManager(this._rowContainer, this._coreBrowserService);
101
+ this._register(addDisposableListener(this._document, 'mousedown', () => this._cursorBlinkStateManager.restartBlinkAnimation()));
102
+ this._register(toDisposable(() => this._cursorBlinkStateManager.dispose()));
103
+ this._textBlinkStateManager = this._register(new TextBlinkStateManager(
104
+ () => this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }),
105
+ this._coreBrowserService,
106
+ this._optionsService
107
+ ));
108
+
92
109
  this._register(toDisposable(() => {
93
110
  this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
94
111
 
@@ -101,7 +118,7 @@ export class DomRenderer extends Disposable implements IRenderer {
101
118
  this._dimensionsStyleElement.remove();
102
119
  }));
103
120
 
104
- this._widthCache = new WidthCache(this._document, this._helperContainer);
121
+ this._widthCache = new WidthCache();
105
122
  this._widthCache.setFont(
106
123
  this._optionsService.rawOptions.fontFamily,
107
124
  this._optionsService.rawOptions.fontSize,
@@ -186,6 +203,9 @@ export class DomRenderer extends Disposable implements IRenderer {
186
203
  `}` +
187
204
  `${this._terminalSelector} span.${RowCss.ITALIC_CLASS} {` +
188
205
  ` font-style: italic;` +
206
+ `}` +
207
+ `${this._terminalSelector} span.${RowCss.BLINK_HIDDEN_CLASS} {` +
208
+ ` visibility: hidden;` +
189
209
  `}`;
190
210
  // Blink animation
191
211
  const blinkAnimationUnderlineId = `blink_underline_${this._terminalClass}`;
@@ -225,6 +245,10 @@ export class DomRenderer extends Disposable implements IRenderer {
225
245
  `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
226
246
  ` animation: ${blinkAnimationBlockId} 1s step-end infinite;` +
227
247
  `}` +
248
+ // Disable cursor blinking when idle
249
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${CURSOR_BLINK_IDLE_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS} {` +
250
+ ` animation: none !important;` +
251
+ `}` +
228
252
  // !important helps fix an issue where the cursor will not render on top of the selection,
229
253
  // however it's very hard to fix this issue and retain the blink animation without the use of
230
254
  // !important. So this edge case fails when cursor blink is on.
@@ -307,10 +331,14 @@ export class DomRenderer extends Disposable implements IRenderer {
307
331
  const row = this._document.createElement('div');
308
332
  this._rowContainer.appendChild(row);
309
333
  this._rowElements.push(row);
334
+ this._rowHasBlinkingCells.push(false);
310
335
  }
311
336
  // Remove excess elements
312
337
  while (this._rowElements.length > rows) {
313
338
  this._rowContainer.removeChild(this._rowElements.pop()!);
339
+ if (this._rowHasBlinkingCells.pop()) {
340
+ this._rowHasBlinkingCellsCount--;
341
+ }
314
342
  }
315
343
  }
316
344
 
@@ -328,14 +356,20 @@ export class DomRenderer extends Disposable implements IRenderer {
328
356
 
329
357
  public handleBlur(): void {
330
358
  this._rowContainer.classList.remove(FOCUS_CLASS);
359
+ this._cursorBlinkStateManager.pause();
331
360
  this.renderRows(0, this._bufferService.rows - 1);
332
361
  }
333
362
 
334
363
  public handleFocus(): void {
335
364
  this._rowContainer.classList.add(FOCUS_CLASS);
365
+ this._cursorBlinkStateManager.resume();
336
366
  this.renderRows(this._bufferService.buffer.y, this._bufferService.buffer.y);
337
367
  }
338
368
 
369
+ public handleViewportVisibilityChange(isVisible: boolean): void {
370
+ this._textBlinkStateManager.setViewportVisible(isVisible);
371
+ }
372
+
339
373
  public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
340
374
  // Remove all selections
341
375
  this._selectionContainer.replaceChildren();
@@ -406,7 +440,8 @@ export class DomRenderer extends Disposable implements IRenderer {
406
440
  }
407
441
 
408
442
  public handleCursorMove(): void {
409
- // No-op, the cursor is drawn when rows are drawn
443
+ // Reset idle timer on cursor movement (which happens on input)
444
+ this._cursorBlinkStateManager.restartBlinkAnimation();
410
445
  }
411
446
 
412
447
  private _handleOptionsChanged(): void {
@@ -436,6 +471,11 @@ export class DomRenderer extends Disposable implements IRenderer {
436
471
  */
437
472
  e.replaceChildren();
438
473
  }
474
+ if (this._rowHasBlinkingCellsCount > 0) {
475
+ this._rowHasBlinkingCells.fill(false);
476
+ this._rowHasBlinkingCellsCount = 0;
477
+ this._textBlinkStateManager.setNeedsBlinkInViewport(false);
478
+ }
439
479
  }
440
480
 
441
481
  public renderRows(start: number, end: number): void {
@@ -445,6 +485,7 @@ export class DomRenderer extends Disposable implements IRenderer {
445
485
  const cursorBlink = this._coreService.decPrivateModes.cursorBlink ?? this._optionsService.rawOptions.cursorBlink;
446
486
  const cursorStyle = this._coreService.decPrivateModes.cursorStyle ?? this._optionsService.rawOptions.cursorStyle;
447
487
  const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle;
488
+ const rowInfo = { hasBlinkingCells: false };
448
489
 
449
490
  for (let y = start; y <= end; y++) {
450
491
  const row = y + buffer.ydisp;
@@ -462,13 +503,17 @@ export class DomRenderer extends Disposable implements IRenderer {
462
503
  cursorInactiveStyle,
463
504
  cursorX,
464
505
  cursorBlink,
506
+ this._textBlinkStateManager.isBlinkOn,
465
507
  this.dimensions.css.cell.width,
466
508
  this._widthCache,
467
509
  -1,
468
- -1
510
+ -1,
511
+ rowInfo
469
512
  )
470
513
  );
514
+ this._setRowBlinkState(y, rowInfo.hasBlinkingCells);
471
515
  }
516
+ this._updateTextBlinkState();
472
517
  }
473
518
 
474
519
  private get _terminalSelector(): string {
@@ -513,6 +558,7 @@ export class DomRenderer extends Disposable implements IRenderer {
513
558
  const cursorBlink = this._optionsService.rawOptions.cursorBlink;
514
559
  const cursorStyle = this._optionsService.rawOptions.cursorStyle;
515
560
  const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle;
561
+ const rowInfo = { hasBlinkingCells: false };
516
562
 
517
563
  // refresh rows within link range
518
564
  for (let i = y; i <= y2; ++i) {
@@ -531,12 +577,86 @@ export class DomRenderer extends Disposable implements IRenderer {
531
577
  cursorInactiveStyle,
532
578
  cursorX,
533
579
  cursorBlink,
580
+ this._textBlinkStateManager.isBlinkOn,
534
581
  this.dimensions.css.cell.width,
535
582
  this._widthCache,
536
583
  enabled ? (i === y ? x : 0) : -1,
537
- enabled ? ((i === y2 ? x2 : cols) - 1) : -1
584
+ enabled ? ((i === y2 ? x2 : cols) - 1) : -1,
585
+ rowInfo
538
586
  )
539
587
  );
588
+ this._setRowBlinkState(i, rowInfo.hasBlinkingCells);
540
589
  }
590
+ this._updateTextBlinkState();
591
+ }
592
+
593
+ private _setRowBlinkState(row: number, hasBlinkingCells: boolean): void {
594
+ const previous = this._rowHasBlinkingCells[row];
595
+ if (previous === hasBlinkingCells) {
596
+ return;
597
+ }
598
+ this._rowHasBlinkingCells[row] = hasBlinkingCells;
599
+ this._rowHasBlinkingCellsCount += hasBlinkingCells ? 1 : -1;
600
+ }
601
+
602
+ private _updateTextBlinkState(): void {
603
+ this._textBlinkStateManager.setNeedsBlinkInViewport(this._rowHasBlinkingCellsCount > 0);
604
+ }
605
+ }
606
+
607
+ class CursorBlinkStateManager {
608
+ private _idleTimeout: number | undefined;
609
+ private _isIdlePaused: boolean = false;
610
+
611
+ constructor(
612
+ private readonly _rowContainer: HTMLElement,
613
+ private readonly _coreBrowserService: ICoreBrowserService
614
+ ) {
615
+ if (this._coreBrowserService.isFocused) {
616
+ this._resetIdleTimer();
617
+ }
618
+ }
619
+
620
+ public dispose(): void {
621
+ this._clearIdleTimer();
622
+ }
623
+
624
+ public restartBlinkAnimation(): void {
625
+ if (this._isIdlePaused) {
626
+ this._rowContainer.classList.remove(CURSOR_BLINK_IDLE_CLASS);
627
+ }
628
+ this._resetIdleTimer();
629
+ }
630
+
631
+ public pause(): void {
632
+ this._isIdlePaused = false;
633
+ this._clearIdleTimer();
634
+ }
635
+
636
+ public resume(): void {
637
+ this._isIdlePaused = false;
638
+ this._rowContainer.classList.remove(CURSOR_BLINK_IDLE_CLASS);
639
+ this._resetIdleTimer();
640
+ }
641
+
642
+ private _resetIdleTimer(): void {
643
+ this._isIdlePaused = false;
644
+ this._clearIdleTimer();
645
+ this._idleTimeout = this._coreBrowserService.window.setTimeout(() => {
646
+ this._stopBlinkingDueToIdle();
647
+ }, RendererConstants.CURSOR_BLINK_IDLE_TIMEOUT);
648
+ }
649
+
650
+ private _clearIdleTimer(): void {
651
+ if (this._idleTimeout) {
652
+ this._coreBrowserService.window.clearTimeout(this._idleTimeout);
653
+ this._idleTimeout = undefined;
654
+ }
655
+ }
656
+
657
+ private _stopBlinkingDueToIdle(): void {
658
+ this._rowContainer.classList.add(CURSOR_BLINK_IDLE_CLASS);
659
+ this._isIdlePaused = true;
660
+ this._idleTimeout = undefined;
541
661
  }
542
662
  }
@@ -24,6 +24,7 @@ export const enum RowCss {
24
24
  UNDERLINE_CLASS = 'xterm-underline',
25
25
  OVERLINE_CLASS = 'xterm-overline',
26
26
  STRIKETHROUGH_CLASS = 'xterm-strikethrough',
27
+ BLINK_HIDDEN_CLASS = 'xterm-blink-hidden',
27
28
  CURSOR_CLASS = 'xterm-cursor',
28
29
  CURSOR_BLINK_CLASS = 'xterm-cursor-blink',
29
30
  CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block',
@@ -66,13 +67,18 @@ export class DomRendererRowFactory {
66
67
  cursorInactiveStyle: string | undefined,
67
68
  cursorX: number,
68
69
  cursorBlink: boolean,
70
+ blinkOn: boolean,
69
71
  cellWidth: number,
70
72
  widthCache: WidthCache,
71
73
  linkStart: number,
72
- linkEnd: number
74
+ linkEnd: number,
75
+ rowInfo?: { hasBlinkingCells: boolean }
73
76
  ): HTMLSpanElement[] {
74
77
 
75
78
  const elements: HTMLSpanElement[] = [];
79
+ if (rowInfo) {
80
+ rowInfo.hasBlinkingCells = false;
81
+ }
76
82
  const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
77
83
  const colors = this._themeService.colors;
78
84
 
@@ -118,7 +124,7 @@ export class DomRendererRowFactory {
118
124
  // Process any joined character ranges as needed. Because of how the
119
125
  // ranges are produced, we know that they are valid for the characters
120
126
  // and attributes of our input.
121
- let cell = this._workCell;
127
+ let cell: ICellData = this._workCell;
122
128
  if (joinedRanges.length > 0 && x === joinedRanges[0][0] && isValidJoinRange) {
123
129
  const range = joinedRanges.shift()!;
124
130
  // If the ligature's selection state is not consistent, don't join it. This helps the
@@ -153,6 +159,13 @@ export class DomRendererRowFactory {
153
159
  const isInSelection = this._isCellInSelection(x, row);
154
160
  const isCursorCell = isCursorRow && x === cursorX;
155
161
  const isLinkHover = hasHover && x >= linkStart && x <= linkEnd;
162
+ if (rowInfo && cell.isBlink()) {
163
+ rowInfo.hasBlinkingCells = true;
164
+ }
165
+ const isBlinkHidden = !blinkOn && cell.isBlink();
166
+ if (isBlinkHidden) {
167
+ classes.push(RowCss.BLINK_HIDDEN_CLASS);
168
+ }
156
169
 
157
170
  let isDecorated = false;
158
171
  this._decorationService.forEachDecorationAtCell(x, row, undefined, d => {
@@ -397,7 +410,7 @@ export class DomRendererRowFactory {
397
410
  break;
398
411
  case Attributes.CM_RGB:
399
412
  resolvedBg = channels.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
400
- this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`);
413
+ this._addStyle(charElement, `background-color:#${(bg >>> 0).toString(16).padStart(6, '0')}`);
401
414
  break;
402
415
  case Attributes.CM_DEFAULT:
403
416
  default:
@@ -434,7 +447,7 @@ export class DomRendererRowFactory {
434
447
  (fg ) & 0xFF
435
448
  );
436
449
  if (!this._applyMinimumContrast(charElement, resolvedBg, color, cell, bgOverride, fgOverride)) {
437
- this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`);
450
+ this._addStyle(charElement, `color:#${fg.toString(16).padStart(6, '0')}`);
438
451
  }
439
452
  break;
440
453
  case Attributes.CM_DEFAULT:
@@ -494,8 +507,8 @@ export class DomRendererRowFactory {
494
507
  // Dim cells only require half the contrast, otherwise they wouldn't be distinguishable from
495
508
  // non-dim cells
496
509
  const ratio = this._optionsService.rawOptions.minimumContrastRatio / (cell.isDim() ? 2 : 1);
497
- adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, ratio);
498
- cache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null);
510
+ adjustedColor = color.ensureContrastRatio(bgOverride ?? bg, fgOverride ?? fg, ratio);
511
+ cache.setColor((bgOverride ?? bg).rgba, (fgOverride ?? fg).rgba, adjustedColor ?? null);
499
512
  }
500
513
 
501
514
  if (adjustedColor) {
@@ -537,10 +550,3 @@ export class DomRendererRowFactory {
537
550
  (start[1] < end[1] && y === start[1] && x >= start[0]);
538
551
  }
539
552
  }
540
-
541
- function padStart(text: string, padChar: string, length: number): string {
542
- while (text.length < length) {
543
- text = padChar + text;
544
- }
545
- return text;
546
- }