@electerm/electerm-react 3.1.26 → 3.3.8
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 +1 -3
- package/client/common/db.js +4 -2
- package/client/components/ai/ai-history.jsx +4 -4
- package/client/components/batch-op/batch-op-alert.jsx +42 -0
- package/client/components/batch-op/batch-op-editor.jsx +202 -0
- package/client/components/batch-op/batch-op-logs.jsx +53 -0
- package/client/components/batch-op/batch-op-runner.jsx +315 -0
- package/client/components/bookmark-form/ai-bookmark-form.jsx +2 -1
- package/client/components/bookmark-form/bookmark-from-history-modal.jsx +2 -1
- package/client/components/bookmark-form/common/bookmark-select.jsx +18 -2
- package/client/components/bookmark-form/common/connection-hopping-form.jsx +153 -0
- package/client/components/bookmark-form/common/connection-hopping.jsx +136 -129
- package/client/components/common/auto-check-update.jsx +31 -0
- package/client/components/common/notification.styl +1 -1
- package/client/components/file-transfer/conflict-resolve.jsx +3 -0
- package/client/components/footer/batch-input.jsx +10 -7
- package/client/components/main/error-wrapper.jsx +18 -7
- package/client/components/main/main.jsx +6 -7
- package/client/components/quick-commands/qm.styl +0 -2
- package/client/components/quick-commands/quick-commands-list-form.jsx +1 -1
- package/client/components/setting-panel/hotkey.jsx +9 -1
- package/client/components/setting-panel/list.jsx +0 -1
- package/client/components/setting-panel/list.styl +4 -0
- package/client/components/setting-panel/setting-modal.jsx +53 -47
- package/client/components/setting-sync/auto-sync.jsx +53 -0
- package/client/components/setting-sync/data-import.jsx +69 -8
- package/client/components/sftp/address-bar.jsx +7 -1
- package/client/components/shortcuts/shortcut-editor.jsx +4 -2
- package/client/components/sidebar/bookmark-select.jsx +3 -2
- package/client/components/sidebar/history-item.jsx +3 -1
- package/client/components/sidebar/history.jsx +1 -0
- package/client/components/sidebar/index.jsx +0 -9
- package/client/components/tabs/add-btn-menu.jsx +1 -1
- package/client/components/tabs/add-btn.jsx +9 -15
- package/client/components/tabs/quick-connect.jsx +6 -10
- package/client/components/terminal/attach-addon-custom.js +86 -0
- package/client/components/terminal/cmd-item.jsx +13 -3
- package/client/components/terminal/drop-file-modal.jsx +57 -0
- package/client/components/terminal/terminal-command-dropdown.jsx +91 -13
- package/client/components/terminal/terminal.jsx +107 -10
- package/client/components/terminal/terminal.styl +9 -0
- package/client/components/tree-list/tree-list-item.jsx +0 -1
- package/client/components/tree-list/tree-list.jsx +115 -10
- package/client/components/tree-list/tree-list.styl +3 -0
- package/client/components/tree-list/tree-search.jsx +9 -1
- package/client/components/vnc/vnc-session.jsx +2 -0
- package/client/components/widgets/widget-control.jsx +3 -0
- package/client/components/widgets/widget-form.jsx +6 -0
- package/client/components/widgets/widget-instance.jsx +26 -7
- package/client/css/includes/box.styl +3 -0
- package/client/store/common.js +0 -28
- package/client/store/init-state.js +2 -1
- package/client/store/load-data.js +6 -4
- package/client/store/mcp-handler.js +20 -2
- package/client/store/sync.js +25 -1
- package/client/store/tab.js +1 -1
- package/client/store/watch.js +10 -18
- package/client/store/widgets.js +54 -0
- package/client/views/index.pug +1 -2
- package/package.json +1 -1
- package/client/components/batch-op/batch-op.jsx +0 -694
|
@@ -19,6 +19,10 @@ export default class AttachAddonCustom {
|
|
|
19
19
|
this._lastInputTime = Date.now()
|
|
20
20
|
this._keepaliveTimer = null
|
|
21
21
|
this._keepaliveInterval = 3000
|
|
22
|
+
this._lastOutputLine = ''
|
|
23
|
+
this._passwordPromptDetected = false
|
|
24
|
+
this._pendingEchoCheck = null
|
|
25
|
+
this._echoCheckTimer = null
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
_initBase = async () => {
|
|
@@ -101,6 +105,52 @@ export default class AttachAddonCustom {
|
|
|
101
105
|
this.writeToTerminal(ev.data)
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
static passwordPromptPatterns = [
|
|
109
|
+
/password\s*[:\]>]\s*$/i,
|
|
110
|
+
/\[sudo\]\s*password\s+for\s+\S+\s*:\s*$/i,
|
|
111
|
+
/enter\s+passphrase/i,
|
|
112
|
+
/enter\s+password/i,
|
|
113
|
+
/密码[::]\s*$/,
|
|
114
|
+
/パスワード[::]\s*$/,
|
|
115
|
+
/mot de passe\s*[:\]]\s*$/i,
|
|
116
|
+
/passwort[:\]]\s*$/i,
|
|
117
|
+
/contraseña[:\]]\s*$/i
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
_checkPasswordPrompt = (str) => {
|
|
121
|
+
// Extract last non-empty line from the output
|
|
122
|
+
const lines = str.split(/\r?\n|\r/)
|
|
123
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
124
|
+
const line = lines[i].trim()
|
|
125
|
+
if (line) {
|
|
126
|
+
this._lastOutputLine = line
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return AttachAddonCustom.passwordPromptPatterns.some(
|
|
131
|
+
p => p.test(this._lastOutputLine)
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_onEchoCheckTimeout = () => {
|
|
136
|
+
// No echo received within timeout → confirms password mode
|
|
137
|
+
this._pendingEchoCheck = null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_handleEchoDetection = (str) => {
|
|
141
|
+
if (this._pendingEchoCheck) {
|
|
142
|
+
// Server sent data back while we were waiting → echo is ON → not password
|
|
143
|
+
if (str.includes(this._pendingEchoCheck.char)) {
|
|
144
|
+
this._passwordPromptDetected = false
|
|
145
|
+
clearTimeout(this._echoCheckTimer)
|
|
146
|
+
this._pendingEchoCheck = null
|
|
147
|
+
this._echoCheckTimer = null
|
|
148
|
+
// Cancel the password dropdown if it was shown
|
|
149
|
+
this.term?.parent?.onPasswordPromptCancelled?.()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
104
154
|
checkForShellIntegration = (str) => {
|
|
105
155
|
const ESC = String.fromCharCode(27)
|
|
106
156
|
return str.includes(ESC + ']633;')
|
|
@@ -152,6 +202,26 @@ export default class AttachAddonCustom {
|
|
|
152
202
|
return
|
|
153
203
|
}
|
|
154
204
|
|
|
205
|
+
// Password prompt detection on output
|
|
206
|
+
let str = data
|
|
207
|
+
if (typeof data !== 'string') {
|
|
208
|
+
try {
|
|
209
|
+
str = this.decoder.decode(
|
|
210
|
+
data instanceof ArrayBuffer ? data : new Uint8Array(data)
|
|
211
|
+
)
|
|
212
|
+
} catch (e) {
|
|
213
|
+
str = ''
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this._handleEchoDetection(str)
|
|
217
|
+
if (this._checkPasswordPrompt(str) && !this._passwordPromptDetected) {
|
|
218
|
+
this._passwordPromptDetected = true
|
|
219
|
+
// Show password dropdown immediately after terminal renders the prompt
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
this.term?.parent?.onPasswordPromptDetected?.()
|
|
222
|
+
}, 100)
|
|
223
|
+
}
|
|
224
|
+
|
|
155
225
|
if (typeof data === 'string') {
|
|
156
226
|
return term.write(data)
|
|
157
227
|
}
|
|
@@ -171,6 +241,20 @@ export default class AttachAddonCustom {
|
|
|
171
241
|
|
|
172
242
|
sendToServer = (data) => {
|
|
173
243
|
this._lastInputTime = Date.now()
|
|
244
|
+
// Start echo detection when password prompt is suspected
|
|
245
|
+
if (this._passwordPromptDetected && !this._pendingEchoCheck && data !== '\r' && data !== '\n') {
|
|
246
|
+
this._pendingEchoCheck = { char: data, time: Date.now() }
|
|
247
|
+
clearTimeout(this._echoCheckTimer)
|
|
248
|
+
this._echoCheckTimer = setTimeout(this._onEchoCheckTimeout, 200)
|
|
249
|
+
}
|
|
250
|
+
// Reset password state on Enter
|
|
251
|
+
if (data === '\r' || data === '\n') {
|
|
252
|
+
this._passwordPromptDetected = false
|
|
253
|
+
this._lastOutputLine = ''
|
|
254
|
+
this._pendingEchoCheck = null
|
|
255
|
+
clearTimeout(this._echoCheckTimer)
|
|
256
|
+
this._echoCheckTimer = null
|
|
257
|
+
}
|
|
174
258
|
this._sendData(data)
|
|
175
259
|
}
|
|
176
260
|
|
|
@@ -228,6 +312,8 @@ export default class AttachAddonCustom {
|
|
|
228
312
|
|
|
229
313
|
dispose = () => {
|
|
230
314
|
this._stopKeepalive()
|
|
315
|
+
clearTimeout(this._echoCheckTimer)
|
|
316
|
+
this._echoCheckTimer = null
|
|
231
317
|
this.term = null
|
|
232
318
|
this._disposables.forEach(d => d.dispose())
|
|
233
319
|
this._disposables.length = 0
|
|
@@ -11,11 +11,21 @@ const SuggestionItem = ({ item, onSelect, onDelete }) => {
|
|
|
11
11
|
onDelete(item)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
const isPassword = item.type === 'PW'
|
|
15
|
+
const displayText = isPassword
|
|
16
|
+
? '••••••••'
|
|
17
|
+
: item.command
|
|
18
|
+
|
|
14
19
|
return (
|
|
15
|
-
<div className='suggestion-item'>
|
|
16
|
-
<span className='suggestion-command'
|
|
17
|
-
{
|
|
20
|
+
<div className='suggestion-item' onClick={handleClick}>
|
|
21
|
+
<span className='suggestion-command'>
|
|
22
|
+
{displayText}
|
|
18
23
|
</span>
|
|
24
|
+
{item.hint && (
|
|
25
|
+
<span className='suggestion-hint'>
|
|
26
|
+
{item.hint}
|
|
27
|
+
</span>
|
|
28
|
+
)}
|
|
19
29
|
<span className='suggestion-type'>
|
|
20
30
|
{item.type}
|
|
21
31
|
</span>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Component } from 'react'
|
|
2
|
+
import Modal from '../common/modal'
|
|
3
|
+
|
|
4
|
+
const e = window.translate
|
|
5
|
+
|
|
6
|
+
export class DropFileModal extends Component {
|
|
7
|
+
render () {
|
|
8
|
+
const {
|
|
9
|
+
visible,
|
|
10
|
+
files,
|
|
11
|
+
onSelect,
|
|
12
|
+
onCancel
|
|
13
|
+
} = this.props
|
|
14
|
+
|
|
15
|
+
if (!visible) {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Modal
|
|
21
|
+
title='?'
|
|
22
|
+
open={visible}
|
|
23
|
+
onCancel={onCancel}
|
|
24
|
+
footer={
|
|
25
|
+
<div className='custom-modal-footer-buttons'>
|
|
26
|
+
<button
|
|
27
|
+
type='button'
|
|
28
|
+
className='custom-modal-ok-btn'
|
|
29
|
+
onClick={() => onSelect('trzUpload')}
|
|
30
|
+
>
|
|
31
|
+
trz
|
|
32
|
+
</button>
|
|
33
|
+
<button
|
|
34
|
+
type='button'
|
|
35
|
+
className='custom-modal-cancel-btn'
|
|
36
|
+
onClick={() => onSelect('rzUpload')}
|
|
37
|
+
>
|
|
38
|
+
rz
|
|
39
|
+
</button>
|
|
40
|
+
<button
|
|
41
|
+
type='button'
|
|
42
|
+
className='custom-modal-cancel-btn'
|
|
43
|
+
onClick={() => onSelect('inputPath')}
|
|
44
|
+
>
|
|
45
|
+
{e('inputOnly')}
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
}
|
|
49
|
+
width={400}
|
|
50
|
+
>
|
|
51
|
+
<p>{files?.map(f => f.path).join(', ')}</p>
|
|
52
|
+
</Modal>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default DropFileModal
|
|
@@ -16,7 +16,8 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
16
16
|
aiSuggestions: [],
|
|
17
17
|
cmdIsDescription: false,
|
|
18
18
|
reverse: false,
|
|
19
|
-
cmd: ''
|
|
19
|
+
cmd: '',
|
|
20
|
+
passwordMode: false
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
componentDidMount () {
|
|
@@ -95,6 +96,9 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
openSuggestions = (cursorPosition, cmd) => {
|
|
99
|
+
if (this.state.passwordMode) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
98
102
|
if (!this.state.showSuggestions) {
|
|
99
103
|
document.addEventListener('click', this.handleClickOutside)
|
|
100
104
|
document.addEventListener('keydown', this.handleKeyDown)
|
|
@@ -128,7 +132,45 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
128
132
|
showSuggestions: true,
|
|
129
133
|
cursorPosition: position,
|
|
130
134
|
cmd,
|
|
131
|
-
reverse
|
|
135
|
+
reverse,
|
|
136
|
+
passwordMode: false
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
openPasswordSuggestions = (cursorPosition) => {
|
|
141
|
+
if (!this.state.showSuggestions) {
|
|
142
|
+
document.addEventListener('click', this.handleClickOutside)
|
|
143
|
+
document.addEventListener('keydown', this.handleKeyDown)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const {
|
|
147
|
+
left,
|
|
148
|
+
top,
|
|
149
|
+
cellHeight
|
|
150
|
+
} = cursorPosition
|
|
151
|
+
const w = window.innerWidth
|
|
152
|
+
const h = window.innerHeight
|
|
153
|
+
|
|
154
|
+
const position = {}
|
|
155
|
+
const reverse = top > h / 2
|
|
156
|
+
|
|
157
|
+
if (left > w / 2) {
|
|
158
|
+
position.right = w - left
|
|
159
|
+
} else {
|
|
160
|
+
position.left = left
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (reverse) {
|
|
164
|
+
position.bottom = h - top + cellHeight * 1.5
|
|
165
|
+
} else {
|
|
166
|
+
position.top = top + cellHeight
|
|
167
|
+
}
|
|
168
|
+
this.setState({
|
|
169
|
+
showSuggestions: true,
|
|
170
|
+
cursorPosition: position,
|
|
171
|
+
cmd: '',
|
|
172
|
+
reverse,
|
|
173
|
+
passwordMode: true
|
|
132
174
|
})
|
|
133
175
|
}
|
|
134
176
|
|
|
@@ -146,7 +188,8 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
146
188
|
}
|
|
147
189
|
this.setState({
|
|
148
190
|
showSuggestions: false,
|
|
149
|
-
aiSuggestions: []
|
|
191
|
+
aiSuggestions: [],
|
|
192
|
+
passwordMode: false
|
|
150
193
|
})
|
|
151
194
|
}
|
|
152
195
|
|
|
@@ -172,18 +215,33 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
172
215
|
const terminal = refs.get('term-' + activeTabId)
|
|
173
216
|
if (!terminal) {
|
|
174
217
|
console.log('No active terminal found')
|
|
218
|
+
this.closeSuggestions()
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (item.type === 'PW') {
|
|
223
|
+
try {
|
|
224
|
+
// Send password + Enter directly, no backspace needed
|
|
225
|
+
terminal.attachAddon._sendData(item.command + '\r')
|
|
226
|
+
terminal.attachAddon._passwordPromptDetected = false
|
|
227
|
+
terminal.attachAddon._lastOutputLine = ''
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.error('Failed to send password:', e)
|
|
230
|
+
}
|
|
231
|
+
terminal.term.focus()
|
|
232
|
+
this.closeSuggestions()
|
|
175
233
|
return
|
|
176
234
|
}
|
|
177
235
|
|
|
178
|
-
// const titleElement = domEvent.target.closest('.ant-menu-title-content')
|
|
179
|
-
// const command = titleElement?.firstChild?.textContent
|
|
180
236
|
const { command } = item
|
|
181
|
-
|
|
237
|
+
// Read current input from buffer directly to avoid stale state
|
|
238
|
+
// (onData fires before echo, so this.state.cmd may lag behind)
|
|
239
|
+
const currentInput = terminal.getCurrentInput() || ''
|
|
182
240
|
let txt = ''
|
|
183
|
-
if (
|
|
184
|
-
txt = command.slice(
|
|
241
|
+
if (currentInput && command.startsWith(currentInput)) {
|
|
242
|
+
txt = command.slice(currentInput.length)
|
|
185
243
|
} else {
|
|
186
|
-
const pre = '\b'.repeat(
|
|
244
|
+
const pre = '\b'.repeat(currentInput.length)
|
|
187
245
|
txt = pre + command
|
|
188
246
|
}
|
|
189
247
|
terminal.attachAddon._sendData(txt)
|
|
@@ -209,6 +267,24 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
209
267
|
})
|
|
210
268
|
}
|
|
211
269
|
|
|
270
|
+
getPasswordSuggestions = () => {
|
|
271
|
+
const bookmarks = window.store.bookmarks || []
|
|
272
|
+
const seen = new Set()
|
|
273
|
+
const res = []
|
|
274
|
+
for (const b of bookmarks) {
|
|
275
|
+
if (b.password && !seen.has(b.password)) {
|
|
276
|
+
seen.add(b.password)
|
|
277
|
+
res.push({
|
|
278
|
+
id: uid(),
|
|
279
|
+
command: b.password,
|
|
280
|
+
type: 'PW',
|
|
281
|
+
hint: [b.username, b.host].filter(Boolean).join('@')
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return this.state.reverse ? res.reverse() : res
|
|
286
|
+
}
|
|
287
|
+
|
|
212
288
|
getSuggestions = () => {
|
|
213
289
|
const uniqueCommands = new Set()
|
|
214
290
|
const {
|
|
@@ -274,17 +350,19 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
274
350
|
}
|
|
275
351
|
|
|
276
352
|
render () {
|
|
277
|
-
const { showSuggestions, cursorPosition, reverse } = this.state
|
|
353
|
+
const { showSuggestions, cursorPosition, reverse, passwordMode } = this.state
|
|
278
354
|
if (!showSuggestions) {
|
|
279
355
|
return null
|
|
280
356
|
}
|
|
281
|
-
const suggestions =
|
|
357
|
+
const suggestions = passwordMode
|
|
358
|
+
? this.getPasswordSuggestions()
|
|
359
|
+
: this.getSuggestions()
|
|
282
360
|
const cls = classnames('terminal-suggestions-wrap', {
|
|
283
361
|
reverse
|
|
284
362
|
})
|
|
285
363
|
return (
|
|
286
364
|
<div className={cls} style={cursorPosition}>
|
|
287
|
-
{this.renderSticky('top')}
|
|
365
|
+
{!passwordMode && this.renderSticky('top')}
|
|
288
366
|
<div className='terminal-suggestions-list'>
|
|
289
367
|
{
|
|
290
368
|
suggestions.map(item => {
|
|
@@ -299,7 +377,7 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
299
377
|
})
|
|
300
378
|
}
|
|
301
379
|
</div>
|
|
302
|
-
{this.renderSticky('bottom')}
|
|
380
|
+
{!passwordMode && this.renderSticky('bottom')}
|
|
303
381
|
</div>
|
|
304
382
|
)
|
|
305
383
|
}
|
|
@@ -19,7 +19,8 @@ import {
|
|
|
19
19
|
isWin,
|
|
20
20
|
rendererTypes,
|
|
21
21
|
isMac,
|
|
22
|
-
isMacJs
|
|
22
|
+
isMacJs,
|
|
23
|
+
connectionMap
|
|
23
24
|
} from '../../common/constants.js'
|
|
24
25
|
import deepCopy from 'json-deep-copy'
|
|
25
26
|
import { readClipboardAsync, readClipboard, copy } from '../../common/clipboard.js'
|
|
@@ -27,6 +28,7 @@ import AttachAddon from './attach-addon-custom.js'
|
|
|
27
28
|
import getProxy from '../../common/get-proxy.js'
|
|
28
29
|
import { ZmodemClient } from './zmodem-client.js'
|
|
29
30
|
import { TrzszClient } from './trzsz-client.js'
|
|
31
|
+
import DropFileModal from './drop-file-modal.jsx'
|
|
30
32
|
import keyControlPressed from '../../common/key-control-pressed.js'
|
|
31
33
|
import NormalBuffer from './normal-buffer.jsx'
|
|
32
34
|
import { createTerm, resizeTerm } from './terminal-apis.js'
|
|
@@ -75,7 +77,9 @@ class Term extends Component {
|
|
|
75
77
|
searchResults: [],
|
|
76
78
|
matchIndex: -1,
|
|
77
79
|
totalLines: 0,
|
|
78
|
-
reconnectCountdown: null
|
|
80
|
+
reconnectCountdown: null,
|
|
81
|
+
dropFileModalVisible: false,
|
|
82
|
+
droppedFiles: []
|
|
79
83
|
}
|
|
80
84
|
this.id = `term-${this.props.tab.id}`
|
|
81
85
|
refs.add(this.id, this)
|
|
@@ -374,11 +378,10 @@ class Term extends Component {
|
|
|
374
378
|
}
|
|
375
379
|
|
|
376
380
|
runQuickCommand = (cmd, inputOnly = false) => {
|
|
377
|
-
this.term && this.attachAddon
|
|
378
|
-
cmd +
|
|
379
|
-
(
|
|
380
|
-
|
|
381
|
-
this.term.focus()
|
|
381
|
+
if (this.term && this.attachAddon) {
|
|
382
|
+
this.attachAddon._sendData(cmd + (inputOnly ? '' : '\r'))
|
|
383
|
+
this.term.focus()
|
|
384
|
+
}
|
|
382
385
|
}
|
|
383
386
|
|
|
384
387
|
cd = (p) => {
|
|
@@ -392,9 +395,9 @@ class Term extends Component {
|
|
|
392
395
|
const dt = e.dataTransfer
|
|
393
396
|
const fromFile = dt.getData('fromFile')
|
|
394
397
|
const notSafeMsg = 'File name contains unsafe characters'
|
|
398
|
+
const isSshTerminal = this.props.tab.type === connectionMap.ssh
|
|
395
399
|
|
|
396
400
|
if (fromFile) {
|
|
397
|
-
// Handle SFTP file drop
|
|
398
401
|
try {
|
|
399
402
|
const fileData = JSON.parse(fromFile)
|
|
400
403
|
const filePath = resolve(fileData.path, fileData.name)
|
|
@@ -402,6 +405,13 @@ class Term extends Component {
|
|
|
402
405
|
message.error(notSafeMsg)
|
|
403
406
|
return
|
|
404
407
|
}
|
|
408
|
+
if (isSshTerminal) {
|
|
409
|
+
this.setState({
|
|
410
|
+
dropFileModalVisible: true,
|
|
411
|
+
droppedFiles: [{ path: filePath, isRemote: true }]
|
|
412
|
+
})
|
|
413
|
+
return
|
|
414
|
+
}
|
|
405
415
|
this.attachAddon._sendData(`"${filePath}" `)
|
|
406
416
|
return
|
|
407
417
|
} catch (e) {
|
|
@@ -409,24 +419,78 @@ class Term extends Component {
|
|
|
409
419
|
}
|
|
410
420
|
}
|
|
411
421
|
|
|
412
|
-
// Handle regular file drop
|
|
413
422
|
const files = dt.files
|
|
414
423
|
if (files && files.length) {
|
|
415
424
|
const arr = Array.from(files)
|
|
416
425
|
const filePaths = arr.map(f => getFilePath(f))
|
|
417
426
|
|
|
418
|
-
// Check each file path individually
|
|
419
427
|
const hasUnsafeFilename = filePaths.some(path => isUnsafeFilename(path))
|
|
420
428
|
if (hasUnsafeFilename) {
|
|
421
429
|
message.error(notSafeMsg)
|
|
422
430
|
return
|
|
423
431
|
}
|
|
424
432
|
|
|
433
|
+
if (isSshTerminal) {
|
|
434
|
+
this.setState({
|
|
435
|
+
dropFileModalVisible: true,
|
|
436
|
+
droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
|
|
437
|
+
})
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
|
|
425
441
|
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
426
442
|
this.attachAddon._sendData(filesAll)
|
|
427
443
|
}
|
|
428
444
|
}
|
|
429
445
|
|
|
446
|
+
handleDropFileModalCancel = () => {
|
|
447
|
+
this.setState({
|
|
448
|
+
dropFileModalVisible: false,
|
|
449
|
+
droppedFiles: []
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
handleDropFileAction = (action) => {
|
|
454
|
+
const { droppedFiles } = this.state
|
|
455
|
+
if (!droppedFiles || !droppedFiles.length) {
|
|
456
|
+
this.handleDropFileModalCancel()
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const filePaths = droppedFiles.map(f => f.path)
|
|
461
|
+
|
|
462
|
+
switch (action) {
|
|
463
|
+
case 'trzUpload': {
|
|
464
|
+
if (this.trzszClient && this.trzszClient.isActive) {
|
|
465
|
+
message.warning('A transfer is already in progress')
|
|
466
|
+
this.handleDropFileModalCancel()
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
window._apiControlSelectFile = filePaths
|
|
470
|
+
this.attachAddon._sendData('trz\r')
|
|
471
|
+
break
|
|
472
|
+
}
|
|
473
|
+
case 'rzUpload': {
|
|
474
|
+
if (this.zmodemClient && this.zmodemClient.isActive) {
|
|
475
|
+
message.warning('A transfer is already in progress')
|
|
476
|
+
this.handleDropFileModalCancel()
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
window._apiControlSelectFile = filePaths
|
|
480
|
+
this.attachAddon._sendData('rz\r')
|
|
481
|
+
break
|
|
482
|
+
}
|
|
483
|
+
case 'inputPath':
|
|
484
|
+
default: {
|
|
485
|
+
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
486
|
+
this.attachAddon._sendData(filesAll)
|
|
487
|
+
break
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
this.handleDropFileModalCancel()
|
|
492
|
+
}
|
|
493
|
+
|
|
430
494
|
onSelection = () => {
|
|
431
495
|
if (
|
|
432
496
|
!this.props.config.copyWhenSelect ||
|
|
@@ -771,8 +835,35 @@ class Term extends Component {
|
|
|
771
835
|
}
|
|
772
836
|
}
|
|
773
837
|
|
|
838
|
+
onPasswordPromptDetected = () => {
|
|
839
|
+
if (!this.props.config.showCmdSuggestions) {
|
|
840
|
+
return
|
|
841
|
+
}
|
|
842
|
+
const cursorPos = this.getCursorPosition()
|
|
843
|
+
if (cursorPos) {
|
|
844
|
+
refsStatic
|
|
845
|
+
.get('terminal-suggestions')
|
|
846
|
+
?.openPasswordSuggestions(cursorPos)
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
onPasswordPromptCancelled = () => {
|
|
851
|
+
const suggestions = refsStatic.get('terminal-suggestions')
|
|
852
|
+
if (suggestions?.state?.passwordMode) {
|
|
853
|
+
suggestions.closeSuggestions()
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
774
857
|
onData = (d) => {
|
|
775
858
|
this.handleInputEvent(d)
|
|
859
|
+
// Skip normal suggestion logic when in password mode
|
|
860
|
+
const suggestions = refsStatic.get('terminal-suggestions')
|
|
861
|
+
if (suggestions?.state?.passwordMode) {
|
|
862
|
+
if (d === '\r' || d === '\n') {
|
|
863
|
+
this.closeSuggestions()
|
|
864
|
+
}
|
|
865
|
+
return
|
|
866
|
+
}
|
|
776
867
|
if (this.props.config.showCmdSuggestions) {
|
|
777
868
|
const data = this.getCurrentInput()
|
|
778
869
|
if (data && d !== '\r' && d !== '\n') {
|
|
@@ -1459,6 +1550,12 @@ class Term extends Component {
|
|
|
1459
1550
|
countdown={this.state.reconnectCountdown}
|
|
1460
1551
|
onCancel={this.handleCancelAutoReconnect}
|
|
1461
1552
|
/>
|
|
1553
|
+
<DropFileModal
|
|
1554
|
+
visible={this.state.dropFileModalVisible}
|
|
1555
|
+
files={this.state.droppedFiles}
|
|
1556
|
+
onSelect={this.handleDropFileAction}
|
|
1557
|
+
onCancel={this.handleDropFileModalCancel}
|
|
1558
|
+
/>
|
|
1462
1559
|
{spin}
|
|
1463
1560
|
</div>
|
|
1464
1561
|
</Dropdown>
|
|
@@ -127,6 +127,15 @@
|
|
|
127
127
|
&:hover
|
|
128
128
|
color var(--success)
|
|
129
129
|
|
|
130
|
+
.suggestion-hint
|
|
131
|
+
margin-left 5px
|
|
132
|
+
font-size 0.8em
|
|
133
|
+
color var(--text-light, #888)
|
|
134
|
+
white-space nowrap
|
|
135
|
+
overflow hidden
|
|
136
|
+
text-overflow ellipsis
|
|
137
|
+
max-width 150px
|
|
138
|
+
|
|
130
139
|
.suggestion-delete
|
|
131
140
|
margin-left 5px
|
|
132
141
|
visibility hidden
|