@electerm/electerm-react 5.5.15 → 5.5.35
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 +14 -4
- package/client/common/bookmark-schemas.js +3 -0
- package/client/common/constants.js +16 -0
- package/client/common/create-title.jsx +5 -1
- package/client/common/default-setting.js +4 -2
- package/client/common/parse-vv.js +407 -0
- package/client/common/theme-defaults.js +5 -2
- package/client/common/update-check.js +5 -4
- package/client/common/vv-to-tab.js +120 -0
- package/client/components/ai/agent-tool-call-card.jsx +19 -4
- package/client/components/ai/agent.js +33 -16
- package/client/components/ai/ai-chat-history-item.jsx +12 -48
- package/client/components/ai/ai-chat.jsx +82 -25
- package/client/components/ai/ai-config-props.js +12 -1
- package/client/components/ai/ai-config.jsx +14 -0
- package/client/components/ai/ai-context-indicator.jsx +83 -0
- package/client/components/ai/ai-context.js +272 -0
- package/client/components/ai/ai-output.jsx +4 -0
- package/client/components/ai/ai-presets.js +13 -2
- package/client/components/ai/ai.styl +105 -6
- package/client/components/ai/copy-icon.jsx +31 -0
- package/client/components/bg/css-overwrite.jsx +16 -1
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/common/vv-file-field.jsx +138 -0
- package/client/components/bookmark-form/config/spice.js +22 -0
- package/client/components/footer/cmd-history.jsx +279 -65
- package/client/components/footer/cmd-history.styl +68 -1
- package/client/components/footer/footer-entry.jsx +46 -15
- package/client/components/icons/ai-icon.styl +4 -1
- package/client/components/layout/layout.jsx +12 -13
- package/client/components/main/main.jsx +23 -3
- package/client/components/main/term-fullscreen.styl +7 -0
- package/client/components/quick-commands/qm.styl +58 -0
- package/client/components/quick-commands/quick-command-ai-editor.jsx +255 -0
- package/client/components/quick-commands/quick-command-ai-editor.styl +13 -0
- package/client/components/quick-commands/quick-commands-box.jsx +197 -82
- package/client/components/quick-commands/quick-commands-form-elem.jsx +129 -65
- package/client/components/quick-commands/quick-commands-select.jsx +11 -0
- package/client/components/setting-panel/list.jsx +2 -2
- package/client/components/setting-panel/setting-common.jsx +2 -0
- package/client/components/side-panel-r/right-side-panel.styl +11 -1
- package/client/components/side-panel-r/side-panel-r.jsx +56 -13
- package/client/components/sidebar/bookmark-select.jsx +17 -3
- package/client/components/sidebar/history-item.jsx +20 -4
- package/client/components/sidebar/history.jsx +7 -0
- package/client/components/sidebar/index.jsx +13 -4
- package/client/components/sidebar/side-panel.jsx +8 -3
- package/client/components/sidebar/sidebar-panel.jsx +20 -7
- package/client/components/terminal/shortcut-bar.styl +14 -0
- package/client/components/terminal/unix-timestamp-tooltip.jsx +27 -1
- package/client/components/terminal-info/terminal-info.styl +3 -0
- package/client/components/theme/terminal-theme-list.styl +3 -0
- package/client/components/theme/theme-ai-editor.jsx +2 -2
- package/client/components/theme/theme-form.jsx +2 -2
- package/client/components/theme/theme-list-item.jsx +59 -25
- package/client/components/tree-list/tree-list.jsx +31 -3
- package/client/css/mobile.styl +28 -0
- package/client/store/common.js +164 -32
- package/client/store/init-state.js +10 -1
- package/client/store/load-data.js +72 -0
- package/client/store/sidebar.js +5 -1
- package/client/store/store.js +15 -2
- package/client/store/watch.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
</h1>
|
|
6
6
|
|
|
7
7
|
[](https://github.com/electerm/electerm/releases)
|
|
8
|
-
[](https://github.com/electerm/electerm/actions)
|
|
9
8
|
[](https://github.com/electerm/electerm/blob/master/LICENSE)
|
|
10
9
|
[](https://github.com/sponsors/electerm)
|
|
11
10
|
[](https://atomgit.com/electerm/electerm)
|
|
@@ -16,7 +15,7 @@ This is react components pack for electerm sub projects
|
|
|
16
15
|
|
|
17
16
|
Open-sourced terminal/ssh/sftp/telnet/serialport/RDP/VNC/Spice/ftp client(Linux, Mac, Windows, Android, HarmonyOS, iOS).
|
|
18
17
|
|
|
19
|
-
Besides mainstream Windows/macOS/Linux/Android, electerm also supports HarmonyOS, iOS, and older systems — Ubuntu 18, Windows 7, macOS 10+, and special Chinese Linux distributions such as UOS, Kylin, and LoongArch (both old-world and new-world).
|
|
18
|
+
Besides mainstream Windows/macOS/Linux/Android, electerm also supports HarmonyOS, iOS, and older systems — Ubuntu 18, Windows 7, macOS 10+, and special Chinese Linux distributions such as UOS, Kylin, and LoongArch (both old-world and new-world), as well as RISC-V (riscv64) and PowerPC 64-bit little-endian (ppc64le) Linux.
|
|
20
19
|
|
|
21
20
|
<p>
|
|
22
21
|
<a href="https://electerm.org">Homepage / Downloads</a> ·
|
|
@@ -41,7 +40,7 @@ Besides mainstream Windows/macOS/Linux/Android, electerm also supports HarmonyOS
|
|
|
41
40
|
|
|
42
41
|
<div align="center">
|
|
43
42
|
<a href="https://www.atlascloud.ai/?ref=PCAEL2&utm_source=github&utm_medium=link&utm_campaign=electerm">
|
|
44
|
-
<img src="https://github.com/electerm/electerm-resource/blob/master/static/images/atlas-cloud.png?raw=true" alt="Atlas Cloud"
|
|
43
|
+
<img src="https://github.com/electerm/electerm-resource/blob/master/static/images/atlas-cloud.png?raw=true" alt="Atlas Cloud" width="260" />
|
|
45
44
|
</a>
|
|
46
45
|
</div>
|
|
47
46
|
|
|
@@ -49,6 +48,17 @@ Besides mainstream Windows/macOS/Linux/Android, electerm also supports HarmonyOS
|
|
|
49
48
|
|
|
50
49
|
----
|
|
51
50
|
|
|
51
|
+
<div align="center">
|
|
52
|
+
<a href="https://fluxionai.world/register?source=github&campaign=electerm-20260923&promo=ELECTERM&aff=7H7DERHU3GFF">
|
|
53
|
+
<img src="https://github.com/electerm/electerm-resource/blob/master/static/images/fluxionai.png?raw=true" alt="FluxionAI" width="210" />
|
|
54
|
+
</a>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
[Fluxion AI](https://fluxionai.world/register?source=github&campaign=electerm-20260923&promo=ELECTERM&aff=7H7DERHU3GFF) — One gateway to the world's leading AI models, One API, multi-route delivery, transparent usage and pricing, Chinese Models — 60% OFF, Claude & OpenAI — 70% OFF [Register through the partner link
|
|
58
|
+
Get $3 in API credit](https://fluxionai.world/register?source=github&campaign=electerm-20260923&promo=ELECTERM&aff=7H7DERHU3GFF).
|
|
59
|
+
|
|
60
|
+
----
|
|
61
|
+
|
|
52
62
|
<div align="center">
|
|
53
63
|
<br />
|
|
54
64
|
<br />
|
|
@@ -86,7 +96,7 @@ Thanks to APIMart for sponsoring electerm! APIMart is a low-cost API platform fo
|
|
|
86
96
|
## Features
|
|
87
97
|
|
|
88
98
|
- Works as a terminal/file manager, ssh/sftp/ftp/telnet/serialport/RDP/VNC/Spice client
|
|
89
|
-
- Support Window 7+(X64/ARM64), HarmonyOS,Android, iOS, Mac OS 10.15+(x64/arm64), Linux(x64/arm64/Loong64 new world & old world), even old Linux with glibc 2.17+ like UOS/Kylin/Ubuntu 18.04 etc
|
|
99
|
+
- Support Window 7+(X64/ARM64), HarmonyOS,Android, iOS, Mac OS 10.15+(x64/arm64), Linux(x64/arm64/riscv64/ppc64le/Loong64 new world & old world), even old Linux with glibc 2.17+ like UOS/Kylin/Ubuntu 18.04 etc
|
|
90
100
|
- Global hotkey to toggle window visibility (similar to guake, default is `ctrl + 2`)
|
|
91
101
|
- 🇺🇸 🇨🇳 🇧🇷 🇷🇺 🇪🇸 🇫🇷 🇹🇷 🇭🇰 🇯🇵 🇸🇦 🇩🇪 🇰🇷 🇮🇩 🇵🇱 Multi-language support([electerm-locales](https://github.com/electerm/electerm-locales), contributions/fixes welcome)
|
|
92
102
|
- Double click to directly edit (small) remote files.
|
|
@@ -152,6 +152,9 @@ export const spiceBookmarkSchema = {
|
|
|
152
152
|
...commonNetworkBookmarkProps,
|
|
153
153
|
host: z.string().describe('Spice host address'),
|
|
154
154
|
port: z.number().optional().describe('Spice port (default 5900)'),
|
|
155
|
+
tls: z.boolean().optional().describe('Connect over TLS, default false'),
|
|
156
|
+
ca: z.string().optional().describe('CA certificate in PEM format, used to verify the TLS peer'),
|
|
157
|
+
hostSubject: z.string().optional().describe('Expected TLS certificate subject, like CN=pve1,O=Example'),
|
|
155
158
|
viewOnly: z.boolean().optional().describe('View only mode'),
|
|
156
159
|
scaleViewport: z.boolean().optional().describe('Scale viewport to window, default is true')
|
|
157
160
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* constants
|
|
3
3
|
*/
|
|
4
4
|
import { isMacJs } from './platform.js'
|
|
5
|
+
import { defaultThemeId, defaultThemeLightId } from './theme-defaults.js'
|
|
5
6
|
import logoPath1Ref from '@electerm/electerm-resource/res/imgs/electerm-round-128x128.png'
|
|
6
7
|
import logoPath2Ref from '@electerm/electerm-resource/res/imgs/electerm.png'
|
|
7
8
|
import logoPath3Ref from '@electerm/electerm-resource/res/imgs/electerm-watermark.png'
|
|
@@ -156,6 +157,9 @@ export const leftSideBarOpenKey = 'left-side-bar-open'
|
|
|
156
157
|
export const pinnedQuickCommandBarKey = 'pinned-quick-command-bar'
|
|
157
158
|
export const leftSidePanelWidthKey = 'left-sidebar-width'
|
|
158
159
|
export const rightSidebarWidthKey = 'right-sidebar-width'
|
|
160
|
+
export const rightPanelPinnedKey = 'right-panel-pinned'
|
|
161
|
+
export const cmdHistoryInRightPanelKey = 'cmd-history-in-right-panel'
|
|
162
|
+
export const quickCommandsInRightPanelKey = 'quick-commands-in-right-panel'
|
|
159
163
|
export const addPanelWidthLsKey = 'addPanelWidth'
|
|
160
164
|
export const sftpDefaultSortSettingKey = 'sftp-default-sort'
|
|
161
165
|
export const qmSortByFrequencyKey = 'qm-sort-by-frequency'
|
|
@@ -241,6 +245,18 @@ export const settingShortcutsId = 'setting-shortcuts'
|
|
|
241
245
|
export const settingAiId = 'setting-ai'
|
|
242
246
|
export const settingCommonId = 'setting-common'
|
|
243
247
|
export const settingPasswordsId = 'setting-passwords'
|
|
248
|
+
// Built in items the user is not allowed to delete: the two settings tabs,
|
|
249
|
+
// the default bookmark group and the two built in terminal themes.
|
|
250
|
+
// Keep this an explicit id list. A prefix test like `id.startsWith('default')`
|
|
251
|
+
// would also cover user facing seeds (default quick commands, default local
|
|
252
|
+
// terminal bookmarks) that the user must be able to remove.
|
|
253
|
+
export const undeletableIds = new Set([
|
|
254
|
+
settingSyncId,
|
|
255
|
+
settingCommonId,
|
|
256
|
+
defaultBookmarkGroupId,
|
|
257
|
+
defaultThemeId,
|
|
258
|
+
defaultThemeLightId
|
|
259
|
+
])
|
|
244
260
|
// settings that only apply to the desktop (electron) app,
|
|
245
261
|
// hidden when window.et.isWebApp is true
|
|
246
262
|
export const webAppHiddenSettings = [
|
|
@@ -28,8 +28,12 @@ export default function createTitle (res, hide = true) {
|
|
|
28
28
|
const h = hide && window.store.config.hideIP ? maskHost(host) : host
|
|
29
29
|
const fixTitle = `${username || ''}@${h}:${port}`
|
|
30
30
|
const extra = host || path ? (path || fixTitle) : (url || '')
|
|
31
|
+
// onlyShowTitleInTab: a session that was given its own title shows just that
|
|
32
|
+
// title. One without a title is left alone and keeps the host/user fallback,
|
|
33
|
+
// otherwise the tab would read as empty.
|
|
34
|
+
const onlyTitle = title && window.store.config.onlyShowTitleInTab
|
|
31
35
|
let f = title
|
|
32
|
-
? `${title}` + (extra ? ` - ${extra}` : '')
|
|
36
|
+
? `${title}` + (extra && !onlyTitle ? ` - ${extra}` : '')
|
|
33
37
|
: extra
|
|
34
38
|
if (connectionHoppings && connectionHoppings.length) {
|
|
35
39
|
f = `[⋙]${f}`
|
|
@@ -78,8 +78,8 @@ export default {
|
|
|
78
78
|
hideIP: false,
|
|
79
79
|
dataSyncSelected: 'all',
|
|
80
80
|
nameAI: '',
|
|
81
|
-
baseURLAI: 'https://
|
|
82
|
-
modelAI: 'deepseek-
|
|
81
|
+
baseURLAI: 'https://fluxionai.world/v1',
|
|
82
|
+
modelAI: 'deepseek-v4-flash-0731',
|
|
83
83
|
roleAI: '终端专家,提供不同系统下命令,简要解释用法,用markdown格式',
|
|
84
84
|
apiPathAI: '/chat/completions',
|
|
85
85
|
proxyAI: '',
|
|
@@ -94,8 +94,10 @@ export default {
|
|
|
94
94
|
dragDropBehavior: 'ask',
|
|
95
95
|
switchTabOnHover: false,
|
|
96
96
|
disableTabIndex: false,
|
|
97
|
+
onlyShowTitleInTab: false,
|
|
97
98
|
disableShortcutBar: false,
|
|
98
99
|
disableConfirmForLargeClipboardContent: false,
|
|
100
|
+
doubleClickToOpenBookmark: false,
|
|
99
101
|
leftSideBarIcons: [
|
|
100
102
|
'newBookmark',
|
|
101
103
|
'quickConnect',
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .vv connection file parser (virt-viewer / remote-viewer)
|
|
3
|
+
*
|
|
4
|
+
* A .vv file is an INI document with a mandatory [virt-viewer] group and a
|
|
5
|
+
* mandatory `type` key. Everything SPICE-related lives directly in
|
|
6
|
+
* [virt-viewer] -- there is NO [spice] group. An optional [ovirt] group
|
|
7
|
+
* carries oVirt REST integration data.
|
|
8
|
+
*
|
|
9
|
+
* Behaviour is deliberately matched against:
|
|
10
|
+
* - src/virt-viewer-file.c (gitlab.com/virt-viewer/virt-viewer)
|
|
11
|
+
* - src/virt-viewer-session-spice.c, for which port wins over tls-port
|
|
12
|
+
* - remote-viewer(1), "CONNECTION FILE" section
|
|
13
|
+
*
|
|
14
|
+
* Rejection rules copied from virt_viewer_file_new():
|
|
15
|
+
* no [virt-viewer] group -> invalid file
|
|
16
|
+
* no `type` key -> invalid file
|
|
17
|
+
* unknown keys/groups -> kept, never an error (the format is
|
|
18
|
+
* extensible; the convention is an `x-` prefix)
|
|
19
|
+
*
|
|
20
|
+
* This is the only copy. .vv loading happens in the renderer only, so there
|
|
21
|
+
* is no src/app/common twin -- unlike sanitize-filename.js, which the main
|
|
22
|
+
* process and the renderer both need.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const MAIN_GROUP = 'virt-viewer'
|
|
26
|
+
const OVIRT_GROUP = 'ovirt'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* .vv key -> bookmark field understood by
|
|
30
|
+
* src/client/components/bookmark-form/config/spice.js
|
|
31
|
+
*
|
|
32
|
+
* `port` and `tls-port` are NOT here: which one wins depends on whether the
|
|
33
|
+
* other is present, so they are resolved in parseVv().
|
|
34
|
+
*/
|
|
35
|
+
const FIELD_MAP = {
|
|
36
|
+
host: 'host',
|
|
37
|
+
password: 'password',
|
|
38
|
+
title: 'title',
|
|
39
|
+
proxy: 'proxy',
|
|
40
|
+
ca: 'ca',
|
|
41
|
+
'host-subject': 'hostSubject'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Keys parseVv() consumes itself, so the "not supported" pass must not report
|
|
46
|
+
* them again.
|
|
47
|
+
*/
|
|
48
|
+
const HANDLED = new Set([
|
|
49
|
+
'type',
|
|
50
|
+
'delete-this-file',
|
|
51
|
+
'port',
|
|
52
|
+
'tls-port',
|
|
53
|
+
...Object.keys(FIELD_MAP)
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Keys the .vv format defines but electerm cannot honour for a spice session.
|
|
58
|
+
* Each becomes a user-visible note instead of being dropped silently.
|
|
59
|
+
*/
|
|
60
|
+
const UNSUPPORTED = {
|
|
61
|
+
'tls-ciphers': 'TLS is used, but the cipher list is not configurable',
|
|
62
|
+
'unix-path': 'Unix-socket transport is not supported',
|
|
63
|
+
username: 'spice sessions authenticate with the password only',
|
|
64
|
+
'disable-channels': 'spice channel selection is not supported',
|
|
65
|
+
'secure-channels': 'spice channel selection is not supported',
|
|
66
|
+
'color-depth': 'colour depth is negotiated with the server',
|
|
67
|
+
'disable-effects': 'remote desktop-effect control is not supported',
|
|
68
|
+
'enable-smartcard': 'smartcard redirection is not supported',
|
|
69
|
+
'enable-usbredir': 'USB redirection is not supported',
|
|
70
|
+
'enable-usb-autoshare': 'USB redirection is not supported',
|
|
71
|
+
'usb-filter': 'USB redirection is not supported',
|
|
72
|
+
'usb-redirect-on-connect': 'USB redirection is not supported',
|
|
73
|
+
fullscreen: 'fullscreen is a session-time toggle',
|
|
74
|
+
version: 'client version requirements are not enforced',
|
|
75
|
+
versions: 'client version requirements are not enforced',
|
|
76
|
+
'newer-version-url': 'client version requirements are not enforced',
|
|
77
|
+
'toggle-fullscreen': 'hotkey rebinding is not supported',
|
|
78
|
+
'release-cursor': 'hotkey rebinding is not supported',
|
|
79
|
+
'secure-attention': 'hotkey rebinding is not supported',
|
|
80
|
+
'zoom-in': 'hotkey rebinding is not supported',
|
|
81
|
+
'zoom-out': 'hotkey rebinding is not supported',
|
|
82
|
+
'zoom-reset': 'hotkey rebinding is not supported',
|
|
83
|
+
'smartcard-insert': 'hotkey rebinding is not supported',
|
|
84
|
+
'smartcard-remove': 'hotkey rebinding is not supported',
|
|
85
|
+
'usb-device-reset': 'hotkey rebinding is not supported'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Proxmox writes the SPICE proxy ticket into `host` (see
|
|
90
|
+
* PVE::AccessControl::remote_viewer_config) and puts the real node name inside
|
|
91
|
+
* it. PVE::Ticket::verify_spice_connect_url() only accepts a ticket younger
|
|
92
|
+
* than 40 seconds, so a .vv from the Proxmox UI is strictly single-use.
|
|
93
|
+
*/
|
|
94
|
+
const PROXMOX_TICKET = /^pvespiceproxy:/i
|
|
95
|
+
|
|
96
|
+
const BOOL_TRUE = new Set(['1', 'true', 'yes', 'on'])
|
|
97
|
+
|
|
98
|
+
function stripBom (text) {
|
|
99
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isComment (line) {
|
|
103
|
+
return line.startsWith('#') || line.startsWith(';')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* GKeyFile escapes \\n, \\t, \\r, \\s and \\\\ inside values, and
|
|
108
|
+
* g_key_file_get_string() expands them on read. parseIni keeps values
|
|
109
|
+
* verbatim (a trailing space in a password is significant), so the one value
|
|
110
|
+
* that is handed to a parser rather than used as-is is expanded here.
|
|
111
|
+
*
|
|
112
|
+
* Proxmox relies on this: it writes the whole CA on a single line with literal
|
|
113
|
+
* "\\n" separators (`$cacert =~ s/\n/\\n/g`), which no PEM parser accepts
|
|
114
|
+
* unexpanded.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} value
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
function unescapeValue (value) {
|
|
120
|
+
return value.replace(/\\(.)/g, (match, char) => {
|
|
121
|
+
switch (char) {
|
|
122
|
+
case 'n': return '\n'
|
|
123
|
+
case 't': return '\t'
|
|
124
|
+
case 'r': return '\r'
|
|
125
|
+
case 's': return ' '
|
|
126
|
+
case '\\': return '\\'
|
|
127
|
+
default: return match
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Read and range-check one of the two port keys.
|
|
134
|
+
* @returns {number|null}
|
|
135
|
+
*/
|
|
136
|
+
function readPort (main, key, result) {
|
|
137
|
+
if (!(key in main)) {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
const raw = main[key].trim()
|
|
141
|
+
const port = parseInt(raw, 10)
|
|
142
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
143
|
+
result.warnings.push('ignoring invalid ' + key + ' "' + raw + '"')
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
return port
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Parse INI text into { groupName: { key: value } }.
|
|
151
|
+
*
|
|
152
|
+
* GKeyFile itself is case-sensitive; group and key names are lowercased on
|
|
153
|
+
* purpose, because hand-written .vv files are inconsistent about casing and
|
|
154
|
+
* the alternative is a confusing "host not set" for a file that clearly
|
|
155
|
+
* sets `Host=`.
|
|
156
|
+
*
|
|
157
|
+
* A trailing backslash continues the value on the next line (GKeyFile rule).
|
|
158
|
+
*
|
|
159
|
+
* @param {string} text
|
|
160
|
+
* @returns {Object} groups
|
|
161
|
+
*/
|
|
162
|
+
function parseIni (text) {
|
|
163
|
+
const groups = {}
|
|
164
|
+
let group = null
|
|
165
|
+
const lines = stripBom(text).split(/\r\n|\r|\n/)
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < lines.length; i++) {
|
|
168
|
+
let line = lines[i]
|
|
169
|
+
|
|
170
|
+
while (/\\[ \t]*$/.test(line) && i + 1 < lines.length) {
|
|
171
|
+
line = line.replace(/\\[ \t]*$/, '') + lines[++i]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const trimmed = line.trim()
|
|
175
|
+
if (!trimmed || isComment(trimmed)) {
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const groupMatch = trimmed.match(/^\[(.+)\]$/)
|
|
180
|
+
if (groupMatch) {
|
|
181
|
+
group = groupMatch[1].trim().toLowerCase()
|
|
182
|
+
if (!groups[group]) {
|
|
183
|
+
groups[group] = {}
|
|
184
|
+
}
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const eq = line.indexOf('=')
|
|
189
|
+
if (eq < 0 || !group) {
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const key = line.slice(0, eq).trim().toLowerCase()
|
|
194
|
+
// Only leading whitespace is stripped. A trailing space is significant
|
|
195
|
+
// for values such as passwords, so it is preserved.
|
|
196
|
+
const value = line.slice(eq + 1).replace(/^[ \t]+/, '')
|
|
197
|
+
if (key) {
|
|
198
|
+
groups[group][key] = value
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return groups
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Parse the contents of a .vv file.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} text - raw contents of a .vv file
|
|
209
|
+
* @returns {{
|
|
210
|
+
* ok: boolean, error: string|null, type: string|null,
|
|
211
|
+
* fields: Object, applied: Array, ignored: Array,
|
|
212
|
+
* unknown: Array, warnings: string[], deleteThisFile: boolean,
|
|
213
|
+
* groups: Object
|
|
214
|
+
* }}
|
|
215
|
+
*/
|
|
216
|
+
function parseVv (text) {
|
|
217
|
+
const result = {
|
|
218
|
+
ok: false,
|
|
219
|
+
error: null,
|
|
220
|
+
type: null,
|
|
221
|
+
fields: {},
|
|
222
|
+
applied: [],
|
|
223
|
+
ignored: [],
|
|
224
|
+
unknown: [],
|
|
225
|
+
warnings: [],
|
|
226
|
+
deleteThisFile: false,
|
|
227
|
+
groups: {}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
231
|
+
result.error = 'empty file'
|
|
232
|
+
return result
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let groups
|
|
236
|
+
try {
|
|
237
|
+
groups = parseIni(text)
|
|
238
|
+
} catch (e) {
|
|
239
|
+
result.error = 'cannot parse file: ' + e.message
|
|
240
|
+
return result
|
|
241
|
+
}
|
|
242
|
+
result.groups = groups
|
|
243
|
+
|
|
244
|
+
const main = groups[MAIN_GROUP]
|
|
245
|
+
if (!main) {
|
|
246
|
+
result.error = 'invalid .vv file: missing [' + MAIN_GROUP + '] group'
|
|
247
|
+
return result
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const type = (main.type || '').trim().toLowerCase()
|
|
251
|
+
if (!type) {
|
|
252
|
+
result.error = 'invalid .vv file: missing "type" key in [' + MAIN_GROUP + ']'
|
|
253
|
+
return result
|
|
254
|
+
}
|
|
255
|
+
result.type = type
|
|
256
|
+
if (type !== 'spice') {
|
|
257
|
+
result.error = 'unsupported session type "' + type + '": spice only'
|
|
258
|
+
return result
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// spice-gtk dials the plain port and only falls back to TLS when the file
|
|
262
|
+
// offers no plain port at all (spice-channel.c: "trying with TLS port"), so
|
|
263
|
+
// a file that sets both keeps its plain port here too.
|
|
264
|
+
//
|
|
265
|
+
// Which port key the file used also decides the transport, so `tls` is set
|
|
266
|
+
// on both branches: the form only overwrites the keys it is handed, and a
|
|
267
|
+
// switch left over from a previous file would silently break the connection.
|
|
268
|
+
const plainPort = readPort(main, 'port', result)
|
|
269
|
+
const tlsPort = readPort(main, 'tls-port', result)
|
|
270
|
+
|
|
271
|
+
if (plainPort) {
|
|
272
|
+
result.fields.port = plainPort
|
|
273
|
+
result.fields.tls = false
|
|
274
|
+
result.applied.push({ key: 'port', value: main.port, field: 'port' })
|
|
275
|
+
} else if (tlsPort) {
|
|
276
|
+
result.fields.port = tlsPort
|
|
277
|
+
result.fields.tls = true
|
|
278
|
+
result.applied.push({
|
|
279
|
+
key: 'tls-port',
|
|
280
|
+
value: main['tls-port'],
|
|
281
|
+
field: 'port'
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
Object.keys(FIELD_MAP).forEach(key => {
|
|
286
|
+
if (!(key in main)) {
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
const raw = main[key]
|
|
290
|
+
const field = FIELD_MAP[key]
|
|
291
|
+
|
|
292
|
+
if (key === 'password') {
|
|
293
|
+
result.fields.password = raw
|
|
294
|
+
} else {
|
|
295
|
+
const value = raw.trim()
|
|
296
|
+
if (!value) {
|
|
297
|
+
result.warnings.push('ignoring empty "' + key + '"')
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
result.fields[field] = key === 'ca' ? unescapeValue(value) : value
|
|
301
|
+
}
|
|
302
|
+
result.applied.push({ key, value: raw, field })
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
if (!result.fields.host) {
|
|
306
|
+
result.error = 'no usable host: [virt-viewer] has no "host" key'
|
|
307
|
+
return result
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// The Proxmox ticket in `host` is signed and time limited: the proxy rejects
|
|
311
|
+
// anything older than 40 seconds, so a saved bookmark can never work.
|
|
312
|
+
if (PROXMOX_TICKET.test(result.fields.host)) {
|
|
313
|
+
result.warnings.push(
|
|
314
|
+
'this is a Proxmox SPICE proxy ticket: it is only valid for about 40 ' +
|
|
315
|
+
'seconds, so connect right away -- saving it as a bookmark will not work'
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// delete-this-file=1 makes remote-viewer unlink the file after loading it
|
|
320
|
+
// (unless VIRT_VIEWER_KEEP_FILE is set). We never delete the user's file;
|
|
321
|
+
// surface it instead so the caller can decide.
|
|
322
|
+
if ('delete-this-file' in main) {
|
|
323
|
+
result.deleteThisFile = BOOL_TRUE.has(
|
|
324
|
+
main['delete-this-file'].trim().toLowerCase()
|
|
325
|
+
)
|
|
326
|
+
if (result.deleteThisFile) {
|
|
327
|
+
result.warnings.push(
|
|
328
|
+
'the file is marked delete-this-file=1; it was NOT deleted'
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
Object.keys(main).forEach(key => {
|
|
334
|
+
if (HANDLED.has(key)) {
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
const value = main[key]
|
|
338
|
+
if (key in UNSUPPORTED) {
|
|
339
|
+
result.ignored.push({ key, value, reason: UNSUPPORTED[key] })
|
|
340
|
+
} else {
|
|
341
|
+
result.unknown.push({ key, value })
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// Both ports offered: the plain one wins (see above), but say so instead of
|
|
346
|
+
// dropping the TLS port in silence.
|
|
347
|
+
if (plainPort && tlsPort) {
|
|
348
|
+
result.ignored.push({
|
|
349
|
+
key: 'tls-port',
|
|
350
|
+
value: main['tls-port'],
|
|
351
|
+
reason:
|
|
352
|
+
'this file also offers a plain port, which is the one remote-viewer uses'
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (groups[OVIRT_GROUP]) {
|
|
357
|
+
result.ignored.push({
|
|
358
|
+
key: '[' + OVIRT_GROUP + ']',
|
|
359
|
+
value: Object.keys(groups[OVIRT_GROUP]).join(', '),
|
|
360
|
+
reason: 'oVirt REST integration is not supported'
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (result.ignored.length) {
|
|
365
|
+
result.warnings.push(
|
|
366
|
+
result.ignored.length +
|
|
367
|
+
' option(s) in this file are not supported and were skipped'
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
result.ok = true
|
|
372
|
+
return result
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Turn a parse result into a one-line summary for a UI hint.
|
|
377
|
+
*
|
|
378
|
+
* @param {Object} parsed - result of parseVv
|
|
379
|
+
* @returns {string}
|
|
380
|
+
*/
|
|
381
|
+
function describeVv (parsed) {
|
|
382
|
+
if (!parsed || !parsed.ok) {
|
|
383
|
+
return (parsed && parsed.error) || 'invalid .vv file'
|
|
384
|
+
}
|
|
385
|
+
const { host, port, title, proxy, tls } = parsed.fields
|
|
386
|
+
const bits = ['spice' + (tls ? '+tls' : '') + '://' + host + (port ? ':' + port : '')]
|
|
387
|
+
if (title) {
|
|
388
|
+
bits.push('"' + title + '"')
|
|
389
|
+
}
|
|
390
|
+
if (proxy) {
|
|
391
|
+
bits.push('via ' + proxy)
|
|
392
|
+
}
|
|
393
|
+
if (parsed.ignored.length) {
|
|
394
|
+
bits.push(parsed.ignored.length + ' option(s) skipped')
|
|
395
|
+
}
|
|
396
|
+
return bits.join(' ')
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export {
|
|
400
|
+
parseVv,
|
|
401
|
+
parseIni,
|
|
402
|
+
describeVv,
|
|
403
|
+
MAIN_GROUP,
|
|
404
|
+
OVIRT_GROUP,
|
|
405
|
+
FIELD_MAP,
|
|
406
|
+
UNSUPPORTED
|
|
407
|
+
}
|
|
@@ -101,9 +101,12 @@ const defaultThemeDarkTerminal = () => {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
export const defaultThemeId = 'default'
|
|
105
|
+
export const defaultThemeLightId = 'defaultLight'
|
|
106
|
+
|
|
104
107
|
export function defaultTheme () {
|
|
105
108
|
return {
|
|
106
|
-
id:
|
|
109
|
+
id: defaultThemeId,
|
|
107
110
|
name: 'default',
|
|
108
111
|
themeConfig: defaultThemeDarkTerminal(),
|
|
109
112
|
uiThemeConfig: defaultThemeDark()
|
|
@@ -112,7 +115,7 @@ export function defaultTheme () {
|
|
|
112
115
|
|
|
113
116
|
export function defaultThemeLight () {
|
|
114
117
|
return {
|
|
115
|
-
id:
|
|
118
|
+
id: defaultThemeLightId,
|
|
116
119
|
name: 'default light',
|
|
117
120
|
themeConfig: defaultThemeLightTerminal(),
|
|
118
121
|
uiThemeConfig: defaultThemeLightFunc()
|
|
@@ -23,7 +23,7 @@ async function fetchData (url, options) {
|
|
|
23
23
|
|
|
24
24
|
function getInfo (url) {
|
|
25
25
|
const n = Date.now()
|
|
26
|
-
const tail = url.includes('?') ? '' : '?_=' + n
|
|
26
|
+
const tail = url.includes('?') ? '&_=' + n : '?_=' + n
|
|
27
27
|
return fetchData(url + tail, {
|
|
28
28
|
action: 'get-update-info',
|
|
29
29
|
headers: {
|
|
@@ -87,16 +87,17 @@ export function getDownloadUrl (browserDownloadUrl, mirror) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export async function getLatestReleaseInfo () {
|
|
90
|
-
|
|
90
|
+
const { installSrc } = window.store
|
|
91
|
+
const src = installSrc ? '?src=' + encodeURIComponent(installSrc) : ''
|
|
92
|
+
let url = `${baseUpdateCheckUrls[0]}/data/electerm-github-release.json${src}`
|
|
91
93
|
let res = await getInfo(url)
|
|
92
94
|
if (!res?.release?.body) {
|
|
93
|
-
url = `${baseUpdateCheckUrls[1]}/data/electerm-github-release.json`
|
|
95
|
+
url = `${baseUpdateCheckUrls[1]}/data/electerm-github-release.json${src}`
|
|
94
96
|
res = await getInfo(url)
|
|
95
97
|
}
|
|
96
98
|
if (!res || !res.release) {
|
|
97
99
|
return undefined
|
|
98
100
|
}
|
|
99
|
-
const { installSrc } = window.store
|
|
100
101
|
const asset = (res.release.assets || []).find(r => r.name.includes(installSrc))
|
|
101
102
|
return {
|
|
102
103
|
body: res.release.body,
|