@electerm/electerm-react 3.11.0 → 3.11.11
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/bookmark-schemas.js +1 -0
- package/client/common/constants.js +7 -0
- package/client/common/parse-quick-connect.js +13 -10
- package/client/common/sanitize-filename.js +66 -0
- package/client/components/bookmark-form/bookmark-schema.js +1 -0
- package/client/components/bookmark-form/config/serial.js +2 -1
- package/client/components/common/font-select.jsx +45 -0
- package/client/components/session/session.jsx +2 -2
- package/client/components/setting-panel/setting-terminal.jsx +6 -28
- package/client/components/setting-panel/text-bg-modal.jsx +8 -27
- package/client/components/setting-sync/setting-sync-form.jsx +1 -1
- package/client/components/sftp/file-item.jsx +5 -4
- package/client/components/shortcuts/shortcut-handler.js +9 -9
- package/client/components/terminal/terminal.jsx +11 -0
- package/client/store/mcp-handler.js +22 -2
- package/client/store/watch.js +38 -36
- package/package.json +1 -1
- package/client/common/safe-name.js +0 -19
|
@@ -96,6 +96,7 @@ export const serialBookmarkSchema = {
|
|
|
96
96
|
xon: z.boolean().optional().describe('XON flow control'),
|
|
97
97
|
xoff: z.boolean().optional().describe('XOFF flow control'),
|
|
98
98
|
xany: z.boolean().optional().describe('XANY flow control'),
|
|
99
|
+
lineEnding: z.enum(['', '\r', '\n', '\r\n']).optional().describe('Line ending for Enter key: "" (none), "\\r" (CR), "\\n" (LF), "\\r\\n" (CR+LF)'),
|
|
99
100
|
description: z.string().optional().describe('Bookmark description')
|
|
100
101
|
}
|
|
101
102
|
|
|
@@ -180,6 +180,13 @@ export const commonParities = [
|
|
|
180
180
|
'none', 'even', 'mark', 'odd', 'space'
|
|
181
181
|
]
|
|
182
182
|
|
|
183
|
+
export const commonLineEndings = [
|
|
184
|
+
{ value: '', label: 'none' },
|
|
185
|
+
{ value: '\r', label: 'CR' },
|
|
186
|
+
{ value: '\n', label: 'LF' },
|
|
187
|
+
{ value: '\r\n', label: 'CR+LF' }
|
|
188
|
+
]
|
|
189
|
+
|
|
183
190
|
export const maxBatchInput = 30
|
|
184
191
|
export const windowControlWidth = 94
|
|
185
192
|
export const baseUpdateCheckUrls = [
|
|
@@ -115,8 +115,11 @@ function parseQuickConnect (str) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
try {
|
|
118
|
+
// Strip trailing slashes (supports pasted URLs like host/ or ssh://host/)
|
|
119
|
+
const input = trimmed.replace(/\/+$/, '')
|
|
120
|
+
|
|
118
121
|
// Detect protocol
|
|
119
|
-
const protocolMatch =
|
|
122
|
+
const protocolMatch = input.match(/^(ssh|telnet|vnc|rdp|spice|serial|ftp|https?|electerm):\/\//i)
|
|
120
123
|
|
|
121
124
|
let protocol = ''
|
|
122
125
|
let connectionString = ''
|
|
@@ -129,28 +132,28 @@ function parseQuickConnect (str) {
|
|
|
129
132
|
if (protocol === 'http' || protocol === 'https') {
|
|
130
133
|
protocol = 'web'
|
|
131
134
|
}
|
|
132
|
-
connectionString =
|
|
135
|
+
connectionString = input.slice(protocolMatch[0].length)
|
|
133
136
|
} else {
|
|
134
137
|
// Shortcut format - default to SSH
|
|
135
138
|
// Match user@host or user@host:port or just host or host:port
|
|
136
139
|
// Use last colon to determine port for host:port format
|
|
137
|
-
if (/^[\w.-]+@[\w.-]+/.test(
|
|
140
|
+
if (/^[\w.-]+@[\w.-]+/.test(input)) {
|
|
138
141
|
// user@host or user@host:port
|
|
139
142
|
protocol = 'ssh'
|
|
140
|
-
connectionString =
|
|
141
|
-
} else if (/^[\w.-]+:.*:[\d]+$/.test(
|
|
143
|
+
connectionString = input
|
|
144
|
+
} else if (/^[\w.-]+:.*:[\d]+$/.test(input)) {
|
|
142
145
|
// host:port format with colons in hostname (e.g., localhost:23344, zxd:localhost:23344)
|
|
143
146
|
// Check if the last colon is followed by digits (port number)
|
|
144
147
|
protocol = 'ssh'
|
|
145
|
-
connectionString =
|
|
146
|
-
} else if (/^[\w.-]+:[\d]+$/.test(
|
|
148
|
+
connectionString = input
|
|
149
|
+
} else if (/^[\w.-]+:[\d]+$/.test(input)) {
|
|
147
150
|
// host:port (no username, simple format like host:22)
|
|
148
151
|
protocol = 'ssh'
|
|
149
|
-
connectionString =
|
|
150
|
-
} else if (/^[\w.-]+$/.test(
|
|
152
|
+
connectionString = input
|
|
153
|
+
} else if (/^[\w.-]+$/.test(input)) {
|
|
151
154
|
// just host
|
|
152
155
|
protocol = 'ssh'
|
|
153
|
-
connectionString =
|
|
156
|
+
connectionString = input
|
|
154
157
|
} else {
|
|
155
158
|
return null
|
|
156
159
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize a filename for cross-platform file transfers.
|
|
3
|
+
*
|
|
4
|
+
* When transferring files between different OS (Linux <-> Windows <-> macOS),
|
|
5
|
+
* filenames may contain characters that are illegal on the destination OS.
|
|
6
|
+
* Windows is the most restrictive common platform, so we use its rules as
|
|
7
|
+
* the baseline for maximum compatibility.
|
|
8
|
+
*
|
|
9
|
+
* Rules applied:
|
|
10
|
+
* - Remove control characters (0x00-0x1F)
|
|
11
|
+
* - Replace reserved characters: < > : " / \ | ? * with _
|
|
12
|
+
* - Remove leading/trailing dots and spaces (Windows restriction)
|
|
13
|
+
* - Reject reserved Windows device names: CON, PRN, AUX, NUL, COM1-9, LPT1-9
|
|
14
|
+
* - Limit filename length to 255 bytes (common filesystem limit)
|
|
15
|
+
* - Fallback to 'unnamed' if result is empty
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Characters illegal on Windows (and problematic on many systems)
|
|
19
|
+
// eslint-disable-next-line no-control-regex
|
|
20
|
+
const ILLEGAL_CHARS = /[<>:"/\\|?\x00-\x1f]/g
|
|
21
|
+
|
|
22
|
+
// Leading/trailing dots and spaces are problematic on Windows
|
|
23
|
+
const LEADING_TRAILING = /^[.\s]+|[.\s]+$/g
|
|
24
|
+
|
|
25
|
+
// Reserved Windows device names (case-insensitive)
|
|
26
|
+
const RESERVED_NAMES = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i
|
|
27
|
+
|
|
28
|
+
const MAX_FILENAME_LENGTH = 255
|
|
29
|
+
|
|
30
|
+
const REPLACEMENT_CHAR = '_'
|
|
31
|
+
|
|
32
|
+
export default function sanitizeFilename (name) {
|
|
33
|
+
if (!name || typeof name !== 'string') {
|
|
34
|
+
return 'unnamed'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let safe = name
|
|
38
|
+
// Replace illegal characters
|
|
39
|
+
.replace(ILLEGAL_CHARS, REPLACEMENT_CHAR)
|
|
40
|
+
// Strip leading/trailing dots and spaces
|
|
41
|
+
.replace(LEADING_TRAILING, '')
|
|
42
|
+
|
|
43
|
+
// Handle reserved Windows device names by appending underscore
|
|
44
|
+
if (RESERVED_NAMES.test(safe)) {
|
|
45
|
+
safe = safe + REPLACEMENT_CHAR
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Truncate to max length
|
|
49
|
+
if (safe.length > MAX_FILENAME_LENGTH) {
|
|
50
|
+
const ext = safe.lastIndexOf('.')
|
|
51
|
+
if (ext > 0) {
|
|
52
|
+
// Preserve extension when truncating
|
|
53
|
+
const extension = safe.slice(ext)
|
|
54
|
+
safe = safe.slice(0, MAX_FILENAME_LENGTH - extension.length) + extension
|
|
55
|
+
} else {
|
|
56
|
+
safe = safe.slice(0, MAX_FILENAME_LENGTH)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fallback for empty result
|
|
61
|
+
if (!safe) {
|
|
62
|
+
return 'unnamed'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return safe
|
|
66
|
+
}
|
|
@@ -81,6 +81,7 @@ const bookmarkSchema = {
|
|
|
81
81
|
xon: 'boolean - enable XON flow control, default is false',
|
|
82
82
|
xoff: 'boolean - enable XOFF flow control, default is false',
|
|
83
83
|
xany: 'boolean - enable XANY flow control, default is false',
|
|
84
|
+
lineEnding: 'string - line ending for Enter key: "" (none), "\\r" (CR), "\\n" (LF), "\\r\\n" (CR+LF)',
|
|
84
85
|
runScripts: 'array - run scripts after connected ({delay,script})',
|
|
85
86
|
description: 'string - bookmark description'
|
|
86
87
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formItemLayout } from '../../../common/form-layout.js'
|
|
2
|
-
import { terminalSerialType, commonBaudRates, commonDataBits, commonStopBits, commonParities } from '../../../common/constants.js'
|
|
2
|
+
import { terminalSerialType, commonBaudRates, commonDataBits, commonStopBits, commonParities, commonLineEndings } from '../../../common/constants.js'
|
|
3
3
|
import defaultSettings from '../../../common/default-setting.js'
|
|
4
4
|
import { createBaseInitValues, getTerminalBackgroundDefaults } from '../common/init-values.js'
|
|
5
5
|
import { commonFields } from './common-fields.js'
|
|
@@ -57,6 +57,7 @@ const serialConfig = {
|
|
|
57
57
|
{ type: 'switch', name: 'xon', label: 'xon', valuePropName: 'checked' },
|
|
58
58
|
{ type: 'switch', name: 'xoff', label: 'xoff', valuePropName: 'checked' },
|
|
59
59
|
{ type: 'switch', name: 'xany', label: 'xany', valuePropName: 'checked' },
|
|
60
|
+
{ type: 'select', name: 'lineEnding', label: 'lineEnding', options: commonLineEndings.map(d => ({ value: d.value, label: d.label })) },
|
|
60
61
|
commonFields.runScripts,
|
|
61
62
|
commonFields.description,
|
|
62
63
|
{ type: 'input', name: 'type', label: 'type', hidden: true }
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { useMemo, useCallback } from 'react'
|
|
2
|
+
import { Select } from 'antd'
|
|
3
|
+
|
|
4
|
+
const e = window.translate
|
|
5
|
+
|
|
6
|
+
export default function FontSelect ({
|
|
7
|
+
value,
|
|
8
|
+
onChange,
|
|
9
|
+
placeholder,
|
|
10
|
+
style
|
|
11
|
+
}) {
|
|
12
|
+
const { fonts = [] } = window.et || {}
|
|
13
|
+
|
|
14
|
+
const options = useMemo(() => {
|
|
15
|
+
return fonts.map(f => ({
|
|
16
|
+
value: f,
|
|
17
|
+
label: (
|
|
18
|
+
<span
|
|
19
|
+
className='iblock'
|
|
20
|
+
style={{ fontFamily: f }}
|
|
21
|
+
>
|
|
22
|
+
{f}
|
|
23
|
+
</span>
|
|
24
|
+
)
|
|
25
|
+
}))
|
|
26
|
+
}, [fonts])
|
|
27
|
+
|
|
28
|
+
const handleChange = useCallback((vals) => {
|
|
29
|
+
onChange(vals)
|
|
30
|
+
}, [onChange])
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Select
|
|
34
|
+
mode='tags'
|
|
35
|
+
value={value}
|
|
36
|
+
onChange={handleChange}
|
|
37
|
+
className='width-100'
|
|
38
|
+
placeholder={placeholder || e('selectFontFamily')}
|
|
39
|
+
showSearch
|
|
40
|
+
options={options}
|
|
41
|
+
filterOption={(input, option) =>
|
|
42
|
+
(option?.value ?? '').toLowerCase().includes(input.toLowerCase())}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
} from '../../common/constants'
|
|
36
36
|
import { SplitViewIcon } from '../icons/split-view'
|
|
37
37
|
import { refs } from '../common/ref'
|
|
38
|
-
import
|
|
38
|
+
import sanitizeFilename from '../../common/sanitize-filename.js'
|
|
39
39
|
import { HeartbeatIcon } from '../icons/heartbeat'
|
|
40
40
|
import './session.styl'
|
|
41
41
|
|
|
@@ -343,7 +343,7 @@ export default class SessionWrapper extends Component {
|
|
|
343
343
|
height
|
|
344
344
|
} = this.calcTermWidthHeight()
|
|
345
345
|
const themeConfig = copy(window.store.getThemeConfig())
|
|
346
|
-
const logName =
|
|
346
|
+
const logName = sanitizeFilename(`${tab.title ? tab.title + '_' : ''}${tab.host ? tab.host + '_' : ''}${tab.id}`)
|
|
347
347
|
const pops = {
|
|
348
348
|
...this.props,
|
|
349
349
|
sftpPathFollowSsh,
|
|
@@ -28,6 +28,7 @@ import { chooseSaveDirectory } from '../../common/choose-save-folder'
|
|
|
28
28
|
import mapper from '../../common/auto-complete-data-mapper'
|
|
29
29
|
import KeywordForm from './keywords-form'
|
|
30
30
|
import Link from '../common/external-link'
|
|
31
|
+
import FontSelect from '../common/font-select'
|
|
31
32
|
import HelpIcon from '../common/help-icon'
|
|
32
33
|
import KeywordsTransport from './keywords-transport'
|
|
33
34
|
import fs from '../../common/fs'
|
|
@@ -427,36 +428,13 @@ export default class SettingTerminal extends Component {
|
|
|
427
428
|
}
|
|
428
429
|
|
|
429
430
|
renderFontFamily = () => {
|
|
430
|
-
const { fonts = [] } = window.et
|
|
431
431
|
const { fontFamily } = this.props.config
|
|
432
|
-
const props = {
|
|
433
|
-
mode: 'multiple',
|
|
434
|
-
onChange: this.handleChangeFont,
|
|
435
|
-
value: fontFamily.split(/, */g).filter(d => d.trim()),
|
|
436
|
-
style: { width: '100%' }
|
|
437
|
-
}
|
|
438
432
|
return (
|
|
439
|
-
<
|
|
440
|
-
{
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
fonts.map(f => {
|
|
445
|
-
return (
|
|
446
|
-
<Option value={f} key={f}>
|
|
447
|
-
<span
|
|
448
|
-
className='font-option'
|
|
449
|
-
style={{
|
|
450
|
-
fontFamily: f
|
|
451
|
-
}}
|
|
452
|
-
>
|
|
453
|
-
{f}
|
|
454
|
-
</span>
|
|
455
|
-
</Option>
|
|
456
|
-
)
|
|
457
|
-
})
|
|
458
|
-
}
|
|
459
|
-
</Select>
|
|
433
|
+
<FontSelect
|
|
434
|
+
onChange={this.handleChangeFont}
|
|
435
|
+
value={fontFamily.split(/, */g).filter(d => d.trim())}
|
|
436
|
+
style={{ width: '100%' }}
|
|
437
|
+
/>
|
|
460
438
|
)
|
|
461
439
|
}
|
|
462
440
|
|
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
Input,
|
|
4
4
|
InputNumber,
|
|
5
5
|
Space,
|
|
6
|
-
Select,
|
|
7
6
|
Button,
|
|
8
7
|
Modal
|
|
9
8
|
} from 'antd'
|
|
10
9
|
import { ColorPicker } from '../bookmark-form/common/color-picker.jsx'
|
|
10
|
+
import FontSelect from '../common/font-select'
|
|
11
11
|
|
|
12
12
|
const { TextArea } = Input
|
|
13
13
|
const e = window.translate
|
|
@@ -24,16 +24,16 @@ export default function TextBgModal ({
|
|
|
24
24
|
const [text, setText] = useState(initialText)
|
|
25
25
|
const [fontSize, setFontSize] = useState(initialSize)
|
|
26
26
|
const [color, setColor] = useState(initialColor)
|
|
27
|
-
const [fontFamily, setFontFamily] = useState(
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const [fontFamily, setFontFamily] = useState(
|
|
28
|
+
initialFontFamily.split(/, */g).filter(d => d.trim())
|
|
29
|
+
)
|
|
30
30
|
|
|
31
31
|
const handleOk = () => {
|
|
32
32
|
onOk({
|
|
33
33
|
text,
|
|
34
34
|
fontSize,
|
|
35
35
|
color,
|
|
36
|
-
fontFamily
|
|
36
|
+
fontFamily: fontFamily.join(', ')
|
|
37
37
|
})
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -43,7 +43,7 @@ export default function TextBgModal ({
|
|
|
43
43
|
setText(initialText)
|
|
44
44
|
setFontSize(initialSize)
|
|
45
45
|
setColor(initialColor)
|
|
46
|
-
setFontFamily(initialFontFamily)
|
|
46
|
+
setFontFamily(initialFontFamily.split(/, */g).filter(d => d.trim()))
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const footer = (
|
|
@@ -108,30 +108,11 @@ export default function TextBgModal ({
|
|
|
108
108
|
|
|
109
109
|
<div>
|
|
110
110
|
<b>{e('fontFamily')}</b>
|
|
111
|
-
<
|
|
111
|
+
<FontSelect
|
|
112
112
|
value={fontFamily}
|
|
113
113
|
onChange={setFontFamily}
|
|
114
|
-
style={{ width: '100%' }}
|
|
115
114
|
placeholder={e('selectFontFamily')}
|
|
116
|
-
|
|
117
|
-
>
|
|
118
|
-
{
|
|
119
|
-
fonts.map(f => {
|
|
120
|
-
return (
|
|
121
|
-
<Select.Option value={f} key={f}>
|
|
122
|
-
<span
|
|
123
|
-
className='font-option'
|
|
124
|
-
style={{
|
|
125
|
-
fontFamily: f
|
|
126
|
-
}}
|
|
127
|
-
>
|
|
128
|
-
{f}
|
|
129
|
-
</span>
|
|
130
|
-
</Select.Option>
|
|
131
|
-
)
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
</Select>
|
|
115
|
+
/>
|
|
135
116
|
</div>
|
|
136
117
|
</Space>
|
|
137
118
|
</div>
|
|
@@ -36,6 +36,7 @@ import time from '../../common/time'
|
|
|
36
36
|
import { filesize } from 'filesize'
|
|
37
37
|
import { createTransferProps } from './transfer-common'
|
|
38
38
|
import generate from '../../common/uid'
|
|
39
|
+
import sanitizeFilename from '../../common/sanitize-filename'
|
|
39
40
|
import { refsStatic, refs, filesRef } from '../common/ref'
|
|
40
41
|
import iconsMap from '../sys-menu/icons-map'
|
|
41
42
|
|
|
@@ -174,7 +175,7 @@ export default class FileSection extends React.Component {
|
|
|
174
175
|
? item.replace(/^remote:/, '')
|
|
175
176
|
: item
|
|
176
177
|
const { name } = getFolderFromFilePath(fromPath, isRemote)
|
|
177
|
-
const toPath = resolve(path, name)
|
|
178
|
+
const toPath = resolve(path, sanitizeFilename(name))
|
|
178
179
|
res.push({
|
|
179
180
|
typeFrom: isRemote ? typeMap.remote : typeMap.local,
|
|
180
181
|
typeTo: type,
|
|
@@ -326,7 +327,7 @@ export default class FileSection extends React.Component {
|
|
|
326
327
|
toFile = {
|
|
327
328
|
...toFile,
|
|
328
329
|
...getFolderFromFilePath(
|
|
329
|
-
resolve(toFile.path, toFile.name)
|
|
330
|
+
resolve(toFile.path, sanitizeFilename(toFile.name))
|
|
330
331
|
),
|
|
331
332
|
id: undefined
|
|
332
333
|
}
|
|
@@ -367,7 +368,7 @@ export default class FileSection extends React.Component {
|
|
|
367
368
|
return this.doTransferSelected(
|
|
368
369
|
null,
|
|
369
370
|
files,
|
|
370
|
-
resolve(toFile.path, toFile.name),
|
|
371
|
+
resolve(toFile.path, sanitizeFilename(toFile.name)),
|
|
371
372
|
toFile.type,
|
|
372
373
|
operation
|
|
373
374
|
)
|
|
@@ -784,7 +785,7 @@ export default class FileSection extends React.Component {
|
|
|
784
785
|
if (toPathBase) {
|
|
785
786
|
toPath = toPathBase
|
|
786
787
|
}
|
|
787
|
-
toPath = resolve(toPath, name)
|
|
788
|
+
toPath = resolve(toPath, sanitizeFilename(name))
|
|
788
789
|
const obj = {
|
|
789
790
|
host: this.props.tab?.host,
|
|
790
791
|
tabType: this.props.tab?.type,
|
|
@@ -54,6 +54,9 @@ export function handleTerminalSelectionReplace (event, ctx) {
|
|
|
54
54
|
const isPrintable = key && key.length === 1
|
|
55
55
|
if (!isBackspace && !isDelete && !isPrintable) return false
|
|
56
56
|
|
|
57
|
+
const info = getSelectionReplaceInfo(ctx.term)
|
|
58
|
+
if (!info) return false
|
|
59
|
+
|
|
57
60
|
if (event && event.preventDefault) {
|
|
58
61
|
event.preventDefault()
|
|
59
62
|
}
|
|
@@ -61,9 +64,6 @@ export function handleTerminalSelectionReplace (event, ctx) {
|
|
|
61
64
|
event.stopPropagation()
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
const info = getSelectionReplaceInfo(ctx.term)
|
|
65
|
-
if (!info) return false
|
|
66
|
-
|
|
67
67
|
const { startX, endX, cursorX } = info
|
|
68
68
|
const move = startX - cursorX
|
|
69
69
|
if (move > 0) {
|
|
@@ -146,6 +146,12 @@ export function shortcutExtend (Cls) {
|
|
|
146
146
|
if (isInAntdInput()) {
|
|
147
147
|
return
|
|
148
148
|
}
|
|
149
|
+
// During IME composition, let xterm handle the event through its
|
|
150
|
+
// CompositionHelper. Intercepting here breaks composition (e.g. first
|
|
151
|
+
// char lost, closing bracket duplicated when typing Chinese inside brackets).
|
|
152
|
+
if (event.isComposing) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
149
155
|
if (handleTerminalSelectionReplace(event, this)) {
|
|
150
156
|
return false
|
|
151
157
|
}
|
|
@@ -156,12 +162,6 @@ export function shortcutExtend (Cls) {
|
|
|
156
162
|
!altKey &&
|
|
157
163
|
!ctrlKey
|
|
158
164
|
) {
|
|
159
|
-
// If IME is composing, let the browser delete the composition char only
|
|
160
|
-
// Returning false tells xterm not to process the event (and not to call
|
|
161
|
-
// preventDefault), so the native textarea backspace still works for IME.
|
|
162
|
-
if (event.isComposing) {
|
|
163
|
-
return false
|
|
164
|
-
}
|
|
165
165
|
this.props.onDelKeyPressed()
|
|
166
166
|
const delKey = this.props.config.backspaceMode === '^?' ? 8 : 127
|
|
167
167
|
const altDelDelKey = delKey === 8 ? 127 : 8
|
|
@@ -1263,6 +1263,17 @@ class Term extends Component {
|
|
|
1263
1263
|
this.handleError({ message: text, from, srcId })
|
|
1264
1264
|
}
|
|
1265
1265
|
})
|
|
1266
|
+
// Guard: component was unmounted while createTerm was pending.
|
|
1267
|
+
// The child process is already running; connect briefly to trigger its cleanup.
|
|
1268
|
+
if (this.onClose) {
|
|
1269
|
+
if (r && r.port) {
|
|
1270
|
+
try {
|
|
1271
|
+
const tmpSock = new WebSocket(this.buildWsUrl(r.port))
|
|
1272
|
+
tmpSock.onopen = () => tmpSock.close()
|
|
1273
|
+
} catch (_e) {}
|
|
1274
|
+
}
|
|
1275
|
+
return
|
|
1276
|
+
}
|
|
1266
1277
|
if (typeof r === 'string' && r.includes('fail')) {
|
|
1267
1278
|
return this.promote()
|
|
1268
1279
|
}
|
|
@@ -172,8 +172,28 @@ export default Store => {
|
|
|
172
172
|
|
|
173
173
|
// ==================== Bookmark APIs ====================
|
|
174
174
|
|
|
175
|
+
const bookmarkSensitiveFields = [
|
|
176
|
+
'password', 'privateKey', 'passphrase', 'certificate', 'proxy',
|
|
177
|
+
'connectionHoppings', 'sshTunnels'
|
|
178
|
+
]
|
|
179
|
+
const bookmarkFeatureFields = [
|
|
180
|
+
'connectionHoppings', 'sshTunnels', 'quickCommands', 'runScripts'
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
function sanitizeBookmark (b) {
|
|
184
|
+
const safe = Object.fromEntries(
|
|
185
|
+
Object.entries(b).filter(([k]) => !bookmarkSensitiveFields.includes(k))
|
|
186
|
+
)
|
|
187
|
+
for (const key of bookmarkFeatureFields) {
|
|
188
|
+
if (Array.isArray(b[key]) && b[key].length) {
|
|
189
|
+
safe[`has${key.charAt(0).toUpperCase() + key.slice(1)}`] = true
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return safe
|
|
193
|
+
}
|
|
194
|
+
|
|
175
195
|
Store.prototype.mcpListBookmarks = function () {
|
|
176
|
-
return deepCopy(window.store.bookmarks)
|
|
196
|
+
return deepCopy(window.store.bookmarks).map(sanitizeBookmark)
|
|
177
197
|
}
|
|
178
198
|
|
|
179
199
|
Store.prototype.mcpGetBookmark = function (args) {
|
|
@@ -182,7 +202,7 @@ export default Store => {
|
|
|
182
202
|
if (!bookmark) {
|
|
183
203
|
throw new Error(`Bookmark not found: ${args.id}`)
|
|
184
204
|
}
|
|
185
|
-
return deepCopy(bookmark)
|
|
205
|
+
return deepCopy(sanitizeBookmark(bookmark))
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
Store.prototype.mcpAddBookmark = async function (args) {
|
package/client/store/watch.js
CHANGED
|
@@ -21,49 +21,51 @@ import dataCompare from '../common/data-compare'
|
|
|
21
21
|
|
|
22
22
|
export default store => {
|
|
23
23
|
for (const name of dbNamesForWatch) {
|
|
24
|
+
window[`watch${name}Running`] = false
|
|
24
25
|
window[`watch${name}`] = autoRun(async () => {
|
|
25
|
-
if (window.migrating) {
|
|
26
|
+
if (window.migrating || window[`watch${name}Running`]) {
|
|
26
27
|
return
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await remove(name, item.id)
|
|
36
|
-
}
|
|
37
|
-
for (const item of updated) {
|
|
38
|
-
await update(item.id, item, name, false)
|
|
39
|
-
}
|
|
40
|
-
for (const item of added) {
|
|
41
|
-
await insert(name, item)
|
|
42
|
-
}
|
|
43
|
-
const newOrder = (n || []).map(d => d.id)
|
|
44
|
-
await update(
|
|
45
|
-
`${name}:order`,
|
|
46
|
-
newOrder
|
|
47
|
-
)
|
|
48
|
-
refsStatic.add('oldState-' + name, deepCopy(n) || [])
|
|
49
|
-
if (name === 'bookmarks') {
|
|
50
|
-
store.bookmarksMap = new Map(
|
|
51
|
-
n.map(d => [d.id, d])
|
|
29
|
+
window[`watch${name}Running`] = true
|
|
30
|
+
try {
|
|
31
|
+
const old = refsStatic.get('oldState-' + name)
|
|
32
|
+
const n = store.getItems(name)
|
|
33
|
+
const { updated, added, removed } = dataCompare(
|
|
34
|
+
old,
|
|
35
|
+
n
|
|
52
36
|
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
37
|
+
await Promise.all([
|
|
38
|
+
...removed.map(item => remove(name, item.id)),
|
|
39
|
+
...updated.map(item => update(item.id, item, name, false)),
|
|
40
|
+
added.length ? insert(name, added) : Promise.resolve()
|
|
41
|
+
])
|
|
42
|
+
const newOrder = (n || []).map(d => d.id)
|
|
43
|
+
await update(
|
|
44
|
+
`${name}:order`,
|
|
45
|
+
newOrder
|
|
46
|
+
)
|
|
47
|
+
refsStatic.add('oldState-' + name, deepCopy(n) || [])
|
|
48
|
+
if (name === 'bookmarks') {
|
|
49
|
+
store.bookmarksMap = new Map(
|
|
50
|
+
n.map(d => [d.id, d])
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
await store.updateLastDataUpdateTime()
|
|
54
|
+
if (dbNamesForSync.includes(name)) {
|
|
55
|
+
const syncSetting = store.config.syncSetting || {}
|
|
56
|
+
const { autoSync, autoSyncInterval, autoSyncDirection } = syncSetting
|
|
57
|
+
if (autoSync && autoSyncInterval === 0) {
|
|
58
|
+
if (autoSyncDirection === 'download') {
|
|
59
|
+
await store.downloadSettingAll()
|
|
60
|
+
} else {
|
|
61
|
+
await store.uploadSettingAll()
|
|
62
|
+
}
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
return store[name]
|
|
66
|
+
} finally {
|
|
67
|
+
window[`watch${name}Running`] = false
|
|
65
68
|
}
|
|
66
|
-
return store[name]
|
|
67
69
|
})
|
|
68
70
|
window[`watch${name}`].start()
|
|
69
71
|
}
|
package/package.json
CHANGED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* convert string to safe name
|
|
3
|
-
* from https://github.com/Jaliborc/safe-filename/blob/master/index.js
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export default (name) => {
|
|
7
|
-
return name
|
|
8
|
-
.replace(/\.$/, '')
|
|
9
|
-
.replace('?', '❓')
|
|
10
|
-
.replace('\\', ' ⃥')
|
|
11
|
-
.replace('/', '⟋')
|
|
12
|
-
.replace('|', '│')
|
|
13
|
-
.replace(':', '꞉')
|
|
14
|
-
.replace('<', 'ᐸ')
|
|
15
|
-
.replace('>', 'ᐳ')
|
|
16
|
-
.replace('>', 'ᐳ')
|
|
17
|
-
.replace('"', 'ᐦ')
|
|
18
|
-
.replace('*', '꘎')
|
|
19
|
-
}
|