@electerm/electerm-react 3.15.90 → 3.15.105
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/common/notification.jsx +42 -20
- package/client/components/common/placeholder.jsx +5 -0
- package/client/components/common/zoom-control.jsx +24 -2
- package/client/components/footer/footer.styl +7 -0
- package/client/components/main/main.jsx +1 -0
- package/client/components/setting-panel/col.jsx +8 -1
- package/client/components/setting-panel/setting-terminal.jsx +4 -0
- package/client/components/setting-panel/setting-wrap.styl +18 -0
- package/client/components/shortcuts/shortcut-handler.js +39 -0
- package/client/components/sidebar/index.jsx +22 -2
- package/client/components/sidebar/info-modal.jsx +8 -6
- package/client/components/sidebar/sidebar.styl +4 -0
- 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 +131 -29
- package/client/components/terminal/terminal.styl +9 -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)) {
|
|
@@ -9,7 +9,36 @@ import './notification.styl'
|
|
|
9
9
|
const notifications = []
|
|
10
10
|
const NOTIFICATION_EVENT = 'notification-update'
|
|
11
11
|
|
|
12
|
+
function getTextFromReactChildren (children) {
|
|
13
|
+
if (typeof children === 'string' || typeof children === 'number') {
|
|
14
|
+
return String(children)
|
|
15
|
+
}
|
|
16
|
+
if (React.isValidElement(children)) {
|
|
17
|
+
return getTextFromReactChildren(children.props.children)
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(children)) {
|
|
20
|
+
return children.map(getTextFromReactChildren).join('\n')
|
|
21
|
+
}
|
|
22
|
+
return ''
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
function addNotification (notif) {
|
|
26
|
+
notif.descriptions = notif.description ? [notif.description] : []
|
|
27
|
+
if (notif.type === 'error') {
|
|
28
|
+
const messageText = getTextFromReactChildren(notif.message)
|
|
29
|
+
const existingIndex = notifications.findIndex(n =>
|
|
30
|
+
n.type === 'error' &&
|
|
31
|
+
getTextFromReactChildren(n.message) === messageText
|
|
32
|
+
)
|
|
33
|
+
if (existingIndex > -1) {
|
|
34
|
+
const existing = notifications[existingIndex]
|
|
35
|
+
existing.count = (existing.count || 1) + 1
|
|
36
|
+
existing.descriptions.push(notif.description)
|
|
37
|
+
window.dispatchEvent(new CustomEvent(NOTIFICATION_EVENT))
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
notif.count = 1
|
|
41
|
+
}
|
|
13
42
|
notifications.push(notif)
|
|
14
43
|
window.dispatchEvent(new CustomEvent(NOTIFICATION_EVENT))
|
|
15
44
|
}
|
|
@@ -60,9 +89,10 @@ export function NotificationContainer () {
|
|
|
60
89
|
<NotificationItem
|
|
61
90
|
key={notif.key}
|
|
62
91
|
message={notif.message || notif.type}
|
|
63
|
-
|
|
92
|
+
descriptions={notif.descriptions}
|
|
64
93
|
type={notif.type}
|
|
65
94
|
duration={notif.duration}
|
|
95
|
+
count={notif.count || 1}
|
|
66
96
|
onClose={() => removeNotification(notif.key)}
|
|
67
97
|
/>
|
|
68
98
|
))}
|
|
@@ -70,20 +100,7 @@ export function NotificationContainer () {
|
|
|
70
100
|
)
|
|
71
101
|
}
|
|
72
102
|
|
|
73
|
-
function
|
|
74
|
-
if (typeof children === 'string' || typeof children === 'number') {
|
|
75
|
-
return String(children)
|
|
76
|
-
}
|
|
77
|
-
if (React.isValidElement(children)) {
|
|
78
|
-
return getTextFromReactChildren(children.props.children)
|
|
79
|
-
}
|
|
80
|
-
if (Array.isArray(children)) {
|
|
81
|
-
return children.map(getTextFromReactChildren).join('\n')
|
|
82
|
-
}
|
|
83
|
-
return ''
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function NotificationItem ({ message, description, type, onClose, duration = 18.5 }) {
|
|
103
|
+
function NotificationItem ({ message, descriptions = [], type, onClose, duration = 18.5, count = 1 }) {
|
|
87
104
|
const timeoutRef = useRef(null)
|
|
88
105
|
|
|
89
106
|
useEffect(() => {
|
|
@@ -96,7 +113,7 @@ function NotificationItem ({ message, description, type, onClose, duration = 18.
|
|
|
96
113
|
timeoutRef.current = null
|
|
97
114
|
}
|
|
98
115
|
}
|
|
99
|
-
}, [])
|
|
116
|
+
}, [count])
|
|
100
117
|
|
|
101
118
|
const handleMouseEnter = () => {
|
|
102
119
|
if (timeoutRef.current) {
|
|
@@ -118,6 +135,9 @@ function NotificationItem ({ message, description, type, onClose, duration = 18.
|
|
|
118
135
|
}
|
|
119
136
|
|
|
120
137
|
const className = classnames('notification', type)
|
|
138
|
+
const titleText = getTextFromReactChildren(message)
|
|
139
|
+
const title = count > 1 ? `(${count}) ${titleText}` : message
|
|
140
|
+
const validDescriptions = descriptions.filter(Boolean)
|
|
121
141
|
|
|
122
142
|
return (
|
|
123
143
|
<div
|
|
@@ -128,18 +148,20 @@ function NotificationItem ({ message, description, type, onClose, duration = 18.
|
|
|
128
148
|
<div className='notification-content'>
|
|
129
149
|
<div className='notification-message'>
|
|
130
150
|
<div className='notification-icon'>{messageIcons[type]}</div>
|
|
131
|
-
<div className='notification-title' title={
|
|
151
|
+
<div className='notification-title' title={titleText}>{title}</div>
|
|
132
152
|
<CopyOutlined
|
|
133
153
|
className='notification-copy-icon'
|
|
134
154
|
onClick={(e) => handleCopy(message, e)}
|
|
135
155
|
/>
|
|
136
156
|
</div>
|
|
137
|
-
{
|
|
157
|
+
{validDescriptions.length > 0 && (
|
|
138
158
|
<div className='notification-description'>
|
|
139
|
-
{
|
|
159
|
+
{validDescriptions.map((desc, i) => (
|
|
160
|
+
<div key={i}>{desc}</div>
|
|
161
|
+
))}
|
|
140
162
|
<CopyOutlined
|
|
141
163
|
className='notification-copy-icon'
|
|
142
|
-
onClick={(e) => handleCopy(
|
|
164
|
+
onClick={(e) => handleCopy(validDescriptions, e)}
|
|
143
165
|
/>
|
|
144
166
|
</div>
|
|
145
167
|
)}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import InputNumberConfirm from './input-number-confirm'
|
|
3
3
|
import {
|
|
4
|
-
Space
|
|
4
|
+
Space,
|
|
5
|
+
Tooltip
|
|
5
6
|
} from 'antd'
|
|
6
7
|
import {
|
|
7
8
|
MinusCircleOutlined,
|
|
8
|
-
PlusCircleOutlined
|
|
9
|
+
PlusCircleOutlined,
|
|
10
|
+
AimOutlined
|
|
9
11
|
} from '@ant-design/icons'
|
|
10
12
|
|
|
13
|
+
const e = window.translate
|
|
14
|
+
|
|
11
15
|
export default function ZoomControl (props) {
|
|
12
16
|
const {
|
|
13
17
|
value,
|
|
@@ -33,6 +37,23 @@ export default function ZoomControl (props) {
|
|
|
33
37
|
onChange(next)
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
const handleReset = () => {
|
|
41
|
+
onChange(1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const isDefault = Math.round(value * 100) === 100
|
|
45
|
+
|
|
46
|
+
const resetIcon = isDefault
|
|
47
|
+
? null
|
|
48
|
+
: (
|
|
49
|
+
<Tooltip title={e('resetzoom')}>
|
|
50
|
+
<AimOutlined
|
|
51
|
+
onClick={handleReset}
|
|
52
|
+
className='pointer font16 mg1r'
|
|
53
|
+
/>
|
|
54
|
+
</Tooltip>
|
|
55
|
+
)
|
|
56
|
+
|
|
36
57
|
return (
|
|
37
58
|
<InputNumberConfirm
|
|
38
59
|
value={Math.round(value * 100)}
|
|
@@ -43,6 +64,7 @@ export default function ZoomControl (props) {
|
|
|
43
64
|
suffix='%'
|
|
44
65
|
addonBefore={
|
|
45
66
|
<Space.Compact>
|
|
67
|
+
{resetIcon}
|
|
46
68
|
<PlusCircleOutlined
|
|
47
69
|
onClick={handleAdd}
|
|
48
70
|
className='mg1r pointer font16'
|
|
@@ -31,6 +31,13 @@
|
|
|
31
31
|
.terminal-footer-info
|
|
32
32
|
margin-right 10px
|
|
33
33
|
line-height 39px
|
|
34
|
+
.terminal-footer-history
|
|
35
|
+
display flex
|
|
36
|
+
align-items center
|
|
37
|
+
.terminal-footer-qm
|
|
38
|
+
// Q is now an icon-only button (5px internal padding); reduce left
|
|
39
|
+
// margin so the history icon stays roughly equidistant from AI and Q
|
|
40
|
+
margin-left 3px
|
|
34
41
|
.batch-input-outer
|
|
35
42
|
.bi-full
|
|
36
43
|
display none
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* two column layout, left column fixed with, right column auto width
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import Placeholder from '../common/placeholder'
|
|
6
|
+
|
|
5
7
|
export default function SettingCol (props) {
|
|
6
8
|
return (
|
|
7
9
|
<div className='setting-col'>
|
|
@@ -11,7 +13,12 @@ export default function SettingCol (props) {
|
|
|
11
13
|
<div
|
|
12
14
|
className='setting-row setting-row-right'
|
|
13
15
|
>
|
|
14
|
-
|
|
16
|
+
<div className='setting-col-content'>
|
|
17
|
+
{props.children[1]}
|
|
18
|
+
</div>
|
|
19
|
+
<div className='setting-col-placeholder'>
|
|
20
|
+
<Placeholder />
|
|
21
|
+
</div>
|
|
15
22
|
</div>
|
|
16
23
|
</div>
|
|
17
24
|
)
|
|
@@ -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
|
}
|
|
@@ -38,6 +38,20 @@
|
|
|
38
38
|
bottom 0
|
|
39
39
|
top 129px
|
|
40
40
|
padding 20px
|
|
41
|
+
display flex
|
|
42
|
+
flex-direction column
|
|
43
|
+
overflow hidden
|
|
44
|
+
min-height 400px
|
|
45
|
+
.setting-col-content
|
|
46
|
+
flex 1
|
|
47
|
+
overflow-y auto
|
|
48
|
+
min-height 0
|
|
49
|
+
.setting-col-placeholder
|
|
50
|
+
flex-shrink 0
|
|
51
|
+
display flex
|
|
52
|
+
justify-content flex-end
|
|
53
|
+
.carbonads-wrap
|
|
54
|
+
margin 8px 0 0
|
|
41
55
|
.setting-tabs-bookmarks
|
|
42
56
|
.setting-row-left
|
|
43
57
|
width 440px
|
|
@@ -75,6 +89,10 @@
|
|
|
75
89
|
bottom auto
|
|
76
90
|
top auto
|
|
77
91
|
padding 10px
|
|
92
|
+
position static
|
|
93
|
+
overflow visible
|
|
94
|
+
.setting-col-content
|
|
95
|
+
overflow visible
|
|
78
96
|
.setting-tabs-bookmarks
|
|
79
97
|
.setting-row-left
|
|
80
98
|
width auto
|
|
@@ -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'
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
SettingOutlined,
|
|
8
8
|
UpCircleOutlined,
|
|
9
9
|
AppstoreOutlined,
|
|
10
|
-
ThunderboltOutlined
|
|
10
|
+
ThunderboltOutlined,
|
|
11
|
+
AimOutlined
|
|
11
12
|
} from '@ant-design/icons'
|
|
12
13
|
import { Tooltip, Popover } from 'antd'
|
|
13
14
|
import SideBarPanel from './sidebar-panel'
|
|
@@ -42,7 +43,8 @@ export default function Sidebar (props) {
|
|
|
42
43
|
showModal,
|
|
43
44
|
showInfoModal,
|
|
44
45
|
sidebarPanelTab,
|
|
45
|
-
openWidgetsModal
|
|
46
|
+
openWidgetsModal,
|
|
47
|
+
zoom
|
|
46
48
|
} = props
|
|
47
49
|
|
|
48
50
|
const { store } = window
|
|
@@ -85,6 +87,10 @@ export default function Sidebar (props) {
|
|
|
85
87
|
window.store.upgradeInfo.showUpgradeModal = true
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
const handleZoomReset = () => {
|
|
91
|
+
store.onZoomReset()
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
const {
|
|
89
95
|
onNewSsh,
|
|
90
96
|
openSetting,
|
|
@@ -203,6 +209,20 @@ export default function Sidebar (props) {
|
|
|
203
209
|
onClick={openAbout}
|
|
204
210
|
/>
|
|
205
211
|
</SideIcon>
|
|
212
|
+
{
|
|
213
|
+
Math.round((zoom ?? 1) * 100) !== 100
|
|
214
|
+
? (
|
|
215
|
+
<SideIcon
|
|
216
|
+
title={e('resetzoom')}
|
|
217
|
+
>
|
|
218
|
+
<AimOutlined
|
|
219
|
+
className='iblock font16 control-icon zoom-reset-icon'
|
|
220
|
+
onClick={handleZoomReset}
|
|
221
|
+
/>
|
|
222
|
+
</SideIcon>
|
|
223
|
+
)
|
|
224
|
+
: null
|
|
225
|
+
}
|
|
206
226
|
{
|
|
207
227
|
!checkingRemoteVersion && !showUpgradeModal && shouldUpgrade
|
|
208
228
|
? (
|
|
@@ -4,16 +4,17 @@ 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'
|
|
15
15
|
import Link from '../common/external-link'
|
|
16
16
|
import LogoElem from '../common/logo-elem'
|
|
17
|
+
import Placeholder from '../common/placeholder'
|
|
17
18
|
import RunningTime from './app-running-time'
|
|
18
19
|
import { auto } from 'manate/react'
|
|
19
20
|
import { useState } from 'react'
|
|
@@ -136,10 +137,10 @@ export default auto(function InfoModal (props) {
|
|
|
136
137
|
url: bugReportLink
|
|
137
138
|
},
|
|
138
139
|
releases: releaseLink,
|
|
139
|
-
privacyNoticeLink,
|
|
140
140
|
sponsorLink,
|
|
141
141
|
knownIssuesLink
|
|
142
142
|
} = packInfo
|
|
143
|
+
const electermOnline = 'https://cloud.electerm.org'
|
|
143
144
|
const link = releaseLink.replace('/releases', '')
|
|
144
145
|
const { versions } = window.pre
|
|
145
146
|
const deps = {
|
|
@@ -215,9 +216,9 @@ export default auto(function InfoModal (props) {
|
|
|
215
216
|
</Link>
|
|
216
217
|
</p>
|
|
217
218
|
<p className='mg1b'>
|
|
218
|
-
<
|
|
219
|
-
<Link to={
|
|
220
|
-
{
|
|
219
|
+
<CloudOutlined /> <b className='mg1r'>electerm Online ➾</b>
|
|
220
|
+
<Link to={electermOnline} className='mg1l'>
|
|
221
|
+
{electermOnline}
|
|
221
222
|
</Link>
|
|
222
223
|
</p>
|
|
223
224
|
<p className='mg1b'>
|
|
@@ -230,6 +231,7 @@ export default auto(function InfoModal (props) {
|
|
|
230
231
|
<InfoCircleOutlined /> <b className='mg1r'>{window.store.installSrc}</b>
|
|
231
232
|
</p>
|
|
232
233
|
{renderCheckUpdate()}
|
|
234
|
+
<Placeholder />
|
|
233
235
|
</>
|
|
234
236
|
)
|
|
235
237
|
},
|
|
@@ -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
|
+
}
|
|
@@ -4,7 +4,8 @@ import clone from '../../common/to-simple-obj.js'
|
|
|
4
4
|
import resolve from '../../common/resolve.js'
|
|
5
5
|
import {
|
|
6
6
|
Spin,
|
|
7
|
-
Dropdown
|
|
7
|
+
Dropdown,
|
|
8
|
+
Button
|
|
8
9
|
} from 'antd'
|
|
9
10
|
import message from '../common/message'
|
|
10
11
|
import { notification } from '../common/notification'
|
|
@@ -40,6 +41,9 @@ import { getFolderFromFilePath } from '../sftp/file-read.js'
|
|
|
40
41
|
import { CommandTrackerAddon } from './command-tracker-addon.js'
|
|
41
42
|
import { Osc52Addon } from './osc52-addon.js'
|
|
42
43
|
import AIIcon from '../icons/ai-icon.jsx'
|
|
44
|
+
import {
|
|
45
|
+
AimOutlined
|
|
46
|
+
} from '@ant-design/icons'
|
|
43
47
|
import {
|
|
44
48
|
getShellIntegrationCommand,
|
|
45
49
|
detectRemoteShell,
|
|
@@ -57,13 +61,16 @@ import {
|
|
|
57
61
|
loadTerminal,
|
|
58
62
|
loadFitAddon,
|
|
59
63
|
loadWebLinksAddon,
|
|
60
|
-
loadCanvasAddon,
|
|
61
64
|
loadWebglAddon,
|
|
62
65
|
loadSearchAddon,
|
|
63
66
|
loadLigaturesAddon,
|
|
64
67
|
loadUnicode11Addon,
|
|
65
68
|
loadImageAddon
|
|
66
69
|
} from './xterm-loader.js'
|
|
70
|
+
import {
|
|
71
|
+
createRendererThemeConfig,
|
|
72
|
+
handleTerminalColorQuery
|
|
73
|
+
} from './terminal-color-query.mjs'
|
|
67
74
|
|
|
68
75
|
const e = window.translate
|
|
69
76
|
|
|
@@ -87,7 +94,8 @@ class Term extends Component {
|
|
|
87
94
|
reconnectCountdown: null,
|
|
88
95
|
terminalError: null,
|
|
89
96
|
dropFileModalVisible: false,
|
|
90
|
-
droppedFiles: []
|
|
97
|
+
droppedFiles: [],
|
|
98
|
+
fontSizeChanged: false
|
|
91
99
|
}
|
|
92
100
|
this.id = `term-${this.props.tab.id}`
|
|
93
101
|
refs.add(this.id, this)
|
|
@@ -141,10 +149,8 @@ class Term extends Component {
|
|
|
141
149
|
prevProps.themeConfig
|
|
142
150
|
)
|
|
143
151
|
if (themeChanged && this.term) {
|
|
144
|
-
this.term.options.theme =
|
|
145
|
-
|
|
146
|
-
background: 'rgba(0,0,0,0)'
|
|
147
|
-
}
|
|
152
|
+
this.term.options.theme = this.getRendererThemeConfig(this.props.themeConfig)
|
|
153
|
+
this.registerTerminalColorQueryHandlers(this.term, this.props.themeConfig)
|
|
148
154
|
}
|
|
149
155
|
}
|
|
150
156
|
|
|
@@ -156,6 +162,7 @@ class Term extends Component {
|
|
|
156
162
|
if (this.term) {
|
|
157
163
|
this.term.parent = null
|
|
158
164
|
}
|
|
165
|
+
this.disposeTerminalColorQueryHandlers()
|
|
159
166
|
Object.keys(this.timers).forEach(k => {
|
|
160
167
|
clearTimeout(this.timers[k])
|
|
161
168
|
this.timers[k] = null
|
|
@@ -228,6 +235,10 @@ class Term extends Component {
|
|
|
228
235
|
if (['fontFamily', 'fontSize'].includes(name)) {
|
|
229
236
|
this.onResize()
|
|
230
237
|
}
|
|
238
|
+
if (name === 'fontSize') {
|
|
239
|
+
this.originalFontSize = curr
|
|
240
|
+
this.setState({ fontSizeChanged: false })
|
|
241
|
+
}
|
|
231
242
|
}
|
|
232
243
|
}
|
|
233
244
|
|
|
@@ -289,6 +300,23 @@ class Term extends Component {
|
|
|
289
300
|
}
|
|
290
301
|
term.options.fontSize = term.options.fontSize + v
|
|
291
302
|
window.store.triggerResize()
|
|
303
|
+
if (this.originalFontSize == null) {
|
|
304
|
+
this.originalFontSize = term.options.fontSize - v
|
|
305
|
+
}
|
|
306
|
+
this.setState({
|
|
307
|
+
fontSizeChanged: term.options.fontSize !== this.originalFontSize
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
handleResetFontSize = () => {
|
|
312
|
+
const { term } = this
|
|
313
|
+
if (!term || this.originalFontSize == null) {
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
term.options.fontSize = this.originalFontSize
|
|
317
|
+
window.store.triggerResize()
|
|
318
|
+
this.setState({ fontSizeChanged: false })
|
|
319
|
+
term.focus()
|
|
292
320
|
}
|
|
293
321
|
|
|
294
322
|
isActiveTerminal = () => {
|
|
@@ -451,13 +479,19 @@ class Term extends Component {
|
|
|
451
479
|
return
|
|
452
480
|
}
|
|
453
481
|
|
|
482
|
+
const hasMultiFiles = filePaths.length > 1
|
|
483
|
+
|
|
454
484
|
if (isSshTerminal) {
|
|
455
485
|
const behavior = this.props.config.dragDropBehavior || 'ask'
|
|
456
|
-
|
|
486
|
+
// multi-file drops always paste paths, skip the action modal
|
|
487
|
+
if (behavior === 'ask' && !hasMultiFiles) {
|
|
457
488
|
this.setState({
|
|
458
489
|
dropFileModalVisible: true,
|
|
459
490
|
droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
|
|
460
491
|
})
|
|
492
|
+
} else if (hasMultiFiles) {
|
|
493
|
+
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
494
|
+
this.attachAddon._sendData(filesAll)
|
|
461
495
|
} else {
|
|
462
496
|
this.handleDropFileAction(behavior, filePaths.map(path => ({ path, isRemote: false })))
|
|
463
497
|
}
|
|
@@ -465,10 +499,16 @@ class Term extends Component {
|
|
|
465
499
|
}
|
|
466
500
|
|
|
467
501
|
if (isSerialTerminal) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
502
|
+
// multi-file drops always paste paths, skip the action modal
|
|
503
|
+
if (hasMultiFiles) {
|
|
504
|
+
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
505
|
+
this.attachAddon._sendData(filesAll)
|
|
506
|
+
} else {
|
|
507
|
+
this.setState({
|
|
508
|
+
dropFileModalVisible: true,
|
|
509
|
+
droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
|
|
510
|
+
})
|
|
511
|
+
}
|
|
472
512
|
return
|
|
473
513
|
}
|
|
474
514
|
|
|
@@ -606,7 +646,7 @@ class Term extends Component {
|
|
|
606
646
|
this.term.clear()
|
|
607
647
|
this.term.focus()
|
|
608
648
|
if (shouldClear) {
|
|
609
|
-
this.searchAddon.
|
|
649
|
+
this.searchAddon._lineCache.clear()
|
|
610
650
|
this.timers.clearSearchTimer = setTimeout(() => {
|
|
611
651
|
refsStatic.get('term-search')?.next()
|
|
612
652
|
}, 100)
|
|
@@ -658,8 +698,8 @@ class Term extends Component {
|
|
|
658
698
|
}
|
|
659
699
|
|
|
660
700
|
updateSearchResults = (resultIndex) => {
|
|
661
|
-
const matches = this.searchAddon.
|
|
662
|
-
return
|
|
701
|
+
const matches = this.searchAddon._resultTracker.searchResults.map((result, i) => {
|
|
702
|
+
return result.row
|
|
663
703
|
})
|
|
664
704
|
|
|
665
705
|
this.setState({
|
|
@@ -1066,26 +1106,70 @@ class Term extends Component {
|
|
|
1066
1106
|
}
|
|
1067
1107
|
|
|
1068
1108
|
loadRenderer = async (term, config) => {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1109
|
+
// xterm 6.x: only the built-in DOM renderer and the WebGL addon exist
|
|
1110
|
+
// (the canvas renderer addon was removed in 6.x). 'dom' = no addon loaded
|
|
1111
|
+
// (built-in DOM renderer). Legacy 'canvas' settings fall back to DOM.
|
|
1112
|
+
if (config.rendererType === rendererTypes.webGL) {
|
|
1073
1113
|
try {
|
|
1074
1114
|
const WebglAddon = await loadWebglAddon()
|
|
1075
1115
|
term.loadAddon(new WebglAddon())
|
|
1076
1116
|
} catch (e) {
|
|
1077
|
-
console.error('render with webgl failed, fallback to
|
|
1117
|
+
console.error('render with webgl failed, fallback to dom renderer')
|
|
1078
1118
|
console.error(e)
|
|
1079
|
-
|
|
1080
|
-
term.loadAddon(new CanvasAddon())
|
|
1119
|
+
// built-in DOM renderer is used (no addon loaded)
|
|
1081
1120
|
}
|
|
1082
1121
|
}
|
|
1083
1122
|
}
|
|
1084
1123
|
|
|
1124
|
+
terminalColorQueryDisposables = []
|
|
1125
|
+
|
|
1126
|
+
disposeTerminalColorQueryHandlers = () => {
|
|
1127
|
+
this.terminalColorQueryDisposables.forEach(disposable => disposable?.dispose?.())
|
|
1128
|
+
this.terminalColorQueryDisposables.length = 0
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
getVisibleTerminalBackground = () => {
|
|
1132
|
+
const uiThemeConfig = window.store?.getUiThemeConfig?.() || {}
|
|
1133
|
+
const dom = this.domRef.current
|
|
1134
|
+
const cssMain = dom && window.getComputedStyle
|
|
1135
|
+
? window.getComputedStyle(dom).getPropertyValue('--main').trim()
|
|
1136
|
+
: ''
|
|
1137
|
+
return uiThemeConfig.main || cssMain || this.props.themeConfig.background
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
getVisibleTerminalForeground = () => {
|
|
1141
|
+
const uiThemeConfig = window.store?.getUiThemeConfig?.() || {}
|
|
1142
|
+
return uiThemeConfig.text
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
registerTerminalColorQueryHandlers = (term, themeConfig = {}) => {
|
|
1146
|
+
this.disposeTerminalColorQueryHandlers()
|
|
1147
|
+
if (!term?.parser?.registerOscHandler) {
|
|
1148
|
+
return
|
|
1149
|
+
}
|
|
1150
|
+
const background = this.getVisibleTerminalBackground()
|
|
1151
|
+
const foregroundFallback = this.getVisibleTerminalForeground()
|
|
1152
|
+
this.terminalColorQueryDisposables.push(
|
|
1153
|
+
term.parser.registerOscHandler(10, data => {
|
|
1154
|
+
return handleTerminalColorQuery(term, 10, themeConfig.foreground, foregroundFallback, data)
|
|
1155
|
+
}),
|
|
1156
|
+
term.parser.registerOscHandler(11, data => {
|
|
1157
|
+
return handleTerminalColorQuery(term, 11, background, themeConfig.background, data)
|
|
1158
|
+
})
|
|
1159
|
+
)
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
getRendererThemeConfig = (themeConfig = this.props.themeConfig) => {
|
|
1163
|
+
return createRendererThemeConfig(
|
|
1164
|
+
deepCopy(themeConfig),
|
|
1165
|
+
this.props.config.rendererType,
|
|
1166
|
+
this.getVisibleTerminalBackground()
|
|
1167
|
+
)
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1085
1170
|
initTerminal = async () => {
|
|
1086
1171
|
const { themeConfig, tab = {}, config = {} } = this.props
|
|
1087
|
-
const tc =
|
|
1088
|
-
tc.background = 'rgba(0,0,0,0)'
|
|
1172
|
+
const tc = this.getRendererThemeConfig(themeConfig)
|
|
1089
1173
|
const Terminal = await loadTerminal()
|
|
1090
1174
|
const term = new Terminal({
|
|
1091
1175
|
allowProposedApi: true,
|
|
@@ -1104,6 +1188,7 @@ class Term extends Component {
|
|
|
1104
1188
|
term.parent = this
|
|
1105
1189
|
term.onSelectionChange(this.onSelection)
|
|
1106
1190
|
term.open(this.domRef.current, true)
|
|
1191
|
+
this.registerTerminalColorQueryHandlers(term, themeConfig)
|
|
1107
1192
|
await this.loadRenderer(term, config)
|
|
1108
1193
|
|
|
1109
1194
|
const FitAddon = await loadFitAddon()
|
|
@@ -1324,7 +1409,7 @@ class Term extends Component {
|
|
|
1324
1409
|
}
|
|
1325
1410
|
|
|
1326
1411
|
openNormalBuffer = () => {
|
|
1327
|
-
const normal = this.term.buffer.
|
|
1412
|
+
const normal = this.term.buffer.normal
|
|
1328
1413
|
const len = normal.length
|
|
1329
1414
|
const lines = new Array(len).fill('').map((x, i) => {
|
|
1330
1415
|
return normal.getLine(i).translateToString(false)
|
|
@@ -1486,13 +1571,12 @@ class Term extends Component {
|
|
|
1486
1571
|
socket.onopen = async () => {
|
|
1487
1572
|
await this.initAttachAddon()
|
|
1488
1573
|
this.runInitScript()
|
|
1489
|
-
term._initialized = true
|
|
1490
1574
|
}
|
|
1491
1575
|
// term.onRrefresh(this.onRefresh)
|
|
1492
1576
|
term.onResize(this.onResizeTerminal)
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1577
|
+
// xterm 6.x exposes buffer change as a public event (IBufferNamespace.onBufferChange).
|
|
1578
|
+
// Previously this reached into the private _onBufferChange._listeners array.
|
|
1579
|
+
term.buffer.onBufferChange(this.onBufferChange)
|
|
1496
1580
|
const WebLinksAddon = await loadWebLinksAddon()
|
|
1497
1581
|
term.loadAddon(new WebLinksAddon(this.webLinkHandler))
|
|
1498
1582
|
term.focus()
|
|
@@ -1662,6 +1746,23 @@ class Term extends Component {
|
|
|
1662
1746
|
Object.assign(window.store.terminalInfoProps, infoProps)
|
|
1663
1747
|
}
|
|
1664
1748
|
|
|
1749
|
+
renderResetFontSizeButton = () => {
|
|
1750
|
+
if (!this.state.fontSizeChanged) {
|
|
1751
|
+
return null
|
|
1752
|
+
}
|
|
1753
|
+
const txt = `${e('reset')} ${e('fontSize')}`
|
|
1754
|
+
return (
|
|
1755
|
+
<Button
|
|
1756
|
+
className='terminal-fontsize-reset'
|
|
1757
|
+
onClick={this.handleResetFontSize}
|
|
1758
|
+
type='default'
|
|
1759
|
+
size='small'
|
|
1760
|
+
title={txt}
|
|
1761
|
+
icon={<AimOutlined />}
|
|
1762
|
+
/>
|
|
1763
|
+
)
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1665
1766
|
// getPwd = async () => {
|
|
1666
1767
|
// const { sessionId, config } = this.props
|
|
1667
1768
|
// const { pid } = this.state
|
|
@@ -1760,6 +1861,7 @@ class Term extends Component {
|
|
|
1760
1861
|
<ReconnectOverlay
|
|
1761
1862
|
countdown={this.state.reconnectCountdown}
|
|
1762
1863
|
/>
|
|
1864
|
+
{this.renderResetFontSizeButton()}
|
|
1763
1865
|
<DropFileModal
|
|
1764
1866
|
visible={this.state.dropFileModalVisible}
|
|
1765
1867
|
files={this.state.droppedFiles}
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
margin-left 5px
|
|
33
33
|
|
|
34
34
|
#container
|
|
35
|
+
.xterm
|
|
36
|
+
background transparent
|
|
37
|
+
.xterm-viewport
|
|
38
|
+
background-color transparent !important
|
|
35
39
|
.xterm-screen::before
|
|
36
40
|
content ''
|
|
37
41
|
position absolute
|
|
@@ -167,3 +171,8 @@
|
|
|
167
171
|
z-index 100
|
|
168
172
|
background var(--main)
|
|
169
173
|
max-width calc(100% - 24px)
|
|
174
|
+
|
|
175
|
+
.terminal-fontsize-reset
|
|
176
|
+
position absolute
|
|
177
|
+
left 8px
|
|
178
|
+
bottom 4px
|
|
@@ -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
|