@electerm/electerm-react 1.35.6 → 1.36.2

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.
@@ -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