@electerm/electerm-react 5.1.25 → 5.3.0
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/client/common/build-map.js +12 -0
- package/client/common/constants.js +12 -0
- package/client/common/db.js +7 -5
- package/client/common/import-task.js +174 -0
- package/client/common/term-of-use.jsx +69 -0
- package/client/components/ai/ai-config-props.js +8 -0
- package/client/components/bookmark-form/ai-bookmark-form.jsx +18 -15
- package/client/components/bookmark-form/bookmark-schema.js +1 -0
- package/client/components/bookmark-form/common/bookmark-select.jsx +6 -5
- package/client/components/bookmark-form/common/fields.jsx +1 -1
- package/client/components/bookmark-form/common/profile-item.jsx +12 -2
- package/client/components/bookmark-form/config/common-fields.js +11 -0
- package/client/components/bookmark-form/config/rdp.js +1 -1
- package/client/components/bookmark-form/tree-select.jsx +5 -16
- package/client/components/common/import-progress.jsx +90 -0
- package/client/components/footer/footer-entry.jsx +1 -1
- package/client/components/main/main.jsx +2 -0
- package/client/components/profile/profile-form-rdp.jsx +1 -1
- package/client/components/quick-commands/quick-command-transport-mod.jsx +24 -18
- package/client/components/rdp/rdp-cred-form.jsx +55 -0
- package/client/components/rdp/rdp-session.jsx +97 -6
- package/client/components/setting-panel/keywords-transport.jsx +1 -4
- package/client/components/setting-panel/setting-modal.jsx +8 -2
- package/client/components/setting-panel/start-session-select.jsx +5 -16
- package/client/components/setting-sync/setting-sync.jsx +15 -7
- package/client/components/sidebar/info-modal.jsx +7 -0
- package/client/components/spice/spice-session.jsx +40 -0
- package/client/components/terminal/attach-addon-custom.js +12 -10
- package/client/components/terminal/shell.js +22 -11
- package/client/components/terminal/shortcut-bar-entry.jsx +52 -1
- package/client/components/terminal/terminal-color-query.mjs +32 -27
- package/client/components/terminal/terminal-interactive.jsx +35 -0
- package/client/components/terminal/terminal.jsx +24 -15
- package/client/components/terminal-info/activity.jsx +5 -1
- package/client/components/terminal-info/base.jsx +37 -22
- package/client/components/terminal-info/disk.jsx +5 -1
- package/client/components/terminal-info/local-info-resolver.js +89 -0
- package/client/components/terminal-info/network.jsx +5 -1
- package/client/components/terminal-info/run-cmd.jsx +42 -3
- package/client/components/terminal-info/sys-info.jsx +56 -0
- package/client/components/terminal-info/terminal-info.jsx +4 -1
- package/client/components/terminal-info/terminal-info.styl +22 -7
- package/client/components/tree-list/bookmark-transport.jsx +2 -1
- package/client/components/tree-list/bookmark-upload.js +56 -41
- package/client/components/tree-list/tree-list-row.jsx +11 -4
- package/client/components/tree-list/tree-list-rows.js +6 -3
- package/client/components/tree-list/tree-list.jsx +4 -2
- package/client/store/common.js +38 -3
- package/client/store/load-data.js +6 -1
- package/client/store/setting.js +7 -4
- package/client/store/store.js +3 -12
- package/client/store/sync.js +31 -2
- package/client/store/tab.js +16 -11
- package/client/store/watch.js +10 -1
- package/package.json +1 -1
|
@@ -22,6 +22,18 @@
|
|
|
22
22
|
* visualViewport.height against a baseline captured at the moment the
|
|
23
23
|
* terminal input is focused (i.e. before the keyboard opens), which holds
|
|
24
24
|
* the full pre-keyboard height on both platforms.
|
|
25
|
+
*
|
|
26
|
+
* Telling a keyboard from a desktop window resize (pure size heuristics
|
|
27
|
+
* fail — dragging a window's top/bottom edge is also a height-only shrink)
|
|
28
|
+
* needs one of two extra signals, checked in onResize:
|
|
29
|
+
* - an overlay-style keyboard (iOS / HarmonyOS) shrinks the visual
|
|
30
|
+
* viewport but keeps the layout viewport (window.innerHeight) full —
|
|
31
|
+
* a window resize shrinks both.
|
|
32
|
+
* - an Android-style keyboard resizes both viewports, but a soft keyboard
|
|
33
|
+
* only ever opens in response to a *touch* into the terminal — a window
|
|
34
|
+
* resized with a mouse never shows one.
|
|
35
|
+
* Tablets running the desktop program still get the bar; mouse-driven
|
|
36
|
+
* desktops never do.
|
|
25
37
|
*/
|
|
26
38
|
|
|
27
39
|
import { lazy, Suspense, useEffect, useState } from 'react'
|
|
@@ -35,6 +47,11 @@ const ShortcutBar = lazy(() => import('./shortcut-bar'))
|
|
|
35
47
|
// bar above a keyboard that overlays the page (iOS / HarmonyOS).
|
|
36
48
|
export const KEYBOARD_MIN = 120
|
|
37
49
|
|
|
50
|
+
// how long (ms) after an in-page touch an Android-style (layout-resizing)
|
|
51
|
+
// keyboard is expected to be up. Bounds the touch signal used to tell that
|
|
52
|
+
// keyboard from a mouse-driven window resize on touch-capable desktops.
|
|
53
|
+
const TOUCH_GRACE_MS = 3000
|
|
54
|
+
|
|
38
55
|
// kept as a coarse capability fallback for engines without visualViewport.
|
|
39
56
|
// Modern Chromium (Electron) always exposes window.visualViewport, so this is
|
|
40
57
|
// effectively only reached on very old runtimes — where remote-control
|
|
@@ -77,6 +94,12 @@ export default function ShortcutBarEntry (props) {
|
|
|
77
94
|
// the pre-keyboard (full) size, and the subsequent resize lets us measure
|
|
78
95
|
// exactly how much the keyboard took — on both iOS and Android.
|
|
79
96
|
let baseline = { height: vv.height, width: vv.width }
|
|
97
|
+
// when the last in-page touch happened (0 = never). An Android-style
|
|
98
|
+
// keyboard resizes the layout viewport exactly like a window resize does,
|
|
99
|
+
// so size alone can't separate them there — but a keyboard only ever
|
|
100
|
+
// opens right after a *touch*, while a window drag doesn't fire in-page
|
|
101
|
+
// pointer events at all, so an old touch must not count forever.
|
|
102
|
+
let lastTouchAt = 0
|
|
80
103
|
|
|
81
104
|
function onFocusIn (ev) {
|
|
82
105
|
const el = ev.target
|
|
@@ -85,11 +108,25 @@ export default function ShortcutBarEntry (props) {
|
|
|
85
108
|
}
|
|
86
109
|
}
|
|
87
110
|
|
|
111
|
+
function onPointerDown (ev) {
|
|
112
|
+
// touch or stylus — both tap like a finger and can raise the keyboard
|
|
113
|
+
if (ev.pointerType === 'touch' || ev.pointerType === 'pen') {
|
|
114
|
+
lastTouchAt = Date.now()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
88
118
|
function onResize () {
|
|
89
119
|
// ignore viewport changes that also changed width (rotation / split) —
|
|
90
120
|
// a keyboard changes height only.
|
|
91
121
|
const sameWidth = Math.abs(vv.width - baseline.width) < 20
|
|
92
|
-
if (sameWidth
|
|
122
|
+
if (!sameWidth || baseline.height - vv.height <= KEYBOARD_MIN) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
// overlay keyboard (iOS / HarmonyOS): the layout viewport stays full
|
|
126
|
+
// while the visual one shrinks. A window resize shrinks both — this is
|
|
127
|
+
// what rules out mouse window-dragging on those platforms.
|
|
128
|
+
const layoutShrunk = window.innerHeight < baseline.height - KEYBOARD_MIN
|
|
129
|
+
if (!layoutShrunk) {
|
|
93
130
|
setKeyboardSeen(true)
|
|
94
131
|
// The user is focused in a terminal with the keyboard up — they need
|
|
95
132
|
// the bar right now. Set visibility here rather than waiting for the
|
|
@@ -100,13 +137,27 @@ export default function ShortcutBarEntry (props) {
|
|
|
100
137
|
if (store && store.inActiveTerminal) {
|
|
101
138
|
store.shortcutBarVisible = true
|
|
102
139
|
}
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
// Android-style resize keyboard: both viewports shrink, so require a
|
|
143
|
+
// recent touch — a keyboard opens within moments of the tap, while a
|
|
144
|
+
// window drag fires no in-page pointerdown at all, so any touch old
|
|
145
|
+
// enough to have expired can't have been the cause.
|
|
146
|
+
if (Date.now() - lastTouchAt < TOUCH_GRACE_MS) {
|
|
147
|
+
setKeyboardSeen(true)
|
|
148
|
+
const { store } = window
|
|
149
|
+
if (store && store.inActiveTerminal) {
|
|
150
|
+
store.shortcutBarVisible = true
|
|
151
|
+
}
|
|
103
152
|
}
|
|
104
153
|
}
|
|
105
154
|
|
|
106
155
|
document.addEventListener('focusin', onFocusIn)
|
|
156
|
+
document.addEventListener('pointerdown', onPointerDown)
|
|
107
157
|
vv.addEventListener('resize', onResize)
|
|
108
158
|
return () => {
|
|
109
159
|
document.removeEventListener('focusin', onFocusIn)
|
|
160
|
+
document.removeEventListener('pointerdown', onPointerDown)
|
|
110
161
|
vv.removeEventListener('resize', onResize)
|
|
111
162
|
}
|
|
112
163
|
}, [])
|
|
@@ -57,7 +57,8 @@ function parseHexColor (color) {
|
|
|
57
57
|
return [
|
|
58
58
|
parseInt(expanded.slice(0, 2), 16),
|
|
59
59
|
parseInt(expanded.slice(2, 4), 16),
|
|
60
|
-
parseInt(expanded.slice(4, 6), 16)
|
|
60
|
+
parseInt(expanded.slice(4, 6), 16),
|
|
61
|
+
alpha
|
|
61
62
|
]
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -82,10 +83,15 @@ function parseRgbColor (color) {
|
|
|
82
83
|
if (alpha === null || alpha <= 0) {
|
|
83
84
|
return null
|
|
84
85
|
}
|
|
85
|
-
return channels
|
|
86
|
+
return [...channels, alpha]
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
export function parseColorToRgb (color) {
|
|
90
|
+
const rgba = parseColorToRgba(color)
|
|
91
|
+
return rgba ? rgba.slice(0, 3) : null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function parseColorToRgba (color) {
|
|
89
95
|
if (typeof color !== 'string') {
|
|
90
96
|
return null
|
|
91
97
|
}
|
|
@@ -105,46 +111,45 @@ export function colorToOscRgb (color) {
|
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
/**
|
|
108
|
-
* Blend a (possibly translucent) selection
|
|
114
|
+
* Blend a (possibly translucent) selection colour over an opaque background,
|
|
109
115
|
* producing the colour in xterm's internal format ({ css, rgba } where
|
|
110
116
|
* rgba = r<<24 | g<<16 | b<<8 | a).
|
|
111
117
|
*
|
|
112
118
|
* xterm derives its DOM selection colour as blend(theme.background,
|
|
113
|
-
* selectionBackground)
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
119
|
+
* selectionBackground) into `selectionBackgroundOpaque`, and separately forces
|
|
120
|
+
* any opaque selectionBackground down to 0.3 alpha in
|
|
121
|
+
* `selectionBackgroundTransparent` (xterm#2737). electerm renders the terminal
|
|
122
|
+
* background via CSS and hands xterm a transparent background, so the blend
|
|
123
|
+
* must be redone over the real visible background — starting from the
|
|
124
|
+
* *configured* colour so an opaque selection stays opaque (xterm's forced
|
|
125
|
+
* 0.3-alpha variant would make every selection translucent).
|
|
117
126
|
*
|
|
118
|
-
* `selectionColor` is
|
|
119
|
-
*
|
|
120
|
-
*
|
|
127
|
+
* `selectionColor` is a CSS colour string from the terminal theme, e.g.
|
|
128
|
+
* themeConfig.selectionBackground; `visibleBackground` is an opaque colour
|
|
129
|
+
* string.
|
|
121
130
|
*/
|
|
122
131
|
export function blendSelectionOverBackground (visibleBackground, selectionColor) {
|
|
123
|
-
const
|
|
124
|
-
const sel = selectionColor
|
|
125
|
-
if (!
|
|
126
|
-
return null
|
|
127
|
-
}
|
|
128
|
-
const [br, bg, bb] = bgRgb
|
|
129
|
-
const a = (sel & 255) / 255
|
|
130
|
-
if (a <= 0) {
|
|
132
|
+
const bg = parseColorToRgb(visibleBackground)
|
|
133
|
+
const sel = parseColorToRgba(selectionColor)
|
|
134
|
+
if (!bg || !sel) {
|
|
131
135
|
return null
|
|
132
136
|
}
|
|
137
|
+
const [br, bgg, bb] = bg
|
|
138
|
+
const [sr, sg, sb, sa] = sel
|
|
133
139
|
const toCss = (r, g, b, alpha) =>
|
|
134
140
|
`#${[r, g, b, alpha].map(toHexByte).join('')}`
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (a >= 1) {
|
|
141
|
+
if (sa >= 1) {
|
|
142
|
+
// Fully opaque: use the configured colour as-is, just normalised to
|
|
143
|
+
// 8-digit hex so both the overlay and per-cell inline styles agree.
|
|
139
144
|
return {
|
|
140
145
|
css: toCss(sr, sg, sb, 255),
|
|
141
|
-
rgba: (
|
|
146
|
+
rgba: (sr << 24 | sg << 16 | sb << 8 | 255) >>> 0
|
|
142
147
|
}
|
|
143
148
|
}
|
|
144
|
-
const r = br + Math.round((sr - br) *
|
|
145
|
-
const g =
|
|
146
|
-
const b = bb + Math.round((sb - bb) *
|
|
147
|
-
const alpha = Math.round(
|
|
149
|
+
const r = br + Math.round((sr - br) * sa)
|
|
150
|
+
const g = bgg + Math.round((sg - bgg) * sa)
|
|
151
|
+
const b = bb + Math.round((sb - bb) * sa)
|
|
152
|
+
const alpha = Math.round(sa * 255)
|
|
148
153
|
return {
|
|
149
154
|
css: toCss(r, g, b, alpha),
|
|
150
155
|
rgba: (r << 24 | g << 16 | b << 8 | alpha) >>> 0
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { useEffect, useState, useRef, useCallback } from 'react'
|
|
6
6
|
import wait from '../../common/wait'
|
|
7
7
|
import TermInteractiveUI from './terminal-interactive-ui'
|
|
8
|
+
import ExternalLink from '../common/external-link.jsx'
|
|
9
|
+
import { notification } from '../common/notification'
|
|
8
10
|
|
|
9
11
|
export default function TermInteractive () {
|
|
10
12
|
const [current, setCurrent] = useState(null)
|
|
@@ -15,6 +17,32 @@ export default function TermInteractive () {
|
|
|
15
17
|
window.store.updateTab(data.tabId, data.update)
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
function notifyProxyCommandMessage (data) {
|
|
21
|
+
const lines = (data.message || '').split('\n')
|
|
22
|
+
const title = lines[0] || 'ssh proxy command'
|
|
23
|
+
const body = lines.slice(1).join('\n').trim()
|
|
24
|
+
// make any http(s) link in the message clickable (SSO login url)
|
|
25
|
+
const renderBody = (text) => {
|
|
26
|
+
const parts = text.split(/(https?:\/\/\S+)/)
|
|
27
|
+
return (
|
|
28
|
+
<pre className='pd0 mg0 overflow-auto'>
|
|
29
|
+
{
|
|
30
|
+
parts.map((part, i) => /^https?:\/\//.test(part)
|
|
31
|
+
? <ExternalLink key={i} to={part}>{part}</ExternalLink>
|
|
32
|
+
: part)
|
|
33
|
+
}
|
|
34
|
+
</pre>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
notification.info({
|
|
38
|
+
message: title,
|
|
39
|
+
description: body
|
|
40
|
+
? renderBody(body)
|
|
41
|
+
: undefined,
|
|
42
|
+
duration: 30
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
function processNext () {
|
|
19
47
|
const next = queueRef.current.shift()
|
|
20
48
|
if (next) {
|
|
@@ -47,6 +75,13 @@ export default function TermInteractive () {
|
|
|
47
75
|
e.data.includes('ssh-tunnel-result')
|
|
48
76
|
) {
|
|
49
77
|
updateTab(JSON.parse(e.data))
|
|
78
|
+
} else if (
|
|
79
|
+
e &&
|
|
80
|
+
e.data &&
|
|
81
|
+
typeof e.data === 'string' &&
|
|
82
|
+
e.data.includes('ssh-proxy-command-message')
|
|
83
|
+
) {
|
|
84
|
+
notifyProxyCommandMessage(JSON.parse(e.data))
|
|
50
85
|
}
|
|
51
86
|
}
|
|
52
87
|
|
|
@@ -1474,31 +1474,35 @@ class Term extends Component {
|
|
|
1474
1474
|
|
|
1475
1475
|
/**
|
|
1476
1476
|
* xterm computes its DOM selection colour as blend(theme.background,
|
|
1477
|
-
* selectionBackground). The
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
1477
|
+
* selectionBackground). The terminal background is forced transparent
|
|
1478
|
+
* (rgba(0,0,0,0)) so electerm's own CSS background / image shows through,
|
|
1479
|
+
* which makes the blend run over transparent-black. Recompute the selection
|
|
1480
|
+
* colours over the real visible background, from the *configured* theme
|
|
1481
|
+
* colours — xterm's internal selectionBackgroundTransparent forces opaque
|
|
1482
|
+
* colours down to 0.3 alpha (xterm#2737), which would make every selection
|
|
1483
|
+
* translucent even when the user configured an opaque one.
|
|
1483
1484
|
*/
|
|
1484
1485
|
fixSelectionColors = (term) => {
|
|
1486
|
+
const themeConfig = this.props.themeConfig || {}
|
|
1485
1487
|
const themeService = term?._core?._themeService
|
|
1486
1488
|
if (!themeService?.modifyColors) {
|
|
1487
1489
|
return
|
|
1488
1490
|
}
|
|
1489
1491
|
const visibleBackground = this.getVisibleTerminalBackground()
|
|
1490
|
-
|
|
1492
|
+
// xterm's DEFAULT_SELECTION, used when the theme omits a selection color
|
|
1493
|
+
const selectionFallback = 'rgba(255, 255, 255, 0.3)'
|
|
1494
|
+
const inactiveFallback = themeConfig.selectionBackground || selectionFallback
|
|
1491
1495
|
themeService.modifyColors((c) => {
|
|
1492
1496
|
const active = blendSelectionOverBackground(
|
|
1493
1497
|
visibleBackground,
|
|
1494
|
-
|
|
1498
|
+
themeConfig.selectionBackground || selectionFallback
|
|
1495
1499
|
)
|
|
1496
1500
|
if (active) {
|
|
1497
1501
|
c.selectionBackgroundOpaque = active
|
|
1498
1502
|
}
|
|
1499
1503
|
const inactive = blendSelectionOverBackground(
|
|
1500
1504
|
visibleBackground,
|
|
1501
|
-
|
|
1505
|
+
themeConfig.selectionInactiveBackground || inactiveFallback
|
|
1502
1506
|
)
|
|
1503
1507
|
if (inactive) {
|
|
1504
1508
|
c.selectionInactiveBackgroundOpaque = inactive
|
|
@@ -1719,12 +1723,6 @@ class Term extends Component {
|
|
|
1719
1723
|
}
|
|
1720
1724
|
|
|
1721
1725
|
this.shellType = shellType
|
|
1722
|
-
if (shellType === 'fish') {
|
|
1723
|
-
if (this.props.sftpPathFollowSsh) {
|
|
1724
|
-
this.warnSftpFollowUnsupported()
|
|
1725
|
-
}
|
|
1726
|
-
return Promise.resolve()
|
|
1727
|
-
}
|
|
1728
1726
|
|
|
1729
1727
|
// Don't inject for sh type shells unless sftpPathFollowSsh is true
|
|
1730
1728
|
if (shellType === 'sh' && !this.props.sftpPathFollowSsh) {
|
|
@@ -1942,6 +1940,17 @@ class Term extends Component {
|
|
|
1942
1940
|
if (this.isElementVisible()) {
|
|
1943
1941
|
this.fitAddon.fit()
|
|
1944
1942
|
}
|
|
1943
|
+
// Force-send the current size now that `this.pid` is finally valid.
|
|
1944
|
+
// A resize may already have fired earlier (e.g. from the tab-becomes-
|
|
1945
|
+
// visible fit in componentDidUpdate, which can run before createTerm()
|
|
1946
|
+
// resolves) while this.pid was still undefined - the backend silently
|
|
1947
|
+
// drops resize calls for an unknown pid. If that earlier call already
|
|
1948
|
+
// updated term.cols/rows locally, xterm's resize() no-ops on the next
|
|
1949
|
+
// fit() (dims unchanged) and never fires onResize again, so the
|
|
1950
|
+
// backend pty would otherwise be stuck at its initial (possibly wrong)
|
|
1951
|
+
// size forever. Sending explicitly here, keyed off the real pid,
|
|
1952
|
+
// guarantees the remote pty/ssh channel is told the true current size.
|
|
1953
|
+
resizeTerm(this.pid, term.cols, term.rows)
|
|
1945
1954
|
term.displayRaw = displayRaw
|
|
1946
1955
|
term.loadAddon(
|
|
1947
1956
|
new KeywordHighlighterAddon(keywords)
|
|
@@ -42,7 +42,11 @@ export default function TerminalInfoActivities (props) {
|
|
|
42
42
|
bordered: true,
|
|
43
43
|
columns: col,
|
|
44
44
|
size: 'small',
|
|
45
|
-
pagination: false
|
|
45
|
+
pagination: false,
|
|
46
|
+
scroll: {
|
|
47
|
+
y: 180,
|
|
48
|
+
x: 'max-content'
|
|
49
|
+
}
|
|
46
50
|
}
|
|
47
51
|
return (
|
|
48
52
|
<div className='terminal-info-section terminal-info-act'>
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Component } from 'react'
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
Button,
|
|
7
|
+
Dropdown
|
|
8
8
|
} from 'antd'
|
|
9
9
|
import SwitchLabel from '../common/switch'
|
|
10
10
|
import defaults from '../../common/default-setting'
|
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
DatabaseOutlined,
|
|
16
16
|
BarsOutlined,
|
|
17
17
|
ApiOutlined,
|
|
18
|
-
PartitionOutlined
|
|
18
|
+
PartitionOutlined,
|
|
19
|
+
FilterOutlined,
|
|
20
|
+
CheckOutlined
|
|
19
21
|
} from '@ant-design/icons'
|
|
20
22
|
import { refs } from '../common/ref'
|
|
21
23
|
import ShowItem from '../common/show-item'
|
|
@@ -136,26 +138,39 @@ export default class TerminalInfoBase extends Component {
|
|
|
136
138
|
const {
|
|
137
139
|
terminalInfos
|
|
138
140
|
} = this.props
|
|
141
|
+
const items = defaults.terminalInfos.map(f => {
|
|
142
|
+
const checked = terminalInfos.includes(f)
|
|
143
|
+
return {
|
|
144
|
+
key: f,
|
|
145
|
+
label: (
|
|
146
|
+
<span
|
|
147
|
+
className='term-info-filter-label'
|
|
148
|
+
style={{
|
|
149
|
+
display: 'flex',
|
|
150
|
+
alignItems: 'center',
|
|
151
|
+
gap: '6px'
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<span style={{ width: '14px' }}>{checked ? <CheckOutlined /> : null}</span>
|
|
155
|
+
{mapper[f]} {f}
|
|
156
|
+
</span>
|
|
157
|
+
),
|
|
158
|
+
onClick: () => this.toggleTerminalLogInfo(f)
|
|
159
|
+
}
|
|
160
|
+
})
|
|
139
161
|
return (
|
|
140
|
-
<
|
|
141
|
-
{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
>
|
|
153
|
-
{f}
|
|
154
|
-
</Button>
|
|
155
|
-
)
|
|
156
|
-
})
|
|
157
|
-
}
|
|
158
|
-
</Space.Compact>
|
|
162
|
+
<Dropdown
|
|
163
|
+
menu={{ items }}
|
|
164
|
+
trigger={['click']}
|
|
165
|
+
placement='bottomRight'
|
|
166
|
+
>
|
|
167
|
+
<Button
|
|
168
|
+
size='small'
|
|
169
|
+
icon={<FilterOutlined />}
|
|
170
|
+
>
|
|
171
|
+
{e('filter')}({terminalInfos.length}/{defaults.terminalInfos.length})
|
|
172
|
+
</Button>
|
|
173
|
+
</Dropdown>
|
|
159
174
|
)
|
|
160
175
|
}
|
|
161
176
|
|
|
@@ -30,7 +30,11 @@ export default function TerminalInfoDisk (props) {
|
|
|
30
30
|
bordered: true,
|
|
31
31
|
columns: col,
|
|
32
32
|
size: 'small',
|
|
33
|
-
pagination: false
|
|
33
|
+
pagination: false,
|
|
34
|
+
scroll: {
|
|
35
|
+
y: 180,
|
|
36
|
+
x: 'max-content'
|
|
37
|
+
}
|
|
34
38
|
}
|
|
35
39
|
return (
|
|
36
40
|
<div className='terminal-info-section terminal-info-disk'>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolve local terminal info: os / hostname / kernel / arch / shell + version
|
|
3
|
+
* uses the electron os module via window.pre.osInfo() (sync) and probes the
|
|
4
|
+
* configured shell binary for its version through the local fs run bridge
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isMac, isWin } from '../../common/constants'
|
|
8
|
+
import fs from '../../common/fs'
|
|
9
|
+
|
|
10
|
+
function parseOsInfo (list) {
|
|
11
|
+
const map = {}
|
|
12
|
+
for (const { k, v } of list || []) {
|
|
13
|
+
let val
|
|
14
|
+
try {
|
|
15
|
+
val = JSON.parse(v)
|
|
16
|
+
} catch (e) {
|
|
17
|
+
val = v
|
|
18
|
+
}
|
|
19
|
+
map[k] = Array.isArray(val) ? val.join(' ') : String(val)
|
|
20
|
+
}
|
|
21
|
+
return map
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const shellNameReg = /(\w+)$/
|
|
25
|
+
|
|
26
|
+
function formatOsName (map) {
|
|
27
|
+
const ver = map.release || ''
|
|
28
|
+
if (isWin) {
|
|
29
|
+
const main = parseInt(ver.split('.')[0], 10)
|
|
30
|
+
if (main >= 10) {
|
|
31
|
+
const build = parseInt(ver.split('.')[2], 10)
|
|
32
|
+
return build >= 22000 ? `Windows 11 (${ver})` : `Windows 10 (${ver})`
|
|
33
|
+
}
|
|
34
|
+
return `Windows (${ver})`
|
|
35
|
+
}
|
|
36
|
+
if (isMac) {
|
|
37
|
+
return `macOS ${map.version || ''}`.trim()
|
|
38
|
+
}
|
|
39
|
+
return `Linux ${ver}`.trim()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function probeShellVersion (shellPath) {
|
|
43
|
+
if (!shellPath) {
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
let out = ''
|
|
48
|
+
if (isWin) {
|
|
49
|
+
// runWinCmd wraps the given script with powershell.exe -Command
|
|
50
|
+
const res = await fs.runWinCmd(
|
|
51
|
+
'$PSVersionTable.PSVersion.ToString()'
|
|
52
|
+
).catch(() => null)
|
|
53
|
+
out = res?.stdout || ''
|
|
54
|
+
} else {
|
|
55
|
+
out = await fs.run(`'${shellPath.replace(/'/g, "'\\''")}' --version`).catch(() => '')
|
|
56
|
+
}
|
|
57
|
+
return (out || '').split('\n')[0].trim()
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return ''
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default async function resolveLocalInfo (pid) {
|
|
64
|
+
const store = window.store
|
|
65
|
+
if (!window.pre?.osInfo) {
|
|
66
|
+
// web app build: no local os bridge available
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
const tab = store?.getTabs?.().find(t => t.id === pid)
|
|
70
|
+
const config = store?.config || {}
|
|
71
|
+
const execProp = isWin ? 'execWindows' : isMac ? 'execMac' : 'execLinux'
|
|
72
|
+
// tab settings (bookmark) win over global config, same as terminal spawn logic
|
|
73
|
+
const shellPath = tab?.[execProp] || config[execProp] || ''
|
|
74
|
+
const shellBase = isWin
|
|
75
|
+
? (shellPath.split(/[\\/]/).pop() || '')
|
|
76
|
+
: shellPath.split('/').pop() || ''
|
|
77
|
+
const nameMatch = shellBase.match(shellNameReg)
|
|
78
|
+
const shellName = nameMatch ? nameMatch[1] : shellBase
|
|
79
|
+
const map = parseOsInfo(window.pre.osInfo())
|
|
80
|
+
const shellVersion = await probeShellVersion(shellPath)
|
|
81
|
+
return {
|
|
82
|
+
os: formatOsName(map),
|
|
83
|
+
hostname: map.hostname || '',
|
|
84
|
+
kernel: isWin ? `Windows NT ${map.release || ''}`.trim() : (map.release || ''),
|
|
85
|
+
arch: map.arch || '',
|
|
86
|
+
shell: shellName,
|
|
87
|
+
shellVersion
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -106,7 +106,11 @@ export default function TerminalInfoDisk (props) {
|
|
|
106
106
|
bordered: true,
|
|
107
107
|
size: 'small',
|
|
108
108
|
columns: col,
|
|
109
|
-
pagination: false
|
|
109
|
+
pagination: false,
|
|
110
|
+
scroll: {
|
|
111
|
+
y: 180,
|
|
112
|
+
x: 'max-content'
|
|
113
|
+
}
|
|
110
114
|
}
|
|
111
115
|
return (
|
|
112
116
|
<div className='terminal-info-section terminal-info-network'>
|
|
@@ -32,6 +32,33 @@ function formatActivities (str) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function formatSysInfo (unameStr, prettyName) {
|
|
36
|
+
if (!unameStr) {
|
|
37
|
+
return {}
|
|
38
|
+
}
|
|
39
|
+
const parts = unameStr.trim().split(/\s+/)
|
|
40
|
+
if (parts.length < 4) {
|
|
41
|
+
return {}
|
|
42
|
+
}
|
|
43
|
+
const [sysname, hostname, kernel, arch] = parts
|
|
44
|
+
let os = sysname
|
|
45
|
+
const m = (prettyName || '').match(/PRETTY_NAME="?([^"\r\n]+)"?/)
|
|
46
|
+
if (m) {
|
|
47
|
+
os = m[1].trim()
|
|
48
|
+
} else if (sysname === 'Darwin') {
|
|
49
|
+
os = 'macOS'
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
sysInfo: {
|
|
53
|
+
os,
|
|
54
|
+
sysname,
|
|
55
|
+
hostname,
|
|
56
|
+
kernel,
|
|
57
|
+
arch
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
function formatCpu (str) {
|
|
36
63
|
if (!str) {
|
|
37
64
|
return {
|
|
@@ -153,9 +180,11 @@ function InfoGetter (props) {
|
|
|
153
180
|
props.setState(update)
|
|
154
181
|
}
|
|
155
182
|
run()
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
183
|
+
if (interval > 0) {
|
|
184
|
+
const ref = setInterval(run, interval)
|
|
185
|
+
return () => {
|
|
186
|
+
clearInterval(ref)
|
|
187
|
+
}
|
|
159
188
|
}
|
|
160
189
|
}, [pid])
|
|
161
190
|
return null
|
|
@@ -166,6 +195,16 @@ export default (props) => {
|
|
|
166
195
|
return null
|
|
167
196
|
}
|
|
168
197
|
const cmds = [
|
|
198
|
+
{
|
|
199
|
+
name: 'sysinfo',
|
|
200
|
+
cmds: [
|
|
201
|
+
'uname -s -n -r -m',
|
|
202
|
+
'grep PRETTY_NAME= /etc/os-release 2>/dev/null || echo'
|
|
203
|
+
],
|
|
204
|
+
interval: 0,
|
|
205
|
+
delay: 0,
|
|
206
|
+
formatter: formatSysInfo
|
|
207
|
+
},
|
|
169
208
|
{
|
|
170
209
|
name: 'uptime',
|
|
171
210
|
cmd: 'uptime -p',
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* system info: os / hostname / kernel / arch (+ shell for local terminals)
|
|
3
|
+
* remote info comes from uname via the session runCmd bridge,
|
|
4
|
+
* local info from the os module + a shell version probe
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useEffect, useState } from 'react'
|
|
8
|
+
import { DesktopOutlined } from '@ant-design/icons'
|
|
9
|
+
import resolveLocalInfo from './local-info-resolver'
|
|
10
|
+
|
|
11
|
+
export default function TerminalInfoSys (props) {
|
|
12
|
+
const { sysInfo, isRemote, pid } = props
|
|
13
|
+
const [localInfo, setLocalInfo] = useState(null)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (isRemote) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
resolveLocalInfo(pid).then(setLocalInfo)
|
|
20
|
+
}, [isRemote, pid])
|
|
21
|
+
|
|
22
|
+
const info = isRemote ? sysInfo : localInfo
|
|
23
|
+
if (!info || !info.os) {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
const rows = [
|
|
27
|
+
['os', info.os],
|
|
28
|
+
['hostname', info.hostname],
|
|
29
|
+
['kernel', info.kernel],
|
|
30
|
+
['arch', info.arch]
|
|
31
|
+
]
|
|
32
|
+
if (info.shell) {
|
|
33
|
+
rows.push(['shell', info.shellVersion ? `${info.shell} ${info.shellVersion}` : info.shell])
|
|
34
|
+
}
|
|
35
|
+
const final = rows.filter(d => d[1])
|
|
36
|
+
if (!final.length) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
return (
|
|
40
|
+
<div className='terminal-info-section terminal-info-sys'>
|
|
41
|
+
<div className='pd1y bold'><DesktopOutlined /> System</div>
|
|
42
|
+
<div className='terminal-info-sys-rows'>
|
|
43
|
+
{
|
|
44
|
+
final.map(([k, v]) => {
|
|
45
|
+
return (
|
|
46
|
+
<div className='terminal-info-sys-row' key={k}>
|
|
47
|
+
<span className='terminal-info-sys-key'>{k}</span>
|
|
48
|
+
<span className='terminal-info-sys-val' title={v}>{v}</span>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|