@electerm/electerm-react 5.3.26 → 5.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +246 -12
- package/client/common/choose-save-folder.js +21 -2
- package/client/common/constants.js +20 -6
- package/client/common/create-title.jsx +6 -0
- package/client/common/default-setting.js +15 -0
- package/client/common/init-setting-item.js +5 -0
- package/client/common/platform.js +24 -0
- package/client/components/ai/agent-tool-call-card.jsx +10 -2
- package/client/components/ai/agent.js +4 -1
- package/client/components/ai/ai-attachments.js +87 -0
- package/client/components/ai/ai-chat-empty.jsx +43 -0
- package/client/components/ai/ai-chat-entry.jsx +13 -4
- package/client/components/ai/ai-chat-history-item.jsx +28 -4
- package/client/components/ai/ai-chat-history.jsx +2 -1
- package/client/components/ai/ai-chat.jsx +104 -13
- package/client/components/ai/ai-config-props.js +4 -130
- package/client/components/ai/ai-config.jsx +135 -15
- package/client/components/ai/ai-presets.js +173 -0
- package/client/components/ai/ai.styl +83 -0
- package/client/components/bg/css-overwrite.jsx +11 -0
- package/client/components/bookmark-form/bookmark-schema.js +4 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/config/common-fields.js +12 -0
- package/client/components/bookmark-form/config/local.js +3 -2
- package/client/components/bookmark-form/config/serial.js +3 -2
- package/client/components/bookmark-form/config/ssh.js +2 -1
- package/client/components/bookmark-form/config/telnet.js +3 -2
- package/client/components/common/item-filter.jsx +76 -0
- package/client/components/common/item-filter.styl +54 -0
- package/client/components/common/lazy-boundary.jsx +34 -0
- package/client/components/common/opacity.jsx +3 -1
- package/client/components/footer/cmd-history.jsx +132 -45
- package/client/components/footer/cmd-history.styl +42 -4
- package/client/components/footer/footer-entry.jsx +35 -5
- package/client/components/footer/footer.styl +8 -1
- package/client/components/footer/multi-tab-run-modal.jsx +83 -0
- package/client/components/footer/tab-select.jsx +34 -17
- package/client/components/layout/layout.jsx +23 -31
- package/client/components/layout/pixed.js +3 -0
- package/client/components/main/main.jsx +55 -28
- package/client/components/main/term-fullscreen.styl +9 -3
- package/client/components/quick-commands/qm.styl +3 -2
- package/client/components/quick-commands/quick-command-create-modal.jsx +60 -0
- package/client/components/quick-commands/quick-commands-form-elem.jsx +10 -4
- package/client/components/rdp/file-transfer.js +4 -4
- package/client/components/remote-monitor/monitor-details.jsx +458 -0
- package/client/components/remote-monitor/monitor-details.styl +87 -0
- package/client/components/remote-monitor/monitor-model.js +764 -0
- package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
- package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
- package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
- package/client/components/remote-monitor/session-monitor.js +361 -0
- package/client/components/remote-monitor/use-monitor-details.js +10 -0
- package/client/components/remote-monitor/visibility.js +18 -0
- package/client/components/session/session-control.jsx +29 -2
- package/client/components/session/session.jsx +40 -18
- package/client/components/session/sessions.jsx +5 -2
- package/client/components/setting-panel/setting-common.jsx +25 -56
- package/client/components/setting-panel/setting-modal.jsx +57 -42
- package/client/components/setting-panel/setting-terminal.jsx +8 -2
- package/client/components/setting-panel/setting.styl +1 -1
- package/client/components/setting-panel/tab-triggers.jsx +71 -0
- package/client/components/sftp/file-compare-modal.jsx +6 -3
- package/client/components/sftp/file-item.jsx +5 -3
- package/client/components/sftp/owner-list.js +11 -2
- package/client/components/sftp/sftp-entry.jsx +4 -0
- package/client/components/side-panel-r/side-panel-r.jsx +2 -2
- package/client/components/sidebar/side-panel.jsx +1 -1
- package/client/components/sys-menu/menu-btn.jsx +30 -1
- package/client/components/tabs/index.jsx +2 -2
- package/client/components/tabs/tab-title.jsx +3 -2
- package/client/components/tabs/tab.jsx +5 -1
- package/client/components/terminal/attach-addon-custom.js +295 -49
- package/client/components/terminal/automation/index.js +64 -0
- package/client/components/terminal/automation/keys.js +49 -0
- package/client/components/terminal/automation/stream-matcher.js +82 -0
- package/client/components/terminal/automation/strip-ansi.js +19 -0
- package/client/components/terminal/automation/trigger-engine.js +233 -0
- package/client/components/terminal/automation/trigger-presets.js +110 -0
- package/client/components/terminal/command-tracker-addon.js +31 -1
- package/client/components/terminal/mixins/index.js +27 -0
- package/client/components/terminal/mixins/term-attach.js +123 -0
- package/client/components/terminal/mixins/term-config.jsx +84 -0
- package/client/components/terminal/mixins/term-content.js +126 -0
- package/client/components/terminal/mixins/term-context-menu.jsx +323 -0
- package/client/components/terminal/mixins/term-file-drop.js +149 -0
- package/client/components/terminal/mixins/term-init.js +122 -0
- package/client/components/terminal/mixins/term-log.jsx +96 -0
- package/client/components/terminal/mixins/term-resize.js +87 -0
- package/client/components/terminal/mixins/term-socket.js +433 -0
- package/client/components/terminal/mixins/term-suggestions.js +224 -0
- package/client/components/terminal/mixins/term-theme.js +190 -0
- package/client/components/terminal/mixins/term-touch.js +523 -0
- package/client/components/terminal/shell-detect.js +37 -0
- package/client/components/terminal/shell.js +0 -28
- package/client/components/terminal/shortcut-bar-defs.js +30 -4
- package/client/components/terminal/shortcut-bar-edit.jsx +40 -9
- package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
- package/client/components/terminal/shortcut-bar.jsx +17 -10
- package/client/components/terminal/shortcut-bar.styl +4 -0
- package/client/components/terminal/ssh-reload-state.js +96 -0
- package/client/components/terminal/startup-queue.js +433 -0
- package/client/components/terminal/terminal-interactive.jsx +32 -0
- package/client/components/terminal/terminal-select-text.jsx +69 -0
- package/client/components/terminal/terminal.jsx +227 -2064
- package/client/components/terminal/terminal.styl +61 -0
- package/client/components/terminal/transfer-client-base.js +5 -29
- package/client/components/terminal/trzsz-client.js +1 -3
- package/client/components/terminal/xmodem-client.js +1 -3
- package/client/components/terminal/xterm-loader.js +7 -0
- package/client/components/terminal/zmodem-client.js +1 -3
- package/client/components/terminal-info/base.jsx +18 -70
- package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
- package/client/components/terminal-info/terminal-info.jsx +50 -59
- package/client/components/terminal-info/terminal-info.styl +6 -49
- package/client/components/text-editor/open-text-editor.js +30 -0
- package/client/components/text-editor/text-editor-entry.jsx +17 -6
- package/client/components/text-editor/text-editor.jsx +14 -0
- package/client/components/triggers/trigger-editor.jsx +246 -0
- package/client/components/triggers/trigger-form.jsx +200 -0
- package/client/components/triggers/trigger-session-modal.jsx +199 -0
- package/client/components/triggers/trigger-setting-list.jsx +89 -0
- package/client/components/triggers/trigger-transport.jsx +60 -0
- package/client/components/triggers/triggers-field.jsx +19 -0
- package/client/components/widgets/widgets-list.jsx +11 -1
- package/client/css/mobile.styl +29 -8
- package/client/store/common.js +12 -1
- package/client/store/init-state.js +7 -0
- package/client/store/mcp-handler.js +158 -30
- package/client/store/setting.js +9 -0
- package/client/store/store.js +2 -0
- package/client/store/sync.js +2 -0
- package/client/store/tab.js +42 -3
- package/client/store/trigger.js +118 -0
- package/client/store/watch.js +13 -5
- package/client/views/index.pug +3 -0
- package/package.json +1 -1
- package/client/components/terminal-info/activity.jsx +0 -57
- package/client/components/terminal-info/data-cols-parser.jsx +0 -54
- package/client/components/terminal-info/disk.jsx +0 -45
- package/client/components/terminal-info/format-disks.js +0 -45
- package/client/components/terminal-info/network.jsx +0 -121
- package/client/components/terminal-info/resource.jsx +0 -99
- package/client/components/terminal-info/run-cmd.jsx +0 -263
- package/client/components/terminal-info/sys-info.jsx +0 -56
- package/client/components/terminal-info/up.jsx +0 -17
|
@@ -1,117 +1,67 @@
|
|
|
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 TerminalSelectText from './terminal-select-text.jsx'
|
|
26
|
+
import { StartupQueue } from './startup-queue.js'
|
|
27
|
+
import { detectRemoteShell } from './shell-detect.js'
|
|
28
|
+
import { shortcutExtend, shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
|
|
29
|
+
import { applyMixins } from './mixins/index.js'
|
|
30
|
+
import { initMixin } from './mixins/term-init.js'
|
|
31
|
+
import { resizeMixin } from './mixins/term-resize.js'
|
|
32
|
+
import { themeMixin } from './mixins/term-theme.js'
|
|
33
|
+
import { attachMixin } from './mixins/term-attach.js'
|
|
34
|
+
import { socketMixin } from './mixins/term-socket.js'
|
|
35
|
+
import { configMixin } from './mixins/term-config.jsx'
|
|
36
|
+
import { contextMenuMixin } from './mixins/term-context-menu.jsx'
|
|
37
|
+
import { contentMixin } from './mixins/term-content.js'
|
|
38
|
+
import { logMixin } from './mixins/term-log.jsx'
|
|
39
|
+
import { touchMixin } from './mixins/term-touch.js'
|
|
40
|
+
import { fileDropMixin } from './mixins/term-file-drop.js'
|
|
41
|
+
import { suggestionsMixin } from './mixins/term-suggestions.js'
|
|
77
42
|
|
|
78
43
|
const e = window.translate
|
|
79
44
|
|
|
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
|
-
}
|
|
45
|
+
// The terminal behaviour, grouped by concern - see ./mixins/index.js
|
|
46
|
+
const mixins = [
|
|
47
|
+
initMixin,
|
|
48
|
+
resizeMixin,
|
|
49
|
+
themeMixin,
|
|
50
|
+
attachMixin,
|
|
51
|
+
socketMixin,
|
|
52
|
+
configMixin,
|
|
53
|
+
contextMenuMixin,
|
|
54
|
+
contentMixin,
|
|
55
|
+
logMixin,
|
|
56
|
+
touchMixin,
|
|
57
|
+
fileDropMixin,
|
|
58
|
+
suggestionsMixin
|
|
59
|
+
]
|
|
111
60
|
|
|
112
61
|
class Term extends Component {
|
|
113
62
|
constructor (props) {
|
|
114
63
|
super(props)
|
|
64
|
+
applyMixins(this, mixins)
|
|
115
65
|
this.state = {
|
|
116
66
|
loading: false,
|
|
117
67
|
hasSelection: false,
|
|
@@ -130,19 +80,72 @@ class Term extends Component {
|
|
|
130
80
|
terminalError: null,
|
|
131
81
|
dropFileModalVisible: false,
|
|
132
82
|
droppedFiles: [],
|
|
133
|
-
fontSizeChanged: false
|
|
83
|
+
fontSizeChanged: false,
|
|
84
|
+
// touch-only: the buffer snapshot shown by the select-text overlay
|
|
85
|
+
selectTextVisible: false,
|
|
86
|
+
selectTextContent: ''
|
|
134
87
|
}
|
|
135
88
|
this.id = `term-${this.props.tab.id}`
|
|
136
89
|
refs.add(this.id, this)
|
|
137
90
|
this.currentInput = ''
|
|
138
|
-
|
|
139
|
-
this.
|
|
91
|
+
// Owns the shell integration injection + cd + runScripts sequence.
|
|
92
|
+
this.startupQueue = new StartupQueue(this, { detectRemoteShell })
|
|
140
93
|
}
|
|
141
94
|
|
|
142
95
|
domRef = createRef()
|
|
143
96
|
|
|
97
|
+
timers = {}
|
|
98
|
+
|
|
99
|
+
// Config keys that cannot be applied by a re-render - they are pushed into
|
|
100
|
+
// the live xterm options instead.
|
|
101
|
+
terminalConfigProps = [
|
|
102
|
+
{
|
|
103
|
+
name: 'rightClickSelectsWord',
|
|
104
|
+
type: 'glob'
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'fontSize',
|
|
108
|
+
type: 'glob_local'
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'fontFamily',
|
|
112
|
+
type: 'glob_local'
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
|
|
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.
|
|
120
|
+
longPressTimer = null
|
|
121
|
+
touchStartPos = null
|
|
122
|
+
longPressFired = false
|
|
123
|
+
longPressThreshold = 500 // ms
|
|
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()
|
|
138
|
+
|
|
139
|
+
terminalColorQueryDisposables = []
|
|
140
|
+
|
|
144
141
|
componentDidMount () {
|
|
145
|
-
|
|
142
|
+
// xterm and every addon are dynamic imports: a failed chunk fetch would
|
|
143
|
+
// otherwise end as a silent unhandled rejection with a permanently blank
|
|
144
|
+
// terminal pane
|
|
145
|
+
this.initTerminal().catch(e => {
|
|
146
|
+
console.error(e)
|
|
147
|
+
this.handleError({ message: e.message })
|
|
148
|
+
})
|
|
146
149
|
if (this.props.tab.enableSsh === false) {
|
|
147
150
|
this.props.tab.pane = paneMap.fileManager
|
|
148
151
|
}
|
|
@@ -187,6 +190,12 @@ class Term extends Component {
|
|
|
187
190
|
prevProps,
|
|
188
191
|
this.props
|
|
189
192
|
)
|
|
193
|
+
if (
|
|
194
|
+
prevProps.tab?.triggers !== this.props.tab?.triggers &&
|
|
195
|
+
this.triggerManager
|
|
196
|
+
) {
|
|
197
|
+
this.refreshTriggers()
|
|
198
|
+
}
|
|
190
199
|
const themeChanged = !isEqual(
|
|
191
200
|
this.props.themeConfig,
|
|
192
201
|
prevProps.themeConfig
|
|
@@ -207,6 +216,12 @@ class Term extends Component {
|
|
|
207
216
|
clearTimeout(this.longPressTimer)
|
|
208
217
|
this.longPressTimer = null
|
|
209
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
|
|
210
225
|
if (window.store.activeTerminalId === this.props.tab.id) {
|
|
211
226
|
window.store.activeTerminalId = ''
|
|
212
227
|
}
|
|
@@ -214,10 +229,16 @@ class Term extends Component {
|
|
|
214
229
|
this.term.parent = null
|
|
215
230
|
}
|
|
216
231
|
this.disposeTerminalColorQueryHandlers()
|
|
232
|
+
this.startupQueue?.dispose()
|
|
233
|
+
try {
|
|
234
|
+
this._notifyOnDataDebounced?.cancel?.()
|
|
235
|
+
} catch (_) {}
|
|
236
|
+
this._notifyOnDataDebounced = null
|
|
217
237
|
window.cancelAnimationFrame(this.timers.themeRaf)
|
|
218
238
|
this.timers.themeRaf = null
|
|
219
239
|
Object.keys(this.timers).forEach(k => {
|
|
220
240
|
clearTimeout(this.timers[k])
|
|
241
|
+
clearInterval(this.timers[k])
|
|
221
242
|
this.timers[k] = null
|
|
222
243
|
})
|
|
223
244
|
this.onClose = true
|
|
@@ -225,11 +246,17 @@ class Term extends Component {
|
|
|
225
246
|
this.socket.close()
|
|
226
247
|
this.socket = null
|
|
227
248
|
}
|
|
249
|
+
if (this.attachAddon) {
|
|
250
|
+
try {
|
|
251
|
+
this.attachAddon.dispose?.()
|
|
252
|
+
} catch (_) {}
|
|
253
|
+
this.attachAddon = null
|
|
254
|
+
}
|
|
228
255
|
if (this.term) {
|
|
229
256
|
this.term.dispose()
|
|
230
257
|
this.term = null
|
|
231
258
|
}
|
|
232
|
-
this.
|
|
259
|
+
this.disposeTriggerManager?.()
|
|
233
260
|
this.fitAddon = null
|
|
234
261
|
this.zmodemClient = null
|
|
235
262
|
this.trzszClient = null
|
|
@@ -237,2016 +264,144 @@ class Term extends Component {
|
|
|
237
264
|
this.searchAddon = null
|
|
238
265
|
this.fitAddon = null
|
|
239
266
|
this.cmdAddon = null
|
|
267
|
+
this.serializeAddon = null
|
|
240
268
|
this.imageAddon = null
|
|
241
269
|
this.webglContextLossDisposable?.dispose?.()
|
|
242
270
|
this.webglContextLossDisposable = null
|
|
271
|
+
try {
|
|
272
|
+
this.webglAddon?.dispose?.()
|
|
273
|
+
} catch (_) {}
|
|
243
274
|
this.webglAddon = null
|
|
244
275
|
this.webglRecovering = false
|
|
245
276
|
}
|
|
246
277
|
|
|
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 = {}
|
|
278
|
+
// ---- small helpers other mixins build on ----
|
|
360
279
|
|
|
361
|
-
getDomId
|
|
280
|
+
getDomId () {
|
|
362
281
|
return `term-${this.props.tab.id}`
|
|
363
282
|
}
|
|
364
283
|
|
|
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 = () => {
|
|
284
|
+
isActiveTerminal () {
|
|
392
285
|
return this.props.tab.id === this.props.activeTabId &&
|
|
393
286
|
this.props.tab.pane === paneMap.terminal
|
|
394
287
|
}
|
|
395
288
|
|
|
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()
|
|
289
|
+
isRemote () {
|
|
290
|
+
return this.props.tab?.host
|
|
418
291
|
}
|
|
419
292
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
293
|
+
isSsh () {
|
|
294
|
+
const { host, type } = this.props.tab
|
|
295
|
+
return host && (type === 'ssh' || type === undefined)
|
|
423
296
|
}
|
|
424
297
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const text = readClipboard()
|
|
430
|
-
return text.length > 500
|
|
298
|
+
isLocal () {
|
|
299
|
+
const { host, type } = this.props.tab
|
|
300
|
+
return !host &&
|
|
301
|
+
(type === 'local' || type === undefined)
|
|
431
302
|
}
|
|
432
303
|
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
})
|
|
304
|
+
isTelnet () {
|
|
305
|
+
return this.props.tab?.type === connectionMap.telnet
|
|
450
306
|
}
|
|
451
307
|
|
|
452
|
-
|
|
453
|
-
|
|
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)
|
|
308
|
+
isSerial () {
|
|
309
|
+
return this.props.tab?.type === connectionMap.serial
|
|
458
310
|
}
|
|
459
311
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
e.preventDefault()
|
|
464
|
-
e.stopPropagation()
|
|
465
|
-
return false
|
|
466
|
-
}
|
|
467
|
-
if (isMac) {
|
|
468
|
-
return true
|
|
469
|
-
}
|
|
470
|
-
if (!this.isRemote()) {
|
|
312
|
+
isShellTerminal () {
|
|
313
|
+
const { type } = this.props.tab || {}
|
|
314
|
+
if (!type) {
|
|
471
315
|
return true
|
|
472
316
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
showNormalBufferShortcut = (e) => {
|
|
480
|
-
e.stopPropagation()
|
|
481
|
-
this.openNormalBuffer()
|
|
317
|
+
return (
|
|
318
|
+
type === connectionMap.ssh ||
|
|
319
|
+
type === connectionMap.local ||
|
|
320
|
+
type === connectionMap.telnet ||
|
|
321
|
+
type === connectionMap.serial
|
|
322
|
+
)
|
|
482
323
|
}
|
|
483
324
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
325
|
+
setStatus = status => {
|
|
326
|
+
const id = this.props.tab?.id
|
|
327
|
+
this.props.editTab(id, {
|
|
328
|
+
status
|
|
329
|
+
})
|
|
489
330
|
}
|
|
490
331
|
|
|
491
|
-
|
|
492
|
-
if (
|
|
493
|
-
return
|
|
332
|
+
renderResetFontSizeButton () {
|
|
333
|
+
if (!this.state.fontSizeChanged) {
|
|
334
|
+
return null
|
|
494
335
|
}
|
|
495
|
-
const
|
|
496
|
-
|
|
336
|
+
const txt = `${e('reset')} ${e('fontSize')}`
|
|
337
|
+
return (
|
|
338
|
+
<Button
|
|
339
|
+
className='terminal-fontsize-reset'
|
|
340
|
+
onClick={this.handleResetFontSize}
|
|
341
|
+
type='default'
|
|
342
|
+
size='small'
|
|
343
|
+
title={txt}
|
|
344
|
+
icon={<AimOutlined />}
|
|
345
|
+
/>
|
|
346
|
+
)
|
|
497
347
|
}
|
|
498
348
|
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
const
|
|
502
|
-
const
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
|
349
|
+
render () {
|
|
350
|
+
const { loading } = this.state
|
|
351
|
+
const { height, width, left, top, fullscreen } = this.props
|
|
352
|
+
const { id } = this.props.tab
|
|
353
|
+
const isActive = this.isActiveTerminal()
|
|
354
|
+
const cls = classnames(
|
|
355
|
+
'term-wrap',
|
|
356
|
+
'tw-' + id,
|
|
357
|
+
{
|
|
358
|
+
'terminal-not-active': !isActive
|
|
570
359
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (!droppedFiles || !droppedFiles.length) {
|
|
587
|
-
this.handleDropFileModalCancel()
|
|
588
|
-
return
|
|
360
|
+
)
|
|
361
|
+
const prps1 = {
|
|
362
|
+
className: cls,
|
|
363
|
+
style: {
|
|
364
|
+
height,
|
|
365
|
+
width,
|
|
366
|
+
left,
|
|
367
|
+
top,
|
|
368
|
+
zIndex: 10
|
|
369
|
+
},
|
|
370
|
+
onDrop: this.onDrop,
|
|
371
|
+
onContextMenu: this.onContextMenuInner,
|
|
372
|
+
onTouchStart: this.onTouchStart,
|
|
373
|
+
onTouchMove: this.onTouchMove,
|
|
374
|
+
onTouchEnd: this.onTouchEnd
|
|
589
375
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
|
376
|
+
// const fileProps = {
|
|
377
|
+
// type: 'file',
|
|
378
|
+
// multiple: true,
|
|
379
|
+
// id: `${id}-file-sel`,
|
|
380
|
+
// className: 'hide'
|
|
381
|
+
// }
|
|
382
|
+
const prps3 = {
|
|
383
|
+
id: this.getDomId(),
|
|
384
|
+
ref: this.domRef,
|
|
385
|
+
className: 'absolute term-wrap-2',
|
|
386
|
+
style: {
|
|
387
|
+
left: 0,
|
|
388
|
+
top: 0,
|
|
389
|
+
right: 0,
|
|
390
|
+
bottom: 0
|
|
632
391
|
}
|
|
633
392
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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)
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
onTouchMove = (e) => {
|
|
703
|
-
if (!this.touchStartPos) {
|
|
704
|
-
return
|
|
393
|
+
const dropdownProps = {
|
|
394
|
+
menu: {
|
|
395
|
+
items: this.renderContextMenu(),
|
|
396
|
+
onClick: this.onContextMenu
|
|
397
|
+
},
|
|
398
|
+
trigger: this.props.config.pasteWhenContextMenu ? [] : ['contextMenu']
|
|
705
399
|
}
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
this.longPressTimer = null
|
|
712
|
-
this.touchStartPos = null
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
onTouchEnd = () => {
|
|
717
|
-
const wasTap = this.touchStartPos && !this.longPressFired
|
|
718
|
-
clearTimeout(this.longPressTimer)
|
|
719
|
-
this.longPressTimer = null
|
|
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
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
handleLongPress = () => {
|
|
734
|
-
if (!this.touchStartPos || this.state.loading) {
|
|
735
|
-
return
|
|
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
|
|
758
|
-
})
|
|
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
|
-
}
|
|
851
|
-
|
|
852
|
-
isRemote = () => {
|
|
853
|
-
return this.props.tab?.host
|
|
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
|
|
2161
|
-
}
|
|
2162
|
-
const txt = `${e('reset')} ${e('fontSize')}`
|
|
2163
|
-
return (
|
|
2164
|
-
<Button
|
|
2165
|
-
className='terminal-fontsize-reset'
|
|
2166
|
-
onClick={this.handleResetFontSize}
|
|
2167
|
-
type='default'
|
|
2168
|
-
size='small'
|
|
2169
|
-
title={txt}
|
|
2170
|
-
icon={<AimOutlined />}
|
|
2171
|
-
/>
|
|
2172
|
-
)
|
|
2173
|
-
}
|
|
2174
|
-
|
|
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
|
-
render () {
|
|
2195
|
-
const { loading } = this.state
|
|
2196
|
-
const { height, width, left, top, fullscreen } = this.props
|
|
2197
|
-
const { id } = this.props.tab
|
|
2198
|
-
const isActive = this.isActiveTerminal()
|
|
2199
|
-
const cls = classnames(
|
|
2200
|
-
'term-wrap',
|
|
2201
|
-
'tw-' + id,
|
|
2202
|
-
{
|
|
2203
|
-
'terminal-not-active': !isActive
|
|
2204
|
-
}
|
|
2205
|
-
)
|
|
2206
|
-
const prps1 = {
|
|
2207
|
-
className: cls,
|
|
2208
|
-
style: {
|
|
2209
|
-
height,
|
|
2210
|
-
width,
|
|
2211
|
-
left,
|
|
2212
|
-
top,
|
|
2213
|
-
zIndex: 10
|
|
2214
|
-
},
|
|
2215
|
-
onDrop: this.onDrop,
|
|
2216
|
-
onContextMenu: this.onContextMenuInner,
|
|
2217
|
-
onTouchStart: this.onTouchStart,
|
|
2218
|
-
onTouchMove: this.onTouchMove,
|
|
2219
|
-
onTouchEnd: this.onTouchEnd
|
|
2220
|
-
}
|
|
2221
|
-
// const fileProps = {
|
|
2222
|
-
// type: 'file',
|
|
2223
|
-
// multiple: true,
|
|
2224
|
-
// id: `${id}-file-sel`,
|
|
2225
|
-
// className: 'hide'
|
|
2226
|
-
// }
|
|
2227
|
-
const prps3 = {
|
|
2228
|
-
id: this.getDomId(),
|
|
2229
|
-
ref: this.domRef,
|
|
2230
|
-
className: 'absolute term-wrap-2',
|
|
2231
|
-
style: {
|
|
2232
|
-
left: 0,
|
|
2233
|
-
top: 0,
|
|
2234
|
-
right: 0,
|
|
2235
|
-
bottom: 0
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
const dropdownProps = {
|
|
2239
|
-
menu: {
|
|
2240
|
-
items: this.renderContextMenu(),
|
|
2241
|
-
onClick: this.onContextMenu
|
|
2242
|
-
},
|
|
2243
|
-
trigger: this.props.config.pasteWhenContextMenu ? [] : ['contextMenu']
|
|
2244
|
-
}
|
|
2245
|
-
const barProps = {
|
|
2246
|
-
matchIndex: this.state.matchIndex,
|
|
2247
|
-
matches: this.state.searchResults,
|
|
2248
|
-
totalLines: this.state.totalLines,
|
|
2249
|
-
height
|
|
400
|
+
const barProps = {
|
|
401
|
+
matchIndex: this.state.matchIndex,
|
|
402
|
+
matches: this.state.searchResults,
|
|
403
|
+
totalLines: this.state.totalLines,
|
|
404
|
+
height
|
|
2250
405
|
}
|
|
2251
406
|
const spin = loading ? <Spin className='loading-wrapper' spinning={loading} /> : null
|
|
2252
407
|
return (
|
|
@@ -2282,6 +437,14 @@ class Term extends Component {
|
|
|
2282
437
|
onCancel={this.handleDropFileModalCancel}
|
|
2283
438
|
/>
|
|
2284
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
|
+
/>
|
|
2285
448
|
</div>
|
|
2286
449
|
</Dropdown>
|
|
2287
450
|
)
|