@electerm/electerm-react 1.35.0 → 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.
@@ -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
  ]
@@ -5,6 +5,7 @@
5
5
 
6
6
  import React from 'react'
7
7
  import { shortcutExtend } from './shortcut-handler.js'
8
+ import { throttle } from 'lodash-es'
8
9
 
9
10
  class ShortcutControl extends React.PureComponent {
10
11
  componentDidMount () {
@@ -27,10 +28,18 @@ class ShortcutControl extends React.PureComponent {
27
28
  window.store.onNewSsh()
28
29
  }
29
30
 
30
- togglefullscreenShortcut = (e) => {
31
+ togglefullscreenShortcut = throttle((e) => {
31
32
  e.stopPropagation()
32
- document.querySelector('.term-fullscreen-control').click()
33
- }
33
+ const x = document.querySelector('.term-fullscreen-control') ||
34
+ document.querySelector('.term-fullscreen-control1')
35
+ x && x.click()
36
+ }, 300)
37
+
38
+ splitShortcut = throttle((e) => {
39
+ e.stopPropagation()
40
+ const x = document.querySelector('.icon-split')
41
+ x && x.click()
42
+ }, 300)
34
43
 
35
44
  zoominShortcut = (e) => {
36
45
  e.stopPropagation()
@@ -9,7 +9,7 @@ import {
9
9
  CheckOutlined,
10
10
  CloseOutlined
11
11
  } from '@ant-design/icons'
12
- import { debounce } from 'lodash-es'
12
+ import { throttle } from 'lodash-es'
13
13
  import { getKeyCharacter } from './get-key-char.js'
14
14
 
15
15
  export default class ShortcutEdit extends PureComponent {
@@ -83,14 +83,14 @@ export default class ShortcutEdit extends PureComponent {
83
83
  return 'shortcut-control-' + index
84
84
  }
85
85
 
86
- warnCtrolKey = debounce(() => {
86
+ warnCtrolKey = throttle(() => {
87
87
  message.info(
88
88
  'Must have one of Ctrl or Shift or Alt or Meta key',
89
89
  undefined
90
90
  )
91
91
  }, 3000)
92
92
 
93
- warnExist = debounce(() => {
93
+ warnExist = throttle(() => {
94
94
  message.info(
95
95
  'Shortcut already exists',
96
96
  undefined
@@ -24,7 +24,6 @@ function buildConfig (config) {
24
24
 
25
25
  export function shortcutExtend (Cls) {
26
26
  Cls.prototype.handleKeyboardEvent = function (event) {
27
- // console.log('event', event)
28
27
  const {
29
28
  code,
30
29
  ctrlKey,
@@ -2,13 +2,13 @@ export default () => {
2
2
  return [
3
3
  {
4
4
  name: 'app_closeCurrentTab',
5
- shortcut: 'ctrl+w',
6
- shortcutMac: 'ctrl+w'
5
+ shortcut: 'alt+w',
6
+ shortcutMac: 'alt+w'
7
7
  },
8
8
  {
9
9
  name: 'app_newBookmark',
10
- shortcut: 'ctrl+b',
11
- shortcutMac: 'meta+b'
10
+ shortcut: 'ctrl+n',
11
+ shortcutMac: 'meta+n'
12
12
  },
13
13
  {
14
14
  name: 'app_togglefullscreen',
@@ -35,6 +35,11 @@ export default () => {
35
35
  shortcut: 'ctrl+tab',
36
36
  shortcutMac: 'ctrl+tab'
37
37
  },
38
+ {
39
+ name: 'terminal_split',
40
+ shortcut: 'ctrl+/',
41
+ shortcutMac: 'meta+/'
42
+ },
38
43
  {
39
44
  name: 'terminal_clear',
40
45
  shortcut: 'ctrl+l,ctrl+shift+l',
@@ -91,7 +91,7 @@ export default class Shortcuts extends Component {
91
91
  const pre = a === 'terminal' ? `[${ss('terminal')}] ` : ''
92
92
  if (
93
93
  [
94
- 'clear', 'selectAll', 'search'
94
+ 'clear', 'selectAll', 'search', 'split'
95
95
  ].includes(b)
96
96
  ) {
97
97
  return pre + ss(b)
@@ -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
+ }