@electerm/electerm-react 5.5.56 → 5.5.76
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 +12 -0
- package/client/common/dangerous-session-fields.js +39 -0
- package/client/common/default-setting.js +1 -0
- package/client/common/distribute-tabs.js +42 -0
- package/client/common/import-task.js +46 -3
- package/client/common/parse-quick-connect.js +47 -6
- package/client/common/quick-connect-string.js +134 -0
- package/client/common/safe-join.js +33 -0
- package/client/common/sanitize-filename.js +8 -0
- package/client/common/ui-theme-color.mjs +136 -0
- package/client/common/update-check.js +14 -0
- package/client/components/ai/ai-output.jsx +5 -0
- package/client/components/ai/ai.styl +19 -0
- package/client/components/bookmark-form/copy-bookmark.jsx +83 -0
- package/client/components/bookmark-form/form-renderer.jsx +13 -1
- package/client/components/bookmark-form/index.jsx +20 -1
- package/client/components/bookmark-form/render-form.jsx +2 -2
- package/client/components/file-transfer/remote2remote-handlers.jsx +8 -3
- package/client/components/main/ui-theme.jsx +1 -20
- package/client/components/main/upgrade.jsx +2 -0
- package/client/components/setting-panel/install-command.jsx +151 -0
- package/client/components/setting-panel/setting-common.jsx +5 -0
- package/client/components/setting-panel/setting-terminal.jsx +10 -1
- package/client/components/terminal/mixins/term-attach.js +10 -2
- package/client/components/terminal/mixins/term-init.js +1 -0
- package/client/components/terminal/mixins/term-socket.js +12 -2
- package/client/components/terminal/mixins/term-suggestions.js +16 -3
- package/client/components/terminal/osc52-addon.js +7 -2
- package/client/components/terminal/session-status-overlay.jsx +44 -0
- package/client/components/terminal/shell.js +11 -3
- package/client/components/terminal/shortcut-bar-press.js +192 -0
- package/client/components/terminal/shortcut-bar.jsx +199 -4
- package/client/components/terminal/shortcut-bar.styl +40 -21
- package/client/components/terminal/ssh-reload-state.js +41 -2
- package/client/components/terminal/startup-queue.js +6 -2
- package/client/components/terminal/terminal.jsx +17 -2
- package/client/components/terminal/terminal.styl +14 -5
- package/client/store/mcp-handler.js +8 -15
- package/client/store/sync.js +47 -18
- package/client/store/tab.js +47 -17
- package/client/store/watch.js +8 -0
- package/package.json +1 -1
- package/client/components/terminal/reconnect-overlay.jsx +0 -14
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy the bookmark form's current values: as a quick connect string, or as
|
|
3
|
+
* the raw JSON that lands in the config file.
|
|
4
|
+
*/
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import { Dropdown } from 'antd'
|
|
7
|
+
import { CopyOutlined } from '@ant-design/icons'
|
|
8
|
+
import { copy } from '../../common/clipboard'
|
|
9
|
+
import { stringifyQuickConnect } from '../../common/quick-connect-string'
|
|
10
|
+
|
|
11
|
+
const e = window.translate
|
|
12
|
+
|
|
13
|
+
const maxLen = 50
|
|
14
|
+
|
|
15
|
+
function oneLine (str) {
|
|
16
|
+
const s = String(str || '').replace(/\s+/g, ' ').trim()
|
|
17
|
+
return s.length > maxLen ? s.slice(0, maxLen) + '...' : s
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function label (name, value) {
|
|
21
|
+
return (
|
|
22
|
+
<span title={value}>
|
|
23
|
+
<b className='mg1r'>{name}</b>
|
|
24
|
+
<span>{oneLine(value)}</span>
|
|
25
|
+
<CopyOutlined className='mg1l' />
|
|
26
|
+
</span>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function CopyBookmark ({ getValues }) {
|
|
31
|
+
const [open, setOpen] = useState(false)
|
|
32
|
+
const [items, setItems] = useState([])
|
|
33
|
+
|
|
34
|
+
const getStrings = () => {
|
|
35
|
+
const values = getValues() || {}
|
|
36
|
+
return {
|
|
37
|
+
quickConnect: stringifyQuickConnect(values),
|
|
38
|
+
json: JSON.stringify(values, null, 2)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const onOpenChange = (next) => {
|
|
43
|
+
setOpen(next)
|
|
44
|
+
if (!next) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
const { quickConnect, json } = getStrings()
|
|
48
|
+
setItems([
|
|
49
|
+
{
|
|
50
|
+
key: 'quickConnect',
|
|
51
|
+
disabled: !quickConnect,
|
|
52
|
+
label: label(e('quickConnect'), quickConnect)
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: 'json',
|
|
56
|
+
label: label('JSON', json)
|
|
57
|
+
}
|
|
58
|
+
])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const onClick = ({ key }) => {
|
|
62
|
+
const { quickConnect, json } = getStrings()
|
|
63
|
+
const value = key === 'json' ? json : quickConnect
|
|
64
|
+
if (value) {
|
|
65
|
+
copy(value)
|
|
66
|
+
}
|
|
67
|
+
setOpen(false)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Dropdown
|
|
72
|
+
menu={{ items, onClick }}
|
|
73
|
+
open={open}
|
|
74
|
+
onOpenChange={onOpenChange}
|
|
75
|
+
trigger={['click']}
|
|
76
|
+
placement='bottomRight'
|
|
77
|
+
>
|
|
78
|
+
<span className='pointer fright' title={e('copy')}>
|
|
79
|
+
<CopyOutlined />
|
|
80
|
+
</span>
|
|
81
|
+
</Dropdown>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -22,7 +22,7 @@ import newTerm from '../../common/new-terminal'
|
|
|
22
22
|
import { isValidIP } from '../../common/is-ip'
|
|
23
23
|
import { action as manateAction } from 'manate'
|
|
24
24
|
|
|
25
|
-
export default function FormRenderer ({ config, props }) {
|
|
25
|
+
export default function FormRenderer ({ config, props, onForm }) {
|
|
26
26
|
const initialValues = config.initValues(props)
|
|
27
27
|
const [form] = Form.useForm()
|
|
28
28
|
const [ips, setIps] = useState([])
|
|
@@ -30,6 +30,18 @@ export default function FormRenderer ({ config, props }) {
|
|
|
30
30
|
const [testing, setTesting] = useState(false)
|
|
31
31
|
const action = useRef('submit')
|
|
32
32
|
|
|
33
|
+
// hand the form instance up so the header can read the current values
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (onForm) {
|
|
36
|
+
onForm(form)
|
|
37
|
+
}
|
|
38
|
+
return () => {
|
|
39
|
+
if (onForm) {
|
|
40
|
+
onForm(null)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}, [onForm, form])
|
|
44
|
+
|
|
33
45
|
useEffect(() => {
|
|
34
46
|
const init = config.initValues(props)
|
|
35
47
|
form.resetFields()
|
|
@@ -21,6 +21,7 @@ import { LoadingOutlined, BookOutlined, RobotOutlined } from '@ant-design/icons'
|
|
|
21
21
|
import sessionConfig from './config/session-config'
|
|
22
22
|
import renderForm from './render-form'
|
|
23
23
|
import AIBookmarkForm from './ai-bookmark-form'
|
|
24
|
+
import CopyBookmark from './copy-bookmark'
|
|
24
25
|
import { isAIDisabled } from '../../common/ai-feature'
|
|
25
26
|
import './bookmark-form.styl'
|
|
26
27
|
|
|
@@ -80,6 +81,19 @@ export default class BookmarkIndex2 extends PureComponent {
|
|
|
80
81
|
this.setState({ bookmarkType: e.target.value })
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
// the form instance lives in FormRenderer, the copy menu lives in the header
|
|
85
|
+
handleFormReady = (form) => {
|
|
86
|
+
this.form = form
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// same shape the form submits: saved bookmark + what is currently typed
|
|
90
|
+
getFormValues = () => {
|
|
91
|
+
return {
|
|
92
|
+
...this.props.formData,
|
|
93
|
+
...(this.form ? this.form.getFieldsValue() : {})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
handleCancelAiMode = () => {
|
|
84
98
|
this.setState({ aiMode: false })
|
|
85
99
|
}
|
|
@@ -147,12 +161,16 @@ export default class BookmarkIndex2 extends PureComponent {
|
|
|
147
161
|
)
|
|
148
162
|
}
|
|
149
163
|
|
|
164
|
+
renderCopy () {
|
|
165
|
+
return <CopyBookmark getValues={this.getFormValues} />
|
|
166
|
+
}
|
|
167
|
+
|
|
150
168
|
renderForm () {
|
|
151
169
|
const { bookmarkType, aiMode } = this.state
|
|
152
170
|
if (aiMode) {
|
|
153
171
|
return this.renderAiForm()
|
|
154
172
|
}
|
|
155
|
-
return renderForm(bookmarkType, this.props)
|
|
173
|
+
return renderForm(bookmarkType, this.props, this.handleFormReady)
|
|
156
174
|
}
|
|
157
175
|
|
|
158
176
|
render () {
|
|
@@ -180,6 +198,7 @@ export default class BookmarkIndex2 extends PureComponent {
|
|
|
180
198
|
</span>
|
|
181
199
|
{this.renderTitle(formData, isNew)}
|
|
182
200
|
{this.renderAIButton(isNew)}
|
|
201
|
+
{this.renderCopy()}
|
|
183
202
|
</p>
|
|
184
203
|
{this.renderTypes(bookmarkType, isNew, keys)}
|
|
185
204
|
</div>
|
|
@@ -5,7 +5,7 @@ import sessionConfig from './config/session-config'
|
|
|
5
5
|
import { connectionMap } from '../../common/constants'
|
|
6
6
|
import FormRenderer from './form-renderer'
|
|
7
7
|
|
|
8
|
-
export default function renderForm (type, props) {
|
|
8
|
+
export default function renderForm (type, props, onForm) {
|
|
9
9
|
const conf = sessionConfig[type] || sessionConfig[connectionMap.ssh]
|
|
10
|
-
return <FormRenderer config={conf} props={props} />
|
|
10
|
+
return <FormRenderer config={conf} props={props} onForm={onForm} />
|
|
11
11
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Component } from 'react'
|
|
2
|
-
import
|
|
2
|
+
import safeJoin from '../../common/safe-join'
|
|
3
3
|
import { typeMap } from '../../common/constants'
|
|
4
4
|
import { refsStatic } from '../common/ref'
|
|
5
5
|
import Remote2RemoteHandler from './remote2remote-handler'
|
|
@@ -35,7 +35,10 @@ export default class Remote2RemoteHandlers extends Component {
|
|
|
35
35
|
createHandler = ({ fromFile, targetPathBase, targetTab }) => {
|
|
36
36
|
const handler = new Remote2RemoteHandler({
|
|
37
37
|
fromFile,
|
|
38
|
-
|
|
38
|
+
// `fromFile.name` comes straight off the source server's READDIR, so it
|
|
39
|
+
// is attacker-controlled. It is written to the target host as the victim
|
|
40
|
+
// below, so it must not be able to leave `targetPathBase`.
|
|
41
|
+
toPath: safeJoin(targetPathBase, fromFile.name),
|
|
39
42
|
sourceHost: fromFile.host,
|
|
40
43
|
sourceTabId: fromFile.tabId,
|
|
41
44
|
title: fromFile.title,
|
|
@@ -58,7 +61,9 @@ export default class Remote2RemoteHandlers extends Component {
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
onRemote2RemoteDrop = ({ fromFiles, toFile, targetTab }) => {
|
|
61
|
-
|
|
64
|
+
// `toFile.name` is an entry from the target panel's listing. The victim
|
|
65
|
+
// chose the panel, but the name is still server-supplied.
|
|
66
|
+
const targetPathBase = safeJoin(toFile.path, toFile.name)
|
|
62
67
|
const targetHost = targetTab?.host
|
|
63
68
|
let handled = false
|
|
64
69
|
for (const fromFile of fromFiles) {
|
|
@@ -5,29 +5,10 @@
|
|
|
5
5
|
import { useEffect, useRef } from 'react'
|
|
6
6
|
import eq from 'fast-deep-equal'
|
|
7
7
|
import isColorDark from '../../common/is-color-dark'
|
|
8
|
+
import { darker } from '../../common/ui-theme-color.mjs'
|
|
8
9
|
|
|
9
10
|
const themeDomId = 'theme-css'
|
|
10
11
|
|
|
11
|
-
function darker (color, amount = 0.1) {
|
|
12
|
-
let usePound = false
|
|
13
|
-
|
|
14
|
-
if (color[0] === '#') {
|
|
15
|
-
color = color.slice(1)
|
|
16
|
-
usePound = true
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const num = parseInt(color, 16)
|
|
20
|
-
|
|
21
|
-
let r = (num >> 16) - Math.round(255 * amount)
|
|
22
|
-
if (r < 0) r = 0
|
|
23
|
-
let b = ((num >> 8) & 0x00FF) - Math.round(255 * amount)
|
|
24
|
-
if (b < 0) b = 0
|
|
25
|
-
let g = (num & 0x0000FF) - Math.round(255 * amount)
|
|
26
|
-
if (g < 0) g = 0
|
|
27
|
-
|
|
28
|
-
return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
12
|
function buildTheme (themeConfig) {
|
|
32
13
|
const keys = Object.keys(themeConfig || {})
|
|
33
14
|
const themeCss = keys.map(key => {
|
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
const downloadMirrorList = [
|
|
29
29
|
'github',
|
|
30
30
|
'gh-proxy',
|
|
31
|
+
'atomgit',
|
|
31
32
|
'sourceforge',
|
|
32
33
|
'r2'
|
|
33
34
|
]
|
|
@@ -293,6 +294,7 @@ export default class Upgrade extends PureComponent {
|
|
|
293
294
|
getPopupContainer={() => document.body}
|
|
294
295
|
size='small'
|
|
295
296
|
style={{ height: 32 }}
|
|
297
|
+
popupMatchSelectWidth={false}
|
|
296
298
|
>
|
|
297
299
|
{downloadMirrorList.map((opt) => (
|
|
298
300
|
<Select.Option key={opt} value={opt}>{opt}</Select.Option>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
import { Button, Tag, Space, Tooltip } from 'antd'
|
|
3
|
+
import {
|
|
4
|
+
CheckCircleOutlined,
|
|
5
|
+
CloseCircleOutlined,
|
|
6
|
+
WarningOutlined
|
|
7
|
+
} from '@ant-design/icons'
|
|
8
|
+
import message from '../common/message'
|
|
9
|
+
import HelpIcon from '../common/help-icon'
|
|
10
|
+
import { installCommandHelpLink } from '../../common/constants'
|
|
11
|
+
|
|
12
|
+
// The row title and the two buttons come from electerm-locales
|
|
13
|
+
// (`command` / `install` / `uninstall`); the descriptive lines are still plain
|
|
14
|
+
// English and the maintainer adds keys for them as needed.
|
|
15
|
+
// window.translate is installed by entry/basic.js, so look it up at call time
|
|
16
|
+
// rather than capturing it when this module is evaluated.
|
|
17
|
+
const t = key => window.translate(key)
|
|
18
|
+
|
|
19
|
+
export default function InstallCommand () {
|
|
20
|
+
const [loading, setLoading] = useState(false)
|
|
21
|
+
const [status, setStatus] = useState(null)
|
|
22
|
+
|
|
23
|
+
const refresh = useCallback(async () => {
|
|
24
|
+
try {
|
|
25
|
+
const s = await window.pre.runGlobalAsync('getElectermCommandStatus')
|
|
26
|
+
setStatus(s)
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error('Failed to read electerm command status', err)
|
|
29
|
+
}
|
|
30
|
+
}, [])
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
refresh()
|
|
34
|
+
}, [refresh])
|
|
35
|
+
|
|
36
|
+
const run = async (name, okText) => {
|
|
37
|
+
setLoading(true)
|
|
38
|
+
try {
|
|
39
|
+
const res = await window.pre.runGlobalAsync(name)
|
|
40
|
+
if (res && res.ok) {
|
|
41
|
+
message.success(okText)
|
|
42
|
+
} else {
|
|
43
|
+
message.error((res && res.message) || 'Failed to update the electerm command')
|
|
44
|
+
}
|
|
45
|
+
await refresh()
|
|
46
|
+
} catch (err) {
|
|
47
|
+
message.error(err.message || String(err))
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const handleInstall = () =>
|
|
54
|
+
run('installElectermCommand', `electerm ${t('command')} installed`)
|
|
55
|
+
const handleUninstall = () =>
|
|
56
|
+
run('uninstallElectermCommand', `electerm ${t('command')} removed`)
|
|
57
|
+
|
|
58
|
+
if (!status) {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { installed, inPath, binDir, stale, blocked, unpackaged } = status
|
|
63
|
+
const needsPath = installed && inPath === false
|
|
64
|
+
|
|
65
|
+
// In a dev run process.execPath is Electron, so installing would point the
|
|
66
|
+
// command at Electron. Say so rather than offering a button that breaks it.
|
|
67
|
+
if (unpackaged) {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className='pd2b'>
|
|
73
|
+
<div className='pd1b'>
|
|
74
|
+
<Space size='small' wrap>
|
|
75
|
+
<span className='inline-title'>{t('install')} electerm {t('command')} to PATH</span>
|
|
76
|
+
<HelpIcon link={installCommandHelpLink} />
|
|
77
|
+
<Tag
|
|
78
|
+
variant='solid'
|
|
79
|
+
icon={installed ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
|
|
80
|
+
color={installed ? 'success' : 'default'}
|
|
81
|
+
>
|
|
82
|
+
{installed ? 'Installed' : (stale ? 'Outdated' : 'Not installed')}
|
|
83
|
+
</Tag>
|
|
84
|
+
{
|
|
85
|
+
blocked
|
|
86
|
+
? (
|
|
87
|
+
<Tag variant='solid' color='error' icon={<WarningOutlined />}>
|
|
88
|
+
Blocked
|
|
89
|
+
</Tag>
|
|
90
|
+
)
|
|
91
|
+
: null
|
|
92
|
+
}
|
|
93
|
+
</Space>
|
|
94
|
+
</div>
|
|
95
|
+
{
|
|
96
|
+
stale
|
|
97
|
+
? (
|
|
98
|
+
<div className='pd1b'>
|
|
99
|
+
<WarningOutlined className='mg1r' />
|
|
100
|
+
This command points somewhere else now. Install again to update it.
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
: null
|
|
104
|
+
}
|
|
105
|
+
{
|
|
106
|
+
binDir
|
|
107
|
+
? (
|
|
108
|
+
<div className='pd1b'>
|
|
109
|
+
<Tooltip title={binDir}>
|
|
110
|
+
<span>{binDir}</span>
|
|
111
|
+
</Tooltip>
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
: null
|
|
115
|
+
}
|
|
116
|
+
{
|
|
117
|
+
needsPath
|
|
118
|
+
? (
|
|
119
|
+
<div className='pd1b'>
|
|
120
|
+
<WarningOutlined className='mg1r' />
|
|
121
|
+
{binDir} is not in your PATH. Add it to PATH so the command can be found.
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
: null
|
|
125
|
+
}
|
|
126
|
+
<div className='deep-link-btns'>
|
|
127
|
+
<Button
|
|
128
|
+
type='primary'
|
|
129
|
+
onClick={handleInstall}
|
|
130
|
+
loading={loading}
|
|
131
|
+
>
|
|
132
|
+
{t('install')}
|
|
133
|
+
</Button>
|
|
134
|
+
{
|
|
135
|
+
installed || stale
|
|
136
|
+
? (
|
|
137
|
+
<Button
|
|
138
|
+
color='danger'
|
|
139
|
+
variant='solid'
|
|
140
|
+
onClick={handleUninstall}
|
|
141
|
+
loading={loading}
|
|
142
|
+
>
|
|
143
|
+
{t('uninstall')}
|
|
144
|
+
</Button>
|
|
145
|
+
)
|
|
146
|
+
: null
|
|
147
|
+
}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
@@ -33,6 +33,7 @@ import HelpIcon from '../common/help-icon'
|
|
|
33
33
|
import delay from '../../common/wait.js'
|
|
34
34
|
import isColorDark from '../../common/is-color-dark'
|
|
35
35
|
import DeepLinkControl from './deep-link-control'
|
|
36
|
+
import InstallCommand from './install-command'
|
|
36
37
|
import HotkeySetting from './hotkey'
|
|
37
38
|
import SettingLeftSidebarIcons from './setting-left-sidebar-icons'
|
|
38
39
|
import './setting.styl'
|
|
@@ -578,11 +579,15 @@ export default class SettingCommon extends Component {
|
|
|
578
579
|
'disableTabIndex',
|
|
579
580
|
'onlyShowTitleInTab',
|
|
580
581
|
'disableShortcutBar',
|
|
582
|
+
'autoDistributeTabsWhenLayoutChange',
|
|
581
583
|
'debug'
|
|
582
584
|
]
|
|
583
585
|
.filter(name => !isWebApp || !webAppHiddenSettings.includes(name))
|
|
584
586
|
.map(this.renderToggle)
|
|
585
587
|
}
|
|
588
|
+
{
|
|
589
|
+
window.et.isWebApp ? null : <InstallCommand />
|
|
590
|
+
}
|
|
586
591
|
{
|
|
587
592
|
window.et.isWebApp ? null : <DeepLinkControl />
|
|
588
593
|
}
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
noTerminalBgValue,
|
|
19
19
|
rendererTypes,
|
|
20
20
|
regexHelpLink,
|
|
21
|
-
terminalTypes
|
|
21
|
+
terminalTypes,
|
|
22
|
+
toggleWikiMap
|
|
22
23
|
} from '../../common/constants'
|
|
23
24
|
import defaultSettings from '../../common/default-setting'
|
|
24
25
|
import ShowItem from '../common/show-item'
|
|
@@ -121,6 +122,8 @@ export default class SettingTerminal extends Component {
|
|
|
121
122
|
renderToggle = (name, cls = 'pd2b', label) => {
|
|
122
123
|
const checked = !!this.props.config[name]
|
|
123
124
|
const txt = label || e(name)
|
|
125
|
+
// only toggles listed in toggleWikiMap get a help icon
|
|
126
|
+
const wiki = toggleWikiMap[name]
|
|
124
127
|
return (
|
|
125
128
|
<div className={cls} key={'rt' + name}>
|
|
126
129
|
<SwitchLabel
|
|
@@ -128,6 +131,11 @@ export default class SettingTerminal extends Component {
|
|
|
128
131
|
label={txt}
|
|
129
132
|
onChange={v => this.onChangeValue(v, name)}
|
|
130
133
|
/>
|
|
134
|
+
{
|
|
135
|
+
wiki
|
|
136
|
+
? <HelpIcon link={wiki} />
|
|
137
|
+
: null
|
|
138
|
+
}
|
|
131
139
|
</div>
|
|
132
140
|
)
|
|
133
141
|
}
|
|
@@ -569,6 +577,7 @@ export default class SettingTerminal extends Component {
|
|
|
569
577
|
[
|
|
570
578
|
'cursorBlink',
|
|
571
579
|
'rightClickSelectsWord',
|
|
580
|
+
'mouseEventsRequireAlt',
|
|
572
581
|
'pasteWhenContextMenu',
|
|
573
582
|
'copyWhenSelect',
|
|
574
583
|
'disableConfirmForLargeClipboardContent',
|
|
@@ -5,6 +5,7 @@ import AttachAddon from '../attach-addon-custom.js'
|
|
|
5
5
|
import { createTriggerManager } from '../automation/index.js'
|
|
6
6
|
import { isWin } from '../../../common/constants.js'
|
|
7
7
|
import { isUnsafeFilename } from '../../../common/file-drop-utils.js'
|
|
8
|
+
import { createCdCommand } from '../ssh-reload-state.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Everything that writes to the session: the socket attach addon, encoding,
|
|
@@ -57,8 +58,15 @@ export const attachMixin = {
|
|
|
57
58
|
if (isUnsafeFilename(p)) {
|
|
58
59
|
return message.error('File name contains unsafe characters')
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
// Same shell resolution the session used: the bookmark/profile override
|
|
62
|
+
// first, then the global setting. `localShell` is what actually spawned.
|
|
63
|
+
const cmd = createCdCommand(p, {
|
|
64
|
+
remote: !!this.isRemote(),
|
|
65
|
+
shell: this.localShell || this.props.tab.execWindows || this.props.config.execWindows
|
|
66
|
+
})
|
|
67
|
+
if (cmd) {
|
|
68
|
+
this.runQuickCommand(cmd)
|
|
69
|
+
}
|
|
62
70
|
},
|
|
63
71
|
|
|
64
72
|
initTriggerManager () {
|
|
@@ -27,6 +27,7 @@ export const initMixin = {
|
|
|
27
27
|
allowProposedApi: true,
|
|
28
28
|
scrollback: config.scrollback,
|
|
29
29
|
rightClickSelectsWord: config.rightClickSelectsWord || false,
|
|
30
|
+
mouseEventsRequireAlt: config.mouseEventsRequireAlt || false,
|
|
30
31
|
fontFamily: tab.fontFamily || config.fontFamily,
|
|
31
32
|
theme: tc,
|
|
32
33
|
allowTransparency: true,
|
|
@@ -78,7 +78,8 @@ export const socketMixin = {
|
|
|
78
78
|
async remoteInit (term = this.term) {
|
|
79
79
|
this.setState({
|
|
80
80
|
loading: true,
|
|
81
|
-
terminalError: null
|
|
81
|
+
terminalError: null,
|
|
82
|
+
sessionStopped: false
|
|
82
83
|
})
|
|
83
84
|
const { cols, rows } = term
|
|
84
85
|
const { config } = this.props
|
|
@@ -124,6 +125,10 @@ export const socketMixin = {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
const keepaliveInterval = tab.keepaliveInterval || config.keepaliveInterval
|
|
128
|
+
// Remember which executable this session actually spawned (profile- and
|
|
129
|
+
// bookmark-applied). `cd()` in term-attach needs the same answer to pick
|
|
130
|
+
// CMD vs PowerShell syntax; the global setting alone is not enough.
|
|
131
|
+
this.localShell = execOpts[execPropName] || ''
|
|
127
132
|
const opts = clone({
|
|
128
133
|
cols,
|
|
129
134
|
rows,
|
|
@@ -188,6 +193,10 @@ export const socketMixin = {
|
|
|
188
193
|
return
|
|
189
194
|
}
|
|
190
195
|
this.setStatus(statusMap.error)
|
|
196
|
+
// The pane is dead even though the socket never opened. The notice itself
|
|
197
|
+
// stays hidden while `terminalError` is set - the error alert is already
|
|
198
|
+
// the message.
|
|
199
|
+
this.setState({ sessionStopped: true })
|
|
191
200
|
return
|
|
192
201
|
}
|
|
193
202
|
this.port = r.port
|
|
@@ -207,7 +216,7 @@ export const socketMixin = {
|
|
|
207
216
|
// instead of leaving an unhandled rejection behind
|
|
208
217
|
try {
|
|
209
218
|
await this.initAttachAddon()
|
|
210
|
-
this.startupQueue.runInitScript()
|
|
219
|
+
this.startupQueue.runInitScript(opts[execPropName])
|
|
211
220
|
} catch (e) {
|
|
212
221
|
console.error(e)
|
|
213
222
|
this.handleError({ message: e.message })
|
|
@@ -319,6 +328,7 @@ export const socketMixin = {
|
|
|
319
328
|
if (this.userTypeExit) {
|
|
320
329
|
return this.props.delTab(this.props.tab.id)
|
|
321
330
|
}
|
|
331
|
+
this.setState({ sessionStopped: true })
|
|
322
332
|
const { autoReconnectTerminal } = this.props.config
|
|
323
333
|
if (autoReconnectTerminal) {
|
|
324
334
|
this.scheduleAutoReconnect(3000)
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { debounce } from 'lodash-es'
|
|
2
2
|
import { refsStatic } from '../../common/ref.js'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Lines that end the shell on purpose. `userTypeExit` decides whether a closed
|
|
6
|
+
* socket closes the tab straight away or shows the "session stopped" notice,
|
|
7
|
+
* so anything a user would type to leave has to be listed here.
|
|
8
|
+
*/
|
|
9
|
+
const exitCommandReg = /^(?:exit|logout)(?:\s|$)/
|
|
10
|
+
|
|
4
11
|
/**
|
|
5
12
|
* Command suggestions: reading the current input straight from the terminal
|
|
6
13
|
* buffer, positioning the dropdown at the cursor, and the password prompt
|
|
@@ -93,15 +100,21 @@ export const suggestionsMixin = {
|
|
|
93
100
|
* The actual input reading is done via getCurrentInput from buffer
|
|
94
101
|
*/
|
|
95
102
|
handleInputEvent (d) {
|
|
103
|
+
// Ctrl+D on an empty line is EOF, which ends the shell like `exit`. Typed
|
|
104
|
+
// anywhere else it is delete-char, so only an empty line counts.
|
|
105
|
+
if (d === '\x04') {
|
|
106
|
+
this.userTypeExit = !this.getCurrentInput().trim()
|
|
107
|
+
}
|
|
96
108
|
// Handle Enter - add command to history
|
|
97
109
|
if (d === '\r' || d === '\n') {
|
|
98
110
|
const currentCmd = this.getCurrentInput()
|
|
99
111
|
if (currentCmd && currentCmd.trim() && this.shouldUseManualHistory()) {
|
|
100
112
|
window.store.addCmdHistory(currentCmd.trim())
|
|
101
113
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
// Recomputed on every submitted line rather than only ever set: an
|
|
115
|
+
// `exit` that did not actually end the shell must not silence the next
|
|
116
|
+
// real disconnect.
|
|
117
|
+
this.userTypeExit = !!currentCmd && exitCommandReg.test(currentCmd.trim())
|
|
105
118
|
this.closeSuggestions()
|
|
106
119
|
}
|
|
107
120
|
},
|
|
@@ -64,8 +64,13 @@ export class Osc52Addon {
|
|
|
64
64
|
const target = data.substring(0, semicolonIdx)
|
|
65
65
|
const payload = data.substring(semicolonIdx + 1)
|
|
66
66
|
|
|
67
|
-
// Only handle clipboard target ('c'), also accept 'c' among multiple targets
|
|
68
|
-
|
|
67
|
+
// Only handle clipboard target ('c'), also accept 'c' among multiple targets.
|
|
68
|
+
// An EMPTY target is the spec's "default selection", i.e. the clipboard, and is
|
|
69
|
+
// what tmux sends: `set -g mouse on` + a mouse drag + wheel (to select across
|
|
70
|
+
// screens) makes tmux emit `ESC ] 52 ; ; <base64> BEL` on release. Rejecting an
|
|
71
|
+
// empty target silently drops that payload, so copying from tmux never reaches
|
|
72
|
+
// the system clipboard.
|
|
73
|
+
if (target && !target.includes('c')) return false
|
|
69
74
|
|
|
70
75
|
if (payload === '?') {
|
|
71
76
|
// Read request - send clipboard content back
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { memo } from 'react'
|
|
2
|
+
|
|
3
|
+
const e = window.translate
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shown over a dead terminal pane. Deliberately a DOM overlay and not
|
|
7
|
+
* `term.write()`: anything written into the buffer is captured by
|
|
8
|
+
* `socketMixin.getReloadState()` (serializeAddon + alternate-buffer snapshot)
|
|
9
|
+
* and replayed into the *next* session after a restart, and it would also land
|
|
10
|
+
* in the terminal log file and in copy selections.
|
|
11
|
+
*
|
|
12
|
+
* It only *names* shortcuts that already act on a dead session - reload (which
|
|
13
|
+
* restarts it) and close - so there is nothing new to learn, no key handling
|
|
14
|
+
* here, and no new locale strings. Both states share the component, so a user
|
|
15
|
+
* with auto reconnect on never sees a countdown and a notice at the same time.
|
|
16
|
+
*/
|
|
17
|
+
export default memo(function SessionStatusOverlay ({
|
|
18
|
+
stopped,
|
|
19
|
+
countdown,
|
|
20
|
+
reloadShortcut,
|
|
21
|
+
closeShortcut
|
|
22
|
+
}) {
|
|
23
|
+
const reconnecting = countdown !== null && countdown !== undefined
|
|
24
|
+
if (!reconnecting && !stopped) {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
return (
|
|
28
|
+
<div className='terminal-session-stopped pd1'>
|
|
29
|
+
{
|
|
30
|
+
reconnecting && (
|
|
31
|
+
<div className='pd1'>
|
|
32
|
+
{e('autoReconnectTerminal')}: {countdown}s
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
<div className='pd1'>
|
|
37
|
+
{e('reload')}: {reloadShortcut}
|
|
38
|
+
</div>
|
|
39
|
+
<div className='pd1'>
|
|
40
|
+
{e('close')}: {closeShortcut}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
})
|
|
@@ -79,13 +79,21 @@ function getShInlineIntegration () {
|
|
|
79
79
|
return [
|
|
80
80
|
'if [ -z "$ELECTERM_SHELL_INTEGRATION" ]',
|
|
81
81
|
'then export ELECTERM_SHELL_INTEGRATION=1',
|
|
82
|
-
'__e_esc() { printf \'%s\' "$1" | sed \'s/\\\\/\\\\\\\\/g; s/;/\\\\x3b/g\'; }',
|
|
83
82
|
// sh/dash/ash re-evaluate $(...) inside PS1 every time the prompt is
|
|
84
83
|
// shown, but only if the $() stays literal — so PS1 must be assigned
|
|
85
84
|
// single-quoted. printf interprets \033/\a, unlike PS1 backslash
|
|
86
85
|
// escapes which dash does not expand.
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
//
|
|
87
|
+
// The prompt is deliberately self-contained: it must never call a shell
|
|
88
|
+
// function. On OpenWrt /etc/profile does `export PS1=...`, so PS1 is an
|
|
89
|
+
// environment variable and every child shell inherits it — while shell
|
|
90
|
+
// functions are not inherited. A `PS1='$(__e_ps1)'...` prompt therefore
|
|
91
|
+
// dies with "ash: __e_ps1: not found" on every prompt of any nested shell
|
|
92
|
+
// (ash, su, login, ...), and the exported ELECTERM_SHELL_INTEGRATION
|
|
93
|
+
// guard stops the child from ever defining the missing function.
|
|
94
|
+
// Inlining keeps it working there too, and printf is an ash builtin so
|
|
95
|
+
// this is cheaper than the old function + sed pair.
|
|
96
|
+
'PS1=\'$(printf "\\033]633;P;Cwd=%s\\a\\033]633;A\\a" "$PWD")\'"${PS1:-# }"',
|
|
89
97
|
'fi'
|
|
90
98
|
].join('; ')
|
|
91
99
|
}
|