@electerm/electerm-react 5.3.26 → 5.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +246 -12
- package/client/common/choose-save-folder.js +21 -2
- package/client/common/constants.js +20 -6
- package/client/common/create-title.jsx +6 -0
- package/client/common/default-setting.js +14 -0
- package/client/common/init-setting-item.js +5 -0
- package/client/common/platform.js +24 -0
- package/client/components/ai/agent-tool-call-card.jsx +10 -2
- package/client/components/ai/agent.js +4 -1
- package/client/components/ai/ai-attachments.js +87 -0
- package/client/components/ai/ai-chat-empty.jsx +43 -0
- package/client/components/ai/ai-chat-entry.jsx +13 -4
- package/client/components/ai/ai-chat-history-item.jsx +28 -4
- package/client/components/ai/ai-chat-history.jsx +2 -1
- package/client/components/ai/ai-chat.jsx +104 -13
- package/client/components/ai/ai-config-props.js +4 -130
- package/client/components/ai/ai-config.jsx +135 -15
- package/client/components/ai/ai-presets.js +173 -0
- package/client/components/ai/ai.styl +83 -0
- package/client/components/bg/css-overwrite.jsx +11 -0
- package/client/components/bookmark-form/bookmark-schema.js +4 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/config/common-fields.js +12 -0
- package/client/components/bookmark-form/config/local.js +3 -2
- package/client/components/bookmark-form/config/serial.js +3 -2
- package/client/components/bookmark-form/config/ssh.js +2 -1
- package/client/components/bookmark-form/config/telnet.js +3 -2
- package/client/components/common/item-filter.jsx +76 -0
- package/client/components/common/item-filter.styl +54 -0
- package/client/components/common/lazy-boundary.jsx +34 -0
- package/client/components/common/opacity.jsx +3 -1
- package/client/components/footer/footer-entry.jsx +35 -5
- package/client/components/footer/footer.styl +8 -1
- package/client/components/layout/layout.jsx +23 -31
- package/client/components/layout/pixed.js +3 -0
- package/client/components/main/main.jsx +55 -28
- package/client/components/main/term-fullscreen.styl +9 -3
- package/client/components/quick-commands/qm.styl +3 -2
- package/client/components/rdp/file-transfer.js +4 -4
- package/client/components/remote-monitor/monitor-details.jsx +458 -0
- package/client/components/remote-monitor/monitor-details.styl +87 -0
- package/client/components/remote-monitor/monitor-model.js +764 -0
- package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
- package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
- package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
- package/client/components/remote-monitor/session-monitor.js +361 -0
- package/client/components/remote-monitor/use-monitor-details.js +10 -0
- package/client/components/remote-monitor/visibility.js +18 -0
- package/client/components/session/session-control.jsx +29 -2
- package/client/components/session/session.jsx +40 -18
- package/client/components/session/sessions.jsx +5 -2
- package/client/components/setting-panel/setting-common.jsx +25 -56
- package/client/components/setting-panel/setting-modal.jsx +57 -42
- package/client/components/setting-panel/setting-terminal.jsx +7 -2
- package/client/components/setting-panel/setting.styl +1 -1
- package/client/components/setting-panel/tab-triggers.jsx +71 -0
- package/client/components/sftp/file-compare-modal.jsx +6 -3
- package/client/components/sftp/file-item.jsx +5 -3
- package/client/components/sftp/owner-list.js +11 -2
- package/client/components/side-panel-r/side-panel-r.jsx +2 -2
- package/client/components/sys-menu/menu-btn.jsx +30 -1
- package/client/components/tabs/index.jsx +2 -2
- package/client/components/tabs/tab-title.jsx +3 -2
- package/client/components/tabs/tab.jsx +5 -1
- package/client/components/terminal/attach-addon-custom.js +295 -49
- package/client/components/terminal/automation/index.js +64 -0
- package/client/components/terminal/automation/keys.js +49 -0
- package/client/components/terminal/automation/stream-matcher.js +82 -0
- package/client/components/terminal/automation/strip-ansi.js +19 -0
- package/client/components/terminal/automation/trigger-engine.js +233 -0
- package/client/components/terminal/automation/trigger-presets.js +110 -0
- package/client/components/terminal/command-tracker-addon.js +31 -1
- package/client/components/terminal/mixins/index.js +27 -0
- package/client/components/terminal/mixins/term-attach.js +123 -0
- package/client/components/terminal/mixins/term-config.jsx +84 -0
- package/client/components/terminal/mixins/term-content.js +88 -0
- package/client/components/terminal/mixins/term-context-menu.jsx +291 -0
- package/client/components/terminal/mixins/term-file-drop.js +149 -0
- package/client/components/terminal/mixins/term-init.js +122 -0
- package/client/components/terminal/mixins/term-log.jsx +96 -0
- package/client/components/terminal/mixins/term-resize.js +87 -0
- package/client/components/terminal/mixins/term-socket.js +433 -0
- package/client/components/terminal/mixins/term-suggestions.js +224 -0
- package/client/components/terminal/mixins/term-theme.js +190 -0
- package/client/components/terminal/mixins/term-touch.js +136 -0
- package/client/components/terminal/shell-detect.js +37 -0
- package/client/components/terminal/shell.js +0 -28
- package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
- package/client/components/terminal/shortcut-bar.jsx +4 -6
- package/client/components/terminal/shortcut-bar.styl +4 -0
- package/client/components/terminal/ssh-reload-state.js +56 -0
- package/client/components/terminal/startup-queue.js +379 -0
- package/client/components/terminal/terminal-interactive.jsx +32 -0
- package/client/components/terminal/terminal.jsx +102 -2007
- package/client/components/terminal/transfer-client-base.js +5 -29
- package/client/components/terminal/trzsz-client.js +1 -3
- package/client/components/terminal/xmodem-client.js +1 -3
- package/client/components/terminal/xterm-loader.js +7 -0
- package/client/components/terminal/zmodem-client.js +1 -3
- package/client/components/terminal-info/base.jsx +18 -70
- package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
- package/client/components/terminal-info/terminal-info.jsx +50 -59
- package/client/components/terminal-info/terminal-info.styl +6 -49
- package/client/components/text-editor/open-text-editor.js +30 -0
- package/client/components/text-editor/text-editor-entry.jsx +17 -6
- package/client/components/text-editor/text-editor.jsx +14 -0
- package/client/components/triggers/trigger-editor.jsx +246 -0
- package/client/components/triggers/trigger-form.jsx +200 -0
- package/client/components/triggers/trigger-session-modal.jsx +199 -0
- package/client/components/triggers/trigger-setting-list.jsx +89 -0
- package/client/components/triggers/trigger-transport.jsx +60 -0
- package/client/components/triggers/triggers-field.jsx +19 -0
- package/client/components/widgets/widgets-list.jsx +11 -1
- package/client/css/mobile.styl +9 -1
- package/client/store/common.js +12 -1
- package/client/store/init-state.js +7 -0
- package/client/store/mcp-handler.js +158 -30
- package/client/store/setting.js +9 -0
- package/client/store/store.js +2 -0
- package/client/store/sync.js +2 -0
- package/client/store/tab.js +41 -3
- package/client/store/trigger.js +118 -0
- package/client/store/watch.js +13 -5
- package/client/views/index.pug +3 -0
- package/package.json +1 -1
- package/client/components/terminal-info/activity.jsx +0 -57
- package/client/components/terminal-info/data-cols-parser.jsx +0 -54
- package/client/components/terminal-info/disk.jsx +0 -45
- package/client/components/terminal-info/format-disks.js +0 -45
- package/client/components/terminal-info/network.jsx +0 -121
- package/client/components/terminal-info/resource.jsx +0 -99
- package/client/components/terminal-info/run-cmd.jsx +0 -263
- package/client/components/terminal-info/sys-info.jsx +0 -56
- package/client/components/terminal-info/up.jsx +0 -17
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { debounce } from 'lodash-es'
|
|
2
|
+
import { refsStatic } from '../../common/ref.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Command suggestions: reading the current input straight from the terminal
|
|
6
|
+
* buffer, positioning the dropdown at the cursor, and the password prompt
|
|
7
|
+
* variant triggered by the shell integration OSC sequences.
|
|
8
|
+
*/
|
|
9
|
+
export const suggestionsMixin = {
|
|
10
|
+
getCursorPosition () {
|
|
11
|
+
if (!this.term) return null
|
|
12
|
+
|
|
13
|
+
// Get the active buffer and cursor position
|
|
14
|
+
const buffer = this.term.buffer.active
|
|
15
|
+
const cursorRow = buffer.cursorY
|
|
16
|
+
const cursorCol = buffer.cursorX
|
|
17
|
+
|
|
18
|
+
// Get dimensions from term element
|
|
19
|
+
const termElement = this.term.element
|
|
20
|
+
if (!termElement) return null
|
|
21
|
+
|
|
22
|
+
// Get the exact position of the terminal element
|
|
23
|
+
const termRect = termElement.getBoundingClientRect()
|
|
24
|
+
|
|
25
|
+
// Calculate cell dimensions
|
|
26
|
+
const cellWidth = termRect.width / this.term.cols
|
|
27
|
+
const cellHeight = termRect.height / this.term.rows
|
|
28
|
+
|
|
29
|
+
// Calculate absolute position relative to terminal element
|
|
30
|
+
const left = Math.floor(termRect.left + (cursorCol * cellWidth))
|
|
31
|
+
const top = Math.floor(termRect.top + ((cursorRow + 1) * cellHeight))
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
cellWidth,
|
|
35
|
+
cellHeight,
|
|
36
|
+
left,
|
|
37
|
+
top
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
closeSuggestions () {
|
|
42
|
+
refsStatic
|
|
43
|
+
.get('terminal-suggestions')
|
|
44
|
+
?.closeSuggestions()
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
openSuggestions (cursorPos, data) {
|
|
48
|
+
refsStatic
|
|
49
|
+
.get('terminal-suggestions')
|
|
50
|
+
?.openSuggestions(cursorPos, data)
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read current input directly from terminal buffer
|
|
55
|
+
* This is more reliable than tracking character-by-character
|
|
56
|
+
*/
|
|
57
|
+
getCurrentInput () {
|
|
58
|
+
if (!this.term) return ''
|
|
59
|
+
|
|
60
|
+
const buffer = this.term.buffer.active
|
|
61
|
+
const cursorY = buffer.cursorY
|
|
62
|
+
const cursorX = buffer.cursorX
|
|
63
|
+
|
|
64
|
+
// Get the current line from buffer (baseY + cursorY gives absolute position)
|
|
65
|
+
const absoluteY = buffer.baseY + cursorY
|
|
66
|
+
const line = buffer.getLine(absoluteY)
|
|
67
|
+
if (!line) return ''
|
|
68
|
+
|
|
69
|
+
// Get text from start of line up to cursor position
|
|
70
|
+
const lineText = line.translateToString(true, 0, cursorX)
|
|
71
|
+
|
|
72
|
+
// Try to extract command after prompt
|
|
73
|
+
// Common prompt endings with trailing space
|
|
74
|
+
const promptEndings = ['$ ', '# ', '> ', '% ', '] ', ') ']
|
|
75
|
+
|
|
76
|
+
let commandStart = 0
|
|
77
|
+
for (const ending of promptEndings) {
|
|
78
|
+
const idx = lineText.lastIndexOf(ending)
|
|
79
|
+
if (idx !== -1 && idx + ending.length > commandStart) {
|
|
80
|
+
commandStart = idx + ending.length
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return lineText.slice(commandStart)
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
setCurrentInput (value) {
|
|
88
|
+
this.currentInput = value
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Handle special input events for command history tracking
|
|
93
|
+
* The actual input reading is done via getCurrentInput from buffer
|
|
94
|
+
*/
|
|
95
|
+
handleInputEvent (d) {
|
|
96
|
+
// Handle Enter - add command to history
|
|
97
|
+
if (d === '\r' || d === '\n') {
|
|
98
|
+
const currentCmd = this.getCurrentInput()
|
|
99
|
+
if (currentCmd && currentCmd.trim() && this.shouldUseManualHistory()) {
|
|
100
|
+
window.store.addCmdHistory(currentCmd.trim())
|
|
101
|
+
}
|
|
102
|
+
if (currentCmd && currentCmd.trim() === 'exit') {
|
|
103
|
+
this.userTypeExit = true
|
|
104
|
+
}
|
|
105
|
+
this.closeSuggestions()
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
onData (d) {
|
|
110
|
+
this.handleInputEvent(d)
|
|
111
|
+
// Skip normal suggestion logic when in password mode
|
|
112
|
+
const suggestions = refsStatic.get('terminal-suggestions')
|
|
113
|
+
if (suggestions?.state?.passwordMode) {
|
|
114
|
+
if (d === '\r' || d === '\n') {
|
|
115
|
+
this.closeSuggestions()
|
|
116
|
+
}
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
if (this.props.config.showCmdSuggestions) {
|
|
120
|
+
if (d === '\r' || d === '\n') {
|
|
121
|
+
this.closeSuggestions()
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
// Debounce the suggestion opening to avoid expensive work
|
|
125
|
+
// (buffer read + getBoundingClientRect + React re-render) on every keystroke
|
|
126
|
+
this._debouncedOpenSuggestions()
|
|
127
|
+
} else {
|
|
128
|
+
this.closeSuggestions()
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Called by AttachAddonCustom after data is written to the terminal buffer.
|
|
134
|
+
* This fires after server echo arrives, so getCurrentInput() reflects the
|
|
135
|
+
* latest state. We trigger a debounced suggestion refresh so the dropdown
|
|
136
|
+
* updates correctly after backspace, delete, and other edits that rely on
|
|
137
|
+
* server-side echo to update the buffer.
|
|
138
|
+
*/
|
|
139
|
+
onTerminalWrite () {
|
|
140
|
+
if (!this.props.config.showCmdSuggestions) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
const suggestions = refsStatic.get('terminal-suggestions')
|
|
144
|
+
if (suggestions?.state?.showSuggestions && !suggestions?.state?.passwordMode) {
|
|
145
|
+
this._debouncedOpenSuggestions()
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// One debounced runner per terminal, so a keystroke does not trigger a
|
|
150
|
+
// buffer read + layout + re-render for every character.
|
|
151
|
+
_debouncedOpenSuggestions () {
|
|
152
|
+
if (!this._openSuggestionsDebounced) {
|
|
153
|
+
this._openSuggestionsDebounced = debounce(() => {
|
|
154
|
+
const data = this.getCurrentInput()
|
|
155
|
+
if (!data) {
|
|
156
|
+
this.closeSuggestions()
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const cursorPos = this.getCursorPosition()
|
|
160
|
+
this.openSuggestions(cursorPos, data)
|
|
161
|
+
}, 80)
|
|
162
|
+
}
|
|
163
|
+
this._openSuggestionsDebounced()
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
onPasswordPromptDetected () {
|
|
167
|
+
window.store.notifyTabPasswordPrompt(this.props.tab.id)
|
|
168
|
+
if (!this.props.config.showCmdSuggestions) {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
const cursorPos = this.getCursorPosition()
|
|
172
|
+
if (cursorPos) {
|
|
173
|
+
refsStatic
|
|
174
|
+
.get('terminal-suggestions')
|
|
175
|
+
?.openPasswordSuggestions(cursorPos)
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
onPasswordPromptCancelled () {
|
|
180
|
+
window.store.clearTabPasswordPrompt(this.props.tab.id)
|
|
181
|
+
const suggestions = refsStatic.get('terminal-suggestions')
|
|
182
|
+
if (suggestions?.state?.passwordMode) {
|
|
183
|
+
suggestions.closeSuggestions()
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
getCmd () {
|
|
188
|
+
return this.cmdAddon.getCurrentCommand()
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
getCwd () {
|
|
192
|
+
// Use shell integration CWD if available
|
|
193
|
+
if (this.cmdAddon && this.cmdAddon.hasShellIntegration()) {
|
|
194
|
+
const cwd = this.cmdAddon.getCwd()
|
|
195
|
+
if (cwd) {
|
|
196
|
+
this.setCwd(cwd)
|
|
197
|
+
return cwd
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Fallback: no longer needed with shell integration
|
|
201
|
+
return ''
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
setCwd (cwd) {
|
|
205
|
+
this.props.setCwd(cwd, this.state.id)
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
shouldUseManualHistory () {
|
|
209
|
+
return !this.cmdAddon || !this.cmdAddon.hasShellIntegration()
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
parse (rawText) {
|
|
213
|
+
let result = ''
|
|
214
|
+
const len = rawText.length
|
|
215
|
+
for (let i = 0; i < len; i++) {
|
|
216
|
+
if (rawText[i] === '\b') {
|
|
217
|
+
result = result.slice(0, -1)
|
|
218
|
+
} else {
|
|
219
|
+
result += rawText[i]
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return result
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import deepCopy from 'json-deep-copy'
|
|
2
|
+
import { loadWebglAddon } from '../xterm-loader.js'
|
|
3
|
+
import { rendererTypes } from '../../../common/constants.js'
|
|
4
|
+
import {
|
|
5
|
+
createRendererThemeConfig,
|
|
6
|
+
handleTerminalColorQuery,
|
|
7
|
+
blendSelectionOverBackground
|
|
8
|
+
} from '../terminal-color-query.mjs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renderer (DOM / WebGL) and theming, including the OSC 10/11 colour query
|
|
12
|
+
* handlers that let shells ask the terminal for its background colour.
|
|
13
|
+
*/
|
|
14
|
+
export const themeMixin = {
|
|
15
|
+
async loadRenderer (term, config) {
|
|
16
|
+
// xterm 6.x: only the built-in DOM renderer and the WebGL addon exist
|
|
17
|
+
// (the canvas renderer addon was removed in 6.x). 'dom' = no addon loaded
|
|
18
|
+
// (built-in DOM renderer). Legacy 'canvas' settings fall back to DOM.
|
|
19
|
+
if (config.rendererType === rendererTypes.webGL) {
|
|
20
|
+
try {
|
|
21
|
+
const WebglAddon = await loadWebglAddon()
|
|
22
|
+
const webglAddon = new WebglAddon()
|
|
23
|
+
this.webglAddon = webglAddon
|
|
24
|
+
// On macOS native fullscreen the GPU/WebGL context can be lost when
|
|
25
|
+
// the window migrates across Spaces. Without a listener xterm keeps
|
|
26
|
+
// drawing into a dead context and every terminal goes black while the
|
|
27
|
+
// rest of the UI stays alive. Rebuild the addon to recover.
|
|
28
|
+
this.webglContextLossDisposable = webglAddon.onContextLoss(this.handleWebglContextLoss)
|
|
29
|
+
term.loadAddon(webglAddon)
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error('render with webgl failed, fallback to dom renderer')
|
|
32
|
+
console.error(e)
|
|
33
|
+
// built-in DOM renderer is used (no addon loaded)
|
|
34
|
+
this.webglAddon = null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
reloadWebglRenderer (reason = 'reload') {
|
|
40
|
+
console.warn(`webgl renderer ${reason}, rebuilding`)
|
|
41
|
+
try {
|
|
42
|
+
this.webglContextLossDisposable?.dispose?.()
|
|
43
|
+
this.webglContextLossDisposable = null
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.error(e)
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
this.webglAddon?.dispose?.()
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error(e)
|
|
51
|
+
}
|
|
52
|
+
this.webglAddon = null
|
|
53
|
+
const { term } = this
|
|
54
|
+
const { config } = this.props
|
|
55
|
+
return this.loadRenderer(term, config)
|
|
56
|
+
.then(() => {
|
|
57
|
+
term.refresh(0, term.rows - 1)
|
|
58
|
+
})
|
|
59
|
+
.catch(e => {
|
|
60
|
+
console.error(`webgl renderer ${reason} failed`, e)
|
|
61
|
+
})
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
handleWebglContextLoss (webglAddon = this.webglAddon) {
|
|
65
|
+
if (this.webglRecovering || !webglAddon) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
this.webglRecovering = true
|
|
69
|
+
this.reloadWebglRenderer('context loss')
|
|
70
|
+
.finally(() => {
|
|
71
|
+
this.webglRecovering = false
|
|
72
|
+
})
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
disposeTerminalColorQueryHandlers () {
|
|
76
|
+
this.terminalColorQueryDisposables.forEach(disposable => disposable?.dispose?.())
|
|
77
|
+
this.terminalColorQueryDisposables.length = 0
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
getVisibleTerminalBackground () {
|
|
81
|
+
const uiThemeConfig = window.store?.getUiThemeConfig?.() || {}
|
|
82
|
+
// The store value (uiThemeConfig.main) is always immediately up-to-date
|
|
83
|
+
// when the theme changes, because it reads directly from store.config.theme.
|
|
84
|
+
// The CSS --main variable lags behind because UiTheme's useEffect runs
|
|
85
|
+
// asynchronously after componentDidUpdate. So we prioritise the store
|
|
86
|
+
// value, and only fall back to CSS (for custom-CSS edge cases) or the
|
|
87
|
+
// terminal theme background (last resort).
|
|
88
|
+
const root = document.documentElement
|
|
89
|
+
const cssMain = root && window.getComputedStyle
|
|
90
|
+
? window.getComputedStyle(root).getPropertyValue('--main').trim()
|
|
91
|
+
: ''
|
|
92
|
+
return uiThemeConfig.main || cssMain || this.props.themeConfig.background
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
getVisibleTerminalForeground () {
|
|
96
|
+
const uiThemeConfig = window.store?.getUiThemeConfig?.() || {}
|
|
97
|
+
return uiThemeConfig.text
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
registerTerminalColorQueryHandlers (term, themeConfig = {}) {
|
|
101
|
+
this.disposeTerminalColorQueryHandlers()
|
|
102
|
+
if (!term?.parser?.registerOscHandler) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
const background = this.getVisibleTerminalBackground()
|
|
106
|
+
const foregroundFallback = this.getVisibleTerminalForeground()
|
|
107
|
+
this.terminalColorQueryDisposables.push(
|
|
108
|
+
term.parser.registerOscHandler(10, data => {
|
|
109
|
+
return handleTerminalColorQuery(term, 10, themeConfig.foreground, foregroundFallback, data)
|
|
110
|
+
}),
|
|
111
|
+
term.parser.registerOscHandler(11, data => {
|
|
112
|
+
return handleTerminalColorQuery(term, 11, background, themeConfig.background, data)
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
getRendererThemeConfig (themeConfig = this.props.themeConfig) {
|
|
118
|
+
return createRendererThemeConfig(
|
|
119
|
+
deepCopy(themeConfig),
|
|
120
|
+
this.props.config.rendererType,
|
|
121
|
+
this.getVisibleTerminalBackground()
|
|
122
|
+
)
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Apply the current renderer theme to the terminal and trigger a repaint.
|
|
127
|
+
* When `deferred` is true (WebGL mode), a second repaint is scheduled on
|
|
128
|
+
* the next animation frame so the theme picks up CSS --main changes that
|
|
129
|
+
* UiTheme's useEffect applies asynchronously after componentDidUpdate.
|
|
130
|
+
* The optional `term` parameter is used during initTerminal, where
|
|
131
|
+
* this.term hasn't been assigned yet.
|
|
132
|
+
*/
|
|
133
|
+
applyTerminalTheme (deferred = false, term = this.term) {
|
|
134
|
+
if (!term || this.onClose) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
term.options.theme = this.getRendererThemeConfig(this.props.themeConfig)
|
|
138
|
+
this.fixSelectionColors(term)
|
|
139
|
+
term.refresh(0, term.rows - 1)
|
|
140
|
+
if (deferred && this.props.config.rendererType === rendererTypes.webGL) {
|
|
141
|
+
window.cancelAnimationFrame(this.timers.themeRaf)
|
|
142
|
+
this.timers.themeRaf = window.requestAnimationFrame(() => {
|
|
143
|
+
if (!this.term || this.onClose) {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
this.term.options.theme = this.getRendererThemeConfig(this.props.themeConfig)
|
|
147
|
+
this.fixSelectionColors(this.term)
|
|
148
|
+
this.term.refresh(0, this.term.rows - 1)
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* xterm computes its DOM selection colour as blend(theme.background,
|
|
155
|
+
* selectionBackground). The terminal background is forced transparent
|
|
156
|
+
* (rgba(0,0,0,0)) so electerm's own CSS background / image shows through,
|
|
157
|
+
* which makes the blend run over transparent-black. Recompute the selection
|
|
158
|
+
* colours over the real visible background, from the *configured* theme
|
|
159
|
+
* colours — xterm's internal selectionBackgroundTransparent forces opaque
|
|
160
|
+
* colours down to 0.3 alpha (xterm#2737), which would make every selection
|
|
161
|
+
* translucent even when the user configured an opaque one.
|
|
162
|
+
*/
|
|
163
|
+
fixSelectionColors (term) {
|
|
164
|
+
const themeConfig = this.props.themeConfig || {}
|
|
165
|
+
const themeService = term?._core?._themeService
|
|
166
|
+
if (!themeService?.modifyColors) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
const visibleBackground = this.getVisibleTerminalBackground()
|
|
170
|
+
// xterm's DEFAULT_SELECTION, used when the theme omits a selection color
|
|
171
|
+
const selectionFallback = 'rgba(255, 255, 255, 0.3)'
|
|
172
|
+
const inactiveFallback = themeConfig.selectionBackground || selectionFallback
|
|
173
|
+
themeService.modifyColors((c) => {
|
|
174
|
+
const active = blendSelectionOverBackground(
|
|
175
|
+
visibleBackground,
|
|
176
|
+
themeConfig.selectionBackground || selectionFallback
|
|
177
|
+
)
|
|
178
|
+
if (active) {
|
|
179
|
+
c.selectionBackgroundOpaque = active
|
|
180
|
+
}
|
|
181
|
+
const inactive = blendSelectionOverBackground(
|
|
182
|
+
visibleBackground,
|
|
183
|
+
themeConfig.selectionInactiveBackground || inactiveFallback
|
|
184
|
+
)
|
|
185
|
+
if (inactive) {
|
|
186
|
+
c.selectionInactiveBackgroundOpaque = inactive
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile touch support.
|
|
3
|
+
* On touch devices, long-press should (1) select the word under the finger
|
|
4
|
+
* and (2) open the context menu — mirroring desktop right-click behaviour.
|
|
5
|
+
* xterm.js only uses touch events for scrolling, so we add explicit
|
|
6
|
+
* long-press detection here.
|
|
7
|
+
*/
|
|
8
|
+
export const touchMixin = {
|
|
9
|
+
onTouchStart (e) {
|
|
10
|
+
if (e.touches.length !== 1) {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
const touch = e.touches[0]
|
|
14
|
+
this.touchStartPos = {
|
|
15
|
+
clientX: touch.clientX,
|
|
16
|
+
clientY: touch.clientY,
|
|
17
|
+
target: e.currentTarget
|
|
18
|
+
}
|
|
19
|
+
this.longPressFired = false
|
|
20
|
+
clearTimeout(this.longPressTimer)
|
|
21
|
+
this.longPressTimer = setTimeout(() => {
|
|
22
|
+
this.handleLongPress()
|
|
23
|
+
}, this.longPressThreshold)
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
onTouchMove (e) {
|
|
27
|
+
if (!this.touchStartPos) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
const touch = e.touches[0]
|
|
31
|
+
const dx = touch.clientX - this.touchStartPos.clientX
|
|
32
|
+
const dy = touch.clientY - this.touchStartPos.clientY
|
|
33
|
+
if (Math.sqrt(dx * dx + dy * dy) > this.longPressMoveTolerance) {
|
|
34
|
+
clearTimeout(this.longPressTimer)
|
|
35
|
+
this.longPressTimer = null
|
|
36
|
+
this.touchStartPos = null
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
onTouchEnd () {
|
|
41
|
+
const wasTap = this.touchStartPos && !this.longPressFired
|
|
42
|
+
clearTimeout(this.longPressTimer)
|
|
43
|
+
this.longPressTimer = null
|
|
44
|
+
this.touchStartPos = null
|
|
45
|
+
// xterm's own touch (Gesture) handler calls preventDefault()/stopPropagation()
|
|
46
|
+
// on touchstart/touchend, which suppresses the synthesised mousedown xterm
|
|
47
|
+
// relies on to focus its hidden helper textarea. As a result a tap never
|
|
48
|
+
// focuses the terminal on touch devices, so the soft keyboard never opens
|
|
49
|
+
// and you cannot type. Focus explicitly on a clean tap (not a long-press,
|
|
50
|
+
// not a scroll) so mobile input works. The focus() runs synchronously inside
|
|
51
|
+
// this user-gesture handler, so iOS/Android will show the keyboard.
|
|
52
|
+
if (wasTap && this.term) {
|
|
53
|
+
this.term.focus()
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
handleLongPress () {
|
|
58
|
+
if (!this.touchStartPos || this.state.loading) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
this.longPressFired = true
|
|
62
|
+
const { clientX, clientY, target } = this.touchStartPos
|
|
63
|
+
|
|
64
|
+
// Select the word at the touch position (same as desktop right-click word
|
|
65
|
+
// select) so the user can immediately copy or act on it.
|
|
66
|
+
this.selectWordAt(clientX, clientY)
|
|
67
|
+
|
|
68
|
+
// Respect pasteWhenContextMenu: when enabled, long-press pastes instead
|
|
69
|
+
// of showing the menu (same as desktop right-click).
|
|
70
|
+
if (this.props.config.pasteWhenContextMenu) {
|
|
71
|
+
this.onPaste()
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Dispatch a synthetic contextmenu event so antd Dropdown's contextMenu
|
|
76
|
+
// trigger opens the menu at the exact finger position.
|
|
77
|
+
const event = new MouseEvent('contextmenu', {
|
|
78
|
+
bubbles: true,
|
|
79
|
+
cancelable: true,
|
|
80
|
+
clientX,
|
|
81
|
+
clientY
|
|
82
|
+
})
|
|
83
|
+
target.dispatchEvent(event)
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
selectWordAt (clientX, clientY) {
|
|
87
|
+
if (!this.term) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
const termElement = this.term.element
|
|
91
|
+
if (!termElement) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
const rect = termElement.getBoundingClientRect()
|
|
95
|
+
const x = clientX - rect.left
|
|
96
|
+
const y = clientY - rect.top
|
|
97
|
+
if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const cellWidth = rect.width / this.term.cols
|
|
102
|
+
const cellHeight = rect.height / this.term.rows
|
|
103
|
+
const col = Math.floor(x / cellWidth)
|
|
104
|
+
const row = Math.floor(y / cellHeight)
|
|
105
|
+
|
|
106
|
+
const buffer = this.term.buffer.active
|
|
107
|
+
const line = buffer.getLine(row)
|
|
108
|
+
if (!line) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const text = line.translateToString(true)
|
|
113
|
+
const wordSeparator = this.props.config.terminalWordSeparator ||
|
|
114
|
+
' ./\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~ ?'
|
|
115
|
+
|
|
116
|
+
// If the touched cell is empty or a separator, nothing to select
|
|
117
|
+
if (col >= text.length || wordSeparator.includes(text[col])) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Find word start
|
|
122
|
+
let start = col
|
|
123
|
+
while (start > 0 && !wordSeparator.includes(text[start - 1])) {
|
|
124
|
+
start--
|
|
125
|
+
}
|
|
126
|
+
// Find word end
|
|
127
|
+
let end = col
|
|
128
|
+
while (end < text.length && !wordSeparator.includes(text[end])) {
|
|
129
|
+
end++
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (end > start) {
|
|
133
|
+
this.term.select(start, row, end - start)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote shell detection.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of shell.js on purpose: it needs the network (runCmd), which drags
|
|
5
|
+
* the whole app runtime into the import graph. shell.js stays pure so the
|
|
6
|
+
* startup queue can be imported (and unit tested) on its own.
|
|
7
|
+
*/
|
|
8
|
+
import { runCmd } from './terminal-apis.js'
|
|
9
|
+
import { detectShellType } from './shell.js'
|
|
10
|
+
|
|
11
|
+
export async function detectRemoteShell (pid) {
|
|
12
|
+
// SSH exec runs under the account shell, so prefer the configured shell path
|
|
13
|
+
// instead of probing for any shell binary installed on the host.
|
|
14
|
+
const cmd = 'printf "%s\n" "$SHELL"'
|
|
15
|
+
|
|
16
|
+
// { silent: true } so this best-effort probe does not emit a transport-level
|
|
17
|
+
// fetch warning; a single, clearer warning is logged below if it fails.
|
|
18
|
+
const r = await runCmd(pid, cmd, { silent: true })
|
|
19
|
+
.catch((err) => {
|
|
20
|
+
// Non-fatal: the interactive shell already opened, so the terminal keeps
|
|
21
|
+
// working. We just can't inject OSC 633 shell integration for command
|
|
22
|
+
// tracking. This commonly happens when the server limits concurrent
|
|
23
|
+
// sessions (sshd MaxSessions) or the account uses a forced/restricted
|
|
24
|
+
// command, which rejects the auxiliary exec channel with
|
|
25
|
+
// "(SSH) Channel open failure: open failed".
|
|
26
|
+
console.warn('detectRemoteShell: exec channel rejected by server, shell integration disabled, falling back to sh —', err?.message || err)
|
|
27
|
+
return 'sh'
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const shell = r.trim().toLowerCase()
|
|
31
|
+
|
|
32
|
+
if (!shell) {
|
|
33
|
+
return 'sh'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return detectShellType(shell)
|
|
37
|
+
}
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/* eslint-disable no-template-curly-in-string, no-useless-escape */
|
|
18
|
-
import { runCmd } from './terminal-apis.js'
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
20
|
* Get inline shell integration command for bash (one-liner format)
|
|
@@ -153,30 +152,3 @@ export function getShellIntegrationCommand (shellType = 'bash') {
|
|
|
153
152
|
const cmd = getInlineShellIntegration(shellType)
|
|
154
153
|
return wrapSilent(cmd, shellType)
|
|
155
154
|
}
|
|
156
|
-
export async function detectRemoteShell (pid) {
|
|
157
|
-
// SSH exec runs under the account shell, so prefer the configured shell path
|
|
158
|
-
// instead of probing for any shell binary installed on the host.
|
|
159
|
-
const cmd = 'printf "%s\n" "$SHELL"'
|
|
160
|
-
|
|
161
|
-
// { silent: true } so this best-effort probe does not emit a transport-level
|
|
162
|
-
// fetch warning; a single, clearer warning is logged below if it fails.
|
|
163
|
-
const r = await runCmd(pid, cmd, { silent: true })
|
|
164
|
-
.catch((err) => {
|
|
165
|
-
// Non-fatal: the interactive shell already opened, so the terminal keeps
|
|
166
|
-
// working. We just can't inject OSC 633 shell integration for command
|
|
167
|
-
// tracking. This commonly happens when the server limits concurrent
|
|
168
|
-
// sessions (sshd MaxSessions) or the account uses a forced/restricted
|
|
169
|
-
// command, which rejects the auxiliary exec channel with
|
|
170
|
-
// "(SSH) Channel open failure: open failed".
|
|
171
|
-
console.warn('detectRemoteShell: exec channel rejected by server, shell integration disabled, falling back to sh —', err?.message || err)
|
|
172
|
-
return 'sh'
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
const shell = r.trim().toLowerCase()
|
|
176
|
-
|
|
177
|
-
if (!shell) {
|
|
178
|
-
return 'sh'
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return detectShellType(shell)
|
|
182
|
-
}
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
import { lazy, Suspense, useEffect, useState } from 'react'
|
|
40
40
|
import { auto } from 'manate/react'
|
|
41
|
+
import LazyBoundary from '../common/lazy-boundary'
|
|
41
42
|
|
|
42
43
|
const ShortcutBar = lazy(() => import('./shortcut-bar'))
|
|
43
44
|
|
|
@@ -166,9 +167,11 @@ function ShortcutBarEntry (props) {
|
|
|
166
167
|
return null
|
|
167
168
|
}
|
|
168
169
|
return (
|
|
169
|
-
<
|
|
170
|
-
<
|
|
171
|
-
|
|
170
|
+
<LazyBoundary>
|
|
171
|
+
<Suspense fallback={null}>
|
|
172
|
+
<ShortcutBar {...props} />
|
|
173
|
+
</Suspense>
|
|
174
|
+
</LazyBoundary>
|
|
172
175
|
)
|
|
173
176
|
}
|
|
174
177
|
|
|
@@ -135,12 +135,10 @@ function ShortcutBar (props) {
|
|
|
135
135
|
// keep the offset in a CSS var consumed by shortcut-bar.styl, and expose the
|
|
136
136
|
// lifted height so layout.jsx can shrink the terminal accordingly.
|
|
137
137
|
useEffect(() => {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
store.shortcutBarKbOffset = kbOffset
|
|
143
|
-
}, [kbOffset])
|
|
138
|
+
const offset = store.shortcutBarVisible ? kbOffset : 0
|
|
139
|
+
document.documentElement.style.setProperty('--shortcut-bar-kb-offset', offset + 'px')
|
|
140
|
+
store.shortcutBarKbOffset = offset
|
|
141
|
+
}, [kbOffset, store.shortcutBarVisible])
|
|
144
142
|
|
|
145
143
|
// reserve layout space + lift the footer while the bar is shown.
|
|
146
144
|
// expose the bar height as a CSS var so the .styl + footer-lift rule stay
|
|
@@ -79,6 +79,10 @@
|
|
|
79
79
|
.shortcut-bar-on
|
|
80
80
|
.main-footer
|
|
81
81
|
bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
|
|
82
|
+
.remote-monitor-bar
|
|
83
|
+
bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px) + 36px)
|
|
84
|
+
.qm-wrap-tooltip
|
|
85
|
+
bottom calc(var(--footer-stack-height, 36px) + var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
|
|
82
86
|
|
|
83
87
|
// --- edit modal -----------------------------------------------------------
|
|
84
88
|
|