@electerm/electerm-react 2.13.6 → 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.
- package/client/components/ai/ai-chat.jsx +44 -2
- package/client/components/ai/ai-stop-icon.jsx +13 -0
- package/client/components/ai/ai.styl +10 -0
- package/client/components/bg/css-overwrite.jsx +158 -187
- package/client/components/bg/custom-css.jsx +9 -15
- package/client/components/bookmark-form/bookmark-schema.js +7 -1
- package/client/components/bookmark-form/common/color-picker.jsx +4 -8
- package/client/components/bookmark-form/common/exec-settings-field.jsx +44 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/config/common-fields.js +1 -0
- package/client/components/bookmark-form/config/local.js +3 -1
- package/client/components/common/animate-text.jsx +22 -23
- package/client/components/common/modal.jsx +2 -0
- package/client/components/common/notification.jsx +1 -1
- package/client/components/common/opacity.jsx +8 -6
- package/client/components/common/password.jsx +19 -32
- package/client/components/footer/cmd-history.jsx +154 -0
- package/client/components/footer/cmd-history.styl +73 -0
- package/client/components/footer/footer-entry.jsx +15 -1
- package/client/components/main/main.jsx +2 -3
- package/client/components/main/ui-theme.jsx +10 -6
- package/client/components/profile/profile-list.jsx +1 -1
- package/client/components/quick-commands/quick-commands-list.jsx +1 -1
- package/client/components/quick-commands/quick-commands-select.jsx +1 -4
- package/client/components/rdp/rdp-session.jsx +23 -4
- package/client/components/session/session.styl +1 -3
- package/client/components/setting-panel/list.styl +7 -0
- package/client/components/setting-panel/terminal-bg-config.jsx +2 -0
- package/client/components/setting-panel/text-bg-modal.jsx +9 -9
- package/client/components/setting-sync/setting-sync-form.jsx +10 -5
- package/client/components/sftp/file-item.jsx +22 -0
- package/client/components/sidebar/history-item.jsx +6 -3
- package/client/components/sidebar/history.jsx +48 -5
- package/client/components/sidebar/sidebar-panel.jsx +0 -13
- package/client/components/sidebar/sidebar.styl +19 -0
- package/client/components/ssh-config/load-ssh-configs-item.jsx +99 -0
- package/client/components/ssh-config/load-ssh-configs.jsx +38 -9
- package/client/components/ssh-config/ssh-config.styl +3 -0
- package/client/components/tabs/add-btn-menu.jsx +28 -4
- package/client/components/tabs/add-btn.jsx +1 -1
- package/client/components/tabs/add-btn.styl +8 -0
- package/client/components/terminal/terminal.jsx +28 -11
- package/client/components/terminal/transfer-client-base.js +44 -0
- package/client/components/terminal/trzsz-client.js +10 -11
- package/client/components/terminal/zmodem-client.js +10 -11
- package/client/components/text-editor/edit-with-custom-editor.jsx +49 -0
- package/client/components/text-editor/simple-editor.jsx +38 -6
- package/client/components/text-editor/text-editor-form.jsx +13 -5
- package/client/components/text-editor/text-editor.jsx +20 -1
- package/client/components/vnc/vnc-session.jsx +3 -0
- package/client/components/vnc/vnc.styl +1 -1
- package/client/store/bookmark.js +3 -11
- package/client/store/common.js +31 -4
- package/client/store/init-state.js +26 -1
- package/client/store/store.js +1 -1
- package/client/store/sync.js +2 -3
- package/client/store/watch.js +8 -1
- package/package.json +1 -1
- package/client/components/ssh-config/ssh-config-item.jsx +0 -24
|
@@ -18,6 +18,7 @@ export class TransferClientBase {
|
|
|
18
18
|
this.currentTransfer = null
|
|
19
19
|
this.savePath = null
|
|
20
20
|
this.messageHandler = null
|
|
21
|
+
this._prevProgressRows = 0
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -107,6 +108,49 @@ export class TransferClientBase {
|
|
|
107
108
|
this.writeToTerminal(`\x1b[32m\x1b[1m${this.getProtocolDisplayName()}::${type}::START\x1b[0m\r\n`)
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Write progress bar to terminal
|
|
113
|
+
* @param {Object} options - Progress options
|
|
114
|
+
* @param {string} options.name - File name
|
|
115
|
+
* @param {number} options.size - Total size in bytes
|
|
116
|
+
* @param {number} options.transferred - Transferred bytes
|
|
117
|
+
* @param {number} options.speed - Transfer speed in bytes/s
|
|
118
|
+
* @param {boolean} options.isComplete - Whether transfer is complete
|
|
119
|
+
* @param {Function} options.formatSize - Function to format size
|
|
120
|
+
* @returns {string} The progress string written to terminal
|
|
121
|
+
*/
|
|
122
|
+
writeProgressBar ({ name, size, transferred, speed, isComplete = false, formatSize = (b) => b }) {
|
|
123
|
+
const percent = size > 0 ? Math.floor(transferred * 100 / size) : 100
|
|
124
|
+
const barWidth = 30
|
|
125
|
+
const filledWidth = Math.floor(percent / 100 * barWidth)
|
|
126
|
+
const emptyWidth = barWidth - filledWidth
|
|
127
|
+
|
|
128
|
+
const bar = '\x1b[32m' + '\u2588'.repeat(filledWidth) + '\x1b[90m' + '\u2591'.repeat(emptyWidth) + '\x1b[0m'
|
|
129
|
+
|
|
130
|
+
const sizeStr = `${formatSize(transferred)}/${formatSize(size)}`
|
|
131
|
+
const speedStr = speed > 0 ? `, ${formatSize(speed)}/s` : ''
|
|
132
|
+
const doneStr = isComplete ? ' \x1b[32m\x1b[1m[DONE]\x1b[0m' : ''
|
|
133
|
+
|
|
134
|
+
const str = `\x1b[32m${name}\x1b[0m: ${percent}% ${bar} ${sizeStr}${speedStr}${doneStr}`
|
|
135
|
+
|
|
136
|
+
// Calculate visible length (no ANSI codes) to detect line wrapping
|
|
137
|
+
const visibleLen = name.length + 2 + String(percent).length + 2 + barWidth + 1 + sizeStr.length + speedStr.length + (isComplete ? 7 : 0)
|
|
138
|
+
const cols = this.terminal?.term?.cols || 80
|
|
139
|
+
const currentRows = Math.max(1, Math.ceil(visibleLen / cols))
|
|
140
|
+
|
|
141
|
+
// Move cursor back up to the start of the previous progress block, then
|
|
142
|
+
// erase everything from there to end-of-display so wrapped lines are gone.
|
|
143
|
+
let clearSeq = '\r'
|
|
144
|
+
for (let i = 0; i < this._prevProgressRows; i++) {
|
|
145
|
+
clearSeq += '\x1b[A' // cursor up one row
|
|
146
|
+
}
|
|
147
|
+
clearSeq += '\x1b[J' // erase from cursor to end of display
|
|
148
|
+
|
|
149
|
+
this._prevProgressRows = currentRows - 1
|
|
150
|
+
this.writeToTerminal(clearSeq + str + '\r')
|
|
151
|
+
return str
|
|
152
|
+
}
|
|
153
|
+
|
|
110
154
|
/**
|
|
111
155
|
* Get protocol display name
|
|
112
156
|
* Should be overridden by subclass
|
|
@@ -208,8 +208,9 @@ export class TrzszClient extends TransferClientBase {
|
|
|
208
208
|
this.currentTransfer.path = path
|
|
209
209
|
// Call directly to ensure 100% is displayed immediately
|
|
210
210
|
this._doWriteProgress(true)
|
|
211
|
-
// Add newline after completion
|
|
211
|
+
// Add newline after completion and reset row tracker for next file
|
|
212
212
|
this.writeToTerminal('\r\n')
|
|
213
|
+
this._prevProgressRows = 0
|
|
213
214
|
}
|
|
214
215
|
this.currentTransfer = null
|
|
215
216
|
}
|
|
@@ -230,20 +231,18 @@ export class TrzszClient extends TransferClientBase {
|
|
|
230
231
|
if (!this.currentTransfer || !this.terminal?.term) return
|
|
231
232
|
|
|
232
233
|
const { name, size, transferred, path, serverSpeed } = this.currentTransfer
|
|
233
|
-
const percent = size > 0 ? Math.floor(transferred * 100 / size) : 100
|
|
234
|
-
|
|
235
|
-
// Use server's speed if available, otherwise calculate locally
|
|
236
234
|
const speed = serverSpeed || 0
|
|
237
|
-
|
|
238
|
-
// Use full path if available, otherwise just name
|
|
239
235
|
const displayName = path || name
|
|
240
|
-
|
|
241
|
-
// filesize expects bytes and formats to human readable
|
|
242
236
|
const formatSize = (bytes) => filesize(bytes)
|
|
243
237
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
238
|
+
this.writeProgressBar({
|
|
239
|
+
name: displayName,
|
|
240
|
+
size,
|
|
241
|
+
transferred,
|
|
242
|
+
speed,
|
|
243
|
+
isComplete,
|
|
244
|
+
formatSize
|
|
245
|
+
})
|
|
247
246
|
}
|
|
248
247
|
|
|
249
248
|
/**
|
|
@@ -180,8 +180,9 @@ export class ZmodemClient extends TransferClientBase {
|
|
|
180
180
|
this.currentTransfer.path = path
|
|
181
181
|
// Call directly to ensure 100% is displayed immediately
|
|
182
182
|
this._doWriteProgress(true)
|
|
183
|
-
// Add newline after completion
|
|
183
|
+
// Add newline after completion and reset row tracker for next file
|
|
184
184
|
this.writeToTerminal('\r\n')
|
|
185
|
+
this._prevProgressRows = 0
|
|
185
186
|
}
|
|
186
187
|
this.currentTransfer = null
|
|
187
188
|
}
|
|
@@ -212,20 +213,18 @@ export class ZmodemClient extends TransferClientBase {
|
|
|
212
213
|
if (!this.currentTransfer || !this.terminal?.term) return
|
|
213
214
|
|
|
214
215
|
const { name, size, transferred, path, serverSpeed } = this.currentTransfer
|
|
215
|
-
const percent = size > 0 ? Math.floor(transferred * 100 / size) : 100
|
|
216
|
-
|
|
217
|
-
// Use server's speed if available, otherwise calculate locally
|
|
218
216
|
const speed = serverSpeed || 0
|
|
219
|
-
|
|
220
|
-
// Use full path if available, otherwise just name
|
|
221
217
|
const displayName = path || name
|
|
222
|
-
|
|
223
|
-
// filesize expects bytes and formats to human readable
|
|
224
218
|
const formatSize = (bytes) => filesize(bytes)
|
|
225
219
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
220
|
+
this.writeProgressBar({
|
|
221
|
+
name: displayName,
|
|
222
|
+
size,
|
|
223
|
+
transferred,
|
|
224
|
+
speed,
|
|
225
|
+
isComplete,
|
|
226
|
+
formatSize
|
|
227
|
+
})
|
|
229
228
|
}
|
|
230
229
|
}
|
|
231
230
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit with custom editor - input + button component
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import { Button, Input, Space } from 'antd'
|
|
7
|
+
|
|
8
|
+
const LS_KEY = 'customEditorCommand'
|
|
9
|
+
const e = window.translate
|
|
10
|
+
|
|
11
|
+
export default function EditWithCustomEditor ({ loading, editWithCustom }) {
|
|
12
|
+
const [editorCommand, setEditorCommand] = useState(
|
|
13
|
+
() => window.localStorage.getItem(LS_KEY) || ''
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
function handleChange (ev) {
|
|
17
|
+
const val = ev.target.value
|
|
18
|
+
setEditorCommand(val)
|
|
19
|
+
window.localStorage.setItem(LS_KEY, val)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function handleClick () {
|
|
23
|
+
const cmd = editorCommand.trim()
|
|
24
|
+
if (cmd) {
|
|
25
|
+
editWithCustom(cmd)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (window.et.isWebApp) {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Space.Compact className='mg1b'>
|
|
35
|
+
<Button
|
|
36
|
+
type='primary'
|
|
37
|
+
disabled={loading || !editorCommand.trim()}
|
|
38
|
+
onClick={handleClick}
|
|
39
|
+
>
|
|
40
|
+
{e('editWith')}
|
|
41
|
+
</Button>
|
|
42
|
+
<Input
|
|
43
|
+
value={editorCommand}
|
|
44
|
+
onChange={handleChange}
|
|
45
|
+
disabled={loading}
|
|
46
|
+
/>
|
|
47
|
+
</Space.Compact>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -18,6 +18,11 @@ export default function SimpleEditor (props) {
|
|
|
18
18
|
|
|
19
19
|
// When currentMatch changes, highlight the match in textarea
|
|
20
20
|
useEffect(() => {
|
|
21
|
+
// Only process navigation when explicitly triggered (not when text changes)
|
|
22
|
+
if (!isNavigating) {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
if (currentMatch >= 0 && occurrences.length > 0) {
|
|
22
27
|
const match = occurrences[currentMatch]
|
|
23
28
|
if (editorRef.current) {
|
|
@@ -26,10 +31,8 @@ export default function SimpleEditor (props) {
|
|
|
26
31
|
// Set selection range to select the matched text
|
|
27
32
|
textarea.setSelectionRange(match.start, match.end)
|
|
28
33
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
textarea.focus()
|
|
32
|
-
}
|
|
34
|
+
// Focus the textarea when explicitly navigating between matches
|
|
35
|
+
textarea.focus()
|
|
33
36
|
|
|
34
37
|
// Scroll to the selection position
|
|
35
38
|
// Use setTimeout to ensure the selection is rendered before scrolling
|
|
@@ -53,10 +56,17 @@ export default function SimpleEditor (props) {
|
|
|
53
56
|
setIsNavigating(false)
|
|
54
57
|
}, [currentMatch, occurrences])
|
|
55
58
|
|
|
56
|
-
// Auto-search when keyword changes
|
|
59
|
+
// Auto-search when keyword changes (but not when text is being edited)
|
|
57
60
|
useEffect(() => {
|
|
61
|
+
// Set navigating to true so first match is highlighted when searching
|
|
62
|
+
setIsNavigating(true)
|
|
58
63
|
findMatches()
|
|
59
|
-
}, [searchKeyword
|
|
64
|
+
}, [searchKeyword])
|
|
65
|
+
|
|
66
|
+
// Update matches when text changes, but don't change currentMatch position
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
updateMatchesOnly()
|
|
69
|
+
}, [props.value])
|
|
60
70
|
|
|
61
71
|
// Copy the editor content to clipboard
|
|
62
72
|
const copyEditorContent = () => {
|
|
@@ -87,6 +97,28 @@ export default function SimpleEditor (props) {
|
|
|
87
97
|
setCurrentMatch(matches.length ? 0 : -1)
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
// Update matches only (without changing currentMatch position)
|
|
101
|
+
const updateMatchesOnly = () => {
|
|
102
|
+
if (!searchKeyword) {
|
|
103
|
+
setOccurrences([])
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const matches = []
|
|
108
|
+
const text = props.value || ''
|
|
109
|
+
const escapedKeyword = escapeRegExp(searchKeyword)
|
|
110
|
+
const regex = new RegExp(escapedKeyword, 'gi')
|
|
111
|
+
let match
|
|
112
|
+
|
|
113
|
+
while ((match = regex.exec(text)) !== null) {
|
|
114
|
+
matches.push({
|
|
115
|
+
start: match.index,
|
|
116
|
+
end: match.index + searchKeyword.length
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
setOccurrences(matches)
|
|
120
|
+
}
|
|
121
|
+
|
|
90
122
|
// Handle search action when user presses enter or clicks the search button
|
|
91
123
|
const handleSearch = (e) => {
|
|
92
124
|
if (e && e.stopPropagation) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { useEffect } from 'react'
|
|
6
6
|
import { Form, Button } from 'antd'
|
|
7
7
|
import SimpleEditor from './simple-editor'
|
|
8
|
+
import EditWithCustomEditor from './edit-with-custom-editor'
|
|
8
9
|
|
|
9
10
|
const FormItem = Form.Item
|
|
10
11
|
const e = window.translate
|
|
@@ -38,7 +39,7 @@ export default function TextEditorForm (props) {
|
|
|
38
39
|
} = props
|
|
39
40
|
const popsEdit = {
|
|
40
41
|
type: 'primary',
|
|
41
|
-
className: '
|
|
42
|
+
className: 'mg1r mg1b',
|
|
42
43
|
disabled: loading,
|
|
43
44
|
onClick: props.editWith
|
|
44
45
|
}
|
|
@@ -62,10 +63,6 @@ export default function TextEditorForm (props) {
|
|
|
62
63
|
<SimpleEditor />
|
|
63
64
|
</FormItem>
|
|
64
65
|
<div className='pd1t pd2b'>
|
|
65
|
-
<Button
|
|
66
|
-
{...popsEdit}
|
|
67
|
-
>{e('editWithSystemEditor')}
|
|
68
|
-
</Button>
|
|
69
66
|
<Button
|
|
70
67
|
type='primary'
|
|
71
68
|
className='mg1r mg1b'
|
|
@@ -85,6 +82,17 @@ export default function TextEditorForm (props) {
|
|
|
85
82
|
>{e('cancel')}
|
|
86
83
|
</Button>
|
|
87
84
|
</div>
|
|
85
|
+
<div className='pd1t pd2b'>
|
|
86
|
+
<Button
|
|
87
|
+
{...popsEdit}
|
|
88
|
+
>
|
|
89
|
+
{e('editWithSystemEditor')}
|
|
90
|
+
</Button>
|
|
91
|
+
<EditWithCustomEditor
|
|
92
|
+
loading={loading}
|
|
93
|
+
editWithCustom={props.editWithCustom}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
88
96
|
</Form>
|
|
89
97
|
)
|
|
90
98
|
}
|
|
@@ -118,6 +118,24 @@ export default class TextEditor extends PureComponent {
|
|
|
118
118
|
fileRef.editWithSystemEditor(text)
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
editWithCustom = async (editorCommand) => {
|
|
122
|
+
this.setStateProxy({
|
|
123
|
+
loading: true
|
|
124
|
+
})
|
|
125
|
+
const {
|
|
126
|
+
id, text
|
|
127
|
+
} = this.state
|
|
128
|
+
const fileRef = refs.get(id)
|
|
129
|
+
if (!fileRef) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
await fileRef.editWithCustomEditor(text, editorCommand)
|
|
133
|
+
.catch(err => {
|
|
134
|
+
this.setStateProxy({ loading: false })
|
|
135
|
+
window.store.onError(err)
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
cancel = () => {
|
|
122
140
|
this.setStateProxy({
|
|
123
141
|
id: '',
|
|
@@ -150,7 +168,8 @@ export default class TextEditor extends PureComponent {
|
|
|
150
168
|
submit: this.handleSubmit,
|
|
151
169
|
text,
|
|
152
170
|
cancel: this.cancel,
|
|
153
|
-
editWith: this.editWith
|
|
171
|
+
editWith: this.editWith,
|
|
172
|
+
editWithCustom: this.editWithCustom
|
|
154
173
|
}
|
|
155
174
|
return (
|
|
156
175
|
<Modal
|
|
@@ -373,6 +373,9 @@ export default class VncSession extends PureComponent {
|
|
|
373
373
|
}
|
|
374
374
|
rfb.scaleViewport = scaleViewport
|
|
375
375
|
rfb.clipViewport = clipViewport
|
|
376
|
+
rfb.qualityLevel = qualityLevel
|
|
377
|
+
rfb.compressionLevel = compressionLevel
|
|
378
|
+
rfb.viewOnly = viewOnly
|
|
376
379
|
this.rfb = rfb
|
|
377
380
|
}
|
|
378
381
|
|
package/client/store/bookmark.js
CHANGED
|
@@ -33,18 +33,10 @@ export default Store => {
|
|
|
33
33
|
return {
|
|
34
34
|
term: 'xterm-256color',
|
|
35
35
|
id: uid(),
|
|
36
|
-
type: '
|
|
37
|
-
title: 'ssh config: ' + t.title,
|
|
36
|
+
type: 'ssh',
|
|
38
37
|
color: '#0088cc',
|
|
39
|
-
|
|
40
|
-
{
|
|
41
|
-
script: `ssh ${t.title}`,
|
|
42
|
-
delay: 500
|
|
43
|
-
}
|
|
44
|
-
]
|
|
38
|
+
...t
|
|
45
39
|
}
|
|
46
|
-
}).filter(d => {
|
|
47
|
-
return !store.bookmarks.find(t => t.title === d.title)
|
|
48
40
|
})
|
|
49
41
|
const ids = bookmarksToAdd.map(d => d.id)
|
|
50
42
|
let sshConfigGroup = store.bookmarkGroups.find(d => d.id === 'sshConfig')
|
|
@@ -61,7 +53,7 @@ export default Store => {
|
|
|
61
53
|
...ids,
|
|
62
54
|
...(sshConfigGroup.bookmarkIds || [])
|
|
63
55
|
]
|
|
64
|
-
})
|
|
56
|
+
}, 'bookmarkGroups')
|
|
65
57
|
}
|
|
66
58
|
return store.addItems(bookmarksToAdd, 'bookmarks')
|
|
67
59
|
}
|
package/client/store/common.js
CHANGED
|
@@ -345,13 +345,40 @@ export default Store => {
|
|
|
345
345
|
return
|
|
346
346
|
}
|
|
347
347
|
const { terminalCommandHistory } = window.store
|
|
348
|
-
terminalCommandHistory.
|
|
348
|
+
const existing = terminalCommandHistory.get(cmd)
|
|
349
|
+
if (existing) {
|
|
350
|
+
// Use set() to trigger reactivity
|
|
351
|
+
terminalCommandHistory.set(cmd, {
|
|
352
|
+
count: existing.count + 1,
|
|
353
|
+
lastUseTime: new Date().toISOString()
|
|
354
|
+
})
|
|
355
|
+
} else {
|
|
356
|
+
terminalCommandHistory.set(cmd, {
|
|
357
|
+
count: 1,
|
|
358
|
+
lastUseTime: new Date().toISOString()
|
|
359
|
+
})
|
|
360
|
+
}
|
|
349
361
|
if (terminalCommandHistory.size > 100) {
|
|
350
362
|
// Delete oldest 20 items when history exceeds 100
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
363
|
+
const entries = Array.from(terminalCommandHistory.entries())
|
|
364
|
+
entries.sort((a, b) => new Date(a[1].lastUseTime).getTime() - new Date(b[1].lastUseTime).getTime())
|
|
365
|
+
for (let i = 0; i < 20 && i < entries.length; i++) {
|
|
366
|
+
terminalCommandHistory.delete(entries[i][0])
|
|
354
367
|
}
|
|
355
368
|
}
|
|
356
369
|
})
|
|
370
|
+
|
|
371
|
+
Store.prototype.deleteCmdHistory = function (cmd) {
|
|
372
|
+
const { terminalCommandHistory } = window.store
|
|
373
|
+
terminalCommandHistory.delete(cmd)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
Store.prototype.clearAllCmdHistory = function () {
|
|
377
|
+
window.store.terminalCommandHistory = new Map()
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
Store.prototype.runCmdFromHistory = function (cmd) {
|
|
381
|
+
window.store.runQuickCommand(cmd)
|
|
382
|
+
window.store.addCmdHistory(cmd)
|
|
383
|
+
}
|
|
357
384
|
}
|
|
@@ -74,7 +74,32 @@ export default () => {
|
|
|
74
74
|
addressBookmarksLocal: ls.getItemJSON(localAddrBookmarkLsKey, []),
|
|
75
75
|
openResolutionEdit: false,
|
|
76
76
|
resolutions: ls.getItemJSON(resolutionsLsKey, []),
|
|
77
|
-
terminalCommandHistory:
|
|
77
|
+
// terminalCommandHistory: Map<cmdString, {count: Number, lastUseTime: DateString}>
|
|
78
|
+
// Load from localStorage and migrate from old format (Set of strings) if needed
|
|
79
|
+
terminalCommandHistory: (() => {
|
|
80
|
+
const savedData = ls.getItemJSON(cmdHistoryKey, [])
|
|
81
|
+
const map = new Map()
|
|
82
|
+
if (Array.isArray(savedData)) {
|
|
83
|
+
// Check if old format (array of strings) or new format (array of objects)
|
|
84
|
+
if (savedData.length > 0 && typeof savedData[0] === 'string') {
|
|
85
|
+
// Old format: migrate to new format
|
|
86
|
+
savedData.forEach(cmd => {
|
|
87
|
+
map.set(cmd, { count: 1, lastUseTime: new Date().toISOString() })
|
|
88
|
+
})
|
|
89
|
+
} else {
|
|
90
|
+
// New format: array of {cmd, count, lastUseTime}
|
|
91
|
+
savedData.forEach(item => {
|
|
92
|
+
if (item.cmd) {
|
|
93
|
+
map.set(item.cmd, {
|
|
94
|
+
count: item.count || 1,
|
|
95
|
+
lastUseTime: item.lastUseTime || new Date().toISOString()
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return map
|
|
102
|
+
})(),
|
|
78
103
|
|
|
79
104
|
// workspaces
|
|
80
105
|
workspaces: [],
|
package/client/store/store.js
CHANGED
|
@@ -176,7 +176,7 @@ class Store {
|
|
|
176
176
|
|
|
177
177
|
get terminalCommandSuggestions () {
|
|
178
178
|
const { store } = window
|
|
179
|
-
const historyCommands = Array.from(store.terminalCommandHistory)
|
|
179
|
+
const historyCommands = Array.from(store.terminalCommandHistory.keys())
|
|
180
180
|
const batchInputCommands = store.batchInputs
|
|
181
181
|
const quickCommands = store.quickCommands.reduce(
|
|
182
182
|
(p, q) => {
|
package/client/store/sync.js
CHANGED
|
@@ -35,9 +35,8 @@ async function fetchData (type, func, args, token, proxy) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function updateSyncServerStatusFromGist (store, gist, type) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
)
|
|
38
|
+
const statusContent = get(gist, 'files["electerm-status.json"].content')
|
|
39
|
+
const status = statusContent ? parseJsonSafe(statusContent) : undefined
|
|
41
40
|
store.syncServerStatus[type] = status
|
|
42
41
|
}
|
|
43
42
|
|
package/client/store/watch.js
CHANGED
|
@@ -145,7 +145,14 @@ export default store => {
|
|
|
145
145
|
}).start()
|
|
146
146
|
|
|
147
147
|
autoRun(() => {
|
|
148
|
-
|
|
148
|
+
const history = store.terminalCommandHistory
|
|
149
|
+
// Save in new format: array of {cmd, count, lastUseTime}
|
|
150
|
+
const data = Array.from(history.entries()).map(([cmd, info]) => ({
|
|
151
|
+
cmd,
|
|
152
|
+
count: info.count,
|
|
153
|
+
lastUseTime: info.lastUseTime
|
|
154
|
+
}))
|
|
155
|
+
ls.setItemJSON(cmdHistoryKey, data)
|
|
149
156
|
return store.terminalCommandHistory
|
|
150
157
|
}).start()
|
|
151
158
|
|
package/package.json
CHANGED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { Tooltip } from 'antd'
|
|
2
|
-
|
|
3
|
-
export default function SshConfigItem (props) {
|
|
4
|
-
const { item } = props
|
|
5
|
-
|
|
6
|
-
const generateTooltipContent = (item) => {
|
|
7
|
-
return Object.entries(item)
|
|
8
|
-
.filter(([key]) => key !== 'id')
|
|
9
|
-
.map(([key, value]) => (
|
|
10
|
-
<div key={key}>
|
|
11
|
-
<b className='mg1r'>{key}:</b>
|
|
12
|
-
<span>{value}</span>
|
|
13
|
-
</div>
|
|
14
|
-
))
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<Tooltip title={generateTooltipContent(item)}>
|
|
19
|
-
<div className='elli pd1y pd2x'>
|
|
20
|
-
ssh {item.title}
|
|
21
|
-
</div>
|
|
22
|
-
</Tooltip>
|
|
23
|
-
)
|
|
24
|
-
}
|