@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
@@ -1,5 +1,4 @@
1
- import { createRef } from 'react'
2
- import RdpSession from '../rdp/rdp-session'
1
+ import { PureComponent, createRef } from 'react'
3
2
  import { createTerm } from '../terminal/terminal-apis'
4
3
  import deepCopy from 'json-deep-copy'
5
4
  import clone from '../../common/to-simple-obj'
@@ -9,30 +8,41 @@ import {
9
8
  } from '../../common/constants'
10
9
  import {
11
10
  Spin,
12
- Tag
11
+ Select
13
12
  } from 'antd'
13
+ import {
14
+ ReloadOutlined
15
+ } from '@ant-design/icons'
14
16
  import message from '../common/message'
15
17
  import Modal from '../common/modal'
16
- import * as ls from '../../common/safe-local-storage'
17
18
  import { copy } from '../../common/clipboard'
18
- import resolutions from '../rdp/resolutions'
19
- import RFB from '@novnc/novnc/core/rfb'
20
19
  import VncForm from './vnc-form'
20
+ import RemoteFloatControl from '../common/remote-float-control'
21
+
22
+ // noVNC module imports — loaded dynamically
23
+ async function loadVncModule () {
24
+ if (window.novnc) return
25
+ console.debug('[VNC-CLIENT] Loading noVNC module...')
26
+ const mod = await import('@novnc/novnc/core/rfb')
27
+ window.novnc = {
28
+ RFB: mod.default
29
+ }
30
+ console.debug('[VNC-CLIENT] noVNC module loaded')
31
+ }
21
32
 
33
+ const { Option } = Select
22
34
  const e = window.translate
23
35
 
24
- export default class VncSession extends RdpSession {
36
+ export default class VncSession extends PureComponent {
25
37
  constructor (props) {
26
- const id = `vnc-reso-${props.tab.host}`
27
- const resObj = ls.getItemJSON(id, resolutions[0])
28
38
  super(props)
29
39
  this.state = {
30
40
  types: [],
31
41
  showConfirm: false,
32
42
  loading: false,
33
- aspectRatio: 4 / 3,
34
43
  name: '',
35
- ...resObj
44
+ screens: [],
45
+ currentScreen: '0'
36
46
  }
37
47
  }
38
48
 
@@ -45,22 +55,119 @@ export default class VncSession extends RdpSession {
45
55
  componentWillUnmount () {
46
56
  this.rfb && this.rfb.disconnect()
47
57
  delete this.rfb
58
+ clearTimeout(this.timer)
59
+ }
60
+
61
+ setStatus = status => {
62
+ const id = this.props.tab?.id
63
+ this.props.editTab(id, {
64
+ status
65
+ })
66
+ }
67
+
68
+ getScreenSize = () => {
69
+ const { screens, currentScreen } = this.state
70
+ const currentScreenData = screens.find(s => s.id === currentScreen)
71
+ return {
72
+ remoteWidth: currentScreenData ? currentScreenData.width : null,
73
+ remoteHeight: currentScreenData ? currentScreenData.height : null
74
+ }
75
+ }
76
+
77
+ calcCanvasSize = () => {
78
+ const { width, height } = this.props
79
+ return {
80
+ width: width - 10,
81
+ height: height - 80
82
+ }
83
+ }
84
+
85
+ buildWsUrl = (port, type = 'rdp', extra = '') => {
86
+ const { host, tokenElecterm } = this.props.config
87
+ const { id } = this.props.tab
88
+ if (window.et.buildWsUrl) {
89
+ return window.et.buildWsUrl(
90
+ host,
91
+ port,
92
+ tokenElecterm,
93
+ id,
94
+ type,
95
+ extra
96
+ )
97
+ }
98
+ return `ws://${host}:${port}/${type}/${id}?token=${tokenElecterm}${extra}`
48
99
  }
49
100
 
50
- // computeProps = () => {
51
- // const {
52
- // height,
53
- // width,
54
- // tabsHeight,
55
- // leftSidebarWidth,
56
- // pinned,
57
- // openedSideBar
58
- // } = this.props
59
- // return {
60
- // width: width - (pinned && openedSideBar ? leftSidebarWidth : 0),
61
- // height: height - tabsHeight
62
- // }
63
- // }
101
+ getControlProps = (options = {}) => {
102
+ const {
103
+ screens,
104
+ currentScreen
105
+ } = this.state
106
+ const {
107
+ fixedPosition = true,
108
+ showExitFullscreen = true,
109
+ className = ''
110
+ } = options
111
+
112
+ return {
113
+ isFullScreen: this.props.fullscreen,
114
+ onSendCtrlAltDel: this.handleSendCtrlAltDel,
115
+ screens: screens.length > 1 ? screens : [],
116
+ currentScreen,
117
+ onSelectScreen: this.handleSelectScreen,
118
+ fixedPosition,
119
+ showExitFullscreen,
120
+ className
121
+ }
122
+ }
123
+
124
+ renderControl = () => {
125
+ const contrlProps = this.getControlProps({
126
+ fixedPosition: false,
127
+ showExitFullscreen: false,
128
+ className: 'mg1l'
129
+ })
130
+ return (
131
+ <div className='pd1 fix session-v-info'>
132
+ <div className='fleft'>
133
+ <ReloadOutlined
134
+ onClick={this.handleReInit}
135
+ className='mg2r mg1l pointer'
136
+ />
137
+ {this.renderScreensSelect()}
138
+ {this.renderInfo()}
139
+ </div>
140
+ <div className='fright'>
141
+ {this.props.fullscreenIcon()}
142
+ <RemoteFloatControl {...contrlProps} />
143
+ </div>
144
+ </div>
145
+ )
146
+ }
147
+
148
+ renderScreensSelect = () => {
149
+ const {
150
+ screens,
151
+ currentScreen
152
+ } = this.state
153
+ if (screens.length <= 1) {
154
+ return null
155
+ }
156
+ return (
157
+ <Select
158
+ value={currentScreen}
159
+ onChange={this.handleSelectScreen}
160
+ className='mg2r'
161
+ popupMatchSelectWidth={false}
162
+ >
163
+ {
164
+ screens.map(s => (
165
+ <Option key={s.id} value={s.id}>{s.name}</Option>
166
+ ))
167
+ }
168
+ </Select>
169
+ )
170
+ }
64
171
 
65
172
  remoteInit = async (term = this.term) => {
66
173
  this.setState({
@@ -75,6 +182,9 @@ export default class VncSession extends RdpSession {
75
182
  viewOnly = false,
76
183
  scaleViewport = true,
77
184
  clipViewport = false,
185
+ qualityLevel = 3, // 0-9, lower = faster performance
186
+ compressionLevel = 1, // 0-9, lower = faster performance
187
+ shared = true,
78
188
  username,
79
189
  password
80
190
  } = tab
@@ -102,17 +212,35 @@ export default class VncSession extends RdpSession {
102
212
  const { pid, port } = r
103
213
  this.pid = pid
104
214
  this.port = port
105
- const { width, height } = this.state
215
+
216
+ // Load VNC module if not already loaded
217
+ try {
218
+ await loadVncModule()
219
+ } catch (e) {
220
+ console.error('[VNC-CLIENT] Failed to load VNC module:', e)
221
+ this.setState({ loading: false })
222
+ this.setStatus(statusMap.error)
223
+ return
224
+ }
225
+
226
+ const { width, height } = this.calcCanvasSize()
106
227
  const wsUrl = this.buildWsUrl(port, 'vnc', `&width=${width}&height=${height}`)
228
+ // When scaleViewport is false, we don't set fixed dimensions on the canvas
229
+ // so it can render at the actual remote screen size
107
230
  const vncOpts = {
108
231
  clipViewport,
109
232
  scaleViewport,
110
233
  viewOnly,
111
- style: {
112
- width: width + 'px',
113
- height: height + 'px',
114
- overflow: 'scroll'
115
- },
234
+ qualityLevel, // JPEG quality 0-9, lower = faster
235
+ compressionLevel, // Compression 0-9, lower = faster
236
+ shared, // Allow shared connections
237
+ style: scaleViewport
238
+ ? {
239
+ width: width + 'px',
240
+ height: height + 'px',
241
+ overflow: 'hidden'
242
+ }
243
+ : {},
116
244
  credentials: {}
117
245
  }
118
246
  if (username) {
@@ -121,11 +249,112 @@ export default class VncSession extends RdpSession {
121
249
  if (password) {
122
250
  vncOpts.credentials.password = password
123
251
  }
252
+ const RFB = window.novnc.RFB
124
253
  const rfb = new RFB(
125
254
  this.getDom(),
126
255
  wsUrl,
127
256
  vncOpts
128
257
  )
258
+
259
+ // Monkey patch _handleExtendedDesktopSize to capture screen info
260
+ const originalHandleExtendedDesktopSize = rfb._handleExtendedDesktopSize
261
+ rfb._handleExtendedDesktopSize = function () {
262
+ try {
263
+ // Wait for header
264
+ if (this._sock.rQwait('ExtendedDesktopSize', 4)) {
265
+ return false
266
+ }
267
+
268
+ const numberOfScreens = this._sock.rQpeek8()
269
+ const bytes = 4 + (numberOfScreens * 16)
270
+ if (this._sock.rQwait('ExtendedDesktopSize', bytes)) {
271
+ return false
272
+ }
273
+
274
+ const firstUpdate = !this._supportsSetDesktopSize
275
+ this._supportsSetDesktopSize = true
276
+
277
+ if (firstUpdate) {
278
+ this._requestRemoteResize()
279
+ }
280
+
281
+ const num = this._sock.rQshift8() // number-of-screens
282
+ this._sock.rQskipBytes(3) // padding
283
+
284
+ const screens = []
285
+ for (let i = 0; i < num; i += 1) {
286
+ const idBytes = this._sock.rQshiftBytes(4)
287
+ const id = (idBytes[0] << 24 | idBytes[1] << 16 | idBytes[2] << 8 | idBytes[3]) >>> 0
288
+
289
+ const x = this._sock.rQshift16() // x-position
290
+ const y = this._sock.rQshift16() // y-position
291
+ const w = this._sock.rQshift16() // width
292
+ const h = this._sock.rQshift16() // height
293
+
294
+ const flagsBytes = this._sock.rQshiftBytes(4)
295
+ const flags = (flagsBytes[0] << 24 | flagsBytes[1] << 16 | flagsBytes[2] << 8 | flagsBytes[3]) >>> 0
296
+
297
+ screens.push({ id, x, y, width: w, height: h, flags })
298
+
299
+ if (i === 0) {
300
+ this._screenID = idBytes
301
+ this._screenFlags = flagsBytes
302
+ }
303
+ }
304
+
305
+ // Store the screens in a custom property and dispatch event
306
+ this.screens = screens
307
+ this.dispatchEvent(new CustomEvent('screens', { detail: screens }))
308
+
309
+ // Handle the resize feedback logic from original code
310
+ if (this._FBU.x === 1 && this._FBU.y !== 0) {
311
+ let msg = ''
312
+ switch (this._FBU.y) {
313
+ case 1: msg = 'Resize is administratively prohibited'; break
314
+ case 2: msg = 'Out of resources'; break
315
+ case 3: msg = 'Invalid screen layout'; break
316
+ default: msg = 'Unknown reason'; break
317
+ }
318
+ console.warn('Server did not accept the resize request: ' + msg)
319
+ } else {
320
+ this._resize(this._FBU.width, this._FBU.height)
321
+ }
322
+
323
+ return true
324
+ } catch (e) {
325
+ console.error('Error in patched _handleExtendedDesktopSize', e)
326
+ return originalHandleExtendedDesktopSize.call(this)
327
+ }
328
+ }
329
+
330
+ // Monkey patch autoscale to support single screen scaling
331
+ const originalAutoscale = rfb._display.autoscale
332
+ rfb._display.autoscale = (containerWidth, containerHeight) => {
333
+ const { currentScreen, screens } = this.state
334
+
335
+ if (currentScreen === 'all' || !screens.length) {
336
+ return originalAutoscale.call(rfb._display, containerWidth, containerHeight)
337
+ }
338
+
339
+ const screen = screens.find(s => s.id === currentScreen)
340
+ if (!screen) {
341
+ return originalAutoscale.call(rfb._display, containerWidth, containerHeight)
342
+ }
343
+
344
+ let scaleRatio = 1.0
345
+ if (containerWidth > 0 && containerHeight > 0) {
346
+ const targetAspectRatio = containerWidth / containerHeight
347
+ const screenAspectRatio = screen.width / screen.height
348
+
349
+ if (screenAspectRatio >= targetAspectRatio) {
350
+ scaleRatio = containerWidth / screen.width
351
+ } else {
352
+ scaleRatio = containerHeight / screen.height
353
+ }
354
+ }
355
+ rfb._display._rescale(scaleRatio)
356
+ }
357
+
129
358
  const events = [
130
359
  'connect',
131
360
  'disconnect',
@@ -134,21 +363,29 @@ export default class VncSession extends RdpSession {
134
363
  'clipboard',
135
364
  'bell',
136
365
  'desktopname',
137
- 'capabilities'
366
+ 'capabilities',
367
+ 'screens',
368
+ 'desktopsize'
138
369
  ]
139
370
  for (const event of events) {
140
371
  rfb.addEventListener(event, this[`on${window.capitalizeFirstLetter(event)}`])
141
372
  }
373
+ rfb.scaleViewport = scaleViewport
374
+ rfb.clipViewport = clipViewport
142
375
  this.rfb = rfb
143
376
  }
144
377
 
145
378
  onConnect = (event) => {
146
379
  this.setStatus(statusMap.success)
380
+ // Capture the remote desktop size from the RFB connection
147
381
  this.setState({
148
382
  loading: false
149
383
  })
150
384
  }
151
385
 
386
+ onDesktopsize = (event) => {
387
+ }
388
+
152
389
  onDisconnect = () => {
153
390
  this.setStatus(statusMap.error)
154
391
  }
@@ -192,10 +429,97 @@ export default class VncSession extends RdpSession {
192
429
  this.setState({
193
430
  name: event?.detail?.name || ''
194
431
  })
432
+ this.checkScreens()
195
433
  }
196
434
 
197
435
  onCapabilities = (capabilities) => {
198
- console.log('onCapabilities', capabilities)
436
+ setTimeout(this.checkScreens, 1000)
437
+ }
438
+
439
+ processScreens = (screens) => {
440
+ if (!screens || !screens.length) {
441
+ return []
442
+ }
443
+ return screens.map((s, i) => ({
444
+ name: `Screen ${i + 1}`,
445
+ ...s,
446
+ id: s.id !== undefined ? s.id : i
447
+ }))
448
+ }
449
+
450
+ onScreens = (event) => {
451
+ const screensData = event.detail
452
+ if (screensData && screensData.length) {
453
+ const screens = this.processScreens(screensData)
454
+ this.setState({
455
+ screens,
456
+ currentScreen: screens[0].id
457
+ })
458
+ }
459
+ }
460
+
461
+ checkScreens = () => {
462
+ // Attempt to detect screens from noVNC
463
+ let screens = this.rfb?.screens
464
+ if (!screens && this.rfb?.capabilities?.screens) {
465
+ screens = this.rfb.capabilities.screens
466
+ }
467
+
468
+ if (screens && screens.length) {
469
+ this.setState({
470
+ screens: this.processScreens(screens)
471
+ })
472
+ }
473
+ }
474
+
475
+ componentDidUpdate (prevProps, prevState) {
476
+ if (
477
+ prevProps.width !== this.props.width ||
478
+ prevProps.height !== this.props.height ||
479
+ prevState.currentScreen !== this.state.currentScreen
480
+ ) {
481
+ this.updateViewLayout()
482
+ }
483
+ }
484
+
485
+ updateViewLayout = () => {
486
+ const { currentScreen, screens } = this.state
487
+ const { scaleViewport = true, clipViewport = false } = this.props.tab
488
+ const container = this.domRef.current ? this.domRef.current.firstElementChild : null
489
+
490
+ if (!this.rfb || !container) {
491
+ return
492
+ }
493
+
494
+ // Force re-apply scaleViewport to trigger autoscale logic (if enabled)
495
+ // or reset to 1.0 (if disabled)
496
+ this.rfb.scaleViewport = scaleViewport
497
+ this.rfb.clipViewport = clipViewport
498
+
499
+ // Explicitly trigger autoscale to ensure our patched logic runs
500
+ if (scaleViewport && this.rfb._display) {
501
+ this.rfb._display.autoscale(container.clientWidth, container.clientHeight)
502
+ }
503
+
504
+ // Handle scrolling to the selected screen
505
+ this.timer = setTimeout(() => {
506
+ if (currentScreen === 'all') {
507
+ container.scrollTo(0, 0)
508
+ } else {
509
+ const screen = screens.find(s => s.id === currentScreen)
510
+ if (screen) {
511
+ const scale = this.rfb._display.scale
512
+ container.scrollTo(screen.x * scale, screen.y * scale)
513
+ }
514
+ }
515
+ }, 50)
516
+ }
517
+
518
+ handleSelectScreen = (id) => {
519
+ this.setState({
520
+ currentScreen: id
521
+ })
522
+ // updateViewLayout will be called by componentDidUpdate
199
523
  }
200
524
 
201
525
  getDom = () => {
@@ -224,10 +548,8 @@ export default class VncSession extends RdpSession {
224
548
  )
225
549
  }
226
550
 
227
- renderHelp = () => {
228
- return (
229
- <Tag color='red' className='mg1l' variant='solid'>Beta</Tag>
230
- )
551
+ handleSendCtrlAltDel = () => {
552
+ this.rfb?.sendCtrlAltDel()
231
553
  }
232
554
 
233
555
  renderConfirm () {
@@ -254,31 +576,54 @@ export default class VncSession extends RdpSession {
254
576
 
255
577
  render () {
256
578
  const { width: w, height: h } = this.props
257
- const vncProps = {
258
- style: {
259
- width: w + 'px',
260
- height: h + 'px'
261
- }
579
+ const { loading } = this.state
580
+ const { remoteWidth, remoteHeight } = this.getScreenSize()
581
+ const { scaleViewport = true } = this.props.tab
582
+ // When not in scale mode, we need a wrapper with container size,
583
+ // and the inner div should have remote screen size to show scrollbars
584
+ const isScaled = scaleViewport
585
+
586
+ const {
587
+ width: innerWidth,
588
+ height: innerHeight
589
+ } = this.calcCanvasSize()
590
+ const wrapperStyle = {
591
+ width: innerWidth + 'px',
592
+ height: innerHeight + 'px',
593
+ overflow: isScaled ? 'hidden' : 'auto'
594
+ }
595
+ const remoteStyle = {
596
+ width: isScaled ? '100%' : remoteWidth + 'px',
597
+ height: isScaled ? '100%' : remoteHeight + 'px',
598
+ overflow: 'hidden'
262
599
  }
263
- const { width, height, loading } = this.state
264
600
  const divProps = {
265
- style: {
266
- width: width + 'px',
267
- height: height + 'px'
268
- }
601
+ style: remoteStyle,
602
+ className: 'vnc-session-wrap session-v-wrap'
269
603
  }
604
+ const contrlProps = this.getControlProps()
270
605
  return (
271
606
  <Spin spinning={loading}>
272
607
  <div
273
- {...vncProps}
274
608
  className='rdp-session-wrap pd1'
609
+ style={{
610
+ width: w + 'px',
611
+ height: h + 'px'
612
+ }}
275
613
  >
276
614
  {this.renderControl()}
277
- <div
278
- {...divProps}
279
- className='vnc-session-wrap session-v-wrap'
280
- ref={this.domRef}
615
+ <RemoteFloatControl
616
+ {...contrlProps}
281
617
  />
618
+ <div
619
+ style={wrapperStyle}
620
+ className='vnc-scroll-wrapper'
621
+ >
622
+ <div
623
+ {...divProps}
624
+ ref={this.domRef}
625
+ />
626
+ </div>
282
627
  {this.renderConfirm()}
283
628
  </div>
284
629
  </Spin>
@@ -25,6 +25,9 @@ body
25
25
  background-color var(--primary)
26
26
  outline none
27
27
 
28
+ ::-webkit-scrollbar-corner
29
+ background-color transparent
30
+
28
31
  .common-err
29
32
  max-height 100px
30
33
  overflow-y scroll
@@ -37,8 +37,8 @@ export default Store => {
37
37
  window.dispatchEvent(new Event('resize'))
38
38
  }
39
39
 
40
- Store.prototype.toggleTermFullscreen = function (terminalFullScreen) {
41
- window.store.terminalFullScreen = terminalFullScreen
40
+ Store.prototype.toggleSessFullscreen = function (fullscreen) {
41
+ window.store.fullscreen = fullscreen
42
42
  setTimeout(window.store.triggerResize, 500)
43
43
  }
44
44
  }
@@ -193,7 +193,7 @@ export default () => {
193
193
  height: 500,
194
194
  isMaximized: window.pre.runSync('isMaximized'),
195
195
  hasNodePty: window.pre.runSync('nodePtyCheck'),
196
- terminalFullScreen: false,
196
+ fullscreen: false,
197
197
  hideDelKeyTip: ls.getItem(dismissDelKeyTipLsKey) === 'y',
198
198
  tabsHeight: 36,
199
199
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "2.7.9",
3
+ "version": "2.8.6",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",
@@ -1,14 +0,0 @@
1
- export function formatBytes (sizeInKB) {
2
- if (!sizeInKB) {
3
- return ''
4
- }
5
- const units = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
6
- let index = 0
7
-
8
- while (sizeInKB >= 1024 && index < units.length - 1) {
9
- sizeInKB /= 1024
10
- index++
11
- }
12
-
13
- return `${sizeInKB.toFixed(2)} ${units[index]}`
14
- }
@@ -1,21 +0,0 @@
1
- /**
2
- * btns
3
- */
4
-
5
- import { FullscreenExitOutlined } from '@ant-design/icons'
6
- import './term-fullscreen.styl'
7
-
8
- export default function TermFullscreenControl (props) {
9
- const handleExitFullscreen = () => {
10
- window.store.toggleTermFullscreen(false)
11
- }
12
- if (!props.terminalFullScreen) {
13
- return null
14
- }
15
- return (
16
- <FullscreenExitOutlined
17
- className='mg1r icon-info font16 pointer spliter term-fullscreen-control'
18
- onClick={handleExitFullscreen}
19
- />
20
- )
21
- }
@@ -1,55 +0,0 @@
1
- // import zmodem from 'zmodem-ts/dist/zmodem.mjs'
2
- import Sentry from 'zmodem-ts/dist/zsentry.js'
3
- export class AddonZmodem {
4
- _disposables = []
5
-
6
- activate (terminal) {
7
- terminal.zmodemAttach = this.zmodemAttach
8
- }
9
-
10
- sendWebSocket = (octets) => {
11
- const { socket } = this
12
- if (socket && socket.readyState === WebSocket.OPEN) {
13
- return socket.send(new Uint8Array(octets))
14
- } else {
15
- console.error('WebSocket is not open')
16
- }
17
- }
18
-
19
- zmodemAttach = (ctx) => {
20
- this.socket = ctx.socket
21
- this.term = ctx.term
22
- this.ctx = ctx
23
- this.zsentry = new Sentry({
24
- to_terminal: (octets) => {
25
- if (ctx.onZmodem) {
26
- this.term.write(String.fromCharCode.apply(String, octets))
27
- }
28
- },
29
- sender: this.sendWebSocket,
30
- on_retract: ctx.onzmodemRetract,
31
- on_detect: ctx.onZmodemDetect
32
- })
33
- this.socket.binaryType = 'arraybuffer'
34
- this.socket.addEventListener('message', this.handleWSMessage)
35
- }
36
-
37
- handleWSMessage = (evt) => {
38
- if (typeof evt.data === 'string') {
39
- if (this.ctx.onZmodem) {
40
- this.term.write(evt.data)
41
- }
42
- } else {
43
- this.zsentry.consume(evt.data)
44
- }
45
- }
46
-
47
- dispose = () => {
48
- this.socket && this.socket.removeEventListener('message', this.handleWSMessage)
49
- this._disposables.forEach(d => d.dispose())
50
- this._disposables.length = 0
51
- this.term = null
52
- this.zsentry = null
53
- this.socket = null
54
- }
55
- }