@xterm/xterm 5.4.0-beta.1
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/LICENSE +21 -0
- package/README.md +235 -0
- package/css/xterm.css +209 -0
- package/lib/xterm.js +2 -0
- package/lib/xterm.js.map +1 -0
- package/package.json +101 -0
- package/src/browser/AccessibilityManager.ts +278 -0
- package/src/browser/Clipboard.ts +93 -0
- package/src/browser/ColorContrastCache.ts +34 -0
- package/src/browser/Lifecycle.ts +33 -0
- package/src/browser/Linkifier2.ts +416 -0
- package/src/browser/LocalizableStrings.ts +12 -0
- package/src/browser/OscLinkProvider.ts +128 -0
- package/src/browser/RenderDebouncer.ts +83 -0
- package/src/browser/Terminal.ts +1317 -0
- package/src/browser/TimeBasedDebouncer.ts +86 -0
- package/src/browser/Types.d.ts +181 -0
- package/src/browser/Viewport.ts +401 -0
- package/src/browser/decorations/BufferDecorationRenderer.ts +134 -0
- package/src/browser/decorations/ColorZoneStore.ts +117 -0
- package/src/browser/decorations/OverviewRulerRenderer.ts +218 -0
- package/src/browser/input/CompositionHelper.ts +246 -0
- package/src/browser/input/Mouse.ts +54 -0
- package/src/browser/input/MoveToCell.ts +249 -0
- package/src/browser/public/Terminal.ts +260 -0
- package/src/browser/renderer/dom/DomRenderer.ts +509 -0
- package/src/browser/renderer/dom/DomRendererRowFactory.ts +526 -0
- package/src/browser/renderer/dom/WidthCache.ts +160 -0
- package/src/browser/renderer/shared/CellColorResolver.ts +137 -0
- package/src/browser/renderer/shared/CharAtlasCache.ts +96 -0
- package/src/browser/renderer/shared/CharAtlasUtils.ts +75 -0
- package/src/browser/renderer/shared/Constants.ts +14 -0
- package/src/browser/renderer/shared/CursorBlinkStateManager.ts +146 -0
- package/src/browser/renderer/shared/CustomGlyphs.ts +687 -0
- package/src/browser/renderer/shared/DevicePixelObserver.ts +41 -0
- package/src/browser/renderer/shared/README.md +1 -0
- package/src/browser/renderer/shared/RendererUtils.ts +58 -0
- package/src/browser/renderer/shared/SelectionRenderModel.ts +91 -0
- package/src/browser/renderer/shared/TextureAtlas.ts +1082 -0
- package/src/browser/renderer/shared/Types.d.ts +173 -0
- package/src/browser/selection/SelectionModel.ts +144 -0
- package/src/browser/selection/Types.d.ts +15 -0
- package/src/browser/services/CharSizeService.ts +102 -0
- package/src/browser/services/CharacterJoinerService.ts +339 -0
- package/src/browser/services/CoreBrowserService.ts +137 -0
- package/src/browser/services/MouseService.ts +46 -0
- package/src/browser/services/RenderService.ts +279 -0
- package/src/browser/services/SelectionService.ts +1031 -0
- package/src/browser/services/Services.ts +147 -0
- package/src/browser/services/ThemeService.ts +237 -0
- package/src/common/CircularList.ts +241 -0
- package/src/common/Clone.ts +23 -0
- package/src/common/Color.ts +357 -0
- package/src/common/CoreTerminal.ts +284 -0
- package/src/common/EventEmitter.ts +78 -0
- package/src/common/InputHandler.ts +3461 -0
- package/src/common/Lifecycle.ts +108 -0
- package/src/common/MultiKeyMap.ts +42 -0
- package/src/common/Platform.ts +44 -0
- package/src/common/SortedList.ts +118 -0
- package/src/common/TaskQueue.ts +166 -0
- package/src/common/TypedArrayUtils.ts +17 -0
- package/src/common/Types.d.ts +553 -0
- package/src/common/WindowsMode.ts +27 -0
- package/src/common/buffer/AttributeData.ts +196 -0
- package/src/common/buffer/Buffer.ts +654 -0
- package/src/common/buffer/BufferLine.ts +524 -0
- package/src/common/buffer/BufferRange.ts +13 -0
- package/src/common/buffer/BufferReflow.ts +223 -0
- package/src/common/buffer/BufferSet.ts +134 -0
- package/src/common/buffer/CellData.ts +94 -0
- package/src/common/buffer/Constants.ts +149 -0
- package/src/common/buffer/Marker.ts +43 -0
- package/src/common/buffer/Types.d.ts +52 -0
- package/src/common/data/Charsets.ts +256 -0
- package/src/common/data/EscapeSequences.ts +153 -0
- package/src/common/input/Keyboard.ts +398 -0
- package/src/common/input/TextDecoder.ts +346 -0
- package/src/common/input/UnicodeV6.ts +145 -0
- package/src/common/input/WriteBuffer.ts +246 -0
- package/src/common/input/XParseColor.ts +80 -0
- package/src/common/parser/Constants.ts +58 -0
- package/src/common/parser/DcsParser.ts +192 -0
- package/src/common/parser/EscapeSequenceParser.ts +792 -0
- package/src/common/parser/OscParser.ts +238 -0
- package/src/common/parser/Params.ts +229 -0
- package/src/common/parser/Types.d.ts +275 -0
- package/src/common/public/AddonManager.ts +53 -0
- package/src/common/public/BufferApiView.ts +35 -0
- package/src/common/public/BufferLineApiView.ts +29 -0
- package/src/common/public/BufferNamespaceApi.ts +36 -0
- package/src/common/public/ParserApi.ts +37 -0
- package/src/common/public/UnicodeApi.ts +27 -0
- package/src/common/services/BufferService.ts +151 -0
- package/src/common/services/CharsetService.ts +34 -0
- package/src/common/services/CoreMouseService.ts +318 -0
- package/src/common/services/CoreService.ts +87 -0
- package/src/common/services/DecorationService.ts +140 -0
- package/src/common/services/InstantiationService.ts +85 -0
- package/src/common/services/LogService.ts +124 -0
- package/src/common/services/OptionsService.ts +202 -0
- package/src/common/services/OscLinkService.ts +115 -0
- package/src/common/services/ServiceRegistry.ts +49 -0
- package/src/common/services/Services.ts +373 -0
- package/src/common/services/UnicodeService.ts +111 -0
- package/src/headless/Terminal.ts +136 -0
- package/src/headless/public/Terminal.ts +195 -0
- package/typings/xterm.d.ts +1857 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second
|
|
7
|
+
|
|
8
|
+
import { IRenderDebouncer } from 'browser/Types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Debounces calls to update screen readers to update at most once configurable interval of time.
|
|
12
|
+
*/
|
|
13
|
+
export class TimeBasedDebouncer implements IRenderDebouncer {
|
|
14
|
+
private _rowStart: number | undefined;
|
|
15
|
+
private _rowEnd: number | undefined;
|
|
16
|
+
private _rowCount: number | undefined;
|
|
17
|
+
|
|
18
|
+
// The last moment that the Terminal was refreshed at
|
|
19
|
+
private _lastRefreshMs = 0;
|
|
20
|
+
// Whether a trailing refresh should be triggered due to a refresh request that was throttled
|
|
21
|
+
private _additionalRefreshRequested = false;
|
|
22
|
+
|
|
23
|
+
private _refreshTimeoutID: number | undefined;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
private _renderCallback: (start: number, end: number) => void,
|
|
27
|
+
private readonly _debounceThresholdMS = RENDER_DEBOUNCE_THRESHOLD_MS
|
|
28
|
+
) {
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public dispose(): void {
|
|
32
|
+
if (this._refreshTimeoutID) {
|
|
33
|
+
clearTimeout(this._refreshTimeoutID);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
|
38
|
+
this._rowCount = rowCount;
|
|
39
|
+
// Get the min/max row start/end for the arg values
|
|
40
|
+
rowStart = rowStart !== undefined ? rowStart : 0;
|
|
41
|
+
rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1;
|
|
42
|
+
// Set the properties to the updated values
|
|
43
|
+
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
|
44
|
+
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
|
45
|
+
|
|
46
|
+
// Only refresh if the time since last refresh is above a threshold, otherwise wait for
|
|
47
|
+
// enough time to pass before refreshing again.
|
|
48
|
+
const refreshRequestTime: number = Date.now();
|
|
49
|
+
if (refreshRequestTime - this._lastRefreshMs >= this._debounceThresholdMS) {
|
|
50
|
+
// Enough time has lapsed since the last refresh; refresh immediately
|
|
51
|
+
this._lastRefreshMs = refreshRequestTime;
|
|
52
|
+
this._innerRefresh();
|
|
53
|
+
} else if (!this._additionalRefreshRequested) {
|
|
54
|
+
// This is the first additional request throttled; set up trailing refresh
|
|
55
|
+
const elapsed = refreshRequestTime - this._lastRefreshMs;
|
|
56
|
+
const waitPeriodBeforeTrailingRefresh = this._debounceThresholdMS - elapsed;
|
|
57
|
+
this._additionalRefreshRequested = true;
|
|
58
|
+
|
|
59
|
+
this._refreshTimeoutID = window.setTimeout(() => {
|
|
60
|
+
this._lastRefreshMs = Date.now();
|
|
61
|
+
this._innerRefresh();
|
|
62
|
+
this._additionalRefreshRequested = false;
|
|
63
|
+
this._refreshTimeoutID = undefined; // No longer need to clear the timeout
|
|
64
|
+
}, waitPeriodBeforeTrailingRefresh);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private _innerRefresh(): void {
|
|
69
|
+
// Make sure values are set
|
|
70
|
+
if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Clamp values
|
|
75
|
+
const start = Math.max(this._rowStart, 0);
|
|
76
|
+
const end = Math.min(this._rowEnd, this._rowCount - 1);
|
|
77
|
+
|
|
78
|
+
// Reset debouncer (this happens before render callback as the render could trigger it again)
|
|
79
|
+
this._rowStart = undefined;
|
|
80
|
+
this._rowEnd = undefined;
|
|
81
|
+
|
|
82
|
+
// Run render callback
|
|
83
|
+
this._renderCallback(start, end);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IEvent } from 'common/EventEmitter';
|
|
7
|
+
import { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types';
|
|
8
|
+
import { IBuffer } from 'common/buffer/Types';
|
|
9
|
+
import { IDisposable, Terminal as ITerminalApi } from '@xterm/xterm';
|
|
10
|
+
import { IMouseService, IRenderService } from './services/Services';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A portion of the public API that are implemented identially internally and simply passed through.
|
|
14
|
+
*/
|
|
15
|
+
type InternalPassthroughApis = Omit<ITerminalApi, 'buffer' | 'parser' | 'unicode' | 'modes' | 'writeln' | 'loadAddon'>;
|
|
16
|
+
|
|
17
|
+
export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
|
|
18
|
+
screenElement: HTMLElement | undefined;
|
|
19
|
+
browser: IBrowser;
|
|
20
|
+
buffer: IBuffer;
|
|
21
|
+
viewport: IViewport | undefined;
|
|
22
|
+
options: Required<ITerminalOptions>;
|
|
23
|
+
linkifier2: ILinkifier2;
|
|
24
|
+
|
|
25
|
+
onBlur: IEvent<void>;
|
|
26
|
+
onFocus: IEvent<void>;
|
|
27
|
+
onA11yChar: IEvent<string>;
|
|
28
|
+
onA11yTab: IEvent<number>;
|
|
29
|
+
onWillOpen: IEvent<HTMLElement>;
|
|
30
|
+
|
|
31
|
+
cancel(ev: Event, force?: boolean): boolean | void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
|
35
|
+
|
|
36
|
+
export type LineData = CharData[];
|
|
37
|
+
|
|
38
|
+
export interface ICompositionHelper {
|
|
39
|
+
readonly isComposing: boolean;
|
|
40
|
+
compositionstart(): void;
|
|
41
|
+
compositionupdate(ev: CompositionEvent): void;
|
|
42
|
+
compositionend(): void;
|
|
43
|
+
updateCompositionElements(dontRecurse?: boolean): void;
|
|
44
|
+
keydown(ev: KeyboardEvent): boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface IBrowser {
|
|
48
|
+
isNode: boolean;
|
|
49
|
+
userAgent: string;
|
|
50
|
+
platform: string;
|
|
51
|
+
isFirefox: boolean;
|
|
52
|
+
isMac: boolean;
|
|
53
|
+
isIpad: boolean;
|
|
54
|
+
isIphone: boolean;
|
|
55
|
+
isWindows: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface IColorSet {
|
|
59
|
+
foreground: IColor;
|
|
60
|
+
background: IColor;
|
|
61
|
+
cursor: IColor;
|
|
62
|
+
cursorAccent: IColor;
|
|
63
|
+
selectionForeground: IColor | undefined;
|
|
64
|
+
selectionBackgroundTransparent: IColor;
|
|
65
|
+
/** The selection blended on top of background. */
|
|
66
|
+
selectionBackgroundOpaque: IColor;
|
|
67
|
+
selectionInactiveBackgroundTransparent: IColor;
|
|
68
|
+
selectionInactiveBackgroundOpaque: IColor;
|
|
69
|
+
ansi: IColor[];
|
|
70
|
+
/** Maps original colors to colors that respect minimum contrast ratio. */
|
|
71
|
+
contrastCache: IColorContrastCache;
|
|
72
|
+
/** Maps original colors to colors that respect _half_ of the minimum contrast ratio. */
|
|
73
|
+
halfContrastCache: IColorContrastCache;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ReadonlyColorSet = Readonly<Omit<IColorSet, 'ansi'>> & { ansi: Readonly<Pick<IColorSet, 'ansi'>['ansi']> };
|
|
77
|
+
|
|
78
|
+
export interface IColorContrastCache {
|
|
79
|
+
clear(): void;
|
|
80
|
+
setCss(bg: number, fg: number, value: string | null): void;
|
|
81
|
+
getCss(bg: number, fg: number): string | null | undefined;
|
|
82
|
+
setColor(bg: number, fg: number, value: IColor | null): void;
|
|
83
|
+
getColor(bg: number, fg: number): IColor | null | undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface IPartialColorSet {
|
|
87
|
+
foreground: IColor;
|
|
88
|
+
background: IColor;
|
|
89
|
+
cursor?: IColor;
|
|
90
|
+
cursorAccent?: IColor;
|
|
91
|
+
selectionBackground?: IColor;
|
|
92
|
+
ansi: IColor[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface IViewport extends IDisposable {
|
|
96
|
+
scrollBarWidth: number;
|
|
97
|
+
readonly onRequestScrollLines: IEvent<{ amount: number, suppressScrollEvent: boolean }>;
|
|
98
|
+
syncScrollArea(immediate?: boolean, force?: boolean): void;
|
|
99
|
+
getLinesScrolled(ev: WheelEvent): number;
|
|
100
|
+
getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[], cursorElement?: HTMLElement };
|
|
101
|
+
handleWheel(ev: WheelEvent): boolean;
|
|
102
|
+
handleTouchStart(ev: TouchEvent): void;
|
|
103
|
+
handleTouchMove(ev: TouchEvent): boolean;
|
|
104
|
+
scrollLines(disp: number): void; // todo api name?
|
|
105
|
+
reset(): void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ILinkifierEvent {
|
|
109
|
+
x1: number;
|
|
110
|
+
y1: number;
|
|
111
|
+
x2: number;
|
|
112
|
+
y2: number;
|
|
113
|
+
cols: number;
|
|
114
|
+
fg: number | undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface ILinkState {
|
|
118
|
+
decorations: ILinkDecorations;
|
|
119
|
+
isHovered: boolean;
|
|
120
|
+
}
|
|
121
|
+
export interface ILinkWithState {
|
|
122
|
+
link: ILink;
|
|
123
|
+
state?: ILinkState;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ILinkifier2 extends IDisposable {
|
|
127
|
+
onShowLinkUnderline: IEvent<ILinkifierEvent>;
|
|
128
|
+
onHideLinkUnderline: IEvent<ILinkifierEvent>;
|
|
129
|
+
readonly currentLink: ILinkWithState | undefined;
|
|
130
|
+
|
|
131
|
+
attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void;
|
|
132
|
+
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface ILinkProvider {
|
|
136
|
+
provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface ILink {
|
|
140
|
+
range: IBufferRange;
|
|
141
|
+
text: string;
|
|
142
|
+
decorations?: ILinkDecorations;
|
|
143
|
+
activate(event: MouseEvent, text: string): void;
|
|
144
|
+
hover?(event: MouseEvent, text: string): void;
|
|
145
|
+
leave?(event: MouseEvent, text: string): void;
|
|
146
|
+
dispose?(): void;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface ILinkDecorations {
|
|
150
|
+
pointerCursor: boolean;
|
|
151
|
+
underline: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface IBufferRange {
|
|
155
|
+
start: IBufferCellPosition;
|
|
156
|
+
end: IBufferCellPosition;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface IBufferCellPosition {
|
|
160
|
+
x: number;
|
|
161
|
+
y: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type CharacterJoinerHandler = (text: string) => [number, number][];
|
|
165
|
+
|
|
166
|
+
export interface ICharacterJoiner {
|
|
167
|
+
id: number;
|
|
168
|
+
handler: CharacterJoinerHandler;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface IRenderDebouncer extends IDisposable {
|
|
172
|
+
refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface IRenderDebouncerWithCallback extends IRenderDebouncer {
|
|
176
|
+
addRefreshCallback(callback: FrameRequestCallback): number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface IBufferElementProvider {
|
|
180
|
+
provideBufferElements(): DocumentFragment | HTMLElement;
|
|
181
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { addDisposableDomListener } from 'browser/Lifecycle';
|
|
7
|
+
import { IViewport, ReadonlyColorSet } from 'browser/Types';
|
|
8
|
+
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
|
9
|
+
import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
|
10
|
+
import { EventEmitter } from 'common/EventEmitter';
|
|
11
|
+
import { Disposable } from 'common/Lifecycle';
|
|
12
|
+
import { IBuffer } from 'common/buffer/Types';
|
|
13
|
+
import { IBufferService, IOptionsService } from 'common/services/Services';
|
|
14
|
+
|
|
15
|
+
const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
|
16
|
+
|
|
17
|
+
interface ISmoothScrollState {
|
|
18
|
+
startTime: number;
|
|
19
|
+
origin: number;
|
|
20
|
+
target: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
|
|
25
|
+
* Logic for the virtual scroll bar is included in this object.
|
|
26
|
+
*/
|
|
27
|
+
export class Viewport extends Disposable implements IViewport {
|
|
28
|
+
public scrollBarWidth: number = 0;
|
|
29
|
+
private _currentRowHeight: number = 0;
|
|
30
|
+
private _currentDeviceCellHeight: number = 0;
|
|
31
|
+
private _lastRecordedBufferLength: number = 0;
|
|
32
|
+
private _lastRecordedViewportHeight: number = 0;
|
|
33
|
+
private _lastRecordedBufferHeight: number = 0;
|
|
34
|
+
private _lastTouchY: number = 0;
|
|
35
|
+
private _lastScrollTop: number = 0;
|
|
36
|
+
private _activeBuffer: IBuffer;
|
|
37
|
+
private _renderDimensions: IRenderDimensions;
|
|
38
|
+
|
|
39
|
+
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
|
|
40
|
+
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
|
|
41
|
+
// quick fix and could have a more robust solution in place that reset the value when needed.
|
|
42
|
+
private _wheelPartialScroll: number = 0;
|
|
43
|
+
|
|
44
|
+
private _refreshAnimationFrame: number | null = null;
|
|
45
|
+
private _ignoreNextScrollEvent: boolean = false;
|
|
46
|
+
private _smoothScrollState: ISmoothScrollState = {
|
|
47
|
+
startTime: 0,
|
|
48
|
+
origin: -1,
|
|
49
|
+
target: -1
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
private readonly _onRequestScrollLines = this.register(new EventEmitter<{ amount: number, suppressScrollEvent: boolean }>());
|
|
53
|
+
public readonly onRequestScrollLines = this._onRequestScrollLines.event;
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
private readonly _viewportElement: HTMLElement,
|
|
57
|
+
private readonly _scrollArea: HTMLElement,
|
|
58
|
+
@IBufferService private readonly _bufferService: IBufferService,
|
|
59
|
+
@IOptionsService private readonly _optionsService: IOptionsService,
|
|
60
|
+
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
|
61
|
+
@IRenderService private readonly _renderService: IRenderService,
|
|
62
|
+
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
|
|
63
|
+
@IThemeService themeService: IThemeService
|
|
64
|
+
) {
|
|
65
|
+
super();
|
|
66
|
+
|
|
67
|
+
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
|
|
68
|
+
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that
|
|
69
|
+
// case, therefore we account for a standard amount to make it visible
|
|
70
|
+
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
|
|
71
|
+
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._handleScroll.bind(this)));
|
|
72
|
+
|
|
73
|
+
// Track properties used in performance critical code manually to avoid using slow getters
|
|
74
|
+
this._activeBuffer = this._bufferService.buffer;
|
|
75
|
+
this.register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer));
|
|
76
|
+
this._renderDimensions = this._renderService.dimensions;
|
|
77
|
+
this.register(this._renderService.onDimensionsChange(e => this._renderDimensions = e));
|
|
78
|
+
|
|
79
|
+
this._handleThemeChange(themeService.colors);
|
|
80
|
+
this.register(themeService.onChangeColors(e => this._handleThemeChange(e)));
|
|
81
|
+
this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.syncScrollArea()));
|
|
82
|
+
|
|
83
|
+
// Perform this async to ensure the ICharSizeService is ready.
|
|
84
|
+
setTimeout(() => this.syncScrollArea());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private _handleThemeChange(colors: ReadonlyColorSet): void {
|
|
88
|
+
this._viewportElement.style.backgroundColor = colors.background.css;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public reset(): void {
|
|
92
|
+
this._currentRowHeight = 0;
|
|
93
|
+
this._currentDeviceCellHeight = 0;
|
|
94
|
+
this._lastRecordedBufferLength = 0;
|
|
95
|
+
this._lastRecordedViewportHeight = 0;
|
|
96
|
+
this._lastRecordedBufferHeight = 0;
|
|
97
|
+
this._lastTouchY = 0;
|
|
98
|
+
this._lastScrollTop = 0;
|
|
99
|
+
// Sync on next animation frame to ensure the new terminal state is used
|
|
100
|
+
this._coreBrowserService.window.requestAnimationFrame(() => this.syncScrollArea());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Refreshes row height, setting line-height, viewport height and scroll area height if
|
|
105
|
+
* necessary.
|
|
106
|
+
*/
|
|
107
|
+
private _refresh(immediate: boolean): void {
|
|
108
|
+
if (immediate) {
|
|
109
|
+
this._innerRefresh();
|
|
110
|
+
if (this._refreshAnimationFrame !== null) {
|
|
111
|
+
this._coreBrowserService.window.cancelAnimationFrame(this._refreshAnimationFrame);
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (this._refreshAnimationFrame === null) {
|
|
116
|
+
this._refreshAnimationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private _innerRefresh(): void {
|
|
121
|
+
if (this._charSizeService.height > 0) {
|
|
122
|
+
this._currentRowHeight = this._renderDimensions.device.cell.height / this._coreBrowserService.dpr;
|
|
123
|
+
this._currentDeviceCellHeight = this._renderDimensions.device.cell.height;
|
|
124
|
+
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
|
|
125
|
+
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderDimensions.css.canvas.height);
|
|
126
|
+
if (this._lastRecordedBufferHeight !== newBufferHeight) {
|
|
127
|
+
this._lastRecordedBufferHeight = newBufferHeight;
|
|
128
|
+
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Sync scrollTop
|
|
133
|
+
const scrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight;
|
|
134
|
+
if (this._viewportElement.scrollTop !== scrollTop) {
|
|
135
|
+
// Ignore the next scroll event which will be triggered by setting the scrollTop as we do not
|
|
136
|
+
// want this event to scroll the terminal
|
|
137
|
+
this._ignoreNextScrollEvent = true;
|
|
138
|
+
this._viewportElement.scrollTop = scrollTop;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this._refreshAnimationFrame = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Updates dimensions and synchronizes the scroll area if necessary.
|
|
146
|
+
*/
|
|
147
|
+
public syncScrollArea(immediate: boolean = false): void {
|
|
148
|
+
// If buffer height changed
|
|
149
|
+
if (this._lastRecordedBufferLength !== this._bufferService.buffer.lines.length) {
|
|
150
|
+
this._lastRecordedBufferLength = this._bufferService.buffer.lines.length;
|
|
151
|
+
this._refresh(immediate);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// If viewport height changed
|
|
156
|
+
if (this._lastRecordedViewportHeight !== this._renderService.dimensions.css.canvas.height) {
|
|
157
|
+
this._refresh(immediate);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// If the buffer position doesn't match last scroll top
|
|
162
|
+
if (this._lastScrollTop !== this._activeBuffer.ydisp * this._currentRowHeight) {
|
|
163
|
+
this._refresh(immediate);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// If row height changed
|
|
168
|
+
if (this._renderDimensions.device.cell.height !== this._currentDeviceCellHeight) {
|
|
169
|
+
this._refresh(immediate);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Handles scroll events on the viewport, calculating the new viewport and requesting the
|
|
176
|
+
* terminal to scroll to it.
|
|
177
|
+
* @param ev The scroll event.
|
|
178
|
+
*/
|
|
179
|
+
private _handleScroll(ev: Event): void {
|
|
180
|
+
// Record current scroll top position
|
|
181
|
+
this._lastScrollTop = this._viewportElement.scrollTop;
|
|
182
|
+
|
|
183
|
+
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
|
|
184
|
+
// which causes the terminal to scroll the buffer to the top
|
|
185
|
+
if (!this._viewportElement.offsetParent) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Ignore the event if it was flagged to ignore (when the source of the event is from Viewport)
|
|
190
|
+
if (this._ignoreNextScrollEvent) {
|
|
191
|
+
this._ignoreNextScrollEvent = false;
|
|
192
|
+
// Still trigger the scroll so lines get refreshed
|
|
193
|
+
this._onRequestScrollLines.fire({ amount: 0, suppressScrollEvent: true });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
|
|
198
|
+
const diff = newRow - this._bufferService.buffer.ydisp;
|
|
199
|
+
this._onRequestScrollLines.fire({ amount: diff, suppressScrollEvent: true });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private _smoothScroll(): void {
|
|
203
|
+
// Check valid state
|
|
204
|
+
if (this._isDisposed || this._smoothScrollState.origin === -1 || this._smoothScrollState.target === -1) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Calculate position complete
|
|
209
|
+
const percent = this._smoothScrollPercent();
|
|
210
|
+
this._viewportElement.scrollTop = this._smoothScrollState.origin + Math.round(percent * (this._smoothScrollState.target - this._smoothScrollState.origin));
|
|
211
|
+
|
|
212
|
+
// Continue or finish smooth scroll
|
|
213
|
+
if (percent < 1) {
|
|
214
|
+
this._coreBrowserService.window.requestAnimationFrame(() => this._smoothScroll());
|
|
215
|
+
} else {
|
|
216
|
+
this._clearSmoothScrollState();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private _smoothScrollPercent(): number {
|
|
221
|
+
if (!this._optionsService.rawOptions.smoothScrollDuration || !this._smoothScrollState.startTime) {
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
return Math.max(Math.min((Date.now() - this._smoothScrollState.startTime) / this._optionsService.rawOptions.smoothScrollDuration, 1), 0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private _clearSmoothScrollState(): void {
|
|
228
|
+
this._smoothScrollState.startTime = 0;
|
|
229
|
+
this._smoothScrollState.origin = -1;
|
|
230
|
+
this._smoothScrollState.target = -1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Handles bubbling of scroll event in case the viewport has reached top or bottom
|
|
235
|
+
* @param ev The scroll event.
|
|
236
|
+
* @param amount The amount scrolled
|
|
237
|
+
*/
|
|
238
|
+
private _bubbleScroll(ev: Event, amount: number): boolean {
|
|
239
|
+
const scrollPosFromTop = this._viewportElement.scrollTop + this._lastRecordedViewportHeight;
|
|
240
|
+
if ((amount < 0 && this._viewportElement.scrollTop !== 0) ||
|
|
241
|
+
(amount > 0 && scrollPosFromTop < this._lastRecordedBufferHeight)) {
|
|
242
|
+
if (ev.cancelable) {
|
|
243
|
+
ev.preventDefault();
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual
|
|
252
|
+
* scrolling to `onScroll`, this event needs to be attached manually by the consumer of
|
|
253
|
+
* `Viewport`.
|
|
254
|
+
* @param ev The mouse wheel event.
|
|
255
|
+
*/
|
|
256
|
+
public handleWheel(ev: WheelEvent): boolean {
|
|
257
|
+
const amount = this._getPixelsScrolled(ev);
|
|
258
|
+
if (amount === 0) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if (!this._optionsService.rawOptions.smoothScrollDuration) {
|
|
262
|
+
this._viewportElement.scrollTop += amount;
|
|
263
|
+
} else {
|
|
264
|
+
this._smoothScrollState.startTime = Date.now();
|
|
265
|
+
if (this._smoothScrollPercent() < 1) {
|
|
266
|
+
this._smoothScrollState.origin = this._viewportElement.scrollTop;
|
|
267
|
+
if (this._smoothScrollState.target === -1) {
|
|
268
|
+
this._smoothScrollState.target = this._viewportElement.scrollTop + amount;
|
|
269
|
+
} else {
|
|
270
|
+
this._smoothScrollState.target += amount;
|
|
271
|
+
}
|
|
272
|
+
this._smoothScrollState.target = Math.max(Math.min(this._smoothScrollState.target, this._viewportElement.scrollHeight), 0);
|
|
273
|
+
this._smoothScroll();
|
|
274
|
+
} else {
|
|
275
|
+
this._clearSmoothScrollState();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return this._bubbleScroll(ev, amount);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
public scrollLines(disp: number): void {
|
|
282
|
+
if (disp === 0) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (!this._optionsService.rawOptions.smoothScrollDuration) {
|
|
286
|
+
this._onRequestScrollLines.fire({ amount: disp, suppressScrollEvent: false });
|
|
287
|
+
} else {
|
|
288
|
+
const amount = disp * this._currentRowHeight;
|
|
289
|
+
this._smoothScrollState.startTime = Date.now();
|
|
290
|
+
if (this._smoothScrollPercent() < 1) {
|
|
291
|
+
this._smoothScrollState.origin = this._viewportElement.scrollTop;
|
|
292
|
+
this._smoothScrollState.target = this._smoothScrollState.origin + amount;
|
|
293
|
+
this._smoothScrollState.target = Math.max(Math.min(this._smoothScrollState.target, this._viewportElement.scrollHeight), 0);
|
|
294
|
+
this._smoothScroll();
|
|
295
|
+
} else {
|
|
296
|
+
this._clearSmoothScrollState();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private _getPixelsScrolled(ev: WheelEvent): number {
|
|
302
|
+
// Do nothing if it's not a vertical scroll event
|
|
303
|
+
if (ev.deltaY === 0 || ev.shiftKey) {
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Fallback to WheelEvent.DOM_DELTA_PIXEL
|
|
308
|
+
let amount = this._applyScrollModifier(ev.deltaY, ev);
|
|
309
|
+
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
|
310
|
+
amount *= this._currentRowHeight;
|
|
311
|
+
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
|
312
|
+
amount *= this._currentRowHeight * this._bufferService.rows;
|
|
313
|
+
}
|
|
314
|
+
return amount;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
public getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[], cursorElement?: HTMLElement } {
|
|
319
|
+
let currentLine: string = '';
|
|
320
|
+
let cursorElement: HTMLElement | undefined;
|
|
321
|
+
const bufferElements: HTMLElement[] = [];
|
|
322
|
+
const end = endLine ?? this._bufferService.buffer.lines.length;
|
|
323
|
+
const lines = this._bufferService.buffer.lines;
|
|
324
|
+
for (let i = startLine; i < end; i++) {
|
|
325
|
+
const line = lines.get(i);
|
|
326
|
+
if (!line) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const isWrapped = lines.get(i + 1)?.isWrapped;
|
|
330
|
+
currentLine += line.translateToString(!isWrapped);
|
|
331
|
+
if (!isWrapped || i === lines.length - 1) {
|
|
332
|
+
const div = document.createElement('div');
|
|
333
|
+
div.textContent = currentLine;
|
|
334
|
+
bufferElements.push(div);
|
|
335
|
+
if (currentLine.length > 0) {
|
|
336
|
+
cursorElement = div;
|
|
337
|
+
}
|
|
338
|
+
currentLine = '';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return { bufferElements, cursorElement };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Gets the number of pixels scrolled by the mouse event taking into account what type of delta
|
|
346
|
+
* is being used.
|
|
347
|
+
* @param ev The mouse wheel event.
|
|
348
|
+
*/
|
|
349
|
+
public getLinesScrolled(ev: WheelEvent): number {
|
|
350
|
+
// Do nothing if it's not a vertical scroll event
|
|
351
|
+
if (ev.deltaY === 0 || ev.shiftKey) {
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Fallback to WheelEvent.DOM_DELTA_LINE
|
|
356
|
+
let amount = this._applyScrollModifier(ev.deltaY, ev);
|
|
357
|
+
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
|
|
358
|
+
amount /= this._currentRowHeight + 0.0; // Prevent integer division
|
|
359
|
+
this._wheelPartialScroll += amount;
|
|
360
|
+
amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
|
|
361
|
+
this._wheelPartialScroll %= 1;
|
|
362
|
+
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
|
363
|
+
amount *= this._bufferService.rows;
|
|
364
|
+
}
|
|
365
|
+
return amount;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private _applyScrollModifier(amount: number, ev: WheelEvent): number {
|
|
369
|
+
const modifier = this._optionsService.rawOptions.fastScrollModifier;
|
|
370
|
+
// Multiply the scroll speed when the modifier is down
|
|
371
|
+
if ((modifier === 'alt' && ev.altKey) ||
|
|
372
|
+
(modifier === 'ctrl' && ev.ctrlKey) ||
|
|
373
|
+
(modifier === 'shift' && ev.shiftKey)) {
|
|
374
|
+
return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return amount * this._optionsService.rawOptions.scrollSensitivity;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Handles the touchstart event, recording the touch occurred.
|
|
382
|
+
* @param ev The touch event.
|
|
383
|
+
*/
|
|
384
|
+
public handleTouchStart(ev: TouchEvent): void {
|
|
385
|
+
this._lastTouchY = ev.touches[0].pageY;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Handles the touchmove event, scrolling the viewport if the position shifted.
|
|
390
|
+
* @param ev The touch event.
|
|
391
|
+
*/
|
|
392
|
+
public handleTouchMove(ev: TouchEvent): boolean {
|
|
393
|
+
const deltaY = this._lastTouchY - ev.touches[0].pageY;
|
|
394
|
+
this._lastTouchY = ev.touches[0].pageY;
|
|
395
|
+
if (deltaY === 0) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
this._viewportElement.scrollTop += deltaY;
|
|
399
|
+
return this._bubbleScroll(ev, deltaY);
|
|
400
|
+
}
|
|
401
|
+
}
|