@electerm/electerm-react 2.15.8 → 2.16.6

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 (45) 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-select.jsx +1 -4
  20. package/client/components/rdp/rdp-session.jsx +23 -4
  21. package/client/components/session/session.styl +1 -3
  22. package/client/components/setting-panel/terminal-bg-config.jsx +2 -0
  23. package/client/components/setting-panel/text-bg-modal.jsx +9 -9
  24. package/client/components/sftp/file-item.jsx +22 -0
  25. package/client/components/sidebar/history-item.jsx +6 -3
  26. package/client/components/sidebar/history.jsx +48 -5
  27. package/client/components/sidebar/sidebar-panel.jsx +0 -13
  28. package/client/components/sidebar/sidebar.styl +19 -0
  29. package/client/components/tabs/add-btn-menu.jsx +28 -4
  30. package/client/components/tabs/add-btn.jsx +1 -1
  31. package/client/components/tabs/add-btn.styl +8 -0
  32. package/client/components/terminal/terminal.jsx +28 -11
  33. package/client/components/terminal/transfer-client-base.js +18 -2
  34. package/client/components/terminal/trzsz-client.js +2 -1
  35. package/client/components/terminal/zmodem-client.js +2 -1
  36. package/client/components/text-editor/edit-with-custom-editor.jsx +49 -0
  37. package/client/components/text-editor/text-editor-form.jsx +13 -5
  38. package/client/components/text-editor/text-editor.jsx +20 -1
  39. package/client/components/vnc/vnc-session.jsx +3 -0
  40. package/client/components/vnc/vnc.styl +1 -1
  41. package/client/store/common.js +31 -4
  42. package/client/store/init-state.js +26 -1
  43. package/client/store/store.js +1 -1
  44. package/client/store/watch.js +8 -1
  45. package/package.json +1 -1
@@ -6,7 +6,6 @@ import uid from '../../common/uid'
6
6
  import { pick } from 'lodash-es'
7
7
  import {
8
8
  SettingOutlined,
9
- LoadingOutlined,
10
9
  SendOutlined,
11
10
  UnorderedListOutlined
12
11
  } from '@ant-design/icons'
@@ -15,6 +14,7 @@ import {
15
14
  } from '../../common/constants'
16
15
  import HelpIcon from '../common/help-icon'
17
16
  import { refsStatic } from '../common/ref'
17
+ import AIStopIcon from './ai-stop-icon'
18
18
  import './ai.styl'
19
19
 
20
20
  const { TextArea } = Input
@@ -23,6 +23,7 @@ const MAX_HISTORY = 100
23
23
  export default function AIChat (props) {
24
24
  const [prompt, setPrompt] = useState('')
25
25
  const [isLoading, setIsLoading] = useState(false)
26
+ const [currentSessionId, setCurrentSessionId] = useState(null)
26
27
 
27
28
  function handlePromptChange (e) {
28
29
  setPrompt(e.target.value)
@@ -91,6 +92,8 @@ export default function AIChat (props) {
91
92
  window.store.aiChatHistory[index].sessionId = aiResponse.sessionId
92
93
  window.store.aiChatHistory[index].response = aiResponse.content || ''
93
94
  }
95
+ // Store current session ID for stop functionality
96
+ setCurrentSessionId(aiResponse.sessionId)
94
97
 
95
98
  // Start polling for updates
96
99
  pollStreamContent(aiResponse.sessionId, chatId)
@@ -125,6 +128,12 @@ export default function AIChat (props) {
125
128
  const streamResponse = await window.pre.runGlobalAsync('getStreamContent', sessionId)
126
129
 
127
130
  if (streamResponse && streamResponse.error) {
131
+ // Session not found or error - stop polling
132
+ if (streamResponse.error === 'Session not found') {
133
+ setCurrentSessionId(null)
134
+ setIsLoading(false)
135
+ return
136
+ }
128
137
  // Remove the entry and show error
129
138
  const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
130
139
  if (index !== -1) {
@@ -147,6 +156,7 @@ export default function AIChat (props) {
147
156
  if (streamResponse.hasMore) {
148
157
  setTimeout(() => pollStreamContent(sessionId, chatId), 200) // Poll every 200ms
149
158
  } else {
159
+ setCurrentSessionId(null)
150
160
  setIsLoading(false)
151
161
  }
152
162
  }
@@ -156,6 +166,7 @@ export default function AIChat (props) {
156
166
  if (index !== -1) {
157
167
  window.store.aiChatHistory.splice(index, 1)
158
168
  }
169
+ setCurrentSessionId(null)
159
170
  setIsLoading(false)
160
171
  window.store.onError(error)
161
172
  }
@@ -177,9 +188,40 @@ export default function AIChat (props) {
177
188
  window.store.aiChatHistory = []
178
189
  }
179
190
 
191
+ const handleStop = useCallback(async function () {
192
+ if (!currentSessionId || !isLoading) return
193
+
194
+ try {
195
+ // Call server to stop the stream
196
+ await window.pre.runGlobalAsync('stopStream', currentSessionId)
197
+
198
+ // Reset state
199
+ setCurrentSessionId(null)
200
+ setIsLoading(false)
201
+
202
+ // Update the chat entry to mark as stopped
203
+ const chatEntries = window.store.aiChatHistory
204
+ for (let i = chatEntries.length - 1; i >= 0; i--) {
205
+ if (chatEntries[i].isStreaming) {
206
+ chatEntries[i].isStreaming = false
207
+ break
208
+ }
209
+ }
210
+ window.store.aiChatHistory = [...chatEntries]
211
+ } catch (error) {
212
+ console.error('Error stopping stream:', error)
213
+ setCurrentSessionId(null)
214
+ setIsLoading(false)
215
+ }
216
+ }, [currentSessionId, isLoading])
217
+
180
218
  function renderSendIcon () {
181
219
  if (isLoading) {
182
- return <LoadingOutlined />
220
+ return (
221
+ <AIStopIcon
222
+ onClick={handleStop}
223
+ />
224
+ )
183
225
  }
184
226
  return (
185
227
  <SendOutlined
@@ -0,0 +1,13 @@
1
+ import { LoadingOutlined } from '@ant-design/icons'
2
+
3
+ export default function AIStopIcon (props) {
4
+ return (
5
+ <div
6
+ className='ai-stop-icon-square mg1l pointer'
7
+ onClick={props.onClick}
8
+ title={props.title || 'Stop AI request'}
9
+ >
10
+ <LoadingOutlined spin />
11
+ </div>
12
+ )
13
+ }
@@ -62,3 +62,13 @@
62
62
  top 8px
63
63
  right -4px
64
64
  font-weight bold
65
+
66
+ .ai-stop-icon-square
67
+ width 20px
68
+ height 20px
69
+ background var(--error)
70
+ border-radius 2px
71
+ display flex
72
+ align-items center
73
+ justify-content center
74
+ font-size 10px
@@ -1,100 +1,46 @@
1
1
  /**
2
2
  * btns
3
3
  */
4
- import { Component } from 'react'
4
+ import { useEffect, useRef } from 'react'
5
5
  import fs from '../../common/fs'
6
6
  import { noTerminalBgValue, textTerminalBgValue } from '../../common/constants'
7
7
  import { generateMosaicBackground } from './shapes'
8
8
 
9
- export default class CssOverwrite extends Component {
10
- static styleTag = null
9
+ const themeDomId = 'css-overwrite-terminal-backgrounds'
11
10
 
12
- shouldComponentUpdate (nextProps) {
13
- if (!this.props.wsInited && nextProps.wsInited) {
14
- return true
15
- }
16
-
17
- const bgProps = [
18
- 'terminalBackgroundImagePath',
19
- 'terminalBackgroundFilterBlur',
20
- 'terminalBackgroundFilterOpacity',
21
- 'terminalBackgroundFilterBrightness',
22
- 'terminalBackgroundFilterContrast',
23
- 'terminalBackgroundFilterGrayscale',
24
- 'terminalBackgroundText',
25
- 'terminalBackgroundTextSize',
26
- 'terminalBackgroundTextColor',
27
- 'terminalBackgroundTextFontFamily'
28
- ]
29
- const globalChanged = bgProps.some(prop => this.props[prop] !== nextProps[prop])
30
- if (globalChanged) {
31
- return true
32
- }
33
-
34
- const currentTabs = this.props.tabs || []
35
- const nextTabs = nextProps.tabs || []
36
- if (currentTabs.length !== nextTabs.length) {
37
- return true
38
- }
39
-
40
- // If no tabs in both cases
41
- if (!currentTabs.length && !nextTabs.length) {
42
- return false
43
- }
44
-
45
- // Since tab bg settings never change, we only need to compare tab IDs
46
- const currentIds = new Set(currentTabs.map(t => t.id))
47
- const nextIds = new Set(nextTabs.map(t => t.id))
48
-
49
- // Check if all current IDs exist in next IDs
50
- for (const id of currentIds) {
51
- if (!nextIds.has(id)) return true
52
- }
53
-
54
- return false
11
+ function createBackgroundStyle (imagePath) {
12
+ if (!imagePath || imagePath === '') {
13
+ return ''
55
14
  }
56
15
 
57
- componentDidUpdate (prevProps) {
58
- if (!prevProps.wsInited && this.props.wsInited) {
59
- setTimeout(this.writeCss, 1500)
60
- return
61
- }
62
- setTimeout(this.updateCss, 1000)
63
- }
64
-
65
- // Common function to handle background image style creation
66
- createBackgroundStyle = async (imagePath, textBgProps = null) => {
67
- if (!imagePath || imagePath === '') {
68
- return ''
69
- }
70
-
71
- let content = ''
72
- let st = ''
73
- const isWebImg = /^https?:\/\//.test(imagePath)
74
- if (imagePath === 'randomShape') {
75
- st = `url(${generateMosaicBackground()})`
76
- } else if (imagePath === 'index') {
77
- st = 'index'
78
- } else if (noTerminalBgValue === imagePath) {
79
- st = 'none'
80
- } else if (textTerminalBgValue === imagePath) {
81
- st = 'text'
82
- } else if (imagePath && !isWebImg) {
83
- content = await fs.readFileAsBase64(imagePath)
84
- .catch(console.error)
85
- if (content) {
86
- st = `url(data:image;base64,${content})`
87
- }
88
- } else if (imagePath && isWebImg) {
89
- st = `url(${imagePath})`
90
- }
91
- return st
16
+ let st = ''
17
+ const isWebImg = /^https?:\/\//.test(imagePath)
18
+ if (imagePath === 'randomShape') {
19
+ st = `url(${generateMosaicBackground()})`
20
+ } else if (imagePath === 'index') {
21
+ st = 'index'
22
+ } else if (noTerminalBgValue === imagePath) {
23
+ st = 'none'
24
+ } else if (textTerminalBgValue === imagePath) {
25
+ st = 'text'
26
+ } else if (imagePath && !isWebImg) {
27
+ return fs.readFileAsBase64(imagePath)
28
+ .then(content => {
29
+ if (content) {
30
+ return `url(data:image;base64,${content})`
31
+ }
32
+ return ''
33
+ })
34
+ .catch(() => '')
35
+ } else if (imagePath && isWebImg) {
36
+ st = `url(${imagePath})`
92
37
  }
38
+ return st
39
+ }
93
40
 
94
- // Common function to create filter styles
95
- createFilterStyle = (props, tabProps = null) => {
96
- return `blur(${
97
- (tabProps?.terminalBackgroundFilterBlur || props.terminalBackgroundFilterBlur)
41
+ function createFilterStyle (props, tabProps = null) {
42
+ return `blur(${
43
+ (tabProps?.terminalBackgroundFilterBlur || props.terminalBackgroundFilterBlur)
98
44
  }px) opacity(${
99
45
  +(tabProps?.terminalBackgroundFilterOpacity || props.terminalBackgroundFilterOpacity)
100
46
  }) brightness(${
@@ -104,128 +50,153 @@ export default class CssOverwrite extends Component {
104
50
  }) grayscale(${
105
51
  +(tabProps?.terminalBackgroundFilterGrayscale || props.terminalBackgroundFilterGrayscale)
106
52
  })`
107
- }
53
+ }
108
54
 
109
- createStyleForTab = async (tab) => {
110
- const bg = tab.terminalBackground || {}
111
- const img = bg.terminalBackgroundImagePath || this.props.terminalBackgroundImagePath
112
- const st = await this.createBackgroundStyle(img)
55
+ async function createStyleForTab (tab, props) {
56
+ const bg = tab.terminalBackground || {}
57
+ const img = bg.terminalBackgroundImagePath || props.terminalBackgroundImagePath
58
+ const st = await createBackgroundStyle(img)
113
59
 
114
- if (!st) {
115
- return ''
116
- }
60
+ if (!st) {
61
+ return ''
62
+ }
117
63
 
118
- const selector = `#container .sessions .session-${tab.id} .xterm-screen::before`
119
- const styles = []
120
- if (st === 'index') {
121
- styles.push(
122
- `content: '${tab.tabCount}'`,
123
- 'background-image: none',
124
- 'opacity: 0.1'
125
- )
126
- } else if (st === 'text') {
127
- const text = bg.terminalBackgroundText || this.props.terminalBackgroundText || ''
128
- const size = bg.terminalBackgroundTextSize || this.props.terminalBackgroundTextSize || 48
129
- const color = bg.terminalBackgroundTextColor || this.props.terminalBackgroundTextColor || '#ffffff'
130
- const fontFamily = bg.terminalBackgroundTextFontFamily || this.props.terminalBackgroundTextFontFamily || 'monospace'
131
- if (text) {
132
- styles.push(
133
- `content: '${text.replace(/'/g, "\\'").replace(/\n/g, '\\A ')}'`,
134
- `font-size: ${size}px`,
135
- `color: ${color}`,
136
- 'white-space: pre-wrap',
137
- 'word-wrap: break-word',
138
- 'text-align: center',
139
- 'display: flex',
140
- 'align-items: center',
141
- 'justify-content: center',
142
- `font-family: ${fontFamily}`,
143
- 'opacity: 0.3',
144
- 'background-image: none' // Override default background when text is set
145
- )
146
- }
147
- } else if (st !== 'none') {
64
+ const selector = `#container .sessions .session-${tab.id} .xterm-screen::before`
65
+ const styles = []
66
+ if (st === 'index') {
67
+ styles.push(
68
+ `content: '${tab.tabCount}'`,
69
+ 'background-image: none',
70
+ 'opacity: 0.1'
71
+ )
72
+ } else if (st === 'text') {
73
+ const text = bg.terminalBackgroundText || props.terminalBackgroundText || ''
74
+ const size = bg.terminalBackgroundTextSize || props.terminalBackgroundTextSize || 48
75
+ const color = bg.terminalBackgroundTextColor || props.terminalBackgroundTextColor || '#ffffff'
76
+ const fontFamily = bg.terminalBackgroundTextFontFamily || props.terminalBackgroundTextFontFamily || 'monospace'
77
+ if (text) {
148
78
  styles.push(
149
- `background-image: ${st}`,
150
- 'background-position: center',
151
- `filter: ${this.createFilterStyle(this.props, tab)}`
79
+ `content: '${text.replace(/'/g, "\\'").replace(/\n/g, '\\A ')}'`,
80
+ `font-size: ${size}px`,
81
+ `color: ${color}`,
82
+ 'white-space: pre-wrap',
83
+ 'word-wrap: break-word',
84
+ 'text-align: center',
85
+ 'display: flex',
86
+ 'align-items: center',
87
+ 'justify-content: center',
88
+ `font-family: ${fontFamily}`,
89
+ 'opacity: 0.3',
90
+ 'background-image: none'
152
91
  )
153
92
  }
154
- return `${selector} {
93
+ } else if (st !== 'none') {
94
+ styles.push(
95
+ `background-image: ${st}`,
96
+ 'background-position: center',
97
+ `filter: ${createFilterStyle(props, tab)}`
98
+ )
99
+ }
100
+ return `${selector} {
155
101
  ${styles.join(';')};
156
102
  }`
103
+ }
104
+
105
+ async function createGlobalStyle (props) {
106
+ const st = await createBackgroundStyle(props.terminalBackgroundImagePath)
107
+ if (!st) {
108
+ return '#container .session-batch-active .xterm-screen::before {' +
109
+ 'background-image: url("./images/electerm-watermark.png");' +
110
+ '}'
157
111
  }
158
112
 
159
- createGlobalStyle = async () => {
160
- const st = await this.createBackgroundStyle(this.props.terminalBackgroundImagePath)
161
- if (!st) {
162
- return '#container .session-batch-active .xterm-screen::before {' +
163
- 'background-image: url("./images/electerm-watermark.png");' +
164
- '}'
165
- }
113
+ const styles = []
166
114
 
167
- const styles = []
168
-
169
- if (st === 'text') {
170
- const text = this.props.terminalBackgroundText || ''
171
- const size = this.props.terminalBackgroundTextSize || 48
172
- const color = this.props.terminalBackgroundTextColor || '#ffffff'
173
- const fontFamily = this.props.terminalBackgroundTextFontFamily || 'monospace'
174
- if (text) {
175
- styles.push(
176
- `content: '${text.replace(/'/g, "\\'").replace(/\n/g, '\\A ')}'`,
177
- `font-size: ${size}px`,
178
- `color: ${color}`,
179
- 'white-space: pre-wrap',
180
- 'word-wrap: break-word',
181
- 'text-align: center',
182
- 'display: flex',
183
- 'align-items: center',
184
- 'justify-content: center',
185
- `font-family: ${fontFamily}`,
186
- 'opacity: 0.3',
187
- 'background-image: none' // Override default background when text is set
188
- )
189
- }
190
- } else if (st !== 'none' && st !== 'index') {
115
+ if (st === 'text') {
116
+ const text = props.terminalBackgroundText || ''
117
+ const size = props.terminalBackgroundTextSize || 48
118
+ const color = props.terminalBackgroundTextColor || '#ffffff'
119
+ const fontFamily = props.terminalBackgroundTextFontFamily || 'monospace'
120
+ if (text) {
191
121
  styles.push(
192
- `background-image: ${st}`,
193
- 'background-position: center',
194
- `filter: ${this.createFilterStyle(this.props)}`
122
+ `content: '${text.replace(/'/g, "\\'").replace(/\n/g, '\\A ')}'`,
123
+ `font-size: ${size}px`,
124
+ `color: ${color}`,
125
+ 'white-space: pre-wrap',
126
+ 'word-wrap: break-word',
127
+ 'text-align: center',
128
+ 'display: flex',
129
+ 'align-items: center',
130
+ 'justify-content: center',
131
+ `font-family: ${fontFamily}`,
132
+ 'opacity: 0.3',
133
+ 'background-image: none'
195
134
  )
196
135
  }
136
+ } else if (st !== 'none' && st !== 'index') {
137
+ styles.push(
138
+ `background-image: ${st}`,
139
+ 'background-position: center',
140
+ `filter: ${createFilterStyle(props)}`
141
+ )
142
+ }
197
143
 
198
- return `#container .session-batch-active .xterm-screen::before {
144
+ return `#container .session-batch-active .xterm-screen::before {
199
145
  ${styles.join(';')};
200
146
  }`
201
- }
147
+ }
148
+
149
+ async function writeCss (props, styleTag) {
150
+ const { tabs = [] } = props
151
+ const tabStyles = await Promise.all(
152
+ tabs
153
+ .map(tab => createStyleForTab(tab, props))
154
+ )
155
+ const globalStyle = await createGlobalStyle(props)
156
+ const allStyles = [
157
+ globalStyle,
158
+ ...tabStyles
159
+ ].filter(Boolean).join('\n')
160
+ styleTag.innerHTML = allStyles
161
+ }
202
162
 
203
- writeCss = async () => {
204
- if (!CssOverwrite.styleTag) {
205
- CssOverwrite.styleTag = document.createElement('style')
206
- CssOverwrite.styleTag.type = 'text/css'
207
- CssOverwrite.styleTag.id = 'css-overwrite-terminal-backgrounds'
208
- document.getElementsByTagName('head')[0].appendChild(CssOverwrite.styleTag)
163
+ export default function CssOverwrite (props) {
164
+ const { configLoaded } = props
165
+ const styleTagRef = useRef(null)
166
+
167
+ useEffect(() => {
168
+ if (!configLoaded) {
169
+ return
209
170
  }
210
171
 
211
- const { tabs = [] } = this.props
212
- const tabStyles = await Promise.all(
213
- tabs
214
- .map(tab => this.createStyleForTab(tab))
215
- )
216
- const globalStyle = await this.createGlobalStyle()
217
- const allStyles = [
218
- globalStyle,
219
- ...tabStyles
220
- ].filter(Boolean).join('\n')
221
- CssOverwrite.styleTag.innerHTML = allStyles
222
- }
172
+ if (!styleTagRef.current) {
173
+ styleTagRef.current = document.createElement('style')
174
+ styleTagRef.current.type = 'text/css'
175
+ styleTagRef.current.id = themeDomId
176
+ document.getElementsByTagName('head')[0].appendChild(styleTagRef.current)
177
+ }
223
178
 
224
- updateCss = async () => {
225
- await this.writeCss()
226
- }
179
+ const timeoutId = setTimeout(() => {
180
+ writeCss(props, styleTagRef.current)
181
+ }, 100)
227
182
 
228
- render () {
229
- return null
230
- }
183
+ return () => {
184
+ clearTimeout(timeoutId)
185
+ }
186
+ }, [
187
+ configLoaded,
188
+ props.terminalBackgroundImagePath,
189
+ props.terminalBackgroundFilterBlur,
190
+ props.terminalBackgroundFilterOpacity,
191
+ props.terminalBackgroundFilterBrightness,
192
+ props.terminalBackgroundFilterContrast,
193
+ props.terminalBackgroundFilterGrayscale,
194
+ props.terminalBackgroundText,
195
+ props.terminalBackgroundTextSize,
196
+ props.terminalBackgroundTextColor,
197
+ props.terminalBackgroundTextFontFamily,
198
+ props.tabs
199
+ ])
200
+
201
+ return null
231
202
  }
@@ -2,30 +2,21 @@
2
2
  * ui theme
3
3
  */
4
4
 
5
- import { useEffect, useRef } from 'react'
6
- import eq from 'fast-deep-equal'
5
+ import { useEffect } from 'react'
7
6
 
8
7
  const themeDomId = 'custom-css'
9
8
 
10
9
  export default function CustomCss (props) {
11
- const { customCss } = props
12
- const prevRef = useRef(null)
13
-
14
- async function applyTheme () {
15
- const style = document.getElementById(themeDomId)
16
- style.innerHTML = customCss
17
- }
18
-
19
- useEffect(() => {
20
- applyTheme()
21
- }, [])
10
+ const { customCss, configLoaded } = props
22
11
 
23
12
  useEffect(() => {
24
- if (prevRef.current && !eq(prevRef.current, customCss)) {
25
- applyTheme()
13
+ if (configLoaded) {
14
+ const style = document.getElementById(themeDomId)
15
+ if (style) {
16
+ style.innerHTML = customCss || ''
17
+ }
26
18
  }
27
- prevRef.current = customCss
28
- }, [customCss])
19
+ }, [customCss, configLoaded])
29
20
 
30
21
  return null
31
22
  }
@@ -137,7 +137,13 @@ const bookmarkSchema = {
137
137
  title: 'string - bookmark title',
138
138
  description: 'string - bookmark description',
139
139
  startDirectoryLocal: 'string - local starting directory',
140
- runScripts: 'array - run scripts after connected ({delay,script})'
140
+ runScripts: 'array - run scripts after connected ({delay,script})',
141
+ execWindows: 'string - Windows exec path (overrides global setting)',
142
+ execMac: 'string - Mac exec path (overrides global setting)',
143
+ execLinux: 'string - Linux exec path (overrides global setting)',
144
+ execWindowsArgs: 'array - Windows exec arguments',
145
+ execMacArgs: 'array - Mac exec arguments',
146
+ execLinuxArgs: 'array - Linux exec arguments'
141
147
  },
142
148
  spice: {
143
149
  type: 'spice',
@@ -4,8 +4,7 @@ import { defaultColors, getRandomHexColor } from '../../../common/rand-hex-color
4
4
  import { HexInput } from './hex-input.jsx'
5
5
  import './color-picker.styl'
6
6
 
7
- export const ColorPicker = React.forwardRef((props, ref) => {
8
- const { value, onChange } = props
7
+ export function ColorPicker ({ value, onChange, ref, disabled, isRgba }) {
9
8
  const [visible, setVisible] = useState(false)
10
9
 
11
10
  const handleChange = (color) => {
@@ -18,7 +17,7 @@ export const ColorPicker = React.forwardRef((props, ref) => {
18
17
  }
19
18
 
20
19
  function onColorChange (color) {
21
- handleChange(props.isRgba ? color.toRgbString() : color.toHexString())
20
+ handleChange(isRgba ? color.toRgbString() : color.toHexString())
22
21
  }
23
22
 
24
23
  function renderContent () {
@@ -59,7 +58,7 @@ export const ColorPicker = React.forwardRef((props, ref) => {
59
58
  <div ref={ref} className='color-picker-choose' style={{ backgroundColor: value }} />
60
59
  )
61
60
 
62
- if (props.disabled) return inner
61
+ if (disabled) return inner
63
62
 
64
63
  return (
65
64
  <Popover
@@ -72,7 +71,4 @@ export const ColorPicker = React.forwardRef((props, ref) => {
72
71
  {inner}
73
72
  </Popover>
74
73
  )
75
- })
76
-
77
- ColorPicker.displayName = 'ColorPicker'
78
- ColorPicker.static = true
74
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * bookmark form - exec settings field
3
+ * Renders exec path and arguments fields for Windows/Mac/Linux
4
+ */
5
+ import React from 'react'
6
+ import { Form, Input, Select, Space } from 'antd'
7
+ import { formItemLayout } from '../../../common/form-layout'
8
+
9
+ const FormItem = Form.Item
10
+
11
+ export default function ExecSettingsField () {
12
+ const platforms = ['linux', 'mac', 'windows']
13
+ return platforms.map((platform) => {
14
+ const platformCapitalized = platform.charAt(0).toUpperCase() + platform.slice(1)
15
+ const label = `exec${platformCapitalized}`
16
+ return (
17
+ <React.Fragment key={platform}>
18
+ <FormItem
19
+ {...formItemLayout}
20
+ label={label}
21
+ >
22
+ <Space.Compact className='width-100'>
23
+ <FormItem noStyle name={label}>
24
+ <Input
25
+ placeholder={`${platformCapitalized} exec path`}
26
+ maxLength={500}
27
+ />
28
+ </FormItem>
29
+ <FormItem
30
+ noStyle
31
+ name={`exec${platformCapitalized}Args`}
32
+ >
33
+ <Select
34
+ mode='tags'
35
+ placeholder={`${platformCapitalized} exec arguments`}
36
+ tokenSeparators={['\n']}
37
+ />
38
+ </FormItem>
39
+ </Space.Compact>
40
+ </FormItem>
41
+ </React.Fragment>
42
+ )
43
+ })
44
+ }
@@ -13,6 +13,7 @@ import SshTunnels from './ssh-tunnels.jsx'
13
13
  import SshAgent from './ssh-agent.jsx'
14
14
  import ConnectionHopping from './connection-hopping.jsx'
15
15
  import TerminalBackgroundField from './terminal-background.jsx'
16
+ import ExecSettingsField from './exec-settings-field.jsx'
16
17
  import useQuickCmds from './quick-commands.jsx'
17
18
  import ProfileItem from './profile-item.jsx'
18
19
  import renderRunScripts from './run-scripts.jsx'
@@ -150,6 +151,8 @@ export function renderFormItem (item, formItemLayout, form, ctxProps, index) {
150
151
  )
151
152
  case 'terminalBackground':
152
153
  return <TerminalBackgroundField key={name} />
154
+ case 'execSettings':
155
+ return <ExecSettingsField key={name} />
153
156
  case 'profileItem':
154
157
  return <ProfileItem key={name} store={ctxProps.store} profileFilter={item.profileFilter} />
155
158
  case 'quickCommands':