@electerm/electerm-react 5.5.6 → 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/client/common/default-setting.js +1 -0
- package/client/components/footer/cmd-history.jsx +132 -45
- package/client/components/footer/cmd-history.styl +42 -4
- package/client/components/footer/multi-tab-run-modal.jsx +83 -0
- package/client/components/footer/tab-select.jsx +34 -17
- 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/setting-panel/setting-terminal.jsx +1 -0
- package/client/components/sftp/sftp-entry.jsx +4 -0
- package/client/components/sidebar/side-panel.jsx +1 -1
- package/client/components/terminal/mixins/term-content.js +38 -0
- package/client/components/terminal/mixins/term-context-menu.jsx +35 -3
- package/client/components/terminal/mixins/term-socket.js +5 -5
- package/client/components/terminal/mixins/term-touch.js +444 -57
- 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.jsx +13 -4
- package/client/components/terminal/ssh-reload-state.js +41 -1
- package/client/components/terminal/startup-queue.js +67 -13
- package/client/components/terminal/terminal-select-text.jsx +69 -0
- package/client/components/terminal/terminal.jsx +72 -4
- package/client/components/terminal/terminal.styl +61 -0
- package/client/css/mobile.styl +20 -7
- package/client/store/tab.js +5 -4
- package/package.json +1 -1
|
@@ -100,9 +100,17 @@ const modifierOptions = [
|
|
|
100
100
|
{ id: 'meta', label: 'Meta' }
|
|
101
101
|
]
|
|
102
102
|
|
|
103
|
+
const MODIFIER_IDS = new Set(modifierOptions.map(m => m.id))
|
|
104
|
+
|
|
105
|
+
// the builder's second slot accepts either a modifier (Ctrl/Alt/Shift/Meta)
|
|
106
|
+
// or a bare key, so we need to tell them apart.
|
|
107
|
+
function isModifier (id) {
|
|
108
|
+
return MODIFIER_IDS.has(id)
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
// compose the bytes to send for a (mods, key) combo.
|
|
104
112
|
// Ctrl+letter -> the control byte; Alt -> ESC prefix; Shift+letter -> uppercase
|
|
105
|
-
function
|
|
113
|
+
function applyMods (mods, key) {
|
|
106
114
|
const ctrl = mods.includes('ctrl')
|
|
107
115
|
const alt = mods.includes('alt')
|
|
108
116
|
const shift = mods.includes('shift')
|
|
@@ -118,10 +126,27 @@ function buildComboData (mods, key) {
|
|
|
118
126
|
return data
|
|
119
127
|
}
|
|
120
128
|
|
|
121
|
-
//
|
|
122
|
-
|
|
129
|
+
// `prefix` is an optional first key typed BEFORE `key` — that's how you build
|
|
130
|
+
// two-step combos such as tmux's Ctrl+A then D: the modifiers apply to the
|
|
131
|
+
// prefix only, the trailing key is sent plain.
|
|
132
|
+
// mods [ctrl], prefix A, key D -> Ctrl+A then D
|
|
133
|
+
// mods [ctrl, shift], key D -> Ctrl+Shift+D
|
|
134
|
+
function buildComboData (mods, key, prefix) {
|
|
135
|
+
if (prefix) {
|
|
136
|
+
return applyMods(mods, prefix) + key.data
|
|
137
|
+
}
|
|
138
|
+
return applyMods(mods, key)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// human-readable label, e.g.
|
|
142
|
+
// mods ['ctrl','shift'] + key A -> "Ctrl+Shift+A"
|
|
143
|
+
// mods ['ctrl'] + prefix A + key D -> "Ctrl+A D"
|
|
144
|
+
function buildComboLabel (mods, key, prefix) {
|
|
123
145
|
const byId = new Map(modifierOptions.map(m => [m.id, m.label]))
|
|
124
|
-
|
|
146
|
+
const head = mods.map(m => byId.get(m) || m)
|
|
147
|
+
return prefix
|
|
148
|
+
? [...head, prefix.label].join('+') + ' ' + key.label
|
|
149
|
+
: [...head, key.label].join('+')
|
|
125
150
|
}
|
|
126
151
|
|
|
127
152
|
// full library, de-duplicated by id
|
|
@@ -196,6 +221,7 @@ export {
|
|
|
196
221
|
candidates,
|
|
197
222
|
keyOptions,
|
|
198
223
|
modifierOptions,
|
|
224
|
+
isModifier,
|
|
199
225
|
buildComboData,
|
|
200
226
|
buildComboLabel
|
|
201
227
|
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
defaultActive,
|
|
27
27
|
keyOptions,
|
|
28
28
|
modifierOptions,
|
|
29
|
+
isModifier,
|
|
29
30
|
buildComboData,
|
|
30
31
|
buildComboLabel
|
|
31
32
|
} from './shortcut-bar-defs'
|
|
@@ -43,6 +44,9 @@ export default function ShortcutBarEdit (props) {
|
|
|
43
44
|
|
|
44
45
|
const [keyword, setKeyword] = useState('')
|
|
45
46
|
const [customMod1, setCustomMod1] = useState(undefined)
|
|
47
|
+
// second slot takes a modifier OR a bare key: a modifier stacks onto the
|
|
48
|
+
// first one (Ctrl+Shift+D), a key becomes a prefix typed before the third
|
|
49
|
+
// slot's key (Ctrl+A then D).
|
|
46
50
|
const [customMod2, setCustomMod2] = useState(undefined)
|
|
47
51
|
const [customKey, setCustomKey] = useState(undefined)
|
|
48
52
|
|
|
@@ -70,13 +74,21 @@ export default function ShortcutBarEdit (props) {
|
|
|
70
74
|
if (!customKey) {
|
|
71
75
|
return
|
|
72
76
|
}
|
|
73
|
-
const mods = [customMod1, customMod2].filter(Boolean)
|
|
74
77
|
const key = keyOptions.find(k => k.id === customKey)
|
|
75
78
|
if (!key) {
|
|
76
79
|
return
|
|
77
80
|
}
|
|
78
|
-
const
|
|
79
|
-
|
|
81
|
+
const mods = [customMod1].filter(Boolean)
|
|
82
|
+
let prefix
|
|
83
|
+
if (customMod2) {
|
|
84
|
+
if (isModifier(customMod2)) {
|
|
85
|
+
mods.push(customMod2)
|
|
86
|
+
} else {
|
|
87
|
+
prefix = keyOptions.find(k => k.id === customMod2)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const label = buildComboLabel(mods, key, prefix)
|
|
91
|
+
const data = buildComboData(mods, key, prefix)
|
|
80
92
|
const id = 'custom-' + label + '-' + Date.now()
|
|
81
93
|
onSave([...active, { id, label, data, custom: true }])
|
|
82
94
|
setCustomMod1(undefined)
|
|
@@ -88,6 +100,26 @@ export default function ShortcutBarEdit (props) {
|
|
|
88
100
|
onSave(defaultActive())
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
// second slot option list: modifiers first, then the same bare-key list the
|
|
104
|
+
// third slot offers (so Ctrl+A then D is expressible).
|
|
105
|
+
const secondOptions = [
|
|
106
|
+
{
|
|
107
|
+
label: e('modifier'),
|
|
108
|
+
options: modifierOptions.map(m => ({
|
|
109
|
+
value: m.id,
|
|
110
|
+
label: m.label,
|
|
111
|
+
disabled: m.id === customMod1
|
|
112
|
+
}))
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
label: e('key'),
|
|
116
|
+
options: keyOptions.map(k => ({
|
|
117
|
+
value: k.id,
|
|
118
|
+
label: k.label
|
|
119
|
+
}))
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
|
|
91
123
|
const kw = keyword.trim().toLowerCase()
|
|
92
124
|
const filtered = allCandidates.filter(b => {
|
|
93
125
|
if (!kw) {
|
|
@@ -195,15 +227,13 @@ export default function ShortcutBarEdit (props) {
|
|
|
195
227
|
<Select
|
|
196
228
|
value={customMod2}
|
|
197
229
|
onChange={setCustomMod2}
|
|
198
|
-
placeholder={e('modifier') + ' (' + e('optional') + ')'}
|
|
230
|
+
placeholder={e('modifier') + '/' + e('key') + ' (' + e('optional') + ')'}
|
|
199
231
|
className='shortcut-custom-select'
|
|
200
232
|
allowClear
|
|
233
|
+
showSearch
|
|
234
|
+
optionFilterProp='label'
|
|
201
235
|
getPopupContainer={popupContainer}
|
|
202
|
-
options={
|
|
203
|
-
value: m.id,
|
|
204
|
-
label: m.label,
|
|
205
|
-
disabled: m.id === customMod1
|
|
206
|
-
}))}
|
|
236
|
+
options={secondOptions}
|
|
207
237
|
/>
|
|
208
238
|
<Select
|
|
209
239
|
value={customKey}
|
|
@@ -212,6 +242,7 @@ export default function ShortcutBarEdit (props) {
|
|
|
212
242
|
className='shortcut-custom-select shortcut-custom-key'
|
|
213
243
|
allowClear
|
|
214
244
|
showSearch
|
|
245
|
+
optionFilterProp='label'
|
|
215
246
|
getPopupContainer={popupContainer}
|
|
216
247
|
options={keyOptions.map(k => ({
|
|
217
248
|
value: k.id,
|
|
@@ -56,11 +56,20 @@ function sendToTerminal (data) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
// honor the active terminal's DECCKM state for the cursor / Home / End keys
|
|
59
|
+
// honor the active terminal's DECCKM state for the cursor / Home / End keys.
|
|
60
|
+
// custom combos can be multi-key sequences (Ctrl+A then ↑), so match the
|
|
61
|
+
// trailing key too — not only the whole payload.
|
|
60
62
|
function resolveCursorMode (data, term) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
if (!term?.term?.modes?.applicationCursorKeysMode) {
|
|
64
|
+
return data
|
|
65
|
+
}
|
|
66
|
+
for (const normal in appCursorMap) {
|
|
67
|
+
if (data === normal) {
|
|
68
|
+
return appCursorMap[normal]
|
|
69
|
+
}
|
|
70
|
+
if (data.length > normal.length && data.endsWith(normal)) {
|
|
71
|
+
return data.slice(0, data.length - normal.length) + appCursorMap[normal]
|
|
72
|
+
}
|
|
64
73
|
}
|
|
65
74
|
return data
|
|
66
75
|
}
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
const maxCwdLength = 4096
|
|
2
2
|
|
|
3
|
+
// Shell-backed terminal types whose cwd + screen can be restored on reload.
|
|
4
|
+
// Graphical / file-transfer tabs (rdp, vnc, web, ftp, spice, sftp, ...) are
|
|
5
|
+
// intentionally excluded: they have no shell cwd to `cd` back to.
|
|
6
|
+
export const restorableTerminalTypes = new Set([
|
|
7
|
+
'ssh',
|
|
8
|
+
'local',
|
|
9
|
+
'telnet',
|
|
10
|
+
'serial'
|
|
11
|
+
])
|
|
12
|
+
|
|
3
13
|
export function isSshTab (tab = {}) {
|
|
4
14
|
return !!tab.host && (!tab.type || tab.type === 'ssh')
|
|
5
15
|
}
|
|
6
16
|
|
|
17
|
+
export function isRestorableTerminalTab (tab = {}) {
|
|
18
|
+
if (!tab.type) {
|
|
19
|
+
// Legacy tabs without an explicit type are ssh (host set) or local
|
|
20
|
+
// (no host) - both are shell terminals.
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
return restorableTerminalTypes.has(tab.type)
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
export function sanitizeSshCwd (cwd) {
|
|
8
27
|
if (typeof cwd !== 'string' || !cwd || cwd.length > maxCwdLength) {
|
|
9
28
|
return ''
|
|
@@ -14,6 +33,8 @@ export function sanitizeSshCwd (cwd) {
|
|
|
14
33
|
return cwd
|
|
15
34
|
}
|
|
16
35
|
|
|
36
|
+
export const sanitizeTerminalCwd = sanitizeSshCwd
|
|
37
|
+
|
|
17
38
|
export function quotePosixShellArg (value) {
|
|
18
39
|
const quote = '\''
|
|
19
40
|
const escapedQuote = quote + '"' + quote + '"' + quote
|
|
@@ -25,8 +46,25 @@ export function createRestoreCwdCommand (cwd) {
|
|
|
25
46
|
return safeCwd ? `cd -- ${quotePosixShellArg(safeCwd)}` : ''
|
|
26
47
|
}
|
|
27
48
|
|
|
49
|
+
export function createWindowsRestoreCwdCommand (cwd) {
|
|
50
|
+
const safeCwd = sanitizeSshCwd(cwd)
|
|
51
|
+
if (!safeCwd) {
|
|
52
|
+
return ''
|
|
53
|
+
}
|
|
54
|
+
// Strip control chars already handled by sanitize; escape embedded double
|
|
55
|
+
// quotes for cmd/powershell (`""` is accepted by both as a literal quote).
|
|
56
|
+
const escaped = safeCwd.replace(/"/g, '""')
|
|
57
|
+
return `cd /d "${escaped}"`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function shouldCaptureTerminalReloadState (tab, config = {}) {
|
|
61
|
+
return !!config.restoreTerminalSessionOnReload && isRestorableTerminalTab(tab)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Kept for backward compatibility - now covers all shell terminals
|
|
65
|
+
// (ssh, local, telnet, serial), not just ssh.
|
|
28
66
|
export function shouldCaptureSshReloadState (tab, config = {}) {
|
|
29
|
-
return
|
|
67
|
+
return shouldCaptureTerminalReloadState(tab, config)
|
|
30
68
|
}
|
|
31
69
|
|
|
32
70
|
export function createSshReloadState ({ cwd, screen } = {}) {
|
|
@@ -41,6 +79,8 @@ export function createSshReloadState ({ cwd, screen } = {}) {
|
|
|
41
79
|
}
|
|
42
80
|
}
|
|
43
81
|
|
|
82
|
+
export const createTerminalReloadState = createSshReloadState
|
|
83
|
+
|
|
44
84
|
export function getAlternateBufferSnapshot (buffer) {
|
|
45
85
|
if (!buffer || buffer.type !== 'alternate') {
|
|
46
86
|
return ''
|
|
@@ -22,7 +22,11 @@ import {
|
|
|
22
22
|
getShellIntegrationCommand,
|
|
23
23
|
detectShellType
|
|
24
24
|
} from './shell.js'
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
createRestoreCwdCommand,
|
|
27
|
+
createWindowsRestoreCwdCommand,
|
|
28
|
+
isRestorableTerminalTab
|
|
29
|
+
} from './ssh-reload-state.js'
|
|
26
30
|
import { isMac, isWin } from '../../common/platform.js'
|
|
27
31
|
|
|
28
32
|
// Pacing: `settleIdleMs` of silence counts as "the shell is done",
|
|
@@ -77,11 +81,49 @@ export class StartupQueue {
|
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
isSsh = () => {
|
|
80
|
-
return this.term.isSsh()
|
|
84
|
+
return this.term.isSsh ? this.term.isSsh() : this.getTabType() === 'ssh'
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
isLocal = () => {
|
|
84
|
-
return this.term.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)
|
|
85
127
|
}
|
|
86
128
|
|
|
87
129
|
sleep = (ms) => {
|
|
@@ -108,13 +150,16 @@ export class StartupQueue {
|
|
|
108
150
|
} = term.props.tab
|
|
109
151
|
|
|
110
152
|
const scripts = runScripts ? [...runScripts] : []
|
|
111
|
-
const reloadCwd = term.props.config.restoreTerminalSessionOnReload && this.
|
|
153
|
+
const reloadCwd = term.props.config.restoreTerminalSessionOnReload && this.isRestorable()
|
|
112
154
|
? term.props.tab._reloadState?.cwd
|
|
113
155
|
: ''
|
|
114
156
|
const startFolder = reloadCwd || startDirectory || window.initFolder
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
157
|
+
let cwdCommand = ''
|
|
158
|
+
if (startFolder) {
|
|
159
|
+
cwdCommand = this.isPosixShell()
|
|
160
|
+
? createRestoreCwdCommand(startFolder)
|
|
161
|
+
: createWindowsRestoreCwdCommand(startFolder) || `cd "${startFolder}"`
|
|
162
|
+
}
|
|
118
163
|
if (cwdCommand) {
|
|
119
164
|
scripts.unshift({ script: cwdCommand, delay: 0 })
|
|
120
165
|
}
|
|
@@ -277,10 +322,12 @@ export class StartupQueue {
|
|
|
277
322
|
return (
|
|
278
323
|
config.showCmdSuggestions ||
|
|
279
324
|
sftpPathFollowSsh ||
|
|
280
|
-
(config.restoreTerminalSessionOnReload && this.
|
|
325
|
+
(config.restoreTerminalSessionOnReload && this.isRestorable())
|
|
281
326
|
) &&
|
|
282
327
|
(
|
|
283
328
|
this.isSsh() ||
|
|
329
|
+
this.isTelnet() ||
|
|
330
|
+
this.isSerial() ||
|
|
284
331
|
(this.isLocal() && !isWin)
|
|
285
332
|
)
|
|
286
333
|
}
|
|
@@ -305,8 +352,8 @@ export class StartupQueue {
|
|
|
305
352
|
if (this.isLocal()) {
|
|
306
353
|
const { config } = this.term.props
|
|
307
354
|
const localShell = isMac ? config.execMac : config.execLinux
|
|
308
|
-
shellType = detectShellType(localShell)
|
|
309
|
-
} else if (this.
|
|
355
|
+
shellType = detectShellType(localShell || '')
|
|
356
|
+
} else if (this.isRemoteShell()) {
|
|
310
357
|
shellType = await Promise.race([
|
|
311
358
|
this.detectRemoteShell(this.term.pid),
|
|
312
359
|
new Promise(resolve => {
|
|
@@ -317,8 +364,15 @@ export class StartupQueue {
|
|
|
317
364
|
|
|
318
365
|
this.shellType = shellType
|
|
319
366
|
|
|
320
|
-
//
|
|
321
|
-
|
|
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
|
+
) {
|
|
322
376
|
return
|
|
323
377
|
}
|
|
324
378
|
|
|
@@ -331,7 +385,7 @@ export class StartupQueue {
|
|
|
331
385
|
}
|
|
332
386
|
|
|
333
387
|
const integrationCmd = getShellIntegrationCommand(shellType)
|
|
334
|
-
const suppressionTimeout = this.
|
|
388
|
+
const suppressionTimeout = this.isRemoteShell() ? 5000 : 3000
|
|
335
389
|
|
|
336
390
|
return new Promise((resolve) => {
|
|
337
391
|
let done = false
|
|
@@ -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
|
+
})
|
|
@@ -22,6 +22,7 @@ import RemoteFloatControl from '../common/remote-float-control'
|
|
|
22
22
|
import ReconnectOverlay from './reconnect-overlay.jsx'
|
|
23
23
|
import TerminalErrorHandle from './terminal-error-handle.jsx'
|
|
24
24
|
import DropFileModal from './drop-file-modal.jsx'
|
|
25
|
+
import TerminalSelectText from './terminal-select-text.jsx'
|
|
25
26
|
import { StartupQueue } from './startup-queue.js'
|
|
26
27
|
import { detectRemoteShell } from './shell-detect.js'
|
|
27
28
|
import { shortcutExtend, shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
|
|
@@ -79,7 +80,10 @@ class Term extends Component {
|
|
|
79
80
|
terminalError: null,
|
|
80
81
|
dropFileModalVisible: false,
|
|
81
82
|
droppedFiles: [],
|
|
82
|
-
fontSizeChanged: false
|
|
83
|
+
fontSizeChanged: false,
|
|
84
|
+
// touch-only: the buffer snapshot shown by the select-text overlay
|
|
85
|
+
selectTextVisible: false,
|
|
86
|
+
selectTextContent: ''
|
|
83
87
|
}
|
|
84
88
|
this.id = `term-${this.props.tab.id}`
|
|
85
89
|
refs.add(this.id, this)
|
|
@@ -109,12 +113,28 @@ class Term extends Component {
|
|
|
109
113
|
}
|
|
110
114
|
]
|
|
111
115
|
|
|
112
|
-
// ---- Mobile touch support ----
|
|
116
|
+
// ---- Mobile touch support (behaviour lives in mixins/term-touch.js) ----
|
|
117
|
+
// Only used while store.isTouchDevice is true, so a mouse-driven desktop
|
|
118
|
+
// never touches any of it. Per-instance state has to stay a class field
|
|
119
|
+
// here — a mixin may only export functions, see ./mixins/index.js.
|
|
113
120
|
longPressTimer = null
|
|
114
121
|
touchStartPos = null
|
|
115
122
|
longPressFired = false
|
|
116
123
|
longPressThreshold = 500 // ms
|
|
117
|
-
|
|
124
|
+
// a finger drifts further than a mouse while holding still, so the tolerance
|
|
125
|
+
// is looser than the old 10px to stop long presses being read as scrolls
|
|
126
|
+
longPressMoveTolerance = 12 // px
|
|
127
|
+
// { anchor: {col, absRow}, moved, clientX, clientY, target } while a
|
|
128
|
+
// long-press selection is being dragged out
|
|
129
|
+
dragSelect = null
|
|
130
|
+
lastDragTouch = null
|
|
131
|
+
autoScrollDir = 0
|
|
132
|
+
dragFrame = 0
|
|
133
|
+
tapCount = 0
|
|
134
|
+
lastTapTime = 0
|
|
135
|
+
lastTapPos = null
|
|
136
|
+
// the read-only textarea of the select-text overlay
|
|
137
|
+
selectTextRef = createRef()
|
|
118
138
|
|
|
119
139
|
terminalColorQueryDisposables = []
|
|
120
140
|
|
|
@@ -196,6 +216,12 @@ class Term extends Component {
|
|
|
196
216
|
clearTimeout(this.longPressTimer)
|
|
197
217
|
this.longPressTimer = null
|
|
198
218
|
this.touchStartPos = null
|
|
219
|
+
this.stopAutoScroll()
|
|
220
|
+
window.cancelAnimationFrame(this.dragFrame)
|
|
221
|
+
this.dragFrame = 0
|
|
222
|
+
this.dragSelect = null
|
|
223
|
+
this.lastDragTouch = null
|
|
224
|
+
this.lastTapPos = null
|
|
199
225
|
if (window.store.activeTerminalId === this.props.tab.id) {
|
|
200
226
|
window.store.activeTerminalId = ''
|
|
201
227
|
}
|
|
@@ -204,10 +230,15 @@ class Term extends Component {
|
|
|
204
230
|
}
|
|
205
231
|
this.disposeTerminalColorQueryHandlers()
|
|
206
232
|
this.startupQueue?.dispose()
|
|
233
|
+
try {
|
|
234
|
+
this._notifyOnDataDebounced?.cancel?.()
|
|
235
|
+
} catch (_) {}
|
|
236
|
+
this._notifyOnDataDebounced = null
|
|
207
237
|
window.cancelAnimationFrame(this.timers.themeRaf)
|
|
208
238
|
this.timers.themeRaf = null
|
|
209
239
|
Object.keys(this.timers).forEach(k => {
|
|
210
240
|
clearTimeout(this.timers[k])
|
|
241
|
+
clearInterval(this.timers[k])
|
|
211
242
|
this.timers[k] = null
|
|
212
243
|
})
|
|
213
244
|
this.onClose = true
|
|
@@ -215,11 +246,16 @@ class Term extends Component {
|
|
|
215
246
|
this.socket.close()
|
|
216
247
|
this.socket = null
|
|
217
248
|
}
|
|
249
|
+
if (this.attachAddon) {
|
|
250
|
+
try {
|
|
251
|
+
this.attachAddon.dispose?.()
|
|
252
|
+
} catch (_) {}
|
|
253
|
+
this.attachAddon = null
|
|
254
|
+
}
|
|
218
255
|
if (this.term) {
|
|
219
256
|
this.term.dispose()
|
|
220
257
|
this.term = null
|
|
221
258
|
}
|
|
222
|
-
this.attachAddon = null
|
|
223
259
|
this.disposeTriggerManager?.()
|
|
224
260
|
this.fitAddon = null
|
|
225
261
|
this.zmodemClient = null
|
|
@@ -232,6 +268,9 @@ class Term extends Component {
|
|
|
232
268
|
this.imageAddon = null
|
|
233
269
|
this.webglContextLossDisposable?.dispose?.()
|
|
234
270
|
this.webglContextLossDisposable = null
|
|
271
|
+
try {
|
|
272
|
+
this.webglAddon?.dispose?.()
|
|
273
|
+
} catch (_) {}
|
|
235
274
|
this.webglAddon = null
|
|
236
275
|
this.webglRecovering = false
|
|
237
276
|
}
|
|
@@ -262,6 +301,27 @@ class Term extends Component {
|
|
|
262
301
|
(type === 'local' || type === undefined)
|
|
263
302
|
}
|
|
264
303
|
|
|
304
|
+
isTelnet () {
|
|
305
|
+
return this.props.tab?.type === connectionMap.telnet
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
isSerial () {
|
|
309
|
+
return this.props.tab?.type === connectionMap.serial
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
isShellTerminal () {
|
|
313
|
+
const { type } = this.props.tab || {}
|
|
314
|
+
if (!type) {
|
|
315
|
+
return true
|
|
316
|
+
}
|
|
317
|
+
return (
|
|
318
|
+
type === connectionMap.ssh ||
|
|
319
|
+
type === connectionMap.local ||
|
|
320
|
+
type === connectionMap.telnet ||
|
|
321
|
+
type === connectionMap.serial
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
265
325
|
setStatus = status => {
|
|
266
326
|
const id = this.props.tab?.id
|
|
267
327
|
this.props.editTab(id, {
|
|
@@ -377,6 +437,14 @@ class Term extends Component {
|
|
|
377
437
|
onCancel={this.handleDropFileModalCancel}
|
|
378
438
|
/>
|
|
379
439
|
{spin}
|
|
440
|
+
<TerminalSelectText
|
|
441
|
+
visible={this.state.selectTextVisible}
|
|
442
|
+
text={this.state.selectTextContent}
|
|
443
|
+
textareaRef={this.selectTextRef}
|
|
444
|
+
onSelectAll={this.handleSelectTextAll}
|
|
445
|
+
onCopy={this.handleSelectTextCopy}
|
|
446
|
+
onClose={this.handleSelectTextClose}
|
|
447
|
+
/>
|
|
380
448
|
</div>
|
|
381
449
|
</Dropdown>
|
|
382
450
|
)
|
|
@@ -53,6 +53,67 @@
|
|
|
53
53
|
background-position center
|
|
54
54
|
background-color transparent !important
|
|
55
55
|
|
|
56
|
+
// Full-screen plain-text view of the buffer, opened from the touch context
|
|
57
|
+
// menu (see terminal-select-text.jsx) so the platform's own selection handles
|
|
58
|
+
// and Copy callout can be used instead of xterm's canvas.
|
|
59
|
+
//
|
|
60
|
+
// 900 clears the touch shortcut bar (400) and the terminal panes, while
|
|
61
|
+
// staying below antd's modal (1000) and message (1010) layers so a "copied"
|
|
62
|
+
// toast still shows up over this panel.
|
|
63
|
+
//
|
|
64
|
+
// The callout/user-select resets are load-bearing, not decoration: the
|
|
65
|
+
// component is portalled onto <body>, but `.term-wrap` switches off
|
|
66
|
+
// -webkit-touch-callout for the terminal's own gestures and the property is
|
|
67
|
+
// inherited — if this panel is ever rendered in place again, without the
|
|
68
|
+
// resets the native Copy callout would never appear on the textarea.
|
|
69
|
+
.terminal-select-text
|
|
70
|
+
position fixed
|
|
71
|
+
left 0
|
|
72
|
+
top 0
|
|
73
|
+
width 100%
|
|
74
|
+
height 100%
|
|
75
|
+
z-index 900
|
|
76
|
+
display flex
|
|
77
|
+
flex-direction column
|
|
78
|
+
background var(--main)
|
|
79
|
+
color var(--text)
|
|
80
|
+
-webkit-touch-callout default
|
|
81
|
+
-webkit-user-select text
|
|
82
|
+
user-select text
|
|
83
|
+
|
|
84
|
+
.terminal-select-text-bar
|
|
85
|
+
flex-shrink 0
|
|
86
|
+
display flex
|
|
87
|
+
align-items center
|
|
88
|
+
justify-content space-between
|
|
89
|
+
height 40px
|
|
90
|
+
padding 0 10px
|
|
91
|
+
font-size 14px
|
|
92
|
+
background var(--main-lighter)
|
|
93
|
+
.terminal-select-text-btns
|
|
94
|
+
a
|
|
95
|
+
margin-left 16px
|
|
96
|
+
color var(--text)
|
|
97
|
+
&:hover
|
|
98
|
+
color var(--text-light)
|
|
99
|
+
|
|
100
|
+
.terminal-select-text-area
|
|
101
|
+
flex 1
|
|
102
|
+
width 100%
|
|
103
|
+
margin 0
|
|
104
|
+
padding 8px 10px
|
|
105
|
+
border 0
|
|
106
|
+
outline none
|
|
107
|
+
resize none
|
|
108
|
+
background var(--main)
|
|
109
|
+
color var(--text)
|
|
110
|
+
font-family monospace
|
|
111
|
+
font-size 13px
|
|
112
|
+
line-height 1.4
|
|
113
|
+
-webkit-touch-callout default
|
|
114
|
+
-webkit-user-select text
|
|
115
|
+
user-select text
|
|
116
|
+
|
|
56
117
|
.terminal-normal-buffer
|
|
57
118
|
position absolute
|
|
58
119
|
left 0
|