@electerm/electerm-react 5.1.8 → 5.1.20
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/setting-panel/setting-common.jsx +1 -0
- package/client/components/terminal/attach-addon-custom.js +11 -1
- 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',
|
|
@@ -216,8 +216,15 @@ export default class AttachAddonCustom {
|
|
|
216
216
|
let str = data
|
|
217
217
|
if (typeof data !== 'string') {
|
|
218
218
|
try {
|
|
219
|
+
// Decode in streaming mode: slow SSH servers (e.g. embedded router
|
|
220
|
+
// CLIs) often deliver a multi-byte UTF-8 char (CJK = 3 bytes) split
|
|
221
|
+
// across TCP segments, and the server-side idle fast path may forward
|
|
222
|
+
// the first segment immediately. Without { stream: true } the decoder
|
|
223
|
+
// would turn each partial fragment into U+FFFD instead of carrying the
|
|
224
|
+
// trailing bytes over to the next chunk and reassembling the char.
|
|
219
225
|
str = this.decoder.decode(
|
|
220
|
-
data instanceof ArrayBuffer ? data : new Uint8Array(data)
|
|
226
|
+
data instanceof ArrayBuffer ? data : new Uint8Array(data),
|
|
227
|
+
{ stream: true }
|
|
221
228
|
)
|
|
222
229
|
} catch (e) {
|
|
223
230
|
str = ''
|
|
@@ -435,6 +442,9 @@ export default class AttachAddonCustom {
|
|
|
435
442
|
this._flushScheduled = false
|
|
436
443
|
this._writeBuffer = []
|
|
437
444
|
this._bufferBytes = 0
|
|
445
|
+
// Reset the streaming decoder so any partial multi-byte sequence held
|
|
446
|
+
// over from this connection can not leak into a reused instance.
|
|
447
|
+
this.decoder = new TextDecoder('utf-8')
|
|
438
448
|
this.term = null
|
|
439
449
|
this._disposables.forEach(d => d.dispose())
|
|
440
450
|
this._disposables.length = 0
|
|
@@ -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
|