@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
|
@@ -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
|
-
}
|
|
@@ -100,9 +100,17 @@ const modifierOptions = [
|
|
|
100
100
|
{ id: 'meta', label: 'Meta' }
|
|
101
101
|
]
|
|
102
102
|
|
|
103
|
+
const MODIFIER_IDS = new Set(modifierOptions.map(m => m.id))
|
|
104
|
+
|
|
105
|
+
// the builder's second slot accepts either a modifier (Ctrl/Alt/Shift/Meta)
|
|
106
|
+
// or a bare key, so we need to tell them apart.
|
|
107
|
+
function isModifier (id) {
|
|
108
|
+
return MODIFIER_IDS.has(id)
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
// compose the bytes to send for a (mods, key) combo.
|
|
104
112
|
// Ctrl+letter -> the control byte; Alt -> ESC prefix; Shift+letter -> uppercase
|
|
105
|
-
function
|
|
113
|
+
function applyMods (mods, key) {
|
|
106
114
|
const ctrl = mods.includes('ctrl')
|
|
107
115
|
const alt = mods.includes('alt')
|
|
108
116
|
const shift = mods.includes('shift')
|
|
@@ -118,10 +126,27 @@ function buildComboData (mods, key) {
|
|
|
118
126
|
return data
|
|
119
127
|
}
|
|
120
128
|
|
|
121
|
-
//
|
|
122
|
-
|
|
129
|
+
// `prefix` is an optional first key typed BEFORE `key` — that's how you build
|
|
130
|
+
// two-step combos such as tmux's Ctrl+A then D: the modifiers apply to the
|
|
131
|
+
// prefix only, the trailing key is sent plain.
|
|
132
|
+
// mods [ctrl], prefix A, key D -> Ctrl+A then D
|
|
133
|
+
// mods [ctrl, shift], key D -> Ctrl+Shift+D
|
|
134
|
+
function buildComboData (mods, key, prefix) {
|
|
135
|
+
if (prefix) {
|
|
136
|
+
return applyMods(mods, prefix) + key.data
|
|
137
|
+
}
|
|
138
|
+
return applyMods(mods, key)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// human-readable label, e.g.
|
|
142
|
+
// mods ['ctrl','shift'] + key A -> "Ctrl+Shift+A"
|
|
143
|
+
// mods ['ctrl'] + prefix A + key D -> "Ctrl+A D"
|
|
144
|
+
function buildComboLabel (mods, key, prefix) {
|
|
123
145
|
const byId = new Map(modifierOptions.map(m => [m.id, m.label]))
|
|
124
|
-
|
|
146
|
+
const head = mods.map(m => byId.get(m) || m)
|
|
147
|
+
return prefix
|
|
148
|
+
? [...head, prefix.label].join('+') + ' ' + key.label
|
|
149
|
+
: [...head, key.label].join('+')
|
|
125
150
|
}
|
|
126
151
|
|
|
127
152
|
// full library, de-duplicated by id
|
|
@@ -196,6 +221,7 @@ export {
|
|
|
196
221
|
candidates,
|
|
197
222
|
keyOptions,
|
|
198
223
|
modifierOptions,
|
|
224
|
+
isModifier,
|
|
199
225
|
buildComboData,
|
|
200
226
|
buildComboLabel
|
|
201
227
|
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
defaultActive,
|
|
27
27
|
keyOptions,
|
|
28
28
|
modifierOptions,
|
|
29
|
+
isModifier,
|
|
29
30
|
buildComboData,
|
|
30
31
|
buildComboLabel
|
|
31
32
|
} from './shortcut-bar-defs'
|
|
@@ -43,6 +44,9 @@ export default function ShortcutBarEdit (props) {
|
|
|
43
44
|
|
|
44
45
|
const [keyword, setKeyword] = useState('')
|
|
45
46
|
const [customMod1, setCustomMod1] = useState(undefined)
|
|
47
|
+
// second slot takes a modifier OR a bare key: a modifier stacks onto the
|
|
48
|
+
// first one (Ctrl+Shift+D), a key becomes a prefix typed before the third
|
|
49
|
+
// slot's key (Ctrl+A then D).
|
|
46
50
|
const [customMod2, setCustomMod2] = useState(undefined)
|
|
47
51
|
const [customKey, setCustomKey] = useState(undefined)
|
|
48
52
|
|
|
@@ -70,13 +74,21 @@ export default function ShortcutBarEdit (props) {
|
|
|
70
74
|
if (!customKey) {
|
|
71
75
|
return
|
|
72
76
|
}
|
|
73
|
-
const mods = [customMod1, customMod2].filter(Boolean)
|
|
74
77
|
const key = keyOptions.find(k => k.id === customKey)
|
|
75
78
|
if (!key) {
|
|
76
79
|
return
|
|
77
80
|
}
|
|
78
|
-
const
|
|
79
|
-
|
|
81
|
+
const mods = [customMod1].filter(Boolean)
|
|
82
|
+
let prefix
|
|
83
|
+
if (customMod2) {
|
|
84
|
+
if (isModifier(customMod2)) {
|
|
85
|
+
mods.push(customMod2)
|
|
86
|
+
} else {
|
|
87
|
+
prefix = keyOptions.find(k => k.id === customMod2)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const label = buildComboLabel(mods, key, prefix)
|
|
91
|
+
const data = buildComboData(mods, key, prefix)
|
|
80
92
|
const id = 'custom-' + label + '-' + Date.now()
|
|
81
93
|
onSave([...active, { id, label, data, custom: true }])
|
|
82
94
|
setCustomMod1(undefined)
|
|
@@ -88,6 +100,26 @@ export default function ShortcutBarEdit (props) {
|
|
|
88
100
|
onSave(defaultActive())
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
// second slot option list: modifiers first, then the same bare-key list the
|
|
104
|
+
// third slot offers (so Ctrl+A then D is expressible).
|
|
105
|
+
const secondOptions = [
|
|
106
|
+
{
|
|
107
|
+
label: e('modifier'),
|
|
108
|
+
options: modifierOptions.map(m => ({
|
|
109
|
+
value: m.id,
|
|
110
|
+
label: m.label,
|
|
111
|
+
disabled: m.id === customMod1
|
|
112
|
+
}))
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
label: e('key'),
|
|
116
|
+
options: keyOptions.map(k => ({
|
|
117
|
+
value: k.id,
|
|
118
|
+
label: k.label
|
|
119
|
+
}))
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
|
|
91
123
|
const kw = keyword.trim().toLowerCase()
|
|
92
124
|
const filtered = allCandidates.filter(b => {
|
|
93
125
|
if (!kw) {
|
|
@@ -195,15 +227,13 @@ export default function ShortcutBarEdit (props) {
|
|
|
195
227
|
<Select
|
|
196
228
|
value={customMod2}
|
|
197
229
|
onChange={setCustomMod2}
|
|
198
|
-
placeholder={e('modifier') + ' (' + e('optional') + ')'}
|
|
230
|
+
placeholder={e('modifier') + '/' + e('key') + ' (' + e('optional') + ')'}
|
|
199
231
|
className='shortcut-custom-select'
|
|
200
232
|
allowClear
|
|
233
|
+
showSearch
|
|
234
|
+
optionFilterProp='label'
|
|
201
235
|
getPopupContainer={popupContainer}
|
|
202
|
-
options={
|
|
203
|
-
value: m.id,
|
|
204
|
-
label: m.label,
|
|
205
|
-
disabled: m.id === customMod1
|
|
206
|
-
}))}
|
|
236
|
+
options={secondOptions}
|
|
207
237
|
/>
|
|
208
238
|
<Select
|
|
209
239
|
value={customKey}
|
|
@@ -212,6 +242,7 @@ export default function ShortcutBarEdit (props) {
|
|
|
212
242
|
className='shortcut-custom-select shortcut-custom-key'
|
|
213
243
|
allowClear
|
|
214
244
|
showSearch
|
|
245
|
+
optionFilterProp='label'
|
|
215
246
|
getPopupContainer={popupContainer}
|
|
216
247
|
options={keyOptions.map(k => ({
|
|
217
248
|
value: k.id,
|
|
@@ -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
|
|
|
@@ -56,11 +56,20 @@ function sendToTerminal (data) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
// honor the active terminal's DECCKM state for the cursor / Home / End keys
|
|
59
|
+
// honor the active terminal's DECCKM state for the cursor / Home / End keys.
|
|
60
|
+
// custom combos can be multi-key sequences (Ctrl+A then ↑), so match the
|
|
61
|
+
// trailing key too — not only the whole payload.
|
|
60
62
|
function resolveCursorMode (data, term) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
if (!term?.term?.modes?.applicationCursorKeysMode) {
|
|
64
|
+
return data
|
|
65
|
+
}
|
|
66
|
+
for (const normal in appCursorMap) {
|
|
67
|
+
if (data === normal) {
|
|
68
|
+
return appCursorMap[normal]
|
|
69
|
+
}
|
|
70
|
+
if (data.length > normal.length && data.endsWith(normal)) {
|
|
71
|
+
return data.slice(0, data.length - normal.length) + appCursorMap[normal]
|
|
72
|
+
}
|
|
64
73
|
}
|
|
65
74
|
return data
|
|
66
75
|
}
|
|
@@ -135,12 +144,10 @@ function ShortcutBar (props) {
|
|
|
135
144
|
// keep the offset in a CSS var consumed by shortcut-bar.styl, and expose the
|
|
136
145
|
// lifted height so layout.jsx can shrink the terminal accordingly.
|
|
137
146
|
useEffect(() => {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
store.shortcutBarKbOffset = kbOffset
|
|
143
|
-
}, [kbOffset])
|
|
147
|
+
const offset = store.shortcutBarVisible ? kbOffset : 0
|
|
148
|
+
document.documentElement.style.setProperty('--shortcut-bar-kb-offset', offset + 'px')
|
|
149
|
+
store.shortcutBarKbOffset = offset
|
|
150
|
+
}, [kbOffset, store.shortcutBarVisible])
|
|
144
151
|
|
|
145
152
|
// reserve layout space + lift the footer while the bar is shown.
|
|
146
153
|
// 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
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const maxCwdLength = 4096
|
|
2
|
+
|
|
3
|
+
// Shell-backed terminal types whose cwd + screen can be restored on reload.
|
|
4
|
+
// Graphical / file-transfer tabs (rdp, vnc, web, ftp, spice, sftp, ...) are
|
|
5
|
+
// intentionally excluded: they have no shell cwd to `cd` back to.
|
|
6
|
+
export const restorableTerminalTypes = new Set([
|
|
7
|
+
'ssh',
|
|
8
|
+
'local',
|
|
9
|
+
'telnet',
|
|
10
|
+
'serial'
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
export function isSshTab (tab = {}) {
|
|
14
|
+
return !!tab.host && (!tab.type || tab.type === 'ssh')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isRestorableTerminalTab (tab = {}) {
|
|
18
|
+
if (!tab.type) {
|
|
19
|
+
// Legacy tabs without an explicit type are ssh (host set) or local
|
|
20
|
+
// (no host) - both are shell terminals.
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
return restorableTerminalTypes.has(tab.type)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sanitizeSshCwd (cwd) {
|
|
27
|
+
if (typeof cwd !== 'string' || !cwd || cwd.length > maxCwdLength) {
|
|
28
|
+
return ''
|
|
29
|
+
}
|
|
30
|
+
if (/[\0\r\n]/.test(cwd)) {
|
|
31
|
+
return ''
|
|
32
|
+
}
|
|
33
|
+
return cwd
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const sanitizeTerminalCwd = sanitizeSshCwd
|
|
37
|
+
|
|
38
|
+
export function quotePosixShellArg (value) {
|
|
39
|
+
const quote = '\''
|
|
40
|
+
const escapedQuote = quote + '"' + quote + '"' + quote
|
|
41
|
+
return quote + value.replace(/'/g, escapedQuote) + quote
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createRestoreCwdCommand (cwd) {
|
|
45
|
+
const safeCwd = sanitizeSshCwd(cwd)
|
|
46
|
+
return safeCwd ? `cd -- ${quotePosixShellArg(safeCwd)}` : ''
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createWindowsRestoreCwdCommand (cwd) {
|
|
50
|
+
const safeCwd = sanitizeSshCwd(cwd)
|
|
51
|
+
if (!safeCwd) {
|
|
52
|
+
return ''
|
|
53
|
+
}
|
|
54
|
+
// Strip control chars already handled by sanitize; escape embedded double
|
|
55
|
+
// quotes for cmd/powershell (`""` is accepted by both as a literal quote).
|
|
56
|
+
const escaped = safeCwd.replace(/"/g, '""')
|
|
57
|
+
return `cd /d "${escaped}"`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function shouldCaptureTerminalReloadState (tab, config = {}) {
|
|
61
|
+
return !!config.restoreTerminalSessionOnReload && isRestorableTerminalTab(tab)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Kept for backward compatibility - now covers all shell terminals
|
|
65
|
+
// (ssh, local, telnet, serial), not just ssh.
|
|
66
|
+
export function shouldCaptureSshReloadState (tab, config = {}) {
|
|
67
|
+
return shouldCaptureTerminalReloadState(tab, config)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createSshReloadState ({ cwd, screen } = {}) {
|
|
71
|
+
const safeCwd = sanitizeSshCwd(cwd)
|
|
72
|
+
const safeScreen = typeof screen === 'string' ? screen : ''
|
|
73
|
+
if (!safeCwd && !safeScreen) {
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
cwd: safeCwd,
|
|
78
|
+
screen: safeScreen
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const createTerminalReloadState = createSshReloadState
|
|
83
|
+
|
|
84
|
+
export function getAlternateBufferSnapshot (buffer) {
|
|
85
|
+
if (!buffer || buffer.type !== 'alternate') {
|
|
86
|
+
return ''
|
|
87
|
+
}
|
|
88
|
+
const lines = []
|
|
89
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
90
|
+
lines.push(buffer.getLine(i)?.translateToString(true) || '')
|
|
91
|
+
}
|
|
92
|
+
while (lines.length && !lines[lines.length - 1]) {
|
|
93
|
+
lines.pop()
|
|
94
|
+
}
|
|
95
|
+
return lines.join('\r\n')
|
|
96
|
+
}
|