@electerm/electerm-react 5.0.7 → 5.1.6

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 (48) hide show
  1. package/client/common/constants.js +2 -0
  2. package/client/common/default-setting.js +10 -1
  3. package/client/common/left-sidebar-icon-defs.js +36 -0
  4. package/client/components/ai/ai-config-props.js +8 -0
  5. package/client/components/bookmark-form/common/fields.jsx +3 -2
  6. package/client/components/bookmark-form/common/ssh-agent.jsx +3 -2
  7. package/client/components/bookmark-form/common/x11.jsx +2 -2
  8. package/client/components/common/logo-elem.jsx +1 -1
  9. package/client/components/common/logo.styl +8 -2
  10. package/client/components/common/switch.jsx +63 -0
  11. package/client/components/common/switch.styl +7 -0
  12. package/client/components/footer/cmd-history.jsx +10 -8
  13. package/client/components/footer/cmd-history.styl +8 -0
  14. package/client/components/layout/layout.jsx +8 -5
  15. package/client/components/main/main.jsx +7 -2
  16. package/client/components/profile/profile-form-elem.jsx +2 -2
  17. package/client/components/quick-commands/quick-commands-box.jsx +2 -19
  18. package/client/components/quick-commands/quick-commands-form-elem.jsx +2 -2
  19. package/client/components/quick-commands/quick-commands-select.jsx +10 -20
  20. package/client/components/rdp/rdp-session.jsx +3 -4
  21. package/client/components/setting-panel/setting-common.jsx +9 -5
  22. package/client/components/setting-panel/setting-left-sidebar-icons.jsx +54 -0
  23. package/client/components/setting-panel/setting-terminal.jsx +3 -4
  24. package/client/components/setting-sync/data-import.jsx +3 -4
  25. package/client/components/setting-sync/setting-sync-form.jsx +3 -2
  26. package/client/components/sftp/file-compare-modal.jsx +1 -1
  27. package/client/components/sidebar/history.jsx +9 -11
  28. package/client/components/sidebar/index.jsx +22 -71
  29. package/client/components/sidebar/left-sidebar-icons.jsx +148 -0
  30. package/client/components/sidebar/sidebar.styl +6 -0
  31. package/client/components/sidebar/transfer-list.jsx +1 -1
  32. package/client/components/spice/spice-session.jsx +4 -5
  33. package/client/components/tabs/no-session.jsx +1 -1
  34. package/client/components/tabs/no-session.styl +31 -8
  35. package/client/components/terminal/shortcut-bar-defs.js +131 -0
  36. package/client/components/terminal/shortcut-bar-edit.jsx +205 -0
  37. package/client/components/terminal/shortcut-bar-entry.jsx +34 -0
  38. package/client/components/terminal/shortcut-bar.jsx +150 -0
  39. package/client/components/terminal/shortcut-bar.styl +186 -0
  40. package/client/components/terminal/terminal-color-query.mjs +47 -0
  41. package/client/components/terminal/terminal.jsx +39 -1
  42. package/client/components/terminal/terminal.styl +8 -0
  43. package/client/components/terminal-info/base.jsx +5 -7
  44. package/client/components/widgets/widget-form.jsx +3 -2
  45. package/client/css/mobile.styl +18 -6
  46. package/client/store/init-state.js +3 -0
  47. package/client/store/mcp-handler.js +29 -1
  48. package/package.json +1 -1
@@ -0,0 +1,205 @@
1
+ /**
2
+ * edit modal for the shortcut bar.
3
+ *
4
+ * - tap a candidate to append it to the active list (order = click order)
5
+ * - tap the × on an active item to remove it
6
+ * - build a custom button (label + literal text to send)
7
+ * - search filters the candidate library
8
+ *
9
+ * Rendered through a portal to document.body so it isn't trapped inside the
10
+ * bar's (z-index 400, 44px tall) stacking context — otherwise higher-z app
11
+ * layers would paint over it on mobile.
12
+ */
13
+
14
+ import { useState } from 'react'
15
+ import { createPortal } from 'react-dom'
16
+ import { Input } from 'antd'
17
+ import {
18
+ CloseOutlined,
19
+ PlusOutlined,
20
+ ReloadOutlined,
21
+ SearchOutlined
22
+ } from '@ant-design/icons'
23
+ import classnames from 'classnames'
24
+ import Modal from '../common/modal'
25
+ import { defaultActive } from './shortcut-bar-defs'
26
+
27
+ const e = window.translate
28
+
29
+ export default function ShortcutBarEdit (props) {
30
+ const {
31
+ active,
32
+ candidates: allCandidates,
33
+ isMobile,
34
+ onCancel,
35
+ onSave
36
+ } = props
37
+
38
+ const [draft, setDraft] = useState(() => active.map(b => ({ ...b })))
39
+ const [keyword, setKeyword] = useState('')
40
+ const [customLabel, setCustomLabel] = useState('')
41
+ const [customData, setCustomData] = useState('')
42
+
43
+ const activeIds = new Set(draft.map(b => b.id))
44
+
45
+ function handleAddCandidate (btn) {
46
+ if (activeIds.has(btn.id)) {
47
+ return
48
+ }
49
+ setDraft(prev => [...prev, { ...btn }])
50
+ }
51
+
52
+ function handleRemoveActive (id) {
53
+ setDraft(prev => prev.filter(b => b.id !== id))
54
+ }
55
+
56
+ function handleAddCustom () {
57
+ const label = customLabel.trim()
58
+ const data = customData
59
+ if (!label) {
60
+ return
61
+ }
62
+ const id = 'custom-' + label + '-' + Date.now()
63
+ setDraft(prev => [...prev, { id, label, data, custom: true }])
64
+ setCustomLabel('')
65
+ setCustomData('')
66
+ }
67
+
68
+ function handleReset () {
69
+ setDraft(defaultActive())
70
+ }
71
+
72
+ function handleSave () {
73
+ onSave(draft)
74
+ }
75
+
76
+ const kw = keyword.trim().toLowerCase()
77
+ const filtered = allCandidates.filter(b => {
78
+ if (!kw) {
79
+ return true
80
+ }
81
+ return b.label.toLowerCase().includes(kw) || b.id.includes(kw)
82
+ })
83
+
84
+ function renderActive (b) {
85
+ return (
86
+ <div
87
+ key={b.id}
88
+ className='shortcut-active-item'
89
+ >
90
+ <span className='shortcut-active-label'>{b.label}</span>
91
+ <button
92
+ type='button'
93
+ className='shortcut-active-remove'
94
+ onClick={() => handleRemoveActive(b.id)}
95
+ >
96
+ <CloseOutlined />
97
+ </button>
98
+ </div>
99
+ )
100
+ }
101
+
102
+ function renderCandidate (b) {
103
+ const added = activeIds.has(b.id)
104
+ return (
105
+ <button
106
+ type='button'
107
+ key={b.id}
108
+ className={classnames('shortcut-candidate-item', { added })}
109
+ onClick={() => handleAddCandidate(b)}
110
+ >
111
+ {b.label}
112
+ </button>
113
+ )
114
+ }
115
+
116
+ const footer = (
117
+ <div className='custom-modal-footer-buttons shortcut-edit-footer'>
118
+ <button
119
+ type='button'
120
+ className='custom-modal-cancel-btn'
121
+ onClick={handleReset}
122
+ >
123
+ <ReloadOutlined /> {e('reset')}
124
+ </button>
125
+ <button
126
+ type='button'
127
+ className='custom-modal-cancel-btn'
128
+ onClick={onCancel}
129
+ >
130
+ {e('cancel')}
131
+ </button>
132
+ <button
133
+ type='button'
134
+ className='custom-modal-ok-btn'
135
+ onClick={handleSave}
136
+ >
137
+ {e('save')}
138
+ </button>
139
+ </div>
140
+ )
141
+ const sst = e('keyboardShortcuts')
142
+ return createPortal(
143
+ <Modal
144
+ open
145
+ title={e('edit')}
146
+ onCancel={onCancel}
147
+ footer={footer}
148
+ width={560}
149
+ zIndex={1100}
150
+ wrapClassName={classnames('shortcut-edit-wrap', { 'is-mobile': isMobile })}
151
+ >
152
+ <div className='shortcut-edit-section'>
153
+ <div className='shortcut-edit-section-title'>{sst}</div>
154
+ <div className='shortcut-active-list'>
155
+ {draft.length === 0 && (
156
+ <div className='shortcut-active-empty'>0 {sst}</div>
157
+ )}
158
+ {draft.map(renderActive)}
159
+ </div>
160
+ </div>
161
+
162
+ <div className='shortcut-edit-section'>
163
+ <div className='shortcut-custom-form'>
164
+ <Input
165
+ prefix={<SearchOutlined />}
166
+ value={keyword}
167
+ onChange={ev => setKeyword(ev.target.value)}
168
+ placeholder={e('search')}
169
+ className='shortcut-search-input'
170
+ allowClear
171
+ />
172
+ </div>
173
+ <div className='shortcut-candidate-list'>
174
+ {filtered.map(renderCandidate)}
175
+ </div>
176
+ </div>
177
+
178
+ <div className='shortcut-edit-section'>
179
+ <div className='shortcut-edit-section-title'>+ {sst}</div>
180
+ <div className='shortcut-custom-form'>
181
+ <Input
182
+ value={customLabel}
183
+ onChange={ev => setCustomLabel(ev.target.value)}
184
+ placeholder={e('label')}
185
+ className='shortcut-custom-input'
186
+ />
187
+ <Input
188
+ value={customData}
189
+ onChange={ev => setCustomData(ev.target.value)}
190
+ placeholder={e('edit')}
191
+ className='shortcut-custom-input'
192
+ />
193
+ <button
194
+ type='button'
195
+ className='custom-modal-ok-btn shortcut-custom-add'
196
+ onClick={handleAddCustom}
197
+ >
198
+ <PlusOutlined /> {e('add')}
199
+ </button>
200
+ </div>
201
+ </div>
202
+ </Modal>,
203
+ document.body
204
+ )
205
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * lazy entry for shortcut-bar.
3
+ *
4
+ * The bar only makes sense on touch devices (it exists to compensate for the
5
+ * lack of a physical keyboard when the system input panel is shown). On
6
+ * non-touch devices we render nothing AND never import the real component, so
7
+ * its code (and the antd/icon deps it pulls in) stays out of the main bundle.
8
+ */
9
+
10
+ import { lazy, Suspense } from 'react'
11
+
12
+ const ShortcutBar = lazy(() => import('./shortcut-bar'))
13
+
14
+ export function isTouchDevice () {
15
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
16
+ return false
17
+ }
18
+ const coarse = typeof window.matchMedia === 'function' &&
19
+ window.matchMedia('(pointer: coarse)').matches
20
+ const hasTouch = (navigator.maxTouchPoints || 0) > 0 ||
21
+ ('ontouchstart' in window)
22
+ return Boolean(coarse || hasTouch)
23
+ }
24
+
25
+ export default function ShortcutBarEntry (props) {
26
+ if (!isTouchDevice()) {
27
+ return null
28
+ }
29
+ return (
30
+ <Suspense fallback={null}>
31
+ <ShortcutBar {...props} />
32
+ </Suspense>
33
+ )
34
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * shortcut-bar
3
+ *
4
+ * On touch devices, tapping a terminal pops the system input panel.
5
+ * Without a physical keyboard, modifier / function keys are hard to type, so
6
+ * this renders a slim, horizontally-scrollable bar pinned just outside the
7
+ * page content (its space is reserved by layout.jsx, so it never overlaps the
8
+ * terminal, footer or any other UI).
9
+ *
10
+ * - first (fixed) button collapses the bar
11
+ * - second (fixed) button opens the edit modal (shortcut-bar-edit.jsx)
12
+ * - tapping a shortcut sends it to the active terminal via runQuickCommand
13
+ *
14
+ * Only loaded on touch devices — see shortcut-bar-entry.jsx.
15
+ * Data + persistence live in shortcut-bar-defs.js.
16
+ */
17
+
18
+ import { auto } from 'manate/react'
19
+ import { useEffect, useState } from 'react'
20
+ import { createPortal } from 'react-dom'
21
+ import { DownOutlined, EditOutlined } from '@ant-design/icons'
22
+ import { refs } from '../common/ref'
23
+ import { shortcutBarHeight } from '../../common/constants'
24
+ import ShortcutBarEdit from './shortcut-bar-edit'
25
+ import { candidates, loadActive, saveActive } from './shortcut-bar-defs'
26
+ import './shortcut-bar.styl'
27
+
28
+ const e = window.translate
29
+
30
+ function sendToTerminal (data) {
31
+ const { store } = window
32
+ const term = refs.get('term-' + store.activeTabId)
33
+ if (term && typeof term.runQuickCommand === 'function') {
34
+ term.runQuickCommand(data, true)
35
+ }
36
+ }
37
+
38
+ function ShortcutBar (props) {
39
+ const { store } = props
40
+ const [buttons, setButtons] = useState(loadActive)
41
+ const [editing, setEditing] = useState(false)
42
+
43
+ // show the bar whenever a terminal becomes active / gets focused,
44
+ // hide it when the active tab is not a terminal.
45
+ useEffect(() => {
46
+ store.shortcutBarVisible = !!store.inActiveTerminal
47
+ }, [store.activeTabId, store.inActiveTerminal])
48
+
49
+ useEffect(() => {
50
+ function onTerminalFocus (ev) {
51
+ const el = ev.target
52
+ if (el && el.closest && el.closest('.term-wrap')) {
53
+ if (window.store.inActiveTerminal) {
54
+ window.store.shortcutBarVisible = true
55
+ }
56
+ }
57
+ }
58
+ document.addEventListener('focusin', onTerminalFocus)
59
+ document.addEventListener('pointerdown', onTerminalFocus)
60
+ return () => {
61
+ document.removeEventListener('focusin', onTerminalFocus)
62
+ document.removeEventListener('pointerdown', onTerminalFocus)
63
+ }
64
+ }, [])
65
+
66
+ // reserve layout space + lift the footer while the bar is shown.
67
+ // expose the bar height as a CSS var so the .styl + footer-lift rule stay
68
+ // in sync with the shortcutBarHeight constant consumed by layout.jsx.
69
+ useEffect(() => {
70
+ const on = !!store.shortcutBarVisible
71
+ document.body.classList.toggle('shortcut-bar-on', on)
72
+ if (on) {
73
+ document.documentElement.style.setProperty(
74
+ '--shortcut-bar-h',
75
+ shortcutBarHeight + 'px'
76
+ )
77
+ }
78
+ return () => {
79
+ document.body.classList.remove('shortcut-bar-on')
80
+ document.documentElement.style.removeProperty('--shortcut-bar-h')
81
+ }
82
+ }, [store.shortcutBarVisible])
83
+
84
+ function handleClose () {
85
+ store.shortcutBarVisible = false
86
+ }
87
+
88
+ function handleSave (next) {
89
+ setButtons(next)
90
+ saveActive(next)
91
+ setEditing(false)
92
+ }
93
+
94
+ function renderButton (b, i) {
95
+ return (
96
+ <button
97
+ type='button'
98
+ key={b.id + '-' + i}
99
+ className='shortcut-bar-btn'
100
+ onClick={() => sendToTerminal(b.data)}
101
+ >
102
+ {b.label}
103
+ </button>
104
+ )
105
+ }
106
+
107
+ if (!store.shortcutBarVisible) {
108
+ return null
109
+ }
110
+
111
+ return createPortal(
112
+ <div className='shortcut-bar' role='toolbar'>
113
+ <div className='shortcut-bar-fixed'>
114
+ <button
115
+ type='button'
116
+ className='shortcut-bar-icon-btn'
117
+ title={e('close')}
118
+ onClick={handleClose}
119
+ >
120
+ <DownOutlined />
121
+ </button>
122
+ <button
123
+ type='button'
124
+ className='shortcut-bar-icon-btn'
125
+ title={e('edit')}
126
+ onClick={() => setEditing(true)}
127
+ >
128
+ <EditOutlined />
129
+ </button>
130
+ </div>
131
+ <div className='shortcut-bar-scroll'>
132
+ {buttons.map(renderButton)}
133
+ </div>
134
+ {
135
+ editing && (
136
+ <ShortcutBarEdit
137
+ active={buttons}
138
+ candidates={candidates}
139
+ isMobile={store.isMobile}
140
+ onCancel={() => setEditing(false)}
141
+ onSave={handleSave}
142
+ />
143
+ )
144
+ }
145
+ </div>,
146
+ document.body
147
+ )
148
+ }
149
+
150
+ export default auto(ShortcutBar)
@@ -0,0 +1,186 @@
1
+ // shortcut-bar
2
+ // sits in reserved space at the bottom of the viewport (see layout.jsx +
3
+ // .shortcut-bar-on rule below). Height tracks --shortcut-bar-h which the
4
+ // component sets from the shortcutBarHeight constant.
5
+
6
+ .shortcut-bar
7
+ position fixed
8
+ left 0
9
+ right 0
10
+ bottom 0
11
+ height var(--shortcut-bar-h, 44px)
12
+ z-index 400
13
+ display flex
14
+ align-items stretch
15
+ background var(--main)
16
+ border-top 1px solid var(--main-lighter)
17
+ user-select none
18
+ -webkit-user-select none
19
+
20
+ .shortcut-bar-fixed
21
+ display flex
22
+ align-items stretch
23
+ flex-shrink 0
24
+ border-right 1px solid var(--main-lighter)
25
+
26
+ .shortcut-bar-icon-btn
27
+ width 44px
28
+ display flex
29
+ align-items center
30
+ justify-content center
31
+ background transparent
32
+ border none
33
+ color var(--text)
34
+ cursor pointer
35
+ font-size 16px
36
+ &:active
37
+ background var(--main-lighter)
38
+ &:hover
39
+ color var(--text-light)
40
+
41
+ .shortcut-bar-scroll
42
+ flex 1
43
+ display flex
44
+ align-items center
45
+ gap 6px
46
+ padding 0 8px
47
+ overflow-x auto
48
+ overflow-y hidden
49
+ scrollbar-width none
50
+ -webkit-overflow-scrolling touch
51
+ &::-webkit-scrollbar
52
+ display none
53
+
54
+ .shortcut-bar-btn
55
+ flex-shrink 0
56
+ height 32px
57
+ min-width 32px
58
+ padding 0 10px
59
+ display inline-flex
60
+ align-items center
61
+ justify-content center
62
+ border-radius 6px
63
+ border 1px solid var(--main-lighter)
64
+ background var(--main-light)
65
+ color var(--text)
66
+ font-size 13px
67
+ cursor pointer
68
+ white-space nowrap
69
+ &:active
70
+ background var(--primary)
71
+ color var(--primary-contrast)
72
+ &:hover
73
+ color var(--text-light)
74
+
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)
77
+ .shortcut-bar-on
78
+ .main-footer
79
+ bottom var(--shortcut-bar-h, 44px)
80
+
81
+ // --- edit modal -----------------------------------------------------------
82
+
83
+ .shortcut-edit-wrap
84
+ .custom-modal-content
85
+ max-width 92vw
86
+ .shortcut-edit-section
87
+ margin-bottom 16px
88
+ &:last-child
89
+ margin-bottom 0
90
+ .shortcut-edit-section-title
91
+ margin-bottom 8px
92
+ color var(--text)
93
+ font-weight bold
94
+ font-size 13px
95
+
96
+ .shortcut-active-list
97
+ display flex
98
+ flex-wrap wrap
99
+ gap 6px
100
+ min-height 48px
101
+ padding 8px
102
+ border 1px dashed var(--main-lighter)
103
+ border-radius 6px
104
+ background var(--main-dark)
105
+ .shortcut-active-empty
106
+ color var(--text-dark)
107
+ font-size 12px
108
+ width 100%
109
+ text-align center
110
+ line-height 32px
111
+
112
+ .shortcut-active-item
113
+ display inline-flex
114
+ align-items center
115
+ gap 4px
116
+ height 30px
117
+ padding 0 4px 0 10px
118
+ border-radius 6px
119
+ background var(--main-light)
120
+ border 1px solid var(--main-lighter)
121
+ color var(--text)
122
+ font-size 13px
123
+ .shortcut-active-label
124
+ white-space nowrap
125
+ .shortcut-active-remove
126
+ display flex
127
+ align-items center
128
+ justify-content center
129
+ width 20px
130
+ height 20px
131
+ border none
132
+ background transparent
133
+ color var(--text-dark)
134
+ border-radius 4px
135
+ cursor pointer
136
+ font-size 11px
137
+ &:hover
138
+ color var(--error)
139
+ background var(--main-lighter)
140
+
141
+ .shortcut-candidate-list
142
+ display flex
143
+ flex-wrap wrap
144
+ gap 6px
145
+ max-height 180px
146
+ overflow-y auto
147
+ padding 4px
148
+ .shortcut-candidate-item
149
+ height 30px
150
+ min-width 34px
151
+ padding 0 10px
152
+ display inline-flex
153
+ align-items center
154
+ justify-content center
155
+ border-radius 6px
156
+ border 1px solid var(--main-lighter)
157
+ background var(--main)
158
+ color var(--text)
159
+ font-size 13px
160
+ cursor pointer
161
+ &:active
162
+ background var(--main-lighter)
163
+ &:hover
164
+ color var(--text-light)
165
+ &.added
166
+ opacity 0.45
167
+ cursor default
168
+
169
+ .shortcut-custom-form
170
+ display flex
171
+ gap 8px
172
+ align-items center
173
+ flex-wrap wrap
174
+ .shortcut-search-input
175
+ flex 1
176
+ min-width 160px
177
+ .shortcut-custom-input
178
+ flex 1
179
+ min-width 120px
180
+ .shortcut-custom-add
181
+ height 32px
182
+ white-space nowrap
183
+
184
+ .shortcut-edit-footer
185
+ justify-content space-between
186
+ width 100%
@@ -104,6 +104,53 @@ export function colorToOscRgb (color) {
104
104
  return `rgb:${rgb.map(toHexByte).join('/')}`
105
105
  }
106
106
 
107
+ /**
108
+ * Blend a (possibly translucent) selection color over an opaque background,
109
+ * producing the colour in xterm's internal format ({ css, rgba } where
110
+ * rgba = r<<24 | g<<16 | b<<8 | a).
111
+ *
112
+ * 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.
117
+ *
118
+ * `selectionColor` is an xterm color object ({ css, rgba }) e.g.
119
+ * colors.selectionBackgroundTransparent; `visibleBackground` is an opaque
120
+ * colour string.
121
+ */
122
+ 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) {
131
+ return null
132
+ }
133
+ const toCss = (r, g, b, alpha) =>
134
+ `#${[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) {
139
+ return {
140
+ css: toCss(sr, sg, sb, 255),
141
+ rgba: ((sel & 0xffffff) | 0xff000000) >>> 0
142
+ }
143
+ }
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)
148
+ return {
149
+ css: toCss(r, g, b, alpha),
150
+ rgba: (r << 24 | g << 16 | b << 8 | alpha) >>> 0
151
+ }
152
+ }
153
+
107
154
  export function buildOscColorResponse (identifier, color, fallbackColor) {
108
155
  const oscColor = colorToOscRgb(color) || colorToOscRgb(fallbackColor)
109
156
  return oscColor
@@ -71,7 +71,8 @@ import {
71
71
  } from './xterm-loader.js'
72
72
  import {
73
73
  createRendererThemeConfig,
74
- handleTerminalColorQuery
74
+ handleTerminalColorQuery,
75
+ blendSelectionOverBackground
75
76
  } from './terminal-color-query.mjs'
76
77
 
77
78
  const e = window.translate
@@ -1456,6 +1457,7 @@ class Term extends Component {
1456
1457
  return
1457
1458
  }
1458
1459
  term.options.theme = this.getRendererThemeConfig(this.props.themeConfig)
1460
+ this.fixSelectionColors(term)
1459
1461
  term.refresh(0, term.rows - 1)
1460
1462
  if (deferred && this.props.config.rendererType === rendererTypes.webGL) {
1461
1463
  window.cancelAnimationFrame(this.timers.themeRaf)
@@ -1464,11 +1466,46 @@ class Term extends Component {
1464
1466
  return
1465
1467
  }
1466
1468
  this.term.options.theme = this.getRendererThemeConfig(this.props.themeConfig)
1469
+ this.fixSelectionColors(this.term)
1467
1470
  this.term.refresh(0, this.term.rows - 1)
1468
1471
  })
1469
1472
  }
1470
1473
  }
1471
1474
 
1475
+ /**
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.
1483
+ */
1484
+ fixSelectionColors = (term) => {
1485
+ const themeService = term?._core?._themeService
1486
+ if (!themeService?.modifyColors) {
1487
+ return
1488
+ }
1489
+ const visibleBackground = this.getVisibleTerminalBackground()
1490
+ const colors = themeService.colors
1491
+ themeService.modifyColors((c) => {
1492
+ const active = blendSelectionOverBackground(
1493
+ visibleBackground,
1494
+ colors.selectionBackgroundTransparent
1495
+ )
1496
+ if (active) {
1497
+ c.selectionBackgroundOpaque = active
1498
+ }
1499
+ const inactive = blendSelectionOverBackground(
1500
+ visibleBackground,
1501
+ colors.selectionInactiveBackgroundTransparent
1502
+ )
1503
+ if (inactive) {
1504
+ c.selectionInactiveBackgroundOpaque = inactive
1505
+ }
1506
+ })
1507
+ }
1508
+
1472
1509
  initTerminal = async () => {
1473
1510
  const { themeConfig, tab = {}, config = {} } = this.props
1474
1511
  const tc = this.getRendererThemeConfig(themeConfig)
@@ -1492,6 +1529,7 @@ class Term extends Component {
1492
1529
  term.open(this.domRef.current, true)
1493
1530
  this.registerTerminalColorQueryHandlers(term, themeConfig)
1494
1531
  await this.loadRenderer(term, config)
1532
+ this.fixSelectionColors(term)
1495
1533
  // Re-apply the theme after the renderer is loaded. The Terminal was
1496
1534
  // constructed before UiTheme's useEffect ran, so the initial theme
1497
1535
  // may have a stale background. Pass `term` directly because this.term