@electerm/electerm-react 2.7.9 → 2.8.6

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 (29) hide show
  1. package/client/common/pre.js +38 -11
  2. package/client/components/bookmark-form/config/rdp.js +1 -0
  3. package/client/components/bookmark-form/config/vnc.js +5 -0
  4. package/client/components/common/remote-float-control.jsx +79 -0
  5. package/client/components/common/remote-float-control.styl +28 -0
  6. package/client/components/layout/layout.jsx +2 -1
  7. package/client/components/main/main.jsx +3 -6
  8. package/client/components/main/term-fullscreen.styl +1 -10
  9. package/client/components/rdp/rdp-session.jsx +113 -4
  10. package/client/components/rdp/resolutions.js +6 -0
  11. package/client/components/session/session.jsx +4 -3
  12. package/client/components/session/session.styl +18 -5
  13. package/client/components/session/sessions.jsx +2 -1
  14. package/client/components/shortcuts/shortcut-control.jsx +5 -3
  15. package/client/components/shortcuts/shortcut-handler.js +4 -2
  16. package/client/components/terminal/attach-addon-custom.js +13 -0
  17. package/client/components/terminal/event-emitter.js +27 -0
  18. package/client/components/terminal/terminal.jsx +10 -297
  19. package/client/components/terminal/zmodem-client.js +385 -0
  20. package/client/components/terminal-info/data-cols-parser.jsx +3 -2
  21. package/client/components/terminal-info/network.jsx +3 -2
  22. package/client/components/vnc/vnc-session.jsx +397 -52
  23. package/client/css/basic.styl +3 -0
  24. package/client/store/event.js +2 -2
  25. package/client/store/init-state.js +1 -1
  26. package/package.json +1 -1
  27. package/client/common/byte-format.js +0 -14
  28. package/client/components/main/term-fullscreen-control.jsx +0 -21
  29. package/client/components/terminal/xterm-zmodem.js +0 -55
@@ -0,0 +1,385 @@
1
+ /**
2
+ * Zmodem client handler for web terminal
3
+ * Handles UI interactions and communicates with server-side zmodem
4
+ */
5
+
6
+ import { throttle } from 'lodash-es'
7
+ import { filesize } from 'filesize'
8
+ import { transferTypeMap } from '../../common/constants.js'
9
+ import { getLocalFileInfo } from '../sftp/file-read.js'
10
+
11
+ const ZMODEM_SAVE_PATH_KEY = 'zmodem-save-path'
12
+
13
+ /**
14
+ * ZmodemClient class handles zmodem UI and client-side logic
15
+ */
16
+ export class ZmodemClient {
17
+ constructor (terminal) {
18
+ this.terminal = terminal
19
+ this.socket = null
20
+ this.isActive = false
21
+ this.currentTransfer = null
22
+ this.savePath = null
23
+ this.transferStartTime = 0
24
+ }
25
+
26
+ /**
27
+ * Initialize zmodem client with socket
28
+ * @param {WebSocket} socket - WebSocket connection
29
+ */
30
+ init (socket) {
31
+ this.socket = socket
32
+ this.setupMessageHandler()
33
+ }
34
+
35
+ /**
36
+ * Setup message handler for zmodem events from server
37
+ */
38
+ setupMessageHandler () {
39
+ if (!this.socket) return
40
+
41
+ // Use addEventListener to avoid conflicting with attach-addon's onmessage
42
+ this.messageHandler = (event) => {
43
+ // Check if it's a JSON message (zmodem control message)
44
+ if (typeof event.data === 'string') {
45
+ try {
46
+ const msg = JSON.parse(event.data)
47
+ if (msg.action === 'zmodem-event') {
48
+ this.handleServerEvent(msg)
49
+ }
50
+ } catch (e) {
51
+ // Not JSON, ignore
52
+ }
53
+ }
54
+ }
55
+ this.socket.addEventListener('message', this.messageHandler)
56
+ }
57
+
58
+ /**
59
+ * Handle server zmodem events
60
+ * @param {Object} msg - Message from server
61
+ */
62
+ handleServerEvent (msg) {
63
+ const { event } = msg
64
+
65
+ switch (event) {
66
+ case 'receive-start':
67
+ this.onReceiveStart()
68
+ break
69
+ case 'send-start':
70
+ this.onSendStart()
71
+ break
72
+ case 'file-start':
73
+ this.onFileStart(msg.name, msg.size)
74
+ break
75
+ case 'file-prepared':
76
+ this.onFilePrepared(msg.name, msg.path, msg.size)
77
+ break
78
+ case 'progress':
79
+ this.onProgress(msg)
80
+ break
81
+ case 'file-complete':
82
+ this.onFileComplete(msg.name, msg.path)
83
+ break
84
+ case 'file-skipped':
85
+ this.onFileSkipped(msg.name, msg.message)
86
+ break
87
+ case 'session-end':
88
+ this.onSessionEnd()
89
+ break
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Send message to server
95
+ * @param {Object} msg - Message to send
96
+ */
97
+ sendToServer (msg) {
98
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
99
+ this.socket.send(JSON.stringify({
100
+ action: 'zmodem-event',
101
+ ...msg
102
+ }))
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Handle receive session start
108
+ */
109
+ async onReceiveStart () {
110
+ this.isActive = true
111
+ this.writeBanner('RECEIVE')
112
+
113
+ // Ask user for save directory
114
+ const savePath = await this.openSaveFolderSelect()
115
+ if (savePath) {
116
+ this.savePath = savePath
117
+ this.sendToServer({
118
+ event: 'set-save-path',
119
+ path: savePath
120
+ })
121
+ } else {
122
+ // User cancelled, end session
123
+ this.sendToServer({
124
+ event: 'cancel'
125
+ })
126
+ this.onSessionEnd()
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Handle send session start
132
+ */
133
+ async onSendStart () {
134
+ this.isActive = true
135
+ this.writeBanner('SEND')
136
+
137
+ // Ask user to select files
138
+ const files = await this.openFileSelect()
139
+ if (files && files.length > 0) {
140
+ this.sendToServer({
141
+ event: 'send-files',
142
+ files
143
+ })
144
+ } else {
145
+ // User cancelled, end session
146
+ this.sendToServer({
147
+ event: 'cancel'
148
+ })
149
+ this.onSessionEnd()
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Handle file start event
155
+ * @param {string} name - File name
156
+ * @param {number} size - File size
157
+ */
158
+ onFileStart (name, size) {
159
+ this.currentTransfer = {
160
+ name,
161
+ size,
162
+ transferred: 0,
163
+ type: this.savePath ? transferTypeMap.download : transferTypeMap.upload,
164
+ path: null
165
+ }
166
+ this.transferStartTime = Date.now()
167
+ this.writeProgress()
168
+ }
169
+
170
+ /**
171
+ * Handle file prepared event (for receive)
172
+ * @param {string} name - File name
173
+ * @param {string} path - File path
174
+ * @param {number} size - File size
175
+ */
176
+ onFilePrepared (name, path, size) {
177
+ // Store the full path for display
178
+ if (this.currentTransfer) {
179
+ this.currentTransfer.path = path
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Handle progress update
185
+ * @param {Object} msg - Progress message
186
+ */
187
+ onProgress (msg) {
188
+ if (!this.currentTransfer) {
189
+ this.currentTransfer = {
190
+ name: msg.name,
191
+ size: msg.size,
192
+ transferred: 0,
193
+ type: msg.type,
194
+ path: msg.path || null
195
+ }
196
+ this.transferStartTime = Date.now()
197
+ }
198
+
199
+ this.currentTransfer.transferred = msg.transferred
200
+ this.currentTransfer.serverSpeed = msg.speed // Use server's speed calculation
201
+ this.currentTransfer.path = msg.path || this.currentTransfer.path
202
+ this.writeProgress()
203
+ }
204
+
205
+ /**
206
+ * Handle file complete
207
+ * @param {string} name - File name
208
+ * @param {string} path - File path
209
+ */
210
+ onFileComplete (name, path) {
211
+ if (this.currentTransfer) {
212
+ this.currentTransfer.transferred = this.currentTransfer.size
213
+ this.currentTransfer.path = path
214
+ // Call directly to ensure 100% is displayed immediately
215
+ this._doWriteProgress(true)
216
+ // Add newline after completion
217
+ this.writeToTerminal('\r\n')
218
+ }
219
+ this.currentTransfer = null
220
+ }
221
+
222
+ /**
223
+ * Handle file skipped (already exists on remote side)
224
+ * @param {string} name - File name
225
+ * @param {string} message - Skip message
226
+ */
227
+ onFileSkipped (name, message) {
228
+ this.writeToTerminal(`\r\n\x1b[33m\x1b[1mSKIPPED: ${name} - ${message}\x1b[0m\r\n`)
229
+ this.currentTransfer = null
230
+ }
231
+
232
+ /**
233
+ * Handle session end
234
+ */
235
+ onSessionEnd () {
236
+ this.isActive = false
237
+ this.currentTransfer = null
238
+ this.savePath = null
239
+ if (this.terminal && this.terminal.term) {
240
+ this.terminal.term.focus()
241
+ this.terminal.term.write('\r\n')
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Write banner to terminal
247
+ * @param {string} type - 'RECEIVE' or 'SEND'
248
+ */
249
+ writeBanner (type) {
250
+ const border = '='.repeat(50)
251
+ this.writeToTerminal(`\r\n${border}\r\n`)
252
+ this.writeToTerminal('\x1b[33m\x1b[1mRecommend use trzsz instead: https://github.com/trzsz/trzsz\x1b[0m\r\n')
253
+ this.writeToTerminal(`${border}\r\n\r\n`)
254
+ this.writeToTerminal(`\x1b[32m\x1b[1mZMODEM::${type}::START\x1b[0m\r\n`)
255
+ }
256
+
257
+ /**
258
+ * Write progress to terminal (throttled for updates, immediate for completion)
259
+ * @param {boolean} isComplete - Whether this is the final completion display
260
+ */
261
+ writeProgress = throttle((isComplete = false) => {
262
+ this._doWriteProgress(isComplete)
263
+ }, 500)
264
+
265
+ /**
266
+ * Internal function to actually write progress
267
+ * @param {boolean} isComplete - Whether this is the final completion display
268
+ */
269
+ _doWriteProgress (isComplete = false) {
270
+ if (!this.currentTransfer || !this.terminal?.term) return
271
+
272
+ const { name, size, transferred, path, serverSpeed } = this.currentTransfer
273
+ const percent = size > 0 ? Math.floor(transferred * 100 / size) : 100
274
+
275
+ // Use server's speed if available, otherwise calculate locally
276
+ const speed = serverSpeed || 0
277
+
278
+ // Use full path if available, otherwise just name
279
+ const displayName = path || name
280
+
281
+ // filesize expects bytes and formats to human readable
282
+ const formatSize = (bytes) => filesize(bytes)
283
+
284
+ // Clear line and write progress
285
+ const str = `\r\x1b[2K\x1b[32m${displayName}\x1b[0m: ${percent}%, ${formatSize(transferred)}/${formatSize(size)}, ${formatSize(speed)}/s${isComplete ? ' \x1b[32m\x1b[1m[DONE]\x1b[0m' : ''}`
286
+ this.writeToTerminal(str + '\r')
287
+ }
288
+
289
+ /**
290
+ * Write text to terminal
291
+ * @param {string} text - Text to write
292
+ */
293
+ writeToTerminal (text) {
294
+ if (this.terminal && this.terminal.term) {
295
+ this.terminal.term.write(text)
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Open file select dialog
301
+ * @returns {Promise<Array>} - Selected files
302
+ */
303
+ openFileSelect = async () => {
304
+ const properties = [
305
+ 'openFile',
306
+ 'multiSelections',
307
+ 'showHiddenFiles',
308
+ 'noResolveAliases',
309
+ 'treatPackageAsDirectory',
310
+ 'dontAddToRecent'
311
+ ]
312
+ const files = await window.api.openDialog({
313
+ title: 'Choose some files to send',
314
+ message: 'Choose some files to send',
315
+ properties
316
+ }).catch(() => false)
317
+ if (!files || !files.length) {
318
+ return null
319
+ }
320
+ const r = []
321
+ for (const filePath of files) {
322
+ const stat = await getLocalFileInfo(filePath)
323
+ r.push({ ...stat, filePath, path: filePath })
324
+ }
325
+ return r
326
+ }
327
+
328
+ /**
329
+ * Open save folder select dialog
330
+ * @returns {Promise<string>} - Selected folder path
331
+ */
332
+ openSaveFolderSelect = async () => {
333
+ // Try to use last saved path
334
+ const lastPath = window.localStorage.getItem(ZMODEM_SAVE_PATH_KEY)
335
+
336
+ const savePaths = await window.api.openDialog({
337
+ title: 'Choose a folder to save file(s)',
338
+ message: 'Choose a folder to save file(s)',
339
+ defaultPath: lastPath || undefined,
340
+ properties: [
341
+ 'openDirectory',
342
+ 'showHiddenFiles',
343
+ 'createDirectory',
344
+ 'noResolveAliases',
345
+ 'treatPackageAsDirectory',
346
+ 'dontAddToRecent'
347
+ ]
348
+ }).catch(() => false)
349
+ if (!savePaths || !savePaths.length) {
350
+ return null
351
+ }
352
+ // Save for next time
353
+ window.localStorage.setItem(ZMODEM_SAVE_PATH_KEY, savePaths[0])
354
+ return savePaths[0]
355
+ }
356
+
357
+ /**
358
+ * Cancel ongoing transfer
359
+ */
360
+ cancel () {
361
+ if (!this.isActive) return
362
+ this.writeToTerminal('\r\n\x1b[33m\x1b[1mZMODEM transfer cancelled by user\x1b[0m\r\n')
363
+ this.sendToServer({
364
+ event: 'cancel'
365
+ })
366
+ this.onSessionEnd()
367
+ }
368
+
369
+ /**
370
+ * Clean up resources
371
+ */
372
+ destroy () {
373
+ if (this.socket && this.messageHandler) {
374
+ this.socket.removeEventListener('message', this.messageHandler)
375
+ this.messageHandler = null
376
+ }
377
+ this.isActive = false
378
+ this.currentTransfer = null
379
+ this.savePath = null
380
+ this.socket = null
381
+ this.terminal = null
382
+ }
383
+ }
384
+
385
+ export default ZmodemClient
@@ -1,6 +1,6 @@
1
1
  import { copy } from '../../common/clipboard'
2
2
  import filesizeParser from 'filesize-parser'
3
- import { formatBytes } from '../../common/byte-format'
3
+ import { filesize } from 'filesize'
4
4
 
5
5
  const valueParserMaps = {
6
6
  size: v => v,
@@ -23,7 +23,8 @@ function copyValue (event) {
23
23
  export default (data) => {
24
24
  return Object.keys(data).map(k => {
25
25
  const rd = (txt) => {
26
- const r = k === 'mem' ? formatBytes(parseInt(txt, 10)) : txt
26
+ // txt is in KB, convert to bytes for filesize function
27
+ const r = k === 'mem' ? filesize(parseInt(txt, 10) * 1024) : txt
27
28
  const itemProps = {
28
29
  className: 'activity-item pointer',
29
30
  'data-content': r,
@@ -5,7 +5,7 @@
5
5
  import { Table } from 'antd'
6
6
  import { isEmpty } from 'lodash-es'
7
7
  import { useEffect, useState } from 'react'
8
- import { formatBytes } from '../../common/byte-format'
8
+ import { filesize } from 'filesize'
9
9
  import copy from 'json-deep-copy'
10
10
  import { ApiOutlined } from '@ant-design/icons'
11
11
 
@@ -92,7 +92,8 @@ export default function TerminalInfoDisk (props) {
92
92
  },
93
93
  render: (v) => {
94
94
  if (k === 'up' || k === 'down') {
95
- return formatBytes(v)
95
+ console.log('render traffic', k, v)
96
+ return filesize(v || 0)
96
97
  }
97
98
  return v
98
99
  }