dsh-ssh-manage 0.3.0 → 0.3.2
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.en.md +10 -9
- package/README.md +10 -9
- package/assets/LICENSE.addon-search +19 -0
- package/assets/addon-search.js +2 -0
- package/client.js +570 -27
- package/index.js +23 -2
- package/lib/ssh-client.js +87 -2
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -161,12 +161,40 @@ window.__ModuleLoader__.load({
|
|
|
161
161
|
'terminal.openDetails': ['打开主机详情', 'Open details'],
|
|
162
162
|
'terminal.assetsError': ['终端组件加载失败:{message}。如果刚更新插件,请重启 DSH 后重试。', 'Could not load the terminal component: {message}. If the plugin was just updated, restart DSH and retry.'],
|
|
163
163
|
'terminal.resyncFailed': ['终端输出曾中断,已尝试重新同步;如仍异常请重连。', 'Terminal output was interrupted; attempted a resync. Reconnect if problems persist.'],
|
|
164
|
+
'terminal.search': ['搜索终端', 'Search terminal'],
|
|
165
|
+
'terminal.searchNext': ['下一个', 'Next match'],
|
|
166
|
+
'terminal.searchPrevious': ['上一个', 'Previous match'],
|
|
167
|
+
'terminal.searchCase': ['区分大小写', 'Match case'],
|
|
168
|
+
'terminal.searchClose': ['关闭搜索', 'Close search'],
|
|
169
|
+
'terminal.fontSmaller': ['缩小字号', 'Smaller terminal font'],
|
|
170
|
+
'terminal.fontLarger': ['放大字号', 'Larger terminal font'],
|
|
171
|
+
'terminal.menu.copy': ['复制', 'Copy'],
|
|
172
|
+
'terminal.menu.paste': ['粘贴', 'Paste'],
|
|
173
|
+
'terminal.menu.selectAll': ['全选', 'Select all'],
|
|
174
|
+
'terminal.menu.clear': ['清屏', 'Clear'],
|
|
175
|
+
'terminal.menu.title': ['终端菜单', 'Terminal menu'],
|
|
176
|
+
'terminal.searchUnavailable': ['终端搜索加载失败,请重启 DSH 后重试。', 'Terminal search failed to load; restart DSH and retry.'],
|
|
177
|
+
'terminal.copyFailed': ['复制失败,请检查浏览器剪贴板权限。', 'Copy failed; check browser clipboard permissions.'],
|
|
178
|
+
'terminal.pasteFailed': ['粘贴失败,请检查浏览器剪贴板权限。', 'Paste failed; check browser clipboard permissions.'],
|
|
164
179
|
'terminal.backendHint': ['无法连接到插件后端。如果刚更新插件,请重启 DSH 后重试。', 'Cannot reach the plugin backend. If the plugin was just updated, restart DSH and retry.'],
|
|
165
180
|
'terminal.passwordMissing': ['该主机还没有保存密码,请先打开「主机详情」设置服务器密码。', 'No password stored for this host. Open Details and set the server password first.'],
|
|
166
181
|
'drawer.exit': ['退出码 {code}', 'exit {code}'],
|
|
167
182
|
'drawer.files.path': ['远程路径', 'Remote path'],
|
|
168
183
|
'drawer.files.up': ['上级目录', 'Parent directory'],
|
|
169
184
|
'drawer.files.empty': ['目录暂无内容。', 'This directory is empty.'],
|
|
185
|
+
'drawer.files.mkdir': ['新建文件夹', 'New folder'],
|
|
186
|
+
'drawer.files.mkdirTitle': ['新建文件夹', 'New folder'],
|
|
187
|
+
'drawer.files.rename': ['重命名', 'Rename'],
|
|
188
|
+
'drawer.files.renameTitle': ['重命名', 'Rename'],
|
|
189
|
+
'drawer.files.delete': ['删除', 'Delete'],
|
|
190
|
+
'drawer.files.deleteTitle': ['删除', 'Delete'],
|
|
191
|
+
'drawer.files.deleteConfirm': ['确定删除“{name}”?删除目录时要求目录为空。', 'Delete "{name}"? Directories must be empty.'],
|
|
192
|
+
'drawer.files.name': ['名称', 'Name'],
|
|
193
|
+
'drawer.files.invalidName': ['名称不能为空,也不能包含 / 或 \\。', 'Name must be non-empty and cannot contain / or \\.'],
|
|
194
|
+
'drawer.files.created': ['文件夹已创建', 'Folder created'],
|
|
195
|
+
'drawer.files.renamed': ['已重命名', 'Renamed'],
|
|
196
|
+
'drawer.files.deleted': ['已删除', 'Deleted'],
|
|
197
|
+
'common.create': ['新建', 'Create'],
|
|
170
198
|
'drawer.files.dir': ['目录', 'dir'],
|
|
171
199
|
'drawer.files.uploading': ['上传中…', 'Uploading…'],
|
|
172
200
|
'drawer.details.connection': ['连接信息', 'Connection'],
|
|
@@ -778,7 +806,7 @@ window.__ModuleLoader__.load({
|
|
|
778
806
|
return h('span', { className: 'ssh-spinner', style: { width: size, height: size } })
|
|
779
807
|
}
|
|
780
808
|
|
|
781
|
-
function Button({ children, variant, size, onClick, disabled, title, type = 'button', className, ariaPressed }) {
|
|
809
|
+
function Button({ children, variant, size, onClick, disabled, title, type = 'button', className, ariaPressed, autoFocus }) {
|
|
782
810
|
return h(
|
|
783
811
|
'button',
|
|
784
812
|
{
|
|
@@ -788,6 +816,7 @@ window.__ModuleLoader__.load({
|
|
|
788
816
|
disabled,
|
|
789
817
|
title,
|
|
790
818
|
'aria-pressed': ariaPressed,
|
|
819
|
+
autoFocus,
|
|
791
820
|
},
|
|
792
821
|
children,
|
|
793
822
|
)
|
|
@@ -1152,6 +1181,25 @@ window.__ModuleLoader__.load({
|
|
|
1152
1181
|
}
|
|
1153
1182
|
}
|
|
1154
1183
|
|
|
1184
|
+
const TERMINAL_FONT_KEY = 'dsh-ssh-manage:terminal-font-size'
|
|
1185
|
+
const TERMINAL_FONT_MIN = 10
|
|
1186
|
+
const TERMINAL_FONT_MAX = 20
|
|
1187
|
+
|
|
1188
|
+
function clampTerminalFontSize(value) {
|
|
1189
|
+
const numeric = Number(value)
|
|
1190
|
+
if (!Number.isFinite(numeric)) return 13
|
|
1191
|
+
return Math.min(TERMINAL_FONT_MAX, Math.max(TERMINAL_FONT_MIN, Math.round(numeric)))
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function readTerminalFontSize() {
|
|
1195
|
+
try {
|
|
1196
|
+
const stored = window.localStorage.getItem(TERMINAL_FONT_KEY)
|
|
1197
|
+
return stored === null ? 13 : clampTerminalFontSize(stored)
|
|
1198
|
+
} catch {
|
|
1199
|
+
return 13
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1155
1203
|
function App({ ctx }) {
|
|
1156
1204
|
const t = useT(ctx)
|
|
1157
1205
|
const localeId = useLocaleId(ctx)
|
|
@@ -1187,6 +1235,7 @@ window.__ModuleLoader__.load({
|
|
|
1187
1235
|
)
|
|
1188
1236
|
const [activeSessionId, setActiveSessionId] = React.useState(() => persistentPanelState.activeSessionId)
|
|
1189
1237
|
const [treeWidth, setTreeWidth] = React.useState(readTreeWidth)
|
|
1238
|
+
const [terminalFontSize, setTerminalFontSize] = React.useState(readTerminalFontSize)
|
|
1190
1239
|
const [clearSignals, setClearSignals] = React.useState({})
|
|
1191
1240
|
const [sessionStates, setSessionStates] = React.useState({})
|
|
1192
1241
|
const toastTimer = React.useRef(null)
|
|
@@ -1226,6 +1275,12 @@ window.__ModuleLoader__.load({
|
|
|
1226
1275
|
} catch {}
|
|
1227
1276
|
}, [treeWidth])
|
|
1228
1277
|
|
|
1278
|
+
React.useEffect(() => {
|
|
1279
|
+
try {
|
|
1280
|
+
window.localStorage.setItem(TERMINAL_FONT_KEY, String(terminalFontSize))
|
|
1281
|
+
} catch {}
|
|
1282
|
+
}, [terminalFontSize])
|
|
1283
|
+
|
|
1229
1284
|
React.useEffect(() => {
|
|
1230
1285
|
for (const session of sessions) ptyActiveIds.add(session.id)
|
|
1231
1286
|
}, [sessions])
|
|
@@ -1717,6 +1772,8 @@ window.__ModuleLoader__.load({
|
|
|
1717
1772
|
groupsCloseGuard,
|
|
1718
1773
|
treeWidth,
|
|
1719
1774
|
setTreeWidth,
|
|
1775
|
+
terminalFontSize,
|
|
1776
|
+
setTerminalFontSize,
|
|
1720
1777
|
sessions,
|
|
1721
1778
|
activeSessionId,
|
|
1722
1779
|
sessionStates,
|
|
@@ -2833,6 +2890,24 @@ window.__ModuleLoader__.load({
|
|
|
2833
2890
|
},
|
|
2834
2891
|
h(IconServer, { size: 15 }),
|
|
2835
2892
|
),
|
|
2893
|
+
h(
|
|
2894
|
+
IconButton,
|
|
2895
|
+
{
|
|
2896
|
+
title: t('terminal.fontSmaller'),
|
|
2897
|
+
disabled: model.terminalFontSize <= TERMINAL_FONT_MIN,
|
|
2898
|
+
onClick: () => model.setTerminalFontSize((value) => Math.max(TERMINAL_FONT_MIN, value - 1)),
|
|
2899
|
+
},
|
|
2900
|
+
h('span', { className: 'ssh-fontGlyph' }, 'A−'),
|
|
2901
|
+
),
|
|
2902
|
+
h(
|
|
2903
|
+
IconButton,
|
|
2904
|
+
{
|
|
2905
|
+
title: t('terminal.fontLarger'),
|
|
2906
|
+
disabled: model.terminalFontSize >= TERMINAL_FONT_MAX,
|
|
2907
|
+
onClick: () => model.setTerminalFontSize((value) => Math.min(TERMINAL_FONT_MAX, value + 1)),
|
|
2908
|
+
},
|
|
2909
|
+
h('span', { className: 'ssh-fontGlyph' }, 'A+'),
|
|
2910
|
+
),
|
|
2836
2911
|
h(
|
|
2837
2912
|
IconButton,
|
|
2838
2913
|
{
|
|
@@ -2869,6 +2944,7 @@ window.__ModuleLoader__.load({
|
|
|
2869
2944
|
model,
|
|
2870
2945
|
host,
|
|
2871
2946
|
sessionId: session.id,
|
|
2947
|
+
fontSize: model.terminalFontSize,
|
|
2872
2948
|
clearSignal: clearSignals[session.id] ?? 0,
|
|
2873
2949
|
active: isVisible && session.id === activeSession.id,
|
|
2874
2950
|
})
|
|
@@ -2969,6 +3045,14 @@ window.__ModuleLoader__.load({
|
|
|
2969
3045
|
css: 'dsh-ssh-manage-xterm-css',
|
|
2970
3046
|
script: 'dsh-ssh-manage-xterm-script',
|
|
2971
3047
|
fit: 'dsh-ssh-manage-xterm-fit',
|
|
3048
|
+
search: 'dsh-ssh-manage-xterm-search',
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
function terminalAssetReady(id) {
|
|
3052
|
+
if (id === TERMINAL_ASSET_IDS.script) return Boolean(window.Terminal)
|
|
3053
|
+
if (id === TERMINAL_ASSET_IDS.fit) return Boolean(window.FitAddon && window.FitAddon.FitAddon)
|
|
3054
|
+
if (id === TERMINAL_ASSET_IDS.search) return Boolean(window.SearchAddon && window.SearchAddon.SearchAddon)
|
|
3055
|
+
return false
|
|
2972
3056
|
}
|
|
2973
3057
|
let terminalAssetsPromise = null
|
|
2974
3058
|
|
|
@@ -2976,10 +3060,7 @@ window.__ModuleLoader__.load({
|
|
|
2976
3060
|
return new Promise((resolve, reject) => {
|
|
2977
3061
|
const existing = document.getElementById(id)
|
|
2978
3062
|
if (existing) {
|
|
2979
|
-
const ready =
|
|
2980
|
-
id === TERMINAL_ASSET_IDS.script
|
|
2981
|
-
? Boolean(window.Terminal)
|
|
2982
|
-
: Boolean(window.FitAddon && window.FitAddon.FitAddon)
|
|
3063
|
+
const ready = terminalAssetReady(id)
|
|
2983
3064
|
if (ready) {
|
|
2984
3065
|
resolve()
|
|
2985
3066
|
return
|
|
@@ -3003,8 +3084,12 @@ window.__ModuleLoader__.load({
|
|
|
3003
3084
|
}
|
|
3004
3085
|
|
|
3005
3086
|
function loadTerminalAssets() {
|
|
3006
|
-
if (window.Terminal && window.FitAddon && window.FitAddon.FitAddon) {
|
|
3007
|
-
return Promise.resolve({
|
|
3087
|
+
if (window.Terminal && window.FitAddon && window.FitAddon.FitAddon && window.SearchAddon?.SearchAddon) {
|
|
3088
|
+
return Promise.resolve({
|
|
3089
|
+
Terminal: window.Terminal,
|
|
3090
|
+
FitAddon: window.FitAddon.FitAddon,
|
|
3091
|
+
SearchAddon: window.SearchAddon.SearchAddon,
|
|
3092
|
+
})
|
|
3008
3093
|
}
|
|
3009
3094
|
if (terminalAssetsPromise) return terminalAssetsPromise
|
|
3010
3095
|
if (!document.getElementById(TERMINAL_ASSET_IDS.css)) {
|
|
@@ -3016,9 +3101,17 @@ window.__ModuleLoader__.load({
|
|
|
3016
3101
|
}
|
|
3017
3102
|
terminalAssetsPromise = loadScriptAsset(TERMINAL_ASSET_IDS.script, '/dsh-ssh-manage/assets/xterm.js')
|
|
3018
3103
|
.then(() => loadScriptAsset(TERMINAL_ASSET_IDS.fit, '/dsh-ssh-manage/assets/addon-fit.js'))
|
|
3104
|
+
// Search is optional: a missing/blocked addon must not break the terminal.
|
|
3105
|
+
.then(() => loadScriptAsset(TERMINAL_ASSET_IDS.search, '/dsh-ssh-manage/assets/addon-search.js').catch(() => null))
|
|
3019
3106
|
.then(() => {
|
|
3020
|
-
if (!window.Terminal || !window.FitAddon || !window.FitAddon.FitAddon)
|
|
3021
|
-
|
|
3107
|
+
if (!window.Terminal || !window.FitAddon || !window.FitAddon.FitAddon) {
|
|
3108
|
+
throw new Error('Terminal assets did not initialize')
|
|
3109
|
+
}
|
|
3110
|
+
return {
|
|
3111
|
+
Terminal: window.Terminal,
|
|
3112
|
+
FitAddon: window.FitAddon.FitAddon,
|
|
3113
|
+
SearchAddon: window.SearchAddon?.SearchAddon ?? null,
|
|
3114
|
+
}
|
|
3022
3115
|
})
|
|
3023
3116
|
.catch((error) => {
|
|
3024
3117
|
terminalAssetsPromise = null
|
|
@@ -3315,6 +3408,29 @@ window.__ModuleLoader__.load({
|
|
|
3315
3408
|
void ptyPost(PTY_CLEAR_ROUTE, { id }, 15_000).catch(() => {})
|
|
3316
3409
|
}
|
|
3317
3410
|
|
|
3411
|
+
async function copyTextToClipboard(text) {
|
|
3412
|
+
if (!text) return false
|
|
3413
|
+
try {
|
|
3414
|
+
if (navigator.clipboard?.writeText) {
|
|
3415
|
+
await navigator.clipboard.writeText(text)
|
|
3416
|
+
return true
|
|
3417
|
+
}
|
|
3418
|
+
} catch {}
|
|
3419
|
+
try {
|
|
3420
|
+
const textarea = document.createElement('textarea')
|
|
3421
|
+
textarea.value = text
|
|
3422
|
+
textarea.style.position = 'fixed'
|
|
3423
|
+
textarea.style.opacity = '0'
|
|
3424
|
+
document.body.appendChild(textarea)
|
|
3425
|
+
textarea.select()
|
|
3426
|
+
const copied = document.execCommand('copy')
|
|
3427
|
+
textarea.remove()
|
|
3428
|
+
return copied
|
|
3429
|
+
} catch {
|
|
3430
|
+
return false
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3318
3434
|
function decodeBase64(text) {
|
|
3319
3435
|
const binary = atob(String(text ?? ''))
|
|
3320
3436
|
const bytes = new Uint8Array(binary.length)
|
|
@@ -3322,15 +3438,25 @@ window.__ModuleLoader__.load({
|
|
|
3322
3438
|
return bytes
|
|
3323
3439
|
}
|
|
3324
3440
|
|
|
3325
|
-
function TerminalPane({ model, host, sessionId, active, clearSignal }) {
|
|
3441
|
+
function TerminalPane({ model, host, sessionId, active, clearSignal, fontSize }) {
|
|
3326
3442
|
const { t } = model
|
|
3327
3443
|
const hostRef = React.useRef(host)
|
|
3328
3444
|
hostRef.current = host
|
|
3329
3445
|
const containerRef = React.useRef(null)
|
|
3330
3446
|
const termRef = React.useRef(null)
|
|
3331
3447
|
const fitRef = React.useRef(null)
|
|
3448
|
+
const searchAddonRef = React.useRef(null)
|
|
3449
|
+
const searchInputRef = React.useRef(null)
|
|
3450
|
+
const menuRef = React.useRef(null)
|
|
3451
|
+
const fontSizeRef = React.useRef(fontSize)
|
|
3452
|
+
fontSizeRef.current = fontSize
|
|
3332
3453
|
const activeRef = React.useRef(active === true)
|
|
3333
3454
|
const [status, setStatus] = React.useState('loading')
|
|
3455
|
+
const [searchOpen, setSearchOpen] = React.useState(false)
|
|
3456
|
+
const searchOpenRef = React.useRef(false)
|
|
3457
|
+
const [searchText, setSearchText] = React.useState('')
|
|
3458
|
+
const [searchCase, setSearchCase] = React.useState(false)
|
|
3459
|
+
const [menu, setMenu] = React.useState(null)
|
|
3334
3460
|
const [message, setMessage] = React.useState('')
|
|
3335
3461
|
const [retryToken, setRetryToken] = React.useState(0)
|
|
3336
3462
|
const clearRequested = React.useRef(false)
|
|
@@ -3370,6 +3496,131 @@ window.__ModuleLoader__.load({
|
|
|
3370
3496
|
} catch {}
|
|
3371
3497
|
}, [clearSignal])
|
|
3372
3498
|
|
|
3499
|
+
React.useEffect(() => {
|
|
3500
|
+
const term = termRef.current
|
|
3501
|
+
if (!term) return
|
|
3502
|
+
try {
|
|
3503
|
+
if (term.options.fontSize !== fontSize) {
|
|
3504
|
+
term.options.fontSize = fontSize
|
|
3505
|
+
fitRef.current?.fit()
|
|
3506
|
+
}
|
|
3507
|
+
} catch {}
|
|
3508
|
+
}, [fontSize])
|
|
3509
|
+
|
|
3510
|
+
const openSearch = () => {
|
|
3511
|
+
if (!searchAddonRef.current) {
|
|
3512
|
+
try { model.notify?.(t('terminal.searchUnavailable'), 'error') } catch {}
|
|
3513
|
+
return
|
|
3514
|
+
}
|
|
3515
|
+
searchOpenRef.current = true
|
|
3516
|
+
setSearchOpen(true)
|
|
3517
|
+
requestAnimationFrame(() => searchInputRef.current?.focus())
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
const closeSearch = () => {
|
|
3521
|
+
searchOpenRef.current = false
|
|
3522
|
+
setSearchOpen(false)
|
|
3523
|
+
try { searchAddonRef.current?.clearDecorations?.() } catch {}
|
|
3524
|
+
try { termRef.current?.focus() } catch {}
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
const findInTerminal = (direction, caseSensitive = searchCase) => {
|
|
3528
|
+
const addon = searchAddonRef.current
|
|
3529
|
+
const text = searchText.trim()
|
|
3530
|
+
if (!addon) return
|
|
3531
|
+
try {
|
|
3532
|
+
if (text === '') addon.clearDecorations?.()
|
|
3533
|
+
else if (direction < 0) addon.findPrevious(text, { caseSensitive })
|
|
3534
|
+
else addon.findNext(text, { caseSensitive })
|
|
3535
|
+
} catch {}
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
const updateSearchText = (value) => {
|
|
3539
|
+
setSearchText(value)
|
|
3540
|
+
const addon = searchAddonRef.current
|
|
3541
|
+
if (!addon) return
|
|
3542
|
+
try {
|
|
3543
|
+
if (value.trim() === '') addon.clearDecorations?.()
|
|
3544
|
+
else addon.findNext(value.trim(), { caseSensitive: searchCase })
|
|
3545
|
+
} catch {}
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
const terminalMenuAction = (action) => {
|
|
3549
|
+
const term = termRef.current
|
|
3550
|
+
setMenu(null)
|
|
3551
|
+
if (!term) return
|
|
3552
|
+
const refocus = () => {
|
|
3553
|
+
requestAnimationFrame(() => {
|
|
3554
|
+
try { termRef.current?.focus() } catch {}
|
|
3555
|
+
})
|
|
3556
|
+
}
|
|
3557
|
+
if (action === 'selectAll') {
|
|
3558
|
+
try { term.selectAll() } catch {}
|
|
3559
|
+
refocus()
|
|
3560
|
+
return
|
|
3561
|
+
}
|
|
3562
|
+
if (action === 'clear') {
|
|
3563
|
+
// Route through App so clearRequested/clearEpoch and the host buffer
|
|
3564
|
+
// stay in sync with the session-bar clear button.
|
|
3565
|
+
if (typeof model.clearSession === 'function') model.clearSession(sessionId)
|
|
3566
|
+
else clearPtySession(sessionId)
|
|
3567
|
+
try { term.clear(); term.write('\x1b[H\x1b[2J') } catch {}
|
|
3568
|
+
refocus()
|
|
3569
|
+
return
|
|
3570
|
+
}
|
|
3571
|
+
if (action === 'copy') {
|
|
3572
|
+
const text = term.getSelection()
|
|
3573
|
+
void copyTextToClipboard(text).then((ok) => {
|
|
3574
|
+
if (!ok) {
|
|
3575
|
+
try { model.notify?.(t('terminal.copyFailed'), 'error') } catch {}
|
|
3576
|
+
}
|
|
3577
|
+
refocus()
|
|
3578
|
+
})
|
|
3579
|
+
return
|
|
3580
|
+
}
|
|
3581
|
+
if (action === 'paste') {
|
|
3582
|
+
if (!navigator.clipboard?.readText) {
|
|
3583
|
+
try { model.notify?.(t('terminal.pasteFailed'), 'error') } catch {}
|
|
3584
|
+
refocus()
|
|
3585
|
+
return
|
|
3586
|
+
}
|
|
3587
|
+
navigator.clipboard.readText().then((text) => {
|
|
3588
|
+
if (text) {
|
|
3589
|
+
try { term.paste(text) } catch {}
|
|
3590
|
+
}
|
|
3591
|
+
refocus()
|
|
3592
|
+
}).catch(() => {
|
|
3593
|
+
try { model.notify?.(t('terminal.pasteFailed'), 'error') } catch {}
|
|
3594
|
+
refocus()
|
|
3595
|
+
})
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
React.useEffect(() => {
|
|
3600
|
+
if (!menu) return undefined
|
|
3601
|
+
const close = () => setMenu(null)
|
|
3602
|
+
const onKey = (event) => {
|
|
3603
|
+
if (event.key !== 'Escape') return
|
|
3604
|
+
event.preventDefault()
|
|
3605
|
+
event.stopPropagation()
|
|
3606
|
+
setMenu(null)
|
|
3607
|
+
try { termRef.current?.focus() } catch {}
|
|
3608
|
+
}
|
|
3609
|
+
const onDown = (event) => {
|
|
3610
|
+
if (menuRef.current && menuRef.current.contains(event.target)) return
|
|
3611
|
+
setMenu(null)
|
|
3612
|
+
}
|
|
3613
|
+
document.addEventListener('mousedown', onDown)
|
|
3614
|
+
document.addEventListener('keydown', onKey, true)
|
|
3615
|
+
requestAnimationFrame(() => {
|
|
3616
|
+
menuRef.current?.querySelector('button')?.focus()
|
|
3617
|
+
})
|
|
3618
|
+
return () => {
|
|
3619
|
+
document.removeEventListener('mousedown', onDown)
|
|
3620
|
+
document.removeEventListener('keydown', onKey, true)
|
|
3621
|
+
}
|
|
3622
|
+
}, [menu])
|
|
3623
|
+
|
|
3373
3624
|
React.useEffect(() => {
|
|
3374
3625
|
const container = containerRef.current
|
|
3375
3626
|
if (!container) return undefined
|
|
@@ -3434,7 +3685,7 @@ window.__ModuleLoader__.load({
|
|
|
3434
3685
|
term = new Terminal({
|
|
3435
3686
|
cursorBlink: true,
|
|
3436
3687
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
3437
|
-
fontSize: 13,
|
|
3688
|
+
fontSize: fontSizeRef.current ?? 13,
|
|
3438
3689
|
lineHeight: 1.2,
|
|
3439
3690
|
scrollback: 5000,
|
|
3440
3691
|
allowProposedApi: true,
|
|
@@ -3444,9 +3695,39 @@ window.__ModuleLoader__.load({
|
|
|
3444
3695
|
})
|
|
3445
3696
|
fit = new FitAddon()
|
|
3446
3697
|
term.loadAddon(fit)
|
|
3698
|
+
if (assets.SearchAddon) {
|
|
3699
|
+
const searchAddon = new assets.SearchAddon()
|
|
3700
|
+
term.loadAddon(searchAddon)
|
|
3701
|
+
searchAddonRef.current = searchAddon
|
|
3702
|
+
}
|
|
3447
3703
|
term.open(container)
|
|
3448
3704
|
termRef.current = term
|
|
3449
3705
|
fitRef.current = fit
|
|
3706
|
+
term.attachCustomKeyEventHandler((event) => {
|
|
3707
|
+
if (event.type !== 'keydown') return true
|
|
3708
|
+
const meta = event.metaKey || event.ctrlKey
|
|
3709
|
+
if (meta && event.key.toLowerCase() === 'f') {
|
|
3710
|
+
event.preventDefault()
|
|
3711
|
+
openSearch()
|
|
3712
|
+
return false
|
|
3713
|
+
}
|
|
3714
|
+
if (meta && (event.key === '=' || event.key === '+')) {
|
|
3715
|
+
event.preventDefault()
|
|
3716
|
+
model.setTerminalFontSize((value) => Math.min(TERMINAL_FONT_MAX, value + 1))
|
|
3717
|
+
return false
|
|
3718
|
+
}
|
|
3719
|
+
if (meta && (event.key === '-' || event.key === '_')) {
|
|
3720
|
+
event.preventDefault()
|
|
3721
|
+
model.setTerminalFontSize((value) => Math.max(TERMINAL_FONT_MIN, value - 1))
|
|
3722
|
+
return false
|
|
3723
|
+
}
|
|
3724
|
+
if (event.key === 'Escape' && searchOpenRef.current) {
|
|
3725
|
+
event.preventDefault()
|
|
3726
|
+
closeSearch()
|
|
3727
|
+
return false
|
|
3728
|
+
}
|
|
3729
|
+
return true
|
|
3730
|
+
})
|
|
3450
3731
|
try { fit.fit() } catch {}
|
|
3451
3732
|
themeUnsub = subscribeTerminalTheme(() => {
|
|
3452
3733
|
if (removed || !term) return
|
|
@@ -3691,6 +3972,7 @@ window.__ModuleLoader__.load({
|
|
|
3691
3972
|
}
|
|
3692
3973
|
termRef.current = null
|
|
3693
3974
|
fitRef.current = null
|
|
3975
|
+
searchAddonRef.current = null
|
|
3694
3976
|
}
|
|
3695
3977
|
}, [sessionId, retryToken])
|
|
3696
3978
|
|
|
@@ -3698,8 +3980,81 @@ window.__ModuleLoader__.load({
|
|
|
3698
3980
|
const showOverlay = status !== 'ready'
|
|
3699
3981
|
return h(
|
|
3700
3982
|
'div',
|
|
3701
|
-
{
|
|
3983
|
+
{
|
|
3984
|
+
className: 'ssh-terminalPane',
|
|
3985
|
+
onContextMenu: (event) => {
|
|
3986
|
+
if (!termRef.current || status !== 'ready') return
|
|
3987
|
+
if (event.target?.closest?.('.ssh-terminalSearch')) return
|
|
3988
|
+
event.preventDefault()
|
|
3989
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
3990
|
+
setMenu({
|
|
3991
|
+
x: Math.max(0, Math.min(event.clientX - rect.left, rect.width - 170)),
|
|
3992
|
+
y: Math.max(0, Math.min(event.clientY - rect.top, rect.height - 150)),
|
|
3993
|
+
})
|
|
3994
|
+
},
|
|
3995
|
+
onMouseDown: () => setMenu(null),
|
|
3996
|
+
},
|
|
3702
3997
|
h('div', { className: 'ssh-terminalHost', ref: containerRef }),
|
|
3998
|
+
searchOpen
|
|
3999
|
+
? h(
|
|
4000
|
+
'div',
|
|
4001
|
+
{
|
|
4002
|
+
className: 'ssh-terminalSearch',
|
|
4003
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
4004
|
+
},
|
|
4005
|
+
h(IconSearch, { size: 13 }),
|
|
4006
|
+
h('input', {
|
|
4007
|
+
ref: searchInputRef,
|
|
4008
|
+
className: 'ssh-terminalSearchInput',
|
|
4009
|
+
value: searchText,
|
|
4010
|
+
placeholder: t('terminal.search'),
|
|
4011
|
+
'aria-label': t('terminal.search'),
|
|
4012
|
+
onChange: (event) => updateSearchText(event.target.value),
|
|
4013
|
+
onKeyDown: (event) => {
|
|
4014
|
+
event.stopPropagation()
|
|
4015
|
+
if (event.key === 'Escape') closeSearch()
|
|
4016
|
+
else if (event.key === 'Enter') findInTerminal(event.shiftKey ? -1 : 1)
|
|
4017
|
+
},
|
|
4018
|
+
}),
|
|
4019
|
+
h('button', {
|
|
4020
|
+
type: 'button',
|
|
4021
|
+
className: cx('ssh-terminalSearchBtn', searchCase && 'on'),
|
|
4022
|
+
title: t('terminal.searchCase'),
|
|
4023
|
+
'aria-pressed': searchCase,
|
|
4024
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
4025
|
+
onClick: () => {
|
|
4026
|
+
const next = !searchCase
|
|
4027
|
+
setSearchCase(next)
|
|
4028
|
+
if (searchText.trim()) findInTerminal(1, next)
|
|
4029
|
+
},
|
|
4030
|
+
}, 'Aa'),
|
|
4031
|
+
h('button', { type: 'button', className: 'ssh-terminalSearchBtn', title: t('terminal.searchPrevious'), 'aria-label': t('terminal.searchPrevious'), onMouseDown: (event) => event.preventDefault(), onClick: () => findInTerminal(-1) }, h(IconChevronLeft, { size: 12 })),
|
|
4032
|
+
h('button', { type: 'button', className: 'ssh-terminalSearchBtn', title: t('terminal.searchNext'), 'aria-label': t('terminal.searchNext'), onMouseDown: (event) => event.preventDefault(), onClick: () => findInTerminal(1) }, h(IconChevronRight, { size: 12 })),
|
|
4033
|
+
h('button', { type: 'button', className: 'ssh-terminalSearchBtn', title: t('terminal.searchClose'), 'aria-label': t('terminal.searchClose'), onClick: closeSearch }, h(IconClose, { size: 12 })),
|
|
4034
|
+
)
|
|
4035
|
+
: null,
|
|
4036
|
+
menu
|
|
4037
|
+
? h(
|
|
4038
|
+
'div',
|
|
4039
|
+
{
|
|
4040
|
+
ref: menuRef,
|
|
4041
|
+
className: 'ssh-terminalMenu',
|
|
4042
|
+
role: 'menu',
|
|
4043
|
+
'aria-label': t('terminal.menu.title'),
|
|
4044
|
+
style: { left: menu.x, top: menu.y },
|
|
4045
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
4046
|
+
},
|
|
4047
|
+
['copy', 'paste', 'selectAll', 'clear'].map((action) =>
|
|
4048
|
+
h('button', {
|
|
4049
|
+
key: action,
|
|
4050
|
+
type: 'button',
|
|
4051
|
+
role: 'menuitem',
|
|
4052
|
+
className: 'ssh-terminalMenuItem',
|
|
4053
|
+
onClick: () => terminalMenuAction(action),
|
|
4054
|
+
}, t(`terminal.menu.${action}`)),
|
|
4055
|
+
),
|
|
4056
|
+
)
|
|
4057
|
+
: null,
|
|
3703
4058
|
showOverlay
|
|
3704
4059
|
? h(
|
|
3705
4060
|
'div',
|
|
@@ -3747,25 +4102,50 @@ window.__ModuleLoader__.load({
|
|
|
3747
4102
|
)
|
|
3748
4103
|
}
|
|
3749
4104
|
|
|
4105
|
+
const SFTP_PATH_PREFIX = 'dsh-ssh-manage:sftp-path:'
|
|
4106
|
+
|
|
4107
|
+
function readSftpPath(alias) {
|
|
4108
|
+
try {
|
|
4109
|
+
const stored = window.localStorage.getItem(SFTP_PATH_PREFIX + alias)
|
|
4110
|
+
return stored && stored.trim() ? stored : '.'
|
|
4111
|
+
} catch {
|
|
4112
|
+
return '.'
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
function saveSftpPath(alias, target) {
|
|
4117
|
+
try {
|
|
4118
|
+
window.localStorage.setItem(SFTP_PATH_PREFIX + alias, target)
|
|
4119
|
+
} catch {}
|
|
4120
|
+
}
|
|
4121
|
+
|
|
3750
4122
|
function FilesView({ model, host }) {
|
|
3751
|
-
const { t, notify
|
|
3752
|
-
const [path, setPath] = React.useState(
|
|
4123
|
+
const { t, notify } = model
|
|
4124
|
+
const [path, setPath] = React.useState(() => readSftpPath(host.host))
|
|
3753
4125
|
const [entries, setEntries] = React.useState([])
|
|
3754
4126
|
const [loading, setLoading] = React.useState(true)
|
|
3755
4127
|
const [error, setError] = React.useState('')
|
|
3756
4128
|
const [uploading, setUploading] = React.useState(false)
|
|
4129
|
+
const [dialog, setDialog] = React.useState(null)
|
|
4130
|
+
const [dialogValue, setDialogValue] = React.useState('')
|
|
4131
|
+
const [dialogBusy, setDialogBusy] = React.useState(false)
|
|
4132
|
+
const dialogBusyRef = React.useRef(false)
|
|
3757
4133
|
const fileRef = React.useRef(null)
|
|
3758
4134
|
|
|
3759
4135
|
const loadDirectory = React.useCallback(
|
|
3760
|
-
async (target) => {
|
|
4136
|
+
async (target, options = {}) => {
|
|
4137
|
+
const normalized = String(target ?? '').trim() || '.'
|
|
3761
4138
|
setLoading(true)
|
|
3762
4139
|
setError('')
|
|
3763
4140
|
try {
|
|
3764
|
-
const value = await callAction('sftp/list', { host: host.host, path:
|
|
4141
|
+
const value = await callAction('sftp/list', { host: host.host, path: normalized })
|
|
3765
4142
|
setEntries(value.entries ?? [])
|
|
3766
|
-
setPath(
|
|
4143
|
+
setPath(normalized)
|
|
4144
|
+
if (options.remember !== false) saveSftpPath(host.host, normalized)
|
|
4145
|
+
return true
|
|
3767
4146
|
} catch (failure) {
|
|
3768
4147
|
setError(errorMessage(failure))
|
|
4148
|
+
return false
|
|
3769
4149
|
} finally {
|
|
3770
4150
|
setLoading(false)
|
|
3771
4151
|
}
|
|
@@ -3774,8 +4154,72 @@ window.__ModuleLoader__.load({
|
|
|
3774
4154
|
)
|
|
3775
4155
|
|
|
3776
4156
|
React.useEffect(() => {
|
|
3777
|
-
|
|
3778
|
-
|
|
4157
|
+
let cancelled = false
|
|
4158
|
+
void (async () => {
|
|
4159
|
+
const initial = readSftpPath(host.host)
|
|
4160
|
+
const ok = await loadDirectory(initial)
|
|
4161
|
+
if (!ok && initial !== '.' && !cancelled) await loadDirectory('.', { remember: false })
|
|
4162
|
+
})()
|
|
4163
|
+
return () => { cancelled = true }
|
|
4164
|
+
}, [host.host, loadDirectory])
|
|
4165
|
+
|
|
4166
|
+
React.useEffect(() => {
|
|
4167
|
+
if (!dialog) return undefined
|
|
4168
|
+
const onKey = (event) => {
|
|
4169
|
+
if (event.key === 'Escape' && !dialogBusy) {
|
|
4170
|
+
event.preventDefault()
|
|
4171
|
+
event.stopPropagation()
|
|
4172
|
+
setDialog(null)
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
document.addEventListener('keydown', onKey, true)
|
|
4176
|
+
return () => document.removeEventListener('keydown', onKey, true)
|
|
4177
|
+
}, [dialog, dialogBusy])
|
|
4178
|
+
|
|
4179
|
+
const openDialog = (mode, entry = null) => {
|
|
4180
|
+
setDialog({ mode, entry })
|
|
4181
|
+
setDialogValue(mode === 'rename' && entry ? entry.name : '')
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
async function submitDialog() {
|
|
4185
|
+
if (!dialog || dialogBusyRef.current) return
|
|
4186
|
+
dialogBusyRef.current = true
|
|
4187
|
+
const name = dialogValue.trim()
|
|
4188
|
+
if (dialog.mode !== 'delete') {
|
|
4189
|
+
if (!name || name.includes('/') || name.includes('\\') || name === '.' || name === '..') {
|
|
4190
|
+
notify(t('drawer.files.invalidName'), 'error')
|
|
4191
|
+
return
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
setDialogBusy(true)
|
|
4195
|
+
try {
|
|
4196
|
+
if (dialog.mode === 'mkdir') {
|
|
4197
|
+
await callAction('sftp/mkdir', { host: host.host, path: remoteJoin(path, name) })
|
|
4198
|
+
notify(t('drawer.files.created'), 'success')
|
|
4199
|
+
} else if (dialog.mode === 'rename') {
|
|
4200
|
+
if (name === dialog.entry.name) {
|
|
4201
|
+
setDialog(null)
|
|
4202
|
+
return
|
|
4203
|
+
}
|
|
4204
|
+
await callAction('sftp/rename', {
|
|
4205
|
+
host: host.host,
|
|
4206
|
+
path: remoteJoin(path, dialog.entry.name),
|
|
4207
|
+
newPath: remoteJoin(path, name),
|
|
4208
|
+
})
|
|
4209
|
+
notify(t('drawer.files.renamed'), 'success')
|
|
4210
|
+
} else if (dialog.mode === 'delete') {
|
|
4211
|
+
await callAction('sftp/delete', { host: host.host, path: remoteJoin(path, dialog.entry.name) })
|
|
4212
|
+
notify(t('drawer.files.deleted'), 'success')
|
|
4213
|
+
}
|
|
4214
|
+
setDialog(null)
|
|
4215
|
+
await loadDirectory(path)
|
|
4216
|
+
} catch (failure) {
|
|
4217
|
+
notify(t('state.error', { message: errorMessage(failure) }), 'error')
|
|
4218
|
+
} finally {
|
|
4219
|
+
dialogBusyRef.current = false
|
|
4220
|
+
setDialogBusy(false)
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
3779
4223
|
|
|
3780
4224
|
async function downloadEntry(entry) {
|
|
3781
4225
|
const remote = remoteJoin(path, entry.name)
|
|
@@ -3842,6 +4286,12 @@ window.__ModuleLoader__.load({
|
|
|
3842
4286
|
{ title: t('common.refresh'), onClick: () => void loadDirectory(path), disabled: loading },
|
|
3843
4287
|
loading ? h(Spinner, { size: 13 }) : h(IconRefresh, { size: 15 }),
|
|
3844
4288
|
),
|
|
4289
|
+
h(
|
|
4290
|
+
Button,
|
|
4291
|
+
{ size: 'sm', variant: 'ghost', onClick: () => openDialog('mkdir'), disabled: loading || uploading },
|
|
4292
|
+
h(IconPlus, { size: 13 }),
|
|
4293
|
+
t('drawer.files.mkdir'),
|
|
4294
|
+
),
|
|
3845
4295
|
h(
|
|
3846
4296
|
Button,
|
|
3847
4297
|
{ size: 'sm', onClick: () => fileRef.current && fileRef.current.click(), disabled: uploading },
|
|
@@ -3853,6 +4303,69 @@ window.__ModuleLoader__.load({
|
|
|
3853
4303
|
error
|
|
3854
4304
|
? h('div', { className: 'ssh-fileError' }, h(IconAlert, { size: 13 }), h('span', null, error))
|
|
3855
4305
|
: null,
|
|
4306
|
+
dialog
|
|
4307
|
+
? h(
|
|
4308
|
+
'div',
|
|
4309
|
+
{
|
|
4310
|
+
className: 'ssh-fileDialogScrim',
|
|
4311
|
+
onMouseDown: () => {
|
|
4312
|
+
if (!dialogBusy) setDialog(null)
|
|
4313
|
+
},
|
|
4314
|
+
},
|
|
4315
|
+
h(
|
|
4316
|
+
'div',
|
|
4317
|
+
{
|
|
4318
|
+
className: 'ssh-fileDialog',
|
|
4319
|
+
role: 'dialog',
|
|
4320
|
+
'aria-modal': 'true',
|
|
4321
|
+
'aria-label': dialog.mode === 'mkdir'
|
|
4322
|
+
? t('drawer.files.mkdirTitle')
|
|
4323
|
+
: dialog.mode === 'rename'
|
|
4324
|
+
? t('drawer.files.renameTitle')
|
|
4325
|
+
: t('drawer.files.deleteTitle'),
|
|
4326
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
4327
|
+
},
|
|
4328
|
+
h(
|
|
4329
|
+
'div',
|
|
4330
|
+
{ className: 'ssh-fileDialogTitle' },
|
|
4331
|
+
dialog.mode === 'mkdir'
|
|
4332
|
+
? t('drawer.files.mkdirTitle')
|
|
4333
|
+
: dialog.mode === 'rename'
|
|
4334
|
+
? t('drawer.files.renameTitle')
|
|
4335
|
+
: t('drawer.files.deleteTitle'),
|
|
4336
|
+
),
|
|
4337
|
+
dialog.mode === 'delete'
|
|
4338
|
+
? h('div', { className: 'ssh-fileDialogBody' }, t('drawer.files.deleteConfirm', { name: dialog.entry?.name ?? '' }))
|
|
4339
|
+
: h(TextInput, {
|
|
4340
|
+
value: dialogValue,
|
|
4341
|
+
autoFocus: true,
|
|
4342
|
+
placeholder: t('drawer.files.name'),
|
|
4343
|
+
onChange: setDialogValue,
|
|
4344
|
+
onKeyDown: (event) => {
|
|
4345
|
+
if (event.key === 'Enter') void submitDialog()
|
|
4346
|
+
else if (event.key === 'Escape' && !dialogBusy) setDialog(null)
|
|
4347
|
+
},
|
|
4348
|
+
}),
|
|
4349
|
+
h(
|
|
4350
|
+
'div',
|
|
4351
|
+
{ className: 'ssh-fileDialogActions' },
|
|
4352
|
+
h(Button, { variant: 'ghost', size: 'sm', onClick: () => setDialog(null), disabled: dialogBusy }, t('common.cancel')),
|
|
4353
|
+
h(
|
|
4354
|
+
Button,
|
|
4355
|
+
{
|
|
4356
|
+
variant: dialog.mode === 'delete' ? 'dangerSolid' : 'primary',
|
|
4357
|
+
size: 'sm',
|
|
4358
|
+
autoFocus: dialog.mode === 'delete',
|
|
4359
|
+
onClick: () => void submitDialog(),
|
|
4360
|
+
disabled: dialogBusy,
|
|
4361
|
+
},
|
|
4362
|
+
dialogBusy ? h(Spinner, { size: 12 }) : null,
|
|
4363
|
+
dialog.mode === 'delete' ? t('common.delete') : t('common.create'),
|
|
4364
|
+
),
|
|
4365
|
+
),
|
|
4366
|
+
),
|
|
4367
|
+
)
|
|
4368
|
+
: null,
|
|
3856
4369
|
h(
|
|
3857
4370
|
'div',
|
|
3858
4371
|
{ className: 'ssh-fileList' },
|
|
@@ -3879,13 +4392,27 @@ window.__ModuleLoader__.load({
|
|
|
3879
4392
|
entry.name,
|
|
3880
4393
|
),
|
|
3881
4394
|
h('span', { className: 'ssh-fileSize' }, entry.directory ? t('drawer.files.dir') : formatBytes(entry.size)),
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
:
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
4395
|
+
h(
|
|
4396
|
+
'div',
|
|
4397
|
+
{ className: 'ssh-fileActions' },
|
|
4398
|
+
entry.directory
|
|
4399
|
+
? null
|
|
4400
|
+
: h(
|
|
4401
|
+
IconButton,
|
|
4402
|
+
{ title: t('common.download'), onClick: () => void downloadEntry(entry) },
|
|
4403
|
+
h(IconDownload, { size: 14 }),
|
|
4404
|
+
),
|
|
4405
|
+
h(
|
|
4406
|
+
IconButton,
|
|
4407
|
+
{ title: t('drawer.files.rename'), onClick: () => openDialog('rename', entry) },
|
|
4408
|
+
h(IconEdit, { size: 14 }),
|
|
4409
|
+
),
|
|
4410
|
+
h(
|
|
4411
|
+
IconButton,
|
|
4412
|
+
{ title: t('drawer.files.delete'), onClick: () => openDialog('delete', entry) },
|
|
4413
|
+
h(IconTrash, { size: 14 }),
|
|
4414
|
+
),
|
|
4415
|
+
),
|
|
3889
4416
|
),
|
|
3890
4417
|
),
|
|
3891
4418
|
),
|
|
@@ -4949,6 +5476,16 @@ body.ssh-col-resizing,body.ssh-col-resizing *{cursor:col-resize!important;user-s
|
|
|
4949
5476
|
.ssh-terminalOverlayActions{display:flex;gap:8px;flex-wrap:wrap;justify-content:center}
|
|
4950
5477
|
.ssh-sessionLayer{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--ssh-bg)}
|
|
4951
5478
|
.ssh-sessionState{width:6px;height:6px;border-radius:50%;background:var(--ssh-dim);flex:0 0 auto}
|
|
5479
|
+
.ssh-fontGlyph{font-size:10.5px;font-weight:600;letter-spacing:-.4px}
|
|
5480
|
+
.ssh-terminalSearch{position:absolute;top:8px;right:18px;z-index:5;max-width:calc(100% - 24px);display:flex;align-items:center;gap:5px;padding:5px 7px;border:.5px solid var(--ssh-border-strong);border-radius:10px;background:var(--ssh-layer-1);box-shadow:var(--ssh-shadow);color:var(--ssh-text-3)}
|
|
5481
|
+
.ssh-terminalSearchInput{width:min(180px,42vw);max-width:100%;height:24px;padding:0 7px;border:.5px solid var(--ssh-border);border-radius:6px;background:var(--ssh-bg);color:var(--ssh-text);font:inherit;font-size:12px;outline:none}
|
|
5482
|
+
.ssh-terminalSearchInput:focus{border-color:var(--ssh-brand)}
|
|
5483
|
+
.ssh-terminalSearchBtn{appearance:none;display:inline-flex;align-items:center;justify-content:center;height:24px;min-width:24px;padding:0 5px;border:0;border-radius:6px;background:transparent;color:var(--ssh-text-2);font:inherit;font-size:11px;cursor:pointer}
|
|
5484
|
+
.ssh-terminalSearchBtn:hover{background:var(--ssh-hover);color:var(--ssh-text)}
|
|
5485
|
+
.ssh-terminalSearchBtn.on{background:color-mix(in srgb,var(--ssh-brand) 15%,transparent);color:var(--ssh-brand)}
|
|
5486
|
+
.ssh-terminalMenu{position:absolute;z-index:7;min-width:150px;padding:4px;border:.5px solid var(--ssh-border-strong);border-radius:9px;background:var(--ssh-layer-1);box-shadow:var(--ssh-shadow)}
|
|
5487
|
+
.ssh-terminalMenuItem{appearance:none;display:flex;align-items:center;width:100%;height:28px;padding:0 9px;border:0;border-radius:6px;background:transparent;color:var(--ssh-text-2);font:inherit;font-size:12px;text-align:left;cursor:pointer}
|
|
5488
|
+
.ssh-terminalMenuItem:hover{background:var(--ssh-hover);color:var(--ssh-text)}
|
|
4952
5489
|
.ssh-sessionState.loading,.ssh-sessionState.connecting{background:var(--ssh-warn)}
|
|
4953
5490
|
.ssh-sessionState.ready{background:var(--ssh-success)}
|
|
4954
5491
|
.ssh-sessionState.error,.ssh-sessionState.closed{background:var(--ssh-danger)}
|
|
@@ -4982,7 +5519,13 @@ body.ssh-col-resizing,body.ssh-col-resizing *{cursor:col-resize!important;user-s
|
|
|
4982
5519
|
.ssh-filesPath{flex:1;min-width:140px}
|
|
4983
5520
|
.ssh-fileError{display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--ssh-danger)}
|
|
4984
5521
|
.ssh-fileList{flex:1;min-height:0;overflow:auto;border:.5px solid var(--ssh-border);border-radius:var(--ssh-radius-md)}
|
|
4985
|
-
.ssh-fileRow{display:grid;grid-template-columns:22px minmax(0,1fr) 90px
|
|
5522
|
+
.ssh-fileRow{display:grid;grid-template-columns:22px minmax(0,1fr) 90px auto;align-items:center;gap:8px;padding:0 10px;height:34px;border-bottom:.5px solid var(--ssh-border);font-size:12px}
|
|
5523
|
+
.ssh-fileActions{display:flex;align-items:center;gap:2px}
|
|
5524
|
+
.ssh-fileDialogScrim{position:absolute;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;padding:20px;background:var(--dsw-alias-bg-mask-1,color-mix(in srgb,var(--ssh-text) 18%,transparent))}
|
|
5525
|
+
.ssh-fileDialog{width:min(360px,100%);padding:14px;border:.5px solid var(--ssh-border-strong);border-radius:12px;background:var(--ssh-layer-1);box-shadow:var(--ssh-shadow);display:flex;flex-direction:column;gap:10px}
|
|
5526
|
+
.ssh-fileDialogTitle{font-size:13px;font-weight:600;color:var(--ssh-text)}
|
|
5527
|
+
.ssh-fileDialogBody{font-size:12px;line-height:1.5;color:var(--ssh-text-2)}
|
|
5528
|
+
.ssh-fileDialogActions{display:flex;justify-content:flex-end;gap:8px}
|
|
4986
5529
|
.ssh-fileRow:last-child{border-bottom:none}
|
|
4987
5530
|
.ssh-fileRow:hover{background:var(--ssh-hover)}
|
|
4988
5531
|
.ssh-fileIcon{display:inline-flex;color:var(--ssh-text-3)}
|