@electerm/electerm-react 1.72.18 → 1.72.36

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.
@@ -97,7 +97,7 @@ export default function AIChat (props) {
97
97
  return (
98
98
  <SendOutlined
99
99
  onClick={handleSubmit}
100
- className='mg1l pointer icon-hover'
100
+ className='mg1l pointer icon-hover send-to-ai-icon'
101
101
  title='Ctrl+Enter'
102
102
  />
103
103
  )
@@ -34,7 +34,7 @@ export default function renderTabs (props) {
34
34
  {props.renderEnableSftp()}
35
35
  {props.uis}
36
36
  {props.renderProxy(props)}
37
- {props.renderX11()}
37
+ {props.renderX11(props.form)}
38
38
  </>
39
39
  )
40
40
  },
@@ -147,7 +147,7 @@ export default function LocalFormUi (props) {
147
147
  initialValues={initialValues}
148
148
  name='local-form'
149
149
  >
150
- {renderTabs()}
150
+ {renderTabs({ form })}
151
151
  {submitUi}
152
152
  </Form>
153
153
  )
@@ -69,7 +69,9 @@ export default function BookmarkFormUI (props) {
69
69
  sshTunnels: [],
70
70
  runScripts: [{}],
71
71
  category: initBookmarkGroupId,
72
- connectionHoppings: []
72
+ connectionHoppings: [],
73
+ serverHostKey: [],
74
+ cipher: []
73
75
  }
74
76
  initialValues = defaultsDeep(initialValues, defaultValues)
75
77
  function onChangeAuthType (e) {
@@ -4,6 +4,7 @@
4
4
  import {
5
5
  Switch,
6
6
  Form,
7
+ Button,
7
8
  Select
8
9
  } from 'antd'
9
10
  import { formItemLayout } from '../../common/form-layout'
@@ -46,7 +47,48 @@ const serverHostKeyOptions = [
46
47
  'rsa-sha2-256'
47
48
  ]
48
49
 
49
- export default function renderX11 () {
50
+ const defaultCipherOptions = [
51
+ 'aes128-ctr',
52
+ 'aes192-ctr',
53
+ 'aes256-ctr',
54
+ 'aes128-gcm',
55
+ 'aes128-gcm@openssh.com',
56
+ 'aes256-gcm',
57
+ 'aes256-gcm@openssh.com',
58
+ 'aes256-cbc',
59
+ 'aes192-cbc',
60
+ 'aes128-cbc',
61
+ 'blowfish-cbc',
62
+ '3des-cbc',
63
+ 'arcfour256',
64
+ 'arcfour128',
65
+ 'cast128-cbc',
66
+ 'arcfour'
67
+ ]
68
+
69
+ const defaultServerHostKeyOptions = [
70
+ 'ssh-rsa',
71
+ 'ssh-ed25519',
72
+ 'ecdsa-sha2-nistp256',
73
+ 'ecdsa-sha2-nistp384',
74
+ 'ecdsa-sha2-nistp521',
75
+ 'ssh-dss',
76
+ 'rsa-sha2-512',
77
+ 'rsa-sha2-256'
78
+ ]
79
+
80
+ export default function renderX11 (form) {
81
+ function setDefaults () {
82
+ // form.setFieldsValue({
83
+ // cipher: defaultCipherOptions
84
+ // })
85
+
86
+ // form.setFieldsValue({
87
+ // serverHostKey: defaultServerHostKeyOptions
88
+ // })
89
+ form.setFieldValue('cipher', defaultCipherOptions)
90
+ form.setFieldValue('serverHostKey', defaultServerHostKeyOptions)
91
+ }
50
92
  return (
51
93
  <>
52
94
  <FormItem
@@ -67,17 +109,31 @@ export default function renderX11 () {
67
109
  <FormItem
68
110
  {...formItemLayout}
69
111
  label='serverHostKey'
70
- name='serverHostKey'
71
112
  >
72
- <Select
73
- mode='multiple'
113
+ <FormItem
114
+ {...formItemLayout}
115
+ noStyle
116
+ name='serverHostKey'
74
117
  >
75
- {serverHostKeyOptions.map(key => (
76
- <Option key={key} value={key}>
77
- {key}
78
- </Option>
79
- ))}
80
- </Select>
118
+ <Select
119
+ mode='multiple'
120
+ >
121
+ {serverHostKeyOptions.map(key => (
122
+ <Option key={key} value={key}>
123
+ {key}
124
+ </Option>
125
+ ))}
126
+ </Select>
127
+ </FormItem>
128
+ <div className='mg1t'>
129
+ <Button
130
+ type='dashed'
131
+ onClick={setDefaults}
132
+ size='small'
133
+ >
134
+ Set default cipher and serverHostKey
135
+ </Button>
136
+ </div>
81
137
  </FormItem>
82
138
  <FormItem
83
139
  {...formItemLayout}
@@ -4,14 +4,22 @@ import {
4
4
  } from 'antd'
5
5
 
6
6
  export default function InputAutoFocus (props) {
7
- const { type, ...rest } = props
7
+ const { type, selectall = false, ...rest } = props
8
8
  const inputRef = useRef(null)
9
+ const isFirstRender = useRef(true)
9
10
 
10
11
  useEffect(() => {
11
12
  if (inputRef.current) {
12
- inputRef.current.focus()
13
+ const { value } = props
14
+ if (value && selectall && isFirstRender.current) {
15
+ inputRef.current.focus()
16
+ inputRef.current.setSelectionRange(0, value.length)
17
+ isFirstRender.current = false
18
+ } else {
19
+ inputRef.current.focus()
20
+ }
13
21
  }
14
- }, [props.value])
22
+ }, [props.value, props.selectall])
15
23
 
16
24
  let InputComponent
17
25
  switch (type) {
@@ -30,8 +30,7 @@ import {
30
30
  terminalRdpType,
31
31
  terminalVncType,
32
32
  terminalWebType,
33
- terminalTelnetType,
34
- splitMap
33
+ terminalTelnetType
35
34
  } from '../../common/constants'
36
35
  import { SplitViewIcon } from '../icons/split-view'
37
36
  import { refs } from '../common/ref'
@@ -521,7 +520,6 @@ export default class SessionWrapper extends Component {
521
520
 
522
521
  renderBroadcastIcon = () => {
523
522
  if (
524
- this.props.layout === splitMap.c1 ||
525
523
  this.isSshDisabled()
526
524
  ) {
527
525
  return null
@@ -0,0 +1,48 @@
1
+ import {
2
+ InputNumber
3
+ } from 'antd'
4
+ import { isNumber, isNaN } from 'lodash-es'
5
+
6
+ export default function NumberConfig ({
7
+ min,
8
+ max,
9
+ cls,
10
+ title = '',
11
+ value,
12
+ defaultValue,
13
+ onChange,
14
+ step,
15
+ extraDesc,
16
+ width = 136
17
+ }) {
18
+ const opts = {
19
+ step,
20
+ value,
21
+ min,
22
+ max,
23
+ onChange,
24
+ placeholder: defaultValue
25
+ }
26
+ if (title) {
27
+ opts.formatter = v => `${title}${extraDesc || ''}: ${v}`
28
+ opts.parser = (v) => {
29
+ let vv = isNumber(v)
30
+ ? v
31
+ : Number(v.split(': ')[1], 10)
32
+ if (isNaN(vv)) {
33
+ vv = defaultValue
34
+ }
35
+ return vv
36
+ }
37
+ opts.style = {
38
+ width: width + 'px'
39
+ }
40
+ }
41
+ return (
42
+ <div className={`pd2b ${cls || ''}`}>
43
+ <InputNumber
44
+ {...opts}
45
+ />
46
+ </div>
47
+ )
48
+ }
@@ -9,7 +9,6 @@ import {
9
9
  Switch,
10
10
  Input,
11
11
  Upload,
12
- InputNumber,
13
12
  Button,
14
13
  AutoComplete,
15
14
  Tooltip,
@@ -26,7 +25,6 @@ import defaultSettings from '../../common/default-setting'
26
25
  import ShowItem from '../common/show-item'
27
26
  import { osResolve } from '../../common/resolve'
28
27
  import { chooseSaveDirectory } from '../../common/choose-save-folder'
29
- import { isNumber, isNaN } from 'lodash-es'
30
28
  import mapper from '../../common/auto-complete-data-mapper'
31
29
  import KeywordForm from './keywords-form'
32
30
  import Link from '../common/external-link'
@@ -35,6 +33,8 @@ import KeywordsTransport from './keywords-transport'
35
33
  import fs from '../../common/fs'
36
34
  import uid from '../../common/uid'
37
35
  import createDefaultSessionLogPath from '../../common/default-log-path'
36
+ import TerminalBackgroundConfig from './terminal-bg-config'
37
+ import NumberConfig from './number-config'
38
38
  import './setting.styl'
39
39
 
40
40
  const { Option } = Select
@@ -211,40 +211,23 @@ export default class SettingTerminal extends Component {
211
211
  step = 1,
212
212
  min,
213
213
  max,
214
- cls,
215
- onChange = (v) => {
216
- this.onChangeValue(v, name)
217
- }
214
+ cls
218
215
  } = options
219
216
  const opts = {
220
- step,
221
217
  value,
222
218
  min,
223
219
  max,
224
- onChange,
225
- placeholder: defaultValue
226
- }
227
- if (title) {
228
- opts.formatter = v => `${title}${options.extraDesc || ''}: ${v}`
229
- opts.parser = (v) => {
230
- let vv = isNumber(v)
231
- ? v
232
- : Number(v.split(': ')[1], 10)
233
- if (isNaN(vv)) {
234
- vv = defaultValue
235
- }
236
- return vv
237
- }
238
- opts.style = {
239
- width: width + 'px'
240
- }
220
+ onChange: (v) => {
221
+ this.onChangeValue(v, name)
222
+ },
223
+ defaultValue,
224
+ title,
225
+ width,
226
+ step,
227
+ cls
241
228
  }
242
229
  return (
243
- <div className={`pd2b ${cls || ''}`}>
244
- <InputNumber
245
- {...opts}
246
- />
247
- </div>
230
+ <NumberConfig {...opts} />
248
231
  )
249
232
  }
250
233
 
@@ -499,6 +482,11 @@ export default class SettingTerminal extends Component {
499
482
  submit: this.handleSubmitKeywords,
500
483
  themeConfig: getThemeConfig()
501
484
  }
485
+ const bgProps = {
486
+ onChangeValue: this.onChangeValue,
487
+ name: 'terminalBackgroundImagePath',
488
+ config: this.props.config
489
+ }
502
490
  const tip = (
503
491
  <div>
504
492
  <span className='mg1r'>{e('supportRegexp')}</span>
@@ -569,9 +557,7 @@ export default class SettingTerminal extends Component {
569
557
  }
570
558
  </div>
571
559
  <div className='pd1b'>{e('terminalBackgroundImage')}</div>
572
- {
573
- this.renderTerminalBgSelect('terminalBackgroundImagePath')
574
- }
560
+ <TerminalBackgroundConfig {...bgProps} />
575
561
  <div className='pd1b'>{e('terminalWordSeparator')}</div>
576
562
  {
577
563
  this.renderText('terminalWordSeparator', e('terminalWordSeparator'))
@@ -0,0 +1,152 @@
1
+ import React from 'react'
2
+ import {
3
+ AutoComplete,
4
+ Upload,
5
+ Tooltip,
6
+ Input
7
+ } from 'antd'
8
+ import {
9
+ noTerminalBgValue
10
+ } from '../../common/constants'
11
+ import defaultSettings from '../../common/default-setting'
12
+ import NumberConfig from './number-config'
13
+
14
+ const e = window.translate
15
+
16
+ export default function TerminalBackgroundConfig ({
17
+ onChangeValue,
18
+ name,
19
+ config
20
+ }) {
21
+ const value = config[name]
22
+ const defaultValue = defaultSettings[name]
23
+ const onChange = (v) => onChangeValue(v, name)
24
+ const after = (
25
+ <Upload
26
+ beforeUpload={(file) => {
27
+ onChangeValue(file.path, name)
28
+ return false
29
+ }}
30
+ showUploadList={false}
31
+ >
32
+ <span>{e('chooseFile')}</span>
33
+ </Upload>
34
+ )
35
+ const dataSource = [
36
+ {
37
+ value: '',
38
+ desc: e('default')
39
+ },
40
+ {
41
+ value: noTerminalBgValue,
42
+ desc: e('noTerminalBg')
43
+ }
44
+ ]
45
+ const numberOpts = { step: 0.05, min: 0, max: 1, cls: 'bg-img-setting' }
46
+
47
+ function renderNumber (name, options, title = '', width = 136) {
48
+ let value = config[name]
49
+ if (options.valueParser) {
50
+ value = options.valueParser(value)
51
+ }
52
+ const defaultValue = defaultSettings[name]
53
+ const {
54
+ step = 1,
55
+ min,
56
+ max,
57
+ cls
58
+ } = options
59
+ const opts = {
60
+ value,
61
+ min,
62
+ max,
63
+ onChange: (v) => {
64
+ onChangeValue(v, name)
65
+ },
66
+ defaultValue,
67
+ title,
68
+ width,
69
+ step,
70
+ cls
71
+ }
72
+ return (
73
+ <NumberConfig {...opts} />
74
+ )
75
+ }
76
+
77
+ const renderFilter = () => {
78
+ if (config[name] === noTerminalBgValue) return
79
+
80
+ return (
81
+ <div>
82
+ {
83
+ renderNumber(
84
+ 'terminalBackgroundFilterOpacity',
85
+ numberOpts,
86
+ e('Opacity')
87
+ )
88
+ }
89
+ {
90
+ renderNumber(
91
+ 'terminalBackgroundFilterBlur',
92
+ { ...numberOpts, min: 0, max: 50, step: 0.5 },
93
+ e('Blur')
94
+ )
95
+ }
96
+ {
97
+ renderNumber(
98
+ 'terminalBackgroundFilterBrightness',
99
+ { ...numberOpts, min: 0, max: 10, step: 0.1 },
100
+ e('Brightness')
101
+ )
102
+ }
103
+ {
104
+ renderNumber(
105
+ 'terminalBackgroundFilterGrayscale',
106
+ numberOpts,
107
+ e('Grayscale')
108
+ )
109
+ }
110
+ {
111
+ renderNumber(
112
+ 'terminalBackgroundFilterContrast',
113
+ { ...numberOpts, min: 0, max: 10, step: 0.1 },
114
+ e('Contrast')
115
+ )
116
+ }
117
+ </div>
118
+ )
119
+ }
120
+
121
+ const renderBgOption = item => {
122
+ return {
123
+ value: item.value,
124
+ label: item.desc
125
+ }
126
+ }
127
+ return (
128
+ <div className='pd2b'>
129
+ <div className='pd1b'>
130
+ <Tooltip
131
+ title='eg: https://xx.com/xx.png or /path/to/xx.png'
132
+ >
133
+ <AutoComplete
134
+ value={value}
135
+ onChange={onChange}
136
+ placeholder={defaultValue}
137
+ className='width-100'
138
+ options={dataSource.map(renderBgOption)}
139
+ >
140
+ <Input
141
+ addonAfter={after}
142
+ />
143
+ </AutoComplete>
144
+ </Tooltip>
145
+ </div>
146
+
147
+ {
148
+ renderFilter()
149
+ }
150
+ </div>
151
+ )
152
+ }
@@ -614,6 +614,7 @@ export default class FileSection extends React.Component {
614
614
  if (this.watchingFile) {
615
615
  window.pre.ipcOffEvent('file-change', this.onFileChange)
616
616
  window.pre.runGlobalAsync('unwatchFile', this.watchingFile)
617
+ fs.unlink(this.watchingFile).catch(console.log)
617
618
  delete this.watchingFile
618
619
  }
619
620
  }
@@ -1,7 +1,10 @@
1
1
  @require '../../css/includes/theme-default'
2
2
 
3
+ .sftp-wrap
4
+ display flex
5
+ flex-direction row
3
6
  .sftp-section
4
- position absolute
7
+ flex 1
5
8
  .sftp-title-wrap
6
9
  position relative
7
10
  .sftp-item
@@ -60,15 +60,14 @@ export default class TermSearch extends PureComponent {
60
60
  setTimeout(window.store.focus, 200)
61
61
  }
62
62
 
63
- prev = () => {
63
+ prev = (v = this.props.termSearch) => {
64
64
  const {
65
65
  activeTabId,
66
- termSearch,
67
66
  termSearchOptions
68
67
  } = this.props
69
68
  refs.get('term-' + activeTabId)
70
69
  ?.searchPrev(
71
- termSearch,
70
+ v,
72
71
  copy(termSearchOptions)
73
72
  )
74
73
  }
@@ -82,13 +81,18 @@ export default class TermSearch extends PureComponent {
82
81
  }
83
82
 
84
83
  handleChange = e => {
85
- window.store.termSearch = e.target.value
86
- this.prev()
84
+ const v = e.target.value
85
+ window.store.termSearch = v
86
+ this.prev(v)
87
87
  }
88
88
 
89
89
  clearSearch = () => {
90
- refs.get('term-' + this.props.activeTabId)
91
- ?.searchAddon.clearDecorations()
90
+ const term = refs.get('term-' + this.props.activeTabId)
91
+ term?.searchAddon.clearDecorations()
92
+ term.setState({
93
+ searchResults: [],
94
+ matchIndex: -1
95
+ })
92
96
  }
93
97
 
94
98
  close = () => {
@@ -201,7 +205,8 @@ export default class TermSearch extends PureComponent {
201
205
  onChange: this.handleChange,
202
206
  suffix: this.renderSuffix(),
203
207
  onPressEnter: this.next,
204
- addonAfter: this.renderAfter()
208
+ addonAfter: this.renderAfter(),
209
+ selectall: true
205
210
  }
206
211
  return (
207
212
  <div className='term-search-wrap'>
@@ -0,0 +1,48 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+
3
+ export default function SearchResultBar ({
4
+ matches,
5
+ totalLines,
6
+ matchIndex,
7
+ height
8
+ }) {
9
+ const canvasRef = useRef(null)
10
+ const drawSearchResults = () => {
11
+ const canvas = canvasRef.current
12
+ if (!canvas) return
13
+
14
+ const container = canvas.parentElement
15
+ const containerHeight = container.clientHeight
16
+ const dpr = window.devicePixelRatio || 1
17
+
18
+ // Set both canvas dimensions and style
19
+ canvas.height = containerHeight * dpr
20
+ canvas.width = 16 * dpr
21
+
22
+ const ctx = canvas.getContext('2d')
23
+ // Scale the context to account for the pixel ratio
24
+ ctx.scale(dpr, dpr)
25
+
26
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
27
+ matches.forEach((match, index) => {
28
+ const y = (match / totalLines) * containerHeight
29
+ ctx.fillStyle = index === matchIndex ? 'rgba(243, 67, 9, 0.5)' : 'rgba(243, 196, 9, 0.5)'
30
+ ctx.fillRect(0, y, 16, 2)
31
+ })
32
+ }
33
+
34
+ useEffect(() => {
35
+ drawSearchResults()
36
+ }, [matches, totalLines, matchIndex])
37
+
38
+ if (!matches.length) {
39
+ return null
40
+ }
41
+
42
+ return (
43
+ <canvas
44
+ ref={canvasRef}
45
+ className='term-search-bar'
46
+ />
47
+ )
48
+ }
@@ -52,6 +52,7 @@ import * as fs from './fs.js'
52
52
  import iconsMap from '../sys-menu/icons-map.jsx'
53
53
  import { refs, refsStatic } from '../common/ref.js'
54
54
  import createDefaultLogPath from '../../common/default-log-path.js'
55
+ import SearchResultBar from './terminal-search-bar'
55
56
 
56
57
  const e = window.translate
57
58
 
@@ -64,7 +65,10 @@ class Term extends Component {
64
65
  saveTerminalLogToFile: !!this.props.config.saveTerminalLogToFile,
65
66
  addTimeStampToTermLog: !!this.props.config.addTimeStampToTermLog,
66
67
  passType: 'password',
67
- lines: []
68
+ lines: [],
69
+ searchResults: [],
70
+ matchIndex: -1,
71
+ totalLines: 0
68
72
  }
69
73
  this.id = `term-${this.props.tab.id}`
70
74
  refs.add(this.id, this)
@@ -703,6 +707,20 @@ clear\r`
703
707
  termSearchMatchCount: resultCount,
704
708
  termSearchMatchIndex: resultIndex
705
709
  })
710
+
711
+ this.updateSearchResults(resultIndex)
712
+ }
713
+
714
+ updateSearchResults = (resultIndex) => {
715
+ const matches = this.searchAddon._highlightDecorations.map((highlight, i) => {
716
+ return highlight.match.row
717
+ })
718
+
719
+ this.setState({
720
+ searchResults: matches,
721
+ matchIndex: resultIndex,
722
+ totalLines: this.term.buffer.active.length
723
+ })
706
724
  }
707
725
 
708
726
  searchPrev = (searchInput, options) => {
@@ -1161,10 +1179,7 @@ clear\r`
1161
1179
  }
1162
1180
 
1163
1181
  canReceiveBroadcast = (termRef) => {
1164
- const tabId = termRef.props?.tab?.id
1165
- const isActiveInBatch = termRef.props.currentBatchTabId === tabId
1166
1182
  return (
1167
- isActiveInBatch &&
1168
1183
  termRef.socket &&
1169
1184
  termRef.props?.tab.pane === paneMap.terminal
1170
1185
  )
@@ -1349,6 +1364,12 @@ clear\r`
1349
1364
  },
1350
1365
  trigger: this.props.config.pasteWhenContextMenu ? [] : ['contextMenu']
1351
1366
  }
1367
+ const barProps = {
1368
+ matchIndex: this.state.matchIndex,
1369
+ matches: this.state.searchResults,
1370
+ totalLines: this.state.totalLines,
1371
+ height
1372
+ }
1352
1373
  return (
1353
1374
  <Dropdown {...dropdownProps}>
1354
1375
  <div
@@ -1361,6 +1382,7 @@ clear\r`
1361
1382
  lines={this.state.lines}
1362
1383
  close={this.closeNormalBuffer}
1363
1384
  />
1385
+ <SearchResultBar {...barProps} />
1364
1386
  <Spin className='loading-wrapper' spinning={loading} />
1365
1387
  </div>
1366
1388
  </Dropdown>
@@ -135,3 +135,11 @@
135
135
  .suggestion-delete
136
136
  margin-left 5px
137
137
  visibility hidden
138
+
139
+ .term-search-bar
140
+ position absolute
141
+ right 0
142
+ top 0
143
+ bottom 0
144
+ background transparent
145
+ width 16px
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "1.72.18",
3
+ "version": "1.72.36",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",