@finos/legend-application 10.2.11 → 10.2.13
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/lib/application/LegendApplication.d.ts.map +1 -1
- package/lib/application/LegendApplication.js +1 -2
- package/lib/application/LegendApplication.js.map +1 -1
- package/lib/components/ActionAlert.d.ts +1 -0
- package/lib/components/ActionAlert.d.ts.map +1 -1
- package/lib/components/BlockingAlert.d.ts +1 -0
- package/lib/components/BlockingAlert.d.ts.map +1 -1
- package/lib/components/LegendApplicationComponentFrameworkProvider.d.ts +6 -0
- package/lib/components/LegendApplicationComponentFrameworkProvider.d.ts.map +1 -1
- package/lib/components/LegendApplicationComponentFrameworkProvider.js +21 -13
- package/lib/components/LegendApplicationComponentFrameworkProvider.js.map +1 -1
- package/lib/components/NotificationManager.d.ts +1 -0
- package/lib/components/NotificationManager.d.ts.map +1 -1
- package/lib/components/VirtualAssistant.d.ts +1 -0
- package/lib/components/VirtualAssistant.d.ts.map +1 -1
- package/lib/components/shared/TabManager.d.ts +1 -0
- package/lib/components/shared/TabManager.d.ts.map +1 -1
- package/lib/components/shared/TextSearchAdvancedConfigMenu.d.ts +1 -0
- package/lib/components/shared/TextSearchAdvancedConfigMenu.d.ts.map +1 -1
- package/lib/const.d.ts +1 -1
- package/lib/const.d.ts.map +1 -1
- package/lib/const.js +1 -1
- package/lib/const.js.map +1 -1
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/stores/ApplicationEvent.d.ts +1 -0
- package/lib/stores/ApplicationEvent.d.ts.map +1 -1
- package/lib/stores/ApplicationEvent.js +1 -0
- package/lib/stores/ApplicationEvent.js.map +1 -1
- package/lib/stores/ApplicationStore.d.ts +2 -0
- package/lib/stores/ApplicationStore.d.ts.map +1 -1
- package/lib/stores/ApplicationStore.js +3 -0
- package/lib/stores/ApplicationStore.js.map +1 -1
- package/lib/stores/CommandCenter.d.ts +1 -0
- package/lib/stores/CommandCenter.d.ts.map +1 -1
- package/lib/stores/CommandCenter.js.map +1 -1
- package/lib/stores/KeyboardShortcutsService.d.ts +4 -10
- package/lib/stores/KeyboardShortcutsService.d.ts.map +1 -1
- package/lib/stores/KeyboardShortcutsService.js +36 -33
- package/lib/stores/KeyboardShortcutsService.js.map +1 -1
- package/lib/stores/LegendApplicationDocumentation.d.ts +1 -0
- package/lib/stores/LegendApplicationDocumentation.d.ts.map +1 -1
- package/lib/stores/LegendApplicationDocumentation.js +1 -0
- package/lib/stores/LegendApplicationDocumentation.js.map +1 -1
- package/lib/stores/PureLanguageSupport.d.ts.map +1 -1
- package/lib/stores/PureLanguageSupport.js +14 -1
- package/lib/stores/PureLanguageSupport.js.map +1 -1
- package/lib/stores/TerminalService.d.ts +23 -0
- package/lib/stores/TerminalService.d.ts.map +1 -0
- package/lib/stores/TerminalService.js +25 -0
- package/lib/stores/TerminalService.js.map +1 -0
- package/lib/stores/terminal/Terminal.d.ts +155 -0
- package/lib/stores/terminal/Terminal.d.ts.map +1 -0
- package/lib/stores/terminal/Terminal.js +171 -0
- package/lib/stores/terminal/Terminal.js.map +1 -0
- package/lib/stores/terminal/XTerm.d.ts +91 -0
- package/lib/stores/terminal/XTerm.d.ts.map +1 -0
- package/lib/stores/terminal/XTerm.js +693 -0
- package/lib/stores/terminal/XTerm.js.map +1 -0
- package/package.json +19 -13
- package/src/application/LegendApplication.tsx +5 -2
- package/src/components/LegendApplicationComponentFrameworkProvider.tsx +24 -18
- package/src/const.ts +1 -1
- package/src/index.ts +1 -0
- package/src/stores/ApplicationEvent.ts +1 -0
- package/src/stores/ApplicationStore.ts +4 -1
- package/src/stores/CommandCenter.ts +1 -0
- package/src/stores/KeyboardShortcutsService.ts +43 -48
- package/src/stores/LegendApplicationDocumentation.ts +1 -0
- package/src/stores/PureLanguageSupport.ts +15 -1
- package/src/stores/TerminalService.ts +30 -0
- package/src/stores/terminal/Terminal.ts +259 -0
- package/src/stores/terminal/XTerm.ts +880 -0
- package/tsconfig.json +3 -0
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020-present, Goldman Sachs
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { Terminal as XTermTerminal, } from 'xterm';
|
|
17
|
+
import { WebLinksAddon as XTermWebLinksAddon } from 'xterm-addon-web-links';
|
|
18
|
+
import { FitAddon as XTermFitAddon } from 'xterm-addon-fit';
|
|
19
|
+
import { SearchAddon as XTermSearchAddon, } from 'xterm-addon-search';
|
|
20
|
+
import { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11';
|
|
21
|
+
import { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl';
|
|
22
|
+
import { MONOSPACED_FONT_FAMILY, TAB_SIZE } from '../../const.js';
|
|
23
|
+
import { Terminal, DISPLAY_ANSI_ESCAPE, ANSI_moveCursor, } from './Terminal.js';
|
|
24
|
+
import { ActionState, guaranteeNonNullable, IllegalStateError, isMatchingKeyCombination, LogEvent, prettyCONSTName, uniqBy, } from '@finos/legend-shared';
|
|
25
|
+
import { APPLICATION_EVENT } from '../ApplicationEvent.js';
|
|
26
|
+
import { forceDispatchKeyboardEvent } from '../../components/LegendApplicationComponentFrameworkProvider.js';
|
|
27
|
+
const LEGEND_XTERM_THEME = {
|
|
28
|
+
foreground: '#cccccc',
|
|
29
|
+
background: '#1e1e1e',
|
|
30
|
+
cursor: '#cccccc',
|
|
31
|
+
/** The accent color of the cursor (fg color for a block cursor) */
|
|
32
|
+
// cursorAccent?: string;
|
|
33
|
+
/** The selection background color when the terminal does not have focus (can be transparent) */
|
|
34
|
+
// selectionInactiveBackground?: string;
|
|
35
|
+
selectionBackground: '#264f78',
|
|
36
|
+
black: '#000000',
|
|
37
|
+
red: '#cd3131',
|
|
38
|
+
green: '#0dbc79',
|
|
39
|
+
yellow: '#e5e510',
|
|
40
|
+
blue: '#2472c8',
|
|
41
|
+
magenta: '#bc3fbc',
|
|
42
|
+
cyan: '#11a8cd',
|
|
43
|
+
white: '#e5e5e5',
|
|
44
|
+
brightBlack: '#666666',
|
|
45
|
+
brightRed: '#f14c4c',
|
|
46
|
+
brightGreen: '#23d18b',
|
|
47
|
+
brightYellow: '#f5f543',
|
|
48
|
+
brightBlue: '#3b8eea',
|
|
49
|
+
brightMagenta: '#d670d6',
|
|
50
|
+
brightCyan: '#29b8db',
|
|
51
|
+
brightWhite: '#e5e5e5',
|
|
52
|
+
};
|
|
53
|
+
const LEGEND_XTERM_SEARCH_THEME = {
|
|
54
|
+
matchOverviewRuler: '#d186167e',
|
|
55
|
+
activeMatchColorOverviewRuler: '#A0A0A0CC',
|
|
56
|
+
matchBackground: '#62331c',
|
|
57
|
+
activeMatchBackground: '#515C6A',
|
|
58
|
+
};
|
|
59
|
+
// robot acsii art
|
|
60
|
+
// See https://asciiartist.com/ascii-art-micro-robot/
|
|
61
|
+
const getHelpCommandContent = (commandRegistry) => `
|
|
62
|
+
${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}+-------------------------------------------------------+${DISPLAY_ANSI_ESCAPE.RESET}
|
|
63
|
+
${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}|${DISPLAY_ANSI_ESCAPE.RESET} ${DISPLAY_ANSI_ESCAPE.BRIGHT_GREEN}[@@]${DISPLAY_ANSI_ESCAPE.RESET} "Hi! Welcome to the HELP menu of Pure IDE" ${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}|${DISPLAY_ANSI_ESCAPE.RESET}
|
|
64
|
+
${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}|${DISPLAY_ANSI_ESCAPE.RESET} ${DISPLAY_ANSI_ESCAPE.BRIGHT_GREEN}/|__|\\${DISPLAY_ANSI_ESCAPE.RESET} ${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}|${DISPLAY_ANSI_ESCAPE.RESET}
|
|
65
|
+
${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}+--${DISPLAY_ANSI_ESCAPE.RESET} ${DISPLAY_ANSI_ESCAPE.BRIGHT_GREEN}d b${DISPLAY_ANSI_ESCAPE.RESET} ${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}-----------------------------------------------+${DISPLAY_ANSI_ESCAPE.RESET}
|
|
66
|
+
|
|
67
|
+
Following is the list of supported commands:
|
|
68
|
+
|
|
69
|
+
${uniqBy(Array.from(commandRegistry.values()), (config) => config.command)
|
|
70
|
+
.map((config) => `${DISPLAY_ANSI_ESCAPE.BRIGHT_GREEN}${config.command.padEnd(30)}${DISPLAY_ANSI_ESCAPE.RESET}${config.description}${config.aliases?.length
|
|
71
|
+
? `\n${''.padEnd(30)}Aliases: ${config.aliases.join(', ')}`
|
|
72
|
+
: ''}\n${''.padEnd(30)}Usage: ${DISPLAY_ANSI_ESCAPE.DIM}${config.usage}${DISPLAY_ANSI_ESCAPE.RESET}`)
|
|
73
|
+
.join('\n')}`;
|
|
74
|
+
const getCommonANSIEscapeSequencesForStyling = () => `
|
|
75
|
+
Common ANSI Escape Sequences for Styling:
|
|
76
|
+
|
|
77
|
+
${Object.entries(DISPLAY_ANSI_ESCAPE)
|
|
78
|
+
.map(([key, value]) => `${value}${prettyCONSTName(key).padEnd(20)}${DISPLAY_ANSI_ESCAPE.RESET
|
|
79
|
+
// NOTE: since these are recommended ANSI escape sequences which can be used
|
|
80
|
+
// by users in strings input in Pure IDE, they have to be Unicode escape, if we send
|
|
81
|
+
// the original hexadecimal escape as part of the string, some string escape handling
|
|
82
|
+
// in Pure seems to escape the leading slash of the ANSI escape sequence \x1B; however
|
|
83
|
+
// this is not the case of the escape sequence for Unicode, \u001b hence our logic here
|
|
84
|
+
} ${value.replace('\x1b', '\\u001b')}`)
|
|
85
|
+
.join('\n')}`;
|
|
86
|
+
const DEFAULT_USER = 'purist';
|
|
87
|
+
const DEFAULT_COMMAND_HEADER = `
|
|
88
|
+
${DISPLAY_ANSI_ESCAPE.BOLD}${DISPLAY_ANSI_ESCAPE.BRIGHT_BLUE}$${DEFAULT_USER}${DISPLAY_ANSI_ESCAPE.RESET}
|
|
89
|
+
${DISPLAY_ANSI_ESCAPE.BOLD}${DISPLAY_ANSI_ESCAPE.MAGENTA}\u276f${DISPLAY_ANSI_ESCAPE.RESET} `;
|
|
90
|
+
const COMMAND_START = '\u276f ';
|
|
91
|
+
export class XTerm extends Terminal {
|
|
92
|
+
instance;
|
|
93
|
+
resizer;
|
|
94
|
+
renderer;
|
|
95
|
+
searcher;
|
|
96
|
+
webLinkProvider;
|
|
97
|
+
_TEMPORARY__onKeyListener;
|
|
98
|
+
_TEMPORARY__onDataListener;
|
|
99
|
+
command = '';
|
|
100
|
+
commandHistory = [];
|
|
101
|
+
currentCommandSearchString = '';
|
|
102
|
+
commandHistoryNavigationIdx = undefined;
|
|
103
|
+
isRunningCommand = false;
|
|
104
|
+
setupState = ActionState.create();
|
|
105
|
+
constructor(applicationStore) {
|
|
106
|
+
super(applicationStore);
|
|
107
|
+
this.instance = new XTermTerminal({
|
|
108
|
+
allowProposedApi: true,
|
|
109
|
+
fontSize: 12,
|
|
110
|
+
letterSpacing: 2,
|
|
111
|
+
fontWeight: 400,
|
|
112
|
+
fontWeightBold: 700,
|
|
113
|
+
fontFamily: `"${MONOSPACED_FONT_FAMILY}", Menlo, Consolas, monospace`,
|
|
114
|
+
tabStopWidth: TAB_SIZE,
|
|
115
|
+
theme: LEGEND_XTERM_THEME,
|
|
116
|
+
overviewRulerWidth: 14,
|
|
117
|
+
scrollback: 10000,
|
|
118
|
+
convertEol: true,
|
|
119
|
+
// this is needed so we can control the cursor programmatically using escape sequences
|
|
120
|
+
scrollOnUserInput: false,
|
|
121
|
+
});
|
|
122
|
+
this.resizer = new XTermFitAddon();
|
|
123
|
+
this.searcher = new XTermSearchAddon();
|
|
124
|
+
this.renderer = new XTermWebglAddon();
|
|
125
|
+
}
|
|
126
|
+
setup(configuration) {
|
|
127
|
+
if (this.setupState.hasCompleted) {
|
|
128
|
+
throw new IllegalStateError(`Terminal is already set up`);
|
|
129
|
+
}
|
|
130
|
+
this.setupState.complete();
|
|
131
|
+
// Handling context loss: The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended.
|
|
132
|
+
// An easy, but suboptimal way, to handle this is by disposing of WebglAddon when the `webglcontextlost` event fires
|
|
133
|
+
// NOTE: we don't really have a resilient way to fallback right now, hopefully, the fallback is to render in DOM
|
|
134
|
+
this.renderer.onContextLoss(() => {
|
|
135
|
+
this.renderer.dispose();
|
|
136
|
+
});
|
|
137
|
+
this.instance.loadAddon(this.resizer);
|
|
138
|
+
this.instance.loadAddon(this.searcher);
|
|
139
|
+
this.instance.loadAddon(this.renderer);
|
|
140
|
+
this.instance.loadAddon(new XTermUnicode11Addon());
|
|
141
|
+
this.instance.unicode.activeVersion = '11';
|
|
142
|
+
// NOTE: since we render the terminal using webgl/canvas, event is not bubbled
|
|
143
|
+
// naturally through the DOM tree, we have to manually force this
|
|
144
|
+
this.instance.attachCustomKeyEventHandler((event) => {
|
|
145
|
+
// NOTE: this is a cheap way to handle hotkey, but this is really the only
|
|
146
|
+
// hotkey we want to support at local scope of the terminal
|
|
147
|
+
// also, since here we have prevent default and stop propagation, we have to do
|
|
148
|
+
// this here instead at in `onKey` handler
|
|
149
|
+
if (isMatchingKeyCombination(event, 'Control+KeyF') ||
|
|
150
|
+
isMatchingKeyCombination(event, 'Meta+KeyF')) {
|
|
151
|
+
// prevent default so as to not trigger browser platform search command
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
event.stopPropagation();
|
|
154
|
+
this.searchConfig.focus();
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
return true; // return true to indicate the event should still be handled by xterm
|
|
158
|
+
});
|
|
159
|
+
this.webLinkProvider = configuration?.webLinkProvider
|
|
160
|
+
? new XTermWebLinksAddon(configuration.webLinkProvider.handler, {
|
|
161
|
+
urlRegex: configuration.webLinkProvider.regex,
|
|
162
|
+
})
|
|
163
|
+
: new XTermWebLinksAddon();
|
|
164
|
+
this.instance.loadAddon(this.webLinkProvider);
|
|
165
|
+
(configuration?.commands ?? []).forEach((commandConfig) => {
|
|
166
|
+
[commandConfig.command, ...(commandConfig.aliases ?? [])].forEach((command) => {
|
|
167
|
+
if (!this.commandRegistry.has(command)) {
|
|
168
|
+
this.commandRegistry.set(command, commandConfig);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
this.applicationStore.log.warn(LogEvent.create(APPLICATION_EVENT.APPLICATION_TERMINAL_COMMAND_CONFIGURATION_CHECK_FAILURE), `Found multiple duplicated terminal commands '${command}'`);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
this.searcher.onDidChangeResults((result) => {
|
|
176
|
+
if (result) {
|
|
177
|
+
this.setSearchResultCount(result.resultCount);
|
|
178
|
+
this.setSearchCurrentResultIndex(result.resultIndex);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
this.setSearchResultCount(undefined);
|
|
182
|
+
this.setSearchCurrentResultIndex(undefined);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
// NOTE: `xterm` expects to be attached to a proper terminal program which handles
|
|
186
|
+
// input, since we can't do that yet, we implement a fairly basic input handling flow
|
|
187
|
+
// See https://github.com/xtermjs/xterm.js/issues/617#issuecomment-288849502
|
|
188
|
+
this._TEMPORARY__onKeyListener = this.instance.onKey(({ key, domEvent }) => {
|
|
189
|
+
// take care of command history navigation
|
|
190
|
+
if (domEvent.code === 'ArrowUp') {
|
|
191
|
+
this.setCommandFromHistory(this.commandHistoryNavigationIdx !== undefined
|
|
192
|
+
? this.commandHistoryNavigationIdx + 1
|
|
193
|
+
: 0);
|
|
194
|
+
return;
|
|
195
|
+
// reset current command in place
|
|
196
|
+
}
|
|
197
|
+
else if (domEvent.code === 'ArrowDown') {
|
|
198
|
+
if (this.commandHistoryNavigationIdx !== undefined) {
|
|
199
|
+
this.setCommandFromHistory(this.commandHistoryNavigationIdx === 0
|
|
200
|
+
? undefined
|
|
201
|
+
: this.commandHistoryNavigationIdx - 1);
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// reset navigation history the moment any other key is pressed
|
|
207
|
+
this.commandHistoryNavigationIdx = undefined;
|
|
208
|
+
}
|
|
209
|
+
if (domEvent.code === 'Enter') {
|
|
210
|
+
// run command
|
|
211
|
+
if (this.command.trim()) {
|
|
212
|
+
const text = this.command;
|
|
213
|
+
const [command, ...args] = text.replaceAll(/\s+/g, ' ').split(' ');
|
|
214
|
+
this.addCommandToHistory(this.command);
|
|
215
|
+
if (!command) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const matchingCommand = this.commandRegistry.get(command);
|
|
219
|
+
if (!matchingCommand) {
|
|
220
|
+
this.fail(`command not found: ${command}`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (this.isRunningCommand) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this.isRunningCommand = true;
|
|
227
|
+
matchingCommand
|
|
228
|
+
.handler(args.map((arg) => arg.trim()), command, text)
|
|
229
|
+
.finally(() => {
|
|
230
|
+
this.isRunningCommand = false;
|
|
231
|
+
if (!this.isFlushed) {
|
|
232
|
+
this.abort();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else if (isMatchingKeyCombination(domEvent, 'Control+KeyC') ||
|
|
238
|
+
isMatchingKeyCombination(domEvent, 'Control+KeyD')) {
|
|
239
|
+
// abort command
|
|
240
|
+
this.abort();
|
|
241
|
+
}
|
|
242
|
+
else if (domEvent.code === 'Backspace') {
|
|
243
|
+
// Alt: jump word only, Ctrl: jump to end
|
|
244
|
+
// this would apply for Delete, ArrowLeft, ArrowRight
|
|
245
|
+
this.deleteFromCommand(domEvent.altKey || domEvent.ctrlKey
|
|
246
|
+
? this.computeCursorJumpMovement(true)
|
|
247
|
+
: -1);
|
|
248
|
+
}
|
|
249
|
+
else if (domEvent.code === 'Delete') {
|
|
250
|
+
this.deleteFromCommand(domEvent.altKey || domEvent.ctrlKey
|
|
251
|
+
? this.computeCursorJumpMovement(false)
|
|
252
|
+
: 1);
|
|
253
|
+
}
|
|
254
|
+
else if (domEvent.code === 'ArrowLeft') {
|
|
255
|
+
const movement = this.computeCursorMovement(domEvent.altKey || domEvent.ctrlKey
|
|
256
|
+
? this.computeCursorJumpMovement(true)
|
|
257
|
+
: -1);
|
|
258
|
+
// console.log('left', movement);
|
|
259
|
+
this.instance.scrollLines(movement.scroll);
|
|
260
|
+
this.instance.write(movement.seq);
|
|
261
|
+
}
|
|
262
|
+
else if (domEvent.code === 'ArrowRight') {
|
|
263
|
+
const movement = this.computeCursorMovement(domEvent.altKey || domEvent.ctrlKey
|
|
264
|
+
? this.computeCursorJumpMovement(false)
|
|
265
|
+
: 1);
|
|
266
|
+
// console.log('right', movement);
|
|
267
|
+
this.instance.scrollLines(movement.scroll);
|
|
268
|
+
this.instance.write(movement.seq);
|
|
269
|
+
}
|
|
270
|
+
else if (
|
|
271
|
+
// use key here so we absolute do not allow any characters other than these
|
|
272
|
+
// being added to the input command
|
|
273
|
+
key.match(/^[A-Za-z0-9!@#$%^&*()\-_=+"':;,.<>/?[\]{}|\\~` ]$/)) {
|
|
274
|
+
// commonly supported keys
|
|
275
|
+
this.writeToCommand(key);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// for the rest, allow the keyboard event to be bubbled to
|
|
279
|
+
// application keyboard shortcuts handler
|
|
280
|
+
forceDispatchKeyboardEvent(domEvent);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
// this is needed to support copy-pasting
|
|
284
|
+
this._TEMPORARY__onDataListener = this.instance.onData((val) => {
|
|
285
|
+
// only support pasting (not meant for 1 character though) and special functions starting with special
|
|
286
|
+
// ANSI escape sequence
|
|
287
|
+
if (val.length > 1 && !val.startsWith('\x1b')) {
|
|
288
|
+
this.writeToCommand(val
|
|
289
|
+
// remove all unsupported characters, including newline
|
|
290
|
+
.replaceAll(/[^A-Za-z0-9!@#$%^&*()\-_=+"':;,.<>/?[\]{}|\\~` ]/g, '')
|
|
291
|
+
.trimEnd());
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// NOTE: this is fairly HACKY way to detect command
|
|
296
|
+
// we don't really have a better solution at the moment,
|
|
297
|
+
// but we should come with more systematic way of persisting the start line of command
|
|
298
|
+
// the challenge with this is due to text-reflow
|
|
299
|
+
//
|
|
300
|
+
// there is also a quriky known issue with text-reflow and the line with cursor
|
|
301
|
+
// See https://github.com/xtermjs/xterm.js/issues/1941#issuecomment-463660633
|
|
302
|
+
getCommandRange() {
|
|
303
|
+
const buffer = this.instance.buffer.active;
|
|
304
|
+
const cols = this.instance.cols;
|
|
305
|
+
const commandText = `${COMMAND_START}${this.command}`;
|
|
306
|
+
const commandFirstLine = `${COMMAND_START}${this.command.substring(0, cols - COMMAND_START.length)}${this.command.length < cols - COMMAND_START.length
|
|
307
|
+
? ' '.repeat(cols - this.command.length - COMMAND_START.length)
|
|
308
|
+
: ''}`;
|
|
309
|
+
let startY = 0;
|
|
310
|
+
let cursorIdx = 0;
|
|
311
|
+
for (let i = buffer.baseY + buffer.cursorY; i > -1; --i) {
|
|
312
|
+
const line = guaranteeNonNullable(buffer.getLine(i));
|
|
313
|
+
const lineText = line.translateToString();
|
|
314
|
+
if (lineText === commandFirstLine) {
|
|
315
|
+
startY = i;
|
|
316
|
+
cursorIdx +=
|
|
317
|
+
(i === buffer.baseY + buffer.cursorY ? buffer.cursorX : cols) -
|
|
318
|
+
COMMAND_START.length;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
cursorIdx +=
|
|
323
|
+
i === buffer.baseY + buffer.cursorY ? buffer.cursorX : cols;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// start line == -1 is the rare case where the command is too long and exceeds the buffer length
|
|
327
|
+
// leading to incomplete command being captured
|
|
328
|
+
return {
|
|
329
|
+
startY,
|
|
330
|
+
startX: COMMAND_START.length,
|
|
331
|
+
endY: startY + (commandText.length - (commandText.length % cols)) / cols,
|
|
332
|
+
endX: commandText.length % cols,
|
|
333
|
+
cursorIdx,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
computeCursorJumpMovement(back) {
|
|
337
|
+
const range = this.getCommandRange();
|
|
338
|
+
let distance = undefined;
|
|
339
|
+
let foundWord = false;
|
|
340
|
+
// scan for the boundary of the closest word to the cursor position
|
|
341
|
+
if (back) {
|
|
342
|
+
for (let i = range.cursorIdx - 1; i > -1; --i) {
|
|
343
|
+
const char = this.command.charAt(i);
|
|
344
|
+
if (char.match(/\w/)) {
|
|
345
|
+
if (!foundWord) {
|
|
346
|
+
foundWord = true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
if (foundWord) {
|
|
351
|
+
distance = range.cursorIdx - i - 1;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
for (let i = range.cursorIdx + 1; i < this.command.length; ++i) {
|
|
359
|
+
const char = this.command.charAt(i);
|
|
360
|
+
if (char.match(/\w/)) {
|
|
361
|
+
if (!foundWord) {
|
|
362
|
+
foundWord = true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
if (foundWord) {
|
|
367
|
+
distance = i - range.cursorIdx - 1;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (distance === undefined) {
|
|
374
|
+
distance = back ? range.cursorIdx : this.command.length - range.cursorIdx;
|
|
375
|
+
}
|
|
376
|
+
return back ? -distance : distance;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Generate the ANSI escape sequence for new cursor position
|
|
380
|
+
* after being moved by the the number of cells.
|
|
381
|
+
*
|
|
382
|
+
* @param val a number (negative means cursor move leftwards)
|
|
383
|
+
* @param limit whether to limit the movement of the cursor by the command range
|
|
384
|
+
* @returns cursor movement information including the ANSI escape sequence for new cursor position and scroll distance
|
|
385
|
+
*/
|
|
386
|
+
computeCursorMovement(val, limit = true) {
|
|
387
|
+
const buffer = this.instance.buffer.active;
|
|
388
|
+
const cols = this.instance.cols;
|
|
389
|
+
const rows = this.instance.rows;
|
|
390
|
+
const range = this.getCommandRange();
|
|
391
|
+
const maxDistance = limit
|
|
392
|
+
? val < 0
|
|
393
|
+
? range.cursorIdx
|
|
394
|
+
: this.command.length - range.cursorIdx
|
|
395
|
+
: val;
|
|
396
|
+
const distance = Math.min(Math.abs(val), maxDistance);
|
|
397
|
+
let newCursorX = buffer.cursorX;
|
|
398
|
+
let newCursorY = buffer.cursorY;
|
|
399
|
+
let abs_cursorY = buffer.baseY + buffer.cursorY;
|
|
400
|
+
if (val < 0) {
|
|
401
|
+
// move leftwards
|
|
402
|
+
newCursorX = (cols + ((buffer.cursorX - distance) % cols)) % cols;
|
|
403
|
+
newCursorY =
|
|
404
|
+
buffer.cursorY -
|
|
405
|
+
(distance > buffer.cursorX ? Math.ceil(distance / cols) : 0);
|
|
406
|
+
abs_cursorY = newCursorY + buffer.baseY;
|
|
407
|
+
newCursorY = Math.max(newCursorY, -1);
|
|
408
|
+
}
|
|
409
|
+
else if (val > 0) {
|
|
410
|
+
// move rightwards
|
|
411
|
+
newCursorX = (buffer.cursorX + distance) % cols;
|
|
412
|
+
newCursorY =
|
|
413
|
+
buffer.cursorY +
|
|
414
|
+
(buffer.cursorX + distance >= cols
|
|
415
|
+
? Math.floor((buffer.cursorX + distance) / cols)
|
|
416
|
+
: 0);
|
|
417
|
+
abs_cursorY = newCursorY + buffer.baseY;
|
|
418
|
+
newCursorY = Math.min(newCursorY, rows - 1);
|
|
419
|
+
}
|
|
420
|
+
const scroll = abs_cursorY > buffer.viewportY + rows
|
|
421
|
+
? abs_cursorY - (buffer.viewportY + rows)
|
|
422
|
+
: abs_cursorY < buffer.viewportY
|
|
423
|
+
? abs_cursorY - buffer.viewportY
|
|
424
|
+
: 0;
|
|
425
|
+
return {
|
|
426
|
+
// NOTE: currently, there is a design limitation with programmatically set the cursor using escape sequence
|
|
427
|
+
// by design, the scrollback (everything above the viewport/ybase) is readonly, and most terminals work like this.
|
|
428
|
+
// So for very long command that causes an overflow, one cannot set the cursor position pass the `baseY`
|
|
429
|
+
// this will affect both navigation and delete/backspace behavior
|
|
430
|
+
// See https://github.com/xtermjs/xterm.js/issues/4405
|
|
431
|
+
seq: ANSI_moveCursor(newCursorY + 1, newCursorX + 1),
|
|
432
|
+
scroll,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Write value to command with awareness of the current cursor position
|
|
437
|
+
*/
|
|
438
|
+
writeToCommand(val) {
|
|
439
|
+
const range = this.getCommandRange();
|
|
440
|
+
const left = this.command.slice(0, range.cursorIdx);
|
|
441
|
+
const right = this.command.slice(range.cursorIdx);
|
|
442
|
+
const movement = this.computeCursorMovement(val.length, false);
|
|
443
|
+
this.instance.scrollLines(movement.scroll);
|
|
444
|
+
this.instance.write(val +
|
|
445
|
+
right +
|
|
446
|
+
// update the cursor
|
|
447
|
+
movement.seq);
|
|
448
|
+
this.setCommand(left + val + right);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Remove number of characters from command with awareness of the current cursor position
|
|
452
|
+
* NOTE: negative number means backward deleting (i.e. backspace)
|
|
453
|
+
*/
|
|
454
|
+
deleteFromCommand(val) {
|
|
455
|
+
// console.log(val);
|
|
456
|
+
const range = this.getCommandRange();
|
|
457
|
+
const maxDistance = val < 0 ? range.cursorIdx : this.command.length - range.cursorIdx;
|
|
458
|
+
const distance = Math.min(Math.abs(val), maxDistance);
|
|
459
|
+
let left;
|
|
460
|
+
let right;
|
|
461
|
+
let cursorMovement;
|
|
462
|
+
if (val === 0) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
else if (val < 0) {
|
|
466
|
+
// remove leftwards
|
|
467
|
+
left = this.command.slice(0, range.cursorIdx - distance);
|
|
468
|
+
right = this.command.slice(range.cursorIdx, this.command.length);
|
|
469
|
+
cursorMovement = -distance;
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
// remove rightwards
|
|
473
|
+
left = this.command.slice(0, range.cursorIdx);
|
|
474
|
+
right = this.command.slice(range.cursorIdx + distance, this.command.length);
|
|
475
|
+
cursorMovement = 0;
|
|
476
|
+
}
|
|
477
|
+
const movement = this.computeCursorMovement(cursorMovement);
|
|
478
|
+
this.instance.scrollLines(movement.scroll);
|
|
479
|
+
this.instance.write(
|
|
480
|
+
// reset cursor to start of command, basically here, we're rewriting the entire command
|
|
481
|
+
ANSI_moveCursor(range.startY + 1, range.startX + 1) +
|
|
482
|
+
left +
|
|
483
|
+
right +
|
|
484
|
+
// fill space to erase cells rendered from previous command
|
|
485
|
+
' '.repeat(distance) +
|
|
486
|
+
// move the cursor as well
|
|
487
|
+
movement.seq);
|
|
488
|
+
this.setCommand(left + right);
|
|
489
|
+
}
|
|
490
|
+
get isSetup() {
|
|
491
|
+
return this.setupState.hasCompleted;
|
|
492
|
+
}
|
|
493
|
+
isFocused() {
|
|
494
|
+
return document.activeElement === this.instance.textarea;
|
|
495
|
+
}
|
|
496
|
+
mount(container) {
|
|
497
|
+
if (!this.setupState.hasCompleted) {
|
|
498
|
+
throw new IllegalStateError(`XTerm terminal has not been set up yet`);
|
|
499
|
+
}
|
|
500
|
+
this.instance.open(container);
|
|
501
|
+
}
|
|
502
|
+
dispose() {
|
|
503
|
+
this.searcher.dispose();
|
|
504
|
+
this.resizer.dispose();
|
|
505
|
+
this.renderer.dispose();
|
|
506
|
+
this.webLinkProvider?.dispose();
|
|
507
|
+
this._TEMPORARY__onKeyListener?.dispose();
|
|
508
|
+
this._TEMPORARY__onDataListener?.dispose();
|
|
509
|
+
this.instance.dispose();
|
|
510
|
+
}
|
|
511
|
+
autoResize() {
|
|
512
|
+
this.resizer.fit();
|
|
513
|
+
}
|
|
514
|
+
focus() {
|
|
515
|
+
this.instance.focus();
|
|
516
|
+
}
|
|
517
|
+
addCommandToHistory(val) {
|
|
518
|
+
// if this is the same as previous command, do not push it to the history stack
|
|
519
|
+
if (val === this.commandHistory.at(0)) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
// history command is essentially a stack, so we only insert at the beginning
|
|
523
|
+
this.commandHistory.unshift(val);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* This methods help update the current command to a command in history
|
|
527
|
+
* stack, it does the necessary resetting and helps properly update
|
|
528
|
+
* the history navigation index
|
|
529
|
+
*/
|
|
530
|
+
setCommandFromHistory(idx) {
|
|
531
|
+
const val = idx === undefined
|
|
532
|
+
? this.currentCommandSearchString
|
|
533
|
+
: // NOTE: only consider commands starting with the original command
|
|
534
|
+
// also note that empty string naturaly match all history command
|
|
535
|
+
this.commandHistory
|
|
536
|
+
.filter((command) => command.startsWith(this.currentCommandSearchString))
|
|
537
|
+
.at(idx);
|
|
538
|
+
if (val !== undefined) {
|
|
539
|
+
let range = this.getCommandRange();
|
|
540
|
+
this.instance.write(
|
|
541
|
+
// reset cursor to start of command and rewrite the entire command
|
|
542
|
+
ANSI_moveCursor(range.startY + 1, range.startX + 1) +
|
|
543
|
+
val.padEnd(this.command.length));
|
|
544
|
+
this.command = val;
|
|
545
|
+
range = this.getCommandRange();
|
|
546
|
+
this.instance.write(
|
|
547
|
+
// reset cursor to command end
|
|
548
|
+
ANSI_moveCursor(range.endY + 1, range.endX + 1));
|
|
549
|
+
this.commandHistoryNavigationIdx = idx;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
setCommand(val) {
|
|
553
|
+
this.command = val;
|
|
554
|
+
this.currentCommandSearchString = val;
|
|
555
|
+
this.commandHistoryNavigationIdx = undefined;
|
|
556
|
+
}
|
|
557
|
+
newCommand() {
|
|
558
|
+
this.instance.write(DEFAULT_COMMAND_HEADER);
|
|
559
|
+
this.setCommand('');
|
|
560
|
+
}
|
|
561
|
+
newSystemCommand(command) {
|
|
562
|
+
// if another command is already running, we don't need to print the command header anymore
|
|
563
|
+
// the potential pitfall here is that we could have another process prints to the
|
|
564
|
+
// terminal while the command is being run. Nothing much we can do here for now.
|
|
565
|
+
if (!this.isRunningCommand) {
|
|
566
|
+
if (this.command) {
|
|
567
|
+
this.abort();
|
|
568
|
+
this.newCommand();
|
|
569
|
+
}
|
|
570
|
+
this.instance.write(`${DISPLAY_ANSI_ESCAPE.DIM}(system: ${command})\n${DISPLAY_ANSI_ESCAPE.RESET}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Flush the terminal screen completely
|
|
575
|
+
*
|
|
576
|
+
* Probably due to write buffer batching, calling `reset` or `clear` on xterm terminal immediately after
|
|
577
|
+
* write commands will not work. To solve this, we can either promisify the `reset` call or write the ANSI
|
|
578
|
+
* reset sequence \x1bc
|
|
579
|
+
*/
|
|
580
|
+
flushScreen() {
|
|
581
|
+
this.instance.write('\x1bc');
|
|
582
|
+
this.instance.reset();
|
|
583
|
+
}
|
|
584
|
+
get isFlushed() {
|
|
585
|
+
const buffer = this.instance.buffer.active;
|
|
586
|
+
let isLastLineEmpty = true;
|
|
587
|
+
for (let i = buffer.baseY + buffer.cursorY; i > -1; --i) {
|
|
588
|
+
const line = guaranteeNonNullable(buffer.getLine(i));
|
|
589
|
+
const lineText = line.translateToString();
|
|
590
|
+
// skip empty lines
|
|
591
|
+
if (!lineText.trim()) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
isLastLineEmpty = lineText !== COMMAND_START;
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return this.command === '' && isLastLineEmpty;
|
|
600
|
+
}
|
|
601
|
+
clear() {
|
|
602
|
+
this.flushScreen();
|
|
603
|
+
this.instance.scrollToTop();
|
|
604
|
+
this.newCommand();
|
|
605
|
+
}
|
|
606
|
+
resetANSIStyling() {
|
|
607
|
+
this.instance.write(DISPLAY_ANSI_ESCAPE.RESET);
|
|
608
|
+
}
|
|
609
|
+
showHelp() {
|
|
610
|
+
this.resetANSIStyling();
|
|
611
|
+
this.instance.scrollToBottom();
|
|
612
|
+
if (!this.isFlushed && !this.isRunningCommand) {
|
|
613
|
+
this.abort();
|
|
614
|
+
}
|
|
615
|
+
this.instance.write(getHelpCommandContent(this.commandRegistry));
|
|
616
|
+
this.abort();
|
|
617
|
+
}
|
|
618
|
+
showCommonANSIEscapeSequences() {
|
|
619
|
+
this.resetANSIStyling();
|
|
620
|
+
this.instance.scrollToBottom();
|
|
621
|
+
if (!this.isFlushed && !this.isRunningCommand) {
|
|
622
|
+
this.abort();
|
|
623
|
+
}
|
|
624
|
+
this.instance.write(getCommonANSIEscapeSequencesForStyling());
|
|
625
|
+
this.abort();
|
|
626
|
+
}
|
|
627
|
+
abort() {
|
|
628
|
+
this.resetANSIStyling();
|
|
629
|
+
this.instance.write('\n');
|
|
630
|
+
this.newCommand();
|
|
631
|
+
this.instance.scrollToBottom();
|
|
632
|
+
this.isRunningCommand = false;
|
|
633
|
+
}
|
|
634
|
+
fail(error, opts) {
|
|
635
|
+
if (opts?.systemCommand) {
|
|
636
|
+
this.newSystemCommand(opts.systemCommand);
|
|
637
|
+
}
|
|
638
|
+
this.instance.write(`\n${DISPLAY_ANSI_ESCAPE.RED}${error}${DISPLAY_ANSI_ESCAPE.RED}`);
|
|
639
|
+
this.abort();
|
|
640
|
+
}
|
|
641
|
+
output(val, opts) {
|
|
642
|
+
this.resetANSIStyling();
|
|
643
|
+
if ((!opts?.clear || this.preserveLog) && opts?.systemCommand) {
|
|
644
|
+
this.newSystemCommand(opts.systemCommand);
|
|
645
|
+
}
|
|
646
|
+
if (!this.preserveLog && opts?.clear) {
|
|
647
|
+
this.flushScreen();
|
|
648
|
+
}
|
|
649
|
+
else if (this.preserveLog || this.isRunningCommand) {
|
|
650
|
+
this.instance.write('\n');
|
|
651
|
+
}
|
|
652
|
+
this.instance.write(val);
|
|
653
|
+
this.resetANSIStyling();
|
|
654
|
+
this.instance.write('\n');
|
|
655
|
+
this.instance.scrollToBottom();
|
|
656
|
+
this.newCommand();
|
|
657
|
+
}
|
|
658
|
+
search(val) {
|
|
659
|
+
this.searcher.findNext(val, {
|
|
660
|
+
decorations: LEGEND_XTERM_SEARCH_THEME,
|
|
661
|
+
regex: this.searchConfig.useRegex,
|
|
662
|
+
wholeWord: this.searchConfig.matchWholeWord,
|
|
663
|
+
caseSensitive: this.searchConfig.matchCaseSensitive,
|
|
664
|
+
// do incremental search so that the expansion will be expanded the selection if it
|
|
665
|
+
// still matches the term the user typed.
|
|
666
|
+
incremental: true,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
clearSearch() {
|
|
670
|
+
this.searcher.clearDecorations();
|
|
671
|
+
this.instance.clearSelection();
|
|
672
|
+
this.setSearchText('');
|
|
673
|
+
this.setSearchResultCount(undefined);
|
|
674
|
+
this.setSearchCurrentResultIndex(undefined);
|
|
675
|
+
}
|
|
676
|
+
findPrevious() {
|
|
677
|
+
this.searcher.findPrevious(this.searchConfig.searchText, {
|
|
678
|
+
decorations: LEGEND_XTERM_SEARCH_THEME,
|
|
679
|
+
regex: this.searchConfig.useRegex,
|
|
680
|
+
wholeWord: this.searchConfig.matchWholeWord,
|
|
681
|
+
caseSensitive: this.searchConfig.matchCaseSensitive,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
findNext() {
|
|
685
|
+
this.searcher.findNext(this.searchConfig.searchText, {
|
|
686
|
+
decorations: LEGEND_XTERM_SEARCH_THEME,
|
|
687
|
+
regex: this.searchConfig.useRegex,
|
|
688
|
+
wholeWord: this.searchConfig.matchWholeWord,
|
|
689
|
+
caseSensitive: this.searchConfig.matchCaseSensitive,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
//# sourceMappingURL=XTerm.js.map
|