@electerm/electerm-react 5.1.23 → 5.2.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.
Files changed (37) hide show
  1. package/client/common/build-map.js +12 -0
  2. package/client/common/db.js +7 -5
  3. package/client/common/import-task.js +174 -0
  4. package/client/components/bookmark-form/ai-bookmark-form.jsx +18 -15
  5. package/client/components/bookmark-form/common/bookmark-select.jsx +6 -5
  6. package/client/components/bookmark-form/common/fields.jsx +1 -1
  7. package/client/components/bookmark-form/common/profile-item.jsx +12 -2
  8. package/client/components/bookmark-form/config/rdp.js +1 -1
  9. package/client/components/bookmark-form/tree-select.jsx +5 -16
  10. package/client/components/common/import-progress.jsx +90 -0
  11. package/client/components/layout/layout.jsx +6 -4
  12. package/client/components/main/main.jsx +2 -0
  13. package/client/components/profile/profile-form-rdp.jsx +1 -1
  14. package/client/components/quick-commands/quick-command-transport-mod.jsx +24 -18
  15. package/client/components/rdp/rdp-cred-form.jsx +55 -0
  16. package/client/components/rdp/rdp-session.jsx +97 -6
  17. package/client/components/setting-panel/keywords-transport.jsx +1 -4
  18. package/client/components/setting-panel/setting-modal.jsx +8 -2
  19. package/client/components/setting-panel/start-session-select.jsx +5 -16
  20. package/client/components/spice/spice-session.jsx +40 -0
  21. package/client/components/terminal/shortcut-bar-entry.jsx +4 -2
  22. package/client/components/terminal/shortcut-bar.jsx +59 -2
  23. package/client/components/terminal/shortcut-bar.styl +4 -2
  24. package/client/components/terminal/terminal-color-query.mjs +32 -27
  25. package/client/components/terminal/terminal.jsx +13 -9
  26. package/client/components/tree-list/bookmark-transport.jsx +2 -1
  27. package/client/components/tree-list/bookmark-upload.js +56 -41
  28. package/client/components/tree-list/tree-list-row.jsx +11 -4
  29. package/client/components/tree-list/tree-list-rows.js +6 -3
  30. package/client/components/tree-list/tree-list.jsx +4 -2
  31. package/client/store/common.js +19 -1
  32. package/client/store/init-state.js +3 -0
  33. package/client/store/load-data.js +6 -1
  34. package/client/store/store.js +3 -12
  35. package/client/store/sync.js +31 -2
  36. package/client/store/watch.js +10 -1
  37. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { PureComponent, createRef } from 'react'
2
2
  import { createTerm } from '../terminal/terminal-apis'
3
3
  import deepCopy from 'json-deep-copy'
4
+ import { pick } from 'lodash-es'
4
5
  import clone from '../../common/to-simple-obj'
5
6
  import { handleErr } from '../../common/fetch'
6
7
  import {
@@ -24,6 +25,8 @@ import { readClipboardAsync } from '../../common/clipboard'
24
25
  import RemoteFloatControl from '../common/remote-float-control'
25
26
  import SwitchLabel from '../common/switch'
26
27
  import HelpIcon from '../common/help-icon'
28
+ import Modal from '../common/modal'
29
+ import RdpCredForm from './rdp-cred-form'
27
30
  import { FileTransferManager, createFileLogger } from './file-transfer'
28
31
  import { notification } from '../common/notification'
29
32
  import message from '../common/message'
@@ -65,7 +68,8 @@ export default class RdpSession extends PureComponent {
65
68
  ...resObj,
66
69
  hasRemoteFiles: false,
67
70
  downloadBtnDisabled: true,
68
- uploadReady: false
71
+ uploadReady: false,
72
+ showCredPrompt: false
69
73
  }
70
74
  this.session = null
71
75
  this.fileTransfer = null
@@ -121,13 +125,25 @@ export default class RdpSession extends PureComponent {
121
125
  return `ws://${host}:${port}/${type}/${id}?token=${tokenElecterm}${extra}`
122
126
  }
123
127
 
124
- remoteInit = async () => {
128
+ remoteInit = async (credOverride) => {
125
129
  this.setState({
126
130
  loading: true
127
131
  })
128
132
  const { config } = this.props
129
133
  const { id } = this.props
130
- const tab = window.store.applyProfile(deepCopy(this.props.tab || {}))
134
+ const tab = {
135
+ ...window.store.applyProfile(deepCopy(this.props.tab || {})),
136
+ ...credOverride
137
+ }
138
+ // keep resolved credentials for the connect-time prompt
139
+ this.tab = tab
140
+ if (!tab.username || !tab.password) {
141
+ this.setState({
142
+ loading: false,
143
+ showCredPrompt: true
144
+ })
145
+ return
146
+ }
131
147
  const {
132
148
  type,
133
149
  term: terminalType
@@ -192,6 +208,9 @@ export default class RdpSession extends PureComponent {
192
208
  const builder = new window.ironRdp.SessionBuilder()
193
209
  builder.username(username)
194
210
  builder.password(password)
211
+ if (tab.domain) {
212
+ builder.serverDomain(tab.domain)
213
+ }
195
214
  builder.destination(destination)
196
215
  builder.proxyAddress(proxyAddress)
197
216
  builder.authToken('none')
@@ -277,12 +296,16 @@ export default class RdpSession extends PureComponent {
277
296
  this.log(`Session ended: ${info.reason()}`, 'info')
278
297
  this.onSessionEnd()
279
298
  }).catch((e) => {
280
- this.log(`Session error: ${this.formatError(e)}`, 'error')
299
+ this.showError(e)
281
300
  this.onSessionEnd()
282
301
  })
283
302
  } catch (e) {
284
- this.log(`Connection failed: ${this.formatError(e)}`, 'error')
285
- this.setState({ loading: false })
303
+ this.showError(e)
304
+ this.setState({
305
+ loading: false,
306
+ // let the user retry with different credentials on auth failure
307
+ showCredPrompt: this.isAuthError(e) ? true : this.state.showCredPrompt
308
+ })
286
309
  this.setStatus(statusMap.error)
287
310
  }
288
311
  }
@@ -307,6 +330,73 @@ export default class RdpSession extends PureComponent {
307
330
  return e?.message || e?.toString() || String(e)
308
331
  }
309
332
 
333
+ // WrongPassword(1) / LogonFailure(2)
334
+ isAuthError = (err) => {
335
+ if (err && typeof err === 'object' && '__wbg_ptr' in err && err.kind) {
336
+ try {
337
+ const kind = err.kind()
338
+ return kind === 1 || kind === 2
339
+ } catch (_) { }
340
+ }
341
+ return false
342
+ }
343
+
344
+ // map known IronErrorKind codes to a user-friendly message
345
+ errText = (err) => {
346
+ if (this.isAuthError(err)) {
347
+ return window.translate('loginFail') + ` (${this.formatError(err)})`
348
+ }
349
+ return this.formatError(err)
350
+ }
351
+
352
+ showError = (err) => {
353
+ const text = this.errText(err)
354
+ this.log(`Connection failed: ${this.formatError(err)}`, 'error')
355
+ message.error(text, 10)
356
+ }
357
+
358
+ onCredSubmit = (res) => {
359
+ this.setState({
360
+ showCredPrompt: false
361
+ })
362
+ // tear down the failed attempt before reconnecting
363
+ this.cleanup()
364
+ // reconnect with the provided credentials, not persisted to the bookmark
365
+ this.remoteInit(res)
366
+ }
367
+
368
+ onCredCancel = () => {
369
+ this.setState({
370
+ showCredPrompt: false
371
+ })
372
+ this.setStatus(statusMap.error)
373
+ }
374
+
375
+ renderCredPrompt = () => {
376
+ const {
377
+ showCredPrompt
378
+ } = this.state
379
+ if (!showCredPrompt) {
380
+ return null
381
+ }
382
+ const confirmProps = {
383
+ title: window.translate('credentialsRequired'),
384
+ footer: null,
385
+ open: true,
386
+ onCancel: this.onCredCancel
387
+ }
388
+ return (
389
+ <Modal
390
+ {...confirmProps}
391
+ >
392
+ <RdpCredForm
393
+ initialValues={pick(this.tab, ['username', 'password'])}
394
+ handleFinish={this.onCredSubmit}
395
+ />
396
+ </Modal>
397
+ )
398
+ }
399
+
310
400
  syncLocalToRemote = async () => {
311
401
  if (!this.session || this.fileUploadInProgress) {
312
402
  return
@@ -751,6 +841,7 @@ export default class RdpSession extends PureComponent {
751
841
  ref={this.canvasRef}
752
842
  />
753
843
  </div>
844
+ {this.renderCredPrompt()}
754
845
  </div>
755
846
  </Spin>
756
847
  )
@@ -4,6 +4,7 @@ import time from '../../common/time'
4
4
 
5
5
  export default class KeywordsTransport extends BookmarkTransport {
6
6
  name = 'keywords-highlight'
7
+
7
8
  beforeUpload = async (file) => {
8
9
  const { store } = this.props
9
10
  const txt = file.fileContent !== undefined
@@ -20,10 +21,6 @@ export default class KeywordsTransport extends BookmarkTransport {
20
21
  return false
21
22
  }
22
23
 
23
- renderEdit () {
24
- return null
25
- }
26
-
27
24
  handleDownload = () => {
28
25
  const { store } = this.props
29
26
  const arr = store.config.keywords || []
@@ -33,14 +33,20 @@ export default auto(function SettingModalWrap (props) {
33
33
  settingMap.bookmarks,
34
34
  settingMap.terminalThemes
35
35
  ]
36
- const { settingTab, settingItem, settingSidebarList, bookmarkSelectMode } = store
36
+ const { settingTab, settingItem, bookmarkSelectMode } = store
37
+ // settingSidebarList deep-copies the whole collection; the bookmarks tab
38
+ // ignores `list` (it renders from treeProps), so skip it there — with
39
+ // thousands of bookmarks that copy is the most expensive part of the render
40
+ const list = settingTab === settingMap.bookmarks
41
+ ? []
42
+ : store.settingSidebarList
37
43
  const props0 = {
38
44
  store,
39
45
  activeItemId: settingItem.id,
40
46
  type: settingTab,
41
47
  onClickItem: selectItem,
42
48
  shouldConfirmDel: tabsShouldConfirmDel.includes(settingTab),
43
- list: settingSidebarList
49
+ list
44
50
  }
45
51
  const { bookmarks, bookmarkGroups, widgetInstances } = store
46
52
  const formProps = {
@@ -7,6 +7,7 @@ import {
7
7
  BookOutlined
8
8
  } from '@ant-design/icons'
9
9
  import HelpIcon from '../common/help-icon'
10
+ import buildMap from '../../common/build-map'
10
11
 
11
12
  const e = window.translate
12
13
  const { SHOW_CHILD } = TreeSelect
@@ -21,22 +22,10 @@ function BookmarkSelect (props) {
21
22
 
22
23
  const buildData = () => {
23
24
  const cats = bookmarkGroups
24
- const tree = bookmarks
25
- .reduce((p, k) => {
26
- return {
27
- ...p,
28
- [k.id]: k
29
- }
30
- }, {})
31
- const btree = cats
32
- .reduce((p, k) => {
33
- return {
34
- ...p,
35
- [k.id]: k
36
- }
37
- }, {})
25
+ const tree = buildMap(bookmarks)
26
+ const btree = buildMap(cats)
38
27
  function buildSubCats (id) {
39
- const x = btree[id]
28
+ const x = btree.get(id)
40
29
  if (!x) {
41
30
  return ''
42
31
  }
@@ -55,7 +44,7 @@ function BookmarkSelect (props) {
55
44
  return y
56
45
  }
57
46
  function buildLeaf (id) {
58
- const x = tree[id]
47
+ const x = tree.get(id)
59
48
  if (!x) {
60
49
  return ''
61
50
  }
@@ -44,12 +44,52 @@ export default class SpiceSession extends PureComponent {
44
44
 
45
45
  componentDidMount () {
46
46
  this.remoteInit()
47
+ this.attachMouseScaleFix()
47
48
  }
48
49
 
49
50
  componentWillUnmount () {
51
+ this.detachMouseScaleFix()
50
52
  this.cleanup()
51
53
  }
52
54
 
55
+ // The spice client sends event.offsetX/offsetY to the remote as-is, but when
56
+ // the canvas is CSS-scaled (scaleViewport or the lib's maxWidth/maxHeight),
57
+ // those are display pixels instead of remote surface pixels. On high
58
+ // resolution remote screens the pointer then only reaches the top-left
59
+ // region. Convert display coords to surface coords (the canvas box is
60
+ // letterboxed by object-fit: contain) before the lib's handlers run.
61
+ handleMouseScaleFix = (e) => {
62
+ const canvas = this.domRef.current?.querySelector('canvas')
63
+ if (!canvas || !canvas.width || !canvas.height) return
64
+ const rect = canvas.getBoundingClientRect()
65
+ if (!rect.width || !rect.height) return
66
+ const scale = Math.min(rect.width / canvas.width, rect.height / canvas.height)
67
+ const x = (e.clientX - rect.left - (rect.width - canvas.width * scale) / 2) / scale
68
+ const y = (e.clientY - rect.top - (rect.height - canvas.height * scale) / 2) / scale
69
+ Object.defineProperty(e, 'offsetX', {
70
+ value: Math.max(0, Math.min(x, canvas.width - 1))
71
+ })
72
+ Object.defineProperty(e, 'offsetY', {
73
+ value: Math.max(0, Math.min(y, canvas.height - 1))
74
+ })
75
+ }
76
+
77
+ attachMouseScaleFix = () => {
78
+ this.domRef.current?.addEventListener(
79
+ 'mousemove',
80
+ this.handleMouseScaleFix,
81
+ true
82
+ )
83
+ }
84
+
85
+ detachMouseScaleFix = () => {
86
+ this.domRef.current?.removeEventListener(
87
+ 'mousemove',
88
+ this.handleMouseScaleFix,
89
+ true
90
+ )
91
+ }
92
+
53
93
  cleanup = () => {
54
94
  if (this.spiceConn) {
55
95
  try {
@@ -31,7 +31,9 @@ const ShortcutBar = lazy(() => import('./shortcut-bar'))
31
31
  // minimum height (px) the visual viewport must lose for us to treat it as
32
32
  // "the soft keyboard is now covering part of the screen". A soft keyboard is
33
33
  // typically ≥150px; a browser address-bar show/hide is well under this.
34
- const KEYBOARD_HEIGHT = 120
34
+ // Shared with shortcut-bar.jsx, which uses it to decide how far to lift the
35
+ // bar above a keyboard that overlays the page (iOS / HarmonyOS).
36
+ export const KEYBOARD_MIN = 120
35
37
 
36
38
  // kept as a coarse capability fallback for engines without visualViewport.
37
39
  // Modern Chromium (Electron) always exposes window.visualViewport, so this is
@@ -87,7 +89,7 @@ export default function ShortcutBarEntry (props) {
87
89
  // ignore viewport changes that also changed width (rotation / split) —
88
90
  // a keyboard changes height only.
89
91
  const sameWidth = Math.abs(vv.width - baseline.width) < 20
90
- if (sameWidth && baseline.height - vv.height > KEYBOARD_HEIGHT) {
92
+ if (sameWidth && baseline.height - vv.height > KEYBOARD_MIN) {
91
93
  setKeyboardSeen(true)
92
94
  // The user is focused in a terminal with the keyboard up — they need
93
95
  // the bar right now. Set visibility here rather than waiting for the
@@ -10,18 +10,22 @@
10
10
  * - first (fixed) button collapses the bar
11
11
  * - second (fixed) button opens the edit modal (shortcut-bar-edit.jsx)
12
12
  * - tapping a shortcut sends it to the active terminal via runQuickCommand
13
+ * - while the system input panel overlays the page without resizing the
14
+ * layout viewport (iOS / HarmonyOS do this, Android resizes instead), the
15
+ * bar lifts itself above the panel — see the visualViewport effect below
13
16
  *
14
17
  * Only loaded on touch devices — see shortcut-bar-entry.jsx.
15
18
  * Data + persistence live in shortcut-bar-defs.js.
16
19
  */
17
20
 
18
21
  import { auto } from 'manate/react'
19
- import { useEffect, useState } from 'react'
22
+ import { useEffect, useRef, useState } from 'react'
20
23
  import { createPortal } from 'react-dom'
21
24
  import { DownOutlined, EditOutlined } from '@ant-design/icons'
22
25
  import { refs } from '../common/ref'
23
26
  import { shortcutBarHeight } from '../../common/constants'
24
27
  import ShortcutBarEdit from './shortcut-bar-edit'
28
+ import { KEYBOARD_MIN } from './shortcut-bar-entry'
25
29
  import { candidates, loadActive, saveActive, ESC } from './shortcut-bar-defs'
26
30
  import './shortcut-bar.styl'
27
31
 
@@ -89,6 +93,55 @@ function ShortcutBar (props) {
89
93
  }
90
94
  }, [])
91
95
 
96
+ // On Android the soft keyboard resizes the layout viewport, so a
97
+ // `position: fixed; bottom: 0` bar is pushed up automatically. iOS and
98
+ // HarmonyOS instead keep the layout viewport full-size and let the keyboard
99
+ // overlay it — there the bar sits underneath the keyboard. visualViewport
100
+ // always tracks the *visible* area on every platform, so the gap between it
101
+ // and the layout viewport is exactly the covered height: lift the bar by
102
+ // that much so it rides just above the keyboard.
103
+ const [kbOffset, setKbOffset] = useState(0)
104
+ const kbBaselineRef = useRef({ height: 0, width: 0 })
105
+
106
+ useEffect(() => {
107
+ const vv = window.visualViewport
108
+ if (!vv) {
109
+ return
110
+ }
111
+ // while no keyboard is up, layout viewport == visual viewport (offsetTop
112
+ // 0); keep refreshing the baseline so a later comparison is accurate.
113
+ function sync () {
114
+ if (vv.offsetTop <= 1) {
115
+ kbBaselineRef.current = { height: window.innerHeight, width: vv.width }
116
+ }
117
+ const { height, width } = kbBaselineRef.current
118
+ // a keyboard changes height only — ignore rotation / pinch-zoom, which
119
+ // change width too.
120
+ const sameWidth = Math.abs(vv.width - width) < 20
121
+ const covered = sameWidth ? height - vv.height - vv.offsetTop : 0
122
+ setKbOffset(covered > KEYBOARD_MIN ? Math.ceil(covered) : 0)
123
+ }
124
+ sync()
125
+ vv.addEventListener('resize', sync)
126
+ vv.addEventListener('scroll', sync)
127
+ window.addEventListener('resize', sync)
128
+ return () => {
129
+ vv.removeEventListener('resize', sync)
130
+ vv.removeEventListener('scroll', sync)
131
+ window.removeEventListener('resize', sync)
132
+ }
133
+ }, [])
134
+
135
+ // keep the offset in a CSS var consumed by shortcut-bar.styl, and expose the
136
+ // lifted height so layout.jsx can shrink the terminal accordingly.
137
+ useEffect(() => {
138
+ document.documentElement.style.setProperty(
139
+ '--shortcut-bar-kb-offset',
140
+ kbOffset ? kbOffset + 'px' : '0px'
141
+ )
142
+ store.shortcutBarKbOffset = kbOffset
143
+ }, [kbOffset])
144
+
92
145
  // reserve layout space + lift the footer while the bar is shown.
93
146
  // expose the bar height as a CSS var so the .styl + footer-lift rule stay
94
147
  // in sync with the shortcutBarHeight constant consumed by layout.jsx.
@@ -135,7 +188,11 @@ function ShortcutBar (props) {
135
188
  }
136
189
 
137
190
  return createPortal(
138
- <div className='shortcut-bar' role='toolbar'>
191
+ <div
192
+ className='shortcut-bar'
193
+ role='toolbar'
194
+ style={kbOffset ? { bottom: kbOffset + 'px' } : undefined}
195
+ >
139
196
  <div className='shortcut-bar-fixed'>
140
197
  <button
141
198
  type='button'
@@ -73,10 +73,12 @@
73
73
  color var(--text-light)
74
74
 
75
75
  // while the bar is visible, lift the footer + reserve space so nothing is
76
- // covered (terminal area is shrunk by layout.jsx via store.shortcutBarVisible)
76
+ // covered (terminal area is shrunk by layout.jsx via store.shortcutBarVisible;
77
+ // --shortcut-bar-kb-offset raises both bar + footer above a page-overlaying
78
+ // system input panel on iOS / HarmonyOS)
77
79
  .shortcut-bar-on
78
80
  .main-footer
79
- bottom var(--shortcut-bar-h, 44px)
81
+ bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
80
82
 
81
83
  // --- edit modal -----------------------------------------------------------
82
84
 
@@ -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 color over an opaque background,
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). When the terminal background is forced transparent
114
- * (DOM renderer keeps the real background behind via CSS), that blend runs
115
- * over transparent-black and a light selection turns into dark gray. This
116
- * helper recomputes it over the real visible background instead.
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 an xterm color object ({ css, rgba }) e.g.
119
- * colors.selectionBackgroundTransparent; `visibleBackground` is an opaque
120
- * colour string.
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 bgRgb = parseColorToRgb(visibleBackground)
124
- const sel = selectionColor?.rgba
125
- if (!bgRgb || typeof sel !== 'number') {
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
- const sr = sel >> 24 & 255
136
- const sg = sel >> 16 & 255
137
- const sb = sel >> 8 & 255
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: ((sel & 0xffffff) | 0xff000000) >>> 0
146
+ rgba: (sr << 24 | sg << 16 | sb << 8 | 255) >>> 0
142
147
  }
143
148
  }
144
- const r = br + Math.round((sr - br) * a)
145
- const g = bg + Math.round((sg - bg) * a)
146
- const b = bb + Math.round((sb - bb) * a)
147
- const alpha = Math.round(a * 255)
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
@@ -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 DOM renderer keeps the terminal background
1478
- * transparent (rgba(0,0,0,0)) so electerm's own CSS background / image
1479
- * shows through, which makes a light selection blend over transparent-black
1480
- * and render as dark gray. Recompute the selection over the real visible
1481
- * background so terminal:selectionBackground=rgba(...) is applied as the
1482
- * configured semi-transparent colour.
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
- const colors = themeService.colors
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
- colors.selectionBackgroundTransparent
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
- colors.selectionInactiveBackgroundTransparent
1505
+ themeConfig.selectionInactiveBackground || inactiveFallback
1502
1506
  )
1503
1507
  if (inactive) {
1504
1508
  c.selectionInactiveBackgroundOpaque = inactive
@@ -10,11 +10,12 @@ import {
10
10
  } from '@ant-design/icons'
11
11
  import { Button, Space } from 'antd'
12
12
  import Upload from '../common/upload'
13
+ import { beforeBookmarkUpload } from './bookmark-upload'
13
14
 
14
15
  const e = window.translate
15
16
 
16
17
  export default class BookmarkTransport extends PureComponent {
17
- beforeUpload = async (file) => {}
18
+ beforeUpload = beforeBookmarkUpload
18
19
 
19
20
  handleDownload = () => {}
20
21