@electerm/electerm-react 3.15.90 → 3.15.100
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 +0 -1
- package/client/common/default-setting.js +2 -1
- package/client/common/sanitize-filename.js +12 -5
- package/client/components/setting-panel/setting-terminal.jsx +4 -0
- package/client/components/shortcuts/shortcut-handler.js +39 -0
- package/client/components/sidebar/info-modal.jsx +6 -6
- package/client/components/terminal/term-search.jsx +2 -2
- package/client/components/terminal/terminal-color-query.mjs +133 -0
- package/client/components/terminal/terminal.jsx +85 -27
- package/client/components/terminal/terminal.styl +4 -0
- package/client/components/terminal/xterm-loader.js +0 -11
- package/client/components/tree-list/tree-list-rows.js +33 -2
- package/client/components/tree-list/tree-list.jsx +12 -2
- package/client/components/tree-list/tree-list.styl +36 -0
- package/client/components/tree-list/tree-search.jsx +87 -9
- package/client/components/vnc/vnc-session.jsx +10 -1
- package/package.json +1 -1
|
@@ -28,7 +28,7 @@ export default {
|
|
|
28
28
|
terminalBackgroundTextSize: 48,
|
|
29
29
|
terminalBackgroundTextColor: '#ffffff',
|
|
30
30
|
terminalBackgroundTextFontFamily: 'Maple Mono',
|
|
31
|
-
rendererType: '
|
|
31
|
+
rendererType: 'dom',
|
|
32
32
|
terminalType: 'xterm-256color',
|
|
33
33
|
keepaliveCountMax: 10,
|
|
34
34
|
keyword2FA: 'verification code,otp,one-time,two-factor,2fa,totp,authenticator,duo,yubikey,security code,mfa,passcode',
|
|
@@ -48,6 +48,7 @@ export default {
|
|
|
48
48
|
sftpPathFollowSsh: false,
|
|
49
49
|
keepaliveInterval: 10000,
|
|
50
50
|
backspaceMode: '^?',
|
|
51
|
+
shiftEnterMode: '\\n',
|
|
51
52
|
showHiddenFilesOnSftpStart: true,
|
|
52
53
|
terminalInfos: [
|
|
53
54
|
'uptime',
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* Rules applied:
|
|
10
10
|
* - Remove control characters (0x00-0x1F)
|
|
11
11
|
* - Replace reserved characters: < > : " / \ | ? * with _
|
|
12
|
-
* -
|
|
12
|
+
* - Strip trailing dots and spaces (Windows restriction: "file.", "file ")
|
|
13
|
+
* - Strip leading spaces only (NOT leading dots — they mean hidden file)
|
|
13
14
|
* - Reject reserved Windows device names: CON, PRN, AUX, NUL, COM1-9, LPT1-9
|
|
14
15
|
* - Limit filename length to 255 bytes (common filesystem limit)
|
|
15
16
|
* - Fallback to 'unnamed' if result is empty
|
|
@@ -19,8 +20,12 @@
|
|
|
19
20
|
// eslint-disable-next-line no-control-regex
|
|
20
21
|
const ILLEGAL_CHARS = /[<>:"/\\|?\x00-\x1f]/g
|
|
21
22
|
|
|
22
|
-
//
|
|
23
|
-
|
|
23
|
+
// Trailing dots and spaces = problematic on Windows (e.g. "file." → "file")
|
|
24
|
+
// Leading dots are PRESERVED — they mean "hidden file" on Unix and work on modern Windows
|
|
25
|
+
const TRAILING_DOTS_SPACES = /[.\s]+$/g
|
|
26
|
+
|
|
27
|
+
// Leading spaces only — Windows can't handle filenames starting with space
|
|
28
|
+
const LEADING_SPACES = /^\s+/
|
|
24
29
|
|
|
25
30
|
// Reserved Windows device names (case-insensitive)
|
|
26
31
|
const RESERVED_NAMES = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i
|
|
@@ -37,8 +42,10 @@ export default function sanitizeFilename (name) {
|
|
|
37
42
|
let safe = name
|
|
38
43
|
// Replace illegal characters
|
|
39
44
|
.replace(ILLEGAL_CHARS, REPLACEMENT_CHAR)
|
|
40
|
-
// Strip
|
|
41
|
-
.replace(
|
|
45
|
+
// Strip trailing dots and spaces (Windows restriction)
|
|
46
|
+
.replace(TRAILING_DOTS_SPACES, '')
|
|
47
|
+
// Strip leading spaces only (not dots — they mean hidden file)
|
|
48
|
+
.replace(LEADING_SPACES, '')
|
|
42
49
|
|
|
43
50
|
// Handle reserved Windows device names by appending underscore
|
|
44
51
|
if (RESERVED_NAMES.test(safe)) {
|
|
@@ -552,6 +552,10 @@ export default class SettingTerminal extends Component {
|
|
|
552
552
|
{
|
|
553
553
|
this.renderText('terminalWordSeparator', e('terminalWordSeparator'))
|
|
554
554
|
}
|
|
555
|
+
<div className='pd1b'>Shift+Enter</div>
|
|
556
|
+
{
|
|
557
|
+
this.renderText('shiftEnterMode', 'Text to send on Shift+Enter. Use \\n for newline')
|
|
558
|
+
}
|
|
555
559
|
{
|
|
556
560
|
this.renderCursorStyleSelect()
|
|
557
561
|
}
|
|
@@ -5,6 +5,30 @@ import {
|
|
|
5
5
|
} from '../../common/constants'
|
|
6
6
|
import keyControlPressed from '../../common/key-control-pressed.js'
|
|
7
7
|
|
|
8
|
+
function processEscapeSequences (text) {
|
|
9
|
+
let result = ''
|
|
10
|
+
for (let i = 0; i < text.length; i++) {
|
|
11
|
+
if (text[i] === '\\' && i + 1 < text.length) {
|
|
12
|
+
const next = text[i + 1]
|
|
13
|
+
if (next === 'n') {
|
|
14
|
+
result += '\n'
|
|
15
|
+
} else if (next === 't') {
|
|
16
|
+
result += '\t'
|
|
17
|
+
} else if (next === 'r') {
|
|
18
|
+
result += '\r'
|
|
19
|
+
} else if (next === '\\') {
|
|
20
|
+
result += '\\'
|
|
21
|
+
} else {
|
|
22
|
+
result += '\\' + next
|
|
23
|
+
}
|
|
24
|
+
i++
|
|
25
|
+
} else {
|
|
26
|
+
result += text[i]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result
|
|
30
|
+
}
|
|
31
|
+
|
|
8
32
|
function sendInputData (ctx, data) {
|
|
9
33
|
if (!data) return
|
|
10
34
|
if (ctx.attachAddon && ctx.attachAddon._sendData) {
|
|
@@ -170,6 +194,21 @@ export function shortcutExtend (Cls) {
|
|
|
170
194
|
this.socket.send(char)
|
|
171
195
|
this.term.scrollToBottom()
|
|
172
196
|
return false
|
|
197
|
+
} else if (
|
|
198
|
+
this.term &&
|
|
199
|
+
key === 'Enter' &&
|
|
200
|
+
type === 'keydown' &&
|
|
201
|
+
shiftKey &&
|
|
202
|
+
!ctrlKey &&
|
|
203
|
+
!altKey &&
|
|
204
|
+
!metaKey
|
|
205
|
+
) {
|
|
206
|
+
event.preventDefault()
|
|
207
|
+
event.stopPropagation()
|
|
208
|
+
const shiftEnterText = processEscapeSequences(this.props.config.shiftEnterMode || '\\n')
|
|
209
|
+
this.socket.send(shiftEnterText)
|
|
210
|
+
this.term.scrollToBottom()
|
|
211
|
+
return false
|
|
173
212
|
} else if (
|
|
174
213
|
this.term &&
|
|
175
214
|
this.term.buffer.active.type === 'alternate'
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
HighlightOutlined,
|
|
5
5
|
HomeOutlined,
|
|
6
6
|
UserOutlined,
|
|
7
|
-
WarningOutlined,
|
|
8
7
|
InfoCircleOutlined,
|
|
9
8
|
AlignLeftOutlined,
|
|
10
9
|
BugOutlined,
|
|
11
|
-
HeartOutlined
|
|
10
|
+
HeartOutlined,
|
|
11
|
+
CloudOutlined
|
|
12
12
|
} from '@ant-design/icons'
|
|
13
13
|
import { Tabs, Button } from 'antd'
|
|
14
14
|
import Modal from '../common/modal'
|
|
@@ -136,10 +136,10 @@ export default auto(function InfoModal (props) {
|
|
|
136
136
|
url: bugReportLink
|
|
137
137
|
},
|
|
138
138
|
releases: releaseLink,
|
|
139
|
-
privacyNoticeLink,
|
|
140
139
|
sponsorLink,
|
|
141
140
|
knownIssuesLink
|
|
142
141
|
} = packInfo
|
|
142
|
+
const electermOnline = 'https://cloud.electerm.org'
|
|
143
143
|
const link = releaseLink.replace('/releases', '')
|
|
144
144
|
const { versions } = window.pre
|
|
145
145
|
const deps = {
|
|
@@ -215,9 +215,9 @@ export default auto(function InfoModal (props) {
|
|
|
215
215
|
</Link>
|
|
216
216
|
</p>
|
|
217
217
|
<p className='mg1b'>
|
|
218
|
-
<
|
|
219
|
-
<Link to={
|
|
220
|
-
{
|
|
218
|
+
<CloudOutlined /> <b className='mg1r'>electerm Online ➾</b>
|
|
219
|
+
<Link to={electermOnline} className='mg1l'>
|
|
220
|
+
{electermOnline}
|
|
221
221
|
</Link>
|
|
222
222
|
</p>
|
|
223
223
|
<p className='mg1b'>
|
|
@@ -63,14 +63,14 @@ export default class TermSearch extends PureComponent {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
prev = () => {
|
|
66
|
+
prev = (searchTerm) => {
|
|
67
67
|
const {
|
|
68
68
|
activeTabId,
|
|
69
69
|
termSearchOptions
|
|
70
70
|
} = this.props
|
|
71
71
|
refs.get('term-' + activeTabId)
|
|
72
72
|
?.searchPrev(
|
|
73
|
-
this.props.termSearch,
|
|
73
|
+
searchTerm ?? this.props.termSearch,
|
|
74
74
|
copy(termSearchOptions)
|
|
75
75
|
)
|
|
76
76
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const ESC = '\x1b'
|
|
2
|
+
const ST = '\x1b\\'
|
|
3
|
+
|
|
4
|
+
function toHexByte (value) {
|
|
5
|
+
return value.toString(16).padStart(2, '0')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function clampByte (value) {
|
|
9
|
+
return Math.max(0, Math.min(255, Math.round(value)))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseChannel (value) {
|
|
13
|
+
const trimmed = value.trim()
|
|
14
|
+
if (!trimmed) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
if (trimmed.endsWith('%')) {
|
|
18
|
+
const percent = Number(trimmed.slice(0, -1))
|
|
19
|
+
return Number.isFinite(percent)
|
|
20
|
+
? clampByte(percent * 255 / 100)
|
|
21
|
+
: null
|
|
22
|
+
}
|
|
23
|
+
const num = Number(trimmed)
|
|
24
|
+
return Number.isFinite(num)
|
|
25
|
+
? clampByte(num)
|
|
26
|
+
: null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseAlpha (value) {
|
|
30
|
+
if (!value) {
|
|
31
|
+
return 1
|
|
32
|
+
}
|
|
33
|
+
const trimmed = value.trim()
|
|
34
|
+
if (trimmed.endsWith('%')) {
|
|
35
|
+
const percent = Number(trimmed.slice(0, -1))
|
|
36
|
+
return Number.isFinite(percent) ? percent / 100 : null
|
|
37
|
+
}
|
|
38
|
+
const num = Number(trimmed)
|
|
39
|
+
return Number.isFinite(num) ? num : null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseHexColor (color) {
|
|
43
|
+
const match = color.match(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i)
|
|
44
|
+
if (!match) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
const raw = match[1]
|
|
48
|
+
const expanded = raw.length <= 4
|
|
49
|
+
? raw.split('').map(char => char + char).join('')
|
|
50
|
+
: raw
|
|
51
|
+
const alpha = expanded.length === 8
|
|
52
|
+
? parseInt(expanded.slice(6, 8), 16) / 255
|
|
53
|
+
: 1
|
|
54
|
+
if (alpha <= 0) {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
return [
|
|
58
|
+
parseInt(expanded.slice(0, 2), 16),
|
|
59
|
+
parseInt(expanded.slice(2, 4), 16),
|
|
60
|
+
parseInt(expanded.slice(4, 6), 16)
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseRgbColor (color) {
|
|
65
|
+
const match = color.match(/^rgba?\((.*)\)$/i)
|
|
66
|
+
if (!match) {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
const body = match[1].trim()
|
|
70
|
+
const [channelsPart, slashAlpha] = body.split('/').map(part => part.trim())
|
|
71
|
+
const parts = channelsPart.includes(',')
|
|
72
|
+
? channelsPart.split(',').map(part => part.trim())
|
|
73
|
+
: channelsPart.split(/\s+/).filter(Boolean)
|
|
74
|
+
if (parts.length < 3) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
const channels = parts.slice(0, 3).map(parseChannel)
|
|
78
|
+
if (channels.some(channel => channel === null)) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
const alpha = parseAlpha(slashAlpha || parts[3])
|
|
82
|
+
if (alpha === null || alpha <= 0) {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
return channels
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseColorToRgb (color) {
|
|
89
|
+
if (typeof color !== 'string') {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
const trimmed = color.trim()
|
|
93
|
+
if (!trimmed || trimmed.toLowerCase() === 'transparent') {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
return parseHexColor(trimmed) || parseRgbColor(trimmed)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function colorToOscRgb (color) {
|
|
100
|
+
const rgb = parseColorToRgb(color)
|
|
101
|
+
if (!rgb) {
|
|
102
|
+
return ''
|
|
103
|
+
}
|
|
104
|
+
return `rgb:${rgb.map(toHexByte).join('/')}`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function buildOscColorResponse (identifier, color, fallbackColor) {
|
|
108
|
+
const oscColor = colorToOscRgb(color) || colorToOscRgb(fallbackColor)
|
|
109
|
+
return oscColor
|
|
110
|
+
? `${ESC}]${identifier};${oscColor}${ST}`
|
|
111
|
+
: ''
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function handleTerminalColorQuery (terminal, identifier, color, fallbackColor, data) {
|
|
115
|
+
if (typeof data !== 'string' || data.trim() !== '?') {
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
const response = buildOscColorResponse(identifier, color, fallbackColor)
|
|
119
|
+
if (!response || typeof terminal?.input !== 'function') {
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
terminal.input(response, false)
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function createRendererThemeConfig (themeConfig = {}, rendererType, visibleBackground) {
|
|
127
|
+
return {
|
|
128
|
+
...themeConfig,
|
|
129
|
+
background: rendererType === 'webGL' && colorToOscRgb(visibleBackground)
|
|
130
|
+
? visibleBackground
|
|
131
|
+
: 'rgba(0,0,0,0)'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -57,13 +57,16 @@ import {
|
|
|
57
57
|
loadTerminal,
|
|
58
58
|
loadFitAddon,
|
|
59
59
|
loadWebLinksAddon,
|
|
60
|
-
loadCanvasAddon,
|
|
61
60
|
loadWebglAddon,
|
|
62
61
|
loadSearchAddon,
|
|
63
62
|
loadLigaturesAddon,
|
|
64
63
|
loadUnicode11Addon,
|
|
65
64
|
loadImageAddon
|
|
66
65
|
} from './xterm-loader.js'
|
|
66
|
+
import {
|
|
67
|
+
createRendererThemeConfig,
|
|
68
|
+
handleTerminalColorQuery
|
|
69
|
+
} from './terminal-color-query.mjs'
|
|
67
70
|
|
|
68
71
|
const e = window.translate
|
|
69
72
|
|
|
@@ -141,10 +144,8 @@ class Term extends Component {
|
|
|
141
144
|
prevProps.themeConfig
|
|
142
145
|
)
|
|
143
146
|
if (themeChanged && this.term) {
|
|
144
|
-
this.term.options.theme =
|
|
145
|
-
|
|
146
|
-
background: 'rgba(0,0,0,0)'
|
|
147
|
-
}
|
|
147
|
+
this.term.options.theme = this.getRendererThemeConfig(this.props.themeConfig)
|
|
148
|
+
this.registerTerminalColorQueryHandlers(this.term, this.props.themeConfig)
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
|
|
@@ -156,6 +157,7 @@ class Term extends Component {
|
|
|
156
157
|
if (this.term) {
|
|
157
158
|
this.term.parent = null
|
|
158
159
|
}
|
|
160
|
+
this.disposeTerminalColorQueryHandlers()
|
|
159
161
|
Object.keys(this.timers).forEach(k => {
|
|
160
162
|
clearTimeout(this.timers[k])
|
|
161
163
|
this.timers[k] = null
|
|
@@ -451,13 +453,19 @@ class Term extends Component {
|
|
|
451
453
|
return
|
|
452
454
|
}
|
|
453
455
|
|
|
456
|
+
const hasMultiFiles = filePaths.length > 1
|
|
457
|
+
|
|
454
458
|
if (isSshTerminal) {
|
|
455
459
|
const behavior = this.props.config.dragDropBehavior || 'ask'
|
|
456
|
-
|
|
460
|
+
// multi-file drops always paste paths, skip the action modal
|
|
461
|
+
if (behavior === 'ask' && !hasMultiFiles) {
|
|
457
462
|
this.setState({
|
|
458
463
|
dropFileModalVisible: true,
|
|
459
464
|
droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
|
|
460
465
|
})
|
|
466
|
+
} else if (hasMultiFiles) {
|
|
467
|
+
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
468
|
+
this.attachAddon._sendData(filesAll)
|
|
461
469
|
} else {
|
|
462
470
|
this.handleDropFileAction(behavior, filePaths.map(path => ({ path, isRemote: false })))
|
|
463
471
|
}
|
|
@@ -465,10 +473,16 @@ class Term extends Component {
|
|
|
465
473
|
}
|
|
466
474
|
|
|
467
475
|
if (isSerialTerminal) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
476
|
+
// multi-file drops always paste paths, skip the action modal
|
|
477
|
+
if (hasMultiFiles) {
|
|
478
|
+
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
479
|
+
this.attachAddon._sendData(filesAll)
|
|
480
|
+
} else {
|
|
481
|
+
this.setState({
|
|
482
|
+
dropFileModalVisible: true,
|
|
483
|
+
droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
|
|
484
|
+
})
|
|
485
|
+
}
|
|
472
486
|
return
|
|
473
487
|
}
|
|
474
488
|
|
|
@@ -606,7 +620,7 @@ class Term extends Component {
|
|
|
606
620
|
this.term.clear()
|
|
607
621
|
this.term.focus()
|
|
608
622
|
if (shouldClear) {
|
|
609
|
-
this.searchAddon.
|
|
623
|
+
this.searchAddon._lineCache.clear()
|
|
610
624
|
this.timers.clearSearchTimer = setTimeout(() => {
|
|
611
625
|
refsStatic.get('term-search')?.next()
|
|
612
626
|
}, 100)
|
|
@@ -658,8 +672,8 @@ class Term extends Component {
|
|
|
658
672
|
}
|
|
659
673
|
|
|
660
674
|
updateSearchResults = (resultIndex) => {
|
|
661
|
-
const matches = this.searchAddon.
|
|
662
|
-
return
|
|
675
|
+
const matches = this.searchAddon._resultTracker.searchResults.map((result, i) => {
|
|
676
|
+
return result.row
|
|
663
677
|
})
|
|
664
678
|
|
|
665
679
|
this.setState({
|
|
@@ -1066,26 +1080,70 @@ class Term extends Component {
|
|
|
1066
1080
|
}
|
|
1067
1081
|
|
|
1068
1082
|
loadRenderer = async (term, config) => {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1083
|
+
// xterm 6.x: only the built-in DOM renderer and the WebGL addon exist
|
|
1084
|
+
// (the canvas renderer addon was removed in 6.x). 'dom' = no addon loaded
|
|
1085
|
+
// (built-in DOM renderer). Legacy 'canvas' settings fall back to DOM.
|
|
1086
|
+
if (config.rendererType === rendererTypes.webGL) {
|
|
1073
1087
|
try {
|
|
1074
1088
|
const WebglAddon = await loadWebglAddon()
|
|
1075
1089
|
term.loadAddon(new WebglAddon())
|
|
1076
1090
|
} catch (e) {
|
|
1077
|
-
console.error('render with webgl failed, fallback to
|
|
1091
|
+
console.error('render with webgl failed, fallback to dom renderer')
|
|
1078
1092
|
console.error(e)
|
|
1079
|
-
|
|
1080
|
-
term.loadAddon(new CanvasAddon())
|
|
1093
|
+
// built-in DOM renderer is used (no addon loaded)
|
|
1081
1094
|
}
|
|
1082
1095
|
}
|
|
1083
1096
|
}
|
|
1084
1097
|
|
|
1098
|
+
terminalColorQueryDisposables = []
|
|
1099
|
+
|
|
1100
|
+
disposeTerminalColorQueryHandlers = () => {
|
|
1101
|
+
this.terminalColorQueryDisposables.forEach(disposable => disposable?.dispose?.())
|
|
1102
|
+
this.terminalColorQueryDisposables.length = 0
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
getVisibleTerminalBackground = () => {
|
|
1106
|
+
const uiThemeConfig = window.store?.getUiThemeConfig?.() || {}
|
|
1107
|
+
const dom = this.domRef.current
|
|
1108
|
+
const cssMain = dom && window.getComputedStyle
|
|
1109
|
+
? window.getComputedStyle(dom).getPropertyValue('--main').trim()
|
|
1110
|
+
: ''
|
|
1111
|
+
return uiThemeConfig.main || cssMain || this.props.themeConfig.background
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
getVisibleTerminalForeground = () => {
|
|
1115
|
+
const uiThemeConfig = window.store?.getUiThemeConfig?.() || {}
|
|
1116
|
+
return uiThemeConfig.text
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
registerTerminalColorQueryHandlers = (term, themeConfig = {}) => {
|
|
1120
|
+
this.disposeTerminalColorQueryHandlers()
|
|
1121
|
+
if (!term?.parser?.registerOscHandler) {
|
|
1122
|
+
return
|
|
1123
|
+
}
|
|
1124
|
+
const background = this.getVisibleTerminalBackground()
|
|
1125
|
+
const foregroundFallback = this.getVisibleTerminalForeground()
|
|
1126
|
+
this.terminalColorQueryDisposables.push(
|
|
1127
|
+
term.parser.registerOscHandler(10, data => {
|
|
1128
|
+
return handleTerminalColorQuery(term, 10, themeConfig.foreground, foregroundFallback, data)
|
|
1129
|
+
}),
|
|
1130
|
+
term.parser.registerOscHandler(11, data => {
|
|
1131
|
+
return handleTerminalColorQuery(term, 11, background, themeConfig.background, data)
|
|
1132
|
+
})
|
|
1133
|
+
)
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
getRendererThemeConfig = (themeConfig = this.props.themeConfig) => {
|
|
1137
|
+
return createRendererThemeConfig(
|
|
1138
|
+
deepCopy(themeConfig),
|
|
1139
|
+
this.props.config.rendererType,
|
|
1140
|
+
this.getVisibleTerminalBackground()
|
|
1141
|
+
)
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1085
1144
|
initTerminal = async () => {
|
|
1086
1145
|
const { themeConfig, tab = {}, config = {} } = this.props
|
|
1087
|
-
const tc =
|
|
1088
|
-
tc.background = 'rgba(0,0,0,0)'
|
|
1146
|
+
const tc = this.getRendererThemeConfig(themeConfig)
|
|
1089
1147
|
const Terminal = await loadTerminal()
|
|
1090
1148
|
const term = new Terminal({
|
|
1091
1149
|
allowProposedApi: true,
|
|
@@ -1104,6 +1162,7 @@ class Term extends Component {
|
|
|
1104
1162
|
term.parent = this
|
|
1105
1163
|
term.onSelectionChange(this.onSelection)
|
|
1106
1164
|
term.open(this.domRef.current, true)
|
|
1165
|
+
this.registerTerminalColorQueryHandlers(term, themeConfig)
|
|
1107
1166
|
await this.loadRenderer(term, config)
|
|
1108
1167
|
|
|
1109
1168
|
const FitAddon = await loadFitAddon()
|
|
@@ -1324,7 +1383,7 @@ class Term extends Component {
|
|
|
1324
1383
|
}
|
|
1325
1384
|
|
|
1326
1385
|
openNormalBuffer = () => {
|
|
1327
|
-
const normal = this.term.buffer.
|
|
1386
|
+
const normal = this.term.buffer.normal
|
|
1328
1387
|
const len = normal.length
|
|
1329
1388
|
const lines = new Array(len).fill('').map((x, i) => {
|
|
1330
1389
|
return normal.getLine(i).translateToString(false)
|
|
@@ -1486,13 +1545,12 @@ class Term extends Component {
|
|
|
1486
1545
|
socket.onopen = async () => {
|
|
1487
1546
|
await this.initAttachAddon()
|
|
1488
1547
|
this.runInitScript()
|
|
1489
|
-
term._initialized = true
|
|
1490
1548
|
}
|
|
1491
1549
|
// term.onRrefresh(this.onRefresh)
|
|
1492
1550
|
term.onResize(this.onResizeTerminal)
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1551
|
+
// xterm 6.x exposes buffer change as a public event (IBufferNamespace.onBufferChange).
|
|
1552
|
+
// Previously this reached into the private _onBufferChange._listeners array.
|
|
1553
|
+
term.buffer.onBufferChange(this.onBufferChange)
|
|
1496
1554
|
const WebLinksAddon = await loadWebLinksAddon()
|
|
1497
1555
|
term.loadAddon(new WebLinksAddon(this.webLinkHandler))
|
|
1498
1556
|
term.focus()
|
|
@@ -37,13 +37,6 @@ export async function loadWebLinksAddon () {
|
|
|
37
37
|
return window.xtermAddons.WebLinksAddon
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export async function loadCanvasAddon () {
|
|
41
|
-
if (window.xtermAddons.CanvasAddon) return window.xtermAddons.CanvasAddon
|
|
42
|
-
const mod = await import('@xterm/addon-canvas')
|
|
43
|
-
window.xtermAddons.CanvasAddon = mod.CanvasAddon
|
|
44
|
-
return window.xtermAddons.CanvasAddon
|
|
45
|
-
}
|
|
46
|
-
|
|
47
40
|
export async function loadWebglAddon () {
|
|
48
41
|
if (window.xtermAddons.WebglAddon) return window.xtermAddons.WebglAddon
|
|
49
42
|
const mod = await import('@xterm/addon-webgl')
|
|
@@ -95,10 +88,6 @@ export function getWebLinksAddon () {
|
|
|
95
88
|
return window.xtermAddons.WebLinksAddon
|
|
96
89
|
}
|
|
97
90
|
|
|
98
|
-
export function getCanvasAddon () {
|
|
99
|
-
return window.xtermAddons.CanvasAddon
|
|
100
|
-
}
|
|
101
|
-
|
|
102
91
|
export function getWebglAddon () {
|
|
103
92
|
return window.xtermAddons.WebglAddon
|
|
104
93
|
}
|
|
@@ -4,12 +4,38 @@ function isTopLevelGroup (group) {
|
|
|
4
4
|
return !group?.level || group.level < 2
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
function bookmarkCompare (a, b, bookmarksMap, sortTitle, sortHost) {
|
|
8
|
+
const itemA = bookmarksMap.get(a)
|
|
9
|
+
const itemB = bookmarksMap.get(b)
|
|
10
|
+
if (!itemA || !itemB) {
|
|
11
|
+
return 0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (sortTitle) {
|
|
15
|
+
const cmp = (itemA.title || '').localeCompare(itemB.title || '')
|
|
16
|
+
if (cmp !== 0) {
|
|
17
|
+
return sortTitle === 'asc' ? cmp : -cmp
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (sortHost) {
|
|
22
|
+
const cmp = (itemA.host || '').localeCompare(itemB.host || '')
|
|
23
|
+
if (cmp !== 0) {
|
|
24
|
+
return sortHost === 'asc' ? cmp : -cmp
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return 0
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
export function buildVisibleTreeRows ({
|
|
8
32
|
bookmarkGroups,
|
|
9
33
|
bookmarkGroupTree,
|
|
10
34
|
bookmarksMap,
|
|
11
35
|
expandedKeys,
|
|
12
|
-
keyword
|
|
36
|
+
keyword,
|
|
37
|
+
sortTitle,
|
|
38
|
+
sortHost
|
|
13
39
|
}) {
|
|
14
40
|
const groupTree = bookmarkGroupTree || {}
|
|
15
41
|
const rows = []
|
|
@@ -77,7 +103,12 @@ export function buildVisibleTreeRows ({
|
|
|
77
103
|
visitGroup(groupTree[groupId], nextParentId, depth + 1)
|
|
78
104
|
}
|
|
79
105
|
|
|
80
|
-
|
|
106
|
+
const bookmarkIds = group.bookmarkIds || []
|
|
107
|
+
const sortedIds = (sortTitle || sortHost)
|
|
108
|
+
? [...bookmarkIds].sort((a, b) => bookmarkCompare(a, b, bookmarksMap, sortTitle, sortHost))
|
|
109
|
+
: bookmarkIds
|
|
110
|
+
|
|
111
|
+
for (const bookmarkId of sortedIds) {
|
|
81
112
|
const item = bookmarksMap.get(bookmarkId)
|
|
82
113
|
if (!item || (lowerKeyword && !bookmarkMatches(bookmarkId))) {
|
|
83
114
|
continue
|
|
@@ -44,7 +44,8 @@ export default class ItemListTree extends Component {
|
|
|
44
44
|
categoryTitle: '',
|
|
45
45
|
categoryColor: '',
|
|
46
46
|
categoryId: '',
|
|
47
|
-
searchSelectedRowKey: ''
|
|
47
|
+
searchSelectedRowKey: '',
|
|
48
|
+
sort: null
|
|
48
49
|
}
|
|
49
50
|
this.listRef = React.createRef()
|
|
50
51
|
}
|
|
@@ -109,6 +110,10 @@ export default class ItemListTree extends Component {
|
|
|
109
110
|
})
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
handleSortChange = sort => {
|
|
114
|
+
this.setState({ sort })
|
|
115
|
+
}
|
|
116
|
+
|
|
112
117
|
handleKeyDown = (e) => {
|
|
113
118
|
const { keyword } = this.state
|
|
114
119
|
if (!keyword) return
|
|
@@ -176,12 +181,15 @@ export default class ItemListTree extends Component {
|
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
getVisibleTreeData = () => {
|
|
184
|
+
const sort = this.state.sort
|
|
179
185
|
return buildVisibleTreeRows({
|
|
180
186
|
bookmarkGroups: this.props.bookmarkGroups,
|
|
181
187
|
bookmarkGroupTree: this.props.bookmarkGroupTree,
|
|
182
188
|
bookmarksMap: this.props.bookmarksMap,
|
|
183
189
|
expandedKeys: this.props.expandedKeys,
|
|
184
|
-
keyword: this.state.keyword
|
|
190
|
+
keyword: this.state.keyword,
|
|
191
|
+
sortTitle: sort?.field === 'title' ? sort.dir : null,
|
|
192
|
+
sortHost: sort?.field === 'host' ? sort.dir : null
|
|
185
193
|
})
|
|
186
194
|
}
|
|
187
195
|
|
|
@@ -414,6 +422,8 @@ export default class ItemListTree extends Component {
|
|
|
414
422
|
keyword={this.state.keyword}
|
|
415
423
|
autoFocus={this.props.autoFocus}
|
|
416
424
|
onKeyDown={this.handleKeyDown}
|
|
425
|
+
sort={this.state.sort}
|
|
426
|
+
onSortChange={this.handleSortChange}
|
|
417
427
|
/>
|
|
418
428
|
</div>
|
|
419
429
|
)
|
|
@@ -101,6 +101,42 @@
|
|
|
101
101
|
min-height 0
|
|
102
102
|
position relative
|
|
103
103
|
|
|
104
|
+
.tree-sort-wrap
|
|
105
|
+
position relative
|
|
106
|
+
.tree-sort-dropdown
|
|
107
|
+
display none
|
|
108
|
+
position absolute
|
|
109
|
+
right 0
|
|
110
|
+
top 100%
|
|
111
|
+
margin-top 4px
|
|
112
|
+
background var(--main-dark)
|
|
113
|
+
border 1px solid var(--main)
|
|
114
|
+
border-radius 4px
|
|
115
|
+
padding 4px
|
|
116
|
+
z-index 100
|
|
117
|
+
box-shadow 0 2px 8px rgba(0,0,0,.1)
|
|
118
|
+
&.open
|
|
119
|
+
.tree-sort-dropdown
|
|
120
|
+
display block
|
|
121
|
+
|
|
122
|
+
.tree-sort-trigger
|
|
123
|
+
cursor pointer
|
|
124
|
+
&:hover
|
|
125
|
+
color var(--primary)
|
|
126
|
+
&.active
|
|
127
|
+
color var(--primary)
|
|
128
|
+
|
|
129
|
+
.tree-sort-popover-item
|
|
130
|
+
padding 4px 8px
|
|
131
|
+
border-radius 3px
|
|
132
|
+
cursor pointer
|
|
133
|
+
white-space nowrap
|
|
134
|
+
font-size 12px
|
|
135
|
+
&:hover
|
|
136
|
+
background var(--hover-bg, rgba(0,0,0,.04))
|
|
137
|
+
&.active
|
|
138
|
+
color var(--primary)
|
|
139
|
+
|
|
104
140
|
.tree-list-virtual-spacer
|
|
105
141
|
position relative
|
|
106
142
|
width 100%
|
|
@@ -1,10 +1,33 @@
|
|
|
1
|
-
import React, { useState, memo } from 'react'
|
|
1
|
+
import React, { useState, memo, useCallback, useEffect, useRef } from 'react'
|
|
2
2
|
import { debounce } from 'lodash-es'
|
|
3
|
+
import { Space, Button } from 'antd'
|
|
4
|
+
import { SortAscendingOutlined, CaretUpOutlined, CaretDownOutlined } from '@ant-design/icons'
|
|
3
5
|
import Search from '../common/search'
|
|
4
6
|
import runIdle from '../../common/run-idle'
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
function cycleSort (currentField, currentDir, clickedField) {
|
|
9
|
+
if (currentField === clickedField) {
|
|
10
|
+
if (currentDir === 'asc') {
|
|
11
|
+
return { field: clickedField, dir: 'desc' }
|
|
12
|
+
}
|
|
13
|
+
if (currentDir === 'desc') {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return { field: clickedField, dir: 'asc' }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default memo(function TreeSearchComponent ({
|
|
21
|
+
onSearch,
|
|
22
|
+
keyword,
|
|
23
|
+
autoFocus,
|
|
24
|
+
onKeyDown,
|
|
25
|
+
sort,
|
|
26
|
+
onSortChange
|
|
27
|
+
}) {
|
|
7
28
|
const [searchTerm, setSearchTerm] = useState(keyword)
|
|
29
|
+
const [open, setOpen] = useState(false)
|
|
30
|
+
const wrapRef = useRef(null)
|
|
8
31
|
|
|
9
32
|
const performSearch = debounce((term) => {
|
|
10
33
|
runIdle(() => {
|
|
@@ -24,13 +47,68 @@ export default memo(function TreeSearchComponent ({ onSearch, keyword, autoFocus
|
|
|
24
47
|
}
|
|
25
48
|
}
|
|
26
49
|
|
|
50
|
+
const handleSortClick = useCallback((field) => {
|
|
51
|
+
const next = cycleSort(sort?.field, sort?.dir, field)
|
|
52
|
+
onSortChange(next)
|
|
53
|
+
}, [sort, onSortChange])
|
|
54
|
+
|
|
55
|
+
const handleTriggerClick = () => {
|
|
56
|
+
setOpen(v => !v)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const handleItemClick = (field) => {
|
|
60
|
+
handleSortClick(field)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const isSortActive = !!sort?.field
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!open) return
|
|
67
|
+
const onMouseDown = (e) => {
|
|
68
|
+
if (wrapRef.current && !wrapRef.current.contains(e.target)) {
|
|
69
|
+
setOpen(false)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
document.addEventListener('mousedown', onMouseDown)
|
|
73
|
+
return () => document.removeEventListener('mousedown', onMouseDown)
|
|
74
|
+
}, [open])
|
|
75
|
+
|
|
27
76
|
return (
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
77
|
+
<div className={`tree-sort-wrap iblock${open ? ' open' : ''}`} ref={wrapRef}>
|
|
78
|
+
<Space.Compact className='width-100'>
|
|
79
|
+
<Search
|
|
80
|
+
onChange={handleChange}
|
|
81
|
+
onKeyDown={handleKeyDown}
|
|
82
|
+
value={searchTerm}
|
|
83
|
+
allowClear
|
|
84
|
+
autoFocus={autoFocus}
|
|
85
|
+
/>
|
|
86
|
+
<Button
|
|
87
|
+
className={`tree-sort-trigger pointer${isSortActive ? ' active' : ''}`}
|
|
88
|
+
onClick={handleTriggerClick}
|
|
89
|
+
icon={<SortAscendingOutlined />}
|
|
90
|
+
/>
|
|
91
|
+
</Space.Compact>
|
|
92
|
+
<div className='tree-sort-dropdown'>
|
|
93
|
+
{['title', 'host'].map(field => {
|
|
94
|
+
const isActive = sort?.field === field
|
|
95
|
+
const isAsc = isActive && sort?.dir === 'asc'
|
|
96
|
+
const isDesc = isActive && sort?.dir === 'desc'
|
|
97
|
+
const cap = window.translate(field)
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
key={field}
|
|
101
|
+
className={`tree-sort-popover-item${isActive ? ' active' : ''}`}
|
|
102
|
+
onClick={() => handleItemClick(field)}
|
|
103
|
+
>
|
|
104
|
+
{isDesc
|
|
105
|
+
? <CaretDownOutlined />
|
|
106
|
+
: <CaretUpOutlined className={isAsc ? '' : 'hidden'} />}
|
|
107
|
+
{cap}
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
})}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
35
113
|
)
|
|
36
114
|
})
|
|
@@ -367,7 +367,8 @@ export default class VncSession extends PureComponent {
|
|
|
367
367
|
'desktopname',
|
|
368
368
|
'capabilities',
|
|
369
369
|
'screens',
|
|
370
|
-
'desktopsize'
|
|
370
|
+
'desktopsize',
|
|
371
|
+
'serververification'
|
|
371
372
|
]
|
|
372
373
|
for (const event of events) {
|
|
373
374
|
rfb.addEventListener(event, this[`on${window.capitalizeFirstLetter(event)}`])
|
|
@@ -400,6 +401,14 @@ export default class VncSession extends PureComponent {
|
|
|
400
401
|
message.error('Security Failure: ' + event.detail?.reason)
|
|
401
402
|
}
|
|
402
403
|
|
|
404
|
+
onSerververification = (event) => {
|
|
405
|
+
// RA2ne (RealVNC) authentication requires the client to approve
|
|
406
|
+
// the server's public key before the handshake can proceed.
|
|
407
|
+
// Auto-approve since the user explicitly initiated the connection.
|
|
408
|
+
console.debug('[VNC-CLIENT] Server verification event, approving server:', event.detail?.type)
|
|
409
|
+
this.rfb?.approveServer()
|
|
410
|
+
}
|
|
411
|
+
|
|
403
412
|
onOk = (res) => {
|
|
404
413
|
this.setState({
|
|
405
414
|
showConfirm: false
|