@electerm/electerm-react 5.1.7 → 5.1.16
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/default-setting.js +1 -0
- package/client/common/theme-defaults.js +1 -1
- package/client/components/ai/ai-config-props.js +8 -0
- package/client/components/main/upgrade.jsx +1 -1
- package/client/components/setting-panel/setting-common.jsx +1 -0
- package/client/components/terminal/shortcut-bar-defs.js +72 -2
- package/client/components/terminal/shortcut-bar-edit.jsx +74 -45
- package/client/components/terminal/shortcut-bar-entry.jsx +92 -6
- package/client/components/terminal/shortcut-bar.jsx +29 -3
- package/client/components/terminal/shortcut-bar.styl +8 -0
- package/package.json +1 -1
|
@@ -81,7 +81,7 @@ const defaultThemeDarkTerminal = () => {
|
|
|
81
81
|
background: '#20111b',
|
|
82
82
|
cursor: '#b5bd68',
|
|
83
83
|
cursorAccent: '#1d1f21',
|
|
84
|
-
selectionBackground: 'rgba(
|
|
84
|
+
selectionBackground: 'rgba(200, 200, 200, 0.6)',
|
|
85
85
|
black: '#575757',
|
|
86
86
|
red: '#FF2C6D',
|
|
87
87
|
green: '#19f9d8',
|
|
@@ -51,6 +51,14 @@ export const defaultAIPresets = [
|
|
|
51
51
|
modelAI: 'openai/gpt-4o-mini',
|
|
52
52
|
authHeaderNameAI: 'Authorization: Bearer'
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
id: 'orcarouter',
|
|
56
|
+
nameAI: 'OrcaRouter',
|
|
57
|
+
baseURLAI: 'https://api.orcarouter.ai/v1',
|
|
58
|
+
apiPathAI: '/chat/completions',
|
|
59
|
+
modelAI: 'deepseek/deepseek-v4-flash',
|
|
60
|
+
authHeaderNameAI: 'Authorization: Bearer'
|
|
61
|
+
},
|
|
54
62
|
{
|
|
55
63
|
id: 'google',
|
|
56
64
|
nameAI: 'Google Gemini',
|
|
@@ -361,7 +361,7 @@ export default class Upgrade extends PureComponent {
|
|
|
361
361
|
<div className={cls}>
|
|
362
362
|
<div className='upgrade-panel-title fix'>
|
|
363
363
|
<span className='fleft'>
|
|
364
|
-
{e('newVersion')} <b>{remoteVersion} [{releaseInfo.date}]</b>
|
|
364
|
+
{e('newVersion')} <b>{remoteVersion}{releaseInfo?.date ? ` [${releaseInfo.date}]` : ''}</b>
|
|
365
365
|
</span>
|
|
366
366
|
<span className='fright'>
|
|
367
367
|
<MinusSquareOutlined className='pointer font16 close-upgrade-panel' onClick={this.handleMinimize} />
|
|
@@ -8,7 +8,8 @@ import * as ls from '../../common/safe-local-storage'
|
|
|
8
8
|
import { shortcutBarLsKey } from '../../common/constants'
|
|
9
9
|
|
|
10
10
|
// build control bytes at runtime — keeps source free of literal control chars
|
|
11
|
-
|
|
11
|
+
// exported so shortcut-bar.jsx can honor DECCKM for arrow / Home / End keys.
|
|
12
|
+
export const ESC = String.fromCharCode(0x1b)
|
|
12
13
|
const DEL = String.fromCharCode(0x7f)
|
|
13
14
|
|
|
14
15
|
// --- candidate library -----------------------------------------------------
|
|
@@ -60,6 +61,69 @@ const punctButtons = punctChars.split('').map(ch => ({
|
|
|
60
61
|
data: ch
|
|
61
62
|
}))
|
|
62
63
|
|
|
64
|
+
// --- custom combo builder data ---------------------------------------------
|
|
65
|
+
// Bare keys + modifiers for the edit modal's custom combo builder
|
|
66
|
+
// (shortcut-bar-edit.jsx). Unlike `candidates`, keyOptions holds raw keys
|
|
67
|
+
// (letters, digits, nav, f-keys, punctuation) with no pre-applied modifier —
|
|
68
|
+
// the modal composes Ctrl/Alt/Shift/Meta onto them, so the user can build
|
|
69
|
+
// combos like Ctrl+Shift+C or Alt+Up without typing escape sequences.
|
|
70
|
+
|
|
71
|
+
// letters carry a precomputed control byte so Ctrl+letter maps to the right
|
|
72
|
+
// control byte (charCode - 96) without recomputing at click time.
|
|
73
|
+
const letterKeys = 'abcdefghijklmnopqrstuvwxyz'.split('').map(ch => ({
|
|
74
|
+
id: 'key-' + ch,
|
|
75
|
+
label: ch.toUpperCase(),
|
|
76
|
+
data: ch,
|
|
77
|
+
ctrl: String.fromCharCode(ch.charCodeAt(0) - 96)
|
|
78
|
+
}))
|
|
79
|
+
|
|
80
|
+
const digitKeys = '0123456789'.split('').map(ch => ({
|
|
81
|
+
id: 'key-' + ch,
|
|
82
|
+
label: ch,
|
|
83
|
+
data: ch
|
|
84
|
+
}))
|
|
85
|
+
|
|
86
|
+
// bare keys (no Ctrl pre-applied) for the combo builder
|
|
87
|
+
const keyOptions = [
|
|
88
|
+
...navButtons,
|
|
89
|
+
...letterKeys,
|
|
90
|
+
...digitKeys,
|
|
91
|
+
...fKeys,
|
|
92
|
+
...punctButtons
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
// modifier picklist for the two optional modifier slots in the builder
|
|
96
|
+
const modifierOptions = [
|
|
97
|
+
{ id: 'ctrl', label: 'Ctrl' },
|
|
98
|
+
{ id: 'alt', label: 'Alt' },
|
|
99
|
+
{ id: 'shift', label: 'Shift' },
|
|
100
|
+
{ id: 'meta', label: 'Meta' }
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
// compose the bytes to send for a (mods, key) combo.
|
|
104
|
+
// Ctrl+letter -> the control byte; Alt -> ESC prefix; Shift+letter -> uppercase
|
|
105
|
+
function buildComboData (mods, key) {
|
|
106
|
+
const ctrl = mods.includes('ctrl')
|
|
107
|
+
const alt = mods.includes('alt')
|
|
108
|
+
const shift = mods.includes('shift')
|
|
109
|
+
let data = key.data
|
|
110
|
+
if (ctrl && key.ctrl) {
|
|
111
|
+
data = key.ctrl
|
|
112
|
+
} else if (shift && /^[a-z]$/.test(data)) {
|
|
113
|
+
data = data.toUpperCase()
|
|
114
|
+
}
|
|
115
|
+
if (alt) {
|
|
116
|
+
data = ESC + data
|
|
117
|
+
}
|
|
118
|
+
return data
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// human-readable label, e.g. mods ['ctrl','shift'] + key A -> "Ctrl+Shift+A"
|
|
122
|
+
function buildComboLabel (mods, key) {
|
|
123
|
+
const byId = new Map(modifierOptions.map(m => [m.id, m.label]))
|
|
124
|
+
return [...mods.map(m => byId.get(m) || m), key.label].join('+')
|
|
125
|
+
}
|
|
126
|
+
|
|
63
127
|
// full library, de-duplicated by id
|
|
64
128
|
const candidates = []
|
|
65
129
|
const seenIds = new Set()
|
|
@@ -128,4 +192,10 @@ export function saveActive (arr) {
|
|
|
128
192
|
ls.setItemJSON(shortcutBarLsKey, arr)
|
|
129
193
|
}
|
|
130
194
|
|
|
131
|
-
export {
|
|
195
|
+
export {
|
|
196
|
+
candidates,
|
|
197
|
+
keyOptions,
|
|
198
|
+
modifierOptions,
|
|
199
|
+
buildComboData,
|
|
200
|
+
buildComboLabel
|
|
201
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { useState } from 'react'
|
|
15
15
|
import { createPortal } from 'react-dom'
|
|
16
|
-
import { Input } from 'antd'
|
|
16
|
+
import { Input, Select } from 'antd'
|
|
17
17
|
import {
|
|
18
18
|
CloseOutlined,
|
|
19
19
|
PlusOutlined,
|
|
@@ -22,7 +22,13 @@ import {
|
|
|
22
22
|
} from '@ant-design/icons'
|
|
23
23
|
import classnames from 'classnames'
|
|
24
24
|
import Modal from '../common/modal'
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
defaultActive,
|
|
27
|
+
keyOptions,
|
|
28
|
+
modifierOptions,
|
|
29
|
+
buildComboData,
|
|
30
|
+
buildComboLabel
|
|
31
|
+
} from './shortcut-bar-defs'
|
|
26
32
|
|
|
27
33
|
const e = window.translate
|
|
28
34
|
|
|
@@ -35,42 +41,51 @@ export default function ShortcutBarEdit (props) {
|
|
|
35
41
|
onSave
|
|
36
42
|
} = props
|
|
37
43
|
|
|
38
|
-
const [draft, setDraft] = useState(() => active.map(b => ({ ...b })))
|
|
39
44
|
const [keyword, setKeyword] = useState('')
|
|
40
|
-
const [
|
|
41
|
-
const [
|
|
45
|
+
const [customMod1, setCustomMod1] = useState(undefined)
|
|
46
|
+
const [customMod2, setCustomMod2] = useState(undefined)
|
|
47
|
+
const [customKey, setCustomKey] = useState(undefined)
|
|
48
|
+
|
|
49
|
+
// antd Select portals its dropdown to document.body by default (~z-index
|
|
50
|
+
// 1050), which renders BEHIND this modal (z-index 1100). Anchor each popup
|
|
51
|
+
// to its trigger's parent so it stays inside the modal's stacking context.
|
|
52
|
+
const popupContainer = trigger => trigger.parentElement
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
// edits persist immediately — there is no Save/Cancel, so every change calls
|
|
55
|
+
// onSave to push it live.
|
|
56
|
+
const activeIds = new Set(active.map(b => b.id))
|
|
44
57
|
|
|
45
58
|
function handleAddCandidate (btn) {
|
|
46
59
|
if (activeIds.has(btn.id)) {
|
|
47
60
|
return
|
|
48
61
|
}
|
|
49
|
-
|
|
62
|
+
onSave([...active, { ...btn }])
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
function handleRemoveActive (id) {
|
|
53
|
-
|
|
66
|
+
onSave(active.filter(b => b.id !== id))
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
function handleAddCustom () {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
if (!customKey) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
const mods = [customMod1, customMod2].filter(Boolean)
|
|
74
|
+
const key = keyOptions.find(k => k.id === customKey)
|
|
75
|
+
if (!key) {
|
|
60
76
|
return
|
|
61
77
|
}
|
|
78
|
+
const label = buildComboLabel(mods, key)
|
|
79
|
+
const data = buildComboData(mods, key)
|
|
62
80
|
const id = 'custom-' + label + '-' + Date.now()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
onSave([...active, { id, label, data, custom: true }])
|
|
82
|
+
setCustomMod1(undefined)
|
|
83
|
+
setCustomMod2(undefined)
|
|
84
|
+
setCustomKey(undefined)
|
|
66
85
|
}
|
|
67
86
|
|
|
68
87
|
function handleReset () {
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function handleSave () {
|
|
73
|
-
onSave(draft)
|
|
88
|
+
onSave(defaultActive())
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
const kw = keyword.trim().toLowerCase()
|
|
@@ -122,20 +137,6 @@ export default function ShortcutBarEdit (props) {
|
|
|
122
137
|
>
|
|
123
138
|
<ReloadOutlined /> {e('reset')}
|
|
124
139
|
</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
140
|
</div>
|
|
140
141
|
)
|
|
141
142
|
const sst = e('keyboardShortcuts')
|
|
@@ -152,10 +153,10 @@ export default function ShortcutBarEdit (props) {
|
|
|
152
153
|
<div className='shortcut-edit-section'>
|
|
153
154
|
<div className='shortcut-edit-section-title'>{sst}</div>
|
|
154
155
|
<div className='shortcut-active-list'>
|
|
155
|
-
{
|
|
156
|
+
{active.length === 0 && (
|
|
156
157
|
<div className='shortcut-active-empty'>0 {sst}</div>
|
|
157
158
|
)}
|
|
158
|
-
{
|
|
159
|
+
{active.map(renderActive)}
|
|
159
160
|
</div>
|
|
160
161
|
</div>
|
|
161
162
|
|
|
@@ -178,21 +179,49 @@ export default function ShortcutBarEdit (props) {
|
|
|
178
179
|
<div className='shortcut-edit-section'>
|
|
179
180
|
<div className='shortcut-edit-section-title'>+ {sst}</div>
|
|
180
181
|
<div className='shortcut-custom-form'>
|
|
181
|
-
<
|
|
182
|
-
value={
|
|
183
|
-
onChange={
|
|
184
|
-
placeholder={e('
|
|
185
|
-
className='shortcut-custom-
|
|
182
|
+
<Select
|
|
183
|
+
value={customMod1}
|
|
184
|
+
onChange={setCustomMod1}
|
|
185
|
+
placeholder={e('modifier') + ' (' + e('optional') + ')'}
|
|
186
|
+
className='shortcut-custom-select'
|
|
187
|
+
allowClear
|
|
188
|
+
getPopupContainer={popupContainer}
|
|
189
|
+
options={modifierOptions.map(m => ({
|
|
190
|
+
value: m.id,
|
|
191
|
+
label: m.label,
|
|
192
|
+
disabled: m.id === customMod2
|
|
193
|
+
}))}
|
|
186
194
|
/>
|
|
187
|
-
<
|
|
188
|
-
value={
|
|
189
|
-
onChange={
|
|
190
|
-
placeholder={e('
|
|
191
|
-
className='shortcut-custom-
|
|
195
|
+
<Select
|
|
196
|
+
value={customMod2}
|
|
197
|
+
onChange={setCustomMod2}
|
|
198
|
+
placeholder={e('modifier') + ' (' + e('optional') + ')'}
|
|
199
|
+
className='shortcut-custom-select'
|
|
200
|
+
allowClear
|
|
201
|
+
getPopupContainer={popupContainer}
|
|
202
|
+
options={modifierOptions.map(m => ({
|
|
203
|
+
value: m.id,
|
|
204
|
+
label: m.label,
|
|
205
|
+
disabled: m.id === customMod1
|
|
206
|
+
}))}
|
|
207
|
+
/>
|
|
208
|
+
<Select
|
|
209
|
+
value={customKey}
|
|
210
|
+
onChange={setCustomKey}
|
|
211
|
+
placeholder={e('key')}
|
|
212
|
+
className='shortcut-custom-select shortcut-custom-key'
|
|
213
|
+
allowClear
|
|
214
|
+
showSearch
|
|
215
|
+
getPopupContainer={popupContainer}
|
|
216
|
+
options={keyOptions.map(k => ({
|
|
217
|
+
value: k.id,
|
|
218
|
+
label: k.label
|
|
219
|
+
}))}
|
|
192
220
|
/>
|
|
193
221
|
<button
|
|
194
222
|
type='button'
|
|
195
223
|
className='custom-modal-ok-btn shortcut-custom-add'
|
|
224
|
+
disabled={!customKey}
|
|
196
225
|
onClick={handleAddCustom}
|
|
197
226
|
>
|
|
198
227
|
<PlusOutlined /> {e('add')}
|
|
@@ -1,16 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lazy entry for shortcut-bar.
|
|
3
3
|
*
|
|
4
|
-
* The bar
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The bar compensates for the missing physical keyboard while the system
|
|
5
|
+
* input panel (on-screen keyboard) is shown over a terminal. Capability
|
|
6
|
+
* probes like `(pointer: coarse)` / `maxTouchPoints` are unreliable here —
|
|
7
|
+
* remote-control clients and some touch-capable laptops report touch even
|
|
8
|
+
* though tapping the terminal never brings up a soft keyboard.
|
|
9
|
+
*
|
|
10
|
+
* So instead of guessing, we react to the real signal: the soft keyboard
|
|
11
|
+
* shrinking `window.visualViewport`. Until that is observed we render nothing
|
|
12
|
+
* AND never import the real component, so its code (and the antd/icon deps it
|
|
13
|
+
* pulls in) stays out of the main bundle on every device that doesn't
|
|
14
|
+
* actually show a keyboard.
|
|
15
|
+
*
|
|
16
|
+
* Detection is cross-platform:
|
|
17
|
+
* - visualViewport.height always reflects the *visible* area and shrinks
|
|
18
|
+
* when the keyboard opens on BOTH iOS and Android.
|
|
19
|
+
* - the layout viewport (window.innerHeight) is what differs: iOS keeps it
|
|
20
|
+
* fixed, Android resizes it too — so the naive innerHeight−visualViewport
|
|
21
|
+
* gap works on iOS but is ~0 on Android. We therefore compare
|
|
22
|
+
* visualViewport.height against a baseline captured at the moment the
|
|
23
|
+
* terminal input is focused (i.e. before the keyboard opens), which holds
|
|
24
|
+
* the full pre-keyboard height on both platforms.
|
|
8
25
|
*/
|
|
9
26
|
|
|
10
|
-
import { lazy, Suspense } from 'react'
|
|
27
|
+
import { lazy, Suspense, useEffect, useState } from 'react'
|
|
11
28
|
|
|
12
29
|
const ShortcutBar = lazy(() => import('./shortcut-bar'))
|
|
13
30
|
|
|
31
|
+
// minimum height (px) the visual viewport must lose for us to treat it as
|
|
32
|
+
// "the soft keyboard is now covering part of the screen". A soft keyboard is
|
|
33
|
+
// typically ≥150px; a browser address-bar show/hide is well under this.
|
|
34
|
+
const KEYBOARD_HEIGHT = 120
|
|
35
|
+
|
|
36
|
+
// kept as a coarse capability fallback for engines without visualViewport.
|
|
37
|
+
// Modern Chromium (Electron) always exposes window.visualViewport, so this is
|
|
38
|
+
// effectively only reached on very old runtimes — where remote-control
|
|
39
|
+
// misreporting is not a concern.
|
|
14
40
|
export function isTouchDevice () {
|
|
15
41
|
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
16
42
|
return false
|
|
@@ -23,7 +49,67 @@ export function isTouchDevice () {
|
|
|
23
49
|
}
|
|
24
50
|
|
|
25
51
|
export default function ShortcutBarEntry (props) {
|
|
26
|
-
|
|
52
|
+
// mount the lazy bar only after a soft keyboard has been seen at least once,
|
|
53
|
+
// so its code (and antd/icon deps) stays out of the main bundle on devices
|
|
54
|
+
// that never show one (desktops, remote-control clients mis-reported as
|
|
55
|
+
// touch, etc.).
|
|
56
|
+
//
|
|
57
|
+
// opt out entirely when the user has disabled the shortcut bar in common
|
|
58
|
+
// settings — bail before any listener is attached or component imported.
|
|
59
|
+
const { store } = props
|
|
60
|
+
const disabled = store.config.disableShortcutBar
|
|
61
|
+
const [keyboardSeen, setKeyboardSeen] = useState(() => {
|
|
62
|
+
if (disabled || typeof window === 'undefined' || !window.visualViewport) {
|
|
63
|
+
return disabled ? false : isTouchDevice()
|
|
64
|
+
}
|
|
65
|
+
return false
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const vv = window.visualViewport
|
|
70
|
+
if (!vv) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
// baseline = the visual viewport captured the last time focus moved into a
|
|
74
|
+
// terminal. The keyboard opens *after* that focus, so this baseline holds
|
|
75
|
+
// the pre-keyboard (full) size, and the subsequent resize lets us measure
|
|
76
|
+
// exactly how much the keyboard took — on both iOS and Android.
|
|
77
|
+
let baseline = { height: vv.height, width: vv.width }
|
|
78
|
+
|
|
79
|
+
function onFocusIn (ev) {
|
|
80
|
+
const el = ev.target
|
|
81
|
+
if (el && el.closest && el.closest('.term-wrap')) {
|
|
82
|
+
baseline = { height: vv.height, width: vv.width }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function onResize () {
|
|
87
|
+
// ignore viewport changes that also changed width (rotation / split) —
|
|
88
|
+
// a keyboard changes height only.
|
|
89
|
+
const sameWidth = Math.abs(vv.width - baseline.width) < 20
|
|
90
|
+
if (sameWidth && baseline.height - vv.height > KEYBOARD_HEIGHT) {
|
|
91
|
+
setKeyboardSeen(true)
|
|
92
|
+
// The user is focused in a terminal with the keyboard up — they need
|
|
93
|
+
// the bar right now. Set visibility here rather than waiting for the
|
|
94
|
+
// focus/pointer listener inside the (just-mounted) ShortcutBar, which
|
|
95
|
+
// would otherwise miss the focusin that already fired before it
|
|
96
|
+
// mounted — leaving the bar's reserved space empty until a 2nd tap.
|
|
97
|
+
const { store } = window
|
|
98
|
+
if (store && store.inActiveTerminal) {
|
|
99
|
+
store.shortcutBarVisible = true
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
document.addEventListener('focusin', onFocusIn)
|
|
105
|
+
vv.addEventListener('resize', onResize)
|
|
106
|
+
return () => {
|
|
107
|
+
document.removeEventListener('focusin', onFocusIn)
|
|
108
|
+
vv.removeEventListener('resize', onResize)
|
|
109
|
+
}
|
|
110
|
+
}, [])
|
|
111
|
+
|
|
112
|
+
if (disabled || !keyboardSeen) {
|
|
27
113
|
return null
|
|
28
114
|
}
|
|
29
115
|
return (
|
|
@@ -22,17 +22,43 @@ import { DownOutlined, EditOutlined } from '@ant-design/icons'
|
|
|
22
22
|
import { refs } from '../common/ref'
|
|
23
23
|
import { shortcutBarHeight } from '../../common/constants'
|
|
24
24
|
import ShortcutBarEdit from './shortcut-bar-edit'
|
|
25
|
-
import { candidates, loadActive, saveActive } from './shortcut-bar-defs'
|
|
25
|
+
import { candidates, loadActive, saveActive, ESC } from './shortcut-bar-defs'
|
|
26
26
|
import './shortcut-bar.styl'
|
|
27
27
|
|
|
28
28
|
const e = window.translate
|
|
29
29
|
|
|
30
|
+
// Arrow / Home / End keys have two wire forms: the normal CSI form (ESC [ x)
|
|
31
|
+
// and the application SS3 form (ESC O x). Which one a keypress should produce
|
|
32
|
+
// is decided by the terminal's DECCKM (Application Cursor Keys) state, which
|
|
33
|
+
// the remote program sets. Some shells accept both forms (bash/readline), but
|
|
34
|
+
// stricter line editors such as OpenWrt's busybox ash only recognize the form
|
|
35
|
+
// matching the current mode — so always sending the CSI form breaks arrow
|
|
36
|
+
// keys there. A real keyboard switches on the mode; this does the same.
|
|
37
|
+
const appCursorMap = {
|
|
38
|
+
[ESC + '[A']: ESC + 'OA', // ↑
|
|
39
|
+
[ESC + '[B']: ESC + 'OB', // ↓
|
|
40
|
+
[ESC + '[C']: ESC + 'OC', // →
|
|
41
|
+
[ESC + '[D']: ESC + 'OD', // ←
|
|
42
|
+
[ESC + '[H']: ESC + 'OH', // Home
|
|
43
|
+
[ESC + '[F']: ESC + 'OF' // End
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
function sendToTerminal (data) {
|
|
31
47
|
const { store } = window
|
|
32
48
|
const term = refs.get('term-' + store.activeTabId)
|
|
33
49
|
if (term && typeof term.runQuickCommand === 'function') {
|
|
34
|
-
|
|
50
|
+
const resolved = resolveCursorMode(data, term)
|
|
51
|
+
term.runQuickCommand(resolved, true)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// honor the active terminal's DECCKM state for the cursor / Home / End keys
|
|
56
|
+
function resolveCursorMode (data, term) {
|
|
57
|
+
const app = appCursorMap[data]
|
|
58
|
+
if (app && term?.term?.modes?.applicationCursorKeysMode) {
|
|
59
|
+
return app
|
|
35
60
|
}
|
|
61
|
+
return data
|
|
36
62
|
}
|
|
37
63
|
|
|
38
64
|
function ShortcutBar (props) {
|
|
@@ -85,10 +111,10 @@ function ShortcutBar (props) {
|
|
|
85
111
|
store.shortcutBarVisible = false
|
|
86
112
|
}
|
|
87
113
|
|
|
114
|
+
// edits inside the modal persist live; the modal closes via its own X button.
|
|
88
115
|
function handleSave (next) {
|
|
89
116
|
setButtons(next)
|
|
90
117
|
saveActive(next)
|
|
91
|
-
setEditing(false)
|
|
92
118
|
}
|
|
93
119
|
|
|
94
120
|
function renderButton (b, i) {
|
|
@@ -171,12 +171,20 @@
|
|
|
171
171
|
gap 8px
|
|
172
172
|
align-items center
|
|
173
173
|
flex-wrap wrap
|
|
174
|
+
position relative
|
|
174
175
|
.shortcut-search-input
|
|
175
176
|
flex 1
|
|
176
177
|
min-width 160px
|
|
177
178
|
.shortcut-custom-input
|
|
178
179
|
flex 1
|
|
179
180
|
min-width 120px
|
|
181
|
+
.shortcut-custom-select
|
|
182
|
+
flex 1
|
|
183
|
+
min-width 120px
|
|
184
|
+
width 120px
|
|
185
|
+
.shortcut-custom-key
|
|
186
|
+
min-width 110px
|
|
187
|
+
width 110px
|
|
180
188
|
.shortcut-custom-add
|
|
181
189
|
height 32px
|
|
182
190
|
white-space nowrap
|