@electerm/electerm-react 5.3.26 → 5.5.9
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/README.md +246 -12
- package/client/common/choose-save-folder.js +21 -2
- package/client/common/constants.js +20 -6
- package/client/common/create-title.jsx +6 -0
- package/client/common/default-setting.js +15 -0
- package/client/common/init-setting-item.js +5 -0
- package/client/common/platform.js +24 -0
- package/client/components/ai/agent-tool-call-card.jsx +10 -2
- package/client/components/ai/agent.js +4 -1
- package/client/components/ai/ai-attachments.js +87 -0
- package/client/components/ai/ai-chat-empty.jsx +43 -0
- package/client/components/ai/ai-chat-entry.jsx +13 -4
- package/client/components/ai/ai-chat-history-item.jsx +28 -4
- package/client/components/ai/ai-chat-history.jsx +2 -1
- package/client/components/ai/ai-chat.jsx +104 -13
- package/client/components/ai/ai-config-props.js +4 -130
- package/client/components/ai/ai-config.jsx +135 -15
- package/client/components/ai/ai-presets.js +173 -0
- package/client/components/ai/ai.styl +83 -0
- package/client/components/bg/css-overwrite.jsx +11 -0
- package/client/components/bookmark-form/bookmark-schema.js +4 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/config/common-fields.js +12 -0
- package/client/components/bookmark-form/config/local.js +3 -2
- package/client/components/bookmark-form/config/serial.js +3 -2
- package/client/components/bookmark-form/config/ssh.js +2 -1
- package/client/components/bookmark-form/config/telnet.js +3 -2
- package/client/components/common/item-filter.jsx +76 -0
- package/client/components/common/item-filter.styl +54 -0
- package/client/components/common/lazy-boundary.jsx +34 -0
- package/client/components/common/opacity.jsx +3 -1
- package/client/components/footer/cmd-history.jsx +132 -45
- package/client/components/footer/cmd-history.styl +42 -4
- package/client/components/footer/footer-entry.jsx +35 -5
- package/client/components/footer/footer.styl +8 -1
- package/client/components/footer/multi-tab-run-modal.jsx +83 -0
- package/client/components/footer/tab-select.jsx +34 -17
- package/client/components/layout/layout.jsx +23 -31
- package/client/components/layout/pixed.js +3 -0
- package/client/components/main/main.jsx +55 -28
- package/client/components/main/term-fullscreen.styl +9 -3
- package/client/components/quick-commands/qm.styl +3 -2
- package/client/components/quick-commands/quick-command-create-modal.jsx +60 -0
- package/client/components/quick-commands/quick-commands-form-elem.jsx +10 -4
- package/client/components/rdp/file-transfer.js +4 -4
- package/client/components/remote-monitor/monitor-details.jsx +458 -0
- package/client/components/remote-monitor/monitor-details.styl +87 -0
- package/client/components/remote-monitor/monitor-model.js +764 -0
- package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
- package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
- package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
- package/client/components/remote-monitor/session-monitor.js +361 -0
- package/client/components/remote-monitor/use-monitor-details.js +10 -0
- package/client/components/remote-monitor/visibility.js +18 -0
- package/client/components/session/session-control.jsx +29 -2
- package/client/components/session/session.jsx +40 -18
- package/client/components/session/sessions.jsx +5 -2
- package/client/components/setting-panel/setting-common.jsx +25 -56
- package/client/components/setting-panel/setting-modal.jsx +57 -42
- package/client/components/setting-panel/setting-terminal.jsx +8 -2
- package/client/components/setting-panel/setting.styl +1 -1
- package/client/components/setting-panel/tab-triggers.jsx +71 -0
- package/client/components/sftp/file-compare-modal.jsx +6 -3
- package/client/components/sftp/file-item.jsx +5 -3
- package/client/components/sftp/owner-list.js +11 -2
- package/client/components/sftp/sftp-entry.jsx +4 -0
- package/client/components/side-panel-r/side-panel-r.jsx +2 -2
- package/client/components/sidebar/side-panel.jsx +1 -1
- package/client/components/sys-menu/menu-btn.jsx +30 -1
- package/client/components/tabs/index.jsx +2 -2
- package/client/components/tabs/tab-title.jsx +3 -2
- package/client/components/tabs/tab.jsx +5 -1
- package/client/components/terminal/attach-addon-custom.js +295 -49
- package/client/components/terminal/automation/index.js +64 -0
- package/client/components/terminal/automation/keys.js +49 -0
- package/client/components/terminal/automation/stream-matcher.js +82 -0
- package/client/components/terminal/automation/strip-ansi.js +19 -0
- package/client/components/terminal/automation/trigger-engine.js +233 -0
- package/client/components/terminal/automation/trigger-presets.js +110 -0
- package/client/components/terminal/command-tracker-addon.js +31 -1
- package/client/components/terminal/mixins/index.js +27 -0
- package/client/components/terminal/mixins/term-attach.js +123 -0
- package/client/components/terminal/mixins/term-config.jsx +84 -0
- package/client/components/terminal/mixins/term-content.js +126 -0
- package/client/components/terminal/mixins/term-context-menu.jsx +323 -0
- package/client/components/terminal/mixins/term-file-drop.js +149 -0
- package/client/components/terminal/mixins/term-init.js +122 -0
- package/client/components/terminal/mixins/term-log.jsx +96 -0
- package/client/components/terminal/mixins/term-resize.js +87 -0
- package/client/components/terminal/mixins/term-socket.js +433 -0
- package/client/components/terminal/mixins/term-suggestions.js +224 -0
- package/client/components/terminal/mixins/term-theme.js +190 -0
- package/client/components/terminal/mixins/term-touch.js +523 -0
- package/client/components/terminal/shell-detect.js +37 -0
- package/client/components/terminal/shell.js +0 -28
- package/client/components/terminal/shortcut-bar-defs.js +30 -4
- package/client/components/terminal/shortcut-bar-edit.jsx +40 -9
- package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
- package/client/components/terminal/shortcut-bar.jsx +17 -10
- package/client/components/terminal/shortcut-bar.styl +4 -0
- package/client/components/terminal/ssh-reload-state.js +96 -0
- package/client/components/terminal/startup-queue.js +433 -0
- package/client/components/terminal/terminal-interactive.jsx +32 -0
- package/client/components/terminal/terminal-select-text.jsx +69 -0
- package/client/components/terminal/terminal.jsx +227 -2064
- package/client/components/terminal/terminal.styl +61 -0
- package/client/components/terminal/transfer-client-base.js +5 -29
- package/client/components/terminal/trzsz-client.js +1 -3
- package/client/components/terminal/xmodem-client.js +1 -3
- package/client/components/terminal/xterm-loader.js +7 -0
- package/client/components/terminal/zmodem-client.js +1 -3
- package/client/components/terminal-info/base.jsx +18 -70
- package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
- package/client/components/terminal-info/terminal-info.jsx +50 -59
- package/client/components/terminal-info/terminal-info.styl +6 -49
- package/client/components/text-editor/open-text-editor.js +30 -0
- package/client/components/text-editor/text-editor-entry.jsx +17 -6
- package/client/components/text-editor/text-editor.jsx +14 -0
- package/client/components/triggers/trigger-editor.jsx +246 -0
- package/client/components/triggers/trigger-form.jsx +200 -0
- package/client/components/triggers/trigger-session-modal.jsx +199 -0
- package/client/components/triggers/trigger-setting-list.jsx +89 -0
- package/client/components/triggers/trigger-transport.jsx +60 -0
- package/client/components/triggers/triggers-field.jsx +19 -0
- package/client/components/widgets/widgets-list.jsx +11 -1
- package/client/css/mobile.styl +29 -8
- package/client/store/common.js +12 -1
- package/client/store/init-state.js +7 -0
- package/client/store/mcp-handler.js +158 -30
- package/client/store/setting.js +9 -0
- package/client/store/store.js +2 -0
- package/client/store/sync.js +2 -0
- package/client/store/tab.js +42 -3
- package/client/store/trigger.js +118 -0
- package/client/store/watch.js +13 -5
- package/client/views/index.pug +3 -0
- package/package.json +1 -1
- package/client/components/terminal-info/activity.jsx +0 -57
- package/client/components/terminal-info/data-cols-parser.jsx +0 -54
- package/client/components/terminal-info/disk.jsx +0 -45
- package/client/components/terminal-info/format-disks.js +0 -45
- package/client/components/terminal-info/network.jsx +0 -121
- package/client/components/terminal-info/resource.jsx +0 -99
- package/client/components/terminal-info/run-cmd.jsx +0 -263
- package/client/components/terminal-info/sys-info.jsx +0 -56
- package/client/components/terminal-info/up.jsx +0 -17
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup queue for a terminal session.
|
|
3
|
+
*
|
|
4
|
+
* Everything that has to be typed into a freshly connected shell goes through
|
|
5
|
+
* here, strictly one item at a time:
|
|
6
|
+
*
|
|
7
|
+
* shell integration injection -> cd/startDirectory -> runScripts
|
|
8
|
+
*
|
|
9
|
+
* Two rules keep it reliable:
|
|
10
|
+
*
|
|
11
|
+
* 1. Only one drain loop owns the queue at a time, and every queue generation
|
|
12
|
+
* carries a token. A reconnect (or a config toggle that rebuilds the queue)
|
|
13
|
+
* bumps the token, so a loop still waiting on the *old* connection stops
|
|
14
|
+
* instead of stealing items from the new queue - that used to silently drop
|
|
15
|
+
* the remaining run scripts.
|
|
16
|
+
* 2. After each script we wait for the shell to settle (a fresh prompt, or a
|
|
17
|
+
* quiet period) before typing the next one, so nothing is ever typed into a
|
|
18
|
+
* shell that is still busy. Every wait is bounded, so a hung shell can only
|
|
19
|
+
* delay the queue, never stop it.
|
|
20
|
+
*/
|
|
21
|
+
import {
|
|
22
|
+
getShellIntegrationCommand,
|
|
23
|
+
detectShellType
|
|
24
|
+
} from './shell.js'
|
|
25
|
+
import {
|
|
26
|
+
createRestoreCwdCommand,
|
|
27
|
+
createWindowsRestoreCwdCommand,
|
|
28
|
+
isRestorableTerminalTab
|
|
29
|
+
} from './ssh-reload-state.js'
|
|
30
|
+
import { isMac, isWin } from '../../common/platform.js'
|
|
31
|
+
|
|
32
|
+
// Pacing: `settleIdleMs` of silence counts as "the shell is done",
|
|
33
|
+
// `settleTimeoutMs` caps the wait so a script that never returns can not
|
|
34
|
+
// stall the queue.
|
|
35
|
+
const settleIdleMs = 400
|
|
36
|
+
const settleTimeoutMs = 3000
|
|
37
|
+
// Ceiling for the best-effort remote shell probe. detectRemoteShell opens an
|
|
38
|
+
// extra exec channel and runCmd has no timeout of its own, so a server that
|
|
39
|
+
// never answers would otherwise block the whole queue forever.
|
|
40
|
+
const remoteShellDetectTimeoutMs = 3000
|
|
41
|
+
|
|
42
|
+
export class StartupQueue {
|
|
43
|
+
/**
|
|
44
|
+
* @param {object} term - the terminal component (host)
|
|
45
|
+
* @param {object} [deps] - `detectRemoteShell` needs the network, so it is
|
|
46
|
+
* injected: it keeps this module free of the app runtime and makes the
|
|
47
|
+
* queue testable on its own.
|
|
48
|
+
*/
|
|
49
|
+
constructor (term, deps = {}) {
|
|
50
|
+
this.term = term
|
|
51
|
+
this.detectRemoteShell = deps.detectRemoteShell || (() => Promise.resolve('sh'))
|
|
52
|
+
this.queue = []
|
|
53
|
+
this.token = 0
|
|
54
|
+
this.running = false
|
|
55
|
+
this.shellInjected = false
|
|
56
|
+
this.shellType = null
|
|
57
|
+
this.timers = new Set()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
dispose = () => {
|
|
61
|
+
for (const t of this.timers) {
|
|
62
|
+
clearTimeout(t)
|
|
63
|
+
}
|
|
64
|
+
this.timers.clear()
|
|
65
|
+
this.queue = []
|
|
66
|
+
this.running = false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- host accessors ----
|
|
70
|
+
|
|
71
|
+
get addon () {
|
|
72
|
+
return this.term.attachAddon
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get cmdAddon () {
|
|
76
|
+
return this.term.cmdAddon
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
isClosed = () => {
|
|
80
|
+
return !!this.term.onClose
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
isSsh = () => {
|
|
84
|
+
return this.term.isSsh ? this.term.isSsh() : this.getTabType() === 'ssh'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
isLocal = () => {
|
|
88
|
+
return this.term.isLocal ? this.term.isLocal() : this.getTabType() === 'local'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getTabType = () => {
|
|
92
|
+
return this.term.props?.tab?.type
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
isTelnet = () => {
|
|
96
|
+
if (this.term.isTelnet) {
|
|
97
|
+
return this.term.isTelnet()
|
|
98
|
+
}
|
|
99
|
+
return this.getTabType() === 'telnet'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
isSerial = () => {
|
|
103
|
+
if (this.term.isSerial) {
|
|
104
|
+
return this.term.isSerial()
|
|
105
|
+
}
|
|
106
|
+
return this.getTabType() === 'serial'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
isRemoteShell = () => {
|
|
110
|
+
return this.isSsh() || this.isTelnet() || this.isSerial()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
isRestorable = () => {
|
|
114
|
+
const tab = this.term.props?.tab || {}
|
|
115
|
+
if (typeof this.term.isShellTerminal === 'function') {
|
|
116
|
+
return this.term.isShellTerminal()
|
|
117
|
+
}
|
|
118
|
+
return isRestorableTerminalTab(tab)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
isWindowsLocal = () => {
|
|
122
|
+
return this.isLocal() && isWin
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
isPosixShell = () => {
|
|
126
|
+
return this.isRemoteShell() || (this.isLocal() && !isWin)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
sleep = (ms) => {
|
|
130
|
+
if (ms <= 0) {
|
|
131
|
+
return Promise.resolve()
|
|
132
|
+
}
|
|
133
|
+
return new Promise(resolve => {
|
|
134
|
+
const timer = setTimeout(() => {
|
|
135
|
+
this.timers.delete(timer)
|
|
136
|
+
resolve()
|
|
137
|
+
}, ms)
|
|
138
|
+
this.timers.add(timer)
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---- queue ----
|
|
143
|
+
|
|
144
|
+
runInitScript = () => {
|
|
145
|
+
const { term } = this
|
|
146
|
+
window.store.triggerResize()
|
|
147
|
+
const {
|
|
148
|
+
startDirectory,
|
|
149
|
+
runScripts
|
|
150
|
+
} = term.props.tab
|
|
151
|
+
|
|
152
|
+
const scripts = runScripts ? [...runScripts] : []
|
|
153
|
+
const reloadCwd = term.props.config.restoreTerminalSessionOnReload && this.isRestorable()
|
|
154
|
+
? term.props.tab._reloadState?.cwd
|
|
155
|
+
: ''
|
|
156
|
+
const startFolder = reloadCwd || startDirectory || window.initFolder
|
|
157
|
+
let cwdCommand = ''
|
|
158
|
+
if (startFolder) {
|
|
159
|
+
cwdCommand = this.isPosixShell()
|
|
160
|
+
? createRestoreCwdCommand(startFolder)
|
|
161
|
+
: createWindowsRestoreCwdCommand(startFolder) || `cd "${startFolder}"`
|
|
162
|
+
}
|
|
163
|
+
if (cwdCommand) {
|
|
164
|
+
scripts.unshift({ script: cwdCommand, delay: 0 })
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const token = ++this.token
|
|
168
|
+
this.queue = []
|
|
169
|
+
|
|
170
|
+
if (this.canInjectShellIntegration()) {
|
|
171
|
+
this.queue.push({
|
|
172
|
+
type: 'shell_integration',
|
|
173
|
+
execute: async () => {
|
|
174
|
+
await this.injectShellIntegration()
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
scripts.forEach((script, i) => {
|
|
180
|
+
this.queue.push({
|
|
181
|
+
type: 'delayed_script',
|
|
182
|
+
script: script.script,
|
|
183
|
+
delay: script.delay || 0,
|
|
184
|
+
// Nothing follows the last one, so waiting for the shell to settle
|
|
185
|
+
// would only add latency.
|
|
186
|
+
isLast: i === scripts.length - 1,
|
|
187
|
+
execute: () => {
|
|
188
|
+
if (script.script && this.addon) {
|
|
189
|
+
this.addon._sendData(script.script + '\r')
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
this.start(token)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Put a shell integration injection at the head of the queue.
|
|
200
|
+
* Inserting - instead of firing it off immediately - keeps it serialized
|
|
201
|
+
* against anything still queued and guarantees a single drain loop.
|
|
202
|
+
*/
|
|
203
|
+
enqueueShellIntegration = (sendEnter) => {
|
|
204
|
+
this.queue.unshift({
|
|
205
|
+
type: 'shell_integration',
|
|
206
|
+
execute: async () => {
|
|
207
|
+
await this.injectShellIntegration()
|
|
208
|
+
// A fresh prompt makes the shell emit OSC 633 ;P Cwd= right away, so
|
|
209
|
+
// the file manager picks the current path up without waiting for the
|
|
210
|
+
// user's next command.
|
|
211
|
+
if (sendEnter && this.addon) {
|
|
212
|
+
this.addon._sendData('\r')
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
this.start(this.token)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Drain the queue one item at a time. Re-entrant calls only push items and
|
|
221
|
+
* return, so two loops can never interleave or race for the same array.
|
|
222
|
+
*/
|
|
223
|
+
start = async (token) => {
|
|
224
|
+
if (this.running) {
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
this.running = true
|
|
228
|
+
try {
|
|
229
|
+
while (
|
|
230
|
+
!this.isClosed() &&
|
|
231
|
+
token === this.token &&
|
|
232
|
+
this.queue.length > 0
|
|
233
|
+
) {
|
|
234
|
+
const item = this.queue.shift()
|
|
235
|
+
try {
|
|
236
|
+
await this.runItem(item)
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('[Shell Integration] Error processing queue item:', item.type, error)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} finally {
|
|
242
|
+
this.running = false
|
|
243
|
+
// A newer queue was built while this loop was still draining (reconnect,
|
|
244
|
+
// config toggle). Pick it up here, otherwise the fresh items would sit
|
|
245
|
+
// in the array with nobody left to run them.
|
|
246
|
+
if (
|
|
247
|
+
!this.isClosed() &&
|
|
248
|
+
token !== this.token &&
|
|
249
|
+
this.queue.length > 0
|
|
250
|
+
) {
|
|
251
|
+
this.start(this.token)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
runItem = async (item) => {
|
|
257
|
+
if (this.isClosed() || !this.addon) {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
if (item.type === 'shell_integration') {
|
|
261
|
+
await item.execute()
|
|
262
|
+
// Give the freshly injected prompt a moment to finish drawing before
|
|
263
|
+
// anything is typed into it.
|
|
264
|
+
await this.waitForSettle({ usePrompt: false })
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
if (item.type !== 'delayed_script') {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
item.execute()
|
|
271
|
+
if (item.isLast) {
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
await this.sleep(item.delay)
|
|
275
|
+
// Never type the next script into a shell that is still busy. Bounded, so
|
|
276
|
+
// a script that never returns can not block the rest of the queue.
|
|
277
|
+
await this.waitForSettle()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Wait until the terminal stops producing output before typing the next
|
|
282
|
+
* queued item. Resolves on whichever comes first:
|
|
283
|
+
* - a fresh prompt (OSC 633 ;A), when shell integration is active - the
|
|
284
|
+
* exact "previous command finished" signal
|
|
285
|
+
* - no output for `idleMs`
|
|
286
|
+
* Always resolves; `timeoutMs` caps it.
|
|
287
|
+
*/
|
|
288
|
+
waitForSettle = ({ timeoutMs = settleTimeoutMs, idleMs = settleIdleMs, usePrompt = true } = {}) => {
|
|
289
|
+
const addon = this.addon
|
|
290
|
+
if (this.isClosed() || !addon || !addon.waitForOutputIdle || addon.disposed) {
|
|
291
|
+
return Promise.resolve(false)
|
|
292
|
+
}
|
|
293
|
+
const idle = addon.waitForOutputIdle({ idleMs, timeoutMs })
|
|
294
|
+
if (!usePrompt || !this.cmdAddon || !this.cmdAddon.onPrompt) {
|
|
295
|
+
return idle
|
|
296
|
+
}
|
|
297
|
+
return new Promise((resolve) => {
|
|
298
|
+
let done = false
|
|
299
|
+
let unsubscribe = null
|
|
300
|
+
const finish = (result) => {
|
|
301
|
+
if (done) {
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
done = true
|
|
305
|
+
clearTimeout(capTimer)
|
|
306
|
+
if (unsubscribe) {
|
|
307
|
+
unsubscribe()
|
|
308
|
+
}
|
|
309
|
+
resolve(result)
|
|
310
|
+
}
|
|
311
|
+
unsubscribe = this.cmdAddon.onPrompt(() => finish('prompt'))
|
|
312
|
+
const capTimer = setTimeout(() => finish(false), timeoutMs + 200)
|
|
313
|
+
this.timers.add(capTimer)
|
|
314
|
+
idle.then(quiet => finish(quiet ? 'idle' : false))
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---- shell integration ----
|
|
319
|
+
|
|
320
|
+
canInjectShellIntegration = () => {
|
|
321
|
+
const { config, sftpPathFollowSsh } = this.term.props
|
|
322
|
+
return (
|
|
323
|
+
config.showCmdSuggestions ||
|
|
324
|
+
sftpPathFollowSsh ||
|
|
325
|
+
(config.restoreTerminalSessionOnReload && this.isRestorable())
|
|
326
|
+
) &&
|
|
327
|
+
(
|
|
328
|
+
this.isSsh() ||
|
|
329
|
+
this.isTelnet() ||
|
|
330
|
+
this.isSerial() ||
|
|
331
|
+
(this.isLocal() && !isWin)
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Inject shell integration commands from client-side.
|
|
337
|
+
* Replaces the server-side `source xxx.xxx` approach; output suppression
|
|
338
|
+
* hides the injection command. Resolves when injection is complete - or
|
|
339
|
+
* when it is given up on, never hangs.
|
|
340
|
+
*/
|
|
341
|
+
injectShellIntegration = async () => {
|
|
342
|
+
if (this.shellInjected) {
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const addon = this.addon
|
|
347
|
+
if (!addon) {
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let shellType
|
|
352
|
+
if (this.isLocal()) {
|
|
353
|
+
const { config } = this.term.props
|
|
354
|
+
const localShell = isMac ? config.execMac : config.execLinux
|
|
355
|
+
shellType = detectShellType(localShell || '')
|
|
356
|
+
} else if (this.isRemoteShell()) {
|
|
357
|
+
shellType = await Promise.race([
|
|
358
|
+
this.detectRemoteShell(this.term.pid),
|
|
359
|
+
new Promise(resolve => {
|
|
360
|
+
setTimeout(() => resolve('sh'), remoteShellDetectTimeoutMs)
|
|
361
|
+
})
|
|
362
|
+
])
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
this.shellType = shellType
|
|
366
|
+
|
|
367
|
+
// sh/ash only reports cwd via PS1 (no command tracking), so it is only
|
|
368
|
+
// worth injecting when something actually needs the cwd: sftp follow or
|
|
369
|
+
// session restore. Command suggestions alone stay disabled on sh.
|
|
370
|
+
const { config, sftpPathFollowSsh } = this.term.props
|
|
371
|
+
if (
|
|
372
|
+
shellType === 'sh' &&
|
|
373
|
+
!sftpPathFollowSsh &&
|
|
374
|
+
!config.restoreTerminalSessionOnReload
|
|
375
|
+
) {
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// The connection (or the whole terminal) can go away while
|
|
380
|
+
// detectRemoteShell is in flight. Bail instead of registering callbacks
|
|
381
|
+
// on a dead socket - that is what used to leave the queue, and every run
|
|
382
|
+
// script behind it, waiting forever.
|
|
383
|
+
if (this.isClosed() || this.addon !== addon || addon.disposed) {
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const integrationCmd = getShellIntegrationCommand(shellType)
|
|
388
|
+
const suppressionTimeout = this.isRemoteShell() ? 5000 : 3000
|
|
389
|
+
|
|
390
|
+
return new Promise((resolve) => {
|
|
391
|
+
let done = false
|
|
392
|
+
const finish = () => {
|
|
393
|
+
if (done) {
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
done = true
|
|
397
|
+
this.timers.delete(safetyTimer)
|
|
398
|
+
clearTimeout(safetyTimer)
|
|
399
|
+
resolve()
|
|
400
|
+
}
|
|
401
|
+
// Hard ceiling for the whole injection. onInitialData only fires once
|
|
402
|
+
// the shell produces output; when it never does (dead pty, restricted
|
|
403
|
+
// shell, zmodem in progress) the promise - and the run scripts queued
|
|
404
|
+
// behind it - would otherwise never be released.
|
|
405
|
+
const safetyTimer = setTimeout(() => {
|
|
406
|
+
if (addon.outputSuppressed) {
|
|
407
|
+
// Unfreeze the screen rather than keep hiding output.
|
|
408
|
+
addon.stopOutputSuppression(false)
|
|
409
|
+
}
|
|
410
|
+
finish()
|
|
411
|
+
}, suppressionTimeout + 2000)
|
|
412
|
+
this.timers.add(safetyTimer)
|
|
413
|
+
|
|
414
|
+
// Wait for initial data (prompt/banner) to arrive before injecting
|
|
415
|
+
addon.onInitialData(() => {
|
|
416
|
+
if (done || this.addon !== addon || addon.disposed) {
|
|
417
|
+
return finish()
|
|
418
|
+
}
|
|
419
|
+
// Start suppressing output before sending the integration command.
|
|
420
|
+
// This hides the command and its output until OSC 633 is detected.
|
|
421
|
+
addon.startOutputSuppression(suppressionTimeout, () => {
|
|
422
|
+
if (!addon.disposed) {
|
|
423
|
+
this.shellInjected = true
|
|
424
|
+
}
|
|
425
|
+
finish()
|
|
426
|
+
})
|
|
427
|
+
addon._sendData(integrationCmd)
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export default StartupQueue
|
|
@@ -8,6 +8,9 @@ import TermInteractiveUI from './terminal-interactive-ui'
|
|
|
8
8
|
import ExternalLink from '../common/external-link.jsx'
|
|
9
9
|
import { notification } from '../common/notification'
|
|
10
10
|
|
|
11
|
+
// x11 warnings already shown, keyed by message text
|
|
12
|
+
const shownX11Warnings = new Set()
|
|
13
|
+
|
|
11
14
|
export default function TermInteractive () {
|
|
12
15
|
const [current, setCurrent] = useState(null)
|
|
13
16
|
const queueRef = useRef([])
|
|
@@ -43,6 +46,28 @@ export default function TermInteractive () {
|
|
|
43
46
|
})
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
function notifyX11Warning (data) {
|
|
50
|
+
// one notification per distinct message, several tabs may report it
|
|
51
|
+
if (shownX11Warnings.has(data.message)) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
shownX11Warnings.add(data.message)
|
|
55
|
+
notification.warning({
|
|
56
|
+
message: 'X11 forwarding',
|
|
57
|
+
description: (
|
|
58
|
+
<div className='pd0 mg0'>
|
|
59
|
+
<div>{data.message}</div>
|
|
60
|
+
{
|
|
61
|
+
data.url
|
|
62
|
+
? <ExternalLink to={data.url}>{data.url}</ExternalLink>
|
|
63
|
+
: null
|
|
64
|
+
}
|
|
65
|
+
</div>
|
|
66
|
+
),
|
|
67
|
+
duration: 30
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
46
71
|
function processNext () {
|
|
47
72
|
const next = queueRef.current.shift()
|
|
48
73
|
if (next) {
|
|
@@ -82,6 +107,13 @@ export default function TermInteractive () {
|
|
|
82
107
|
e.data.includes('ssh-proxy-command-message')
|
|
83
108
|
) {
|
|
84
109
|
notifyProxyCommandMessage(JSON.parse(e.data))
|
|
110
|
+
} else if (
|
|
111
|
+
e &&
|
|
112
|
+
e.data &&
|
|
113
|
+
typeof e.data === 'string' &&
|
|
114
|
+
e.data.includes('x11-warning')
|
|
115
|
+
) {
|
|
116
|
+
notifyX11Warning(JSON.parse(e.data))
|
|
85
117
|
}
|
|
86
118
|
}
|
|
87
119
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-screen plain-text view of the buffer, for touch devices.
|
|
3
|
+
*
|
|
4
|
+
* Selecting inside a canvas-rendered terminal on a phone is hopeless: there is
|
|
5
|
+
* no system selection to lean on, so every touch terminal ends up
|
|
6
|
+
* re-implementing magnifiers, draggable handles and edge auto-scroll. This
|
|
7
|
+
* takes the other route — hand the text to an ordinary read-only <textarea>
|
|
8
|
+
* and let iOS/Android supply the whole native toolbox (long-press word select,
|
|
9
|
+
* resizable handles, the Copy callout) for free.
|
|
10
|
+
*
|
|
11
|
+
* Rendered through a portal onto <body> rather than in place: `.term-wrap` is
|
|
12
|
+
* a positioned element with its own z-index, so anything inside it is trapped
|
|
13
|
+
* in that stacking context and would still be covered by the touch shortcut
|
|
14
|
+
* bar (z-index 400). The portal also keeps these styles away from
|
|
15
|
+
* `.term-wrap`, which switches off -webkit-touch-callout and user-select for
|
|
16
|
+
* the terminal's own gestures — `-webkit-touch-callout` is inherited, so
|
|
17
|
+
* without the reset in terminal.styl the native callout would never appear.
|
|
18
|
+
*
|
|
19
|
+
* React events still bubble along the React tree, not the DOM one, so the
|
|
20
|
+
* parent's touch handlers and the antd Dropdown's `contextMenu` trigger both
|
|
21
|
+
* still see events from here. Two guards in response:
|
|
22
|
+
* - term-touch.js bails out of any touch start whose target is this overlay;
|
|
23
|
+
* - the contextmenu handler below stops the event, which would otherwise open
|
|
24
|
+
* the terminal menu on top of the selection. Only propagation is stopped,
|
|
25
|
+
* never the default action, since that default action *is* the native
|
|
26
|
+
* callout being relied on.
|
|
27
|
+
*
|
|
28
|
+
* The content is a snapshot taken when the panel opens: output arriving
|
|
29
|
+
* afterwards keeps going to the terminal underneath and cannot disturb a
|
|
30
|
+
* selection the user is in the middle of making.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { memo } from 'react'
|
|
34
|
+
import { createPortal } from 'react-dom'
|
|
35
|
+
|
|
36
|
+
const e = window.translate
|
|
37
|
+
|
|
38
|
+
export default memo(function TerminalSelectText (props) {
|
|
39
|
+
if (!props.visible) {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
const blockMenu = ev => ev.stopPropagation()
|
|
43
|
+
return createPortal(
|
|
44
|
+
<div
|
|
45
|
+
className='terminal-select-text'
|
|
46
|
+
onContextMenu={blockMenu}
|
|
47
|
+
>
|
|
48
|
+
<div className='terminal-select-text-bar'>
|
|
49
|
+
<span>{e('selectText')}</span>
|
|
50
|
+
<span className='terminal-select-text-btns'>
|
|
51
|
+
<a onClick={props.onSelectAll}>{e('selectall')}</a>
|
|
52
|
+
<a onClick={props.onCopy}>{e('copy')}</a>
|
|
53
|
+
<a onClick={props.onClose}>{e('close')}</a>
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
<textarea
|
|
57
|
+
className='terminal-select-text-area'
|
|
58
|
+
ref={props.textareaRef}
|
|
59
|
+
value={props.text}
|
|
60
|
+
readOnly
|
|
61
|
+
spellCheck={false}
|
|
62
|
+
autoComplete='off'
|
|
63
|
+
autoCorrect='off'
|
|
64
|
+
autoCapitalize='off'
|
|
65
|
+
/>
|
|
66
|
+
</div>,
|
|
67
|
+
document.body
|
|
68
|
+
)
|
|
69
|
+
})
|