@electerm/electerm-react 5.3.26 → 5.5.6
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 +14 -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/footer-entry.jsx +35 -5
- package/client/components/footer/footer.styl +8 -1
- 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/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 +7 -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/side-panel-r/side-panel-r.jsx +2 -2
- 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 +88 -0
- package/client/components/terminal/mixins/term-context-menu.jsx +291 -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 +136 -0
- package/client/components/terminal/shell-detect.js +37 -0
- package/client/components/terminal/shell.js +0 -28
- package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
- package/client/components/terminal/shortcut-bar.jsx +4 -6
- package/client/components/terminal/shortcut-bar.styl +4 -0
- package/client/components/terminal/ssh-reload-state.js +56 -0
- package/client/components/terminal/startup-queue.js +379 -0
- package/client/components/terminal/terminal-interactive.jsx +32 -0
- package/client/components/terminal/terminal.jsx +102 -2007
- 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 +9 -1
- 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 +41 -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
|
@@ -1,117 +1,66 @@
|
|
|
1
1
|
import { Component, createRef } from 'react'
|
|
2
|
-
import { isEqual, pick
|
|
3
|
-
import clone from '../../common/to-simple-obj.js'
|
|
4
|
-
import resolve from '../../common/resolve.js'
|
|
2
|
+
import { isEqual, pick } from 'lodash-es'
|
|
5
3
|
import {
|
|
6
4
|
Spin,
|
|
7
5
|
Dropdown,
|
|
8
6
|
Button
|
|
9
7
|
} from 'antd'
|
|
10
|
-
import message from '../common/message'
|
|
11
|
-
import { notification } from '../common/notification'
|
|
12
|
-
import ShowItem from '../common/show-item.jsx'
|
|
13
|
-
import Modal from '../common/modal'
|
|
14
8
|
import classnames from 'classnames'
|
|
15
9
|
import './terminal.styl'
|
|
16
10
|
import {
|
|
17
|
-
statusMap,
|
|
18
11
|
paneMap,
|
|
19
|
-
|
|
20
|
-
isWin,
|
|
21
|
-
rendererTypes,
|
|
22
|
-
isMac,
|
|
23
|
-
isMacJs,
|
|
24
|
-
connectionMap,
|
|
25
|
-
terminalSerialType
|
|
12
|
+
connectionMap
|
|
26
13
|
} from '../../common/constants.js'
|
|
27
|
-
import deepCopy from 'json-deep-copy'
|
|
28
|
-
import { readClipboardAsync, readClipboard, copy } from '../../common/clipboard.js'
|
|
29
|
-
import AttachAddon from './attach-addon-custom.js'
|
|
30
|
-
import getProxy from '../../common/get-proxy.js'
|
|
31
|
-
import { ZmodemClient } from './zmodem-client.js'
|
|
32
|
-
import { TrzszClient } from './trzsz-client.js'
|
|
33
|
-
import { XmodemClient } from './xmodem-client.js'
|
|
34
|
-
import DropFileModal from './drop-file-modal.jsx'
|
|
35
|
-
import keyControlPressed from '../../common/key-control-pressed.js'
|
|
36
|
-
import NormalBuffer from './normal-buffer.jsx'
|
|
37
|
-
import { createTerm, resizeTerm, startTerminalLogFile, toggleTerminalLog } from './terminal-apis.js'
|
|
38
|
-
import { shortcutExtend, shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
|
|
39
|
-
import { KeywordHighlighterAddon } from './highlight-addon.js'
|
|
40
|
-
import { getFilePath, isUnsafeFilename } from '../../common/file-drop-utils.js'
|
|
41
|
-
import { getFolderFromFilePath } from '../sftp/file-read.js'
|
|
42
|
-
import { CommandTrackerAddon } from './command-tracker-addon.js'
|
|
43
|
-
import { Osc52Addon } from './osc52-addon.js'
|
|
44
|
-
import AIIcon from '../icons/ai-icon.jsx'
|
|
45
|
-
import { isAIDisabled } from '../../common/ai-feature.js'
|
|
46
14
|
import {
|
|
47
15
|
AimOutlined
|
|
48
16
|
} from '@ant-design/icons'
|
|
49
|
-
import {
|
|
50
|
-
|
|
51
|
-
detectRemoteShell,
|
|
52
|
-
detectShellType
|
|
53
|
-
} from './shell.js'
|
|
54
|
-
import iconsMap from '../sys-menu/icons-map.jsx'
|
|
55
|
-
import { refs, refsStatic } from '../common/ref.js'
|
|
56
|
-
import ExternalLink from '../common/external-link.jsx'
|
|
17
|
+
import { refs } from '../common/ref.js'
|
|
18
|
+
import NormalBuffer from './normal-buffer.jsx'
|
|
57
19
|
import createDefaultLogPath from '../../common/default-log-path.js'
|
|
58
20
|
import SearchResultBar from './terminal-search-bar'
|
|
59
21
|
import RemoteFloatControl from '../common/remote-float-control'
|
|
60
22
|
import ReconnectOverlay from './reconnect-overlay.jsx'
|
|
61
23
|
import TerminalErrorHandle from './terminal-error-handle.jsx'
|
|
62
|
-
import
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
} from './
|
|
72
|
-
import {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
} from './
|
|
24
|
+
import DropFileModal from './drop-file-modal.jsx'
|
|
25
|
+
import { StartupQueue } from './startup-queue.js'
|
|
26
|
+
import { detectRemoteShell } from './shell-detect.js'
|
|
27
|
+
import { shortcutExtend, shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
|
|
28
|
+
import { applyMixins } from './mixins/index.js'
|
|
29
|
+
import { initMixin } from './mixins/term-init.js'
|
|
30
|
+
import { resizeMixin } from './mixins/term-resize.js'
|
|
31
|
+
import { themeMixin } from './mixins/term-theme.js'
|
|
32
|
+
import { attachMixin } from './mixins/term-attach.js'
|
|
33
|
+
import { socketMixin } from './mixins/term-socket.js'
|
|
34
|
+
import { configMixin } from './mixins/term-config.jsx'
|
|
35
|
+
import { contextMenuMixin } from './mixins/term-context-menu.jsx'
|
|
36
|
+
import { contentMixin } from './mixins/term-content.js'
|
|
37
|
+
import { logMixin } from './mixins/term-log.jsx'
|
|
38
|
+
import { touchMixin } from './mixins/term-touch.js'
|
|
39
|
+
import { fileDropMixin } from './mixins/term-file-drop.js'
|
|
40
|
+
import { suggestionsMixin } from './mixins/term-suggestions.js'
|
|
77
41
|
|
|
78
42
|
const e = window.translate
|
|
79
43
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
i++
|
|
96
|
-
} else if (next === '\\') {
|
|
97
|
-
result += '\\'
|
|
98
|
-
i++
|
|
99
|
-
} else if (next === 'x' && /^[0-9a-fA-F]{2}$/.test(text.slice(i + 2, i + 4))) {
|
|
100
|
-
result += String.fromCharCode(parseInt(text.slice(i + 2, i + 4), 16))
|
|
101
|
-
i += 3
|
|
102
|
-
} else {
|
|
103
|
-
result += text[i]
|
|
104
|
-
}
|
|
105
|
-
} else {
|
|
106
|
-
result += text[i]
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return result
|
|
110
|
-
}
|
|
44
|
+
// The terminal behaviour, grouped by concern - see ./mixins/index.js
|
|
45
|
+
const mixins = [
|
|
46
|
+
initMixin,
|
|
47
|
+
resizeMixin,
|
|
48
|
+
themeMixin,
|
|
49
|
+
attachMixin,
|
|
50
|
+
socketMixin,
|
|
51
|
+
configMixin,
|
|
52
|
+
contextMenuMixin,
|
|
53
|
+
contentMixin,
|
|
54
|
+
logMixin,
|
|
55
|
+
touchMixin,
|
|
56
|
+
fileDropMixin,
|
|
57
|
+
suggestionsMixin
|
|
58
|
+
]
|
|
111
59
|
|
|
112
60
|
class Term extends Component {
|
|
113
61
|
constructor (props) {
|
|
114
62
|
super(props)
|
|
63
|
+
applyMixins(this, mixins)
|
|
115
64
|
this.state = {
|
|
116
65
|
loading: false,
|
|
117
66
|
hasSelection: false,
|
|
@@ -135,14 +84,48 @@ class Term extends Component {
|
|
|
135
84
|
this.id = `term-${this.props.tab.id}`
|
|
136
85
|
refs.add(this.id, this)
|
|
137
86
|
this.currentInput = ''
|
|
138
|
-
|
|
139
|
-
this.
|
|
87
|
+
// Owns the shell integration injection + cd + runScripts sequence.
|
|
88
|
+
this.startupQueue = new StartupQueue(this, { detectRemoteShell })
|
|
140
89
|
}
|
|
141
90
|
|
|
142
91
|
domRef = createRef()
|
|
143
92
|
|
|
93
|
+
timers = {}
|
|
94
|
+
|
|
95
|
+
// Config keys that cannot be applied by a re-render - they are pushed into
|
|
96
|
+
// the live xterm options instead.
|
|
97
|
+
terminalConfigProps = [
|
|
98
|
+
{
|
|
99
|
+
name: 'rightClickSelectsWord',
|
|
100
|
+
type: 'glob'
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'fontSize',
|
|
104
|
+
type: 'glob_local'
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'fontFamily',
|
|
108
|
+
type: 'glob_local'
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
// ---- Mobile touch support ----
|
|
113
|
+
longPressTimer = null
|
|
114
|
+
touchStartPos = null
|
|
115
|
+
longPressFired = false
|
|
116
|
+
longPressThreshold = 500 // ms
|
|
117
|
+
longPressMoveTolerance = 10 // px
|
|
118
|
+
|
|
119
|
+
terminalColorQueryDisposables = []
|
|
120
|
+
|
|
144
121
|
componentDidMount () {
|
|
145
|
-
|
|
122
|
+
// xterm and every addon are dynamic imports: a failed chunk fetch would
|
|
123
|
+
// otherwise end as a silent unhandled rejection with a permanently blank
|
|
124
|
+
// terminal pane
|
|
125
|
+
this.initTerminal().catch(e => {
|
|
126
|
+
console.error(e)
|
|
127
|
+
this.handleError({ message: e.message })
|
|
128
|
+
})
|
|
146
129
|
if (this.props.tab.enableSsh === false) {
|
|
147
130
|
this.props.tab.pane = paneMap.fileManager
|
|
148
131
|
}
|
|
@@ -187,6 +170,12 @@ class Term extends Component {
|
|
|
187
170
|
prevProps,
|
|
188
171
|
this.props
|
|
189
172
|
)
|
|
173
|
+
if (
|
|
174
|
+
prevProps.tab?.triggers !== this.props.tab?.triggers &&
|
|
175
|
+
this.triggerManager
|
|
176
|
+
) {
|
|
177
|
+
this.refreshTriggers()
|
|
178
|
+
}
|
|
190
179
|
const themeChanged = !isEqual(
|
|
191
180
|
this.props.themeConfig,
|
|
192
181
|
prevProps.themeConfig
|
|
@@ -214,6 +203,7 @@ class Term extends Component {
|
|
|
214
203
|
this.term.parent = null
|
|
215
204
|
}
|
|
216
205
|
this.disposeTerminalColorQueryHandlers()
|
|
206
|
+
this.startupQueue?.dispose()
|
|
217
207
|
window.cancelAnimationFrame(this.timers.themeRaf)
|
|
218
208
|
this.timers.themeRaf = null
|
|
219
209
|
Object.keys(this.timers).forEach(k => {
|
|
@@ -230,6 +220,7 @@ class Term extends Component {
|
|
|
230
220
|
this.term = null
|
|
231
221
|
}
|
|
232
222
|
this.attachAddon = null
|
|
223
|
+
this.disposeTriggerManager?.()
|
|
233
224
|
this.fitAddon = null
|
|
234
225
|
this.zmodemClient = null
|
|
235
226
|
this.trzszClient = null
|
|
@@ -237,6 +228,7 @@ class Term extends Component {
|
|
|
237
228
|
this.searchAddon = null
|
|
238
229
|
this.fitAddon = null
|
|
239
230
|
this.cmdAddon = null
|
|
231
|
+
this.serializeAddon = null
|
|
240
232
|
this.imageAddon = null
|
|
241
233
|
this.webglContextLossDisposable?.dispose?.()
|
|
242
234
|
this.webglContextLossDisposable = null
|
|
@@ -244,1920 +236,42 @@ class Term extends Component {
|
|
|
244
236
|
this.webglRecovering = false
|
|
245
237
|
}
|
|
246
238
|
|
|
247
|
-
|
|
248
|
-
{
|
|
249
|
-
name: 'rightClickSelectsWord',
|
|
250
|
-
type: 'glob'
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
name: 'fontSize',
|
|
254
|
-
type: 'glob_local'
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
name: 'fontFamily',
|
|
258
|
-
type: 'glob_local'
|
|
259
|
-
}
|
|
260
|
-
]
|
|
261
|
-
|
|
262
|
-
initAttachAddon = async () => {
|
|
263
|
-
this.attachAddon = new AttachAddon(
|
|
264
|
-
this.term,
|
|
265
|
-
this.socket,
|
|
266
|
-
isWin && !this.isRemote()
|
|
267
|
-
)
|
|
268
|
-
this.attachAddon.decoder = new TextDecoder(
|
|
269
|
-
this.encode || this.props.tab.encode || 'utf-8'
|
|
270
|
-
)
|
|
271
|
-
await this.attachAddon.activate(this.term)
|
|
272
|
-
if (this.osc52Addon) {
|
|
273
|
-
this.osc52Addon.setSendData(this.attachAddon._sendData.bind(this.attachAddon))
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
getValue = (props, type, name) => {
|
|
278
|
-
return type === 'glob'
|
|
279
|
-
? props.config[name]
|
|
280
|
-
: props.tab[name] || props.config[name]
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
checkConfigChange = (prevProps, props) => {
|
|
284
|
-
for (const k of this.terminalConfigProps) {
|
|
285
|
-
const { name, type } = k
|
|
286
|
-
const prev = this.getValue(prevProps, type, name)
|
|
287
|
-
const curr = this.getValue(props, type, name)
|
|
288
|
-
if (
|
|
289
|
-
prev !== curr
|
|
290
|
-
) {
|
|
291
|
-
this.term.options[name] = curr
|
|
292
|
-
if (['fontFamily', 'fontSize'].includes(name)) {
|
|
293
|
-
this.onResize()
|
|
294
|
-
}
|
|
295
|
-
if (name === 'fontSize') {
|
|
296
|
-
this.originalFontSize = curr
|
|
297
|
-
this.setState({ fontSizeChanged: false })
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Handle renderer type changes (dom <-> webGL) by reloading the
|
|
303
|
-
// renderer and refreshing the theme so the background color is
|
|
304
|
-
// correct for the new renderer.
|
|
305
|
-
if (
|
|
306
|
-
prevProps.config.rendererType !== props.config.rendererType &&
|
|
307
|
-
this.term
|
|
308
|
-
) {
|
|
309
|
-
this.reloadWebglRenderer('renderer type change')
|
|
310
|
-
.then(() => this.applyTerminalTheme())
|
|
311
|
-
.catch(e => console.error('renderer type change failed', e))
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Check for shell integration related config changes
|
|
315
|
-
const prevShowSuggestions = prevProps.config.showCmdSuggestions
|
|
316
|
-
const currShowSuggestions = props.config.showCmdSuggestions
|
|
317
|
-
const prevSftpFollow = prevProps.sftpPathFollowSsh
|
|
318
|
-
const currSftpFollow = props.sftpPathFollowSsh
|
|
319
|
-
|
|
320
|
-
if (
|
|
321
|
-
(!prevShowSuggestions && currShowSuggestions) ||
|
|
322
|
-
(!prevSftpFollow && currSftpFollow)
|
|
323
|
-
) {
|
|
324
|
-
// Config was toggled to true, try to inject shell integration if not already done
|
|
325
|
-
if (this.canInjectShellIntegration() && !this.shellInjected) {
|
|
326
|
-
// If there's an active execution queue, add to it
|
|
327
|
-
if (this.executionQueue && this.executionQueue.length > 0) {
|
|
328
|
-
this.executionQueue.unshift({
|
|
329
|
-
type: 'shell_integration',
|
|
330
|
-
execute: async () => {
|
|
331
|
-
await this.injectShellIntegration()
|
|
332
|
-
if (currSftpFollow) {
|
|
333
|
-
this.attachAddon._sendData('\r')
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
})
|
|
337
|
-
} else {
|
|
338
|
-
// No active queue, inject directly
|
|
339
|
-
this.injectShellIntegration().then(() => {
|
|
340
|
-
if (currSftpFollow) {
|
|
341
|
-
this.attachAddon._sendData('\r')
|
|
342
|
-
}
|
|
343
|
-
})
|
|
344
|
-
}
|
|
345
|
-
} else if (this.shellInjected && currSftpFollow) {
|
|
346
|
-
this.getCwd()
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
if (
|
|
350
|
-
!prevSftpFollow &&
|
|
351
|
-
currSftpFollow &&
|
|
352
|
-
this.isLocal() &&
|
|
353
|
-
isWin
|
|
354
|
-
) {
|
|
355
|
-
return this.warnSftpFollowUnsupported()
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
timers = {}
|
|
239
|
+
// ---- small helpers other mixins build on ----
|
|
360
240
|
|
|
361
|
-
getDomId
|
|
241
|
+
getDomId () {
|
|
362
242
|
return `term-${this.props.tab.id}`
|
|
363
243
|
}
|
|
364
244
|
|
|
365
|
-
|
|
366
|
-
const { term } = this
|
|
367
|
-
if (!term) {
|
|
368
|
-
return
|
|
369
|
-
}
|
|
370
|
-
term.options.fontSize = term.options.fontSize + v
|
|
371
|
-
window.store.triggerResize()
|
|
372
|
-
if (this.originalFontSize == null) {
|
|
373
|
-
this.originalFontSize = term.options.fontSize - v
|
|
374
|
-
}
|
|
375
|
-
this.setState({
|
|
376
|
-
fontSizeChanged: term.options.fontSize !== this.originalFontSize
|
|
377
|
-
})
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
handleResetFontSize = () => {
|
|
381
|
-
const { term } = this
|
|
382
|
-
if (!term || this.originalFontSize == null) {
|
|
383
|
-
return
|
|
384
|
-
}
|
|
385
|
-
term.options.fontSize = this.originalFontSize
|
|
386
|
-
window.store.triggerResize()
|
|
387
|
-
this.setState({ fontSizeChanged: false })
|
|
388
|
-
term.focus()
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
isActiveTerminal = () => {
|
|
245
|
+
isActiveTerminal () {
|
|
392
246
|
return this.props.tab.id === this.props.activeTabId &&
|
|
393
247
|
this.props.tab.pane === paneMap.terminal
|
|
394
248
|
}
|
|
395
249
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
this.onClear()
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// selectAllShortcut = (e) => {
|
|
402
|
-
// e.stopPropagation()
|
|
403
|
-
// this.term.selectAll()
|
|
404
|
-
// }
|
|
405
|
-
|
|
406
|
-
copyShortcut = (e) => {
|
|
407
|
-
const sel = this.term.getSelection()
|
|
408
|
-
if (sel) {
|
|
409
|
-
e.stopPropagation()
|
|
410
|
-
this.copySelectionToClipboard()
|
|
411
|
-
return false
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
searchShortcut = (e) => {
|
|
416
|
-
e.stopPropagation()
|
|
417
|
-
this.toggleSearch()
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
pasteSelectedShortcut = (e) => {
|
|
421
|
-
e.stopPropagation()
|
|
422
|
-
this.tryInsertSelected()
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
pasteTextTooLong = () => {
|
|
426
|
-
if (window.et.isWebApp) {
|
|
427
|
-
return false
|
|
428
|
-
}
|
|
429
|
-
const text = readClipboard()
|
|
430
|
-
return text.length > 500
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
askUserConfirm = () => {
|
|
434
|
-
Modal.confirm({
|
|
435
|
-
title: e('paste'),
|
|
436
|
-
content: (
|
|
437
|
-
<div>
|
|
438
|
-
<p>{e('paste')}:</p>
|
|
439
|
-
<div className='paste-text'>
|
|
440
|
-
<pre>
|
|
441
|
-
<code>{readClipboard()}</code>
|
|
442
|
-
</pre>
|
|
443
|
-
</div>
|
|
444
|
-
</div>
|
|
445
|
-
),
|
|
446
|
-
okText: e('ok'),
|
|
447
|
-
cancelText: e('cancel'),
|
|
448
|
-
onOk: () => this.onPaste(true)
|
|
449
|
-
})
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
warnSftpFollowUnsupported = () => {
|
|
453
|
-
message.warning(
|
|
454
|
-
<span>
|
|
455
|
-
Fish shell/windows shell is not supported for SFTP follow SSH path feature. See: <ExternalLink to='https://github.com/electerm/electerm/wiki/Warning-about-sftp-follow-ssh-path-function'>wiki</ExternalLink>
|
|
456
|
-
</span>
|
|
457
|
-
, 7)
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
pasteShortcut = (e) => {
|
|
461
|
-
if (this.pasteTextTooLong()) {
|
|
462
|
-
this.askUserConfirm()
|
|
463
|
-
e.preventDefault()
|
|
464
|
-
e.stopPropagation()
|
|
465
|
-
return false
|
|
466
|
-
}
|
|
467
|
-
if (isMac) {
|
|
468
|
-
return true
|
|
469
|
-
}
|
|
470
|
-
if (!this.isRemote()) {
|
|
471
|
-
return true
|
|
472
|
-
}
|
|
473
|
-
if (this.term.buffer.active.type !== 'alternate') {
|
|
474
|
-
return false
|
|
475
|
-
}
|
|
476
|
-
return true
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
showNormalBufferShortcut = (e) => {
|
|
480
|
-
e.stopPropagation()
|
|
481
|
-
this.openNormalBuffer()
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
runQuickCommand = (cmd, inputOnly = false) => {
|
|
485
|
-
if (this.term && this.attachAddon) {
|
|
486
|
-
this.attachAddon._sendData(cmd + (inputOnly ? '' : '\r'))
|
|
487
|
-
this.term.focus()
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
cd = (p) => {
|
|
492
|
-
if (isUnsafeFilename(p)) {
|
|
493
|
-
return message.error('File name contains unsafe characters')
|
|
494
|
-
}
|
|
495
|
-
const isWinPath = /^[a-zA-Z]:\\/.test(p)
|
|
496
|
-
this.runQuickCommand(isWinPath ? `cd /d "${p}"` : `cd "${p}"`)
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
onDrop = e => {
|
|
500
|
-
const dt = e.dataTransfer
|
|
501
|
-
const fromFile = dt.getData('fromFile')
|
|
502
|
-
const notSafeMsg = 'File name contains unsafe characters'
|
|
503
|
-
const isSshTerminal = this.props.tab.type === connectionMap.ssh
|
|
504
|
-
const isSerialTerminal = this.props.tab.type === connectionMap.serial
|
|
505
|
-
|
|
506
|
-
if (fromFile) {
|
|
507
|
-
try {
|
|
508
|
-
const fileData = JSON.parse(fromFile)
|
|
509
|
-
const filePath = resolve(fileData.path, fileData.name)
|
|
510
|
-
if (isUnsafeFilename(filePath)) {
|
|
511
|
-
message.error(notSafeMsg)
|
|
512
|
-
return
|
|
513
|
-
}
|
|
514
|
-
if (isSshTerminal) {
|
|
515
|
-
const behavior = this.props.config.dragDropBehavior || 'ask'
|
|
516
|
-
if (behavior === 'ask') {
|
|
517
|
-
this.setState({
|
|
518
|
-
dropFileModalVisible: true,
|
|
519
|
-
droppedFiles: [{ path: filePath, isRemote: true }]
|
|
520
|
-
})
|
|
521
|
-
} else {
|
|
522
|
-
this.handleDropFileAction(behavior, [{ path: filePath, isRemote: true }])
|
|
523
|
-
}
|
|
524
|
-
return
|
|
525
|
-
}
|
|
526
|
-
if (isSerialTerminal) {
|
|
527
|
-
this.setState({
|
|
528
|
-
dropFileModalVisible: true,
|
|
529
|
-
droppedFiles: [{ path: filePath, isRemote: false }]
|
|
530
|
-
})
|
|
531
|
-
return
|
|
532
|
-
}
|
|
533
|
-
this.attachAddon._sendData(`"${filePath}" `)
|
|
534
|
-
return
|
|
535
|
-
} catch (e) {
|
|
536
|
-
console.error('Failed to parse fromFile data:', e)
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const files = dt.files
|
|
541
|
-
if (files && files.length) {
|
|
542
|
-
const arr = Array.from(files)
|
|
543
|
-
const filePaths = arr.map(f => getFilePath(f))
|
|
544
|
-
|
|
545
|
-
const hasUnsafeFilename = filePaths.some(path => isUnsafeFilename(path))
|
|
546
|
-
if (hasUnsafeFilename) {
|
|
547
|
-
message.error(notSafeMsg)
|
|
548
|
-
return
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
if (isSshTerminal) {
|
|
552
|
-
const behavior = this.props.config.dragDropBehavior || 'ask'
|
|
553
|
-
if (behavior === 'ask') {
|
|
554
|
-
this.setState({
|
|
555
|
-
dropFileModalVisible: true,
|
|
556
|
-
droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
|
|
557
|
-
})
|
|
558
|
-
} else {
|
|
559
|
-
this.handleDropFileAction(behavior, filePaths.map(path => ({ path, isRemote: false })))
|
|
560
|
-
}
|
|
561
|
-
return
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
if (isSerialTerminal) {
|
|
565
|
-
this.setState({
|
|
566
|
-
dropFileModalVisible: true,
|
|
567
|
-
droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
|
|
568
|
-
})
|
|
569
|
-
return
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
573
|
-
this.attachAddon._sendData(filesAll)
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
handleDropFileModalCancel = () => {
|
|
578
|
-
this.setState({
|
|
579
|
-
dropFileModalVisible: false,
|
|
580
|
-
droppedFiles: []
|
|
581
|
-
})
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
handleDropFileAction = (action, filesOverride) => {
|
|
585
|
-
const droppedFiles = filesOverride || this.state.droppedFiles
|
|
586
|
-
if (!droppedFiles || !droppedFiles.length) {
|
|
587
|
-
this.handleDropFileModalCancel()
|
|
588
|
-
return
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const filePaths = droppedFiles.map(f => f.path)
|
|
592
|
-
|
|
593
|
-
switch (action) {
|
|
594
|
-
case 'trz': {
|
|
595
|
-
if (this.trzszClient && this.trzszClient.isActive) {
|
|
596
|
-
message.warning('A transfer is already in progress')
|
|
597
|
-
this.handleDropFileModalCancel()
|
|
598
|
-
return
|
|
599
|
-
}
|
|
600
|
-
window._apiControlSelectFile = filePaths
|
|
601
|
-
this.attachAddon._sendData('trz\r')
|
|
602
|
-
break
|
|
603
|
-
}
|
|
604
|
-
case 'rz':{
|
|
605
|
-
if (this.zmodemClient && this.zmodemClient.isActive) {
|
|
606
|
-
message.warning('A transfer is already in progress')
|
|
607
|
-
this.handleDropFileModalCancel()
|
|
608
|
-
return
|
|
609
|
-
}
|
|
610
|
-
window._apiControlSelectFile = filePaths
|
|
611
|
-
this.attachAddon._sendData('rz\r')
|
|
612
|
-
break
|
|
613
|
-
}
|
|
614
|
-
case 'xmodem': {
|
|
615
|
-
if (this.xmodemClient && this.xmodemClient.isActive) {
|
|
616
|
-
message.warning('A transfer is already in progress')
|
|
617
|
-
this.handleDropFileModalCancel()
|
|
618
|
-
return
|
|
619
|
-
}
|
|
620
|
-
// Use XMODEM send with the dropped files
|
|
621
|
-
window._apiControlSelectFile = filePaths
|
|
622
|
-
if (this.xmodemClient) {
|
|
623
|
-
this.xmodemClient.initiateSend()
|
|
624
|
-
}
|
|
625
|
-
break
|
|
626
|
-
}
|
|
627
|
-
case 'inputOnly':
|
|
628
|
-
default: {
|
|
629
|
-
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
630
|
-
this.attachAddon._sendData(filesAll)
|
|
631
|
-
break
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
this.handleDropFileModalCancel()
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
onSelection = () => {
|
|
639
|
-
if (
|
|
640
|
-
!this.props.config.copyWhenSelect ||
|
|
641
|
-
window.store.onOperation
|
|
642
|
-
) {
|
|
643
|
-
return false
|
|
644
|
-
}
|
|
645
|
-
this.copySelectionToClipboard()
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
copySelectionToClipboard = () => {
|
|
649
|
-
const txt = this.term.getSelection()
|
|
650
|
-
if (txt) {
|
|
651
|
-
copy(txt)
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
tryInsertSelected = () => {
|
|
656
|
-
const txt = this.term.getSelection()
|
|
657
|
-
if (txt) {
|
|
658
|
-
this.attachAddon._sendData(txt)
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
webLinkHandler = (event, url) => {
|
|
663
|
-
if (event?.button === 2) {
|
|
664
|
-
return false
|
|
665
|
-
}
|
|
666
|
-
if (!this.props.config.ctrlOrMetaOpenTerminalLink) {
|
|
667
|
-
return window.openLink(url, '_blank')
|
|
668
|
-
}
|
|
669
|
-
if (keyControlPressed(event)) {
|
|
670
|
-
window.openLink(url, '_blank')
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// ---- Mobile touch support ----
|
|
675
|
-
// On touch devices, long-press should (1) select the word under the finger
|
|
676
|
-
// and (2) open the context menu — mirroring desktop right-click behaviour.
|
|
677
|
-
// xterm.js only uses touch events for scrolling, so we add explicit
|
|
678
|
-
// long-press detection here.
|
|
679
|
-
longPressTimer = null
|
|
680
|
-
touchStartPos = null
|
|
681
|
-
longPressFired = false
|
|
682
|
-
longPressThreshold = 500 // ms
|
|
683
|
-
longPressMoveTolerance = 10 // px
|
|
684
|
-
|
|
685
|
-
onTouchStart = (e) => {
|
|
686
|
-
if (e.touches.length !== 1) {
|
|
687
|
-
return
|
|
688
|
-
}
|
|
689
|
-
const touch = e.touches[0]
|
|
690
|
-
this.touchStartPos = {
|
|
691
|
-
clientX: touch.clientX,
|
|
692
|
-
clientY: touch.clientY,
|
|
693
|
-
target: e.currentTarget
|
|
694
|
-
}
|
|
695
|
-
this.longPressFired = false
|
|
696
|
-
clearTimeout(this.longPressTimer)
|
|
697
|
-
this.longPressTimer = setTimeout(() => {
|
|
698
|
-
this.handleLongPress()
|
|
699
|
-
}, this.longPressThreshold)
|
|
250
|
+
isRemote () {
|
|
251
|
+
return this.props.tab?.host
|
|
700
252
|
}
|
|
701
253
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
const touch = e.touches[0]
|
|
707
|
-
const dx = touch.clientX - this.touchStartPos.clientX
|
|
708
|
-
const dy = touch.clientY - this.touchStartPos.clientY
|
|
709
|
-
if (Math.sqrt(dx * dx + dy * dy) > this.longPressMoveTolerance) {
|
|
710
|
-
clearTimeout(this.longPressTimer)
|
|
711
|
-
this.longPressTimer = null
|
|
712
|
-
this.touchStartPos = null
|
|
713
|
-
}
|
|
254
|
+
isSsh () {
|
|
255
|
+
const { host, type } = this.props.tab
|
|
256
|
+
return host && (type === 'ssh' || type === undefined)
|
|
714
257
|
}
|
|
715
258
|
|
|
716
|
-
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
this.touchStartPos = null
|
|
721
|
-
// xterm's own touch (Gesture) handler calls preventDefault()/stopPropagation()
|
|
722
|
-
// on touchstart/touchend, which suppresses the synthesised mousedown xterm
|
|
723
|
-
// relies on to focus its hidden helper textarea. As a result a tap never
|
|
724
|
-
// focuses the terminal on touch devices, so the soft keyboard never opens
|
|
725
|
-
// and you cannot type. Focus explicitly on a clean tap (not a long-press,
|
|
726
|
-
// not a scroll) so mobile input works. The focus() runs synchronously inside
|
|
727
|
-
// this user-gesture handler, so iOS/Android will show the keyboard.
|
|
728
|
-
if (wasTap && this.term) {
|
|
729
|
-
this.term.focus()
|
|
730
|
-
}
|
|
259
|
+
isLocal () {
|
|
260
|
+
const { host, type } = this.props.tab
|
|
261
|
+
return !host &&
|
|
262
|
+
(type === 'local' || type === undefined)
|
|
731
263
|
}
|
|
732
264
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
this.longPressFired = true
|
|
738
|
-
const { clientX, clientY, target } = this.touchStartPos
|
|
739
|
-
|
|
740
|
-
// Select the word at the touch position (same as desktop right-click word
|
|
741
|
-
// select) so the user can immediately copy or act on it.
|
|
742
|
-
this.selectWordAt(clientX, clientY)
|
|
743
|
-
|
|
744
|
-
// Respect pasteWhenContextMenu: when enabled, long-press pastes instead
|
|
745
|
-
// of showing the menu (same as desktop right-click).
|
|
746
|
-
if (this.props.config.pasteWhenContextMenu) {
|
|
747
|
-
this.onPaste()
|
|
748
|
-
return
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// Dispatch a synthetic contextmenu event so antd Dropdown's contextMenu
|
|
752
|
-
// trigger opens the menu at the exact finger position.
|
|
753
|
-
const event = new MouseEvent('contextmenu', {
|
|
754
|
-
bubbles: true,
|
|
755
|
-
cancelable: true,
|
|
756
|
-
clientX,
|
|
757
|
-
clientY
|
|
265
|
+
setStatus = status => {
|
|
266
|
+
const id = this.props.tab?.id
|
|
267
|
+
this.props.editTab(id, {
|
|
268
|
+
status
|
|
758
269
|
})
|
|
759
|
-
target.dispatchEvent(event)
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
selectWordAt = (clientX, clientY) => {
|
|
763
|
-
if (!this.term) {
|
|
764
|
-
return
|
|
765
|
-
}
|
|
766
|
-
const termElement = this.term.element
|
|
767
|
-
if (!termElement) {
|
|
768
|
-
return
|
|
769
|
-
}
|
|
770
|
-
const rect = termElement.getBoundingClientRect()
|
|
771
|
-
const x = clientX - rect.left
|
|
772
|
-
const y = clientY - rect.top
|
|
773
|
-
if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
|
|
774
|
-
return
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const cellWidth = rect.width / this.term.cols
|
|
778
|
-
const cellHeight = rect.height / this.term.rows
|
|
779
|
-
const col = Math.floor(x / cellWidth)
|
|
780
|
-
const row = Math.floor(y / cellHeight)
|
|
781
|
-
|
|
782
|
-
const buffer = this.term.buffer.active
|
|
783
|
-
const line = buffer.getLine(row)
|
|
784
|
-
if (!line) {
|
|
785
|
-
return
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
const text = line.translateToString(true)
|
|
789
|
-
const wordSeparator = this.props.config.terminalWordSeparator ||
|
|
790
|
-
' ./\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~ ?'
|
|
791
|
-
|
|
792
|
-
// If the touched cell is empty or a separator, nothing to select
|
|
793
|
-
if (col >= text.length || wordSeparator.includes(text[col])) {
|
|
794
|
-
return
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// Find word start
|
|
798
|
-
let start = col
|
|
799
|
-
while (start > 0 && !wordSeparator.includes(text[start - 1])) {
|
|
800
|
-
start--
|
|
801
|
-
}
|
|
802
|
-
// Find word end
|
|
803
|
-
let end = col
|
|
804
|
-
while (end < text.length && !wordSeparator.includes(text[end])) {
|
|
805
|
-
end++
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
if (end > start) {
|
|
809
|
-
this.term.select(start, row, end - start)
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
onContextMenuInner = e => {
|
|
814
|
-
e.preventDefault()
|
|
815
|
-
if (this.state.loading) {
|
|
816
|
-
return
|
|
817
|
-
}
|
|
818
|
-
if (this.props.config.pasteWhenContextMenu) {
|
|
819
|
-
return this.onPaste()
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
onCopy = () => {
|
|
824
|
-
const selected = this.term.getSelection()
|
|
825
|
-
copy(selected)
|
|
826
|
-
this.term.focus()
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
onSelectAll = () => {
|
|
830
|
-
this.term.selectAll()
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
onClear = () => {
|
|
834
|
-
const shouldClear = this.searchAddon &&
|
|
835
|
-
window.store.termSearchOpen &&
|
|
836
|
-
window.store.termSearch
|
|
837
|
-
if (
|
|
838
|
-
shouldClear
|
|
839
|
-
) {
|
|
840
|
-
this.searchAddon.clearDecorations()
|
|
841
|
-
}
|
|
842
|
-
this.term.clear()
|
|
843
|
-
this.term.focus()
|
|
844
|
-
if (shouldClear) {
|
|
845
|
-
this.searchAddon._lineCache.clear()
|
|
846
|
-
this.timers.clearSearchTimer = setTimeout(() => {
|
|
847
|
-
refsStatic.get('term-search')?.next()
|
|
848
|
-
}, 100)
|
|
849
|
-
}
|
|
850
270
|
}
|
|
851
271
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
onPaste = async (skipTextLengthCheck) => {
|
|
857
|
-
let selected = await readClipboardAsync()
|
|
858
|
-
if (!skipTextLengthCheck && selected.length > 500) {
|
|
859
|
-
return this.askUserConfirm()
|
|
860
|
-
}
|
|
861
|
-
if (isWin && this.isRemote()) {
|
|
862
|
-
selected = selected.replace(/\r\n/g, '\n')
|
|
863
|
-
}
|
|
864
|
-
this.term.paste(selected || '')
|
|
865
|
-
this.term.focus()
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
onPasteSelected = () => {
|
|
869
|
-
const selected = this.term.getSelection()
|
|
870
|
-
this.term.paste(selected || '')
|
|
871
|
-
this.term.focus()
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
toggleSearch = () => {
|
|
875
|
-
window.store.toggleTerminalSearch()
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
toggleKeepalive = () => {
|
|
879
|
-
if (!this.attachAddon) {
|
|
880
|
-
return false
|
|
881
|
-
}
|
|
882
|
-
this._keepaliveEnabled = !this._keepaliveEnabled
|
|
883
|
-
this.attachAddon.setKeepalive(this._keepaliveEnabled)
|
|
884
|
-
return this._keepaliveEnabled
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
onSearchResultsChange = ({ resultIndex, resultCount }) => {
|
|
888
|
-
window.store.storeAssign({
|
|
889
|
-
termSearchMatchCount: resultCount,
|
|
890
|
-
termSearchMatchIndex: resultIndex
|
|
891
|
-
})
|
|
892
|
-
|
|
893
|
-
this.updateSearchResults(resultIndex)
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
updateSearchResults = (resultIndex) => {
|
|
897
|
-
const matches = this.searchAddon._resultTracker.searchResults.map((result, i) => {
|
|
898
|
-
return result.row
|
|
899
|
-
})
|
|
900
|
-
|
|
901
|
-
this.setState({
|
|
902
|
-
searchResults: matches,
|
|
903
|
-
matchIndex: resultIndex,
|
|
904
|
-
totalLines: this.term.buffer.active.length
|
|
905
|
-
})
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
searchPrev = (searchInput, options) => {
|
|
909
|
-
this.searchAddon.findPrevious(
|
|
910
|
-
searchInput, options
|
|
911
|
-
)
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
searchNext = (searchInput, options) => {
|
|
915
|
-
this.searchAddon.findNext(
|
|
916
|
-
searchInput, options
|
|
917
|
-
)
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
explainWithAi = () => {
|
|
921
|
-
window.store.explainWithAi(
|
|
922
|
-
this.term.getSelection()
|
|
923
|
-
)
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
getTerminalBufferText = () => {
|
|
927
|
-
const { addTimeStampToTermLog } = this.state
|
|
928
|
-
const buffer = this.term.buffer.active
|
|
929
|
-
const len = buffer.length
|
|
930
|
-
const rawLines = []
|
|
931
|
-
for (let i = 0; i < len; i++) {
|
|
932
|
-
const line = buffer.getLine(i)
|
|
933
|
-
rawLines.push(line ? line.translateToString(false) : '')
|
|
934
|
-
}
|
|
935
|
-
// trim trailing blank lines before applying timestamps
|
|
936
|
-
while (rawLines.length && !rawLines[rawLines.length - 1].trim()) {
|
|
937
|
-
rawLines.pop()
|
|
938
|
-
}
|
|
939
|
-
if (!addTimeStampToTermLog) {
|
|
940
|
-
return rawLines.join('\n')
|
|
941
|
-
}
|
|
942
|
-
return rawLines.map(text => {
|
|
943
|
-
const now = new Date()
|
|
944
|
-
const ts = `[${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}] `
|
|
945
|
-
return ts + text
|
|
946
|
-
}).join('\n')
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
syncTermInfo = (stateUpdate) => {
|
|
950
|
-
this.setState(stateUpdate)
|
|
951
|
-
const infoUpdate = pick(stateUpdate, ['saveTerminalLogToFile', 'addTimeStampToTermLog', 'logPath', 'logFileName'])
|
|
952
|
-
if (Object.keys(infoUpdate).length) {
|
|
953
|
-
refs.get('term-info-' + this.props.tab.id)?.setState(infoUpdate)
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
openLogSaveDialog = async (titleKey) => {
|
|
958
|
-
const { logName } = this.props
|
|
959
|
-
const result = await window.api.saveDialog({
|
|
960
|
-
title: e(titleKey),
|
|
961
|
-
defaultPath: logName + '.log',
|
|
962
|
-
filters: [
|
|
963
|
-
{ name: 'Log files', extensions: ['log'] }
|
|
964
|
-
],
|
|
965
|
-
properties: ['createDirectory', 'showOverwriteConfirmation']
|
|
966
|
-
})
|
|
967
|
-
if (result.canceled || !result.filePath) {
|
|
968
|
-
return null
|
|
969
|
-
}
|
|
970
|
-
return result.filePath
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
onSaveTerminalLog = async () => {
|
|
974
|
-
const filePath = await this.openLogSaveDialog('saveTerminalLogToFile')
|
|
975
|
-
if (!filePath) {
|
|
976
|
-
return
|
|
977
|
-
}
|
|
978
|
-
const content = this.getTerminalBufferText()
|
|
979
|
-
await window.fs.writeFile(filePath, content).catch(window.store.onError)
|
|
980
|
-
const { addTimeStampToTermLog } = this.state
|
|
981
|
-
startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
|
|
982
|
-
const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
|
|
983
|
-
this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
|
|
984
|
-
notification.success({
|
|
985
|
-
message: e('saveTerminalLogToFile'),
|
|
986
|
-
description: <ShowItem to={filePath}>{filePath}</ShowItem>,
|
|
987
|
-
duration: 5
|
|
988
|
-
})
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
onRecord = async () => {
|
|
992
|
-
const filePath = await this.openLogSaveDialog('record')
|
|
993
|
-
if (!filePath) {
|
|
994
|
-
return
|
|
995
|
-
}
|
|
996
|
-
const { addTimeStampToTermLog } = this.state
|
|
997
|
-
startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
|
|
998
|
-
const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
|
|
999
|
-
this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
|
|
1000
|
-
this.setState({ recording: true, recordingFilePath: filePath })
|
|
1001
|
-
notification.success({
|
|
1002
|
-
message: e('record'),
|
|
1003
|
-
description: <ShowItem to={filePath}>{filePath}</ShowItem>,
|
|
1004
|
-
duration: 5
|
|
1005
|
-
})
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
onStopRecord = () => {
|
|
1009
|
-
const { recordingFilePath } = this.state
|
|
1010
|
-
toggleTerminalLog(this.pid).catch(window.store.onError)
|
|
1011
|
-
this.syncTermInfo({ saveTerminalLogToFile: false })
|
|
1012
|
-
this.setState({ recording: false, recordingFilePath: '' })
|
|
1013
|
-
notification.success({
|
|
1014
|
-
message: e('stopRecord'),
|
|
1015
|
-
description: <ShowItem to={recordingFilePath}>{recordingFilePath}</ShowItem>
|
|
1016
|
-
})
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
renderContextMenu = () => {
|
|
1020
|
-
const { hasSelection, recording } = this.state
|
|
1021
|
-
const copyed = true
|
|
1022
|
-
const copyShortcut = this.getShortcut('terminal_copy')
|
|
1023
|
-
const pasteShortcut = this.getShortcut('terminal_paste')
|
|
1024
|
-
const clearShortcut = this.getShortcut('terminal_clear')
|
|
1025
|
-
const searchShortcut = this.getShortcut('terminal_search')
|
|
1026
|
-
const selectAllShortcut = isMacJs ? 'meta+a' : 'ctrl+shift+a'
|
|
1027
|
-
const isSerial = this.props.tab?.type === connectionMap.serial
|
|
1028
|
-
const items = [
|
|
1029
|
-
{
|
|
1030
|
-
key: 'onCopy',
|
|
1031
|
-
icon: <iconsMap.CopyOutlined />,
|
|
1032
|
-
label: e('copy'),
|
|
1033
|
-
disabled: !hasSelection,
|
|
1034
|
-
extra: copyShortcut
|
|
1035
|
-
},
|
|
1036
|
-
{
|
|
1037
|
-
key: 'onPaste',
|
|
1038
|
-
icon: <iconsMap.SwitcherOutlined />,
|
|
1039
|
-
label: e('paste'),
|
|
1040
|
-
disabled: !copyed,
|
|
1041
|
-
extra: pasteShortcut
|
|
1042
|
-
},
|
|
1043
|
-
{
|
|
1044
|
-
key: 'onPasteSelected',
|
|
1045
|
-
icon: <iconsMap.SwitcherOutlined />,
|
|
1046
|
-
label: e('pasteSelected'),
|
|
1047
|
-
disabled: !hasSelection
|
|
1048
|
-
},
|
|
1049
|
-
{
|
|
1050
|
-
|
|
1051
|
-
key: 'onSelectAll',
|
|
1052
|
-
icon: <iconsMap.CheckSquareOutlined />,
|
|
1053
|
-
label: e('selectall'),
|
|
1054
|
-
extra: selectAllShortcut
|
|
1055
|
-
},
|
|
1056
|
-
...(
|
|
1057
|
-
isAIDisabled()
|
|
1058
|
-
? []
|
|
1059
|
-
: [{
|
|
1060
|
-
key: 'explainWithAi',
|
|
1061
|
-
icon: <AIIcon />,
|
|
1062
|
-
label: e('explainWithAi'),
|
|
1063
|
-
disabled: !hasSelection
|
|
1064
|
-
}]
|
|
1065
|
-
),
|
|
1066
|
-
{
|
|
1067
|
-
key: 'onClear',
|
|
1068
|
-
icon: <iconsMap.ReloadOutlined />,
|
|
1069
|
-
label: e('clear'),
|
|
1070
|
-
extra: clearShortcut
|
|
1071
|
-
},
|
|
1072
|
-
{
|
|
1073
|
-
key: 'toggleSearch',
|
|
1074
|
-
icon: <iconsMap.SearchOutlined />,
|
|
1075
|
-
label: e('search'),
|
|
1076
|
-
extra: searchShortcut
|
|
1077
|
-
},
|
|
1078
|
-
{
|
|
1079
|
-
key: 'onSaveTerminalLog',
|
|
1080
|
-
icon: <iconsMap.SaveOutlined />,
|
|
1081
|
-
label: e('saveTerminalLogToFile')
|
|
1082
|
-
},
|
|
1083
|
-
{
|
|
1084
|
-
key: recording ? 'onStopRecord' : 'onRecord',
|
|
1085
|
-
icon: recording ? <iconsMap.StopOutlined /> : <iconsMap.PlayCircleFilled />,
|
|
1086
|
-
label: e(recording ? 'stopRecord' : 'record')
|
|
1087
|
-
}
|
|
1088
|
-
]
|
|
1089
|
-
if (isSerial) {
|
|
1090
|
-
items.push(
|
|
1091
|
-
{
|
|
1092
|
-
type: 'divider'
|
|
1093
|
-
},
|
|
1094
|
-
{
|
|
1095
|
-
key: 'onXmodemSend',
|
|
1096
|
-
icon: <iconsMap.CloudUploadOutlined />,
|
|
1097
|
-
label: 'XMODEM Send'
|
|
1098
|
-
},
|
|
1099
|
-
{
|
|
1100
|
-
key: 'onXmodemReceive',
|
|
1101
|
-
icon: <iconsMap.CloudDownloadOutlined />,
|
|
1102
|
-
label: 'XMODEM Receive'
|
|
1103
|
-
}
|
|
1104
|
-
)
|
|
1105
|
-
}
|
|
1106
|
-
return items
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
onContextMenu = ({ key }) => {
|
|
1110
|
-
this[key]()
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
onXmodemSend = () => {
|
|
1114
|
-
if (this.xmodemClient) {
|
|
1115
|
-
this.xmodemClient.initiateSend()
|
|
1116
|
-
}
|
|
1117
|
-
this.term.focus()
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
onXmodemReceive = () => {
|
|
1121
|
-
if (this.xmodemClient) {
|
|
1122
|
-
this.xmodemClient.initiateReceive()
|
|
1123
|
-
}
|
|
1124
|
-
this.term.focus()
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
notifyOnData = debounce(() => {
|
|
1128
|
-
window.store.notifyTabOnData(this.props.tab.id)
|
|
1129
|
-
}, 1000)
|
|
1130
|
-
|
|
1131
|
-
parse (rawText) {
|
|
1132
|
-
let result = ''
|
|
1133
|
-
const len = rawText.length
|
|
1134
|
-
for (let i = 0; i < len; i++) {
|
|
1135
|
-
if (rawText[i] === '\b') {
|
|
1136
|
-
result = result.slice(0, -1)
|
|
1137
|
-
} else {
|
|
1138
|
-
result += rawText[i]
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
return result
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
getCmd = () => {
|
|
1145
|
-
return this.cmdAddon.getCurrentCommand()
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
getCwd = () => {
|
|
1149
|
-
// Use shell integration CWD if available
|
|
1150
|
-
if (this.cmdAddon && this.cmdAddon.hasShellIntegration()) {
|
|
1151
|
-
const cwd = this.cmdAddon.getCwd()
|
|
1152
|
-
if (cwd) {
|
|
1153
|
-
this.setCwd(cwd)
|
|
1154
|
-
return cwd
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
// Fallback: no longer needed with shell integration
|
|
1158
|
-
return ''
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
setCwd = (cwd) => {
|
|
1162
|
-
this.props.setCwd(cwd, this.state.id)
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
getCursorPosition = () => {
|
|
1166
|
-
if (!this.term) return null
|
|
1167
|
-
|
|
1168
|
-
// Get the active buffer and cursor position
|
|
1169
|
-
const buffer = this.term.buffer.active
|
|
1170
|
-
const cursorRow = buffer.cursorY
|
|
1171
|
-
const cursorCol = buffer.cursorX
|
|
1172
|
-
|
|
1173
|
-
// Get dimensions from term element
|
|
1174
|
-
const termElement = this.term.element
|
|
1175
|
-
if (!termElement) return null
|
|
1176
|
-
|
|
1177
|
-
// Get the exact position of the terminal element
|
|
1178
|
-
const termRect = termElement.getBoundingClientRect()
|
|
1179
|
-
|
|
1180
|
-
// Calculate cell dimensions
|
|
1181
|
-
const cellWidth = termRect.width / this.term.cols
|
|
1182
|
-
const cellHeight = termRect.height / this.term.rows
|
|
1183
|
-
|
|
1184
|
-
// Calculate absolute position relative to terminal element
|
|
1185
|
-
const left = Math.floor(termRect.left + (cursorCol * cellWidth))
|
|
1186
|
-
const top = Math.floor(termRect.top + ((cursorRow + 1) * cellHeight))
|
|
1187
|
-
|
|
1188
|
-
return {
|
|
1189
|
-
cellWidth,
|
|
1190
|
-
cellHeight,
|
|
1191
|
-
left,
|
|
1192
|
-
top
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
closeSuggestions = () => {
|
|
1197
|
-
refsStatic
|
|
1198
|
-
.get('terminal-suggestions')
|
|
1199
|
-
?.closeSuggestions()
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
openSuggestions = (cursorPos, data) => {
|
|
1203
|
-
refsStatic
|
|
1204
|
-
.get('terminal-suggestions')
|
|
1205
|
-
?.openSuggestions(cursorPos, data)
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
/**
|
|
1209
|
-
* Read current input directly from terminal buffer
|
|
1210
|
-
* This is more reliable than tracking character-by-character
|
|
1211
|
-
*/
|
|
1212
|
-
getCurrentInput = () => {
|
|
1213
|
-
if (!this.term) return ''
|
|
1214
|
-
|
|
1215
|
-
const buffer = this.term.buffer.active
|
|
1216
|
-
const cursorY = buffer.cursorY
|
|
1217
|
-
const cursorX = buffer.cursorX
|
|
1218
|
-
|
|
1219
|
-
// Get the current line from buffer (baseY + cursorY gives absolute position)
|
|
1220
|
-
const absoluteY = buffer.baseY + cursorY
|
|
1221
|
-
const line = buffer.getLine(absoluteY)
|
|
1222
|
-
if (!line) return ''
|
|
1223
|
-
|
|
1224
|
-
// Get text from start of line up to cursor position
|
|
1225
|
-
const lineText = line.translateToString(true, 0, cursorX)
|
|
1226
|
-
|
|
1227
|
-
// Try to extract command after prompt
|
|
1228
|
-
// Common prompt endings with trailing space
|
|
1229
|
-
const promptEndings = ['$ ', '# ', '> ', '% ', '] ', ') ']
|
|
1230
|
-
|
|
1231
|
-
let commandStart = 0
|
|
1232
|
-
for (const ending of promptEndings) {
|
|
1233
|
-
const idx = lineText.lastIndexOf(ending)
|
|
1234
|
-
if (idx !== -1 && idx + ending.length > commandStart) {
|
|
1235
|
-
commandStart = idx + ending.length
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
return lineText.slice(commandStart)
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
setCurrentInput = (value) => {
|
|
1243
|
-
this.currentInput = value
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
/**
|
|
1247
|
-
* Handle special input events for command history tracking
|
|
1248
|
-
* The actual input reading is done via getCurrentInput from buffer
|
|
1249
|
-
*/
|
|
1250
|
-
handleInputEvent = (d) => {
|
|
1251
|
-
// Handle Enter - add command to history
|
|
1252
|
-
if (d === '\r' || d === '\n') {
|
|
1253
|
-
const currentCmd = this.getCurrentInput()
|
|
1254
|
-
if (currentCmd && currentCmd.trim() && this.shouldUseManualHistory()) {
|
|
1255
|
-
window.store.addCmdHistory(currentCmd.trim())
|
|
1256
|
-
}
|
|
1257
|
-
if (currentCmd && currentCmd.trim() === 'exit') {
|
|
1258
|
-
this.userTypeExit = true
|
|
1259
|
-
}
|
|
1260
|
-
this.closeSuggestions()
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
onPasswordPromptDetected = () => {
|
|
1265
|
-
window.store.notifyTabPasswordPrompt(this.props.tab.id)
|
|
1266
|
-
if (!this.props.config.showCmdSuggestions) {
|
|
1267
|
-
return
|
|
1268
|
-
}
|
|
1269
|
-
const cursorPos = this.getCursorPosition()
|
|
1270
|
-
if (cursorPos) {
|
|
1271
|
-
refsStatic
|
|
1272
|
-
.get('terminal-suggestions')
|
|
1273
|
-
?.openPasswordSuggestions(cursorPos)
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
onPasswordPromptCancelled = () => {
|
|
1278
|
-
window.store.clearTabPasswordPrompt(this.props.tab.id)
|
|
1279
|
-
const suggestions = refsStatic.get('terminal-suggestions')
|
|
1280
|
-
if (suggestions?.state?.passwordMode) {
|
|
1281
|
-
suggestions.closeSuggestions()
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
onData = (d) => {
|
|
1286
|
-
this.handleInputEvent(d)
|
|
1287
|
-
// Skip normal suggestion logic when in password mode
|
|
1288
|
-
const suggestions = refsStatic.get('terminal-suggestions')
|
|
1289
|
-
if (suggestions?.state?.passwordMode) {
|
|
1290
|
-
if (d === '\r' || d === '\n') {
|
|
1291
|
-
this.closeSuggestions()
|
|
1292
|
-
}
|
|
1293
|
-
return
|
|
1294
|
-
}
|
|
1295
|
-
if (this.props.config.showCmdSuggestions) {
|
|
1296
|
-
if (d === '\r' || d === '\n') {
|
|
1297
|
-
this.closeSuggestions()
|
|
1298
|
-
return
|
|
1299
|
-
}
|
|
1300
|
-
// Debounce the suggestion opening to avoid expensive work
|
|
1301
|
-
// (buffer read + getBoundingClientRect + React re-render) on every keystroke
|
|
1302
|
-
this._debouncedOpenSuggestions()
|
|
1303
|
-
} else {
|
|
1304
|
-
this.closeSuggestions()
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
_debouncedOpenSuggestions = debounce(function () {
|
|
1309
|
-
const data = this.getCurrentInput()
|
|
1310
|
-
if (!data) {
|
|
1311
|
-
this.closeSuggestions()
|
|
1312
|
-
return
|
|
1313
|
-
}
|
|
1314
|
-
const cursorPos = this.getCursorPosition()
|
|
1315
|
-
this.openSuggestions(cursorPos, data)
|
|
1316
|
-
}, 80)
|
|
1317
|
-
|
|
1318
|
-
/**
|
|
1319
|
-
* Called by AttachAddonCustom after data is written to the terminal buffer.
|
|
1320
|
-
* This fires after server echo arrives, so getCurrentInput() reflects the
|
|
1321
|
-
* latest state. We trigger a debounced suggestion refresh so the dropdown
|
|
1322
|
-
* updates correctly after backspace, delete, and other edits that rely on
|
|
1323
|
-
* server-side echo to update the buffer.
|
|
1324
|
-
*/
|
|
1325
|
-
onTerminalWrite = () => {
|
|
1326
|
-
if (!this.props.config.showCmdSuggestions) {
|
|
1327
|
-
return
|
|
1328
|
-
}
|
|
1329
|
-
const suggestions = refsStatic.get('terminal-suggestions')
|
|
1330
|
-
if (suggestions?.state?.showSuggestions && !suggestions?.state?.passwordMode) {
|
|
1331
|
-
this._debouncedOpenSuggestions()
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
loadRenderer = async (term, config) => {
|
|
1336
|
-
// xterm 6.x: only the built-in DOM renderer and the WebGL addon exist
|
|
1337
|
-
// (the canvas renderer addon was removed in 6.x). 'dom' = no addon loaded
|
|
1338
|
-
// (built-in DOM renderer). Legacy 'canvas' settings fall back to DOM.
|
|
1339
|
-
if (config.rendererType === rendererTypes.webGL) {
|
|
1340
|
-
try {
|
|
1341
|
-
const WebglAddon = await loadWebglAddon()
|
|
1342
|
-
const webglAddon = new WebglAddon()
|
|
1343
|
-
this.webglAddon = webglAddon
|
|
1344
|
-
// On macOS native fullscreen the GPU/WebGL context can be lost when
|
|
1345
|
-
// the window migrates across Spaces. Without a listener xterm keeps
|
|
1346
|
-
// drawing into a dead context and every terminal goes black while the
|
|
1347
|
-
// rest of the UI stays alive. Rebuild the addon to recover.
|
|
1348
|
-
this.webglContextLossDisposable = webglAddon.onContextLoss(this.handleWebglContextLoss)
|
|
1349
|
-
term.loadAddon(webglAddon)
|
|
1350
|
-
} catch (e) {
|
|
1351
|
-
console.error('render with webgl failed, fallback to dom renderer')
|
|
1352
|
-
console.error(e)
|
|
1353
|
-
// built-in DOM renderer is used (no addon loaded)
|
|
1354
|
-
this.webglAddon = null
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
reloadWebglRenderer = (reason = 'reload') => {
|
|
1360
|
-
console.warn(`webgl renderer ${reason}, rebuilding`)
|
|
1361
|
-
try {
|
|
1362
|
-
this.webglContextLossDisposable?.dispose?.()
|
|
1363
|
-
this.webglContextLossDisposable = null
|
|
1364
|
-
} catch (e) {
|
|
1365
|
-
console.error(e)
|
|
1366
|
-
}
|
|
1367
|
-
try {
|
|
1368
|
-
this.webglAddon?.dispose?.()
|
|
1369
|
-
} catch (e) {
|
|
1370
|
-
console.error(e)
|
|
1371
|
-
}
|
|
1372
|
-
this.webglAddon = null
|
|
1373
|
-
const { term } = this
|
|
1374
|
-
const { config } = this.props
|
|
1375
|
-
return this.loadRenderer(term, config)
|
|
1376
|
-
.then(() => {
|
|
1377
|
-
term.refresh(0, term.rows - 1)
|
|
1378
|
-
})
|
|
1379
|
-
.catch(e => {
|
|
1380
|
-
console.error(`webgl renderer ${reason} failed`, e)
|
|
1381
|
-
})
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
handleWebglContextLoss = (webglAddon = this.webglAddon) => {
|
|
1385
|
-
if (this.webglRecovering || !webglAddon) {
|
|
1386
|
-
return
|
|
1387
|
-
}
|
|
1388
|
-
this.webglRecovering = true
|
|
1389
|
-
this.reloadWebglRenderer('context loss')
|
|
1390
|
-
.finally(() => {
|
|
1391
|
-
this.webglRecovering = false
|
|
1392
|
-
})
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
terminalColorQueryDisposables = []
|
|
1396
|
-
|
|
1397
|
-
disposeTerminalColorQueryHandlers = () => {
|
|
1398
|
-
this.terminalColorQueryDisposables.forEach(disposable => disposable?.dispose?.())
|
|
1399
|
-
this.terminalColorQueryDisposables.length = 0
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
getVisibleTerminalBackground = () => {
|
|
1403
|
-
const uiThemeConfig = window.store?.getUiThemeConfig?.() || {}
|
|
1404
|
-
// The store value (uiThemeConfig.main) is always immediately up-to-date
|
|
1405
|
-
// when the theme changes, because it reads directly from store.config.theme.
|
|
1406
|
-
// The CSS --main variable lags behind because UiTheme's useEffect runs
|
|
1407
|
-
// asynchronously after componentDidUpdate. So we prioritise the store
|
|
1408
|
-
// value, and only fall back to CSS (for custom-CSS edge cases) or the
|
|
1409
|
-
// terminal theme background (last resort).
|
|
1410
|
-
const root = document.documentElement
|
|
1411
|
-
const cssMain = root && window.getComputedStyle
|
|
1412
|
-
? window.getComputedStyle(root).getPropertyValue('--main').trim()
|
|
1413
|
-
: ''
|
|
1414
|
-
return uiThemeConfig.main || cssMain || this.props.themeConfig.background
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
getVisibleTerminalForeground = () => {
|
|
1418
|
-
const uiThemeConfig = window.store?.getUiThemeConfig?.() || {}
|
|
1419
|
-
return uiThemeConfig.text
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
registerTerminalColorQueryHandlers = (term, themeConfig = {}) => {
|
|
1423
|
-
this.disposeTerminalColorQueryHandlers()
|
|
1424
|
-
if (!term?.parser?.registerOscHandler) {
|
|
1425
|
-
return
|
|
1426
|
-
}
|
|
1427
|
-
const background = this.getVisibleTerminalBackground()
|
|
1428
|
-
const foregroundFallback = this.getVisibleTerminalForeground()
|
|
1429
|
-
this.terminalColorQueryDisposables.push(
|
|
1430
|
-
term.parser.registerOscHandler(10, data => {
|
|
1431
|
-
return handleTerminalColorQuery(term, 10, themeConfig.foreground, foregroundFallback, data)
|
|
1432
|
-
}),
|
|
1433
|
-
term.parser.registerOscHandler(11, data => {
|
|
1434
|
-
return handleTerminalColorQuery(term, 11, background, themeConfig.background, data)
|
|
1435
|
-
})
|
|
1436
|
-
)
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
getRendererThemeConfig = (themeConfig = this.props.themeConfig) => {
|
|
1440
|
-
return createRendererThemeConfig(
|
|
1441
|
-
deepCopy(themeConfig),
|
|
1442
|
-
this.props.config.rendererType,
|
|
1443
|
-
this.getVisibleTerminalBackground()
|
|
1444
|
-
)
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
/**
|
|
1448
|
-
* Apply the current renderer theme to the terminal and trigger a repaint.
|
|
1449
|
-
* When `deferred` is true (WebGL mode), a second repaint is scheduled on
|
|
1450
|
-
* the next animation frame so the theme picks up CSS --main changes that
|
|
1451
|
-
* UiTheme's useEffect applies asynchronously after componentDidUpdate.
|
|
1452
|
-
* The optional `term` parameter is used during initTerminal, where
|
|
1453
|
-
* this.term hasn't been assigned yet.
|
|
1454
|
-
*/
|
|
1455
|
-
applyTerminalTheme = (deferred = false, term = this.term) => {
|
|
1456
|
-
if (!term || this.onClose) {
|
|
1457
|
-
return
|
|
1458
|
-
}
|
|
1459
|
-
term.options.theme = this.getRendererThemeConfig(this.props.themeConfig)
|
|
1460
|
-
this.fixSelectionColors(term)
|
|
1461
|
-
term.refresh(0, term.rows - 1)
|
|
1462
|
-
if (deferred && this.props.config.rendererType === rendererTypes.webGL) {
|
|
1463
|
-
window.cancelAnimationFrame(this.timers.themeRaf)
|
|
1464
|
-
this.timers.themeRaf = window.requestAnimationFrame(() => {
|
|
1465
|
-
if (!this.term || this.onClose) {
|
|
1466
|
-
return
|
|
1467
|
-
}
|
|
1468
|
-
this.term.options.theme = this.getRendererThemeConfig(this.props.themeConfig)
|
|
1469
|
-
this.fixSelectionColors(this.term)
|
|
1470
|
-
this.term.refresh(0, this.term.rows - 1)
|
|
1471
|
-
})
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
/**
|
|
1476
|
-
* xterm computes its DOM selection colour as blend(theme.background,
|
|
1477
|
-
* selectionBackground). The terminal background is forced transparent
|
|
1478
|
-
* (rgba(0,0,0,0)) so electerm's own CSS background / image shows through,
|
|
1479
|
-
* which makes the blend run over transparent-black. Recompute the selection
|
|
1480
|
-
* colours over the real visible background, from the *configured* theme
|
|
1481
|
-
* colours — xterm's internal selectionBackgroundTransparent forces opaque
|
|
1482
|
-
* colours down to 0.3 alpha (xterm#2737), which would make every selection
|
|
1483
|
-
* translucent even when the user configured an opaque one.
|
|
1484
|
-
*/
|
|
1485
|
-
fixSelectionColors = (term) => {
|
|
1486
|
-
const themeConfig = this.props.themeConfig || {}
|
|
1487
|
-
const themeService = term?._core?._themeService
|
|
1488
|
-
if (!themeService?.modifyColors) {
|
|
1489
|
-
return
|
|
1490
|
-
}
|
|
1491
|
-
const visibleBackground = this.getVisibleTerminalBackground()
|
|
1492
|
-
// xterm's DEFAULT_SELECTION, used when the theme omits a selection color
|
|
1493
|
-
const selectionFallback = 'rgba(255, 255, 255, 0.3)'
|
|
1494
|
-
const inactiveFallback = themeConfig.selectionBackground || selectionFallback
|
|
1495
|
-
themeService.modifyColors((c) => {
|
|
1496
|
-
const active = blendSelectionOverBackground(
|
|
1497
|
-
visibleBackground,
|
|
1498
|
-
themeConfig.selectionBackground || selectionFallback
|
|
1499
|
-
)
|
|
1500
|
-
if (active) {
|
|
1501
|
-
c.selectionBackgroundOpaque = active
|
|
1502
|
-
}
|
|
1503
|
-
const inactive = blendSelectionOverBackground(
|
|
1504
|
-
visibleBackground,
|
|
1505
|
-
themeConfig.selectionInactiveBackground || inactiveFallback
|
|
1506
|
-
)
|
|
1507
|
-
if (inactive) {
|
|
1508
|
-
c.selectionInactiveBackgroundOpaque = inactive
|
|
1509
|
-
}
|
|
1510
|
-
})
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
initTerminal = async () => {
|
|
1514
|
-
const { themeConfig, tab = {}, config = {} } = this.props
|
|
1515
|
-
const tc = this.getRendererThemeConfig(themeConfig)
|
|
1516
|
-
const Terminal = await loadTerminal()
|
|
1517
|
-
const term = new Terminal({
|
|
1518
|
-
allowProposedApi: true,
|
|
1519
|
-
scrollback: config.scrollback,
|
|
1520
|
-
rightClickSelectsWord: config.rightClickSelectsWord || false,
|
|
1521
|
-
fontFamily: tab.fontFamily || config.fontFamily,
|
|
1522
|
-
theme: tc,
|
|
1523
|
-
allowTransparency: true,
|
|
1524
|
-
wordSeparator: config.terminalWordSeparator,
|
|
1525
|
-
cursorStyle: config.cursorStyle,
|
|
1526
|
-
cursorBlink: config.cursorBlink,
|
|
1527
|
-
fontSize: tab.fontSize || config.fontSize,
|
|
1528
|
-
screenReaderMode: config.screenReaderMode
|
|
1529
|
-
})
|
|
1530
|
-
|
|
1531
|
-
term.parent = this
|
|
1532
|
-
term.onSelectionChange(this.onSelection)
|
|
1533
|
-
term.open(this.domRef.current, true)
|
|
1534
|
-
this.registerTerminalColorQueryHandlers(term, themeConfig)
|
|
1535
|
-
await this.loadRenderer(term, config)
|
|
1536
|
-
this.fixSelectionColors(term)
|
|
1537
|
-
// Re-apply the theme after the renderer is loaded. The Terminal was
|
|
1538
|
-
// constructed before UiTheme's useEffect ran, so the initial theme
|
|
1539
|
-
// may have a stale background. Pass `term` directly because this.term
|
|
1540
|
-
// is not assigned yet. Use deferred=true so a second repaint picks up
|
|
1541
|
-
// any CSS --main changes applied by UiTheme's useEffect.
|
|
1542
|
-
if (config.rendererType === rendererTypes.webGL) {
|
|
1543
|
-
this.applyTerminalTheme(true, term)
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
const FitAddon = await loadFitAddon()
|
|
1547
|
-
this.fitAddon = new FitAddon()
|
|
1548
|
-
this.cmdAddon = new CommandTrackerAddon()
|
|
1549
|
-
this.cmdAddon.onCommandExecuted((cmd) => {
|
|
1550
|
-
if (cmd && cmd.trim()) {
|
|
1551
|
-
window.store.addCmdHistory(cmd.trim())
|
|
1552
|
-
}
|
|
1553
|
-
})
|
|
1554
|
-
this.cmdAddon.onCwdChanged((cwd) => {
|
|
1555
|
-
this.setCwd(cwd)
|
|
1556
|
-
})
|
|
1557
|
-
const SearchAddon = await loadSearchAddon()
|
|
1558
|
-
this.searchAddon = new SearchAddon()
|
|
1559
|
-
const LigaturesAddon = await loadLigaturesAddon()
|
|
1560
|
-
const ligtureAddon = new LigaturesAddon()
|
|
1561
|
-
this.searchAddon.onDidChangeResults(this.onSearchResultsChange)
|
|
1562
|
-
const Unicode11Addon = await loadUnicode11Addon()
|
|
1563
|
-
const unicode11Addon = new Unicode11Addon()
|
|
1564
|
-
term.loadAddon(unicode11Addon)
|
|
1565
|
-
term.loadAddon(ligtureAddon)
|
|
1566
|
-
term.unicode.activeVersion = '11'
|
|
1567
|
-
term.loadAddon(this.fitAddon)
|
|
1568
|
-
term.loadAddon(this.searchAddon)
|
|
1569
|
-
term.loadAddon(this.cmdAddon)
|
|
1570
|
-
this.osc52Addon = new Osc52Addon()
|
|
1571
|
-
term.loadAddon(this.osc52Addon)
|
|
1572
|
-
if (tab.enableTerminalImage) {
|
|
1573
|
-
const ImageAddon = await loadImageAddon()
|
|
1574
|
-
this.imageAddon = new ImageAddon({
|
|
1575
|
-
pixelLimit: 33554432
|
|
1576
|
-
})
|
|
1577
|
-
term.loadAddon(this.imageAddon)
|
|
1578
|
-
}
|
|
1579
|
-
term.onData(this.onData)
|
|
1580
|
-
this.term = term
|
|
1581
|
-
term.onSelectionChange(this.onSelectionChange)
|
|
1582
|
-
term.attachCustomKeyEventHandler(this.handleKeyboardEvent.bind(this))
|
|
1583
|
-
// 容器不可见(隐藏标签 display:none)时不 fit,避免算出 0 列把 shell
|
|
1584
|
-
// 提示符逐字符错误换行;待标签激活可见后由 fitAndRefresh 重新适配。
|
|
1585
|
-
if (this.isElementVisible()) {
|
|
1586
|
-
this.fitAddon.fit()
|
|
1587
|
-
}
|
|
1588
|
-
await this.remoteInit(term)
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
onSelectionChange = () => {
|
|
1592
|
-
const hasSelection = this.term.hasSelection()
|
|
1593
|
-
const txt = hasSelection ? this.term.getSelection().trim() : ''
|
|
1594
|
-
this.setState({ hasSelection })
|
|
1595
|
-
refsStatic.get('unix-timestamp-tooltip')?.onSelection(txt)
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
// setActive = () => {
|
|
1599
|
-
// const name = `activeTabId${this.props.batch}`
|
|
1600
|
-
// const tabId = this.props.tab.id
|
|
1601
|
-
// window.store.storeAssign({
|
|
1602
|
-
// activeTabId: tabId,
|
|
1603
|
-
// [name]: tabId
|
|
1604
|
-
// })
|
|
1605
|
-
// }
|
|
1606
|
-
|
|
1607
|
-
runInitScript = async () => {
|
|
1608
|
-
window.store.triggerResize()
|
|
1609
|
-
const {
|
|
1610
|
-
startDirectory,
|
|
1611
|
-
runScripts
|
|
1612
|
-
} = this.props.tab
|
|
1613
|
-
|
|
1614
|
-
const scripts = runScripts ? [...runScripts] : []
|
|
1615
|
-
const startFolder = startDirectory || window.initFolder
|
|
1616
|
-
if (startFolder) {
|
|
1617
|
-
scripts.unshift({ script: `cd "${startFolder}"`, delay: 0 })
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
// Create unified execution queue
|
|
1621
|
-
this.executionQueue = []
|
|
1622
|
-
|
|
1623
|
-
// Add shell integration injection to queue if needed
|
|
1624
|
-
if (this.canInjectShellIntegration()) {
|
|
1625
|
-
this.executionQueue.push({
|
|
1626
|
-
type: 'shell_integration',
|
|
1627
|
-
execute: async () => {
|
|
1628
|
-
await this.injectShellIntegration()
|
|
1629
|
-
}
|
|
1630
|
-
})
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
// Add delayed scripts to queue
|
|
1634
|
-
scripts.forEach(script => {
|
|
1635
|
-
this.executionQueue.push({
|
|
1636
|
-
type: 'delayed_script',
|
|
1637
|
-
script: script.script,
|
|
1638
|
-
delay: script.delay || 0,
|
|
1639
|
-
execute: () => {
|
|
1640
|
-
if (script.script) {
|
|
1641
|
-
this.attachAddon._sendData(script.script + '\r')
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
})
|
|
1645
|
-
})
|
|
1646
|
-
|
|
1647
|
-
this.processExecutionQueue()
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
shouldUseManualHistory = () => {
|
|
1651
|
-
return !this.cmdAddon || !this.cmdAddon.hasShellIntegration()
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
canInjectShellIntegration = () => {
|
|
1655
|
-
const { config } = this.props
|
|
1656
|
-
const canInject = (config.showCmdSuggestions || this.props.sftpPathFollowSsh) &&
|
|
1657
|
-
(
|
|
1658
|
-
this.isSsh() ||
|
|
1659
|
-
(this.isLocal() && !isWin)
|
|
1660
|
-
)
|
|
1661
|
-
return canInject
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
isSsh = () => {
|
|
1665
|
-
const { host, type } = this.props.tab
|
|
1666
|
-
return host && (type === 'ssh' || type === undefined)
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
isLocal = () => {
|
|
1670
|
-
const { host, type } = this.props.tab
|
|
1671
|
-
return !host &&
|
|
1672
|
-
(type === 'local' || type === undefined)
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
/**
|
|
1676
|
-
* Process the unified execution queue one item at a time
|
|
1677
|
-
*/
|
|
1678
|
-
processExecutionQueue = async () => {
|
|
1679
|
-
if (!this.executionQueue || this.executionQueue.length === 0) {
|
|
1680
|
-
return
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
const item = this.executionQueue.shift()
|
|
1684
|
-
|
|
1685
|
-
try {
|
|
1686
|
-
if (item.type === 'shell_integration') {
|
|
1687
|
-
await item.execute()
|
|
1688
|
-
} else if (item.type === 'delayed_script') {
|
|
1689
|
-
item.execute()
|
|
1690
|
-
// Wait for the specified delay before processing next item
|
|
1691
|
-
if (item.delay > 0) {
|
|
1692
|
-
await new Promise(resolve => {
|
|
1693
|
-
this.timers.timerDelay = setTimeout(resolve, item.delay)
|
|
1694
|
-
})
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
} catch (error) {
|
|
1698
|
-
console.error('[Shell Integration] Error processing queue item:', item.type, error)
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
// Process next item
|
|
1702
|
-
this.processExecutionQueue()
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
/**
|
|
1706
|
-
* Inject shell integration commands from client-side
|
|
1707
|
-
* This replaces the server-side source xxx.xxx approach
|
|
1708
|
-
* Uses output suppression to hide the injection command
|
|
1709
|
-
* Returns a promise that resolves when injection is complete
|
|
1710
|
-
*/
|
|
1711
|
-
injectShellIntegration = async () => {
|
|
1712
|
-
if (this.shellInjected) {
|
|
1713
|
-
return Promise.resolve()
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
let shellType
|
|
1717
|
-
if (this.isLocal()) {
|
|
1718
|
-
const { config } = this.props
|
|
1719
|
-
const localShell = isMac ? config.execMac : config.execLinux
|
|
1720
|
-
shellType = detectShellType(localShell)
|
|
1721
|
-
} else if (this.isSsh()) {
|
|
1722
|
-
shellType = await detectRemoteShell(this.pid)
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
this.shellType = shellType
|
|
1726
|
-
|
|
1727
|
-
// Don't inject for sh type shells unless sftpPathFollowSsh is true
|
|
1728
|
-
if (shellType === 'sh' && !this.props.sftpPathFollowSsh) {
|
|
1729
|
-
return Promise.resolve()
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
const integrationCmd = getShellIntegrationCommand(shellType)
|
|
1733
|
-
|
|
1734
|
-
return new Promise((resolve) => {
|
|
1735
|
-
// Wait for initial data (prompt/banner) to arrive before injecting
|
|
1736
|
-
this.attachAddon.onInitialData(() => {
|
|
1737
|
-
if (this.attachAddon) {
|
|
1738
|
-
// Start suppressing output before sending the integration command
|
|
1739
|
-
// This hides the command and its output until OSC 633 is detected
|
|
1740
|
-
const suppressionTimeout = this.isSsh() ? 5000 : 3000
|
|
1741
|
-
// Pass callback to resolve the promise after suppression ends
|
|
1742
|
-
this.attachAddon.startOutputSuppression(suppressionTimeout, () => {
|
|
1743
|
-
this.shellInjected = true
|
|
1744
|
-
resolve()
|
|
1745
|
-
})
|
|
1746
|
-
this.attachAddon._sendData(integrationCmd)
|
|
1747
|
-
} else {
|
|
1748
|
-
resolve()
|
|
1749
|
-
}
|
|
1750
|
-
})
|
|
1751
|
-
})
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
setStatus = status => {
|
|
1755
|
-
const id = this.props.tab?.id
|
|
1756
|
-
this.props.editTab(id, {
|
|
1757
|
-
status
|
|
1758
|
-
})
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
openNormalBuffer = () => {
|
|
1762
|
-
const normal = this.term.buffer.normal
|
|
1763
|
-
const len = normal.length
|
|
1764
|
-
const lines = new Array(len).fill('').map((x, i) => {
|
|
1765
|
-
return normal.getLine(i).translateToString(false)
|
|
1766
|
-
})
|
|
1767
|
-
this.setState({
|
|
1768
|
-
lines
|
|
1769
|
-
})
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
closeNormalBuffer = () => {
|
|
1773
|
-
this.setState({
|
|
1774
|
-
lines: []
|
|
1775
|
-
})
|
|
1776
|
-
this.term.focus()
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
onBufferChange = buf => {
|
|
1780
|
-
this.bufferMode = buf.type
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
buildWsUrl = (port) => {
|
|
1784
|
-
const { host, tokenElecterm } = this.props.config
|
|
1785
|
-
const { id } = this.props.tab
|
|
1786
|
-
if (window.et.buildWsUrl) {
|
|
1787
|
-
return window.et.buildWsUrl(
|
|
1788
|
-
host,
|
|
1789
|
-
port,
|
|
1790
|
-
tokenElecterm,
|
|
1791
|
-
id
|
|
1792
|
-
)
|
|
1793
|
-
}
|
|
1794
|
-
return `ws://${host}:${port}/terminals/${id}?token=${tokenElecterm}`
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
remoteInit = async (term = this.term) => {
|
|
1798
|
-
this.setState({
|
|
1799
|
-
loading: true,
|
|
1800
|
-
terminalError: null
|
|
1801
|
-
})
|
|
1802
|
-
const { cols, rows } = term
|
|
1803
|
-
const { config } = this.props
|
|
1804
|
-
const {
|
|
1805
|
-
keywords = []
|
|
1806
|
-
} = config
|
|
1807
|
-
const { logName } = this.props
|
|
1808
|
-
const tab = window.store.applyProfileToTabs(deepCopy(this.props.tab || {}))
|
|
1809
|
-
const {
|
|
1810
|
-
srcId, from = 'bookmarks',
|
|
1811
|
-
type,
|
|
1812
|
-
term: terminalType,
|
|
1813
|
-
displayRaw,
|
|
1814
|
-
id
|
|
1815
|
-
} = tab
|
|
1816
|
-
const { savePassword } = this.state
|
|
1817
|
-
const termType = type
|
|
1818
|
-
const extra = this.props.sessionOptions
|
|
1819
|
-
// Determine if this is a local terminal (no host)
|
|
1820
|
-
const isLocalType = !tab.host
|
|
1821
|
-
// Build exec settings: only for local type, prefer tab settings over config
|
|
1822
|
-
let execOpts = {}
|
|
1823
|
-
let execPropName = 'execLinux'
|
|
1824
|
-
if (isWin) {
|
|
1825
|
-
execPropName = 'execWindows'
|
|
1826
|
-
} else if (isMac) {
|
|
1827
|
-
execPropName = 'execMac'
|
|
1828
|
-
}
|
|
1829
|
-
if (isLocalType) {
|
|
1830
|
-
// Check flat properties on tab first (bookmark data), then fall back to config
|
|
1831
|
-
if (tab[execPropName]) {
|
|
1832
|
-
// Use bookmark's exec setting directly
|
|
1833
|
-
execOpts = {
|
|
1834
|
-
[execPropName]: tab[execPropName],
|
|
1835
|
-
[`${execPropName}Args`]: tab[`${execPropName}Args`] || []
|
|
1836
|
-
}
|
|
1837
|
-
} else if (config[execPropName]) {
|
|
1838
|
-
// Use global config exec settings
|
|
1839
|
-
execOpts = {
|
|
1840
|
-
[execPropName]: config[execPropName],
|
|
1841
|
-
[`${execPropName}Args`]: config[`${execPropName}Args`] || []
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
const keepaliveInterval = tab.keepaliveInterval || config.keepaliveInterval
|
|
1846
|
-
const opts = clone({
|
|
1847
|
-
cols,
|
|
1848
|
-
rows,
|
|
1849
|
-
term: terminalType || config.terminalType,
|
|
1850
|
-
saveTerminalLogToFile: config.saveTerminalLogToFile,
|
|
1851
|
-
...tab,
|
|
1852
|
-
...extra,
|
|
1853
|
-
...execOpts,
|
|
1854
|
-
logName,
|
|
1855
|
-
sessionLogPath: this.state.logPath,
|
|
1856
|
-
...pick(config, [
|
|
1857
|
-
'addTimeStampToTermLog',
|
|
1858
|
-
'keepaliveCountMax',
|
|
1859
|
-
'keyword2FA',
|
|
1860
|
-
'debug'
|
|
1861
|
-
]),
|
|
1862
|
-
keepaliveInterval,
|
|
1863
|
-
tabId: id,
|
|
1864
|
-
uid: id,
|
|
1865
|
-
srcTabId: tab.id,
|
|
1866
|
-
termType,
|
|
1867
|
-
readyTimeout: config.sshReadyTimeout,
|
|
1868
|
-
proxy: getProxy(tab, config),
|
|
1869
|
-
type: tab.host
|
|
1870
|
-
? typeMap.remote
|
|
1871
|
-
: typeMap.local
|
|
1872
|
-
})
|
|
1873
|
-
const isAutoReconnect = !!(tab.autoReConnect && this.props.config.autoReconnectTerminal)
|
|
1874
|
-
const r = await createTerm(opts)
|
|
1875
|
-
.catch(err => {
|
|
1876
|
-
if (!isAutoReconnect) {
|
|
1877
|
-
const text = err.message
|
|
1878
|
-
this.handleError({ message: text, from, srcId })
|
|
1879
|
-
}
|
|
1880
|
-
})
|
|
1881
|
-
// Guard: component was unmounted while createTerm was pending.
|
|
1882
|
-
// The child process is already running; connect briefly to trigger its cleanup.
|
|
1883
|
-
if (this.onClose) {
|
|
1884
|
-
if (r && r.port) {
|
|
1885
|
-
try {
|
|
1886
|
-
const tmpSock = new WebSocket(this.buildWsUrl(r.port))
|
|
1887
|
-
tmpSock.onopen = () => tmpSock.close()
|
|
1888
|
-
} catch (_e) {}
|
|
1889
|
-
}
|
|
1890
|
-
return
|
|
1891
|
-
}
|
|
1892
|
-
if (typeof r === 'string' && r.includes('fail')) {
|
|
1893
|
-
return this.promote()
|
|
1894
|
-
}
|
|
1895
|
-
if (savePassword) {
|
|
1896
|
-
window.store.editItem(srcId, extra, from)
|
|
1897
|
-
}
|
|
1898
|
-
this.setState({
|
|
1899
|
-
loading: false
|
|
1900
|
-
})
|
|
1901
|
-
if (!r) {
|
|
1902
|
-
if (isAutoReconnect) {
|
|
1903
|
-
this.scheduleAutoReconnect(3000)
|
|
1904
|
-
return
|
|
1905
|
-
}
|
|
1906
|
-
this.setStatus(statusMap.error)
|
|
1907
|
-
return
|
|
1908
|
-
}
|
|
1909
|
-
this.port = r.port
|
|
1910
|
-
this.setStatus(statusMap.success)
|
|
1911
|
-
refs.get('sftp-' + id)?.initData(id, r.port)
|
|
1912
|
-
term.pid = id
|
|
1913
|
-
this.pid = id
|
|
1914
|
-
const wsUrl = this.buildWsUrl(r.port)
|
|
1915
|
-
const socket = new WebSocket(wsUrl)
|
|
1916
|
-
socket.onclose = this.oncloseSocket
|
|
1917
|
-
socket.onerror = this.onerrorSocket
|
|
1918
|
-
this.socket = socket
|
|
1919
|
-
this.initSocketEvents()
|
|
1920
|
-
this.term = term
|
|
1921
|
-
socket.onopen = async () => {
|
|
1922
|
-
await this.initAttachAddon()
|
|
1923
|
-
this.runInitScript()
|
|
1924
|
-
}
|
|
1925
|
-
// term.onRrefresh(this.onRefresh)
|
|
1926
|
-
term.onResize(this.onResizeTerminal)
|
|
1927
|
-
// xterm 6.x exposes buffer change as a public event (IBufferNamespace.onBufferChange).
|
|
1928
|
-
// Previously this reached into the private _onBufferChange._listeners array.
|
|
1929
|
-
term.buffer.onBufferChange(this.onBufferChange)
|
|
1930
|
-
const WebLinksAddon = await loadWebLinksAddon()
|
|
1931
|
-
term.loadAddon(new WebLinksAddon(this.webLinkHandler))
|
|
1932
|
-
term.focus()
|
|
1933
|
-
this.zmodemClient = new ZmodemClient(this)
|
|
1934
|
-
this.zmodemClient.init(socket)
|
|
1935
|
-
this.trzszClient = new TrzszClient(this)
|
|
1936
|
-
this.trzszClient.init(socket)
|
|
1937
|
-
this.xmodemClient = new XmodemClient(this)
|
|
1938
|
-
this.xmodemClient.init(socket)
|
|
1939
|
-
// 仅在可见时 fit,隐藏标签跳过,避免 0 列损坏提示符。
|
|
1940
|
-
if (this.isElementVisible()) {
|
|
1941
|
-
this.fitAddon.fit()
|
|
1942
|
-
}
|
|
1943
|
-
// Force-send the current size now that `this.pid` is finally valid.
|
|
1944
|
-
// A resize may already have fired earlier (e.g. from the tab-becomes-
|
|
1945
|
-
// visible fit in componentDidUpdate, which can run before createTerm()
|
|
1946
|
-
// resolves) while this.pid was still undefined - the backend silently
|
|
1947
|
-
// drops resize calls for an unknown pid. If that earlier call already
|
|
1948
|
-
// updated term.cols/rows locally, xterm's resize() no-ops on the next
|
|
1949
|
-
// fit() (dims unchanged) and never fires onResize again, so the
|
|
1950
|
-
// backend pty would otherwise be stuck at its initial (possibly wrong)
|
|
1951
|
-
// size forever. Sending explicitly here, keyed off the real pid,
|
|
1952
|
-
// guarantees the remote pty/ssh channel is told the true current size.
|
|
1953
|
-
resizeTerm(this.pid, term.cols, term.rows)
|
|
1954
|
-
term.displayRaw = displayRaw
|
|
1955
|
-
term.loadAddon(
|
|
1956
|
-
new KeywordHighlighterAddon(keywords)
|
|
1957
|
-
)
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
handleError = ({ message: errorMessage, from, srcId }) => {
|
|
1961
|
-
this.setState({
|
|
1962
|
-
terminalError: {
|
|
1963
|
-
message: errorMessage || 'Failed to create terminal session',
|
|
1964
|
-
from,
|
|
1965
|
-
srcId
|
|
1966
|
-
}
|
|
1967
|
-
})
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
handleEditBookmarkFromError = () => {
|
|
1971
|
-
const error = this.state.terminalError
|
|
1972
|
-
if (!error || error.from !== 'bookmarks' || !error.srcId) {
|
|
1973
|
-
return
|
|
1974
|
-
}
|
|
1975
|
-
const item = window.store.bookmarksMap?.get(error.srcId) ||
|
|
1976
|
-
window.store.bookmarks?.find(d => d.id === error.srcId)
|
|
1977
|
-
if (!item) {
|
|
1978
|
-
return
|
|
1979
|
-
}
|
|
1980
|
-
window.store.openBookmarkEdit(item)
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
initSocketEvents = () => {
|
|
1984
|
-
const originalSend = this.socket.send
|
|
1985
|
-
this.socket.send = (data) => {
|
|
1986
|
-
// Call original send first
|
|
1987
|
-
originalSend.call(this.socket, data)
|
|
1988
|
-
|
|
1989
|
-
// Broadcast to other terminals
|
|
1990
|
-
this.broadcastSocketData(data)
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
canReceiveBroadcast = (termRef) => {
|
|
1995
|
-
return (
|
|
1996
|
-
termRef.socket &&
|
|
1997
|
-
termRef.props?.tab.pane === paneMap.terminal
|
|
1998
|
-
)
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
broadcastSocketData = (data) => {
|
|
2002
|
-
if (!this.isActiveTerminal() || !this.props.broadcastInput) {
|
|
2003
|
-
return
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
window.refs.forEach((termRef, refId) => {
|
|
2007
|
-
if (
|
|
2008
|
-
refId !== this.id &&
|
|
2009
|
-
refId.startsWith('term-') &&
|
|
2010
|
-
this.canReceiveBroadcast(termRef)
|
|
2011
|
-
) {
|
|
2012
|
-
termRef.socket.send(data)
|
|
2013
|
-
}
|
|
2014
|
-
})
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
// 判断终端挂载容器当前是否真实可见(非 display:none)。
|
|
2018
|
-
// 隐藏标签的 clientWidth/clientHeight 为 0,此时不应 fit,否则会算出 0 列。
|
|
2019
|
-
isElementVisible = () => {
|
|
2020
|
-
const el = this.domRef.current
|
|
2021
|
-
if (!el) {
|
|
2022
|
-
return false
|
|
2023
|
-
}
|
|
2024
|
-
return el.clientWidth > 0 && el.clientHeight > 0
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
// 重新适配终端尺寸并强制重绘可见区域。
|
|
2028
|
-
// 仅当容器真正可见时才 fit:隐藏标签(display:none)下 clientWidth 为 0,
|
|
2029
|
-
// 若在此时 fit 会算出 0/极小列数,使 shell 提示符被逐字符错误换行,
|
|
2030
|
-
// 出现快速连续打开多个连接时的虚假/重复提示符。因此隐藏态跳过 fit,
|
|
2031
|
-
// 保留 xterm 默认 80x24,待标签激活可见后再 fit 重绘,从根上避免换行损坏。
|
|
2032
|
-
fitAndRefresh = () => {
|
|
2033
|
-
if (!this.term || !this.fitAddon || this.onClose) {
|
|
2034
|
-
return
|
|
2035
|
-
}
|
|
2036
|
-
if (!this.isElementVisible()) {
|
|
2037
|
-
return
|
|
2038
|
-
}
|
|
2039
|
-
try {
|
|
2040
|
-
this.fitAddon.fit()
|
|
2041
|
-
this.term.refresh(0, this.term.rows - 1)
|
|
2042
|
-
} catch (e) {
|
|
2043
|
-
console.info('resize failed', e)
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
onResize = throttle(() => {
|
|
2048
|
-
this.fitAndRefresh()
|
|
2049
|
-
}, 200)
|
|
2050
|
-
|
|
2051
|
-
onerrorSocket = err => {
|
|
2052
|
-
console.error('onerrorSocket', err)
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
oncloseSocket = () => {
|
|
2056
|
-
if (this.onClose || this.props.tab.enableSsh === false) {
|
|
2057
|
-
return
|
|
2058
|
-
}
|
|
2059
|
-
this.setStatus(
|
|
2060
|
-
statusMap.error
|
|
2061
|
-
)
|
|
2062
|
-
if (this.userTypeExit) {
|
|
2063
|
-
return this.props.delTab(this.props.tab.id)
|
|
2064
|
-
}
|
|
2065
|
-
const { autoReconnectTerminal } = this.props.config
|
|
2066
|
-
if (autoReconnectTerminal) {
|
|
2067
|
-
this.scheduleAutoReconnect(3000)
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
|
|
2071
|
-
scheduleAutoReconnect = (delay = 3000) => {
|
|
2072
|
-
clearTimeout(this.timers.reconnectTimer)
|
|
2073
|
-
clearInterval(this.timers.reconnectCountdown)
|
|
2074
|
-
const seconds = Math.round(delay / 1000)
|
|
2075
|
-
this.setState({ reconnectCountdown: seconds })
|
|
2076
|
-
let remaining = seconds
|
|
2077
|
-
this.timers.reconnectCountdown = setInterval(() => {
|
|
2078
|
-
remaining -= 1
|
|
2079
|
-
if (remaining <= 0) {
|
|
2080
|
-
clearInterval(this.timers.reconnectCountdown)
|
|
2081
|
-
this.timers.reconnectCountdown = null
|
|
2082
|
-
}
|
|
2083
|
-
this.setState({ reconnectCountdown: remaining <= 0 ? null : remaining })
|
|
2084
|
-
}, 1000)
|
|
2085
|
-
this.timers.reconnectTimer = setTimeout(() => {
|
|
2086
|
-
clearInterval(this.timers.reconnectCountdown)
|
|
2087
|
-
this.timers.reconnectCountdown = null
|
|
2088
|
-
this.setState({ reconnectCountdown: null })
|
|
2089
|
-
if (this.onClose || !this.props.config.autoReconnectTerminal) {
|
|
2090
|
-
return
|
|
2091
|
-
}
|
|
2092
|
-
const reconnectCount = (this.props.tab.autoReConnect || 0) + 1
|
|
2093
|
-
this.props.reloadTab({ ...this.props.tab, autoReConnect: reconnectCount })
|
|
2094
|
-
}, delay)
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
handleCancelAutoReconnect = () => {
|
|
2098
|
-
clearTimeout(this.timers.reconnectTimer)
|
|
2099
|
-
clearInterval(this.timers.reconnectCountdown)
|
|
2100
|
-
this.timers.reconnectTimer = null
|
|
2101
|
-
this.timers.reconnectCountdown = null
|
|
2102
|
-
this.setState({ reconnectCountdown: null })
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
batchInput = (cmd) => {
|
|
2106
|
-
this.attachAddon._sendData(cmd + '\r')
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
onResizeTerminal = size => {
|
|
2110
|
-
const { cols, rows } = size
|
|
2111
|
-
resizeTerm(this.pid, cols, rows)
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
handleCancel = () => {
|
|
2115
|
-
const { id } = this.props.tab
|
|
2116
|
-
this.props.delTab(id)
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
/**
|
|
2120
|
-
* Manually triggered from the "exit gracefully" control in
|
|
2121
|
-
* session-control.jsx (serial tabs only). Writes the configured key
|
|
2122
|
-
* sequence (default \x01ky = Ctrl+A, k, y to kill a GNU screen window) to
|
|
2123
|
-
* the socket, waits a bit so the remote end (e.g. a Bluetooth console
|
|
2124
|
-
* adapter) can release cleanly, then closes the tab.
|
|
2125
|
-
*/
|
|
2126
|
-
exitGracefully = async () => {
|
|
2127
|
-
const { tab } = this.props
|
|
2128
|
-
if (tab.type !== terminalSerialType) {
|
|
2129
|
-
return
|
|
2130
|
-
}
|
|
2131
|
-
if (!this.onClose && this.attachAddon?._sendData) {
|
|
2132
|
-
try {
|
|
2133
|
-
const data = expandCloseSequence(tab.closeSequence || '\\x01ky')
|
|
2134
|
-
if (data) {
|
|
2135
|
-
this.attachAddon._sendData(data)
|
|
2136
|
-
}
|
|
2137
|
-
} catch (err) {
|
|
2138
|
-
console.error('send close sequence failed', err)
|
|
2139
|
-
}
|
|
2140
|
-
const delay = Number(tab.closeSequenceDelay)
|
|
2141
|
-
await new Promise(resolve => setTimeout(resolve, Number.isFinite(delay) && delay >= 0 ? delay : 500))
|
|
2142
|
-
}
|
|
2143
|
-
this.props.delTab(tab.id)
|
|
2144
|
-
}
|
|
2145
|
-
|
|
2146
|
-
handleShowInfo = () => {
|
|
2147
|
-
const { logName, tab } = this.props
|
|
2148
|
-
const infoProps = {
|
|
2149
|
-
logName,
|
|
2150
|
-
id: tab.id,
|
|
2151
|
-
pid: tab.id,
|
|
2152
|
-
isRemote: this.isRemote(),
|
|
2153
|
-
isActive: this.isActiveTerminal()
|
|
2154
|
-
}
|
|
2155
|
-
Object.assign(window.store.terminalInfoProps, infoProps)
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
renderResetFontSizeButton = () => {
|
|
2159
|
-
if (!this.state.fontSizeChanged) {
|
|
2160
|
-
return null
|
|
272
|
+
renderResetFontSizeButton () {
|
|
273
|
+
if (!this.state.fontSizeChanged) {
|
|
274
|
+
return null
|
|
2161
275
|
}
|
|
2162
276
|
const txt = `${e('reset')} ${e('fontSize')}`
|
|
2163
277
|
return (
|
|
@@ -2172,25 +286,6 @@ class Term extends Component {
|
|
|
2172
286
|
)
|
|
2173
287
|
}
|
|
2174
288
|
|
|
2175
|
-
// getPwd = async () => {
|
|
2176
|
-
// const { sessionId, config } = this.props
|
|
2177
|
-
// const { pid } = this.state
|
|
2178
|
-
// const prps = {
|
|
2179
|
-
// host: config.host,
|
|
2180
|
-
// port: config.port,
|
|
2181
|
-
// pid,
|
|
2182
|
-
// sessionId
|
|
2183
|
-
// }
|
|
2184
|
-
// const result = await runCmds(prps, ['pwd'])
|
|
2185
|
-
// .catch(window.store.onError)
|
|
2186
|
-
// return result ? result[0].trim() : ''
|
|
2187
|
-
// }
|
|
2188
|
-
|
|
2189
|
-
switchEncoding = encode => {
|
|
2190
|
-
this.encode = encode
|
|
2191
|
-
this.attachAddon.decoder = new TextDecoder(encode)
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
289
|
render () {
|
|
2195
290
|
const { loading } = this.state
|
|
2196
291
|
const { height, width, left, top, fullscreen } = this.props
|