@electerm/electerm-react 5.5.6 → 5.5.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/client/common/default-setting.js +1 -0
  2. package/client/common/download.jsx +12 -0
  3. package/client/common/resolve.js +20 -14
  4. package/client/components/ai/ai-history-item.jsx +1 -1
  5. package/client/components/ai/ai-history.jsx +1 -0
  6. package/client/components/ai/ai-history.styl +23 -0
  7. package/client/components/bookmark-form/common/color-picker.styl +3 -0
  8. package/client/components/common/remote-pointer.js +49 -0
  9. package/client/components/common/remote-session-shell.jsx +137 -0
  10. package/client/components/common/remote-session-shell.styl +80 -0
  11. package/client/components/common/responsive-tabs.jsx +89 -0
  12. package/client/components/common/responsive-tabs.styl +34 -0
  13. package/client/components/footer/cmd-history.jsx +132 -45
  14. package/client/components/footer/cmd-history.styl +42 -4
  15. package/client/components/footer/multi-tab-run-modal.jsx +83 -0
  16. package/client/components/footer/tab-select.jsx +34 -17
  17. package/client/components/quick-commands/quick-command-create-modal.jsx +60 -0
  18. package/client/components/quick-commands/quick-commands-form-elem.jsx +10 -4
  19. package/client/components/rdp/rdp-session.jsx +94 -124
  20. package/client/components/rdp/rdp.styl +13 -36
  21. package/client/components/session/session.styl +0 -16
  22. package/client/components/setting-panel/setting-modal.jsx +8 -3
  23. package/client/components/setting-panel/setting-terminal.jsx +1 -0
  24. package/client/components/sftp/sftp-entry.jsx +30 -0
  25. package/client/components/sidebar/side-panel.jsx +1 -1
  26. package/client/components/spice/spice-session.jsx +69 -72
  27. package/client/components/spice/spice.styl +10 -19
  28. package/client/components/terminal/mixins/term-content.js +38 -0
  29. package/client/components/terminal/mixins/term-context-menu.jsx +35 -3
  30. package/client/components/terminal/mixins/term-socket.js +5 -5
  31. package/client/components/terminal/mixins/term-touch.js +444 -57
  32. package/client/components/terminal/shortcut-bar-defs.js +30 -4
  33. package/client/components/terminal/shortcut-bar-edit.jsx +40 -9
  34. package/client/components/terminal/shortcut-bar.jsx +13 -4
  35. package/client/components/terminal/ssh-reload-state.js +41 -1
  36. package/client/components/terminal/startup-queue.js +67 -13
  37. package/client/components/terminal/terminal-select-text.jsx +69 -0
  38. package/client/components/terminal/terminal.jsx +72 -4
  39. package/client/components/terminal/terminal.styl +61 -0
  40. package/client/components/theme/theme-form.jsx +18 -21
  41. package/client/components/theme/theme-form.styl +8 -10
  42. package/client/components/vnc/vnc-session.jsx +72 -104
  43. package/client/components/vnc/vnc.styl +25 -16
  44. package/client/css/mobile.styl +20 -7
  45. package/client/store/setting.js +4 -0
  46. package/client/store/tab.js +5 -4
  47. package/package.json +1 -1
@@ -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 label = buildComboLabel(mods, key)
79
- const data = buildComboData(mods, key)
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={modifierOptions.map(m => ({
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
- const app = appCursorMap[data]
62
- if (app && term?.term?.modes?.applicationCursorKeysMode) {
63
- return app
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 !!config.restoreTerminalSessionOnReload && isSshTab(tab)
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 { createRestoreCwdCommand } from './ssh-reload-state.js'
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.isSsh()
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
- const cwdCommand = this.isSsh()
116
- ? createRestoreCwdCommand(startFolder)
117
- : startFolder ? `cd "${startFolder}"` : ''
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.isSsh())
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.isSsh()) {
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
- // Don't inject for sh type shells unless sftpPathFollowSsh is true
321
- if (shellType === 'sh' && !this.term.props.sftpPathFollowSsh) {
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.isSsh() ? 5000 : 3000
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
- longPressMoveTolerance = 10 // px
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