@electerm/electerm-react 1.37.80 → 1.37.88

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.
@@ -14,6 +14,14 @@ export const readClipboard = () => {
14
14
  return window.pre.readClipboard()
15
15
  }
16
16
 
17
+ export const readClipboardAsync = () => {
18
+ const {
19
+ readClipboardSync,
20
+ readClipboard
21
+ } = window.pre
22
+ return readClipboardSync ? readClipboardSync() : Promise.resolve(readClipboard())
23
+ }
24
+
17
25
  export const copy = (str) => {
18
26
  message.success({
19
27
  content: 'Copied',
@@ -336,3 +336,4 @@ export const instSftpKeys = [
336
336
  'readFile',
337
337
  'writeFile'
338
338
  ]
339
+ export const cwdId = '=__+__'
@@ -40,5 +40,7 @@ export default {
40
40
  screenReaderMode: false,
41
41
  autoRefreshWhenSwitchToSftp: false,
42
42
  hideSshConfig: false,
43
- addTimeStampToTermLog: false
43
+ addTimeStampToTermLog: false,
44
+ sftpPathFollowSsh: false,
45
+ keepaliveInterval: 0
44
46
  }
@@ -1,6 +1,5 @@
1
1
  import { Component } from '../common/react-subx'
2
2
  import {
3
- Tooltip,
4
3
  Select
5
4
  } from 'antd'
6
5
  import { InfoCircleOutlined } from '@ant-design/icons'
@@ -109,14 +108,10 @@ export default class SystemMenu extends Component {
109
108
  }
110
109
  return (
111
110
  <div className='terminal-footer-unit terminal-footer-info'>
112
- <Tooltip
113
- title='Terminal Info'
114
- >
115
- <InfoCircleOutlined
116
- onClick={this.handleInfoPanel}
117
- className='pointer font18 terminal-info-icon'
118
- />
119
- </Tooltip>
111
+ <InfoCircleOutlined
112
+ onClick={this.handleInfoPanel}
113
+ className='pointer font18 terminal-info-icon'
114
+ />
120
115
  </div>
121
116
  )
122
117
  }
@@ -9,9 +9,12 @@ import {
9
9
  BorderHorizontalOutlined,
10
10
  CloseSquareFilled,
11
11
  SearchOutlined,
12
- FullscreenOutlined
12
+ FullscreenOutlined,
13
+ PaperClipOutlined
13
14
  } from '@ant-design/icons'
14
- import { Tooltip } from 'antd'
15
+ import {
16
+ Tooltip
17
+ } from 'antd'
15
18
  import { last, findIndex, pick } from 'lodash-es'
16
19
  import generate from '../../common/uid'
17
20
  import copy from 'json-deep-copy'
@@ -72,6 +75,8 @@ export default class SessionWrapper extends Component {
72
75
  this.state = {
73
76
  pid: null,
74
77
  enableSftp: false,
78
+ cwd: '',
79
+ sftpPathFollowSsh: !!props.config.sftpPathFollowSsh,
75
80
  splitDirection: terminalSplitDirectionMap.horizontal,
76
81
  activeSplitId,
77
82
  infoPanelPinned: false,
@@ -89,14 +94,22 @@ export default class SessionWrapper extends Component {
89
94
  // this.initEvent()
90
95
  }
91
96
 
92
- // isActive () {
93
- // const {
94
- // tab,
95
- // currentTabId
96
- // } = this.props
97
- // return currentTabId === tab.id &&
98
- // tab.pane === paneMap.terminal
99
- // }
97
+ setCwd = (cwd, tid) => {
98
+ this.setState(old => {
99
+ return {
100
+ cwd,
101
+ terminals: old.terminals.map(t => {
102
+ if (t.id === tid) {
103
+ return {
104
+ ...t,
105
+ cwd
106
+ }
107
+ }
108
+ return t
109
+ })
110
+ }
111
+ })
112
+ }
100
113
 
101
114
  handleShowInfo = (infoPanelProps) => {
102
115
  this.setState({
@@ -111,6 +124,12 @@ export default class SessionWrapper extends Component {
111
124
  })
112
125
  }
113
126
 
127
+ toggleCheckSftpPathFollowSsh = () => {
128
+ this.setState({
129
+ sftpPathFollowSsh: !this.state.sftpPathFollowSsh
130
+ })
131
+ }
132
+
114
133
  hideInfoPanel = () => {
115
134
  this.setState({
116
135
  showInfo: false
@@ -266,7 +285,8 @@ export default class SessionWrapper extends Component {
266
285
  activeSplitId,
267
286
  splitDirection,
268
287
  sessionOptions,
269
- sessionId
288
+ sessionId,
289
+ sftpPathFollowSsh
270
290
  } = this.state
271
291
  const {
272
292
  pane
@@ -297,6 +317,7 @@ export default class SessionWrapper extends Component {
297
317
  ...this.props,
298
318
  ...t,
299
319
  activeSplitId,
320
+ sftpPathFollowSsh,
300
321
  themeConfig,
301
322
  pane,
302
323
  ...pick(
@@ -308,7 +329,8 @@ export default class SessionWrapper extends Component {
308
329
  'setSessionState',
309
330
  'handleShowInfo',
310
331
  'onChangePane',
311
- 'hideInfoPanel'
332
+ 'hideInfoPanel',
333
+ 'setCwd'
312
334
  ]),
313
335
  ...this.computePosition(t.position / 10)
314
336
  }
@@ -330,22 +352,34 @@ export default class SessionWrapper extends Component {
330
352
  }
331
353
 
332
354
  renderSftp = () => {
333
- const { sessionOptions, sessionId, pid, enableSftp } = this.state
355
+ const {
356
+ sessionOptions,
357
+ sessionId,
358
+ pid,
359
+ enableSftp,
360
+ sftpPathFollowSsh,
361
+ cwd
362
+ } = this.state
334
363
  const { pane } = this.props.tab
335
364
  const height = this.computeHeight()
336
365
  const cls = pane === paneMap.terminal
337
366
  ? 'hide'
338
367
  : ''
368
+ const exts = {
369
+ sftpPathFollowSsh,
370
+ cwd,
371
+ pid,
372
+ enableSftp,
373
+ sessionOptions,
374
+ height,
375
+ sessionId,
376
+ pane,
377
+ ...this.props
378
+ }
339
379
  return (
340
380
  <div className={cls}>
341
381
  <Sftp
342
- pid={pid}
343
- enableSftp={enableSftp}
344
- sessionOptions={sessionOptions}
345
- height={height}
346
- sessionId={sessionId}
347
- pane={pane}
348
- {...this.props}
382
+ {...exts}
349
383
  />
350
384
  </div>
351
385
  )
@@ -365,7 +399,7 @@ export default class SessionWrapper extends Component {
365
399
  renderSearchIcon = () => {
366
400
  const title = e('search')
367
401
  return (
368
- <Tooltip title={title}>
402
+ <Tooltip title={title} placement='bottomLeft'>
369
403
  <SearchOutlined
370
404
  className='mg1r icon-info font16 iblock pointer spliter'
371
405
  onClick={this.handleOpenSearch}
@@ -377,7 +411,7 @@ export default class SessionWrapper extends Component {
377
411
  fullscreenIcon = () => {
378
412
  const title = e('fullscreen')
379
413
  return (
380
- <Tooltip title={title}>
414
+ <Tooltip title={title} placement='bottomLeft'>
381
415
  <FullscreenOutlined
382
416
  className='mg1r icon-info font16 iblock pointer spliter term-fullscreen-control1'
383
417
  onClick={this.handleFullscreen}
@@ -387,7 +421,7 @@ export default class SessionWrapper extends Component {
387
421
  }
388
422
 
389
423
  renderControl = () => {
390
- const { splitDirection, terminals } = this.state
424
+ const { splitDirection, terminals, sftpPathFollowSsh } = this.state
391
425
  const { props } = this
392
426
  const { pane } = props.tab
393
427
  const termType = props.tab?.type
@@ -413,6 +447,16 @@ export default class SessionWrapper extends Component {
413
447
  if (isSsh || isLocal) {
414
448
  controls.push(isSsh ? paneMap.sftp : paneMap.fileManager)
415
449
  }
450
+ const checkTxt = e('sftpPathFollowSsh') + ' [Beta]'
451
+ const checkProps = {
452
+ onClick: this.toggleCheckSftpPathFollowSsh,
453
+ className: classnames(
454
+ 'sftp-follow-ssh-icon',
455
+ {
456
+ active: sftpPathFollowSsh
457
+ }
458
+ )
459
+ }
416
460
  return (
417
461
  <div
418
462
  className='terminal-control fix'
@@ -442,6 +486,17 @@ export default class SessionWrapper extends Component {
442
486
  })
443
487
  }
444
488
  </div>
489
+ {
490
+ isSsh || isLocal
491
+ ? (
492
+ <Tooltip title={checkTxt}>
493
+ <span {...checkProps}>
494
+ <PaperClipOutlined />
495
+ </span>
496
+ </Tooltip>
497
+ )
498
+ : null
499
+ }
445
500
  {
446
501
  pane === paneMap.terminal
447
502
  ? (
@@ -461,6 +516,7 @@ export default class SessionWrapper extends Component {
461
516
  }
462
517
  <Tooltip
463
518
  title={`${e('split')}`}
519
+ placement='bottomLeft'
464
520
  >
465
521
  <Icon1
466
522
  className={cls1}
@@ -469,6 +525,7 @@ export default class SessionWrapper extends Component {
469
525
  </Tooltip>
470
526
  <Tooltip
471
527
  title={e('changeDirection')}
528
+ placement='bottomLeft'
472
529
  >
473
530
  <Icon2
474
531
  className={cls2}
@@ -16,6 +16,7 @@
16
16
  position relative
17
17
  display inline-block
18
18
 
19
+ .sftp-follow-ssh-icon
19
20
  .type-tab
20
21
  display inline-block
21
22
  vertical-align middle
@@ -31,6 +32,7 @@
31
32
  &.active
32
33
  .type-tab-line
33
34
  display inline-block
35
+
34
36
  .is-transporting
35
37
  .type-tab.sftp
36
38
  .type-tab-line
@@ -111,6 +111,12 @@ export default class Sftp extends Component {
111
111
  selectedFiles: []
112
112
  })
113
113
  }
114
+ if (
115
+ this.props.sftpPathFollowSsh &&
116
+ prevProps.cwd !== this.props.cwd
117
+ ) {
118
+ this.updateCwd(this.props.cwd)
119
+ }
114
120
  }
115
121
 
116
122
  componentWillUnmount () {
@@ -196,7 +202,42 @@ export default class Sftp extends Component {
196
202
  this.props.pane === paneMap.fileManager
197
203
  }
198
204
 
205
+ getCwdLocal = () => {
206
+ if (
207
+ !this.shouldRenderRemote() &&
208
+ this.props.sftpPathFollowSsh &&
209
+ this.props.cwd
210
+ ) {
211
+ return this.props.cwd
212
+ }
213
+ }
214
+
215
+ updateCwd = (cwd) => {
216
+ if (!this.state.inited) {
217
+ return
218
+ }
219
+ const type = this.shouldRenderRemote()
220
+ ? typeMap.remote
221
+ : typeMap.local
222
+ // this.setState({
223
+ // [`${type}PathTemp`]: cwd
224
+ // }, () => {
225
+ // this.onGoto(
226
+ // type
227
+ // )
228
+ // })
229
+ const n = `${type}Path`
230
+ const nt = n + 'Temp'
231
+ this.setState({
232
+ [n]: cwd,
233
+ [nt]: cwd
234
+ }, () => this[`${type}List`]())
235
+ }
236
+
199
237
  getPwd = async (username) => {
238
+ if (this.props.sftpPathFollowSsh && this.props.cwd) {
239
+ return this.props.cwd
240
+ }
200
241
  const home = await runCmd(
201
242
  this.props.pid,
202
243
  this.props.sessionId,
@@ -427,16 +468,19 @@ export default class Sftp extends Component {
427
468
  }
428
469
 
429
470
  initData = async () => {
430
- const { props } = this
431
- const host = props.tab?.host &&
432
- props.tab?.type !== terminalSshConfigType &&
433
- props.tab?.type !== terminalSerialType
434
- if (host) {
471
+ if (this.shouldRenderRemote()) {
435
472
  this.initRemoteAll()
436
473
  }
437
474
  this.initLocalAll()
438
475
  }
439
476
 
477
+ shouldRenderRemote = () => {
478
+ const { props } = this
479
+ return props.tab?.host &&
480
+ props.tab?.type !== terminalSshConfigType &&
481
+ props.tab?.type !== terminalSerialType
482
+ }
483
+
440
484
  initLocalAll = () => {
441
485
  this.localListOwner()
442
486
  this.localList()
@@ -699,7 +743,10 @@ export default class Sftp extends Component {
699
743
  )
700
744
  try {
701
745
  const noPathInit = localPathReal || this.state.localPath
702
- const localPath = noPathInit || this.props.tab.startDirectoryLocal || window.pre.homeOrtmp
746
+ const localPath = noPathInit ||
747
+ this.getCwdLocal() ||
748
+ this.props.tab.startDirectoryLocal ||
749
+ window.pre.homeOrTmp
703
750
  const locals = await fs.readdirAsync(localPath)
704
751
  const local = []
705
752
  for (const name of locals) {
@@ -1010,8 +1057,7 @@ export default class Sftp extends Component {
1010
1057
  const {
1011
1058
  height, width
1012
1059
  } = this.props
1013
- const shouldRenderRemote = this.props.tab?.authType &&
1014
- this.props.tab?.enableSftp !== false
1060
+ const shouldRenderRemote = this.shouldRenderRemote()
1015
1061
  if (!shouldRenderRemote) {
1016
1062
  return (
1017
1063
  this.renderSection(arr[0], {
@@ -53,21 +53,17 @@ export function shortcutExtend (Cls) {
53
53
  shiftKey,
54
54
  metaKey,
55
55
  altKey,
56
- wheelDeltaY
56
+ wheelDeltaY,
57
+ type,
58
+ key
57
59
  } = event
58
- if (this.isTerm) {
59
- const keymap = [
60
- { key: 'Backspace', shiftKey: false, mapCode: 8 },
61
- { key: 'Backspace', shiftKey: true, mapCode: 127 }
62
- ]
63
- if (event.type === 'keydown') {
64
- for (const i in keymap) {
65
- if (keymap[i].key === event.key && keymap[i].shiftKey === shiftKey) {
66
- this.socket.send(String.fromCharCode(keymap[i].mapCode))
67
- return false
68
- }
69
- }
70
- }
60
+ if (key === 'Backspace' && this.isTerm && type === 'keydown') {
61
+ this.socket.send(
62
+ String.fromCharCode(
63
+ shiftKey ? 127 : 8
64
+ )
65
+ )
66
+ return false
71
67
  }
72
68
  const codeName = event instanceof window.WheelEvent
73
69
  ? (wheelDeltaY > 0 ? 'mouseWheelUp' : 'mouseWheelDown')
@@ -2,6 +2,7 @@
2
2
  * customize AttachAddon
3
3
  */
4
4
  import { AttachAddon } from 'xterm-addon-attach'
5
+ import strip from '@electerm/strip-ansi'
5
6
 
6
7
  export default class AttachAddonCustom extends AttachAddon {
7
8
  constructor (term, options, encode, isWindowsShell) {
@@ -22,7 +23,31 @@ export default class AttachAddonCustom extends AttachAddon {
22
23
  const fileReader = new FileReader()
23
24
  fileReader.addEventListener('load', () => {
24
25
  const str = this.decoder.decode(fileReader.result)
25
- terminal.write(str)
26
+ if (terminal.parent.props.sftpPathFollowSsh && terminal.buffer.active.type !== 'alternate') {
27
+ const {
28
+ cwdId
29
+ } = terminal
30
+ const nss = str.split('\r')
31
+ const nnss = []
32
+ for (const str1 of nss) {
33
+ const ns = strip(str1).trim()
34
+ if (ns.includes(cwdId) && ns.includes('$PWD')) {
35
+ nnss.push(str1.replace(`echo "${cwdId}$PWD"`, ''))
36
+ } else if (
37
+ (cwdId && ns.startsWith(cwdId))
38
+ ) {
39
+ delete terminal.cwdId
40
+ const cwd = ns.replace(cwdId, '').trim()
41
+ terminal.parent.setCwd(cwd)
42
+ nnss.push('\x1b[2A\x1b[0J')
43
+ } else {
44
+ nnss.push(str1)
45
+ }
46
+ }
47
+ terminal.write(nnss.join('\r'))
48
+ } else {
49
+ terminal.write(str)
50
+ }
26
51
  })
27
52
  fileReader.readAsArrayBuffer(new window.Blob([data]))
28
53
  }
@@ -25,10 +25,11 @@ import {
25
25
  transferTypeMap,
26
26
  terminalActions,
27
27
  commonActions,
28
- rendererTypes
28
+ rendererTypes,
29
+ cwdId
29
30
  } from '../../common/constants'
30
31
  import deepCopy from 'json-deep-copy'
31
- import { readClipboard, copy } from '../../common/clipboard'
32
+ import { readClipboardAsync, copy } from '../../common/clipboard'
32
33
  import { FitAddon } from 'xterm-addon-fit'
33
34
  import AttachAddon from './attach-addon-custom'
34
35
  import { SearchAddon } from 'xterm-addon-search'
@@ -76,8 +77,6 @@ class Term extends Component {
76
77
 
77
78
  isTerm = true
78
79
 
79
- dataCache = ''
80
-
81
80
  componentDidMount () {
82
81
  this.initTerminal()
83
82
  this.initEvt()
@@ -130,6 +129,7 @@ class Term extends Component {
130
129
  }
131
130
 
132
131
  componentWillUnmount () {
132
+ delete this.term.parent
133
133
  Object.keys(this.timers).forEach(k => {
134
134
  clearTimeout(this.timers[k])
135
135
  })
@@ -646,12 +646,12 @@ class Term extends Component {
646
646
  this.props.tab?.type !== terminalSshConfigType
647
647
  }
648
648
 
649
- onPaste = () => {
650
- let selected = readClipboard()
649
+ onPaste = async () => {
650
+ let selected = await readClipboardAsync()
651
651
  if (isWin && this.isRemote()) {
652
652
  selected = selected.replace(/\r\n/g, '\n')
653
653
  }
654
- this.term.paste(selected)
654
+ this.term.paste(selected || '')
655
655
  this.term.focus()
656
656
  }
657
657
 
@@ -680,7 +680,7 @@ class Term extends Component {
680
680
 
681
681
  renderContext = () => {
682
682
  const hasSlected = this.term.hasSelection()
683
- const copyed = readClipboard()
683
+ const copyed = true
684
684
  const copyShortcut = this.getShortcut('terminal_copy')
685
685
  const pasteShortcut = this.getShortcut('terminal_paste')
686
686
  const clearShortcut = this.getShortcut('terminal_clear')
@@ -762,8 +762,22 @@ class Term extends Component {
762
762
  const str = this.serializeAddon.serialize()
763
763
  const arr = strip(str).split(/ +/)
764
764
  const len = arr.length
765
- const last = arr[len - 1]
766
- this.dataCache = last
765
+ return arr[len - 1]
766
+ }
767
+
768
+ getCwd = () => {
769
+ if (
770
+ this.props.sftpPathFollowSsh &&
771
+ this.term.buffer.active.type !== 'alternate'
772
+ ) {
773
+ const cmd = `\recho "${cwdId}$PWD"\r`
774
+ this.term.cwdId = cwdId
775
+ this.socket.send(cmd)
776
+ }
777
+ }
778
+
779
+ setCwd = (cwd) => {
780
+ this.props.setCwd(cwd, this.state.id)
767
781
  }
768
782
 
769
783
  onData = (d) => {
@@ -771,9 +785,10 @@ class Term extends Component {
771
785
  if (!d.includes('\r')) {
772
786
  delete this.userTypeExit
773
787
  } else {
774
- this.getCmd()
775
- const data = this.dataCache
776
- this.dataCache = ''
788
+ const data = this.getCmd()
789
+ if (this.term.buffer.active.type !== 'alternate') {
790
+ setTimeout(this.getCwd, 200)
791
+ }
777
792
  const exitCmds = [
778
793
  'exit',
779
794
  'logout'
@@ -822,6 +837,7 @@ class Term extends Component {
822
837
 
823
838
  // term.onLineFeed(this.onLineFeed)
824
839
  // term.onTitleChange(this.onTitleChange)
840
+ term.parent = this
825
841
  term.onSelectionChange(this.onSelection)
826
842
  term.open(document.getElementById(id), true)
827
843
  this.loadRenderer(term, config)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "1.37.80",
3
+ "version": "1.37.88",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",
@@ -1,25 +0,0 @@
1
- /**
2
- * show terminal info
3
- * inluding id log path, and system info for remote session
4
- */
5
-
6
- import { InfoCircleOutlined } from '@ant-design/icons'
7
-
8
- import { Tooltip } from 'antd'
9
- import './terminal-info.styl'
10
-
11
- export default function TerminalInfoIndex (props) {
12
- const pops = {
13
- onClick: props.showInfoPanel,
14
- className: 'pointer font18 terminal-info-icon'
15
- }
16
- return (
17
- <Tooltip
18
- title='Terminal Info'
19
- >
20
- <InfoCircleOutlined
21
- {...pops}
22
- />
23
- </Tooltip>
24
- )
25
- }