@electerm/electerm-react 1.100.30 → 1.100.50

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.
@@ -24,7 +24,8 @@ export const maxEditFileSize = 1024 * 3000
24
24
  export const defaultBookmarkGroupId = 'default'
25
25
  export const newBookmarkIdPrefix = 'new-bookmark'
26
26
  export const unexpectedPacketErrorDesc = 'Unexpected packet'
27
- export const noTerminalBgValue = 'no-termimal-bg'
27
+ export const noTerminalBgValue = '[🚫]'
28
+ export const textTerminalBgValue = '[📝]'
28
29
  export const sftpRetryInterval = 3000
29
30
  export const maxBookmarkGroupTitleLength = 33
30
31
  export const termControlHeight = 32
@@ -24,6 +24,10 @@ export default {
24
24
  terminalBackgroundFilterBrightness: 1,
25
25
  terminalBackgroundFilterGrayscale: 0,
26
26
  terminalBackgroundFilterContrast: 1,
27
+ terminalBackgroundText: '',
28
+ terminalBackgroundTextSize: 48,
29
+ terminalBackgroundTextColor: '#ffffff',
30
+ terminalBackgroundTextFontFamily: 'Maple Mono',
27
31
  rendererType: 'canvas',
28
32
  terminalType: 'xterm-256color',
29
33
  keepaliveCountMax: 10,
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Common utilities for handling file drops
3
+ */
4
+
5
+ import { getFolderFromFilePath } from '../components/sftp/file-read'
6
+ import { typeMap } from './constants'
7
+
8
+ /**
9
+ * Safely get file path from dropped file
10
+ * @param {File} file - File object from drop event
11
+ * @returns {string} - File path
12
+ */
13
+ export const getFilePath = (file) => {
14
+ if (file.path) {
15
+ return file.path
16
+ }
17
+ // Try the official Electron 32+ method first if available
18
+ if (window.api && window.api.getPathForFile) {
19
+ return window.api.getPathForFile(file)
20
+ }
21
+ return file.name
22
+ }
23
+
24
+ /**
25
+ * Process dropped files and return file list
26
+ * @param {DataTransfer} dataTransfer - DataTransfer object from drop event
27
+ * @returns {Array} - Array of file objects
28
+ */
29
+ export const getDropFileList = (dataTransfer) => {
30
+ const fromFile = dataTransfer.getData('fromFile')
31
+ if (fromFile) {
32
+ return [JSON.parse(fromFile)]
33
+ }
34
+
35
+ const { files } = dataTransfer
36
+ const res = []
37
+ for (let i = 0, len = files.length; i < len; i++) {
38
+ const item = files[i]
39
+ if (!item) {
40
+ continue
41
+ }
42
+
43
+ const filePath = getFilePath(item)
44
+ const isRemote = false
45
+ const fileObj = getFolderFromFilePath(filePath, isRemote)
46
+ res.push({
47
+ ...fileObj,
48
+ type: typeMap.local
49
+ })
50
+ }
51
+ return res
52
+ }
53
+
54
+ /**
55
+ * Check if filename contains unsafe characters
56
+ * @param {string} filename - Filename to check
57
+ * @returns {boolean} - True if unsafe
58
+ */
59
+ export const isUnsafeFilename = (filename) => {
60
+ return /["'\n\r]/.test(filename)
61
+ }
@@ -10,7 +10,7 @@ export default auto(function AIChatHistory ({ history }) {
10
10
  if (historyRef.current) {
11
11
  historyRef.current.scrollTop = historyRef.current.scrollHeight
12
12
  }
13
- }, [history.length])
13
+ }, [history])
14
14
  if (!history.length) {
15
15
  return <div />
16
16
  }
@@ -39,42 +39,128 @@ export default function AIChat (props) {
39
39
  }
40
40
  if (!prompt.trim() || isLoading) return
41
41
  setIsLoading(true)
42
- const aiResponse = await window.pre.runGlobalAsync(
43
- 'AIchat',
44
- prompt,
45
- props.config.modelAI,
46
- buildRole(),
47
- props.config.baseURLAI,
48
- props.config.apiPathAI,
49
- props.config.apiKeyAI,
50
- props.config.proxyAI
51
- ).catch(
52
- window.store.onError
53
- )
54
- if (aiResponse && aiResponse.error) {
55
- return window.store.onError(
56
- new Error(aiResponse.error)
57
- )
58
- }
59
- window.store.aiChatHistory.push({
42
+
43
+ // Create a placeholder entry for the streaming response
44
+ const chatId = uid()
45
+ const chatEntry = {
60
46
  prompt,
61
- response: aiResponse.response,
47
+ response: '', // Will be updated as stream arrives
48
+ isStreaming: false,
49
+ sessionId: null,
62
50
  ...pick(props.config, [
63
51
  'modelAI',
64
52
  'roleAI',
65
53
  'baseURLAI'
66
54
  ]),
67
55
  timestamp: Date.now(),
68
- id: uid()
69
- })
56
+ id: chatId
57
+ }
58
+
59
+ window.store.aiChatHistory.push(chatEntry)
60
+
61
+ try {
62
+ const aiResponse = await window.pre.runGlobalAsync(
63
+ 'AIchat',
64
+ prompt,
65
+ props.config.modelAI,
66
+ buildRole(),
67
+ props.config.baseURLAI,
68
+ props.config.apiPathAI,
69
+ props.config.apiKeyAI,
70
+ props.config.proxyAI,
71
+ true // Enable streaming for chat
72
+ )
73
+
74
+ if (aiResponse && aiResponse.error) {
75
+ // Remove the placeholder entry and show error
76
+ const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
77
+ if (index !== -1) {
78
+ window.store.aiChatHistory.splice(index, 1)
79
+ }
80
+ setIsLoading(false)
81
+ return window.store.onError(
82
+ new Error(aiResponse.error)
83
+ )
84
+ }
85
+
86
+ if (aiResponse && aiResponse.isStream && aiResponse.sessionId) {
87
+ // Handle streaming response with polling
88
+ const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
89
+ if (index !== -1) {
90
+ window.store.aiChatHistory[index].isStreaming = true
91
+ window.store.aiChatHistory[index].sessionId = aiResponse.sessionId
92
+ window.store.aiChatHistory[index].response = aiResponse.content || ''
93
+ }
94
+
95
+ // Start polling for updates
96
+ pollStreamContent(aiResponse.sessionId, chatId)
97
+ } else if (aiResponse && aiResponse.response) {
98
+ // Handle non-streaming response (fallback)
99
+ const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
100
+ if (index !== -1) {
101
+ window.store.aiChatHistory[index].response = aiResponse.response
102
+ window.store.aiChatHistory[index].isStreaming = false
103
+ }
104
+ setIsLoading(false)
105
+ }
106
+ } catch (error) {
107
+ // Remove the placeholder entry and show error
108
+ const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
109
+ if (index !== -1) {
110
+ window.store.aiChatHistory.splice(index, 1)
111
+ }
112
+ setIsLoading(false)
113
+ window.store.onError(error)
114
+ }
70
115
 
71
116
  if (window.store.aiChatHistory.length > MAX_HISTORY) {
72
117
  window.store.aiChatHistory.splice(MAX_HISTORY)
73
118
  }
74
119
  setPrompt('')
75
- setIsLoading(false)
76
120
  }, [prompt, isLoading])
77
121
 
122
+ // Function to poll for streaming content updates
123
+ const pollStreamContent = async (sessionId, chatId) => {
124
+ try {
125
+ const streamResponse = await window.pre.runGlobalAsync('getStreamContent', sessionId)
126
+
127
+ if (streamResponse && streamResponse.error) {
128
+ // Remove the entry and show error
129
+ const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
130
+ if (index !== -1) {
131
+ window.store.aiChatHistory.splice(index, 1)
132
+ }
133
+ setIsLoading(false)
134
+ return window.store.onError(new Error(streamResponse.error))
135
+ }
136
+
137
+ // Update the chat entry with new content
138
+ const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
139
+ if (index !== -1) {
140
+ window.store.aiChatHistory[index].response = streamResponse.content || ''
141
+ window.store.aiChatHistory[index].isStreaming = streamResponse.hasMore
142
+
143
+ // Force re-render by updating the array reference
144
+ window.store.aiChatHistory = [...window.store.aiChatHistory]
145
+
146
+ // Continue polling if there's more content
147
+ if (streamResponse.hasMore) {
148
+ setTimeout(() => pollStreamContent(sessionId, chatId), 200) // Poll every 200ms
149
+ } else {
150
+ setIsLoading(false)
151
+ }
152
+ }
153
+ } catch (error) {
154
+ // Remove the entry and show error
155
+ const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
156
+ if (index !== -1) {
157
+ window.store.aiChatHistory.splice(index, 1)
158
+ }
159
+ setIsLoading(false)
160
+ window.store.onError(error)
161
+ }
162
+ }
163
+
78
164
  function renderHistory () {
79
165
  return (
80
166
  <AiChatHistory
@@ -27,8 +27,8 @@ const defaultRoles = [
27
27
  ]
28
28
 
29
29
  const proxyOptions = [
30
- { value: 'socks5://localhost:1080' },
31
- { value: 'http://localhost:8080' },
30
+ { value: 'socks5://127.0.0.1:1080' },
31
+ { value: 'http://127.0.0.1:8080' },
32
32
  { value: 'https://proxy.example.com:3128' }
33
33
  ]
34
34
 
@@ -180,7 +180,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
180
180
  <Form.Item
181
181
  label={e('proxy')}
182
182
  name='proxyAI'
183
- tooltip='Proxy for AI API requests (e.g., socks5://localhost:1080)'
183
+ tooltip='Proxy for AI API requests (e.g., socks5://127.0.0.1:1080)'
184
184
  >
185
185
  <AutoComplete
186
186
  options={proxyOptions}
@@ -40,22 +40,22 @@ export default function AIOutput ({ item }) {
40
40
 
41
41
  return (
42
42
  <div className='code-block'>
43
- <pre>
44
- <code className={className} {...rest}>
45
- {children}
46
- </code>
47
- </pre>
48
- <div className='code-block-actions'>
43
+ <div className='code-block-actions alignright'>
49
44
  <CopyOutlined
50
- className='code-action-icon pointer'
45
+ className='code-action-icon pointer iblock'
51
46
  onClick={copyToClipboard}
52
47
  title={e('copy')}
53
48
  />
54
49
  <PlayCircleOutlined
55
- className='code-action-icon pointer mg1l'
50
+ className='code-action-icon pointer mg1l iblock'
56
51
  onClick={runInTerminal}
57
52
  />
58
53
  </div>
54
+ <pre>
55
+ <code className={className} {...rest}>
56
+ {children}
57
+ </code>
58
+ </pre>
59
59
  </div>
60
60
  )
61
61
  }
@@ -22,11 +22,11 @@
22
22
  border-radius 3px
23
23
  pre
24
24
  margin-bottom 0
25
- .code-block-actions
26
- display none
27
- &:hover
28
- .code-block-actions
29
- display block
25
+ // .code-block-actions
26
+ // display block
27
+ // &:hover
28
+ // .code-block-actions
29
+ // display block
30
30
 
31
31
  .ai-chat-input
32
32
  position relative
@@ -63,4 +63,4 @@
63
63
  font-size 12px
64
64
  top 8px
65
65
  right -4px
66
- font-weight bold
66
+ font-weight bold
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { Component } from 'react'
5
5
  import fs from '../../common/fs'
6
- import { noTerminalBgValue } from '../../common/constants'
6
+ import { noTerminalBgValue, textTerminalBgValue } from '../../common/constants'
7
7
  import { generateMosaicBackground } from './shapes'
8
8
 
9
9
  export default class CssOverwrite extends Component {
@@ -20,7 +20,11 @@ export default class CssOverwrite extends Component {
20
20
  'terminalBackgroundFilterOpacity',
21
21
  'terminalBackgroundFilterBrightness',
22
22
  'terminalBackgroundFilterContrast',
23
- 'terminalBackgroundFilterGrayscale'
23
+ 'terminalBackgroundFilterGrayscale',
24
+ 'terminalBackgroundText',
25
+ 'terminalBackgroundTextSize',
26
+ 'terminalBackgroundTextColor',
27
+ 'terminalBackgroundTextFontFamily'
24
28
  ]
25
29
  const globalChanged = bgProps.some(prop => this.props[prop] !== nextProps[prop])
26
30
  if (globalChanged) {
@@ -59,7 +63,7 @@ export default class CssOverwrite extends Component {
59
63
  }
60
64
 
61
65
  // Common function to handle background image style creation
62
- createBackgroundStyle = async (imagePath) => {
66
+ createBackgroundStyle = async (imagePath, textBgProps = null) => {
63
67
  if (!imagePath || imagePath === '') {
64
68
  return ''
65
69
  }
@@ -73,6 +77,8 @@ export default class CssOverwrite extends Component {
73
77
  st = 'index'
74
78
  } else if (noTerminalBgValue === imagePath) {
75
79
  st = 'none'
80
+ } else if (textTerminalBgValue === imagePath) {
81
+ st = 'text'
76
82
  } else if (imagePath && !isWebImg) {
77
83
  content = await fs.readFileAsBase64(imagePath)
78
84
  .catch(log.error)
@@ -113,6 +119,27 @@ export default class CssOverwrite extends Component {
113
119
  const styles = []
114
120
  if (st === 'index') {
115
121
  styles.push(`content: '${tab.tabCount}'`)
122
+ } else if (st === 'text') {
123
+ const text = bg.terminalBackgroundText || this.props.terminalBackgroundText || ''
124
+ const size = bg.terminalBackgroundTextSize || this.props.terminalBackgroundTextSize || 48
125
+ const color = bg.terminalBackgroundTextColor || this.props.terminalBackgroundTextColor || '#ffffff'
126
+ const fontFamily = bg.terminalBackgroundTextFontFamily || this.props.terminalBackgroundTextFontFamily || 'monospace'
127
+ if (text) {
128
+ styles.push(
129
+ `content: '${text.replace(/'/g, "\\'").replace(/\n/g, '\\A ')}'`,
130
+ `font-size: ${size}px`,
131
+ `color: ${color}`,
132
+ 'white-space: pre-wrap',
133
+ 'word-wrap: break-word',
134
+ 'text-align: center',
135
+ 'display: flex',
136
+ 'align-items: center',
137
+ 'justify-content: center',
138
+ `font-family: ${fontFamily}`,
139
+ 'opacity: 0.3',
140
+ 'background-image: none' // Override default background when text is set
141
+ )
142
+ }
116
143
  } else if (st !== 'none') {
117
144
  styles.push(
118
145
  `background-image: ${st}`,
@@ -135,7 +162,28 @@ export default class CssOverwrite extends Component {
135
162
 
136
163
  const styles = []
137
164
 
138
- if (st !== 'none' && st !== 'index') {
165
+ if (st === 'text') {
166
+ const text = this.props.terminalBackgroundText || ''
167
+ const size = this.props.terminalBackgroundTextSize || 48
168
+ const color = this.props.terminalBackgroundTextColor || '#ffffff'
169
+ const fontFamily = this.props.terminalBackgroundTextFontFamily || 'monospace'
170
+ if (text) {
171
+ styles.push(
172
+ `content: '${text.replace(/'/g, "\\'").replace(/\n/g, '\\A ')}'`,
173
+ `font-size: ${size}px`,
174
+ `color: ${color}`,
175
+ 'white-space: pre-wrap',
176
+ 'word-wrap: break-word',
177
+ 'text-align: center',
178
+ 'display: flex',
179
+ 'align-items: center',
180
+ 'justify-content: center',
181
+ `font-family: ${fontFamily}`,
182
+ 'opacity: 0.3',
183
+ 'background-image: none' // Override default background when text is set
184
+ )
185
+ }
186
+ } else if (st !== 'none' && st !== 'index') {
139
187
  styles.push(
140
188
  `background-image: ${st}`,
141
189
  'background-position: center',
@@ -25,7 +25,7 @@ export default function renderProxy (props) {
25
25
  value: d
26
26
  }
27
27
  }),
28
- placeholder: 'socks5://localhost:1080',
28
+ placeholder: 'socks5://127.0.0.1:1080',
29
29
  allowClear: true
30
30
  }
31
31
  return (
@@ -29,9 +29,9 @@ export default function renderSshTunnels (props) {
29
29
  const [initialValues] = useState({
30
30
  sshTunnel: 'forwardRemoteToLocal',
31
31
  sshTunnelLocalPort: 12200,
32
- sshTunnelLocalHost: 'localhost',
32
+ sshTunnelLocalHost: '127.0.0.1',
33
33
  sshTunnelRemotePort: 12300,
34
- sshTunnelRemoteHost: 'localhost'
34
+ sshTunnelRemoteHost: '127.0.0.1'
35
35
  })
36
36
  const [isDynamic, setter] = useState(formData.sshTunnel === 'dynamicForward')
37
37
  const [list, setList] = useState(formData.sshTunnels || [])
@@ -85,9 +85,9 @@ export default function renderSshTunnels (props) {
85
85
  // sshTunnel is forwardRemoteToLocal or forwardLocalToRemote or dynamicForward
86
86
  const {
87
87
  sshTunnel,
88
- sshTunnelRemoteHost = 'localhost',
88
+ sshTunnelRemoteHost = '127.0.0.1',
89
89
  sshTunnelRemotePort = '',
90
- sshTunnelLocalHost = 'localhost',
90
+ sshTunnelLocalHost = '127.0.0.1',
91
91
  sshTunnelLocalPort = '',
92
92
  name
93
93
  } = item
@@ -80,7 +80,11 @@ export default function BookmarkFormUI (props) {
80
80
  'terminalBackgroundFilterBlur',
81
81
  'terminalBackgroundFilterBrightness',
82
82
  'terminalBackgroundFilterGrayscale',
83
- 'terminalBackgroundFilterContrast'
83
+ 'terminalBackgroundFilterContrast',
84
+ 'terminalBackgroundText',
85
+ 'terminalBackgroundTextSize',
86
+ 'terminalBackgroundTextColor',
87
+ 'terminalBackgroundTextFontFamily'
84
88
  ])
85
89
  }
86
90
  initialValues = defaultsDeep(initialValues, defaultValues)
@@ -18,7 +18,7 @@ import TerminalCmdSuggestions from '../terminal/terminal-command-dropdown'
18
18
  import TransportsActionStore from '../file-transfer/transports-action-store.jsx'
19
19
  import classnames from 'classnames'
20
20
  import ShortcutControl from '../shortcuts/shortcut-control.jsx'
21
- import { isMac, isWin } from '../../common/constants'
21
+ import { isMac, isWin, textTerminalBgValue } from '../../common/constants'
22
22
  import TermFullscreenControl from './term-fullscreen-control'
23
23
  import TerminalInfo from '../terminal-info/terminal-info'
24
24
  import { ConfigProvider, notification, message } from 'antd'
@@ -117,7 +117,9 @@ export default auto(function Index (props) {
117
117
  const ext1 = {
118
118
  className: cls
119
119
  }
120
- const bgTabs = config.terminalBackgroundImagePath === 'index' || config.terminalBackgroundImagePath === 'randomShape'
120
+ const bgTabs = config.terminalBackgroundImagePath === 'index' ||
121
+ config.terminalBackgroundImagePath === 'randomShape' ||
122
+ config.terminalBackgroundImagePath === textTerminalBgValue
121
123
  ? store.getTabs()
122
124
  : store.getTabs().filter(tab =>
123
125
  tab.terminalBackground?.terminalBackgroundImagePath
@@ -14,17 +14,28 @@
14
14
  position fixed
15
15
  z-index 100
16
16
  background rgba(45, 245, 108, 0.8)
17
- .term-wrap
18
- .session-v-wrap
17
+ // Hide all sessions first
18
+ .session-wrap
19
+ display none
20
+ // Only show the session that matches the fullscreen tab ID
21
+ .session-wrap.session-current
22
+ display block !important
19
23
  position fixed
20
24
  left 0 !important
21
25
  top 0 !important
22
26
  height 100% !important
23
27
  width 100% !important
24
- .term-wrap-1
25
- left 10px !important
26
- top 10px !important
27
- right 10px !important
28
- bottom 10px !important
28
+ .term-wrap
29
+ .session-v-wrap
30
+ position fixed
31
+ left 0 !important
32
+ top 0 !important
33
+ height 100% !important
34
+ width 100% !important
35
+ .term-wrap-1
36
+ left 10px !important
37
+ top 10px !important
38
+ right 10px !important
39
+ bottom 10px !important
29
40
  .term-fullscreen-control
30
41
  display none
@@ -465,7 +465,9 @@ export default class SessionWrapper extends Component {
465
465
  }
466
466
 
467
467
  handleFullscreen = () => {
468
- window.store.toggleTermFullscreen(true)
468
+ // Make this tab the active tab before fullscreening
469
+ window.store.activeTabId = this.props.tab.id
470
+ window.store.toggleTermFullscreen(true, this.props.tab.id)
469
471
  }
470
472
 
471
473
  toggleBroadcastInput = () => {
@@ -434,7 +434,7 @@ export default class SettingCommon extends Component {
434
434
  />
435
435
  </div>
436
436
  {
437
- this.renderText('proxy', 'socks5://localhost:1080')
437
+ this.renderText('proxy', 'socks5://127.0.0.1:1080')
438
438
  }
439
439
  </div>
440
440
  )
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import React, { useState } from 'react'
2
2
  import {
3
3
  AutoComplete,
4
4
  Upload,
@@ -6,10 +6,12 @@ import {
6
6
  Input
7
7
  } from 'antd'
8
8
  import {
9
- noTerminalBgValue
9
+ noTerminalBgValue,
10
+ textTerminalBgValue
10
11
  } from '../../common/constants'
11
12
  import defaultSettings from '../../common/default-setting'
12
13
  import NumberConfig from './number-config'
14
+ import TextBgModal from './text-bg-modal.jsx'
13
15
 
14
16
  const e = window.translate
15
17
 
@@ -19,6 +21,7 @@ export default function TerminalBackgroundConfig ({
19
21
  config,
20
22
  isGlobal = false
21
23
  }) {
24
+ const [showTextModal, setShowTextModal] = useState(false)
22
25
  const value = config[name]
23
26
  const defaultValue = defaultSettings[name]
24
27
  const onChange = (v) => onChangeValue(v, name)
@@ -33,6 +36,7 @@ export default function TerminalBackgroundConfig ({
33
36
  <span>{e('chooseFile')}</span>
34
37
  </Upload>
35
38
  )
39
+
36
40
  const dataSource = [
37
41
  {
38
42
  value: '',
@@ -41,8 +45,34 @@ export default function TerminalBackgroundConfig ({
41
45
  {
42
46
  value: noTerminalBgValue,
43
47
  desc: e('noTerminalBg')
48
+ },
49
+ {
50
+ value: textTerminalBgValue,
51
+ desc: `📝 ${e('textBackground')}`
44
52
  }
45
53
  ]
54
+
55
+ // Add custom text background option if text is configured
56
+ if (value === textTerminalBgValue && config.terminalBackgroundText) {
57
+ const text = config.terminalBackgroundText
58
+ // Clean up the text for display: remove line breaks, trim whitespace
59
+ const cleanText = text.replace(/\s+/g, ' ').trim()
60
+ // Create a more user-friendly truncation
61
+ const truncatedText = cleanText.length > 25
62
+ ? cleanText.substring(0, 25) + '...'
63
+ : cleanText
64
+ dataSource[2] = {
65
+ value: textTerminalBgValue,
66
+ desc: `📝 "${truncatedText}"`
67
+ }
68
+ } else if (value === textTerminalBgValue) {
69
+ // Show helpful text when text background is selected but no text is configured
70
+ dataSource[2] = {
71
+ value: textTerminalBgValue,
72
+ desc: `📝 ${e('clickToConfigureText') || 'Click to configure text'}`
73
+ }
74
+ }
75
+
46
76
  if (isGlobal) {
47
77
  dataSource.push(
48
78
  {
@@ -51,10 +81,45 @@ export default function TerminalBackgroundConfig ({
51
81
  },
52
82
  {
53
83
  value: 'randomShape',
54
- desc: e('randomShape')
84
+ desc: `🎨 ${e('randomShape')}`
55
85
  }
56
86
  )
57
87
  }
88
+
89
+ const handleTextBgClick = () => {
90
+ setShowTextModal(true)
91
+ }
92
+
93
+ const handleTextBgModalOk = (textConfig) => {
94
+ // Store text configuration in the config
95
+ onChangeValue(textConfig.text, 'terminalBackgroundText')
96
+ onChangeValue(textConfig.fontSize, 'terminalBackgroundTextSize')
97
+ onChangeValue(textConfig.color, 'terminalBackgroundTextColor')
98
+ onChangeValue(textConfig.fontFamily, 'terminalBackgroundTextFontFamily')
99
+ onChange(textTerminalBgValue)
100
+ setShowTextModal(false)
101
+ }
102
+
103
+ const handleTextBgModalCancel = () => {
104
+ setShowTextModal(false)
105
+ }
106
+
107
+ const handleAutocompleteSelect = (v) => {
108
+ if (v === textTerminalBgValue) {
109
+ handleTextBgClick()
110
+ } else {
111
+ onChange(v)
112
+ }
113
+ }
114
+
115
+ const handleAutocompleteChange = (v) => {
116
+ if (v === textTerminalBgValue) {
117
+ handleTextBgClick()
118
+ } else {
119
+ onChange(v)
120
+ }
121
+ }
122
+
58
123
  const numberOpts = { step: 0.05, min: 0, max: 1, cls: 'bg-img-setting' }
59
124
 
60
125
  function renderNumber (name, options, title = '', width = 136) {
@@ -88,7 +153,7 @@ export default function TerminalBackgroundConfig ({
88
153
  }
89
154
 
90
155
  const renderFilter = () => {
91
- if (config[name] === noTerminalBgValue || config[name] === 'index') return
156
+ if (config[name] === noTerminalBgValue || config[name] === 'index' || config[name] === textTerminalBgValue) return
92
157
 
93
158
  return (
94
159
  <div>
@@ -137,6 +202,7 @@ export default function TerminalBackgroundConfig ({
137
202
  label: item.desc
138
203
  }
139
204
  }
205
+
140
206
  return (
141
207
  <div className='pd2b'>
142
208
  <div className='pd1b'>
@@ -145,7 +211,8 @@ export default function TerminalBackgroundConfig ({
145
211
  >
146
212
  <AutoComplete
147
213
  value={value}
148
- onChange={onChange}
214
+ onChange={handleAutocompleteChange}
215
+ onSelect={handleAutocompleteSelect}
149
216
  placeholder={defaultValue}
150
217
  className='width-100'
151
218
  options={dataSource.map(renderBgOption)}
@@ -160,6 +227,16 @@ export default function TerminalBackgroundConfig ({
160
227
  {
161
228
  renderFilter()
162
229
  }
230
+
231
+ <TextBgModal
232
+ visible={showTextModal}
233
+ onOk={handleTextBgModalOk}
234
+ onCancel={handleTextBgModalCancel}
235
+ initialText={config.terminalBackgroundText || ''}
236
+ initialSize={config.terminalBackgroundTextSize || 48}
237
+ initialColor={config.terminalBackgroundTextColor || '#ffffff'}
238
+ initialFontFamily={config.terminalBackgroundTextFontFamily || 'monospace'}
239
+ />
163
240
  </div>
164
241
  )
165
242
  }
@@ -0,0 +1,131 @@
1
+ import React, { useState } from 'react'
2
+ import {
3
+ Modal,
4
+ Input,
5
+ InputNumber,
6
+ Space,
7
+ Typography,
8
+ Select
9
+ } from 'antd'
10
+ import { ColorPicker } from '../bookmark-form/color-picker.jsx'
11
+
12
+ const { TextArea } = Input
13
+ const { Title } = Typography
14
+ const e = window.translate
15
+
16
+ export default function TextBgModal ({
17
+ visible,
18
+ onOk,
19
+ onCancel,
20
+ initialText = '',
21
+ initialSize = 48,
22
+ initialColor = '#ffffff',
23
+ initialFontFamily = 'Maple Mono'
24
+ }) {
25
+ const [text, setText] = useState(initialText)
26
+ const [fontSize, setFontSize] = useState(initialSize)
27
+ const [color, setColor] = useState(initialColor)
28
+ const [fontFamily, setFontFamily] = useState(initialFontFamily)
29
+
30
+ const { fonts = [] } = window.et || {}
31
+
32
+ const handleOk = () => {
33
+ onOk({
34
+ text,
35
+ fontSize,
36
+ color,
37
+ fontFamily
38
+ })
39
+ }
40
+
41
+ const handleCancel = () => {
42
+ onCancel()
43
+ // Reset to initial values
44
+ setText(initialText)
45
+ setFontSize(initialSize)
46
+ setColor(initialColor)
47
+ setFontFamily(initialFontFamily)
48
+ }
49
+
50
+ return (
51
+ <Modal
52
+ title={e('terminalBackgroundText')}
53
+ open={visible}
54
+ onOk={handleOk}
55
+ onCancel={handleCancel}
56
+ width={500}
57
+ destroyOnClose
58
+ >
59
+ <div className='pd1'>
60
+ <Space direction='vertical' size='large' style={{ width: '100%' }}>
61
+ <div>
62
+ <Title level={5}>{e('text')}</Title>
63
+ <TextArea
64
+ value={text}
65
+ onChange={(e) => setText(e.target.value)}
66
+ placeholder={e('enterTextForBackground')}
67
+ rows={4}
68
+ maxLength={500}
69
+ />
70
+ </div>
71
+
72
+ <div>
73
+ <Title level={5}>{e('fontSize')}</Title>
74
+ <InputNumber
75
+ value={fontSize}
76
+ onChange={setFontSize}
77
+ min={12}
78
+ max={200}
79
+ style={{ width: '100%' }}
80
+ placeholder={e('fontSize')}
81
+ />
82
+ </div>
83
+
84
+ <div>
85
+ <Title level={5}>{e('textColor')}</Title>
86
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
87
+ <ColorPicker
88
+ value={color}
89
+ onChange={setColor}
90
+ />
91
+ <Input
92
+ value={color}
93
+ onChange={(e) => setColor(e.target.value)}
94
+ placeholder={e('colorValue')}
95
+ style={{ flex: 1 }}
96
+ />
97
+ </div>
98
+ </div>
99
+
100
+ <div>
101
+ <Title level={5}>{e('fontFamily')}</Title>
102
+ <Select
103
+ value={fontFamily}
104
+ onChange={setFontFamily}
105
+ style={{ width: '100%' }}
106
+ placeholder={e('selectFontFamily')}
107
+ showSearch
108
+ >
109
+ {
110
+ fonts.map(f => {
111
+ return (
112
+ <Select.Option value={f} key={f}>
113
+ <span
114
+ className='font-option'
115
+ style={{
116
+ fontFamily: f
117
+ }}
118
+ >
119
+ {f}
120
+ </span>
121
+ </Select.Option>
122
+ )
123
+ })
124
+ }
125
+ </Select>
126
+ </div>
127
+ </Space>
128
+ </div>
129
+ </Modal>
130
+ )
131
+ }
@@ -30,6 +30,7 @@ import findParent from '../../common/find-parent'
30
30
  import sorter from '../../common/index-sorter'
31
31
  import { getFolderFromFilePath, getLocalFileInfo } from './file-read'
32
32
  import { readClipboard, copy as copyToClipboard, hasFileInClipboardText } from '../../common/clipboard'
33
+ import { getDropFileList } from '../../common/file-drop-utils'
33
34
  import fs from '../../common/fs'
34
35
  import time from '../../common/time'
35
36
  import { filesize } from 'filesize'
@@ -228,26 +229,7 @@ export default class FileSection extends React.Component {
228
229
  }
229
230
 
230
231
  getDropFileList = data => {
231
- const fromFile = data.getData('fromFile')
232
- if (fromFile) {
233
- return [JSON.parse(fromFile)]
234
- }
235
- const { files } = data
236
- const res = []
237
- for (let i = 0, len = files.length; i < len; i++) {
238
- const item = files[i]
239
- if (!item) {
240
- continue
241
- }
242
- // let file = item.getAsFile()
243
- const isRemote = false
244
- const fileObj = getFolderFromFilePath(item.path, isRemote)
245
- res.push({
246
- ...fileObj,
247
- type: typeMap.local
248
- })
249
- }
250
- return res
232
+ return getDropFileList(data)
251
233
  }
252
234
 
253
235
  onDrop = async e => {
@@ -261,6 +243,7 @@ export default class FileSection extends React.Component {
261
243
  if (!fromFiles) {
262
244
  return
263
245
  }
246
+
264
247
  while (!target.className.includes(fileItemCls)) {
265
248
  target = target.parentNode
266
249
  }
@@ -1161,12 +1161,12 @@ export default class Sftp extends Component {
1161
1161
  {
1162
1162
  type === typeMap.remote
1163
1163
  ? (
1164
- <div className='pd1t pd1b pd1x alignright'>
1164
+ <div className='sftp-panel-title pd1t pd1b pd1x alignright'>
1165
1165
  {e('remote')}: {username}@{host}
1166
1166
  </div>
1167
1167
  )
1168
1168
  : (
1169
- <div className='pd1t pd1b pd1x'>
1169
+ <div className='sftp-panel-title pd1t pd1b pd1x'>
1170
1170
  {e('local')}
1171
1171
  </div>
1172
1172
  )
@@ -350,10 +350,10 @@ class Tab extends Component {
350
350
  const list = sshTunnelResults.map(({ sshTunnel: obj, error }, i) => {
351
351
  const {
352
352
  sshTunnelLocalPort,
353
- sshTunnelRemoteHost = 'localhost',
353
+ sshTunnelRemoteHost = '127.0.0.1',
354
354
  sshTunnelRemotePort,
355
355
  sshTunnel,
356
- sshTunnelLocalHost = 'localhost',
356
+ sshTunnelLocalHost = '127.0.0.1',
357
357
  name
358
358
  } = obj
359
359
  let tunnel
@@ -47,6 +47,7 @@ import { createTerm, resizeTerm } from './terminal-apis.js'
47
47
  import { shortcutExtend, shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
48
48
  import { KeywordHighlighterAddon } from './highlight-addon.js'
49
49
  import { getLocalFileInfo } from '../sftp/file-read.js'
50
+ import { getFilePath, isUnsafeFilename } from '../../common/file-drop-utils.js'
50
51
  import { CommandTrackerAddon } from './command-tracker-addon.js'
51
52
  import AIIcon from '../icons/ai-icon.jsx'
52
53
  import { formatBytes } from '../../common/byte-format.js'
@@ -356,12 +357,8 @@ class Term extends Component {
356
357
  this.term.focus()
357
358
  }
358
359
 
359
- isUnsafeFilename = (filename) => {
360
- return /["'\n\r]/.test(filename)
361
- }
362
-
363
360
  cd = (p) => {
364
- if (this.isUnsafeFilename(p)) {
361
+ if (isUnsafeFilename(p)) {
365
362
  return message.error('File name contains unsafe characters')
366
363
  }
367
364
  this.runQuickCommand(`cd "${p}"`)
@@ -377,7 +374,7 @@ class Term extends Component {
377
374
  try {
378
375
  const fileData = JSON.parse(fromFile)
379
376
  const filePath = resolve(fileData.path, fileData.name)
380
- if (this.isUnsafeFilename(filePath)) {
377
+ if (isUnsafeFilename(filePath)) {
381
378
  message.error(notSafeMsg)
382
379
  return
383
380
  }
@@ -392,13 +389,16 @@ class Term extends Component {
392
389
  const files = dt.files
393
390
  if (files && files.length) {
394
391
  const arr = Array.from(files)
392
+ const filePaths = arr.map(f => getFilePath(f))
393
+
395
394
  // Check each file path individually
396
- const hasUnsafeFilename = arr.some(f => this.isUnsafeFilename(f.path))
395
+ const hasUnsafeFilename = filePaths.some(path => isUnsafeFilename(path))
397
396
  if (hasUnsafeFilename) {
398
397
  message.error(notSafeMsg)
399
398
  return
400
399
  }
401
- const filesAll = arr.map(f => `"${f.path}"`).join(' ')
400
+
401
+ const filesAll = filePaths.map(path => `"${path}"`).join(' ')
402
402
  this.attachAddon._sendData(filesAll)
403
403
  }
404
404
  }
@@ -354,7 +354,7 @@ export default class ItemListTree extends Component {
354
354
  showNewBookmarkGroupForm: true,
355
355
  parentId: item.id,
356
356
  bookmarkGroupTitle: '',
357
- bookmarkGroupColor: ''
357
+ bookmarkGroupColor: getRandomDefaultColor()
358
358
  }
359
359
  })
360
360
  window.store.expandedKeys.push(item.id)
@@ -417,7 +417,6 @@ export default class ItemListTree extends Component {
417
417
  if (tar) {
418
418
  target = tar
419
419
  }
420
- console.log('tar', target, tar)
421
420
  const dataDragged = e.dataTransfer.getData('idDragged')
422
421
  const [idDragged, pidDrags, isGroupDragged] = dataDragged.split('@')
423
422
  const isGroupDrag = isGroupDragged === 'true'
@@ -28,6 +28,7 @@ import { buildDefaultThemes } from '../common/terminal-theme'
28
28
  import * as ls from '../common/safe-local-storage'
29
29
  import { exclude } from 'manate'
30
30
  import initSettingItem from '../common/init-setting-item'
31
+ import { getRandomDefaultColor } from '../common/rand-hex-color'
31
32
 
32
33
  const e = window.translate
33
34
 
@@ -36,7 +37,8 @@ function getDefaultBookmarkGroups (bookmarks) {
36
37
  JSON.stringify({
37
38
  title: e(defaultBookmarkGroupId),
38
39
  id: defaultBookmarkGroupId,
39
- bookmarkIds: bookmarks.map(d => d.id)
40
+ bookmarkIds: bookmarks.map(d => d.id),
41
+ color: getRandomDefaultColor()
40
42
  })
41
43
  ]
42
44
  }
@@ -515,7 +515,11 @@ export default (Store) => {
515
515
  'roleAI',
516
516
  'languageAI',
517
517
  'proxyAI',
518
- 'disableDeveloperTool'
518
+ 'disableDeveloperTool',
519
+ 'terminalBackgroundText',
520
+ 'terminalBackgroundTextSize',
521
+ 'terminalBackgroundTextColor',
522
+ 'terminalBackgroundTextFontFamily'
519
523
  ]
520
524
  return pick(store.config, configSyncKeys)
521
525
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "1.100.30",
3
+ "version": "1.100.50",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",