@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.
Files changed (64) hide show
  1. package/README.md +14 -4
  2. package/client/common/bookmark-schemas.js +3 -0
  3. package/client/common/constants.js +16 -0
  4. package/client/common/create-title.jsx +5 -1
  5. package/client/common/default-setting.js +4 -2
  6. package/client/common/parse-vv.js +407 -0
  7. package/client/common/theme-defaults.js +5 -2
  8. package/client/common/update-check.js +5 -4
  9. package/client/common/vv-to-tab.js +120 -0
  10. package/client/components/ai/agent-tool-call-card.jsx +19 -4
  11. package/client/components/ai/agent.js +33 -16
  12. package/client/components/ai/ai-chat-history-item.jsx +12 -48
  13. package/client/components/ai/ai-chat.jsx +82 -25
  14. package/client/components/ai/ai-config-props.js +12 -1
  15. package/client/components/ai/ai-config.jsx +14 -0
  16. package/client/components/ai/ai-context-indicator.jsx +83 -0
  17. package/client/components/ai/ai-context.js +272 -0
  18. package/client/components/ai/ai-output.jsx +4 -0
  19. package/client/components/ai/ai-presets.js +13 -2
  20. package/client/components/ai/ai.styl +105 -6
  21. package/client/components/ai/copy-icon.jsx +31 -0
  22. package/client/components/bg/css-overwrite.jsx +16 -1
  23. package/client/components/bookmark-form/common/fields.jsx +3 -0
  24. package/client/components/bookmark-form/common/vv-file-field.jsx +138 -0
  25. package/client/components/bookmark-form/config/spice.js +22 -0
  26. package/client/components/footer/cmd-history.jsx +279 -65
  27. package/client/components/footer/cmd-history.styl +68 -1
  28. package/client/components/footer/footer-entry.jsx +46 -15
  29. package/client/components/icons/ai-icon.styl +4 -1
  30. package/client/components/layout/layout.jsx +12 -13
  31. package/client/components/main/main.jsx +23 -3
  32. package/client/components/main/term-fullscreen.styl +7 -0
  33. package/client/components/quick-commands/qm.styl +58 -0
  34. package/client/components/quick-commands/quick-command-ai-editor.jsx +255 -0
  35. package/client/components/quick-commands/quick-command-ai-editor.styl +13 -0
  36. package/client/components/quick-commands/quick-commands-box.jsx +197 -82
  37. package/client/components/quick-commands/quick-commands-form-elem.jsx +129 -65
  38. package/client/components/quick-commands/quick-commands-select.jsx +11 -0
  39. package/client/components/setting-panel/list.jsx +2 -2
  40. package/client/components/setting-panel/setting-common.jsx +2 -0
  41. package/client/components/side-panel-r/right-side-panel.styl +11 -1
  42. package/client/components/side-panel-r/side-panel-r.jsx +56 -13
  43. package/client/components/sidebar/bookmark-select.jsx +17 -3
  44. package/client/components/sidebar/history-item.jsx +20 -4
  45. package/client/components/sidebar/history.jsx +7 -0
  46. package/client/components/sidebar/index.jsx +13 -4
  47. package/client/components/sidebar/side-panel.jsx +8 -3
  48. package/client/components/sidebar/sidebar-panel.jsx +20 -7
  49. package/client/components/terminal/shortcut-bar.styl +14 -0
  50. package/client/components/terminal/unix-timestamp-tooltip.jsx +27 -1
  51. package/client/components/terminal-info/terminal-info.styl +3 -0
  52. package/client/components/theme/terminal-theme-list.styl +3 -0
  53. package/client/components/theme/theme-ai-editor.jsx +2 -2
  54. package/client/components/theme/theme-form.jsx +2 -2
  55. package/client/components/theme/theme-list-item.jsx +59 -25
  56. package/client/components/tree-list/tree-list.jsx +31 -3
  57. package/client/css/mobile.styl +28 -0
  58. package/client/store/common.js +164 -32
  59. package/client/store/init-state.js +10 -1
  60. package/client/store/load-data.js +72 -0
  61. package/client/store/sidebar.js +5 -1
  62. package/client/store/store.js +15 -2
  63. package/client/store/watch.js +3 -1
  64. package/package.json +1 -1
package/README.md CHANGED
@@ -5,7 +5,6 @@
5
5
  </h1>
6
6
 
7
7
  [![GitHub version](https://badgers.space/github/release/electerm/electerm?corner_radius=m)](https://github.com/electerm/electerm/releases)
8
- [![Build Status](https://github.com/electerm/electerm/actions/workflows/mac-test-1.yml/badge.svg)](https://github.com/electerm/electerm/actions)
9
8
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/electerm/electerm/blob/master/LICENSE)
10
9
  [![GitHub Sponsors](https://img.shields.io/github/sponsors/electerm?label=Sponsors)](https://github.com/sponsors/electerm)
11
10
  [![star](https://atomgit.com/electerm/electerm/star/badge.svg)](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" height="200" />
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://api.atlascloud.ai/v1',
82
- modelAI: 'deepseek-chat',
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: 'default',
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: 'defaultLight',
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
- let url = `${baseUpdateCheckUrls[0]}/data/electerm-github-release.json`
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,