@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.
Files changed (146) hide show
  1. package/README.md +246 -12
  2. package/client/common/choose-save-folder.js +21 -2
  3. package/client/common/constants.js +20 -6
  4. package/client/common/create-title.jsx +6 -0
  5. package/client/common/default-setting.js +15 -0
  6. package/client/common/init-setting-item.js +5 -0
  7. package/client/common/platform.js +24 -0
  8. package/client/components/ai/agent-tool-call-card.jsx +10 -2
  9. package/client/components/ai/agent.js +4 -1
  10. package/client/components/ai/ai-attachments.js +87 -0
  11. package/client/components/ai/ai-chat-empty.jsx +43 -0
  12. package/client/components/ai/ai-chat-entry.jsx +13 -4
  13. package/client/components/ai/ai-chat-history-item.jsx +28 -4
  14. package/client/components/ai/ai-chat-history.jsx +2 -1
  15. package/client/components/ai/ai-chat.jsx +104 -13
  16. package/client/components/ai/ai-config-props.js +4 -130
  17. package/client/components/ai/ai-config.jsx +135 -15
  18. package/client/components/ai/ai-presets.js +173 -0
  19. package/client/components/ai/ai.styl +83 -0
  20. package/client/components/bg/css-overwrite.jsx +11 -0
  21. package/client/components/bookmark-form/bookmark-schema.js +4 -0
  22. package/client/components/bookmark-form/common/fields.jsx +3 -0
  23. package/client/components/bookmark-form/config/common-fields.js +12 -0
  24. package/client/components/bookmark-form/config/local.js +3 -2
  25. package/client/components/bookmark-form/config/serial.js +3 -2
  26. package/client/components/bookmark-form/config/ssh.js +2 -1
  27. package/client/components/bookmark-form/config/telnet.js +3 -2
  28. package/client/components/common/item-filter.jsx +76 -0
  29. package/client/components/common/item-filter.styl +54 -0
  30. package/client/components/common/lazy-boundary.jsx +34 -0
  31. package/client/components/common/opacity.jsx +3 -1
  32. package/client/components/footer/cmd-history.jsx +132 -45
  33. package/client/components/footer/cmd-history.styl +42 -4
  34. package/client/components/footer/footer-entry.jsx +35 -5
  35. package/client/components/footer/footer.styl +8 -1
  36. package/client/components/footer/multi-tab-run-modal.jsx +83 -0
  37. package/client/components/footer/tab-select.jsx +34 -17
  38. package/client/components/layout/layout.jsx +23 -31
  39. package/client/components/layout/pixed.js +3 -0
  40. package/client/components/main/main.jsx +55 -28
  41. package/client/components/main/term-fullscreen.styl +9 -3
  42. package/client/components/quick-commands/qm.styl +3 -2
  43. package/client/components/quick-commands/quick-command-create-modal.jsx +60 -0
  44. package/client/components/quick-commands/quick-commands-form-elem.jsx +10 -4
  45. package/client/components/rdp/file-transfer.js +4 -4
  46. package/client/components/remote-monitor/monitor-details.jsx +458 -0
  47. package/client/components/remote-monitor/monitor-details.styl +87 -0
  48. package/client/components/remote-monitor/monitor-model.js +764 -0
  49. package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
  50. package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
  51. package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
  52. package/client/components/remote-monitor/session-monitor.js +361 -0
  53. package/client/components/remote-monitor/use-monitor-details.js +10 -0
  54. package/client/components/remote-monitor/visibility.js +18 -0
  55. package/client/components/session/session-control.jsx +29 -2
  56. package/client/components/session/session.jsx +40 -18
  57. package/client/components/session/sessions.jsx +5 -2
  58. package/client/components/setting-panel/setting-common.jsx +25 -56
  59. package/client/components/setting-panel/setting-modal.jsx +57 -42
  60. package/client/components/setting-panel/setting-terminal.jsx +8 -2
  61. package/client/components/setting-panel/setting.styl +1 -1
  62. package/client/components/setting-panel/tab-triggers.jsx +71 -0
  63. package/client/components/sftp/file-compare-modal.jsx +6 -3
  64. package/client/components/sftp/file-item.jsx +5 -3
  65. package/client/components/sftp/owner-list.js +11 -2
  66. package/client/components/sftp/sftp-entry.jsx +4 -0
  67. package/client/components/side-panel-r/side-panel-r.jsx +2 -2
  68. package/client/components/sidebar/side-panel.jsx +1 -1
  69. package/client/components/sys-menu/menu-btn.jsx +30 -1
  70. package/client/components/tabs/index.jsx +2 -2
  71. package/client/components/tabs/tab-title.jsx +3 -2
  72. package/client/components/tabs/tab.jsx +5 -1
  73. package/client/components/terminal/attach-addon-custom.js +295 -49
  74. package/client/components/terminal/automation/index.js +64 -0
  75. package/client/components/terminal/automation/keys.js +49 -0
  76. package/client/components/terminal/automation/stream-matcher.js +82 -0
  77. package/client/components/terminal/automation/strip-ansi.js +19 -0
  78. package/client/components/terminal/automation/trigger-engine.js +233 -0
  79. package/client/components/terminal/automation/trigger-presets.js +110 -0
  80. package/client/components/terminal/command-tracker-addon.js +31 -1
  81. package/client/components/terminal/mixins/index.js +27 -0
  82. package/client/components/terminal/mixins/term-attach.js +123 -0
  83. package/client/components/terminal/mixins/term-config.jsx +84 -0
  84. package/client/components/terminal/mixins/term-content.js +126 -0
  85. package/client/components/terminal/mixins/term-context-menu.jsx +323 -0
  86. package/client/components/terminal/mixins/term-file-drop.js +149 -0
  87. package/client/components/terminal/mixins/term-init.js +122 -0
  88. package/client/components/terminal/mixins/term-log.jsx +96 -0
  89. package/client/components/terminal/mixins/term-resize.js +87 -0
  90. package/client/components/terminal/mixins/term-socket.js +433 -0
  91. package/client/components/terminal/mixins/term-suggestions.js +224 -0
  92. package/client/components/terminal/mixins/term-theme.js +190 -0
  93. package/client/components/terminal/mixins/term-touch.js +523 -0
  94. package/client/components/terminal/shell-detect.js +37 -0
  95. package/client/components/terminal/shell.js +0 -28
  96. package/client/components/terminal/shortcut-bar-defs.js +30 -4
  97. package/client/components/terminal/shortcut-bar-edit.jsx +40 -9
  98. package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
  99. package/client/components/terminal/shortcut-bar.jsx +17 -10
  100. package/client/components/terminal/shortcut-bar.styl +4 -0
  101. package/client/components/terminal/ssh-reload-state.js +96 -0
  102. package/client/components/terminal/startup-queue.js +433 -0
  103. package/client/components/terminal/terminal-interactive.jsx +32 -0
  104. package/client/components/terminal/terminal-select-text.jsx +69 -0
  105. package/client/components/terminal/terminal.jsx +227 -2064
  106. package/client/components/terminal/terminal.styl +61 -0
  107. package/client/components/terminal/transfer-client-base.js +5 -29
  108. package/client/components/terminal/trzsz-client.js +1 -3
  109. package/client/components/terminal/xmodem-client.js +1 -3
  110. package/client/components/terminal/xterm-loader.js +7 -0
  111. package/client/components/terminal/zmodem-client.js +1 -3
  112. package/client/components/terminal-info/base.jsx +18 -70
  113. package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
  114. package/client/components/terminal-info/terminal-info.jsx +50 -59
  115. package/client/components/terminal-info/terminal-info.styl +6 -49
  116. package/client/components/text-editor/open-text-editor.js +30 -0
  117. package/client/components/text-editor/text-editor-entry.jsx +17 -6
  118. package/client/components/text-editor/text-editor.jsx +14 -0
  119. package/client/components/triggers/trigger-editor.jsx +246 -0
  120. package/client/components/triggers/trigger-form.jsx +200 -0
  121. package/client/components/triggers/trigger-session-modal.jsx +199 -0
  122. package/client/components/triggers/trigger-setting-list.jsx +89 -0
  123. package/client/components/triggers/trigger-transport.jsx +60 -0
  124. package/client/components/triggers/triggers-field.jsx +19 -0
  125. package/client/components/widgets/widgets-list.jsx +11 -1
  126. package/client/css/mobile.styl +29 -8
  127. package/client/store/common.js +12 -1
  128. package/client/store/init-state.js +7 -0
  129. package/client/store/mcp-handler.js +158 -30
  130. package/client/store/setting.js +9 -0
  131. package/client/store/store.js +2 -0
  132. package/client/store/sync.js +2 -0
  133. package/client/store/tab.js +42 -3
  134. package/client/store/trigger.js +118 -0
  135. package/client/store/watch.js +13 -5
  136. package/client/views/index.pug +3 -0
  137. package/package.json +1 -1
  138. package/client/components/terminal-info/activity.jsx +0 -57
  139. package/client/components/terminal-info/data-cols-parser.jsx +0 -54
  140. package/client/components/terminal-info/disk.jsx +0 -45
  141. package/client/components/terminal-info/format-disks.js +0 -45
  142. package/client/components/terminal-info/network.jsx +0 -121
  143. package/client/components/terminal-info/resource.jsx +0 -99
  144. package/client/components/terminal-info/run-cmd.jsx +0 -263
  145. package/client/components/terminal-info/sys-info.jsx +0 -56
  146. 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
+ }