@electerm/electerm-react 5.1.25 → 5.3.0
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/build-map.js +12 -0
- package/client/common/constants.js +12 -0
- package/client/common/db.js +7 -5
- package/client/common/import-task.js +174 -0
- package/client/common/term-of-use.jsx +69 -0
- package/client/components/ai/ai-config-props.js +8 -0
- package/client/components/bookmark-form/ai-bookmark-form.jsx +18 -15
- package/client/components/bookmark-form/bookmark-schema.js +1 -0
- package/client/components/bookmark-form/common/bookmark-select.jsx +6 -5
- package/client/components/bookmark-form/common/fields.jsx +1 -1
- package/client/components/bookmark-form/common/profile-item.jsx +12 -2
- package/client/components/bookmark-form/config/common-fields.js +11 -0
- package/client/components/bookmark-form/config/rdp.js +1 -1
- package/client/components/bookmark-form/tree-select.jsx +5 -16
- package/client/components/common/import-progress.jsx +90 -0
- package/client/components/footer/footer-entry.jsx +1 -1
- package/client/components/main/main.jsx +2 -0
- package/client/components/profile/profile-form-rdp.jsx +1 -1
- package/client/components/quick-commands/quick-command-transport-mod.jsx +24 -18
- package/client/components/rdp/rdp-cred-form.jsx +55 -0
- package/client/components/rdp/rdp-session.jsx +97 -6
- package/client/components/setting-panel/keywords-transport.jsx +1 -4
- package/client/components/setting-panel/setting-modal.jsx +8 -2
- package/client/components/setting-panel/start-session-select.jsx +5 -16
- package/client/components/setting-sync/setting-sync.jsx +15 -7
- package/client/components/sidebar/info-modal.jsx +7 -0
- package/client/components/spice/spice-session.jsx +40 -0
- package/client/components/terminal/attach-addon-custom.js +12 -10
- package/client/components/terminal/shell.js +22 -11
- package/client/components/terminal/shortcut-bar-entry.jsx +52 -1
- package/client/components/terminal/terminal-color-query.mjs +32 -27
- package/client/components/terminal/terminal-interactive.jsx +35 -0
- package/client/components/terminal/terminal.jsx +24 -15
- package/client/components/terminal-info/activity.jsx +5 -1
- package/client/components/terminal-info/base.jsx +37 -22
- package/client/components/terminal-info/disk.jsx +5 -1
- package/client/components/terminal-info/local-info-resolver.js +89 -0
- package/client/components/terminal-info/network.jsx +5 -1
- package/client/components/terminal-info/run-cmd.jsx +42 -3
- package/client/components/terminal-info/sys-info.jsx +56 -0
- package/client/components/terminal-info/terminal-info.jsx +4 -1
- package/client/components/terminal-info/terminal-info.styl +22 -7
- package/client/components/tree-list/bookmark-transport.jsx +2 -1
- package/client/components/tree-list/bookmark-upload.js +56 -41
- package/client/components/tree-list/tree-list-row.jsx +11 -4
- package/client/components/tree-list/tree-list-rows.js +6 -3
- package/client/components/tree-list/tree-list.jsx +4 -2
- package/client/store/common.js +38 -3
- package/client/store/load-data.js +6 -1
- package/client/store/setting.js +7 -4
- package/client/store/store.js +3 -12
- package/client/store/sync.js +31 -2
- package/client/store/tab.js +16 -11
- package/client/store/watch.js +10 -1
- package/package.json +1 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import BookmarkTransport from '../tree-list/bookmark-transport'
|
|
2
2
|
import download from '../../common/download'
|
|
3
3
|
import time from '../../common/time'
|
|
4
|
-
import
|
|
4
|
+
import { runImportTask } from '../../common/import-task'
|
|
5
|
+
|
|
6
|
+
const e = window.translate
|
|
5
7
|
|
|
6
8
|
export default class QmTransport extends BookmarkTransport {
|
|
7
9
|
name = 'quickCommands'
|
|
10
|
+
|
|
8
11
|
beforeUpload = async (file) => {
|
|
9
12
|
const { store } = this.props
|
|
10
13
|
const txt = file.fileContent !== undefined
|
|
@@ -13,28 +16,31 @@ export default class QmTransport extends BookmarkTransport {
|
|
|
13
16
|
try {
|
|
14
17
|
const arr = JSON.parse(txt)
|
|
15
18
|
const state = store[this.name]
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
const existing = new Set(state.map(v => v.id))
|
|
20
|
+
const fresh = arr.filter(bg => !existing.has(bg.id))
|
|
21
|
+
if (!fresh.length) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
await runImportTask({
|
|
25
|
+
title: e('import'),
|
|
26
|
+
batch: 200,
|
|
27
|
+
stopWatchers: [this.name],
|
|
28
|
+
steps: [
|
|
29
|
+
{
|
|
30
|
+
label: e('quickCommands'),
|
|
31
|
+
items: fresh,
|
|
32
|
+
process: (chunk) => {
|
|
33
|
+
state.push(...chunk)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
27
37
|
})
|
|
28
|
-
} catch (
|
|
29
|
-
store.onError(
|
|
38
|
+
} catch (err) {
|
|
39
|
+
store.onError(err)
|
|
30
40
|
}
|
|
31
41
|
return false
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
renderEdit () {
|
|
35
|
-
return null
|
|
36
|
-
}
|
|
37
|
-
|
|
38
44
|
handleDownload = () => {
|
|
39
45
|
const { store } = this.props
|
|
40
46
|
const arr = store[this.name]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RDP credentials prompt form, shown at connect time
|
|
3
|
+
* when username/password are missing from the bookmark
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Input,
|
|
8
|
+
Form,
|
|
9
|
+
Button
|
|
10
|
+
} from 'antd'
|
|
11
|
+
import { formItemLayout, tailFormItemLayout } from '../../common/form-layout'
|
|
12
|
+
import Password from '../common/password'
|
|
13
|
+
|
|
14
|
+
const FormItem = Form.Item
|
|
15
|
+
const e = window.translate
|
|
16
|
+
|
|
17
|
+
export default function RdpCredForm (props) {
|
|
18
|
+
const [form] = Form.useForm()
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Form
|
|
22
|
+
form={form}
|
|
23
|
+
onFinish={props.handleFinish}
|
|
24
|
+
initialValues={props.initialValues || {}}
|
|
25
|
+
name='rdp-cred-form'
|
|
26
|
+
>
|
|
27
|
+
<div className='pd3t pd1b'>
|
|
28
|
+
<FormItem
|
|
29
|
+
{...formItemLayout}
|
|
30
|
+
label={e('username')}
|
|
31
|
+
name='username'
|
|
32
|
+
>
|
|
33
|
+
<Input autoFocus />
|
|
34
|
+
</FormItem>
|
|
35
|
+
<FormItem
|
|
36
|
+
{...formItemLayout}
|
|
37
|
+
label={e('password')}
|
|
38
|
+
name='password'
|
|
39
|
+
>
|
|
40
|
+
<Password />
|
|
41
|
+
</FormItem>
|
|
42
|
+
<FormItem
|
|
43
|
+
{...tailFormItemLayout}
|
|
44
|
+
>
|
|
45
|
+
<Button
|
|
46
|
+
type='primary'
|
|
47
|
+
htmlType='submit'
|
|
48
|
+
>
|
|
49
|
+
{e('connect')}
|
|
50
|
+
</Button>
|
|
51
|
+
</FormItem>
|
|
52
|
+
</div>
|
|
53
|
+
</Form>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PureComponent, createRef } from 'react'
|
|
2
2
|
import { createTerm } from '../terminal/terminal-apis'
|
|
3
3
|
import deepCopy from 'json-deep-copy'
|
|
4
|
+
import { pick } from 'lodash-es'
|
|
4
5
|
import clone from '../../common/to-simple-obj'
|
|
5
6
|
import { handleErr } from '../../common/fetch'
|
|
6
7
|
import {
|
|
@@ -24,6 +25,8 @@ import { readClipboardAsync } from '../../common/clipboard'
|
|
|
24
25
|
import RemoteFloatControl from '../common/remote-float-control'
|
|
25
26
|
import SwitchLabel from '../common/switch'
|
|
26
27
|
import HelpIcon from '../common/help-icon'
|
|
28
|
+
import Modal from '../common/modal'
|
|
29
|
+
import RdpCredForm from './rdp-cred-form'
|
|
27
30
|
import { FileTransferManager, createFileLogger } from './file-transfer'
|
|
28
31
|
import { notification } from '../common/notification'
|
|
29
32
|
import message from '../common/message'
|
|
@@ -65,7 +68,8 @@ export default class RdpSession extends PureComponent {
|
|
|
65
68
|
...resObj,
|
|
66
69
|
hasRemoteFiles: false,
|
|
67
70
|
downloadBtnDisabled: true,
|
|
68
|
-
uploadReady: false
|
|
71
|
+
uploadReady: false,
|
|
72
|
+
showCredPrompt: false
|
|
69
73
|
}
|
|
70
74
|
this.session = null
|
|
71
75
|
this.fileTransfer = null
|
|
@@ -121,13 +125,25 @@ export default class RdpSession extends PureComponent {
|
|
|
121
125
|
return `ws://${host}:${port}/${type}/${id}?token=${tokenElecterm}${extra}`
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
remoteInit = async () => {
|
|
128
|
+
remoteInit = async (credOverride) => {
|
|
125
129
|
this.setState({
|
|
126
130
|
loading: true
|
|
127
131
|
})
|
|
128
132
|
const { config } = this.props
|
|
129
133
|
const { id } = this.props
|
|
130
|
-
const tab =
|
|
134
|
+
const tab = {
|
|
135
|
+
...window.store.applyProfile(deepCopy(this.props.tab || {})),
|
|
136
|
+
...credOverride
|
|
137
|
+
}
|
|
138
|
+
// keep resolved credentials for the connect-time prompt
|
|
139
|
+
this.tab = tab
|
|
140
|
+
if (!tab.username) {
|
|
141
|
+
this.setState({
|
|
142
|
+
loading: false,
|
|
143
|
+
showCredPrompt: true
|
|
144
|
+
})
|
|
145
|
+
return
|
|
146
|
+
}
|
|
131
147
|
const {
|
|
132
148
|
type,
|
|
133
149
|
term: terminalType
|
|
@@ -192,6 +208,9 @@ export default class RdpSession extends PureComponent {
|
|
|
192
208
|
const builder = new window.ironRdp.SessionBuilder()
|
|
193
209
|
builder.username(username)
|
|
194
210
|
builder.password(password)
|
|
211
|
+
if (tab.domain) {
|
|
212
|
+
builder.serverDomain(tab.domain)
|
|
213
|
+
}
|
|
195
214
|
builder.destination(destination)
|
|
196
215
|
builder.proxyAddress(proxyAddress)
|
|
197
216
|
builder.authToken('none')
|
|
@@ -277,12 +296,16 @@ export default class RdpSession extends PureComponent {
|
|
|
277
296
|
this.log(`Session ended: ${info.reason()}`, 'info')
|
|
278
297
|
this.onSessionEnd()
|
|
279
298
|
}).catch((e) => {
|
|
280
|
-
this.
|
|
299
|
+
this.showError(e)
|
|
281
300
|
this.onSessionEnd()
|
|
282
301
|
})
|
|
283
302
|
} catch (e) {
|
|
284
|
-
this.
|
|
285
|
-
this.setState({
|
|
303
|
+
this.showError(e)
|
|
304
|
+
this.setState({
|
|
305
|
+
loading: false,
|
|
306
|
+
// let the user retry with different credentials on auth failure
|
|
307
|
+
showCredPrompt: this.isAuthError(e) ? true : this.state.showCredPrompt
|
|
308
|
+
})
|
|
286
309
|
this.setStatus(statusMap.error)
|
|
287
310
|
}
|
|
288
311
|
}
|
|
@@ -307,6 +330,73 @@ export default class RdpSession extends PureComponent {
|
|
|
307
330
|
return e?.message || e?.toString() || String(e)
|
|
308
331
|
}
|
|
309
332
|
|
|
333
|
+
// WrongPassword(1) / LogonFailure(2)
|
|
334
|
+
isAuthError = (err) => {
|
|
335
|
+
if (err && typeof err === 'object' && '__wbg_ptr' in err && err.kind) {
|
|
336
|
+
try {
|
|
337
|
+
const kind = err.kind()
|
|
338
|
+
return kind === 1 || kind === 2
|
|
339
|
+
} catch (_) { }
|
|
340
|
+
}
|
|
341
|
+
return false
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// map known IronErrorKind codes to a user-friendly message
|
|
345
|
+
errText = (err) => {
|
|
346
|
+
if (this.isAuthError(err)) {
|
|
347
|
+
return window.translate('loginFail') + ` (${this.formatError(err)})`
|
|
348
|
+
}
|
|
349
|
+
return this.formatError(err)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
showError = (err) => {
|
|
353
|
+
const text = this.errText(err)
|
|
354
|
+
this.log(`Connection failed: ${this.formatError(err)}`, 'error')
|
|
355
|
+
message.error(text, 10)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
onCredSubmit = (res) => {
|
|
359
|
+
this.setState({
|
|
360
|
+
showCredPrompt: false
|
|
361
|
+
})
|
|
362
|
+
// tear down the failed attempt before reconnecting
|
|
363
|
+
this.cleanup()
|
|
364
|
+
// reconnect with the provided credentials, not persisted to the bookmark
|
|
365
|
+
this.remoteInit(res)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
onCredCancel = () => {
|
|
369
|
+
this.setState({
|
|
370
|
+
showCredPrompt: false
|
|
371
|
+
})
|
|
372
|
+
this.setStatus(statusMap.error)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
renderCredPrompt = () => {
|
|
376
|
+
const {
|
|
377
|
+
showCredPrompt
|
|
378
|
+
} = this.state
|
|
379
|
+
if (!showCredPrompt) {
|
|
380
|
+
return null
|
|
381
|
+
}
|
|
382
|
+
const confirmProps = {
|
|
383
|
+
title: window.translate('credentialsRequired'),
|
|
384
|
+
footer: null,
|
|
385
|
+
open: true,
|
|
386
|
+
onCancel: this.onCredCancel
|
|
387
|
+
}
|
|
388
|
+
return (
|
|
389
|
+
<Modal
|
|
390
|
+
{...confirmProps}
|
|
391
|
+
>
|
|
392
|
+
<RdpCredForm
|
|
393
|
+
initialValues={pick(this.tab, ['username', 'password'])}
|
|
394
|
+
handleFinish={this.onCredSubmit}
|
|
395
|
+
/>
|
|
396
|
+
</Modal>
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
310
400
|
syncLocalToRemote = async () => {
|
|
311
401
|
if (!this.session || this.fileUploadInProgress) {
|
|
312
402
|
return
|
|
@@ -751,6 +841,7 @@ export default class RdpSession extends PureComponent {
|
|
|
751
841
|
ref={this.canvasRef}
|
|
752
842
|
/>
|
|
753
843
|
</div>
|
|
844
|
+
{this.renderCredPrompt()}
|
|
754
845
|
</div>
|
|
755
846
|
</Spin>
|
|
756
847
|
)
|
|
@@ -4,6 +4,7 @@ import time from '../../common/time'
|
|
|
4
4
|
|
|
5
5
|
export default class KeywordsTransport extends BookmarkTransport {
|
|
6
6
|
name = 'keywords-highlight'
|
|
7
|
+
|
|
7
8
|
beforeUpload = async (file) => {
|
|
8
9
|
const { store } = this.props
|
|
9
10
|
const txt = file.fileContent !== undefined
|
|
@@ -20,10 +21,6 @@ export default class KeywordsTransport extends BookmarkTransport {
|
|
|
20
21
|
return false
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
renderEdit () {
|
|
24
|
-
return null
|
|
25
|
-
}
|
|
26
|
-
|
|
27
24
|
handleDownload = () => {
|
|
28
25
|
const { store } = this.props
|
|
29
26
|
const arr = store.config.keywords || []
|
|
@@ -33,14 +33,20 @@ export default auto(function SettingModalWrap (props) {
|
|
|
33
33
|
settingMap.bookmarks,
|
|
34
34
|
settingMap.terminalThemes
|
|
35
35
|
]
|
|
36
|
-
const { settingTab, settingItem,
|
|
36
|
+
const { settingTab, settingItem, bookmarkSelectMode } = store
|
|
37
|
+
// settingSidebarList deep-copies the whole collection; the bookmarks tab
|
|
38
|
+
// ignores `list` (it renders from treeProps), so skip it there — with
|
|
39
|
+
// thousands of bookmarks that copy is the most expensive part of the render
|
|
40
|
+
const list = settingTab === settingMap.bookmarks
|
|
41
|
+
? []
|
|
42
|
+
: store.settingSidebarList
|
|
37
43
|
const props0 = {
|
|
38
44
|
store,
|
|
39
45
|
activeItemId: settingItem.id,
|
|
40
46
|
type: settingTab,
|
|
41
47
|
onClickItem: selectItem,
|
|
42
48
|
shouldConfirmDel: tabsShouldConfirmDel.includes(settingTab),
|
|
43
|
-
list
|
|
49
|
+
list
|
|
44
50
|
}
|
|
45
51
|
const { bookmarks, bookmarkGroups, widgetInstances } = store
|
|
46
52
|
const formProps = {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
BookOutlined
|
|
8
8
|
} from '@ant-design/icons'
|
|
9
9
|
import HelpIcon from '../common/help-icon'
|
|
10
|
+
import buildMap from '../../common/build-map'
|
|
10
11
|
|
|
11
12
|
const e = window.translate
|
|
12
13
|
const { SHOW_CHILD } = TreeSelect
|
|
@@ -21,22 +22,10 @@ function BookmarkSelect (props) {
|
|
|
21
22
|
|
|
22
23
|
const buildData = () => {
|
|
23
24
|
const cats = bookmarkGroups
|
|
24
|
-
const tree = bookmarks
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
...p,
|
|
28
|
-
[k.id]: k
|
|
29
|
-
}
|
|
30
|
-
}, {})
|
|
31
|
-
const btree = cats
|
|
32
|
-
.reduce((p, k) => {
|
|
33
|
-
return {
|
|
34
|
-
...p,
|
|
35
|
-
[k.id]: k
|
|
36
|
-
}
|
|
37
|
-
}, {})
|
|
25
|
+
const tree = buildMap(bookmarks)
|
|
26
|
+
const btree = buildMap(cats)
|
|
38
27
|
function buildSubCats (id) {
|
|
39
|
-
const x = btree
|
|
28
|
+
const x = btree.get(id)
|
|
40
29
|
if (!x) {
|
|
41
30
|
return ''
|
|
42
31
|
}
|
|
@@ -55,7 +44,7 @@ function BookmarkSelect (props) {
|
|
|
55
44
|
return y
|
|
56
45
|
}
|
|
57
46
|
function buildLeaf (id) {
|
|
58
|
-
const x = tree
|
|
47
|
+
const x = tree.get(id)
|
|
59
48
|
if (!x) {
|
|
60
49
|
return ''
|
|
61
50
|
}
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Tabs, Spin } from 'antd'
|
|
6
|
+
import { useEffect } from 'react'
|
|
6
7
|
import SyncForm from './setting-sync-form'
|
|
7
|
-
import {
|
|
8
|
+
import { allowedSyncTypes, syncDataMaps } from '../../common/constants'
|
|
8
9
|
import DataTransport from './data-import'
|
|
9
10
|
import DataSelect from './data-select'
|
|
10
11
|
import { pick } from 'lodash-es'
|
|
@@ -24,6 +25,15 @@ export default auto(function SyncSettingEntry (props) {
|
|
|
24
25
|
const {
|
|
25
26
|
store
|
|
26
27
|
} = window
|
|
28
|
+
const types = allowedSyncTypes()
|
|
29
|
+
const type = types.includes(props.syncType)
|
|
30
|
+
? props.syncType
|
|
31
|
+
: types[0]
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (store.syncType !== type) {
|
|
34
|
+
store.syncType = type
|
|
35
|
+
}
|
|
36
|
+
}, [type])
|
|
27
37
|
function renderForm () {
|
|
28
38
|
const syncProps = {
|
|
29
39
|
...syncSetting,
|
|
@@ -31,12 +41,11 @@ export default auto(function SyncSettingEntry (props) {
|
|
|
31
41
|
'isSyncingSetting',
|
|
32
42
|
'isSyncDownload',
|
|
33
43
|
'isSyncUpload',
|
|
34
|
-
'syncType',
|
|
35
44
|
'serverStatus'
|
|
36
45
|
]),
|
|
37
|
-
|
|
46
|
+
syncType: type,
|
|
47
|
+
serverStatus: deepCopy(store.syncServerStatus[type])
|
|
38
48
|
}
|
|
39
|
-
const type = props.syncType
|
|
40
49
|
const formData = {
|
|
41
50
|
gistId: syncSetting[type + 'GistId'],
|
|
42
51
|
token: syncSetting[type + 'AccessToken'],
|
|
@@ -59,8 +68,7 @@ export default auto(function SyncSettingEntry (props) {
|
|
|
59
68
|
/>
|
|
60
69
|
)
|
|
61
70
|
}
|
|
62
|
-
|
|
63
|
-
const syncItems = Object.keys(syncTypes).map(type => {
|
|
71
|
+
const syncItems = types.map(type => {
|
|
64
72
|
return {
|
|
65
73
|
key: type,
|
|
66
74
|
label: type,
|
|
@@ -84,7 +92,7 @@ export default auto(function SyncSettingEntry (props) {
|
|
|
84
92
|
<DataTransport {...dataImportProps} />
|
|
85
93
|
<Spin spinning={store.isSyncingSetting}>
|
|
86
94
|
<Tabs
|
|
87
|
-
activeKey={
|
|
95
|
+
activeKey={type}
|
|
88
96
|
onChange={handleChange}
|
|
89
97
|
items={syncItems}
|
|
90
98
|
/>
|
|
@@ -127,6 +127,7 @@ export default auto(function InfoModal (props) {
|
|
|
127
127
|
devDependencies,
|
|
128
128
|
dependencies,
|
|
129
129
|
languageRepo,
|
|
130
|
+
privacyNoticeLink,
|
|
130
131
|
author: {
|
|
131
132
|
name: authorName,
|
|
132
133
|
email,
|
|
@@ -203,6 +204,12 @@ export default auto(function InfoModal (props) {
|
|
|
203
204
|
{bugReportLink}
|
|
204
205
|
</Link>
|
|
205
206
|
</p>
|
|
207
|
+
<p className='mg1b'>
|
|
208
|
+
<InfoCircleOutlined /> <b className='mg1r'>{e('privacyNotice')} ➾</b>
|
|
209
|
+
<Link to={privacyNoticeLink} className='mg1l'>
|
|
210
|
+
{privacyNoticeLink}
|
|
211
|
+
</Link>
|
|
212
|
+
</p>
|
|
206
213
|
<p className='mg1b'>
|
|
207
214
|
<HighlightOutlined /> <b className='mg1r'>{e('changeLog')} ➾</b>
|
|
208
215
|
<Link to={releaseLink} className='mg1l'>
|
|
@@ -44,12 +44,52 @@ export default class SpiceSession extends PureComponent {
|
|
|
44
44
|
|
|
45
45
|
componentDidMount () {
|
|
46
46
|
this.remoteInit()
|
|
47
|
+
this.attachMouseScaleFix()
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
componentWillUnmount () {
|
|
51
|
+
this.detachMouseScaleFix()
|
|
50
52
|
this.cleanup()
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
// The spice client sends event.offsetX/offsetY to the remote as-is, but when
|
|
56
|
+
// the canvas is CSS-scaled (scaleViewport or the lib's maxWidth/maxHeight),
|
|
57
|
+
// those are display pixels instead of remote surface pixels. On high
|
|
58
|
+
// resolution remote screens the pointer then only reaches the top-left
|
|
59
|
+
// region. Convert display coords to surface coords (the canvas box is
|
|
60
|
+
// letterboxed by object-fit: contain) before the lib's handlers run.
|
|
61
|
+
handleMouseScaleFix = (e) => {
|
|
62
|
+
const canvas = this.domRef.current?.querySelector('canvas')
|
|
63
|
+
if (!canvas || !canvas.width || !canvas.height) return
|
|
64
|
+
const rect = canvas.getBoundingClientRect()
|
|
65
|
+
if (!rect.width || !rect.height) return
|
|
66
|
+
const scale = Math.min(rect.width / canvas.width, rect.height / canvas.height)
|
|
67
|
+
const x = (e.clientX - rect.left - (rect.width - canvas.width * scale) / 2) / scale
|
|
68
|
+
const y = (e.clientY - rect.top - (rect.height - canvas.height * scale) / 2) / scale
|
|
69
|
+
Object.defineProperty(e, 'offsetX', {
|
|
70
|
+
value: Math.max(0, Math.min(x, canvas.width - 1))
|
|
71
|
+
})
|
|
72
|
+
Object.defineProperty(e, 'offsetY', {
|
|
73
|
+
value: Math.max(0, Math.min(y, canvas.height - 1))
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
attachMouseScaleFix = () => {
|
|
78
|
+
this.domRef.current?.addEventListener(
|
|
79
|
+
'mousemove',
|
|
80
|
+
this.handleMouseScaleFix,
|
|
81
|
+
true
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
detachMouseScaleFix = () => {
|
|
86
|
+
this.domRef.current?.removeEventListener(
|
|
87
|
+
'mousemove',
|
|
88
|
+
this.handleMouseScaleFix,
|
|
89
|
+
true
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
53
93
|
cleanup = () => {
|
|
54
94
|
if (this.spiceConn) {
|
|
55
95
|
try {
|
|
@@ -98,12 +98,6 @@ export default class AttachAddonCustom {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
onShellIntegrationDetected = () => {
|
|
102
|
-
if (this.outputSuppressed) {
|
|
103
|
-
this.stopOutputSuppression(true)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
101
|
activate = async (terminal = this.term) => {
|
|
108
102
|
await this._initBase()
|
|
109
103
|
this.addSocketListener(this._socket, 'message', this.onMsg)
|
|
@@ -177,9 +171,10 @@ export default class AttachAddonCustom {
|
|
|
177
171
|
}
|
|
178
172
|
}
|
|
179
173
|
|
|
180
|
-
|
|
174
|
+
// Index of the first OSC 633 sequence in the chunk, or -1.
|
|
175
|
+
indexOfShellIntegration = (str) => {
|
|
181
176
|
const ESC = String.fromCharCode(27)
|
|
182
|
-
return str.
|
|
177
|
+
return str.indexOf(ESC + ']633;')
|
|
183
178
|
}
|
|
184
179
|
|
|
185
180
|
writeToTerminalDirect = (data) => {
|
|
@@ -232,8 +227,15 @@ export default class AttachAddonCustom {
|
|
|
232
227
|
}
|
|
233
228
|
|
|
234
229
|
if (this.outputSuppressed) {
|
|
235
|
-
|
|
236
|
-
|
|
230
|
+
const oscIdx = this.indexOfShellIntegration(str)
|
|
231
|
+
if (oscIdx !== -1) {
|
|
232
|
+
// Shell integration is confirmed active. The pty often coalesces the
|
|
233
|
+
// tail of the echoed injection command and the first OSC 633 output
|
|
234
|
+
// into one chunk, so write only from the first OSC sequence on —
|
|
235
|
+
// everything before it is echo and stays hidden. A leading newline
|
|
236
|
+
// keeps the fresh prompt off the injection-time prompt line.
|
|
237
|
+
this.stopOutputSuppression(true)
|
|
238
|
+
this._enqueueWrite('\r\n' + str.slice(oscIdx))
|
|
237
239
|
return
|
|
238
240
|
}
|
|
239
241
|
this.suppressedData.push(data)
|
|
@@ -27,7 +27,7 @@ function getBashInlineIntegration () {
|
|
|
27
27
|
'if [[ $- == *i* ]] && [[ -z "${ELECTERM_SHELL_INTEGRATION:-}" ]]',
|
|
28
28
|
'then export ELECTERM_SHELL_INTEGRATION=1',
|
|
29
29
|
'__e_esc() { local v="$1"; v="${v//\\\\/\\\\\\\\}"; v="${v//;/\\\\x3b}"; printf \'%s\' "$v"; }',
|
|
30
|
-
'__e_pre() { [[ "$BASH_COMMAND" == "$
|
|
30
|
+
'__e_pre() { local p; [[ "$BASH_COMMAND" == __e_* || "$BASH_COMMAND" == PROMPT_COMMAND=* ]] && return; [[ "$BASH_COMMAND" == "$PROMPT_COMMAND" ]] && return; if [[ -n "${PROMPT_COMMAND:-}" ]]; then local IFS=\';\'; for p in $PROMPT_COMMAND; do p="${p#"${p%%[! ]*}"}"; [[ "$BASH_COMMAND" == "$p" ]] && return; done; fi; [[ "${__e_in:-0}" == "0" ]] && { __e_in=1; printf \'\\e]633;E;%s\\a\\e]633;C\\a\' "$(__e_esc "$BASH_COMMAND")"; }; }',
|
|
31
31
|
'__e_cmd() { local c="$?"; [[ "${__e_in:-0}" == "1" ]] && { printf \'\\e]633;D;%s\\a\' "$c"; __e_in=0; }; printf \'\\e]633;P;Cwd=%s\\a\\e]633;A\\a\' "$(__e_esc "$PWD")"; return "$c"; }',
|
|
32
32
|
'trap \'__e_pre\' DEBUG',
|
|
33
33
|
'PROMPT_COMMAND="__e_cmd${PROMPT_COMMAND:+; $PROMPT_COMMAND}"',
|
|
@@ -40,17 +40,19 @@ function getBashInlineIntegration () {
|
|
|
40
40
|
* Properly formatted for semicolon joining
|
|
41
41
|
*/
|
|
42
42
|
function getZshInlineIntegration () {
|
|
43
|
-
//
|
|
44
|
-
//
|
|
43
|
+
// Register hooks by appending to the precmd/preexec hook arrays directly.
|
|
44
|
+
// add-zsh-hook is an autoloadable shell function: on systems like NixOS
|
|
45
|
+
// fpath may not include the standard functions directory, the autoload
|
|
46
|
+
// fails silently (stderr is suppressed) and the hooks never run, breaking
|
|
47
|
+
// path following. Appending to the arrays works on every zsh.
|
|
45
48
|
return [
|
|
46
49
|
'if [[ -o interactive ]] && [[ -z "${ELECTERM_SHELL_INTEGRATION:-}" ]]',
|
|
47
50
|
'then export ELECTERM_SHELL_INTEGRATION=1',
|
|
48
51
|
'__e_esc() { local v="$1"; v="${v//\\\\/\\\\\\\\}"; v="${v//;/\\\\x3b}"; builtin printf \'%s\' "$v"; }',
|
|
49
52
|
'__e_preexec() { __e_cmd="$1"; builtin printf \'\\e]633;E;%s\\a\\e]633;C\\a\' "$(__e_esc "$1")"; }',
|
|
50
53
|
'__e_precmd() { local c="$?"; [[ -n "$__e_cmd" ]] && builtin printf \'\\e]633;D;%s\\a\' "$c"; __e_cmd=""; builtin printf \'\\e]633;P;Cwd=%s\\a\\e]633;A\\a\' "$(__e_esc "$PWD")"; }',
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'add-zsh-hook preexec __e_preexec',
|
|
54
|
+
'precmd_functions+=(__e_precmd)',
|
|
55
|
+
'preexec_functions+=(__e_preexec)',
|
|
54
56
|
'fi'
|
|
55
57
|
].join('; ')
|
|
56
58
|
}
|
|
@@ -78,11 +80,13 @@ function getShInlineIntegration () {
|
|
|
78
80
|
return [
|
|
79
81
|
'if [ -z "$ELECTERM_SHELL_INTEGRATION" ]',
|
|
80
82
|
'then export ELECTERM_SHELL_INTEGRATION=1',
|
|
81
|
-
'__e_esc() { printf
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
83
|
+
'__e_esc() { printf \'%s\' "$1" | sed \'s/\\\\/\\\\\\\\/g; s/;/\\\\x3b/g\'; }',
|
|
84
|
+
// sh/dash/ash re-evaluate $(...) inside PS1 every time the prompt is
|
|
85
|
+
// shown, but only if the $() stays literal — so PS1 must be assigned
|
|
86
|
+
// single-quoted. printf interprets \033/\a, unlike PS1 backslash
|
|
87
|
+
// escapes which dash does not expand.
|
|
88
|
+
'__e_ps1() { printf \'\\033]633;P;Cwd=%s\\a\\033]633;A\\a\' "$(__e_esc "$PWD")"; }',
|
|
89
|
+
'PS1=\'$(__e_ps1)\'"${PS1:-# }"',
|
|
86
90
|
'fi'
|
|
87
91
|
].join('; ')
|
|
88
92
|
}
|
|
@@ -139,6 +143,13 @@ export function wrapSilent (cmd, shellType) {
|
|
|
139
143
|
* @returns {string} Complete command to send to terminal
|
|
140
144
|
*/
|
|
141
145
|
export function getShellIntegrationCommand (shellType = 'bash') {
|
|
146
|
+
if (shellType === 'fish') {
|
|
147
|
+
// fish is not a Bourne shell: the eval '\''...'\'' wrapper of wrapSilent
|
|
148
|
+
// is a parse error in fish (nested single quotes stay unbalanced), so
|
|
149
|
+
// the whole injection silently fails. Send the script raw instead —
|
|
150
|
+
// it is already valid fish syntax as-is.
|
|
151
|
+
return ' ' + getInlineShellIntegration(shellType) + '\r'
|
|
152
|
+
}
|
|
142
153
|
const cmd = getInlineShellIntegration(shellType)
|
|
143
154
|
return wrapSilent(cmd, shellType)
|
|
144
155
|
}
|