@electerm/electerm-react 1.35.6 → 1.36.1

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.
@@ -216,6 +216,7 @@ export const syncTokenCreateUrls = {
216
216
  custom: 'https://github.com/electerm/electerm/wiki/Custom-sync-server'
217
217
  }
218
218
  export const settingSyncId = 'setting-sync'
219
+ export const settingTerminalId = 'setting-terminal'
219
220
  export const settingShortcutsId = 'setting-shortcuts'
220
221
  export const settingCommonId = 'setting-common'
221
222
  export const defaultEnvLang = 'en_US.UTF-8'
@@ -3,6 +3,7 @@ import { Popover } from 'antd'
3
3
  import { HexColorPicker } from 'react-colorful'
4
4
  import { defaultColors, getRandomHexColor } from '../../common/rand-hex-color.js'
5
5
  import { HexInput } from './hex-input.jsx'
6
+ import './color-picker.styl'
6
7
 
7
8
  // Your Custom Color Picker component
8
9
  export const ColorPicker = React.forwardRef((props, ref) => {
@@ -22,6 +22,8 @@ import useQm from './use-quick-commands'
22
22
  import copy from 'json-deep-copy'
23
23
  import { defaults } from 'lodash-es'
24
24
  import renderRunScripts from './render-delayed-scripts.jsx'
25
+ import { ColorPickerItem } from './color-picker-item.jsx'
26
+ import { getRandomDefaultColor } from '../../common/rand-hex-color.js'
25
27
 
26
28
  const FormItem = Form.Item
27
29
  const { prefix } = window
@@ -60,6 +62,7 @@ export default function LocalFormUi (props) {
60
62
  term: props.store.config.terminalType,
61
63
  displayRaw: false,
62
64
  type: terminalLocalType,
65
+ color: getRandomDefaultColor(),
63
66
  runScripts: [{}],
64
67
  enableSsh: true
65
68
  }
@@ -75,10 +78,11 @@ export default function LocalFormUi (props) {
75
78
  <FormItem
76
79
  {...formItemLayout}
77
80
  label={e('title')}
78
- name='title'
79
81
  hasFeedback
80
82
  >
81
- <Input />
83
+ <FormItem noStyle name='title'>
84
+ <Input addonBefore={<ColorPickerItem />} />
85
+ </FormItem>
82
86
  </FormItem>
83
87
  <FormItem
84
88
  {...formItemLayout}
@@ -19,6 +19,7 @@ import useSubmit from './use-submit'
19
19
  import useUI from './use-ui'
20
20
  import useQm from './use-quick-commands'
21
21
  import renderCommon from './form-ssh-common'
22
+ import { getRandomDefaultColor } from '../../common/rand-hex-color.js'
22
23
  import copy from 'json-deep-copy'
23
24
  import { defaultsDeep, uniqBy } from 'lodash-es'
24
25
  import './bookmark-form.styl'
@@ -59,6 +60,7 @@ export default function TelnetFormUI (props) {
59
60
  id: '',
60
61
  username: 'root',
61
62
  password: 'guest',
63
+ color: getRandomDefaultColor(),
62
64
  runScripts: [{}],
63
65
  term: defaultSettings.terminalType,
64
66
  displayRaw: false,
@@ -23,6 +23,6 @@ export default function CustomCss (props) {
23
23
  }, [])
24
24
  useConditionalEffect(() => {
25
25
  applyTheme()
26
- }, delta && delta.prev && !eq(delta.prev, delta.curr))
26
+ }, delta && !eq(delta.prev, delta.curr))
27
27
  return null
28
28
  }
@@ -0,0 +1,124 @@
1
+ import {
2
+ Form,
3
+ Input,
4
+ Select,
5
+ Space,
6
+ Button
7
+ } from 'antd'
8
+ import { formItemLayout } from '../../common/form-layout'
9
+ import {
10
+ MinusCircleOutlined,
11
+ PlusOutlined
12
+ } from '@ant-design/icons'
13
+
14
+ const FormItem = Form.Item
15
+ const FormList = Form.List
16
+ const OptionSel = Select.Option
17
+ const { prefix } = window
18
+ const f = prefix('form')
19
+
20
+ export default function KeywordForm (props) {
21
+ const {
22
+ formData
23
+ } = props
24
+ console.log('formData', formData)
25
+ const [formChild] = Form.useForm()
26
+ function handleTrigger () {
27
+ formChild.submit()
28
+ }
29
+
30
+ function handleFinish (data) {
31
+ props.submit(data)
32
+ }
33
+
34
+ function renderItem (field, i, add, remove) {
35
+ return (
36
+ <Space
37
+ align='center'
38
+ key={field.key}
39
+ >
40
+ <FormItem
41
+ hasFeedback
42
+ >
43
+ <FormItem noStyle required name={[field.name, 'keyword']}>
44
+ <Input
45
+ addonBefore={renderBefore(field.name)}
46
+ />
47
+ </FormItem>
48
+ </FormItem>
49
+ <Button
50
+ icon={<MinusCircleOutlined />}
51
+ onClick={() => remove(field.name)}
52
+ className='mg24b'
53
+ />
54
+ </Space>
55
+ )
56
+ }
57
+
58
+ function renderBefore (name) {
59
+ const colors = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white']
60
+ return (
61
+ <FormItem name={[name, 'color']} noStyle>
62
+ <Select>
63
+ {
64
+ colors.map(color => {
65
+ const ps = {
66
+ className: 'color-picker-choose iblock',
67
+ style: {
68
+ backgroundColor: color
69
+ }
70
+ }
71
+ return (
72
+ <OptionSel key={color} value={color}>
73
+ <span
74
+ {...ps}
75
+ />
76
+ </OptionSel>
77
+ )
78
+ })
79
+ }
80
+ </Select>
81
+ </FormItem>
82
+ )
83
+ }
84
+
85
+ return (
86
+ <div>
87
+ <Form
88
+ form={formChild}
89
+ onValuesChange={handleTrigger}
90
+ initialValues={formData}
91
+ onFinish={handleFinish}
92
+ >
93
+ <FormItem {...formItemLayout}>
94
+ <FormList
95
+ name='keywords'
96
+ >
97
+ {
98
+ (fields, { add, remove }, { errors }) => {
99
+ return (
100
+ <div>
101
+ {
102
+ fields.map((field, i) => {
103
+ return renderItem(field, i, add, remove)
104
+ })
105
+ }
106
+ <FormItem>
107
+ <Button
108
+ type='dashed'
109
+ onClick={() => add()}
110
+ icon={<PlusOutlined />}
111
+ >
112
+ {f('keyword')}
113
+ </Button>
114
+ </FormItem>
115
+ </div>
116
+ )
117
+ }
118
+ }
119
+ </FormList>
120
+ </FormItem>
121
+ </Form>
122
+ </div>
123
+ )
124
+ }
@@ -1,6 +1,5 @@
1
1
  import React, { Component } from 'react'
2
2
  import {
3
- CodeOutlined,
4
3
  ArrowRightOutlined,
5
4
  LoadingOutlined
6
5
  } from '@ant-design/icons'
@@ -9,29 +8,21 @@ import {
9
8
  Select,
10
9
  Switch,
11
10
  Input,
12
- Upload,
13
11
  InputNumber,
14
12
  Alert,
15
13
  Button,
16
- AutoComplete,
17
- Tooltip,
18
14
  Table,
19
15
  Space
20
16
  } from 'antd'
21
17
  import deepCopy from 'json-deep-copy'
22
18
  import {
23
- noTerminalBgValue,
24
19
  settingMap,
25
- rendererTypes,
26
20
  proxyHelpLink
27
21
  } from '../../common/constants'
28
22
  import defaultSettings from '../../common/default-setting'
29
- import ShowItem from '../common/show-item'
30
- import { osResolve } from '../../common/resolve'
31
23
  import Link from '../common/external-link'
32
24
  import { isNumber, isNaN } from 'lodash-es'
33
25
  import createEditLangLink from '../../common/create-lang-edit-link'
34
- import mapper from '../../common/auto-complete-data-mapper'
35
26
  import StartSession from './start-session-select'
36
27
  import HelpIcon from '../common/help-icon'
37
28
  import delay from '../../common/wait.js'
@@ -41,7 +32,6 @@ const { Option } = Select
41
32
  const { prefix } = window
42
33
  const e = prefix('setting')
43
34
  const f = prefix('form')
44
- const s = prefix('ssh')
45
35
  const p = prefix('sftp')
46
36
  const t = prefix('terminalThemes')
47
37
 
@@ -58,7 +48,7 @@ const keys = [
58
48
  })
59
49
  ]
60
50
 
61
- export default class Setting extends Component {
51
+ export default class SettingCommon extends Component {
62
52
  state = {
63
53
  ready: false,
64
54
  languageChanged: false,
@@ -202,20 +192,6 @@ export default class Setting extends Component {
202
192
  this.onChangeValue(value, 'onStartSessions')
203
193
  }
204
194
 
205
- handleChangeFont = (values) => {
206
- this.onChangeValue(
207
- values.join(', '),
208
- 'fontFamily'
209
- )
210
- }
211
-
212
- handleChangeCursorStyle = (cursorStyle) => {
213
- this.onChangeValue(
214
- cursorStyle,
215
- 'cursorStyle'
216
- )
217
- }
218
-
219
195
  saveConfig = async (ext) => {
220
196
  const { config } = this.props
221
197
  if (ext.hotkey && ext.hotkey !== config.hotkey) {
@@ -386,111 +362,6 @@ export default class Setting extends Component {
386
362
  )
387
363
  }
388
364
 
389
- renderBgOption = item => {
390
- return {
391
- value: item.value,
392
- label: item.desc
393
- }
394
- }
395
-
396
- renderTerminalBgSelect = (name) => {
397
- const value = this.props.config[name]
398
- const defaultValue = defaultSettings[name]
399
- const onChange = (v) => this.onChangeValue(v, name)
400
- const after = (
401
- <Upload
402
- beforeUpload={(file) => {
403
- this.onChangeValue(file.path, name)
404
- return false
405
- }}
406
- showUploadList={false}
407
- >
408
- <span>{e('chooseFile')}</span>
409
- </Upload>
410
- )
411
- const dataSource = [
412
- {
413
- value: '',
414
- desc: t('default')
415
- },
416
- {
417
- value: noTerminalBgValue,
418
- desc: e('noTerminalBg')
419
- }
420
- ]
421
- const numberOpts = { step: 0.05, min: 0, max: 1, cls: 'bg-img-setting' }
422
-
423
- const renderFilter = () => {
424
- if (this.props.config[name] === noTerminalBgValue) return
425
-
426
- return (
427
- <div>
428
- {
429
- this.renderNumber(
430
- 'terminalBackgroundFilterOpacity',
431
- numberOpts,
432
- e('Opacity')
433
- )
434
- }
435
- {
436
- this.renderNumber(
437
- 'terminalBackgroundFilterBlur',
438
- { ...numberOpts, min: 0, max: 50, step: 0.5 },
439
- e('Blur')
440
- )
441
- }
442
- {
443
- this.renderNumber(
444
- 'terminalBackgroundFilterBrightness',
445
- { ...numberOpts, min: 0, max: 10, step: 0.1 },
446
- e('Brightness')
447
- )
448
- }
449
- {
450
- this.renderNumber(
451
- 'terminalBackgroundFilterGrayscale',
452
- numberOpts,
453
- e('Grayscale')
454
- )
455
- }
456
- {
457
- this.renderNumber(
458
- 'terminalBackgroundFilterContrast',
459
- { ...numberOpts, min: 0, max: 10, step: 0.1 },
460
- e('Contrast')
461
- )
462
- }
463
- </div>
464
- )
465
- }
466
-
467
- return (
468
- <div className='pd2b'>
469
- <div className='pd1b'>
470
- <Tooltip
471
- title='eg: https://xx.com/xx.png or /path/to/xx.png'
472
- >
473
- <AutoComplete
474
- value={value}
475
- onChange={onChange}
476
- placeholder={defaultValue}
477
- className='width-100'
478
- options={dataSource.map(this.renderBgOption)}
479
- >
480
- <Input
481
- addonAfter={after}
482
- />
483
- </AutoComplete>
484
- </Tooltip>
485
- </div>
486
-
487
- {
488
- renderFilter()
489
- }
490
- </div>
491
- )
492
- }
493
-
494
365
  renderReset = () => {
495
366
  return (
496
367
  <div className='pd1b pd1t'>
@@ -503,20 +374,6 @@ export default class Setting extends Component {
503
374
  )
504
375
  }
505
376
 
506
- renderDefaultTerminalType = () => {
507
- const opts = this.props.config.terminalTypes.map(mapper)
508
- return (
509
- <AutoComplete
510
- options={opts}
511
- style={{
512
- width: '200px'
513
- }}
514
- value={this.props.config.terminalType}
515
- onChange={(v) => this.onChangeValue(v, 'terminalType')}
516
- />
517
- )
518
- }
519
-
520
377
  renderProxy () {
521
378
  const {
522
379
  enableGlobalProxy
@@ -587,82 +444,6 @@ export default class Setting extends Component {
587
444
  )
588
445
  }
589
446
 
590
- renderCursorStyleSelect = () => {
591
- const {
592
- cursorStyle = 'block'
593
- } = this.props.config
594
- const sets = [
595
- {
596
- id: 'block',
597
- title: '▊'
598
- },
599
- {
600
- id: 'underline',
601
- title: '_'
602
- },
603
- {
604
- id: 'bar',
605
- title: '|'
606
- }
607
- ]
608
- const props = {
609
- onChange: this.handleChangeCursorStyle,
610
- value: cursorStyle,
611
- style: {
612
- width: '100px'
613
- }
614
- }
615
- return (
616
- <div className='pd1b'>
617
- <span className='inline-title mg1r'>{e('cursorStyle')}</span>
618
- <Select
619
- {...props}
620
- showSearch
621
- >
622
- {
623
- sets.map(f => {
624
- return (
625
- <Option value={f.id} key={f.id}>
626
- <b>{f.title}</b>
627
- </Option>
628
- )
629
- })
630
- }
631
- </Select>
632
- </div>
633
- )
634
- }
635
-
636
- renderFontFamily = () => {
637
- const { fonts } = this.props.store
638
- const { fontFamily } = this.props.config
639
- const props = {
640
- mode: 'multiple',
641
- onChange: this.handleChangeFont,
642
- value: fontFamily.split(/, */g)
643
- }
644
- return (
645
- <Select
646
- {...props}
647
- showSearch
648
- >
649
- {
650
- fonts.map(f => {
651
- return (
652
- <Option value={f} key={f}>
653
- <span style={{
654
- fontFamily: f
655
- }}
656
- >{f}
657
- </span>
658
- </Option>
659
- )
660
- })
661
- }
662
- </Select>
663
- )
664
- }
665
-
666
447
  renderLoginPassAfter () {
667
448
  const {
668
449
  loginPass,
@@ -726,15 +507,12 @@ export default class Setting extends Component {
726
507
  const {
727
508
  hotkey,
728
509
  language,
729
- rendererType,
730
510
  theme,
731
511
  customCss
732
512
  } = this.props.config
733
513
  const {
734
- appPath,
735
514
  langs
736
515
  } = this.props.store
737
- const terminalLogPath = appPath ? osResolve(appPath, 'electerm', 'session_logs') : window.et.sessionLogPath
738
516
  const terminalThemes = this.props.store.getSidebarList(settingMap.terminalThemes)
739
517
  const [modifier, key] = hotkey.split('+')
740
518
  const pops = {
@@ -778,12 +556,6 @@ export default class Setting extends Component {
778
556
  />
779
557
  </div>
780
558
  {this.renderProxy()}
781
- {
782
- this.renderNumber('scrollback', {
783
- step: 200,
784
- min: 1000
785
- }, e('scrollBackDesc'), 400)
786
- }
787
559
  {
788
560
  this.renderNumber('sshReadyTimeout', {
789
561
  step: 200,
@@ -810,7 +582,7 @@ export default class Setting extends Component {
810
582
  }
811
583
 
812
584
  <div className='pd2b'>
813
- <span className='inline-title mg1r'>{e('terminalTheme')}</span>
585
+ <span className='inline-title mg1r'>{t('uiThemes')}</span>
814
586
  <Select
815
587
  onChange={this.handleChangeTerminalTheme}
816
588
  popupMatchSelectWidth={false}
@@ -855,52 +627,6 @@ export default class Setting extends Component {
855
627
  <Link className='mg1l' to={createEditLangLink(language)}>{p('edit')}</Link>
856
628
  </div>
857
629
  {this.renderRestart('languageChanged')}
858
- <div className='pd1y font16 bold'>
859
- <CodeOutlined className='mg1r' />
860
- {s('terminal')} {e('settings')}
861
- </div>
862
- <div className='pd2b'>
863
- <span className='inline-title mg1r'>{e('rendererType')}</span>
864
- <Select
865
- onChange={v => this.onChangeValue(v, 'rendererType')}
866
- value={rendererType}
867
- popupMatchSelectWidth={false}
868
- >
869
- {
870
- Object.keys(rendererTypes).map(id => {
871
- return (
872
- <Option key={id} value={id}>{id}</Option>
873
- )
874
- })
875
- }
876
- </Select>
877
- </div>
878
- {
879
- this.renderNumber('fontSize', {
880
- step: 1,
881
- min: 9
882
- }, `${t('default')} ${e('fontSize')}`, 400)
883
- }
884
- <div className='pd2b'>
885
- <span className='inline-title mg1r'>{t('default')} {e('fontFamily')}</span>
886
- {
887
- this.renderFontFamily()
888
- }
889
- </div>
890
- <div className='pd2b'>
891
- <span className='inline-title mg1r'>{e('defaultTerminalType')}</span>
892
- {
893
- this.renderDefaultTerminalType()
894
- }
895
- </div>
896
- <div className='pd1b'>{e('terminalBackgroundImage')}</div>
897
- {
898
- this.renderTerminalBgSelect('terminalBackgroundImagePath')
899
- }
900
- <div className='pd1b'>{e('terminalWordSeparator')}</div>
901
- {
902
- this.renderText('terminalWordSeparator', e('terminalWordSeparator'))
903
- }
904
630
  <div className='pd1b'>{t('default')} {e('execWindows')}</div>
905
631
  {
906
632
  this.renderTextExec('execWindows')
@@ -913,31 +639,19 @@ export default class Setting extends Component {
913
639
  {
914
640
  this.renderTextExec('execLinux')
915
641
  }
916
- {
917
- this.renderCursorStyleSelect()
918
- }
919
642
  {
920
643
  [
921
644
  'autoRefreshWhenSwitchToSftp',
922
645
  'screenReaderMode',
923
646
  'initDefaultTabOnStart',
924
- 'cursorBlink',
925
- 'rightClickSelectsWord',
926
- 'pasteWhenContextMenu',
927
- 'copyWhenSelect',
928
647
  'disableSshHistory',
929
648
  'disableTransferHistory',
930
- 'ctrlOrMetaOpenTerminalLink',
931
649
  'checkUpdateOnStart',
932
650
  'useSystemTitleBar',
933
651
  'confirmBeforeExit',
934
652
  'debug'
935
653
  ].map(this.renderToggle)
936
654
  }
937
- {this.renderToggle('saveTerminalLogToFile', (
938
- <ShowItem to={terminalLogPath} className='mg1l'>{p('open')}</ShowItem>
939
- ))}
940
-
941
655
  {this.renderLoginPass()}
942
656
  {this.renderReset()}
943
657
  </div>
@@ -0,0 +1,494 @@
1
+ import React, { Component } from 'react'
2
+ import {
3
+ CodeOutlined,
4
+ LoadingOutlined
5
+ } from '@ant-design/icons'
6
+ import {
7
+ message,
8
+ Select,
9
+ Switch,
10
+ Input,
11
+ Upload,
12
+ InputNumber,
13
+ Button,
14
+ AutoComplete,
15
+ Tooltip
16
+ } from 'antd'
17
+ import deepCopy from 'json-deep-copy'
18
+ import {
19
+ noTerminalBgValue,
20
+ rendererTypes
21
+ } from '../../common/constants'
22
+ import defaultSettings from '../../common/default-setting'
23
+ import ShowItem from '../common/show-item'
24
+ import { osResolve } from '../../common/resolve'
25
+ import { isNumber, isNaN } from 'lodash-es'
26
+ import mapper from '../../common/auto-complete-data-mapper'
27
+ import KeywordForm from './keywords-form'
28
+ import HelpIcon from '../common/help-icon'
29
+ import './setting.styl'
30
+
31
+ const { Option } = Select
32
+ const { prefix } = window
33
+ const e = prefix('setting')
34
+ const s = prefix('ssh')
35
+ const p = prefix('sftp')
36
+ const t = prefix('terminalThemes')
37
+ const f = prefix('form')
38
+
39
+ export default class SettingTerminal extends Component {
40
+ state = {
41
+ ready: false
42
+ }
43
+
44
+ componentDidMount () {
45
+ this.timer = setTimeout(() => {
46
+ this.setState({
47
+ ready: true
48
+ })
49
+ }, 200)
50
+ }
51
+
52
+ componentWillUnmount () {
53
+ clearTimeout(this.timer)
54
+ }
55
+
56
+ handleResetAll = () => {
57
+ this.saveConfig(
58
+ deepCopy(defaultSettings)
59
+ )
60
+ }
61
+
62
+ onChangeValue = (value, name) => {
63
+ if (name === 'useSystemTitleBar') {
64
+ message.info(e('useSystemTitleBarTip'), 5)
65
+ }
66
+ this.saveConfig({
67
+ [name]: value
68
+ })
69
+ }
70
+
71
+ handleChangeFont = (values) => {
72
+ this.onChangeValue(
73
+ values.join(', '),
74
+ 'fontFamily'
75
+ )
76
+ }
77
+
78
+ handleChangeCursorStyle = (cursorStyle) => {
79
+ this.onChangeValue(
80
+ cursorStyle,
81
+ 'cursorStyle'
82
+ )
83
+ }
84
+
85
+ saveConfig = async (ext) => {
86
+ const { config } = this.props
87
+ if (ext.hotkey && ext.hotkey !== config.hotkey) {
88
+ const res = await window.pre.runGlobalAsync('changeHotkey', ext.hotkey)
89
+ if (!res) {
90
+ message.warning(e('hotkeyNotOk'))
91
+ delete ext.hotkey
92
+ } else {
93
+ message.success(e('saved'))
94
+ }
95
+ }
96
+ this.props.store.setConfig(ext)
97
+ }
98
+
99
+ handleSubmitKeywords = (data) => {
100
+ return this.saveConfig(data)
101
+ }
102
+
103
+ renderToggle = (name, extra = null) => {
104
+ const checked = !!this.props.config[name]
105
+ return (
106
+ <div className='pd2b' key={'rt' + name}>
107
+ <Switch
108
+ checked={checked}
109
+ checkedChildren={e(name)}
110
+ unCheckedChildren={e(name)}
111
+ onChange={v => this.onChangeValue(v, name)}
112
+ />
113
+ {isNumber(extra) ? null : extra}
114
+ </div>
115
+ )
116
+ }
117
+
118
+ renderNumber = (name, options, title = '', width = 136) => {
119
+ let value = this.props.config[name]
120
+ if (options.valueParser) {
121
+ value = options.valueParser(value)
122
+ }
123
+ const defaultValue = defaultSettings[name]
124
+ const {
125
+ step = 1,
126
+ min,
127
+ max,
128
+ cls,
129
+ onChange = (v) => {
130
+ this.onChangeValue(v, name)
131
+ }
132
+ } = options
133
+ const opts = {
134
+ step,
135
+ value,
136
+ min,
137
+ max,
138
+ onChange,
139
+ placeholder: defaultValue
140
+ }
141
+ if (title) {
142
+ opts.formatter = v => `${title}${options.extraDesc || ''}: ${v}`
143
+ opts.parser = (v) => {
144
+ let vv = isNumber(v)
145
+ ? v
146
+ : Number(v.split(': ')[1], 10)
147
+ if (isNaN(vv)) {
148
+ vv = defaultValue
149
+ }
150
+ return vv
151
+ }
152
+ opts.style = {
153
+ width: width + 'px'
154
+ }
155
+ }
156
+ return (
157
+ <div className={`pd2b ${cls || ''}`}>
158
+ <InputNumber
159
+ {...opts}
160
+ />
161
+ </div>
162
+ )
163
+ }
164
+
165
+ renderText = (name, placeholder) => {
166
+ const value = this.props.config[name]
167
+ const defaultValue = defaultSettings[name]
168
+ const onChange = (e) => this.onChangeValue(e.target.value, name)
169
+ return (
170
+ <div className='pd2b'>
171
+ <Input
172
+ value={value}
173
+ onChange={onChange}
174
+ placeholder={placeholder || defaultValue}
175
+ />
176
+ </div>
177
+ )
178
+ }
179
+
180
+ renderBgOption = item => {
181
+ return {
182
+ value: item.value,
183
+ label: item.desc
184
+ }
185
+ }
186
+
187
+ renderTerminalBgSelect = (name) => {
188
+ const value = this.props.config[name]
189
+ const defaultValue = defaultSettings[name]
190
+ const onChange = (v) => this.onChangeValue(v, name)
191
+ const after = (
192
+ <Upload
193
+ beforeUpload={(file) => {
194
+ this.onChangeValue(file.path, name)
195
+ return false
196
+ }}
197
+ showUploadList={false}
198
+ >
199
+ <span>{e('chooseFile')}</span>
200
+ </Upload>
201
+ )
202
+ const dataSource = [
203
+ {
204
+ value: '',
205
+ desc: t('default')
206
+ },
207
+ {
208
+ value: noTerminalBgValue,
209
+ desc: e('noTerminalBg')
210
+ }
211
+ ]
212
+ const numberOpts = { step: 0.05, min: 0, max: 1, cls: 'bg-img-setting' }
213
+
214
+ const renderFilter = () => {
215
+ if (this.props.config[name] === noTerminalBgValue) return
216
+
217
+ return (
218
+ <div>
219
+ {
220
+ this.renderNumber(
221
+ 'terminalBackgroundFilterOpacity',
222
+ numberOpts,
223
+ e('Opacity')
224
+ )
225
+ }
226
+ {
227
+ this.renderNumber(
228
+ 'terminalBackgroundFilterBlur',
229
+ { ...numberOpts, min: 0, max: 50, step: 0.5 },
230
+ e('Blur')
231
+ )
232
+ }
233
+ {
234
+ this.renderNumber(
235
+ 'terminalBackgroundFilterBrightness',
236
+ { ...numberOpts, min: 0, max: 10, step: 0.1 },
237
+ e('Brightness')
238
+ )
239
+ }
240
+ {
241
+ this.renderNumber(
242
+ 'terminalBackgroundFilterGrayscale',
243
+ numberOpts,
244
+ e('Grayscale')
245
+ )
246
+ }
247
+ {
248
+ this.renderNumber(
249
+ 'terminalBackgroundFilterContrast',
250
+ { ...numberOpts, min: 0, max: 10, step: 0.1 },
251
+ e('Contrast')
252
+ )
253
+ }
254
+ </div>
255
+ )
256
+ }
257
+
258
+ return (
259
+ <div className='pd2b'>
260
+ <div className='pd1b'>
261
+ <Tooltip
262
+ title='eg: https://xx.com/xx.png or /path/to/xx.png'
263
+ >
264
+ <AutoComplete
265
+ value={value}
266
+ onChange={onChange}
267
+ placeholder={defaultValue}
268
+ className='width-100'
269
+ options={dataSource.map(this.renderBgOption)}
270
+ >
271
+ <Input
272
+ addonAfter={after}
273
+ />
274
+ </AutoComplete>
275
+ </Tooltip>
276
+ </div>
277
+
278
+ {
279
+ renderFilter()
280
+ }
281
+ </div>
282
+ )
283
+ }
284
+
285
+ renderReset = () => {
286
+ return (
287
+ <div className='pd1b pd1t'>
288
+ <Button
289
+ onClick={this.handleResetAll}
290
+ >
291
+ {e('resetAllToDefault')}
292
+ </Button>
293
+ </div>
294
+ )
295
+ }
296
+
297
+ renderDefaultTerminalType = () => {
298
+ const opts = this.props.config.terminalTypes.map(mapper)
299
+ return (
300
+ <AutoComplete
301
+ options={opts}
302
+ style={{
303
+ width: '200px'
304
+ }}
305
+ value={this.props.config.terminalType}
306
+ onChange={(v) => this.onChangeValue(v, 'terminalType')}
307
+ />
308
+ )
309
+ }
310
+
311
+ renderCursorStyleSelect = () => {
312
+ const {
313
+ cursorStyle = 'block'
314
+ } = this.props.config
315
+ const sets = [
316
+ {
317
+ id: 'block',
318
+ title: '▊'
319
+ },
320
+ {
321
+ id: 'underline',
322
+ title: '_'
323
+ },
324
+ {
325
+ id: 'bar',
326
+ title: '|'
327
+ }
328
+ ]
329
+ const props = {
330
+ onChange: this.handleChangeCursorStyle,
331
+ value: cursorStyle,
332
+ style: {
333
+ width: '100px'
334
+ }
335
+ }
336
+ return (
337
+ <div className='pd1b'>
338
+ <span className='inline-title mg1r'>{e('cursorStyle')}</span>
339
+ <Select
340
+ {...props}
341
+ showSearch
342
+ >
343
+ {
344
+ sets.map(f => {
345
+ return (
346
+ <Option value={f.id} key={f.id}>
347
+ <b>{f.title}</b>
348
+ </Option>
349
+ )
350
+ })
351
+ }
352
+ </Select>
353
+ </div>
354
+ )
355
+ }
356
+
357
+ renderFontFamily = () => {
358
+ const { fonts } = this.props.store
359
+ const { fontFamily } = this.props.config
360
+ const props = {
361
+ mode: 'multiple',
362
+ onChange: this.handleChangeFont,
363
+ value: fontFamily.split(/, */g)
364
+ }
365
+ return (
366
+ <Select
367
+ {...props}
368
+ showSearch
369
+ >
370
+ {
371
+ fonts.map(f => {
372
+ return (
373
+ <Option value={f} key={f}>
374
+ <span style={{
375
+ fontFamily: f
376
+ }}
377
+ >{f}
378
+ </span>
379
+ </Option>
380
+ )
381
+ })
382
+ }
383
+ </Select>
384
+ )
385
+ }
386
+
387
+ render () {
388
+ const { ready } = this.state
389
+ if (!ready) {
390
+ return (
391
+ <div className='pd3 aligncenter'>
392
+ <LoadingOutlined />
393
+ </div>
394
+ )
395
+ }
396
+ const {
397
+ rendererType,
398
+ keywords = [{ color: 'red' }]
399
+ } = this.props.config
400
+ const {
401
+ appPath
402
+ } = this.props.store
403
+ const ps = {
404
+ formData: {
405
+ keywords
406
+ },
407
+ submit: this.handleSubmitKeywords
408
+ }
409
+ const terminalLogPath = appPath ? osResolve(appPath, 'electerm', 'session_logs') : window.et.sessionLogPath
410
+ return (
411
+ <div className='form-wrap pd1y pd2x'>
412
+ <div className='pd1y font16 bold'>
413
+ <CodeOutlined className='mg1r' />
414
+ {s('terminal')} {e('settings')}
415
+ </div>
416
+ {
417
+ this.renderNumber('scrollback', {
418
+ step: 200,
419
+ min: 1000
420
+ }, e('scrollBackDesc'), 400)
421
+ }
422
+ <div className='pd2b'>
423
+ <span className='inline-title mg1r'>{e('rendererType')}</span>
424
+ <Select
425
+ onChange={v => this.onChangeValue(v, 'rendererType')}
426
+ value={rendererType}
427
+ popupMatchSelectWidth={false}
428
+ >
429
+ {
430
+ Object.keys(rendererTypes).map(id => {
431
+ return (
432
+ <Option key={id} value={id}>{id}</Option>
433
+ )
434
+ })
435
+ }
436
+ </Select>
437
+ </div>
438
+ {
439
+ this.renderNumber('fontSize', {
440
+ step: 1,
441
+ min: 9
442
+ }, `${t('default')} ${e('fontSize')}`, 400)
443
+ }
444
+ <div className='pd2b'>
445
+ <span className='inline-title mg1r'>{t('default')} {e('fontFamily')}</span>
446
+ {
447
+ this.renderFontFamily()
448
+ }
449
+ </div>
450
+ <div className='pd2b'>
451
+ <div className='pd1b'>
452
+ <span className='inline-title mg1r'>{f('keywordsHighlight')}</span>
453
+ <HelpIcon
454
+ title={f('supportRegexp')}
455
+ />
456
+ </div>
457
+ <KeywordForm
458
+ {...ps}
459
+ />
460
+ </div>
461
+ <div className='pd2b'>
462
+ <span className='inline-title mg1r'>{e('defaultTerminalType')}</span>
463
+ {
464
+ this.renderDefaultTerminalType()
465
+ }
466
+ </div>
467
+ <div className='pd1b'>{e('terminalBackgroundImage')}</div>
468
+ {
469
+ this.renderTerminalBgSelect('terminalBackgroundImagePath')
470
+ }
471
+ <div className='pd1b'>{e('terminalWordSeparator')}</div>
472
+ {
473
+ this.renderText('terminalWordSeparator', e('terminalWordSeparator'))
474
+ }
475
+ {
476
+ this.renderCursorStyleSelect()
477
+ }
478
+ {
479
+ [
480
+ 'cursorBlink',
481
+ 'rightClickSelectsWord',
482
+ 'pasteWhenContextMenu',
483
+ 'copyWhenSelect',
484
+ 'ctrlOrMetaOpenTerminalLink'
485
+ ].map(this.renderToggle)
486
+ }
487
+ {this.renderToggle('saveTerminalLogToFile', (
488
+ <ShowItem to={terminalLogPath} className='mg1l'>{p('open')}</ShowItem>
489
+ ))}
490
+ {this.renderReset()}
491
+ </div>
492
+ )
493
+ }
494
+ }
@@ -1,4 +1,5 @@
1
- import Setting from './setting'
1
+ import SettingCommon from './setting-common'
2
+ import SettingTerminal from './setting-terminal'
2
3
  import SettingCol from './col'
3
4
  import SyncSetting from '../setting-sync/setting-sync'
4
5
  import Shortcuts from '../shortcuts/shortcuts'
@@ -6,6 +7,7 @@ import List from './list'
6
7
  import {
7
8
  settingMap,
8
9
  settingSyncId,
10
+ settingTerminalId,
9
11
  settingShortcutsId
10
12
  } from '../../common/constants'
11
13
 
@@ -25,10 +27,12 @@ export default function TabSettings (props) {
25
27
  const sid = settingItem.id
26
28
  if (sid === settingSyncId) {
27
29
  elem = <SyncSetting store={store} />
30
+ } else if (sid === settingTerminalId) {
31
+ elem = <SettingTerminal {...listProps} config={store.config} />
28
32
  } else if (sid === settingShortcutsId) {
29
33
  elem = <Shortcuts store={store} />
30
34
  } else {
31
- elem = <Setting {...listProps} config={store.config} />
35
+ elem = <SettingCommon {...listProps} config={store.config} />
32
36
  }
33
37
  return (
34
38
  <div
@@ -94,7 +94,7 @@ export default class FileListTable extends React.Component {
94
94
  style: {
95
95
  width: w + 'px',
96
96
  left: (w * i) + 'px',
97
- zIndex: 3 + i
97
+ zIndex: 3 + i * 2
98
98
  }
99
99
  }
100
100
  })
@@ -110,7 +110,8 @@ export default class FileListTable extends React.Component {
110
110
  nextProp: properties[i + 1].name,
111
111
  style: {
112
112
  left: (w * (i + 1) - (splitDraggerWidth / 2)) + 'px',
113
- width: splitDraggerWidth + 'px'
113
+ width: splitDraggerWidth + 'px',
114
+ zIndex: 4 + i * 2
114
115
  }
115
116
  }
116
117
  ]
@@ -0,0 +1,63 @@
1
+ export class KeywordHighlighterAddon {
2
+ constructor (keywords) {
3
+ this.keywords = keywords
4
+ }
5
+
6
+ escape = str => {
7
+ return str.replace(/\\x1B/g, '\\x1B')
8
+ .replace(/\033/g, '\\033')
9
+ }
10
+
11
+ colorize = (color) => {
12
+ // Use a switch statement to map color names to ANSI codes
13
+ switch (color) {
14
+ case 'green':
15
+ return '\u001b[32m$&\u001b[0m'
16
+ case 'yellow':
17
+ return '\u001b[33m$&\u001b[0m'
18
+ case 'blue':
19
+ return '\u001b[34m$&\u001b[0m'
20
+ case 'magenta':
21
+ return '\u001b[35m$&\u001b[0m'
22
+ case 'cyan':
23
+ return '\u001b[36m$&\u001b[0m'
24
+ case 'white':
25
+ return '\u001b[37m$&\u001b[0m'
26
+ default:
27
+ return '\u001b[31m$&\u001b[0m'
28
+ }
29
+ }
30
+
31
+ highlightKeywords = (text) => {
32
+ console.log('this.keywords', this.keywords)
33
+ for (const obj of this.keywords) {
34
+ const {
35
+ keyword,
36
+ color = 'red'
37
+ } = obj || {}
38
+ if (keyword) {
39
+ const regex = new RegExp(`(${keyword})`, 'gi')
40
+ text = text.replace(regex, this.colorize(color))
41
+ }
42
+ }
43
+ return text
44
+ }
45
+
46
+ activate (terminal) {
47
+ this.terminal = terminal
48
+ // Override the write method to automatically highlight keywords
49
+ const originalWrite = terminal.write
50
+ terminal.write = (data) => {
51
+ originalWrite.call(
52
+ terminal,
53
+ terminal.displayRaw ? this.escape(data) : this.highlightKeywords(data)
54
+ )
55
+ }
56
+ this.originalWrite = originalWrite
57
+ }
58
+
59
+ dispose () {
60
+ // Restore the original write method when disposing the addon
61
+ this.terminal.write = this.originalWrite
62
+ }
63
+ }
@@ -51,6 +51,7 @@ import NormalBuffer from './normal-buffer'
51
51
  import { createTerm, resizeTerm } from './terminal-apis'
52
52
  import createLsId from './build-ls-term-id'
53
53
  import { shortcutExtend, shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
54
+ import { KeywordHighlighterAddon } from './highlight-addon.js'
54
55
 
55
56
  const { prefix } = window
56
57
  const e = prefix('ssh')
@@ -820,10 +821,6 @@ class Term extends Component {
820
821
  term.onData(this.onData)
821
822
  this.term = term
822
823
  term.attachCustomKeyEventHandler(this.handleKeyboardEvent.bind(this))
823
- term.onKey(this.onKey)
824
- // if (host && !password && !privateKey) {
825
- // return this.promote()
826
- // }
827
824
  await this.remoteInit(term)
828
825
  }
829
826
 
@@ -905,7 +902,13 @@ class Term extends Component {
905
902
  })
906
903
  const { cols, rows } = term
907
904
  const { config } = this.props
908
- const { host, port, tokenElecterm, server = '' } = config
905
+ const {
906
+ host,
907
+ port,
908
+ tokenElecterm,
909
+ keywords = [],
910
+ server = ''
911
+ } = config
909
912
  const { sessionId, terminalIndex, id, logName } = this.props
910
913
  const tab = deepCopy(this.props.tab || {})
911
914
  const {
@@ -1025,46 +1028,14 @@ class Term extends Component {
1025
1028
  noTerminalWriteOutsideSession: true
1026
1029
  }, this)
1027
1030
  }
1028
-
1029
- // this.decoder = new TextDecoder(encode)
1030
- if (displayRaw) {
1031
- const oldWrite = term.write
1032
- const th = this
1033
- term.write = function (data) {
1034
- // let str = ''
1035
- // if (typeof data === 'object') {
1036
- // if (data instanceof ArrayBuffer) {
1037
- // str = th.decoder.decode(data)
1038
- // oldWrite.call(term, th.escape(str))
1039
- // } else {
1040
- // const fileReader = new FileReader()
1041
- // fileReader.addEventListener('load', () => {
1042
- // str = th.decoder.decode(fileReader.result)
1043
- // oldWrite.call(term, th.escape(str))
1044
- // })
1045
- // fileReader.readAsArrayBuffer(new window.Blob([data]))
1046
- // }
1047
- // } else if (typeof data === 'string') {
1048
- // oldWrite.call(term, th.escape(data))
1049
- // } else {
1050
- // throw Error(`Cannot handle ${typeof data} websocket message.`)
1051
- // }
1052
- oldWrite.call(term, th.escape(data))
1053
- }
1054
- }
1031
+ term.displayRaw = displayRaw
1032
+ term.loadAddon(
1033
+ new KeywordHighlighterAddon(keywords)
1034
+ )
1055
1035
  this.term = term
1056
1036
  window.store.triggerResize()
1057
1037
  }
1058
1038
 
1059
- escape = str => {
1060
- return str.replace(/\\x1B/g, '\\x1B')
1061
- .replace(/\033/g, '\\033')
1062
- }
1063
-
1064
- onKey = (key, e) => {
1065
- // log.log('onKey', key, e)
1066
- }
1067
-
1068
1039
  onResize = throttle(() => {
1069
1040
  const cid = this.props.currentTabId
1070
1041
  const tid = this.props.tab?.id
@@ -35,7 +35,8 @@ import {
35
35
  sidebarPanelWidth,
36
36
  paneMap,
37
37
  settingSyncId,
38
- settingShortcutsId
38
+ settingShortcutsId,
39
+ settingTerminalId
39
40
  } from '../common/constants'
40
41
  import getInitItem from '../common/init-setting-item'
41
42
  import {
@@ -45,6 +46,7 @@ import {
45
46
  const { prefix } = window
46
47
  const ss = prefix('settingSync')
47
48
  const s = prefix('setting')
49
+ const sss = prefix('ssh')
48
50
 
49
51
  function getReverseColor (hex) {
50
52
  // Check if the input is a valid hex color code
@@ -248,12 +250,16 @@ class Store {
248
250
  get setting () {
249
251
  return [
250
252
  {
251
- id: settingSyncId,
252
- title: ss('settingSync')
253
+ id: settingTerminalId,
254
+ title: sss('terminal')
253
255
  },
254
256
  {
255
257
  id: settingShortcutsId,
256
258
  title: s('settingShortcuts')
259
+ },
260
+ {
261
+ id: settingSyncId,
262
+ title: ss('settingSync')
257
263
  }
258
264
  ]
259
265
  }
@@ -11,6 +11,7 @@ import copy from 'json-deep-copy'
11
11
  import {
12
12
  settingMap,
13
13
  settingCommonId,
14
+ settingSyncId,
14
15
  modals
15
16
  } from '../common/constants'
16
17
  import { buildNewTheme } from '../common/terminal-theme'
@@ -150,7 +151,7 @@ export default Store => {
150
151
  store.storeAssign({
151
152
  settingTab: settingMap.setting
152
153
  })
153
- store.setSettingItem(copy(store.setting[0]))
154
+ store.setSettingItem(copy(store.setting.find(d => d.id === settingSyncId)))
154
155
  store.openSettingModal()
155
156
  }
156
157
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "1.35.6",
3
+ "version": "1.36.1",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",