@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.
- package/client/common/constants.js +2 -0
- package/client/common/default-setting.js +10 -1
- package/client/common/left-sidebar-icon-defs.js +36 -0
- package/client/components/ai/ai-config-props.js +8 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -2
- package/client/components/bookmark-form/common/ssh-agent.jsx +3 -2
- package/client/components/bookmark-form/common/x11.jsx +2 -2
- package/client/components/common/logo-elem.jsx +1 -1
- package/client/components/common/logo.styl +8 -2
- package/client/components/common/switch.jsx +63 -0
- package/client/components/common/switch.styl +7 -0
- package/client/components/footer/cmd-history.jsx +10 -8
- package/client/components/footer/cmd-history.styl +8 -0
- package/client/components/layout/layout.jsx +8 -5
- package/client/components/main/main.jsx +7 -2
- package/client/components/profile/profile-form-elem.jsx +2 -2
- package/client/components/quick-commands/quick-commands-box.jsx +2 -19
- package/client/components/quick-commands/quick-commands-form-elem.jsx +2 -2
- package/client/components/quick-commands/quick-commands-select.jsx +10 -20
- package/client/components/rdp/rdp-session.jsx +3 -4
- package/client/components/setting-panel/setting-common.jsx +9 -5
- package/client/components/setting-panel/setting-left-sidebar-icons.jsx +54 -0
- package/client/components/setting-panel/setting-terminal.jsx +3 -4
- package/client/components/setting-sync/data-import.jsx +3 -4
- package/client/components/setting-sync/setting-sync-form.jsx +3 -2
- package/client/components/sftp/file-compare-modal.jsx +1 -1
- package/client/components/sidebar/history.jsx +9 -11
- package/client/components/sidebar/index.jsx +22 -71
- package/client/components/sidebar/left-sidebar-icons.jsx +148 -0
- package/client/components/sidebar/sidebar.styl +6 -0
- package/client/components/sidebar/transfer-list.jsx +1 -1
- package/client/components/spice/spice-session.jsx +4 -5
- package/client/components/tabs/no-session.jsx +1 -1
- package/client/components/tabs/no-session.styl +31 -8
- package/client/components/terminal/shortcut-bar-defs.js +131 -0
- package/client/components/terminal/shortcut-bar-edit.jsx +205 -0
- package/client/components/terminal/shortcut-bar-entry.jsx +34 -0
- package/client/components/terminal/shortcut-bar.jsx +150 -0
- package/client/components/terminal/shortcut-bar.styl +186 -0
- package/client/components/terminal/terminal-color-query.mjs +47 -0
- package/client/components/terminal/terminal.jsx +39 -1
- package/client/components/terminal/terminal.styl +8 -0
- package/client/components/terminal-info/base.jsx +5 -7
- package/client/components/widgets/widget-form.jsx +3 -2
- package/client/css/mobile.styl +18 -6
- package/client/store/init-state.js +3 -0
- package/client/store/mcp-handler.js +29 -1
- 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
|