@electerm/electerm-react 2.15.8 → 2.16.8

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.
Files changed (47) hide show
  1. package/client/components/ai/ai-chat.jsx +44 -2
  2. package/client/components/ai/ai-stop-icon.jsx +13 -0
  3. package/client/components/ai/ai.styl +10 -0
  4. package/client/components/bg/css-overwrite.jsx +158 -187
  5. package/client/components/bg/custom-css.jsx +8 -17
  6. package/client/components/bookmark-form/bookmark-schema.js +7 -1
  7. package/client/components/bookmark-form/common/color-picker.jsx +4 -8
  8. package/client/components/bookmark-form/common/exec-settings-field.jsx +44 -0
  9. package/client/components/bookmark-form/common/fields.jsx +3 -0
  10. package/client/components/bookmark-form/config/common-fields.js +1 -0
  11. package/client/components/bookmark-form/config/local.js +3 -1
  12. package/client/components/common/animate-text.jsx +22 -23
  13. package/client/components/common/modal.jsx +2 -0
  14. package/client/components/common/password.jsx +19 -32
  15. package/client/components/footer/cmd-history.jsx +154 -0
  16. package/client/components/footer/cmd-history.styl +73 -0
  17. package/client/components/footer/footer-entry.jsx +15 -1
  18. package/client/components/main/main.jsx +2 -3
  19. package/client/components/quick-commands/quick-commands-box.jsx +6 -3
  20. package/client/components/quick-commands/quick-commands-select.jsx +1 -4
  21. package/client/components/rdp/rdp-session.jsx +23 -4
  22. package/client/components/session/session.styl +1 -3
  23. package/client/components/setting-panel/terminal-bg-config.jsx +2 -0
  24. package/client/components/setting-panel/text-bg-modal.jsx +9 -9
  25. package/client/components/sftp/file-item.jsx +22 -0
  26. package/client/components/sidebar/history-item.jsx +6 -3
  27. package/client/components/sidebar/history.jsx +48 -5
  28. package/client/components/sidebar/sidebar-panel.jsx +0 -13
  29. package/client/components/sidebar/sidebar.styl +19 -0
  30. package/client/components/tabs/add-btn-menu.jsx +28 -4
  31. package/client/components/tabs/add-btn.jsx +1 -1
  32. package/client/components/tabs/add-btn.styl +8 -0
  33. package/client/components/terminal/terminal-command-dropdown.jsx +1 -1
  34. package/client/components/terminal/terminal.jsx +28 -11
  35. package/client/components/terminal/transfer-client-base.js +18 -2
  36. package/client/components/terminal/trzsz-client.js +2 -1
  37. package/client/components/terminal/zmodem-client.js +2 -1
  38. package/client/components/text-editor/edit-with-custom-editor.jsx +49 -0
  39. package/client/components/text-editor/text-editor-form.jsx +13 -5
  40. package/client/components/text-editor/text-editor.jsx +20 -1
  41. package/client/components/vnc/vnc-session.jsx +3 -0
  42. package/client/components/vnc/vnc.styl +1 -1
  43. package/client/store/common.js +31 -4
  44. package/client/store/init-state.js +26 -1
  45. package/client/store/store.js +3 -3
  46. package/client/store/watch.js +8 -1
  47. package/package.json +1 -1
@@ -301,6 +301,7 @@ export const sshAuthFields = [
301
301
  type: 'sshAgent',
302
302
  name: 'useSshAgent'
303
303
  },
304
+ { type: 'switch', name: 'isMFA', label: () => e('MFA/OTP'), valuePropName: 'checked' },
304
305
  commonFields.runScripts,
305
306
  commonFields.description,
306
307
  commonFields.setEnv,
@@ -86,7 +86,9 @@ const localConfig = {
86
86
  step: 1000
87
87
  }
88
88
  },
89
- { type: 'terminalBackground', name: 'terminalBackground', label: () => e('terminalBackgroundImage') }
89
+ { type: 'terminalBackground', name: 'terminalBackground', label: () => e('terminalBackgroundImage') },
90
+ // Exec settings - stored as flat properties on bookmark
91
+ { type: 'execSettings' }
90
92
  ]
91
93
  },
92
94
  {
@@ -2,34 +2,33 @@
2
2
  * animate text when text change
3
3
  */
4
4
 
5
- import React from 'react'
5
+ import React, { useRef, useEffect } from 'react'
6
6
 
7
- export default class AnimateText extends React.PureComponent {
8
- constructor (props) {
9
- super(props)
10
- this.textRef = React.createRef()
11
- }
7
+ export default function AnimateText ({ children, className }) {
8
+ const textRef = useRef(null)
9
+ const timerRef = useRef(null)
12
10
 
13
- componentDidUpdate () {
14
- const dom = this.textRef.current
15
- dom.className = (this.props.className || 'animate-text-wrap')
16
- this.timer = setTimeout(() => {
11
+ useEffect(() => {
12
+ const dom = textRef.current
13
+ if (!dom) return
14
+
15
+ dom.className = className || 'animate-text-wrap'
16
+ timerRef.current = setTimeout(() => {
17
17
  if (dom) {
18
- dom.className = this.props.className || 'animate-text-wrap'
18
+ dom.className = className || 'animate-text-wrap'
19
19
  }
20
20
  }, 450)
21
- }
22
21
 
23
- componentWillUnmount () {
24
- clearTimeout(this.timer)
25
- }
22
+ return () => {
23
+ if (timerRef.current) {
24
+ clearTimeout(timerRef.current)
25
+ }
26
+ }
27
+ }, [className])
26
28
 
27
- render () {
28
- const { children, className } = this.props
29
- return (
30
- <div className={className} ref={this.textRef}>
31
- {children}
32
- </div>
33
- )
34
- }
29
+ return (
30
+ <div className={className} ref={textRef}>
31
+ {children}
32
+ </div>
33
+ )
35
34
  }
@@ -113,6 +113,8 @@ export default function Modal (props) {
113
113
  )
114
114
  }
115
115
 
116
+ Modal.displayName = 'Modal'
117
+
116
118
  function createModalInstance (type, options) {
117
119
  const {
118
120
  title,
@@ -1,17 +1,12 @@
1
- import { useState, useCallback, forwardRef } from 'react'
1
+ import { useState, useCallback } from 'react'
2
2
  import {
3
3
  Input,
4
4
  Tag
5
5
  } from 'antd'
6
6
 
7
- /**
8
- * Password component that extends Ant Design's Password component
9
- * with caps lock detection and visual indicator
10
- */
11
- export default forwardRef(function Password (props, ref) {
7
+ export default function Password ({ ref, onKeyDown, onKeyUp, onFocus, onBlur, prefix, ...props }) {
12
8
  const [isCapsLockOn, setIsCapsLockOn] = useState(false)
13
9
 
14
- // Check caps lock state from keyboard event
15
10
  const checkCapsLock = useCallback((event) => {
16
11
  if (event.getModifierState) {
17
12
  const capsLockState = event.getModifierState('CapsLock')
@@ -19,37 +14,30 @@ export default forwardRef(function Password (props, ref) {
19
14
  }
20
15
  }, [])
21
16
 
22
- // Handle key events to detect caps lock changes
23
17
  const handleKeyEvent = useCallback((event) => {
24
18
  checkCapsLock(event)
25
- // Call original onKeyDown/onKeyUp if provided
26
- if (props.onKeyDown && event.type === 'keydown') {
27
- props.onKeyDown(event)
19
+ if (onKeyDown && event.type === 'keydown') {
20
+ onKeyDown(event)
28
21
  }
29
- if (props.onKeyUp && event.type === 'keyup') {
30
- props.onKeyUp(event)
22
+ if (onKeyUp && event.type === 'keyup') {
23
+ onKeyUp(event)
31
24
  }
32
- }, [props.onKeyDown, props.onKeyUp, checkCapsLock])
25
+ }, [onKeyDown, onKeyUp, checkCapsLock])
33
26
 
34
- // Handle focus event to check initial caps lock state
35
27
  const handleFocus = useCallback((event) => {
36
28
  checkCapsLock(event)
37
- // Call original onFocus if provided
38
- if (props.onFocus) {
39
- props.onFocus(event)
29
+ if (onFocus) {
30
+ onFocus(event)
40
31
  }
41
- }, [props.onFocus, checkCapsLock])
32
+ }, [onFocus, checkCapsLock])
42
33
 
43
- // Handle blur event to reset caps lock state
44
34
  const handleBlur = useCallback((event) => {
45
35
  setIsCapsLockOn(false)
46
- // Call original onBlur if provided
47
- if (props.onBlur) {
48
- props.onBlur(event)
36
+ if (onBlur) {
37
+ onBlur(event)
49
38
  }
50
- }, [props.onBlur])
39
+ }, [onBlur])
51
40
 
52
- // Show caps lock indicator inside prefix to avoid remounting the input wrapper
53
41
  let capsPrefix = null
54
42
  if (isCapsLockOn) {
55
43
  capsPrefix = (
@@ -57,13 +45,12 @@ export default forwardRef(function Password (props, ref) {
57
45
  )
58
46
  }
59
47
 
60
- // Merge any existing prefix from props with our caps indicator
61
- let prefix = capsPrefix
62
- if (props.prefix) {
63
- prefix = (
48
+ let mergedPrefix = capsPrefix
49
+ if (prefix) {
50
+ mergedPrefix = (
64
51
  <>
65
52
  {capsPrefix}
66
- {props.prefix}
53
+ {prefix}
67
54
  </>
68
55
  )
69
56
  }
@@ -72,11 +59,11 @@ export default forwardRef(function Password (props, ref) {
72
59
  <Input.Password
73
60
  {...props}
74
61
  ref={ref}
75
- prefix={prefix}
62
+ prefix={mergedPrefix}
76
63
  onKeyDown={handleKeyEvent}
77
64
  onKeyUp={handleKeyEvent}
78
65
  onFocus={handleFocus}
79
66
  onBlur={handleBlur}
80
67
  />
81
68
  )
82
- })
69
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * cmd history trigger button with popover
3
+ */
4
+
5
+ import { useState, useEffect } from 'react'
6
+ import { Button, Empty, Popover, Switch } from 'antd'
7
+ import { auto } from 'manate/react'
8
+ import { copy } from '../../common/clipboard'
9
+ import { HistoryOutlined, DeleteOutlined, CopyOutlined, UnorderedListOutlined } from '@ant-design/icons'
10
+ import InputAutoFocus from '../common/input-auto-focus'
11
+ import { getItemJSON, setItemJSON } from '../../common/safe-local-storage'
12
+ import './cmd-history.styl'
13
+
14
+ const e = window.translate
15
+ const SORT_BY_FREQ_KEY = 'electerm-cmd-history-sort-by-frequency'
16
+
17
+ export default auto(function CmdHistory (props) {
18
+ const [keyword, setKeyword] = useState('')
19
+ const [sortByFrequency, setSortByFrequency] = useState(() => {
20
+ return getItemJSON(SORT_BY_FREQ_KEY, false)
21
+ })
22
+ const { terminalCommandHistory } = props.store
23
+
24
+ useEffect(() => {
25
+ setItemJSON(SORT_BY_FREQ_KEY, sortByFrequency)
26
+ }, [sortByFrequency])
27
+
28
+ function handleRunCommand (cmd) {
29
+ window.store.runCmdFromHistory(cmd)
30
+ }
31
+
32
+ function handleDeleteCommand (cmd, ev) {
33
+ ev.stopPropagation()
34
+ terminalCommandHistory.delete(cmd)
35
+ }
36
+
37
+ function handleCopyCommand (cmd, ev) {
38
+ ev.stopPropagation()
39
+ copy(cmd)
40
+ }
41
+
42
+ function handleClearAll () {
43
+ window.store.clearAllCmdHistory()
44
+ }
45
+
46
+ function filterArray (array, keyword) {
47
+ if (!keyword) {
48
+ return array
49
+ }
50
+ return array.filter(item => item.cmd.toLowerCase().includes(keyword.toLowerCase()))
51
+ }
52
+
53
+ function handleChange (e) {
54
+ setKeyword(e.target.value)
55
+ }
56
+
57
+ const historyArray = Array.from(terminalCommandHistory || [])
58
+ .map(([cmd, info]) => ({ cmd, ...info }))
59
+ .reverse()
60
+
61
+ let filtered = filterArray(historyArray, keyword)
62
+
63
+ if (sortByFrequency) {
64
+ filtered = filtered.sort((a, b) => b.count - a.count)
65
+ }
66
+
67
+ const handleSortByFrequencyChange = (checked) => {
68
+ setSortByFrequency(checked)
69
+ }
70
+
71
+ const renderList = () => {
72
+ if (filtered.length === 0) {
73
+ return (
74
+ <Empty
75
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
76
+ description={e('noData')}
77
+ />
78
+ )
79
+ }
80
+ return filtered.map((item, index) => (
81
+ <div
82
+ key={index}
83
+ className='cmd-history-item'
84
+ onClick={() => handleRunCommand(item.cmd)}
85
+ >
86
+ <span className='cmd-history-item-text' title={item.cmd}>{item.cmd}</span>
87
+ <div className='cmd-history-item-actions'>
88
+ <span className='cmd-history-item-count' title={e('count') + ': ' + item.count}>
89
+ {item.count}
90
+ </span>
91
+ <Button
92
+ type='text'
93
+ size='small'
94
+ icon={<CopyOutlined />}
95
+ className='cmd-history-item-copy'
96
+ onClick={(ev) => handleCopyCommand(item.cmd, ev)}
97
+ />
98
+ <Button
99
+ type='text'
100
+ size='small'
101
+ icon={<DeleteOutlined />}
102
+ className='cmd-history-item-delete'
103
+ onClick={(ev) => handleDeleteCommand(item.cmd, ev)}
104
+ />
105
+ </div>
106
+ </div>
107
+ ))
108
+ }
109
+
110
+ const content = (
111
+ <div className='cmd-history-popover-content pd2'>
112
+ <div className='cmd-history-search pd2b'>
113
+ <InputAutoFocus
114
+ value={keyword}
115
+ onChange={handleChange}
116
+ placeholder={e('search')}
117
+ className='cmd-history-search-input'
118
+ allowClear
119
+ />
120
+ </div>
121
+ <div className='cmd-history-header pd2b'>
122
+ <Switch
123
+ checkedChildren={e('sortByFrequency')}
124
+ unCheckedChildren={e('sortByFrequency')}
125
+ checked={sortByFrequency}
126
+ onChange={handleSortByFrequencyChange}
127
+ size='small'
128
+ />
129
+ <UnorderedListOutlined
130
+ className='cmd-history-clear-icon pointer clear-ai-icon icon-hover'
131
+ title={e('clear')}
132
+ onClick={handleClearAll}
133
+ />
134
+ </div>
135
+ <div className='cmd-history-list'>
136
+ {renderList()}
137
+ </div>
138
+ </div>
139
+ )
140
+
141
+ return (
142
+ <Popover
143
+ content={content}
144
+ trigger='click'
145
+ placement='topLeft'
146
+ >
147
+ <Button
148
+ size='small'
149
+ type='text'
150
+ icon={<HistoryOutlined />}
151
+ />
152
+ </Popover>
153
+ )
154
+ })
@@ -0,0 +1,73 @@
1
+ .cmd-history-popover-content
2
+ max-height 300px
3
+ width 300px
4
+ overflow hidden
5
+ display flex
6
+ flex-direction column
7
+
8
+ .cmd-history-search
9
+ flex-shrink 0
10
+ position sticky
11
+ top 0
12
+
13
+ .cmd-history-header
14
+ flex-shrink 0
15
+ display flex
16
+ align-items center
17
+ justify-content space-between
18
+ padding-bottom 8px
19
+
20
+ .cmd-history-clear-icon
21
+ font-size 14px
22
+ padding 4px
23
+ cursor pointer
24
+ &:hover
25
+ color red
26
+
27
+ .cmd-history-list
28
+ overflow-x hidden
29
+ overflow-y auto
30
+ flex 1
31
+ min-height 0
32
+
33
+ .cmd-history-item
34
+ display flex
35
+ align-items center
36
+ justify-content space-between
37
+ padding 4px 8px
38
+ cursor pointer
39
+ &:hover
40
+ background var(--main-lighter)
41
+
42
+ .cmd-history-item-text
43
+ overflow hidden
44
+ text-overflow ellipsis
45
+ white-space nowrap
46
+ flex 1
47
+ min-width 0
48
+
49
+ .cmd-history-item-count
50
+ font-size 12px
51
+ color var(--text-color-secondary)
52
+ padding 0 4px
53
+ flex-shrink 0
54
+
55
+ .cmd-history-item-actions
56
+ display none
57
+ flex-shrink 0
58
+ gap 4px
59
+ align-items center
60
+ .cmd-history-item:hover &
61
+ display flex
62
+
63
+ .cmd-history-item-delete
64
+ cursor pointer
65
+ padding 2px 4px
66
+ &:hover
67
+ color red
68
+
69
+ .cmd-history-item-copy
70
+ cursor pointer
71
+ padding 2px 4px
72
+ &:hover
73
+ color blue
@@ -10,6 +10,7 @@ import encodes from '../bookmark-form/common/encodes'
10
10
  import { refs } from '../common/ref'
11
11
  import Qm from '../quick-commands/quick-commands-select'
12
12
  import AIIcon from '../icons/ai-icon'
13
+ import CmdHistory from './cmd-history'
13
14
 
14
15
  const {
15
16
  Option
@@ -131,7 +132,19 @@ export default auto(function FooterEntry (props) {
131
132
  )
132
133
  }
133
134
 
134
- const { leftSidebarWidth, openedSideBar, inActiveTerminal } = props.store
135
+ function renderCmdHistory () {
136
+ return (
137
+ <div className='terminal-footer-unit terminal-footer-history'>
138
+ <CmdHistory store={props.store} />
139
+ </div>
140
+ )
141
+ }
142
+
143
+ const {
144
+ leftSidebarWidth,
145
+ openedSideBar,
146
+ inActiveTerminal
147
+ } = props.store
135
148
  const w = 43 + leftSidebarWidth
136
149
  const sideProps = openedSideBar
137
150
  ? {
@@ -154,6 +167,7 @@ export default auto(function FooterEntry (props) {
154
167
  <div {...sideProps}>
155
168
  <div className='terminal-footer-flex'>
156
169
  {renderAIIcon()}
170
+ {renderCmdHistory()}
157
171
  {renderQuickCommands()}
158
172
  {renderBatchInputs()}
159
173
  {renderEncodingInfo()}
@@ -86,7 +86,6 @@ export default auto(function Index (props) {
86
86
  pinned,
87
87
  isSecondInstance,
88
88
  pinnedQuickCommandBar,
89
- wsInited,
90
89
  installSrc,
91
90
  fileTransfers,
92
91
  uiThemeConfig,
@@ -248,14 +247,14 @@ export default auto(function Index (props) {
248
247
  <ShortcutControl config={config} />
249
248
  <CssOverwrite
250
249
  {...confsCss}
251
- wsInited={wsInited}
250
+ configLoaded={configLoaded}
252
251
  />
253
252
  <Opacity opacity={config.opacity} />
254
253
  <TerminalInteractive />
255
254
  <UiTheme
256
255
  {...themeProps}
257
256
  />
258
- <CustomCss customCss={config.customCss} />
257
+ <CustomCss customCss={config.customCss} configLoaded={configLoaded} />
259
258
  <TextEditor />
260
259
  <UpdateCheck
261
260
  skipVersion={config.skipVersion}
@@ -152,9 +152,9 @@ export default function QuickCommandsFooterBox (props) {
152
152
  return null
153
153
  }
154
154
  const all = props.currentQuickCommands
155
- if (!all.length) {
156
- return renderNoCmd()
157
- }
155
+ // if (!all.length) {
156
+ // return renderNoCmd()
157
+ // }
158
158
  const keyword0 = keyword.toLowerCase()
159
159
  const filtered = filterArray(all, keyword0, label)
160
160
  const sorted = qmSortByFrequency
@@ -226,6 +226,9 @@ export default function QuickCommandsFooterBox (props) {
226
226
  </Flex>
227
227
  <div className={cls}>
228
228
  {sorted.map(renderItem)}
229
+ {
230
+ !sorted.length && renderNoCmd()
231
+ }
229
232
  </div>
230
233
  </div>
231
234
  </div>
@@ -6,8 +6,6 @@ import { PureComponent } from 'react'
6
6
  import { Button } from 'antd'
7
7
  import './qm.styl'
8
8
 
9
- const e = window.translate
10
-
11
9
  export default class QuickCommandsFooter extends PureComponent {
12
10
  componentWillUnmount () {
13
11
  clearTimeout(this.timer)
@@ -37,8 +35,7 @@ export default class QuickCommandsFooter extends PureComponent {
37
35
  size='small'
38
36
  type='text'
39
37
  >
40
- <span className='w500'>{e('quickCommands')}</span>
41
- <span className='l500'>Q</span>
38
+ Q
42
39
  </Button>
43
40
  </div>
44
41
  )
@@ -339,10 +339,29 @@ export default class RdpSession extends PureComponent {
339
339
  if (!this.session) return
340
340
  try {
341
341
  const rect = canvas.getBoundingClientRect()
342
- const scaleX = canvas.width / rect.width
343
- const scaleY = canvas.height / rect.height
344
- const x = Math.round((e.clientX - rect.left) * scaleX)
345
- const y = Math.round((e.clientY - rect.top) * scaleY)
342
+ const { scaleViewport } = this.state
343
+ let scaleX = canvas.width / rect.width
344
+ let scaleY = canvas.height / rect.height
345
+ let offsetX = 0
346
+ let offsetY = 0
347
+ if (scaleViewport) {
348
+ const containerRatio = rect.width / rect.height
349
+ const canvasRatio = canvas.width / canvas.height
350
+ let renderWidth, renderHeight
351
+ if (containerRatio > canvasRatio) {
352
+ renderHeight = rect.height
353
+ renderWidth = rect.height * canvasRatio
354
+ offsetX = (rect.width - renderWidth) / 2
355
+ } else {
356
+ renderWidth = rect.width
357
+ renderHeight = rect.width / canvasRatio
358
+ offsetY = (rect.height - renderHeight) / 2
359
+ }
360
+ scaleX = canvas.width / renderWidth
361
+ scaleY = canvas.height / renderHeight
362
+ }
363
+ const x = Math.round((e.clientX - rect.left - offsetX) * scaleX)
364
+ const y = Math.round((e.clientY - rect.top - offsetY) * scaleY)
346
365
  const event = window.ironRdp.DeviceEvent.mouseMove(x, y)
347
366
  const tx = new window.ironRdp.InputTransaction()
348
367
  tx.addEvent(event)
@@ -78,9 +78,7 @@
78
78
  border-radius 0
79
79
  &::-webkit-scrollbar-corner
80
80
  background var(--main-darker)
81
- .vnc-session-wrap > div
82
- display block !important
83
- overflow hidden !important
81
+
84
82
  .not-split-view > .ant-splitter-bar .ant-splitter-bar-dragger
85
83
  display none
86
84
 
@@ -254,3 +254,5 @@ export default function TerminalBackgroundConfig ({
254
254
  </div>
255
255
  )
256
256
  }
257
+
258
+ TerminalBackgroundConfig.displayName = 'TerminalBackgroundConfig'
@@ -3,15 +3,13 @@ import {
3
3
  Input,
4
4
  InputNumber,
5
5
  Space,
6
- Typography,
7
6
  Select,
8
- Button
7
+ Button,
8
+ Modal
9
9
  } from 'antd'
10
- import Modal from '../common/modal'
11
10
  import { ColorPicker } from '../bookmark-form/common/color-picker.jsx'
12
11
 
13
12
  const { TextArea } = Input
14
- const { Title } = Typography
15
13
  const e = window.translate
16
14
 
17
15
  export default function TextBgModal ({
@@ -68,9 +66,9 @@ export default function TextBgModal ({
68
66
  footer={footer}
69
67
  >
70
68
  <div className='pd1'>
71
- <Space direction='vertical' size='large' style={{ width: '100%' }}>
69
+ <Space orientation='vertical' size='large' className='width-100'>
72
70
  <div>
73
- <Title level={5}>{e('text')}</Title>
71
+ <b>{e('text')}</b>
74
72
  <TextArea
75
73
  value={text}
76
74
  onChange={(e) => setText(e.target.value)}
@@ -81,7 +79,7 @@ export default function TextBgModal ({
81
79
  </div>
82
80
 
83
81
  <div>
84
- <Title level={5}>{e('fontSize')}</Title>
82
+ <b>{e('fontSize')}</b>
85
83
  <InputNumber
86
84
  value={fontSize}
87
85
  onChange={setFontSize}
@@ -93,7 +91,7 @@ export default function TextBgModal ({
93
91
  </div>
94
92
 
95
93
  <div>
96
- <Title level={5}>{e('textColor')}</Title>
94
+ <b>{e('textColor')}</b>
97
95
  <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
98
96
  <ColorPicker
99
97
  value={color}
@@ -109,7 +107,7 @@ export default function TextBgModal ({
109
107
  </div>
110
108
 
111
109
  <div>
112
- <Title level={5}>{e('fontFamily')}</Title>
110
+ <b>{e('fontFamily')}</b>
113
111
  <Select
114
112
  value={fontFamily}
115
113
  onChange={setFontFamily}
@@ -140,3 +138,5 @@ export default function TextBgModal ({
140
138
  </Modal>
141
139
  )
142
140
  }
141
+
142
+ TextBgModal.displayName = 'TextBgModal'
@@ -651,6 +651,28 @@ export default class FileSection extends React.Component {
651
651
  this.watchFile(tempPath)
652
652
  }
653
653
 
654
+ editWithCustomEditor = async (text, editorCommand) => {
655
+ const {
656
+ path,
657
+ name,
658
+ type
659
+ } = this.state.file
660
+ let tempPath = ''
661
+ if (type === typeMap.local) {
662
+ tempPath = window.pre.resolve(path, name)
663
+ } else {
664
+ const id = generate()
665
+ tempPath = window.pre.resolve(
666
+ window.pre.tempDir, `electerm-temp-${id}-${name}`
667
+ )
668
+ await fs.writeFile(tempPath, text)
669
+ }
670
+ this.watchingFile = tempPath
671
+ window.pre.runGlobalAsync('watchFile', tempPath)
672
+ await window.pre.runGlobalAsync('openFileWithEditor', tempPath, editorCommand)
673
+ window.pre.ipcOnEvent('file-change', this.onFileChange)
674
+ }
675
+
654
676
  onFileChange = (e, text) => {
655
677
  this.editor.editWithSystemEditorDone({
656
678
  id: this.id,