@electerm/electerm-react 5.3.26 → 5.5.9
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/README.md +246 -12
- package/client/common/choose-save-folder.js +21 -2
- package/client/common/constants.js +20 -6
- package/client/common/create-title.jsx +6 -0
- package/client/common/default-setting.js +15 -0
- package/client/common/init-setting-item.js +5 -0
- package/client/common/platform.js +24 -0
- package/client/components/ai/agent-tool-call-card.jsx +10 -2
- package/client/components/ai/agent.js +4 -1
- package/client/components/ai/ai-attachments.js +87 -0
- package/client/components/ai/ai-chat-empty.jsx +43 -0
- package/client/components/ai/ai-chat-entry.jsx +13 -4
- package/client/components/ai/ai-chat-history-item.jsx +28 -4
- package/client/components/ai/ai-chat-history.jsx +2 -1
- package/client/components/ai/ai-chat.jsx +104 -13
- package/client/components/ai/ai-config-props.js +4 -130
- package/client/components/ai/ai-config.jsx +135 -15
- package/client/components/ai/ai-presets.js +173 -0
- package/client/components/ai/ai.styl +83 -0
- package/client/components/bg/css-overwrite.jsx +11 -0
- package/client/components/bookmark-form/bookmark-schema.js +4 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/config/common-fields.js +12 -0
- package/client/components/bookmark-form/config/local.js +3 -2
- package/client/components/bookmark-form/config/serial.js +3 -2
- package/client/components/bookmark-form/config/ssh.js +2 -1
- package/client/components/bookmark-form/config/telnet.js +3 -2
- package/client/components/common/item-filter.jsx +76 -0
- package/client/components/common/item-filter.styl +54 -0
- package/client/components/common/lazy-boundary.jsx +34 -0
- package/client/components/common/opacity.jsx +3 -1
- package/client/components/footer/cmd-history.jsx +132 -45
- package/client/components/footer/cmd-history.styl +42 -4
- package/client/components/footer/footer-entry.jsx +35 -5
- package/client/components/footer/footer.styl +8 -1
- package/client/components/footer/multi-tab-run-modal.jsx +83 -0
- package/client/components/footer/tab-select.jsx +34 -17
- package/client/components/layout/layout.jsx +23 -31
- package/client/components/layout/pixed.js +3 -0
- package/client/components/main/main.jsx +55 -28
- package/client/components/main/term-fullscreen.styl +9 -3
- package/client/components/quick-commands/qm.styl +3 -2
- package/client/components/quick-commands/quick-command-create-modal.jsx +60 -0
- package/client/components/quick-commands/quick-commands-form-elem.jsx +10 -4
- package/client/components/rdp/file-transfer.js +4 -4
- package/client/components/remote-monitor/monitor-details.jsx +458 -0
- package/client/components/remote-monitor/monitor-details.styl +87 -0
- package/client/components/remote-monitor/monitor-model.js +764 -0
- package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
- package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
- package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
- package/client/components/remote-monitor/session-monitor.js +361 -0
- package/client/components/remote-monitor/use-monitor-details.js +10 -0
- package/client/components/remote-monitor/visibility.js +18 -0
- package/client/components/session/session-control.jsx +29 -2
- package/client/components/session/session.jsx +40 -18
- package/client/components/session/sessions.jsx +5 -2
- package/client/components/setting-panel/setting-common.jsx +25 -56
- package/client/components/setting-panel/setting-modal.jsx +57 -42
- package/client/components/setting-panel/setting-terminal.jsx +8 -2
- package/client/components/setting-panel/setting.styl +1 -1
- package/client/components/setting-panel/tab-triggers.jsx +71 -0
- package/client/components/sftp/file-compare-modal.jsx +6 -3
- package/client/components/sftp/file-item.jsx +5 -3
- package/client/components/sftp/owner-list.js +11 -2
- package/client/components/sftp/sftp-entry.jsx +4 -0
- package/client/components/side-panel-r/side-panel-r.jsx +2 -2
- package/client/components/sidebar/side-panel.jsx +1 -1
- package/client/components/sys-menu/menu-btn.jsx +30 -1
- package/client/components/tabs/index.jsx +2 -2
- package/client/components/tabs/tab-title.jsx +3 -2
- package/client/components/tabs/tab.jsx +5 -1
- package/client/components/terminal/attach-addon-custom.js +295 -49
- package/client/components/terminal/automation/index.js +64 -0
- package/client/components/terminal/automation/keys.js +49 -0
- package/client/components/terminal/automation/stream-matcher.js +82 -0
- package/client/components/terminal/automation/strip-ansi.js +19 -0
- package/client/components/terminal/automation/trigger-engine.js +233 -0
- package/client/components/terminal/automation/trigger-presets.js +110 -0
- package/client/components/terminal/command-tracker-addon.js +31 -1
- package/client/components/terminal/mixins/index.js +27 -0
- package/client/components/terminal/mixins/term-attach.js +123 -0
- package/client/components/terminal/mixins/term-config.jsx +84 -0
- package/client/components/terminal/mixins/term-content.js +126 -0
- package/client/components/terminal/mixins/term-context-menu.jsx +323 -0
- package/client/components/terminal/mixins/term-file-drop.js +149 -0
- package/client/components/terminal/mixins/term-init.js +122 -0
- package/client/components/terminal/mixins/term-log.jsx +96 -0
- package/client/components/terminal/mixins/term-resize.js +87 -0
- package/client/components/terminal/mixins/term-socket.js +433 -0
- package/client/components/terminal/mixins/term-suggestions.js +224 -0
- package/client/components/terminal/mixins/term-theme.js +190 -0
- package/client/components/terminal/mixins/term-touch.js +523 -0
- package/client/components/terminal/shell-detect.js +37 -0
- package/client/components/terminal/shell.js +0 -28
- package/client/components/terminal/shortcut-bar-defs.js +30 -4
- package/client/components/terminal/shortcut-bar-edit.jsx +40 -9
- package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
- package/client/components/terminal/shortcut-bar.jsx +17 -10
- package/client/components/terminal/shortcut-bar.styl +4 -0
- package/client/components/terminal/ssh-reload-state.js +96 -0
- package/client/components/terminal/startup-queue.js +433 -0
- package/client/components/terminal/terminal-interactive.jsx +32 -0
- package/client/components/terminal/terminal-select-text.jsx +69 -0
- package/client/components/terminal/terminal.jsx +227 -2064
- package/client/components/terminal/terminal.styl +61 -0
- package/client/components/terminal/transfer-client-base.js +5 -29
- package/client/components/terminal/trzsz-client.js +1 -3
- package/client/components/terminal/xmodem-client.js +1 -3
- package/client/components/terminal/xterm-loader.js +7 -0
- package/client/components/terminal/zmodem-client.js +1 -3
- package/client/components/terminal-info/base.jsx +18 -70
- package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
- package/client/components/terminal-info/terminal-info.jsx +50 -59
- package/client/components/terminal-info/terminal-info.styl +6 -49
- package/client/components/text-editor/open-text-editor.js +30 -0
- package/client/components/text-editor/text-editor-entry.jsx +17 -6
- package/client/components/text-editor/text-editor.jsx +14 -0
- package/client/components/triggers/trigger-editor.jsx +246 -0
- package/client/components/triggers/trigger-form.jsx +200 -0
- package/client/components/triggers/trigger-session-modal.jsx +199 -0
- package/client/components/triggers/trigger-setting-list.jsx +89 -0
- package/client/components/triggers/trigger-transport.jsx +60 -0
- package/client/components/triggers/triggers-field.jsx +19 -0
- package/client/components/widgets/widgets-list.jsx +11 -1
- package/client/css/mobile.styl +29 -8
- package/client/store/common.js +12 -1
- package/client/store/init-state.js +7 -0
- package/client/store/mcp-handler.js +158 -30
- package/client/store/setting.js +9 -0
- package/client/store/store.js +2 -0
- package/client/store/sync.js +2 -0
- package/client/store/tab.js +42 -3
- package/client/store/trigger.js +118 -0
- package/client/store/watch.js +13 -5
- package/client/views/index.pug +3 -0
- package/package.json +1 -1
- package/client/components/terminal-info/activity.jsx +0 -57
- package/client/components/terminal-info/data-cols-parser.jsx +0 -54
- package/client/components/terminal-info/disk.jsx +0 -45
- package/client/components/terminal-info/format-disks.js +0 -45
- package/client/components/terminal-info/network.jsx +0 -121
- package/client/components/terminal-info/resource.jsx +0 -99
- package/client/components/terminal-info/run-cmd.jsx +0 -263
- package/client/components/terminal-info/sys-info.jsx +0 -56
- package/client/components/terminal-info/up.jsx +0 -17
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading back what is on screen: search (xterm search addon + the result
|
|
3
|
+
* bar), the normal-buffer overlay used when an app owns the alt buffer, and
|
|
4
|
+
* plain text export of the current buffer.
|
|
5
|
+
*/
|
|
6
|
+
export const contentMixin = {
|
|
7
|
+
toggleSearch () {
|
|
8
|
+
window.store.toggleTerminalSearch()
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
onSearchResultsChange ({ resultIndex, resultCount }) {
|
|
12
|
+
window.store.storeAssign({
|
|
13
|
+
termSearchMatchCount: resultCount,
|
|
14
|
+
termSearchMatchIndex: resultIndex
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
this.updateSearchResults(resultIndex)
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
updateSearchResults (resultIndex) {
|
|
21
|
+
const matches = this.searchAddon._resultTracker.searchResults.map((result, i) => {
|
|
22
|
+
return result.row
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
this.setState({
|
|
26
|
+
searchResults: matches,
|
|
27
|
+
matchIndex: resultIndex,
|
|
28
|
+
totalLines: this.term.buffer.active.length
|
|
29
|
+
})
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
searchPrev (searchInput, options) {
|
|
33
|
+
this.searchAddon.findPrevious(
|
|
34
|
+
searchInput, options
|
|
35
|
+
)
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
searchNext (searchInput, options) {
|
|
39
|
+
this.searchAddon.findNext(
|
|
40
|
+
searchInput, options
|
|
41
|
+
)
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
openNormalBuffer () {
|
|
45
|
+
const normal = this.term.buffer.normal
|
|
46
|
+
const len = normal.length
|
|
47
|
+
const lines = new Array(len).fill('').map((x, i) => {
|
|
48
|
+
return normal.getLine(i).translateToString(false)
|
|
49
|
+
})
|
|
50
|
+
this.setState({
|
|
51
|
+
lines
|
|
52
|
+
})
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
closeNormalBuffer () {
|
|
56
|
+
this.setState({
|
|
57
|
+
lines: []
|
|
58
|
+
})
|
|
59
|
+
this.term.focus()
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
onBufferChange (buf) {
|
|
63
|
+
this.bufferMode = buf.type
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
getTerminalBufferText () {
|
|
67
|
+
const { addTimeStampToTermLog } = this.state
|
|
68
|
+
const buffer = this.term.buffer.active
|
|
69
|
+
const len = buffer.length
|
|
70
|
+
const rawLines = []
|
|
71
|
+
for (let i = 0; i < len; i++) {
|
|
72
|
+
const line = buffer.getLine(i)
|
|
73
|
+
rawLines.push(line ? line.translateToString(false) : '')
|
|
74
|
+
}
|
|
75
|
+
// trim trailing blank lines before applying timestamps
|
|
76
|
+
while (rawLines.length && !rawLines[rawLines.length - 1].trim()) {
|
|
77
|
+
rawLines.pop()
|
|
78
|
+
}
|
|
79
|
+
if (!addTimeStampToTermLog) {
|
|
80
|
+
return rawLines.join('\n')
|
|
81
|
+
}
|
|
82
|
+
return rawLines.map(text => {
|
|
83
|
+
const now = new Date()
|
|
84
|
+
const ts = `[${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}] `
|
|
85
|
+
return ts + text
|
|
86
|
+
}).join('\n')
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The whole buffer as clean plain text, for the touch select-text overlay.
|
|
91
|
+
*
|
|
92
|
+
* Differs from getTerminalBufferText() on purpose: that one is for log files
|
|
93
|
+
* and keeps xterm's fixed-width padding (`translateToString(false)`), which
|
|
94
|
+
* would trail a screen-full of spaces into a textarea and make every line
|
|
95
|
+
* look wrapped. Here each row is right-trimmed, rows xterm reports as
|
|
96
|
+
* `isWrapped` are glued back so a line the shell printed as one line is
|
|
97
|
+
* copied as one line, and the blank rows scrollback starts out with are
|
|
98
|
+
* dropped so the panel opens on real output.
|
|
99
|
+
*/
|
|
100
|
+
getSelectableBufferText () {
|
|
101
|
+
const buffer = this.term.buffer.active
|
|
102
|
+
const len = buffer.length
|
|
103
|
+
const lines = []
|
|
104
|
+
for (let i = 0; i < len; i++) {
|
|
105
|
+
const line = buffer.getLine(i)
|
|
106
|
+
if (!line) {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
const text = line.translateToString(true)
|
|
110
|
+
if (line.isWrapped && lines.length) {
|
|
111
|
+
lines[lines.length - 1] += text
|
|
112
|
+
} else {
|
|
113
|
+
lines.push(text)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
let start = 0
|
|
117
|
+
let end = lines.length
|
|
118
|
+
while (start < end && !lines[start].trim()) {
|
|
119
|
+
start++
|
|
120
|
+
}
|
|
121
|
+
while (end > start && !lines[end - 1].trim()) {
|
|
122
|
+
end--
|
|
123
|
+
}
|
|
124
|
+
return lines.slice(start, end).join('\n')
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import Modal from '../../common/modal'
|
|
2
|
+
import { readClipboardAsync, readClipboard, copy } from '../../../common/clipboard.js'
|
|
3
|
+
import { isWin, isMac, isMacJs, connectionMap } from '../../../common/constants.js'
|
|
4
|
+
import iconsMap from '../../sys-menu/icons-map.jsx'
|
|
5
|
+
import { refsStatic } from '../../common/ref.js'
|
|
6
|
+
import AIIcon from '../../icons/ai-icon.jsx'
|
|
7
|
+
import { isAIDisabled } from '../../../common/ai-feature.js'
|
|
8
|
+
|
|
9
|
+
const e = window.translate
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The right-click (and long-press) menu plus the clipboard actions behind it.
|
|
13
|
+
* Menu item `key`s are method names, so `onContextMenu` just dispatches.
|
|
14
|
+
*/
|
|
15
|
+
export const contextMenuMixin = {
|
|
16
|
+
renderContextMenu () {
|
|
17
|
+
const { hasSelection, recording } = this.state
|
|
18
|
+
const copyed = true
|
|
19
|
+
const copyShortcut = this.getShortcut('terminal_copy')
|
|
20
|
+
const pasteShortcut = this.getShortcut('terminal_paste')
|
|
21
|
+
const clearShortcut = this.getShortcut('terminal_clear')
|
|
22
|
+
const searchShortcut = this.getShortcut('terminal_search')
|
|
23
|
+
const selectAllShortcut = isMacJs ? 'meta+a' : 'ctrl+shift+a'
|
|
24
|
+
const isSerial = this.props.tab?.type === connectionMap.serial
|
|
25
|
+
const items = [
|
|
26
|
+
{
|
|
27
|
+
key: 'onCopy',
|
|
28
|
+
icon: <iconsMap.CopyOutlined />,
|
|
29
|
+
label: e('copy'),
|
|
30
|
+
disabled: !hasSelection,
|
|
31
|
+
extra: copyShortcut
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: 'onPaste',
|
|
35
|
+
icon: <iconsMap.SwitcherOutlined />,
|
|
36
|
+
label: e('paste'),
|
|
37
|
+
disabled: !copyed,
|
|
38
|
+
extra: pasteShortcut
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: 'onPasteSelected',
|
|
42
|
+
icon: <iconsMap.SwitcherOutlined />,
|
|
43
|
+
label: e('pasteSelected'),
|
|
44
|
+
disabled: !hasSelection
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'onSelectAll',
|
|
48
|
+
icon: <iconsMap.CheckSquareOutlined />,
|
|
49
|
+
label: e('selectall'),
|
|
50
|
+
extra: selectAllShortcut
|
|
51
|
+
},
|
|
52
|
+
// Touch only: a selection dragged out on the canvas can only ever be an
|
|
53
|
+
// approximation of a native one, so offer the overlay where the OS does
|
|
54
|
+
// the selecting (see term-touch.js / terminal-select-text.jsx).
|
|
55
|
+
...(
|
|
56
|
+
this.isTouchMode()
|
|
57
|
+
? [{
|
|
58
|
+
key: 'onSelectTextMode',
|
|
59
|
+
icon: <iconsMap.SelectOutlined />,
|
|
60
|
+
label: e('selectText')
|
|
61
|
+
}]
|
|
62
|
+
: []
|
|
63
|
+
),
|
|
64
|
+
...(
|
|
65
|
+
isAIDisabled()
|
|
66
|
+
? []
|
|
67
|
+
: [{
|
|
68
|
+
key: 'explainWithAi',
|
|
69
|
+
icon: <AIIcon />,
|
|
70
|
+
label: e('explainWithAi'),
|
|
71
|
+
disabled: !hasSelection
|
|
72
|
+
}]
|
|
73
|
+
),
|
|
74
|
+
{
|
|
75
|
+
key: 'onClear',
|
|
76
|
+
icon: <iconsMap.ReloadOutlined />,
|
|
77
|
+
label: e('clear'),
|
|
78
|
+
extra: clearShortcut
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: 'toggleSearch',
|
|
82
|
+
icon: <iconsMap.SearchOutlined />,
|
|
83
|
+
label: e('search'),
|
|
84
|
+
extra: searchShortcut
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: 'onSaveTerminalLog',
|
|
88
|
+
icon: <iconsMap.SaveOutlined />,
|
|
89
|
+
label: e('saveTerminalLogToFile')
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: recording ? 'onStopRecord' : 'onRecord',
|
|
93
|
+
icon: recording ? <iconsMap.StopOutlined /> : <iconsMap.PlayCircleFilled />,
|
|
94
|
+
label: e(recording ? 'stopRecord' : 'record')
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
if (isSerial) {
|
|
98
|
+
items.push(
|
|
99
|
+
{
|
|
100
|
+
type: 'divider'
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
key: 'onXmodemSend',
|
|
104
|
+
icon: <iconsMap.CloudUploadOutlined />,
|
|
105
|
+
label: 'XMODEM Send'
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: 'onXmodemReceive',
|
|
109
|
+
icon: <iconsMap.CloudDownloadOutlined />,
|
|
110
|
+
label: 'XMODEM Receive'
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
return items
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
onContextMenu ({ key }) {
|
|
118
|
+
this[key]()
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
onContextMenuInner (e) {
|
|
122
|
+
e.preventDefault()
|
|
123
|
+
if (this.state.loading) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
if (this.props.config.pasteWhenContextMenu) {
|
|
127
|
+
return this.onPaste()
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
onSelection () {
|
|
132
|
+
if (
|
|
133
|
+
!this.props.config.copyWhenSelect ||
|
|
134
|
+
window.store.onOperation
|
|
135
|
+
) {
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
// While a touch drag is still building the selection we copy once on
|
|
139
|
+
// release instead — otherwise every cell the finger crosses would write
|
|
140
|
+
// the clipboard and raise a toast.
|
|
141
|
+
if (this.dragSelect) {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
this.copySelectionToClipboard()
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
copySelectionToClipboard () {
|
|
148
|
+
this.copyToClipboard(this.term.getSelection())
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Shared clipboard write. Lives here rather than in term-touch.js so that
|
|
153
|
+
* the touch module stays free of imports (it is loaded directly by a
|
|
154
|
+
* node:test unit spec), and so every copy raises the same `copied` toast.
|
|
155
|
+
*/
|
|
156
|
+
copyToClipboard (txt) {
|
|
157
|
+
if (txt) {
|
|
158
|
+
copy(txt)
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
tryInsertSelected () {
|
|
163
|
+
const txt = this.term.getSelection()
|
|
164
|
+
if (txt) {
|
|
165
|
+
this.attachAddon._sendData(txt)
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
onCopy () {
|
|
170
|
+
const selected = this.term.getSelection()
|
|
171
|
+
copy(selected)
|
|
172
|
+
this.term.focus()
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
onSelectAll () {
|
|
176
|
+
this.term.selectAll()
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
pasteTextTooLong () {
|
|
180
|
+
if (this.props.config.disableConfirmForLargeClipboardContent) {
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
if (window.et.isWebApp) {
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
const text = readClipboard()
|
|
187
|
+
return text.length > 500
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
askUserConfirm () {
|
|
191
|
+
Modal.confirm({
|
|
192
|
+
title: e('paste'),
|
|
193
|
+
content: (
|
|
194
|
+
<div>
|
|
195
|
+
<p>{e('paste')}:</p>
|
|
196
|
+
<div className='paste-text'>
|
|
197
|
+
<pre>
|
|
198
|
+
<code>{readClipboard()}</code>
|
|
199
|
+
</pre>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
),
|
|
203
|
+
okText: e('ok'),
|
|
204
|
+
cancelText: e('cancel'),
|
|
205
|
+
onOk: () => this.onPaste(true)
|
|
206
|
+
})
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
async onPaste (skipTextLengthCheck) {
|
|
210
|
+
let selected = await readClipboardAsync()
|
|
211
|
+
if (
|
|
212
|
+
!skipTextLengthCheck &&
|
|
213
|
+
!this.props.config.disableConfirmForLargeClipboardContent &&
|
|
214
|
+
selected.length > 500
|
|
215
|
+
) {
|
|
216
|
+
return this.askUserConfirm()
|
|
217
|
+
}
|
|
218
|
+
if (isWin && this.isRemote()) {
|
|
219
|
+
selected = selected.replace(/\r\n/g, '\n')
|
|
220
|
+
}
|
|
221
|
+
this.term.paste(selected || '')
|
|
222
|
+
this.term.focus()
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
onPasteSelected () {
|
|
226
|
+
const selected = this.term.getSelection()
|
|
227
|
+
this.term.paste(selected || '')
|
|
228
|
+
this.term.focus()
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
onClear () {
|
|
232
|
+
const shouldClear = this.searchAddon &&
|
|
233
|
+
window.store.termSearchOpen &&
|
|
234
|
+
window.store.termSearch
|
|
235
|
+
if (
|
|
236
|
+
shouldClear
|
|
237
|
+
) {
|
|
238
|
+
this.searchAddon.clearDecorations()
|
|
239
|
+
}
|
|
240
|
+
this.term.clear()
|
|
241
|
+
this.term.focus()
|
|
242
|
+
if (shouldClear) {
|
|
243
|
+
this.searchAddon._lineCache.clear()
|
|
244
|
+
this.timers.clearSearchTimer = setTimeout(() => {
|
|
245
|
+
refsStatic.get('term-search')?.next()
|
|
246
|
+
}, 100)
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
onXmodemSend () {
|
|
251
|
+
if (this.xmodemClient) {
|
|
252
|
+
this.xmodemClient.initiateSend()
|
|
253
|
+
}
|
|
254
|
+
this.term.focus()
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
onXmodemReceive () {
|
|
258
|
+
if (this.xmodemClient) {
|
|
259
|
+
this.xmodemClient.initiateReceive()
|
|
260
|
+
}
|
|
261
|
+
this.term.focus()
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
explainWithAi () {
|
|
265
|
+
window.store.explainWithAi(
|
|
266
|
+
this.term.getSelection()
|
|
267
|
+
)
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
// ---- shortcut handlers, wired up by shortcutExtend ----
|
|
271
|
+
clearShortcut (e) {
|
|
272
|
+
e.stopPropagation()
|
|
273
|
+
this.onClear()
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
// selectAllShortcut = (e) => {
|
|
277
|
+
// e.stopPropagation()
|
|
278
|
+
// this.term.selectAll()
|
|
279
|
+
// }
|
|
280
|
+
|
|
281
|
+
copyShortcut (e) {
|
|
282
|
+
const sel = this.term.getSelection()
|
|
283
|
+
if (sel) {
|
|
284
|
+
e.stopPropagation()
|
|
285
|
+
this.copySelectionToClipboard()
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
pasteSelectedShortcut (e) {
|
|
291
|
+
e.stopPropagation()
|
|
292
|
+
this.tryInsertSelected()
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
searchShortcut (e) {
|
|
296
|
+
e.stopPropagation()
|
|
297
|
+
this.toggleSearch()
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
pasteShortcut (e) {
|
|
301
|
+
if (this.pasteTextTooLong()) {
|
|
302
|
+
this.askUserConfirm()
|
|
303
|
+
e.preventDefault()
|
|
304
|
+
e.stopPropagation()
|
|
305
|
+
return false
|
|
306
|
+
}
|
|
307
|
+
if (isMac) {
|
|
308
|
+
return true
|
|
309
|
+
}
|
|
310
|
+
if (!this.isRemote()) {
|
|
311
|
+
return true
|
|
312
|
+
}
|
|
313
|
+
if (this.term.buffer.active.type !== 'alternate') {
|
|
314
|
+
return false
|
|
315
|
+
}
|
|
316
|
+
return true
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
showNormalBufferShortcut (e) {
|
|
320
|
+
e.stopPropagation()
|
|
321
|
+
this.openNormalBuffer()
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import resolve from '../../../common/resolve.js'
|
|
2
|
+
import message from '../../common/message'
|
|
3
|
+
import { getFilePath, isUnsafeFilename } from '../../../common/file-drop-utils.js'
|
|
4
|
+
import { connectionMap } from '../../../common/constants.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Files dropped onto the terminal: either quoted paths typed into the shell,
|
|
8
|
+
* or an upload through trzsz / zmodem / xmodem for ssh and serial sessions.
|
|
9
|
+
*/
|
|
10
|
+
export const fileDropMixin = {
|
|
11
|
+
onDrop (e) {
|
|
12
|
+
const dt = e.dataTransfer
|
|
13
|
+
const fromFile = dt.getData('fromFile')
|
|
14
|
+
const notSafeMsg = 'File name contains unsafe characters'
|
|
15
|
+
const isSshTerminal = this.props.tab.type === connectionMap.ssh
|
|
16
|
+
const isSerialTerminal = this.props.tab.type === connectionMap.serial
|
|
17
|
+
|
|
18
|
+
if (fromFile) {
|
|
19
|
+
try {
|
|
20
|
+
const fileData = JSON.parse(fromFile)
|
|
21
|
+
const filePath = resolve(fileData.path, fileData.name)
|
|
22
|
+
if (isUnsafeFilename(filePath)) {
|
|
23
|
+
message.error(notSafeMsg)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
if (isSshTerminal) {
|
|
27
|
+
const behavior = this.props.config.dragDropBehavior || 'ask'
|
|
28
|
+
if (behavior === 'ask') {
|
|
29
|
+
this.setState({
|
|
30
|
+
dropFileModalVisible: true,
|
|
31
|
+
droppedFiles: [{ path: filePath, isRemote: true }]
|
|
32
|
+
})
|
|
33
|
+
} else {
|
|
34
|
+
this.handleDropFileAction(behavior, [{ path: filePath, isRemote: true }])
|
|
35
|
+
}
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
if (isSerialTerminal) {
|
|
39
|
+
this.setState({
|
|
40
|
+
dropFileModalVisible: true,
|
|
41
|
+
droppedFiles: [{ path: filePath, isRemote: false }]
|
|
42
|
+
})
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
this.attachAddon._sendData(`"${filePath}" `)
|
|
46
|
+
return
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error('Failed to parse fromFile data:', e)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const files = dt.files
|
|
53
|
+
if (files && files.length) {
|
|
54
|
+
const arr = Array.from(files)
|
|
55
|
+
const filePaths = arr.map(f => getFilePath(f))
|
|
56
|
+
|
|
57
|
+
const hasUnsafeFilename = filePaths.some(path => isUnsafeFilename(path))
|
|
58
|
+
if (hasUnsafeFilename) {
|
|
59
|
+
message.error(notSafeMsg)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (isSshTerminal) {
|
|
64
|
+
const behavior = this.props.config.dragDropBehavior || 'ask'
|
|
65
|
+
if (behavior === 'ask') {
|
|
66
|
+
this.setState({
|
|
67
|
+
dropFileModalVisible: true,
|
|
68
|
+
droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
|
|
69
|
+
})
|
|
70
|
+
} else {
|
|
71
|
+
this.handleDropFileAction(behavior, filePaths.map(path => ({ path, isRemote: false })))
|
|
72
|
+
}
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isSerialTerminal) {
|
|
77
|
+
this.setState({
|
|
78
|
+
dropFileModalVisible: true,
|
|
79
|
+
droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
|
|
80
|
+
})
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
85
|
+
this.attachAddon._sendData(filesAll)
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
handleDropFileModalCancel () {
|
|
90
|
+
this.setState({
|
|
91
|
+
dropFileModalVisible: false,
|
|
92
|
+
droppedFiles: []
|
|
93
|
+
})
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
handleDropFileAction (action, filesOverride) {
|
|
97
|
+
const droppedFiles = filesOverride || this.state.droppedFiles
|
|
98
|
+
if (!droppedFiles || !droppedFiles.length) {
|
|
99
|
+
this.handleDropFileModalCancel()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const filePaths = droppedFiles.map(f => f.path)
|
|
104
|
+
|
|
105
|
+
switch (action) {
|
|
106
|
+
case 'trz': {
|
|
107
|
+
if (this.trzszClient && this.trzszClient.isActive) {
|
|
108
|
+
message.warning('A transfer is already in progress')
|
|
109
|
+
this.handleDropFileModalCancel()
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
window._apiControlSelectFile = filePaths
|
|
113
|
+
this.attachAddon._sendData('trz\r')
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
case 'rz':{
|
|
117
|
+
if (this.zmodemClient && this.zmodemClient.isActive) {
|
|
118
|
+
message.warning('A transfer is already in progress')
|
|
119
|
+
this.handleDropFileModalCancel()
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
window._apiControlSelectFile = filePaths
|
|
123
|
+
this.attachAddon._sendData('rz\r')
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
case 'xmodem': {
|
|
127
|
+
if (this.xmodemClient && this.xmodemClient.isActive) {
|
|
128
|
+
message.warning('A transfer is already in progress')
|
|
129
|
+
this.handleDropFileModalCancel()
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
// Use XMODEM send with the dropped files
|
|
133
|
+
window._apiControlSelectFile = filePaths
|
|
134
|
+
if (this.xmodemClient) {
|
|
135
|
+
this.xmodemClient.initiateSend()
|
|
136
|
+
}
|
|
137
|
+
break
|
|
138
|
+
}
|
|
139
|
+
case 'inputOnly':
|
|
140
|
+
default: {
|
|
141
|
+
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
142
|
+
this.attachAddon._sendData(filesAll)
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.handleDropFileModalCancel()
|
|
148
|
+
}
|
|
149
|
+
}
|