@electerm/electerm-react 1.37.128 → 1.38.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,7 +35,7 @@ export const filePropMinWidth = 1
35
35
  export const contextMenuHeight = 28
36
36
  export const contextMenuWidth = 280
37
37
  export const contextMenuPaddingTop = 10
38
- export const sftpControlHeight = 28 + 42 + 33 + 46
38
+ export const sftpControlHeight = 28 + 42 + 33 + 36
39
39
  export const sidebarWidth = 43
40
40
  export const maxHistory = 50
41
41
  export const maxTransport = 5
@@ -339,3 +339,4 @@ export const instSftpKeys = [
339
339
  'writeFile'
340
340
  ]
341
341
  export const cwdId = '=__+__'
342
+ export const zmodemTransferPackSize = 1024 * 1024 * 2
@@ -31,7 +31,7 @@ export default {
31
31
  checkUpdateOnStart: true,
32
32
  cursorBlink: false,
33
33
  cursorStyle: 'block',
34
- useSystemTitleBar: false,
34
+ useSystemTitleBar: window.pre.isLinux || window.et.isLinux || false,
35
35
  opacity: 1,
36
36
  defaultEditor: '',
37
37
  terminalWordSeparator: './\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~ ?',
@@ -104,7 +104,7 @@ const fs = {
104
104
  const cb = args.pop()
105
105
  window.fs.readCustom(
106
106
  p1,
107
- encodeUint8Array(arr),
107
+ arr.length,
108
108
  ...args
109
109
  )
110
110
  .then((data) => {
@@ -73,6 +73,7 @@ export default class Index extends Component {
73
73
  const cls = classnames({
74
74
  loaded: configLoaded,
75
75
  'system-ui': store.config.useSystemTitleBar,
76
+ 'not-system-ui': !store.config.useSystemTitleBar,
76
77
  'is-mac': isMac,
77
78
  'is-win': isWin,
78
79
  pinned,
@@ -29,6 +29,7 @@ export default class QuickCommandsFooter extends PureComponent {
29
29
  size='small'
30
30
  onMouseEnter={this.handleOpen}
31
31
  onMouseLeave={this.handleMouseLeave}
32
+ type='ghost'
32
33
  >
33
34
  <span className='w500'>{e('quickCommands')}</span>
34
35
  <span className='l500'>Q</span>
@@ -49,6 +49,13 @@ class Sessions extends Component {
49
49
  )
50
50
  }
51
51
 
52
+ reloadCurrentTabShortcut = (e) => {
53
+ e.stopPropagation()
54
+ this.reloadTab(
55
+ this.getCurrentTab()
56
+ )
57
+ }
58
+
52
59
  watch = () => {
53
60
  window.addEventListener('message', this.onEvent)
54
61
  }
@@ -144,7 +144,8 @@ export default class SettingModalWrap extends Component {
144
144
  const {
145
145
  showModal,
146
146
  hideSettingModal,
147
- innerWidth
147
+ innerWidth,
148
+ useSystemTitleBar
148
149
  } = this.props.store
149
150
  const show = showModal === modals.setting
150
151
  if (!show) {
@@ -154,6 +155,7 @@ export default class SettingModalWrap extends Component {
154
155
  <SettingModal
155
156
  onCancel={hideSettingModal}
156
157
  visible={show}
158
+ useSystemTitleBar={useSystemTitleBar}
157
159
  innerWidth={innerWidth}
158
160
  >
159
161
  {this.renderTabs()}
@@ -6,9 +6,16 @@ import { Component } from 'react'
6
6
  import { Drawer } from 'antd'
7
7
  import { CloseCircleOutlined } from '@ant-design/icons'
8
8
  import { sidebarWidth } from '../../common/constants'
9
+ import AppDrag from '../tabs/app-drag'
9
10
  import './setting-wrap.styl'
10
11
 
11
12
  export default class SettingWrap extends Component {
13
+ renderDrag () {
14
+ return (
15
+ <AppDrag />
16
+ )
17
+ }
18
+
12
19
  render () {
13
20
  const pops = {
14
21
  open: this.props.visible,
@@ -32,6 +39,9 @@ export default class SettingWrap extends Component {
32
39
  className='close-setting-wrap'
33
40
  onClick={this.props.onCancel}
34
41
  />
42
+ {
43
+ this.props.useSystemTitleBar ? null : <AppDrag />
44
+ }
35
45
  {this.props.visible ? this.props.children : null}
36
46
  </Drawer>
37
47
  )
@@ -3,4 +3,11 @@
3
3
  padding 0
4
4
  margin 5px 5px 0 0
5
5
  .font-sel
6
- min-width 200px
6
+ min-width 200px
7
+ .setting-wrap .app-drag
8
+ position fixed
9
+ top 0
10
+ left 43px
11
+ height 60px
12
+ right 0
13
+ z-index 888
@@ -75,6 +75,17 @@ export function shortcutExtend (Cls) {
75
75
  ) {
76
76
  return true
77
77
  }
78
+ if (
79
+ this.term &&
80
+ key === 'c' &&
81
+ type === 'keydown' &&
82
+ !altKey &&
83
+ !shiftKey &&
84
+ ctrlKey &&
85
+ this.onZmodem
86
+ ) {
87
+ this.onZmodemEnd()
88
+ }
78
89
  const codeName = event instanceof window.WheelEvent
79
90
  ? (wheelDeltaY > 0 ? 'mouseWheelUp' : 'mouseWheelDown')
80
91
  : code
@@ -5,6 +5,11 @@ export default () => {
5
5
  shortcut: 'alt+w',
6
6
  shortcutMac: 'alt+w'
7
7
  },
8
+ {
9
+ name: 'app_reloadCurrentTab',
10
+ shortcut: 'alt+r',
11
+ shortcutMac: 'alt+r'
12
+ },
8
13
  {
9
14
  name: 'app_newBookmark',
10
15
  shortcut: 'ctrl+n',
@@ -26,6 +26,9 @@
26
26
  box-shadow 0px 0px 3px 3px alpha(main, .1)
27
27
  .item-list
28
28
  padding-right 0
29
+ .not-system-ui.is-mac
30
+ .sidebar-bar
31
+ margin-top 20px
29
32
  .type-bookmarks
30
33
  .bookmarks-panel
31
34
  width 100%
@@ -50,7 +53,7 @@
50
53
  .sidebar-list
51
54
  position absolute
52
55
  left 43px
53
- top 45px
56
+ top 36px
54
57
  bottom 0
55
58
  z-index 200
56
59
  width 0
@@ -18,7 +18,13 @@ import {
18
18
  import { Dropdown, Menu, Popover } from 'antd'
19
19
  import Tab from './tab'
20
20
  import './tabs.styl'
21
- import { tabWidth, tabMargin, extraTabWidth, windowControlWidth } from '../../common/constants'
21
+ import {
22
+ tabWidth,
23
+ tabMargin,
24
+ extraTabWidth,
25
+ windowControlWidth,
26
+ isMacJs
27
+ } from '../../common/constants'
22
28
  import findParentBySel from '../../common/find-parent'
23
29
  import WindowControl from './window-control'
24
30
  import BookmarksList from '../sidebar/bookmark-select'
@@ -239,8 +245,9 @@ export default class Tabs extends React.Component {
239
245
  const left = overflow
240
246
  ? '100%'
241
247
  : tabsWidthAll
248
+ const w1 = isMacJs ? 30 : windowControlWidth
242
249
  const style = {
243
- width: width - windowControlWidth - 136
250
+ width: width - w1 - 136
244
251
  }
245
252
  return (
246
253
  <div
@@ -2,7 +2,7 @@
2
2
  * file section
3
3
  */
4
4
 
5
- import React from 'react'
5
+ import { Component } from 'react'
6
6
  import runIdle from '../../common/run-idle'
7
7
  import {
8
8
  CloseOutlined,
@@ -21,7 +21,7 @@ import {
21
21
  terminalSshConfigType,
22
22
  commonActions
23
23
  } from '../../common/constants'
24
- import TabTitle from './tab-title'
24
+ import { shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
25
25
 
26
26
  const { prefix } = window
27
27
  const e = prefix('tabs')
@@ -29,7 +29,7 @@ const m = prefix('menu')
29
29
  const onDragCls = 'ondrag-tab'
30
30
  const onDragOverCls = 'dragover-tab'
31
31
 
32
- export default class Tab extends React.Component {
32
+ class Tab extends Component {
33
33
  constructor (props) {
34
34
  super(props)
35
35
  this.state = {
@@ -245,13 +245,14 @@ export default class Tab extends React.Component {
245
245
  setTabs(tabs)
246
246
  }
247
247
 
248
- renderContext () {
248
+ renderContext = () => {
249
249
  const { tabs, tab } = this.props
250
250
  const len = tabs.length
251
251
  const index = findIndex(tabs, t => t.id === tab.id)
252
252
  const noRight = index >= len - 1
253
253
  const isSshConfig = tab.type === terminalSshConfigType
254
254
  const res = []
255
+ const reloadShortcut = this.getShortcut('app_reloadCurrentTab')
255
256
  res.push({
256
257
  func: 'handleClose',
257
258
  icon: 'CloseOutlined',
@@ -288,7 +289,8 @@ export default class Tab extends React.Component {
288
289
  res.push({
289
290
  func: 'handleReloadTab',
290
291
  icon: 'Loading3QuartersOutlined',
291
- text: m('reload')
292
+ text: m('reload'),
293
+ subText: reloadShortcut
292
294
  })
293
295
  return res
294
296
  }
@@ -379,6 +381,10 @@ export default class Tab extends React.Component {
379
381
  if (isEditting) {
380
382
  return this.renderEditting(tab, cls)
381
383
  }
384
+ const { tabCount, color } = tab
385
+ const styleTag = color
386
+ ? { borderTop: `1px solid ${color}` }
387
+ : {}
382
388
  return (
383
389
  <Tooltip
384
390
  title={title}
@@ -404,6 +410,7 @@ export default class Tab extends React.Component {
404
410
  className='tab-title elli pd1x'
405
411
  onClick={this.handleClick}
406
412
  onDoubleClick={this.handleDup}
413
+ style={styleTag}
407
414
  onContextMenu={this.handleContextMenu}
408
415
  >
409
416
  <Loading3QuartersOutlined
@@ -411,7 +418,9 @@ export default class Tab extends React.Component {
411
418
  onClick={this.handleReloadTab}
412
419
  title={m('reload')}
413
420
  />
414
- <TabTitle tab={tab} />
421
+ <span className='tab-title'>
422
+ {tabCount}. {title}
423
+ </span>
415
424
  </div>
416
425
  <div className={'tab-status ' + status} />
417
426
  <div className='tab-traffic' />
@@ -424,3 +433,5 @@ export default class Tab extends React.Component {
424
433
  )
425
434
  }
426
435
  }
436
+
437
+ export default shortcutDescExtend(Tab)
@@ -1,17 +1,24 @@
1
1
  @require '../../css/includes/theme-default'
2
2
  .tabs
3
3
  position relative
4
- height 45px
4
+ height 36px
5
5
  overflow hidden
6
6
  background main-dark
7
-
7
+ ::-webkit-scrollbar
8
+ width 0
9
+ display none
10
+ .not-system-ui.is-mac
11
+ .tabs-inner
12
+ margin-left 72px
13
+ .tabs-extra
14
+ right 0
8
15
  .tabs-inner
9
16
  position relative
10
17
  z-index 2
11
18
  padding 0
12
19
  margin-top 0
13
20
  display inline-block
14
- height 63px
21
+ height 36px
15
22
  overflow-x scroll
16
23
  overflow-y hidden
17
24
  margin-left 42px
@@ -25,7 +32,7 @@
25
32
  min-width 100px
26
33
  max-width 200px
27
34
  line-height 36px
28
- margin 10px 1px 0 0
35
+ margin 0 1px 0 0
29
36
  border-radius 3px 3px 0 0
30
37
  background main-dark
31
38
  text-align center
@@ -118,13 +125,13 @@
118
125
  .tabs-add-btn
119
126
  display inline-block
120
127
  vertical-align middle
121
- margin 10px 3px 0 3px
128
+ margin 0 3px 0 3px
122
129
  -webkit-app-region no-drag
123
130
  color text
124
131
  &.empty
125
132
  font-size 20px
126
133
  margin-left 20px
127
- margin-top 20px
134
+ margin-top 10px
128
135
  &:hover
129
136
  color text-light
130
137
  .tabs-extra
@@ -4,6 +4,9 @@
4
4
 
5
5
  import { CloseOutlined, MinusOutlined } from '@ant-design/icons'
6
6
  import { Component } from '../common/react-subx'
7
+ import {
8
+ isMacJs
9
+ } from '../../common/constants'
7
10
 
8
11
  const { prefix } = window
9
12
  const m = prefix('menu')
@@ -14,7 +17,7 @@ export default class WindowControl extends Component {
14
17
  isMaximized,
15
18
  config
16
19
  } = this.props.store
17
- if (config.useSystemTitleBar) {
20
+ if (config.useSystemTitleBar || isMacJs) {
18
21
  return null
19
22
  }
20
23
  const minimize = () => {
@@ -7,12 +7,16 @@ import strip from '@electerm/strip-ansi'
7
7
  export default class AttachAddonCustom extends AttachAddon {
8
8
  constructor (term, options, encode, isWindowsShell) {
9
9
  super(term, options)
10
+ this.term = term
10
11
  this.decoder = new TextDecoder(encode)
11
12
  this.isWindowsShell = isWindowsShell
12
13
  }
13
14
 
14
- activate (terminal) {
15
+ activate (terminal = this.term) {
15
16
  const writeToTerminal = (data) => {
17
+ if (terminal.parent?.onZmodem) {
18
+ return
19
+ }
16
20
  if (typeof data === 'string') {
17
21
  return terminal.write(data)
18
22
  }
@@ -79,6 +83,12 @@ export default class AttachAddonCustom extends AttachAddon {
79
83
  this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose()))
80
84
  this._disposables.push(addSocketListener(this._socket, 'error', () => this.dispose()))
81
85
  }
86
+
87
+ dispose () {
88
+ this.term = null
89
+ this._disposables.forEach(d => d.dispose())
90
+ this._disposables.length = 0
91
+ }
82
92
  }
83
93
 
84
94
  function addSocketListener (socket, type, handler) {
@@ -86,7 +96,6 @@ function addSocketListener (socket, type, handler) {
86
96
  return {
87
97
  dispose: () => {
88
98
  if (!handler) {
89
- // Already disposed
90
99
  return
91
100
  }
92
101
  socket.removeEventListener(type, handler)
@@ -0,0 +1,59 @@
1
+ export const open = (filePath, flag) => {
2
+ const fs = window.require('fs')
3
+ return new Promise((resolve, reject) => {
4
+ fs.open(filePath, flag, (err, fd) => {
5
+ if (err) {
6
+ return reject(err)
7
+ }
8
+ return resolve(fd)
9
+ })
10
+ })
11
+ }
12
+
13
+ export const close = (fd) => {
14
+ const fs = window.require('fs')
15
+ return new Promise((resolve, reject) => {
16
+ fs.close(fd, (err) => {
17
+ if (err) {
18
+ return reject(err)
19
+ }
20
+ return resolve(true)
21
+ })
22
+ })
23
+ }
24
+
25
+ export const exists = (filePath) => {
26
+ const fs = window.require('fs')
27
+ return new Promise((resolve, reject) => {
28
+ fs.access(filePath, (err) => {
29
+ if (err) {
30
+ return reject(err)
31
+ }
32
+ return resolve(true)
33
+ })
34
+ })
35
+ }
36
+
37
+ export const read = (fd, buffer, offset, length, position) => {
38
+ const fs = window.require('fs')
39
+ return new Promise((resolve, reject) => {
40
+ fs.read(fd, buffer, offset, length, position, (err, bytesRead, buffer) => {
41
+ if (err) {
42
+ return reject(err)
43
+ }
44
+ return resolve(buffer.subarray(0, bytesRead))
45
+ })
46
+ })
47
+ }
48
+
49
+ export const write = (fd, buffer) => {
50
+ const fs = window.require('fs')
51
+ return new Promise((resolve, reject) => {
52
+ fs.write(fd, buffer, (err, buffer) => {
53
+ if (err) {
54
+ return reject(err)
55
+ }
56
+ return resolve(buffer)
57
+ })
58
+ })
59
+ }
@@ -1,5 +1,4 @@
1
1
  import { Component } from 'react'
2
- import ZmodemTransfer from './zmodem-transfer'
3
2
  import { handleErr } from '../../common/fetch'
4
3
  import generate from '../../common/uid'
5
4
  import { isEqual, pick, debounce, throttle } from 'lodash-es'
@@ -27,7 +26,8 @@ import {
27
26
  commonActions,
28
27
  rendererTypes,
29
28
  cwdId,
30
- isMac
29
+ isMac,
30
+ zmodemTransferPackSize
31
31
  } from '../../common/constants'
32
32
  import deepCopy from 'json-deep-copy'
33
33
  import { readClipboardAsync, copy } from '../../common/clipboard'
@@ -39,7 +39,7 @@ import { CanvasAddon } from 'xterm-addon-canvas'
39
39
  import { WebglAddon } from 'xterm-addon-webgl'
40
40
  import { LigaturesAddon } from 'xterm-addon-ligatures'
41
41
  import getProxy from '../../common/get-proxy'
42
- import { Zmodem, AddonZmodem } from './xterm-zmodem'
42
+ import { AddonZmodem } from './xterm-zmodem'
43
43
  import { Unicode11Addon } from 'xterm-addon-unicode11'
44
44
  import keyControlPressed from '../../common/key-control-pressed'
45
45
  import { Terminal } from 'xterm'
@@ -47,8 +47,11 @@ import NormalBuffer from './normal-buffer'
47
47
  import { createTerm, resizeTerm } from './terminal-apis'
48
48
  import { shortcutExtend, shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
49
49
  import { KeywordHighlighterAddon } from './highlight-addon.js'
50
+ import { getLocalFileInfo } from '../sftp/file-read.js'
50
51
  import { SerializeAddon } from 'xterm-addon-serialize'
51
52
  import strip from '@electerm/strip-ansi'
53
+ import { formatBytes } from '../../common/byte-format.js'
54
+ import * as fs from './fs.js'
52
55
 
53
56
  const { prefix } = window
54
57
  const e = prefix('ssh')
@@ -71,7 +74,6 @@ class Term extends Component {
71
74
  saveTerminalLogToFile: !!this.props.config.saveTerminalLogToFile,
72
75
  addTimeStampToTermLog: !!this.props.config.addTimeStampToTermLog,
73
76
  passType: 'password',
74
- zmodemTransfer: null,
75
77
  lines: []
76
78
  }
77
79
  }
@@ -128,6 +130,9 @@ class Term extends Component {
128
130
  }
129
131
 
130
132
  componentWillUnmount () {
133
+ if (this.zsession) {
134
+ this.onZmodemEnd()
135
+ }
131
136
  delete this.term.parent
132
137
  Object.keys(this.timers).forEach(k => {
133
138
  clearTimeout(this.timers[k])
@@ -161,6 +166,21 @@ class Term extends Component {
161
166
  }
162
167
  ]
163
168
 
169
+ initAttachAddon = (encode) => {
170
+ this.attachAddon = new AttachAddon(
171
+ this.socket,
172
+ undefined,
173
+ this.props.tab.encode,
174
+ isWin && !this.isRemote()
175
+ )
176
+ if (encode || this.decode) {
177
+ this.attachAddon.decoder = encode
178
+ ? new TextDecoder(encode)
179
+ : this.decode
180
+ }
181
+ this.term.loadAddon(this.attachAddon)
182
+ }
183
+
164
184
  getValue = (props, type, name) => {
165
185
  return type === 'glob'
166
186
  ? props.config[name]
@@ -420,152 +440,259 @@ class Term extends Component {
420
440
  log.debug('zmodemRetract')
421
441
  }
422
442
 
423
- onReceiveZmodemSession = () => {
424
- // * zmodem transfer
425
- // * then run rz to send from your browser or
426
- // * sz <file> to send from the remote peer.
443
+ writeBanner = (type) => {
427
444
  this.term.write('\r\nRecommmend use trzsz instead: https://github.com/trzsz/trzsz\r\n')
445
+ this.term.write(`\x1b[32mZMODEM::${type}::START\x1b[0m\r\n\r\n`)
446
+ }
447
+
448
+ onReceiveZmodemSession = async () => {
449
+ const savePath = await this.openSaveFolderSelect()
428
450
  this.zsession.on('offer', this.onOfferReceive)
429
451
  this.zsession.start()
452
+ this.term.write('\r\n\x1b[2A\r\n')
453
+ if (!savePath) {
454
+ return this.onZmodemEnd()
455
+ }
456
+ this.writeBanner('RECEIVE')
457
+ this.zmodemSavePath = savePath
430
458
  return new Promise((resolve) => {
431
459
  this.zsession.on('session_end', resolve)
432
- }).then(this.onZmodemEnd).catch(this.onZmodemCatch)
460
+ })
461
+ .then(this.onZmodemEnd)
462
+ .catch(this.onZmodemCatch)
433
463
  }
434
464
 
435
- updateProgress = (xfer, type) => {
436
- if (this.onCanceling) {
465
+ initZmodemDownload = async (name, size) => {
466
+ if (!this.zmodemSavePath) {
437
467
  return
438
468
  }
439
- const fileInfo = xfer.get_details()
440
- const {
441
- size
442
- } = fileInfo
443
- const total = xfer.get_offset() || 0
444
- let percent = Math.floor(100 * total / size)
445
- if (percent > 99) {
446
- percent = 99
469
+ let pth = window.pre.resolve(
470
+ this.zmodemSavePath, name
471
+ )
472
+ const exist = await fs.exists(pth).catch(() => false)
473
+ if (exist) {
474
+ pth = pth + '.' + generate()
447
475
  }
448
- this.setState({
449
- zmodemTransfer: {
450
- fileInfo,
451
- percent,
452
- transferedSize: total,
453
- type
454
- }
455
- })
476
+ const fd = await fs.open(pth, 'w').catch(this.onZmodemEnd)
477
+ this.downloadFd = fd
478
+ this.downloadPath = pth
479
+ this.downloadCount = 0
480
+ this.zmodemStartTime = Date.now()
481
+ this.downloadSize = size
482
+ this.updateZmodemProgress(
483
+ 0, pth, size, transferTypeMap.download
484
+ )
485
+ return fd
456
486
  }
457
487
 
458
- saveToDisk = (xfer, buffer) => {
459
- return Zmodem.Browser
460
- .save_to_disk(buffer, xfer.get_details().name)
488
+ onOfferReceive = async (xfer) => {
489
+ const {
490
+ name,
491
+ size
492
+ } = xfer.get_details()
493
+ if (!this.downloadFd) {
494
+ await this.initZmodemDownload(name, size)
495
+ }
496
+ xfer.on('input', this.onZmodemDownload)
497
+ this.xfer = xfer
498
+ await xfer.accept()
499
+ .then(this.finishZmodemTransfer)
500
+ .catch(this.onZmodemEnd)
461
501
  }
462
502
 
463
- onOfferReceive = xfer => {
464
- this.updateProgress(xfer, transferTypeMap.download)
465
- const FILE_BUFFER = []
466
- xfer.on('input', (payload) => {
467
- this.updateProgress(xfer, transferTypeMap.download)
468
- FILE_BUFFER.push(new Uint8Array(payload))
469
- })
470
- xfer.accept()
471
- .then(
472
- () => {
473
- this.saveToDisk(xfer, FILE_BUFFER)
474
- }
475
- )
476
- .catch(window.store.onError)
503
+ checkCache = async () => {
504
+ if (this.DownloadCache?.length > 0) {
505
+ return fs.write(this.downloadFd, new Uint8Array(this.DownloadCache))
506
+ }
477
507
  }
478
508
 
479
- beforeZmodemUpload = (file, files) => {
480
- if (!files.length) {
481
- return false
509
+ onZmodemDownload = async payload => {
510
+ if (this.onCanceling || !this.downloadFd) {
511
+ return
482
512
  }
483
- // const f = files[0]
484
- // if (f.size > maxZmodemUploadSize) {
485
- // if (this.zsession) {
486
- // this.zsession.abort()
487
- // }
488
- // this.onZmodemEnd()
489
- // // if (this.props.tab.enableSftp) {
490
- // // notification.info({
491
- // // message: `Uploading by sftp`,
492
- // // duration: 8
493
- // // })
494
- // // return this.transferBySftp(files)
495
- // // } else {
496
- // const url = 'https://github.com/FGasper/zmodemjs/issues/11'
497
- // const msg = (
498
- // <div>
499
- // <p>Currently <b>rz</b> only support upload file size less than {filesize(maxZmodemUploadSize)}, due to known issue:</p>
500
- // <p><Link to={url}>{url}</Link></p>
501
- // <p>You can try upload in sftp which is much faster.</p>
502
- // </div>
513
+ // if (!this.DownloadCache) {
514
+ // this.DownloadCache = []
515
+ // }
516
+ // this.DownloadCache = this.DownloadCache.concat(payload)
517
+ // this.downloadCount += payload.length
518
+ // if (this.DownloadCache.length < zmodemTransferPackSize) {
519
+ // return this.updateZmodemProgress(
520
+ // this.downloadCount,
521
+ // this.downloadPath,
522
+ // this.downloadSize,
523
+ // transferTypeMap.download
503
524
  // )
504
- // notification.error({
505
- // message: msg,
506
- // duration: 8
507
- // })
508
- // // }
509
525
  // }
510
- const th = this
511
- Zmodem.Browser.send_files(
512
- this.zsession,
513
- files, {
514
- on_offer_response (obj, xfer) {
515
- if (xfer) {
516
- th.updateProgress(xfer, transferTypeMap.upload)
517
- }
518
- },
519
- on_progress (obj, xfer) {
520
- th.updateProgress(xfer, transferTypeMap.upload)
521
- }
522
- }
526
+ // this.writeCache = this.DownloadCache
527
+ // this.DownloadCache = []
528
+ this.downloadCount += payload.length
529
+ await fs.write(this.downloadFd, new Uint8Array(payload))
530
+ this.updateZmodemProgress(
531
+ this.downloadCount,
532
+ this.downloadPath,
533
+ this.downloadSize,
534
+ transferTypeMap.download
523
535
  )
524
- .then(th.onZmodemEndSend)
525
- .catch(th.onZmodemCatch)
536
+ }
526
537
 
527
- return false
538
+ updateZmodemProgress = throttle((start, name, size, type) => {
539
+ this.zmodemTransfer = {
540
+ type,
541
+ start,
542
+ name,
543
+ size
544
+ }
545
+ this.writeZmodemProgress()
546
+ }, 500)
547
+
548
+ finishZmodemTransfer = () => {
549
+ this.zmodemTransfer = {
550
+ ...this.zmodemTransfer,
551
+ start: this.zmodemTransfer.size
552
+ }
553
+ this.writeZmodemProgress()
528
554
  }
529
555
 
530
- onSendZmodemSession = () => {
531
- this.setState(() => {
532
- return {
533
- zmodemTransfer: {
534
- type: transferTypeMap.upload
535
- }
556
+ writeZmodemProgress = () => {
557
+ if (this.onCanceling) {
558
+ return
559
+ }
560
+ const {
561
+ size, start, name
562
+ } = this.zmodemTransfer
563
+ const speed = size > 0 ? formatBytes(start * 1000 / 1024 / (Date.now() - this.zmodemStartTime)) : 0
564
+ const percent = size > 0 ? Math.floor(start * 100 / size) : 100
565
+ const str = `\x1b[32m${name}\x1b[0m::${percent}%,${start}/${size},${speed}/s`
566
+ this.term.write('\r\n\x1b[2A' + str + '\n')
567
+ }
568
+
569
+ zmodemTransferFile = async (file, filesRemaining, sizeRemaining) => {
570
+ const offer = {
571
+ obj: file,
572
+ name: file.name,
573
+ size: file.size,
574
+ files_remaining: filesRemaining,
575
+ bytes_remaining: sizeRemaining
576
+ }
577
+ const xfer = await this.zsession.send_offer(offer)
578
+ if (!xfer) {
579
+ this.onZmodemEnd()
580
+ return window.store.onError(new Error('Transfer cancelled, maybe file already exists'))
581
+ }
582
+ this.zmodemStartTime = Date.now()
583
+ const fd = await fs.open(file.filePath, 'r')
584
+ let start = 0
585
+ const { size } = file
586
+ let inited = false
587
+ while (start < size || !inited) {
588
+ const rest = size - start
589
+ const len = rest > zmodemTransferPackSize ? zmodemTransferPackSize : rest
590
+ const buffer = new Uint8Array(len)
591
+ const newArr = await fs.read(fd, buffer, 0, len, null)
592
+ const n = newArr.length
593
+ await xfer.send(newArr)
594
+ start = start + n
595
+ inited = true
596
+ this.updateZmodemProgress(start, file.name, size, transferTypeMap.upload)
597
+ if (n < zmodemTransferPackSize || start >= file.size || this.onCanceling) {
598
+ break
536
599
  }
600
+ }
601
+ await fs.close(fd)
602
+ this.finishZmodemTransfer()
603
+ await xfer.end()
604
+ }
605
+
606
+ openFileSelect = async () => {
607
+ const properties = [
608
+ 'openFile',
609
+ 'multiSelections',
610
+ 'showHiddenFiles',
611
+ 'noResolveAliases',
612
+ 'treatPackageAsDirectory',
613
+ 'dontAddToRecent'
614
+ ]
615
+ const files = await window.api.openDialog({
616
+ title: 'Choose some files to send',
617
+ message: 'Choose some files to send',
618
+ properties
537
619
  })
620
+ if (!files || !files.length) {
621
+ return this.onZmodemEnd()
622
+ }
623
+ const r = []
624
+ for (const filePath of files) {
625
+ const stat = await getLocalFileInfo(filePath)
626
+ r.push({ ...stat, filePath })
627
+ }
628
+ return r
629
+ }
630
+
631
+ openSaveFolderSelect = async () => {
632
+ const savePaths = await window.api.openDialog({
633
+ title: 'Choose a folder to save file(s)',
634
+ message: 'Choose a folder to save file(s)',
635
+ properties: [
636
+ 'openDirectory',
637
+ 'showHiddenFiles',
638
+ 'createDirectory',
639
+ 'noResolveAliases',
640
+ 'treatPackageAsDirectory',
641
+ 'dontAddToRecent'
642
+ ]
643
+ })
644
+ if (!savePaths || !savePaths.length) {
645
+ return false
646
+ }
647
+ return savePaths[0]
538
648
  }
539
649
 
540
- cancelZmodem = () => {
541
- this.onZmodemEndSend()
650
+ beforeZmodemUpload = async (files) => {
651
+ if (!files || !files.length) {
652
+ return false
653
+ }
654
+ this.writeBanner('SEND')
655
+ let filesRemaining = files.length
656
+ let sizeRemaining = files.reduce((a, b) => a + b.size, 0)
657
+ for (const f of files) {
658
+ await this.zmodemTransferFile(f, filesRemaining, sizeRemaining)
659
+ filesRemaining = filesRemaining - 1
660
+ sizeRemaining = sizeRemaining - f.size
661
+ }
662
+ this.onZmodemEnd()
542
663
  }
543
664
 
544
- onZmodemEndSend = () => {
545
- this.zsession && this.zsession.close && this.zsession.close()
546
- this.onZmodemEnd()
665
+ onSendZmodemSession = async () => {
666
+ this.term.write('\r\n\x1b[2A\n')
667
+ const files = await this.openFileSelect()
668
+ this.beforeZmodemUpload(files)
547
669
  }
548
670
 
549
- onZmodemEnd = () => {
550
- delete this.onZmodem
671
+ onZmodemEnd = async () => {
672
+ delete this.zmodemSavePath
551
673
  this.onCanceling = true
552
- this.attachAddon = new AttachAddon(
553
- this.socket,
554
- undefined,
555
- this.props.tab.encode,
556
- isWin && !this.isRemote()
557
- )
558
- if (this.decoder) {
559
- this.attachAddon.decoder = this.decode
674
+ if (this.downloadFd) {
675
+ await fs.close(this.downloadFd)
560
676
  }
561
- this.term.loadAddon(this.attachAddon)
562
- this.setState(() => {
563
- return {
564
- zmodemTransfer: null
565
- }
566
- })
677
+ if (this.xfer && this.xfer.end) {
678
+ await this.xfer.end().catch(
679
+ console.error
680
+ )
681
+ }
682
+ delete this.xfer
683
+ if (this.zsession && this.zsession.close) {
684
+ await this.zsession.close().catch(
685
+ console.error
686
+ )
687
+ }
688
+ delete this.zsession
567
689
  this.term.focus()
568
690
  this.term.write('\r\n')
691
+ delete this.downloadFd
692
+ delete this.downloadPath
693
+ delete this.downloadCount
694
+ delete this.downloadSize
695
+ delete this.DownloadCache
569
696
  }
570
697
 
571
698
  onZmodemCatch = (e) => {
@@ -575,7 +702,6 @@ class Term extends Component {
575
702
 
576
703
  onZmodemDetect = detection => {
577
704
  this.onCanceling = false
578
- this.attachAddon.dispose()
579
705
  this.term.blur()
580
706
  this.onZmodem = true
581
707
  const zsession = detection.confirm()
@@ -1055,9 +1181,7 @@ class Term extends Component {
1055
1181
  this.zmodemAddon = new AddonZmodem()
1056
1182
  this.fitAddon.fit()
1057
1183
  term.loadAddon(this.zmodemAddon)
1058
- term.zmodemAttach(this.socket, {
1059
- noTerminalWriteOutsideSession: true
1060
- }, this)
1184
+ term.zmodemAttach(this)
1061
1185
  }
1062
1186
  term.displayRaw = displayRaw
1063
1187
  term.loadAddon(
@@ -1085,7 +1209,7 @@ class Term extends Component {
1085
1209
 
1086
1210
  onerrorSocket = err => {
1087
1211
  this.setStatus(statusMap.error)
1088
- log.warning('onerrorSocket', err)
1212
+ log.error('onerrorSocket', err)
1089
1213
  }
1090
1214
 
1091
1215
  oncloseSocket = () => {
@@ -1184,7 +1308,7 @@ class Term extends Component {
1184
1308
  }
1185
1309
 
1186
1310
  render () {
1187
- const { id, loading, zmodemTransfer } = this.state
1311
+ const { id, loading } = this.state
1188
1312
  const { height, width, left, top, position, id: pid, activeSplitId } = this.props
1189
1313
  const cls = classnames('term-wrap', {
1190
1314
  'not-first-term': !!position
@@ -1242,11 +1366,6 @@ class Term extends Component {
1242
1366
  close={this.closeNormalBuffer}
1243
1367
  />
1244
1368
  </div>
1245
- <ZmodemTransfer
1246
- zmodemTransfer={zmodemTransfer}
1247
- cancelZmodem={this.cancelZmodem}
1248
- beforeZmodemUpload={this.beforeZmodemUpload}
1249
- />
1250
1369
  <Spin className='loading-wrapper' spinning={loading} />
1251
1370
  </div>
1252
1371
  )
@@ -1,48 +1,55 @@
1
1
  import zmodem from 'zmodem.js/src/zmodem_browser'
2
2
 
3
- function zmodemAttach (ws, opts, ctx) {
4
- if (opts === undefined) { opts = {} }
5
- const term = this
6
- const senderFunc = function (octets) { return ws.send(new Uint8Array(octets)) }
7
- let zsentry = null
8
- function shouldWrite () {
9
- return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession
3
+ export class AddonZmodem {
4
+ _disposables = []
5
+
6
+ activate (terminal) {
7
+ terminal.zmodemAttach = this.zmodemAttach.bind(this)
10
8
  }
11
- zsentry = new zmodem.Sentry({
12
- to_terminal: function (octets) {
13
- if (shouldWrite()) {
14
- term.write(String.fromCharCode.apply(String, octets))
15
- }
16
- },
17
- sender: senderFunc,
18
- on_retract: ctx.onzmodemRetract,
19
- on_detect: ctx.onZmodemDetect
20
- })
21
- function handleWSMessage (evt) {
22
- if (typeof evt.data === 'string') {
23
- if (shouldWrite()) {
24
- term.write(evt.data)
25
- }
9
+
10
+ sendWebSocket (octets) {
11
+ const { socket } = this
12
+ if (socket && socket.readyState === WebSocket.OPEN) {
13
+ return socket.send(new Uint8Array(octets))
26
14
  } else {
27
- zsentry.consume(evt.data)
15
+ console.error('WebSocket is not open')
28
16
  }
29
17
  }
30
- ws.binaryType = 'arraybuffer'
31
- ws.addEventListener('message', handleWSMessage)
32
- }
33
18
 
34
- export class AddonZmodem {
35
- _disposables = []
19
+ zmodemAttach (ctx) {
20
+ this.socket = ctx.socket
21
+ this.term = ctx.term
22
+ this.ctx = ctx
23
+ this.zsentry = new zmodem.Sentry({
24
+ to_terminal: (octets) => {
25
+ if (ctx.onZmodem) {
26
+ this.term.write(String.fromCharCode.apply(String, octets))
27
+ }
28
+ },
29
+ sender: this.sendWebSocket.bind(this),
30
+ on_retract: ctx.onzmodemRetract,
31
+ on_detect: ctx.onZmodemDetect
32
+ })
33
+ this.socket.binaryType = 'arraybuffer'
34
+ this.socket.addEventListener('message', this.handleWSMessage.bind(this))
35
+ }
36
36
 
37
- activate (terminal) {
38
- terminal.zmodemAttach = zmodemAttach
39
- terminal.zmodemBrowser = zmodem.Browser
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
+ }
40
45
  }
41
46
 
42
47
  dispose () {
48
+ this.socket && this.socket.removeEventListener('message', this.handleWSMessage)
43
49
  this._disposables.forEach(d => d.dispose())
44
50
  this._disposables.length = 0
51
+ this.term = null
52
+ this.zsentry = null
53
+ this.socket = null
45
54
  }
46
55
  }
47
-
48
- export const Zmodem = zmodem
@@ -216,7 +216,6 @@ class Store {
216
216
  store
217
217
  } = window
218
218
  return store.showModal ||
219
- store.termSearchOpen ||
220
219
  store.showInfoModal ||
221
220
  store.showEditor ||
222
221
  store.showFileModal
@@ -226,10 +225,6 @@ class Store {
226
225
  return 0
227
226
  }
228
227
 
229
- get tabsHeight () {
230
- return 45 // window.store.config.useSystemTitleBar ? 45 : 56
231
- }
232
-
233
228
  get langs () {
234
229
  return JSON.parse(window.store._langs)
235
230
  }
@@ -187,6 +187,7 @@ export default () => {
187
187
  height: 500,
188
188
  isMaximized: window.pre.runSync('isMaximized'),
189
189
  terminalFullScreen: false,
190
- hideDelKeyTip: ls.getItem(dismissDelKeyTipLsKey) === 'y'
190
+ hideDelKeyTip: ls.getItem(dismissDelKeyTipLsKey) === 'y',
191
+ tabsHeight: 36
191
192
  }
192
193
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "1.37.128",
3
+ "version": "1.38.8",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",
@@ -1,98 +0,0 @@
1
- /**
2
- * zmodem transfer UI module
3
- * then run rz to send from your browser or
4
- * sz <file> to send from the remote peer.
5
- */
6
-
7
- import { memo } from 'react'
8
- import { Progress, Button, Upload, Tag } from 'antd'
9
- import { transferTypeMap } from '../../common/constants'
10
- import Link from '../common/external-link'
11
- import './zmodem.styl'
12
-
13
- const { prefix } = window
14
- const s = prefix('sftp')
15
- const c = prefix('common')
16
-
17
- export default memo((props) => {
18
- const { zmodemTransfer, cancelZmodem, beforeZmodemUpload } = props
19
- if (!zmodemTransfer) {
20
- return null
21
- }
22
- const {
23
- fileInfo,
24
- type,
25
- transferedSize,
26
- percent
27
- // options
28
- } = zmodemTransfer
29
- let btn = null
30
- let progress = null
31
- const recm = (
32
- <Tag color='success'>
33
- <div className='pd1y'>
34
- <b className='mg1r'>Recommend Use trzsz instead:</b>
35
- <Link to='https://github.com/trzsz/trzsz'>https://github.com/trzsz/trzsz</Link>
36
- </div>
37
- </Tag>
38
- )
39
- const cancelBtn = (
40
- <Button
41
- type='danger'
42
- className='iblock mg2l'
43
- onClick={cancelZmodem}
44
- >{c('cancel')}
45
- </Button>
46
- )
47
- if (type === transferTypeMap.upload) {
48
- btn = (
49
- <div className={fileInfo ? 'hide' : 'mg2b'}>
50
- <div className='iblock'>
51
- <Upload
52
- showUploadList={false}
53
- beforeUpload={beforeZmodemUpload}
54
- className={fileInfo ? 'hide' : 'iblock'}
55
- >
56
- <Button>
57
- {s(type)}
58
- </Button>
59
- </Upload>
60
- </div>
61
- {cancelBtn}
62
- </div>
63
- )
64
- }
65
- if (fileInfo) {
66
- const {
67
- size,
68
- name
69
- } = fileInfo
70
- progress = (
71
- <div className='pd1b'>
72
- <Progress
73
- percent={percent}
74
- size='small'
75
- status='active'
76
- format={() => {
77
- return `%${percent}(${transferedSize}/${size})`
78
- }}
79
- />
80
- <h2 className='pd2y'>
81
- <span className='iblock'>
82
- {s(type)}: {name}
83
- </span>
84
- </h2>
85
- <h4 className='pd2t pd2x'>Upload file(❯1M) may not show progress and may not end properly, but still would finish uploading in background.</h4>
86
- </div>
87
- )
88
- }
89
- return (
90
- <div className='zmodem-transfer'>
91
- <div className='zmodem-transfer-inner'>
92
- {btn}
93
- {progress}
94
- {recm}
95
- </div>
96
- </div>
97
- )
98
- })