@electerm/electerm-react 5.3.26 → 5.5.9
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/README.md +246 -12
- package/client/common/choose-save-folder.js +21 -2
- package/client/common/constants.js +20 -6
- package/client/common/create-title.jsx +6 -0
- package/client/common/default-setting.js +15 -0
- package/client/common/init-setting-item.js +5 -0
- package/client/common/platform.js +24 -0
- package/client/components/ai/agent-tool-call-card.jsx +10 -2
- package/client/components/ai/agent.js +4 -1
- package/client/components/ai/ai-attachments.js +87 -0
- package/client/components/ai/ai-chat-empty.jsx +43 -0
- package/client/components/ai/ai-chat-entry.jsx +13 -4
- package/client/components/ai/ai-chat-history-item.jsx +28 -4
- package/client/components/ai/ai-chat-history.jsx +2 -1
- package/client/components/ai/ai-chat.jsx +104 -13
- package/client/components/ai/ai-config-props.js +4 -130
- package/client/components/ai/ai-config.jsx +135 -15
- package/client/components/ai/ai-presets.js +173 -0
- package/client/components/ai/ai.styl +83 -0
- package/client/components/bg/css-overwrite.jsx +11 -0
- package/client/components/bookmark-form/bookmark-schema.js +4 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/config/common-fields.js +12 -0
- package/client/components/bookmark-form/config/local.js +3 -2
- package/client/components/bookmark-form/config/serial.js +3 -2
- package/client/components/bookmark-form/config/ssh.js +2 -1
- package/client/components/bookmark-form/config/telnet.js +3 -2
- package/client/components/common/item-filter.jsx +76 -0
- package/client/components/common/item-filter.styl +54 -0
- package/client/components/common/lazy-boundary.jsx +34 -0
- package/client/components/common/opacity.jsx +3 -1
- package/client/components/footer/cmd-history.jsx +132 -45
- package/client/components/footer/cmd-history.styl +42 -4
- package/client/components/footer/footer-entry.jsx +35 -5
- package/client/components/footer/footer.styl +8 -1
- package/client/components/footer/multi-tab-run-modal.jsx +83 -0
- package/client/components/footer/tab-select.jsx +34 -17
- package/client/components/layout/layout.jsx +23 -31
- package/client/components/layout/pixed.js +3 -0
- package/client/components/main/main.jsx +55 -28
- package/client/components/main/term-fullscreen.styl +9 -3
- package/client/components/quick-commands/qm.styl +3 -2
- package/client/components/quick-commands/quick-command-create-modal.jsx +60 -0
- package/client/components/quick-commands/quick-commands-form-elem.jsx +10 -4
- package/client/components/rdp/file-transfer.js +4 -4
- package/client/components/remote-monitor/monitor-details.jsx +458 -0
- package/client/components/remote-monitor/monitor-details.styl +87 -0
- package/client/components/remote-monitor/monitor-model.js +764 -0
- package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
- package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
- package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
- package/client/components/remote-monitor/session-monitor.js +361 -0
- package/client/components/remote-monitor/use-monitor-details.js +10 -0
- package/client/components/remote-monitor/visibility.js +18 -0
- package/client/components/session/session-control.jsx +29 -2
- package/client/components/session/session.jsx +40 -18
- package/client/components/session/sessions.jsx +5 -2
- package/client/components/setting-panel/setting-common.jsx +25 -56
- package/client/components/setting-panel/setting-modal.jsx +57 -42
- package/client/components/setting-panel/setting-terminal.jsx +8 -2
- package/client/components/setting-panel/setting.styl +1 -1
- package/client/components/setting-panel/tab-triggers.jsx +71 -0
- package/client/components/sftp/file-compare-modal.jsx +6 -3
- package/client/components/sftp/file-item.jsx +5 -3
- package/client/components/sftp/owner-list.js +11 -2
- package/client/components/sftp/sftp-entry.jsx +4 -0
- package/client/components/side-panel-r/side-panel-r.jsx +2 -2
- package/client/components/sidebar/side-panel.jsx +1 -1
- package/client/components/sys-menu/menu-btn.jsx +30 -1
- package/client/components/tabs/index.jsx +2 -2
- package/client/components/tabs/tab-title.jsx +3 -2
- package/client/components/tabs/tab.jsx +5 -1
- package/client/components/terminal/attach-addon-custom.js +295 -49
- package/client/components/terminal/automation/index.js +64 -0
- package/client/components/terminal/automation/keys.js +49 -0
- package/client/components/terminal/automation/stream-matcher.js +82 -0
- package/client/components/terminal/automation/strip-ansi.js +19 -0
- package/client/components/terminal/automation/trigger-engine.js +233 -0
- package/client/components/terminal/automation/trigger-presets.js +110 -0
- package/client/components/terminal/command-tracker-addon.js +31 -1
- package/client/components/terminal/mixins/index.js +27 -0
- package/client/components/terminal/mixins/term-attach.js +123 -0
- package/client/components/terminal/mixins/term-config.jsx +84 -0
- package/client/components/terminal/mixins/term-content.js +126 -0
- package/client/components/terminal/mixins/term-context-menu.jsx +323 -0
- package/client/components/terminal/mixins/term-file-drop.js +149 -0
- package/client/components/terminal/mixins/term-init.js +122 -0
- package/client/components/terminal/mixins/term-log.jsx +96 -0
- package/client/components/terminal/mixins/term-resize.js +87 -0
- package/client/components/terminal/mixins/term-socket.js +433 -0
- package/client/components/terminal/mixins/term-suggestions.js +224 -0
- package/client/components/terminal/mixins/term-theme.js +190 -0
- package/client/components/terminal/mixins/term-touch.js +523 -0
- package/client/components/terminal/shell-detect.js +37 -0
- package/client/components/terminal/shell.js +0 -28
- package/client/components/terminal/shortcut-bar-defs.js +30 -4
- package/client/components/terminal/shortcut-bar-edit.jsx +40 -9
- package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
- package/client/components/terminal/shortcut-bar.jsx +17 -10
- package/client/components/terminal/shortcut-bar.styl +4 -0
- package/client/components/terminal/ssh-reload-state.js +96 -0
- package/client/components/terminal/startup-queue.js +433 -0
- package/client/components/terminal/terminal-interactive.jsx +32 -0
- package/client/components/terminal/terminal-select-text.jsx +69 -0
- package/client/components/terminal/terminal.jsx +227 -2064
- package/client/components/terminal/terminal.styl +61 -0
- package/client/components/terminal/transfer-client-base.js +5 -29
- package/client/components/terminal/trzsz-client.js +1 -3
- package/client/components/terminal/xmodem-client.js +1 -3
- package/client/components/terminal/xterm-loader.js +7 -0
- package/client/components/terminal/zmodem-client.js +1 -3
- package/client/components/terminal-info/base.jsx +18 -70
- package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
- package/client/components/terminal-info/terminal-info.jsx +50 -59
- package/client/components/terminal-info/terminal-info.styl +6 -49
- package/client/components/text-editor/open-text-editor.js +30 -0
- package/client/components/text-editor/text-editor-entry.jsx +17 -6
- package/client/components/text-editor/text-editor.jsx +14 -0
- package/client/components/triggers/trigger-editor.jsx +246 -0
- package/client/components/triggers/trigger-form.jsx +200 -0
- package/client/components/triggers/trigger-session-modal.jsx +199 -0
- package/client/components/triggers/trigger-setting-list.jsx +89 -0
- package/client/components/triggers/trigger-transport.jsx +60 -0
- package/client/components/triggers/triggers-field.jsx +19 -0
- package/client/components/widgets/widgets-list.jsx +11 -1
- package/client/css/mobile.styl +29 -8
- package/client/store/common.js +12 -1
- package/client/store/init-state.js +7 -0
- package/client/store/mcp-handler.js +158 -30
- package/client/store/setting.js +9 -0
- package/client/store/store.js +2 -0
- package/client/store/sync.js +2 -0
- package/client/store/tab.js +42 -3
- package/client/store/trigger.js +118 -0
- package/client/store/watch.js +13 -5
- package/client/views/index.pug +3 -0
- package/package.json +1 -1
- package/client/components/terminal-info/activity.jsx +0 -57
- package/client/components/terminal-info/data-cols-parser.jsx +0 -54
- package/client/components/terminal-info/disk.jsx +0 -45
- package/client/components/terminal-info/format-disks.js +0 -45
- package/client/components/terminal-info/network.jsx +0 -121
- package/client/components/terminal-info/resource.jsx +0 -99
- package/client/components/terminal-info/run-cmd.jsx +0 -263
- package/client/components/terminal-info/sys-info.jsx +0 -56
- package/client/components/terminal-info/up.jsx +0 -17
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* eslint-disable no-control-regex */
|
|
2
|
+
const OSC = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g
|
|
3
|
+
const CSI = /\x1b\[[0-?]*[ -/]*[@-~]/g
|
|
4
|
+
const OTHER = /\x1b[@-Z\\-_]/g
|
|
5
|
+
|
|
6
|
+
export const stripAnsi = (s) => {
|
|
7
|
+
if (!s) {
|
|
8
|
+
return ''
|
|
9
|
+
}
|
|
10
|
+
return s.replace(OSC, '').replace(CSI, '').replace(OTHER, '')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// \r not followed by \n is treated as line break (progress bar scenes)
|
|
14
|
+
export const normalizeCR = (s) => {
|
|
15
|
+
if (!s) {
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
18
|
+
return s.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|
19
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Declarative auto-responder engine (ZOC ZocRespond style).
|
|
2
|
+
// Watches a decoded text stream, matches enabled trigger rules and
|
|
3
|
+
// fires their send actions. Trigger rules shape:
|
|
4
|
+
// {
|
|
5
|
+
// id, name, enabled,
|
|
6
|
+
// match: { type: 'text' | 'regex', value, caseSensitive },
|
|
7
|
+
// action: { type: 'send', value },
|
|
8
|
+
// sendEnter, mode: 'repeat' | 'once' | 'cooldown', cooldownMs
|
|
9
|
+
// }
|
|
10
|
+
import { stripAnsi, normalizeCR } from './strip-ansi.js'
|
|
11
|
+
import { escapeRegExp, expandControlChars } from './keys.js'
|
|
12
|
+
|
|
13
|
+
function buildRegExp (rule) {
|
|
14
|
+
const { type = 'text', value = '', caseSensitive = false } = rule.match || {}
|
|
15
|
+
if (!value) {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
if (type === 'regex') {
|
|
20
|
+
return new RegExp(value, caseSensitive ? '' : 'i')
|
|
21
|
+
}
|
|
22
|
+
return new RegExp(escapeRegExp(value), caseSensitive ? '' : 'i')
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validateTriggers (list) {
|
|
29
|
+
const errors = []
|
|
30
|
+
if (!Array.isArray(list)) {
|
|
31
|
+
return ['triggers must be an array']
|
|
32
|
+
}
|
|
33
|
+
list.forEach((t, i) => {
|
|
34
|
+
if (!t || typeof t !== 'object') {
|
|
35
|
+
errors.push(`#${i}: must be an object`)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
if (!t.match || !t.match.value) {
|
|
39
|
+
errors.push(`#${i} (${t.name || 'unnamed'}): match.value required`)
|
|
40
|
+
} else if (t.match.type === 'regex') {
|
|
41
|
+
try {
|
|
42
|
+
// eslint-disable-next-line no-new
|
|
43
|
+
new RegExp(t.match.value)
|
|
44
|
+
} catch (e) {
|
|
45
|
+
errors.push(`#${i} (${t.name || 'unnamed'}): invalid regex: ${e.message}`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (t.mode && !['repeat', 'once', 'cooldown'].includes(t.mode)) {
|
|
49
|
+
errors.push(`#${i} (${t.name || 'unnamed'}): bad mode ${t.mode}`)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
return errors
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default class TriggerEngine {
|
|
56
|
+
constructor ({ send, maxBuffer = 65536, onFire = null, now = Date.now } = {}) {
|
|
57
|
+
this.send = send
|
|
58
|
+
this.maxBuffer = maxBuffer
|
|
59
|
+
this.onFire = onFire
|
|
60
|
+
this.now = now
|
|
61
|
+
this.buf = ''
|
|
62
|
+
this.base = 0
|
|
63
|
+
this.rules = []
|
|
64
|
+
this.reCache = new Map()
|
|
65
|
+
this.ruleState = new Map()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setTriggers (rules) {
|
|
69
|
+
const nextRules = Array.isArray(rules) ? rules.filter(Boolean) : []
|
|
70
|
+
const streamEnd = this.base + this.buf.length
|
|
71
|
+
const nextState = new Map()
|
|
72
|
+
nextRules.forEach((rule, index) => {
|
|
73
|
+
const key = this._getRuleKey(rule, index)
|
|
74
|
+
const signature = this._getRuleSignature(rule)
|
|
75
|
+
const previous = this.ruleState.get(key)
|
|
76
|
+
nextState.set(key, previous?.signature === signature
|
|
77
|
+
? previous
|
|
78
|
+
: {
|
|
79
|
+
signature,
|
|
80
|
+
startAt: streamEnd,
|
|
81
|
+
seenEnd: streamEnd,
|
|
82
|
+
consumedEnd: streamEnd,
|
|
83
|
+
lastFire: 0,
|
|
84
|
+
firedOnce: false
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
this.rules = nextRules
|
|
88
|
+
this.ruleState = nextState
|
|
89
|
+
this.reCache.clear()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
resetOnce (id = null) {
|
|
93
|
+
for (const [key, state] of this.ruleState) {
|
|
94
|
+
if (id == null || key === id) {
|
|
95
|
+
state.firedOnce = false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_getRuleKey (rule, index) {
|
|
101
|
+
return rule.id || `#${index}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_getRuleSignature (rule) {
|
|
105
|
+
return JSON.stringify([
|
|
106
|
+
rule.enabled,
|
|
107
|
+
rule.match?.type,
|
|
108
|
+
rule.match?.value,
|
|
109
|
+
rule.match?.caseSensitive,
|
|
110
|
+
rule.action?.type,
|
|
111
|
+
rule.action?.value,
|
|
112
|
+
rule.sendEnter,
|
|
113
|
+
rule.mode,
|
|
114
|
+
rule.cooldownMs
|
|
115
|
+
])
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_getRe (rule) {
|
|
119
|
+
const key = rule.id + '#' + (rule.match?.type || 'text') + '#' + (rule.match?.value || '') + '#' + (rule.match?.caseSensitive ? 's' : 'i')
|
|
120
|
+
if (!this.reCache.has(key)) {
|
|
121
|
+
this.reCache.set(key, buildRegExp(rule))
|
|
122
|
+
}
|
|
123
|
+
return this.reCache.get(key)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
push (raw) {
|
|
127
|
+
if (!raw) {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
const str = normalizeCR(stripAnsi(raw))
|
|
131
|
+
if (!str) {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
this.buf += str
|
|
135
|
+
if (this.buf.length > this.maxBuffer) {
|
|
136
|
+
const over = this.buf.length - this.maxBuffer
|
|
137
|
+
this.buf = this.buf.slice(over)
|
|
138
|
+
this.base += over
|
|
139
|
+
}
|
|
140
|
+
this._scan()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_scan () {
|
|
144
|
+
const now = this.now()
|
|
145
|
+
const streamEnd = this.base + this.buf.length
|
|
146
|
+
this.rules.forEach((rule, index) => {
|
|
147
|
+
if (!rule || rule.enabled === false) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
const state = this.ruleState.get(this._getRuleKey(rule, index))
|
|
151
|
+
if (!state) {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
const mode = rule.mode || 'cooldown'
|
|
155
|
+
if (mode === 'once' && state.firedOnce) {
|
|
156
|
+
state.seenEnd = streamEnd
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const re = this._getRe(rule)
|
|
160
|
+
if (!re) {
|
|
161
|
+
state.seenEnd = streamEnd
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
const gre = new RegExp(re.source, re.flags.includes('i') ? 'gi' : 'g')
|
|
165
|
+
const previousEnd = state.seenEnd
|
|
166
|
+
// Literal matches only need enough overlap to bridge the latest chunk.
|
|
167
|
+
// Regexes can have arbitrary width, so scan the retained window and use
|
|
168
|
+
// absolute stream positions below to ignore matches already observed.
|
|
169
|
+
if (rule.match?.type !== 'regex') {
|
|
170
|
+
const overlap = Math.max(0, (rule.match?.value || '').length - 1)
|
|
171
|
+
gre.lastIndex = Math.max(0, previousEnd - this.base - overlap)
|
|
172
|
+
}
|
|
173
|
+
let m
|
|
174
|
+
while ((m = gre.exec(this.buf)) !== null) {
|
|
175
|
+
const matchStart = this.base + m.index
|
|
176
|
+
const matchEnd = matchStart + m[0].length
|
|
177
|
+
const isNew = matchEnd > previousEnd &&
|
|
178
|
+
matchStart >= state.startAt &&
|
|
179
|
+
matchStart >= state.consumedEnd
|
|
180
|
+
const cooldown = rule.cooldownMs == null ? 500 : rule.cooldownMs
|
|
181
|
+
const canFire = mode !== 'cooldown' || now - state.lastFire >= cooldown
|
|
182
|
+
if (isNew) {
|
|
183
|
+
// Consume the occurrence even when cooldown suppresses its action.
|
|
184
|
+
// This prevents a greedy regex rooted in old output from growing
|
|
185
|
+
// into later chunks and being mistaken for a fresh match.
|
|
186
|
+
state.consumedEnd = Math.max(state.consumedEnd, matchEnd)
|
|
187
|
+
if (canFire) {
|
|
188
|
+
this._fire(rule, m[0], state, now)
|
|
189
|
+
if (mode === 'once') {
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (m[0].length === 0) {
|
|
195
|
+
gre.lastIndex++
|
|
196
|
+
}
|
|
197
|
+
if (gre.lastIndex > this.buf.length) {
|
|
198
|
+
break
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
state.seenEnd = streamEnd
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_fire (rule, matched, state, now) {
|
|
206
|
+
state.lastFire = now
|
|
207
|
+
if ((rule.mode || 'cooldown') === 'once') {
|
|
208
|
+
state.firedOnce = true
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const action = rule.action || { type: 'send', value: '' }
|
|
212
|
+
if (action.type === 'send') {
|
|
213
|
+
const text = expandControlChars(action.value || '')
|
|
214
|
+
// default: append \r unless text already ends with \r or \n,
|
|
215
|
+
// unless the user explicitly turned sendEnter off
|
|
216
|
+
const enter = rule.sendEnter !== false
|
|
217
|
+
const payload = enter && !/[\r\n]$/.test(text) ? text + '\r' : text
|
|
218
|
+
this.send?.(payload, { rule, matched })
|
|
219
|
+
}
|
|
220
|
+
this.onFire?.({ rule, matched, kind: action.type || 'send' })
|
|
221
|
+
} catch (e) {
|
|
222
|
+
console.error('[trigger-engine] fire failed', e)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
dispose () {
|
|
227
|
+
this.buf = ''
|
|
228
|
+
this.base = 0
|
|
229
|
+
this.rules = []
|
|
230
|
+
this.reCache.clear()
|
|
231
|
+
this.ruleState.clear()
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Built-in trigger presets users can pick from.
|
|
2
|
+
// send values support \n \t \r \\ \xHH and ^X caret notation.
|
|
3
|
+
export const triggerActionTypes = [
|
|
4
|
+
{ value: 'send', label: 'Send text' },
|
|
5
|
+
{ value: 'notify', label: 'Notify only' }
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
export const triggerMatchTypes = [
|
|
9
|
+
{ value: 'text', label: 'Text' },
|
|
10
|
+
{ value: 'regex', label: 'RegExp' }
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
export const triggerModes = [
|
|
14
|
+
{ value: 'cooldown', label: 'Every new match + cooldown' },
|
|
15
|
+
{ value: 'repeat', label: 'Every new match' },
|
|
16
|
+
{ value: 'once', label: 'Once per session' }
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export const triggerPresets = [
|
|
20
|
+
{
|
|
21
|
+
name: 'Cisco pager: --More--',
|
|
22
|
+
match: { type: 'text', value: '--More--', caseSensitive: false },
|
|
23
|
+
action: { type: 'send', value: ' ' },
|
|
24
|
+
sendEnter: false,
|
|
25
|
+
mode: 'cooldown',
|
|
26
|
+
cooldownMs: 500
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'Pager: Press any key to continue',
|
|
30
|
+
match: { type: 'regex', value: 'press any key|press .* to continue|按任意键', caseSensitive: false },
|
|
31
|
+
action: { type: 'send', value: ' ' },
|
|
32
|
+
sendEnter: false,
|
|
33
|
+
mode: 'cooldown',
|
|
34
|
+
cooldownMs: 800
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'Confirm: Are you sure? [y/n]',
|
|
38
|
+
match: { type: 'regex', value: 'are you sure.*\\[y/n\\]|confirm.*\\(y/n\\)', caseSensitive: false },
|
|
39
|
+
action: { type: 'send', value: 'y' },
|
|
40
|
+
sendEnter: true,
|
|
41
|
+
mode: 'cooldown',
|
|
42
|
+
cooldownMs: 1000
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'Overwrite confirm: Overwrite? (y/n)',
|
|
46
|
+
match: { type: 'regex', value: 'overwrite.*\\(y/n\\)|是否覆盖', caseSensitive: false },
|
|
47
|
+
action: { type: 'send', value: 'y' },
|
|
48
|
+
sendEnter: true,
|
|
49
|
+
mode: 'cooldown',
|
|
50
|
+
cooldownMs: 1000
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'sudo password prompt → notify',
|
|
54
|
+
match: { type: 'regex', value: '\\[sudo\\]\\s*password', caseSensitive: false },
|
|
55
|
+
action: { type: 'notify', value: '' },
|
|
56
|
+
sendEnter: false,
|
|
57
|
+
mode: 'cooldown',
|
|
58
|
+
cooldownMs: 5000
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'SSH new host key → accept once',
|
|
62
|
+
match: { type: 'regex', value: 'are you sure you want to continue connecting', caseSensitive: false },
|
|
63
|
+
action: { type: 'send', value: 'yes' },
|
|
64
|
+
sendEnter: true,
|
|
65
|
+
mode: 'once',
|
|
66
|
+
cooldownMs: 0
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'Telnet login: auto username',
|
|
70
|
+
match: { type: 'regex', value: 'login[: ]*$|username[: ]*$', caseSensitive: false },
|
|
71
|
+
action: { type: 'send', value: '' },
|
|
72
|
+
sendEnter: true,
|
|
73
|
+
mode: 'cooldown',
|
|
74
|
+
cooldownMs: 2000
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'Connection closed → notify',
|
|
78
|
+
match: { type: 'regex', value: 'connection (closed|reset|refused)|lost connection', caseSensitive: false },
|
|
79
|
+
action: { type: 'notify', value: '' },
|
|
80
|
+
sendEnter: false,
|
|
81
|
+
mode: 'cooldown',
|
|
82
|
+
cooldownMs: 5000
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
export function buildTriggerFromPreset (preset, uid) {
|
|
87
|
+
return {
|
|
88
|
+
id: uid(),
|
|
89
|
+
name: preset.name,
|
|
90
|
+
enabled: true,
|
|
91
|
+
match: { ...preset.match },
|
|
92
|
+
action: { ...preset.action },
|
|
93
|
+
sendEnter: preset.sendEnter !== false,
|
|
94
|
+
mode: preset.mode || 'cooldown',
|
|
95
|
+
cooldownMs: preset.cooldownMs == null ? 500 : preset.cooldownMs
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildEmptyTrigger (uid) {
|
|
100
|
+
return {
|
|
101
|
+
id: uid(),
|
|
102
|
+
name: '',
|
|
103
|
+
enabled: true,
|
|
104
|
+
match: { type: 'text', value: '', caseSensitive: false },
|
|
105
|
+
action: { type: 'send', value: '' },
|
|
106
|
+
sendEnter: true,
|
|
107
|
+
mode: 'cooldown',
|
|
108
|
+
cooldownMs: 500
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -34,6 +34,25 @@ export class CommandTrackerAddon {
|
|
|
34
34
|
// Event callbacks for shell integration events
|
|
35
35
|
this._onCommandExecuted = null // Called when OSC 633;E is received
|
|
36
36
|
this._onCwdChanged = null // Called when OSC 633;P;Cwd= is received
|
|
37
|
+
// Multi-subscriber "a fresh prompt was drawn" signal. Used to pace
|
|
38
|
+
// scripted input: OSC 633;A means the previous command has finished and
|
|
39
|
+
// the shell is ready for the next one.
|
|
40
|
+
this._promptListeners = new Set()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Subscribe to "prompt started" (OSC 633;A).
|
|
45
|
+
* @param {function} callback
|
|
46
|
+
* @returns {function} unsubscribe
|
|
47
|
+
*/
|
|
48
|
+
onPrompt (callback) {
|
|
49
|
+
if (typeof callback !== 'function') {
|
|
50
|
+
return () => {}
|
|
51
|
+
}
|
|
52
|
+
this._promptListeners.add(callback)
|
|
53
|
+
return () => {
|
|
54
|
+
this._promptListeners.delete(callback)
|
|
55
|
+
}
|
|
37
56
|
}
|
|
38
57
|
|
|
39
58
|
/**
|
|
@@ -67,6 +86,9 @@ export class CommandTrackerAddon {
|
|
|
67
86
|
|
|
68
87
|
dispose () {
|
|
69
88
|
this.terminal = null
|
|
89
|
+
this._promptListeners.clear()
|
|
90
|
+
this._onCommandExecuted = null
|
|
91
|
+
this._onCwdChanged = null
|
|
70
92
|
if (this._disposables) {
|
|
71
93
|
this._disposables.forEach(d => d.dispose())
|
|
72
94
|
this._disposables.length = 0
|
|
@@ -86,11 +108,19 @@ export class CommandTrackerAddon {
|
|
|
86
108
|
const args = data.length > 1 ? data.substring(2) : '' // Skip "X;" part
|
|
87
109
|
|
|
88
110
|
switch (command) {
|
|
89
|
-
case 'A': // Prompt started
|
|
111
|
+
case 'A': { // Prompt started — the previous command is done
|
|
90
112
|
this.shellIntegrationActive = true
|
|
91
113
|
// Reset current command when new prompt appears
|
|
92
114
|
this.currentCommand = ''
|
|
115
|
+
for (const cb of this._promptListeners) {
|
|
116
|
+
try {
|
|
117
|
+
cb()
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.error('[CommandTracker] prompt listener failed', e)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
93
122
|
return true
|
|
123
|
+
}
|
|
94
124
|
|
|
95
125
|
case 'B': // Command input started (after prompt)
|
|
96
126
|
return true
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal mixins.
|
|
3
|
+
*
|
|
4
|
+
* `Term` (terminal.jsx) owns a lot of imperative xterm.js wiring. Instead of
|
|
5
|
+
* one 2000+ line file, its methods are grouped here by concern. Mixin members
|
|
6
|
+
* are plain functions (not arrow class fields) so they can live in their own
|
|
7
|
+
* module, and `applyMixins` binds them onto the instance in the constructor.
|
|
8
|
+
*
|
|
9
|
+
* Binding matters: these methods are handed out as callbacks (xterm event
|
|
10
|
+
* listeners, React props, antd menu onClick) and must therefore keep the
|
|
11
|
+
* "always bound to the instance" behaviour React class fields give.
|
|
12
|
+
*
|
|
13
|
+
* A mixin may only export functions. Per-instance mutable state would be
|
|
14
|
+
* shared by every terminal, so it stays a class field in terminal.jsx.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export function applyMixins (instance, mixins) {
|
|
18
|
+
for (const mixin of mixins) {
|
|
19
|
+
for (const name of Object.keys(mixin)) {
|
|
20
|
+
const fn = mixin[name]
|
|
21
|
+
if (typeof fn !== 'function') {
|
|
22
|
+
throw new Error(`terminal mixin member "${name}" must be a function`)
|
|
23
|
+
}
|
|
24
|
+
instance[name] = fn.bind(instance)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { debounce } from 'lodash-es'
|
|
2
|
+
import message from '../../common/message'
|
|
3
|
+
import { notification } from '../../common/notification'
|
|
4
|
+
import AttachAddon from '../attach-addon-custom.js'
|
|
5
|
+
import { createTriggerManager } from '../automation/index.js'
|
|
6
|
+
import { isWin } from '../../../common/constants.js'
|
|
7
|
+
import { isUnsafeFilename } from '../../../common/file-drop-utils.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Everything that writes to the session: the socket attach addon, encoding,
|
|
11
|
+
* quick commands / batch input, and the automation trigger manager that runs
|
|
12
|
+
* on incoming data.
|
|
13
|
+
*/
|
|
14
|
+
export const attachMixin = {
|
|
15
|
+
async initAttachAddon () {
|
|
16
|
+
this.attachAddon = new AttachAddon(
|
|
17
|
+
this.term,
|
|
18
|
+
this.socket,
|
|
19
|
+
isWin && !this.isRemote()
|
|
20
|
+
)
|
|
21
|
+
this.attachAddon.decoder = new TextDecoder(
|
|
22
|
+
this.encode || this.props.tab.encode || 'utf-8'
|
|
23
|
+
)
|
|
24
|
+
await this.attachAddon.activate(this.term)
|
|
25
|
+
if (this.osc52Addon) {
|
|
26
|
+
this.osc52Addon.setSendData(this.attachAddon._sendData.bind(this.attachAddon))
|
|
27
|
+
}
|
|
28
|
+
this.initTriggerManager()
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
switchEncoding (encode) {
|
|
32
|
+
this.encode = encode
|
|
33
|
+
this.attachAddon.decoder = new TextDecoder(encode)
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
toggleKeepalive () {
|
|
37
|
+
if (!this.attachAddon) {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
this._keepaliveEnabled = !this._keepaliveEnabled
|
|
41
|
+
this.attachAddon.setKeepalive(this._keepaliveEnabled)
|
|
42
|
+
return this._keepaliveEnabled
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
batchInput (cmd) {
|
|
46
|
+
this.attachAddon._sendData(cmd + '\r')
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
runQuickCommand (cmd, inputOnly = false) {
|
|
50
|
+
if (this.term && this.attachAddon) {
|
|
51
|
+
this.attachAddon._sendData(cmd + (inputOnly ? '' : '\r'))
|
|
52
|
+
this.term.focus()
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
cd (p) {
|
|
57
|
+
if (isUnsafeFilename(p)) {
|
|
58
|
+
return message.error('File name contains unsafe characters')
|
|
59
|
+
}
|
|
60
|
+
const isWinPath = /^[a-zA-Z]:\\/.test(p)
|
|
61
|
+
this.runQuickCommand(isWinPath ? `cd /d "${p}"` : `cd "${p}"`)
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
initTriggerManager () {
|
|
65
|
+
this.disposeTriggerManager()
|
|
66
|
+
if (!this.attachAddon) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const attachAddon = this.attachAddon
|
|
70
|
+
this.triggerManager = createTriggerManager({
|
|
71
|
+
attachAddon,
|
|
72
|
+
send: (payload) => {
|
|
73
|
+
if (payload) {
|
|
74
|
+
attachAddon._sendData(payload)
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
getTriggers: () => {
|
|
78
|
+
try {
|
|
79
|
+
return window.store.getEffectiveTriggers(this.props.tab)
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return []
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
onFire: ({ rule, matched, kind }) => {
|
|
85
|
+
if (kind === 'notify' && rule) {
|
|
86
|
+
notification.warning({
|
|
87
|
+
message: window.translate('triggers') + ': ' + (rule.name || rule.match?.value || ''),
|
|
88
|
+
description: String(matched || '').slice(-240),
|
|
89
|
+
duration: 6
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
refreshTriggers () {
|
|
97
|
+
try {
|
|
98
|
+
this.triggerManager?.refresh()
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.debug(e)
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
disposeTriggerManager () {
|
|
105
|
+
try {
|
|
106
|
+
this.triggerManager?.dispose()
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.debug(e)
|
|
109
|
+
}
|
|
110
|
+
this.triggerManager = null
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// Called by attach-addon-custom once per flushed write, so the tab badge
|
|
114
|
+
// updates without one notification per network chunk.
|
|
115
|
+
notifyOnData () {
|
|
116
|
+
if (!this._notifyOnDataDebounced) {
|
|
117
|
+
this._notifyOnDataDebounced = debounce(() => {
|
|
118
|
+
window.store.notifyTabOnData(this.props.tab.id)
|
|
119
|
+
}, 1000)
|
|
120
|
+
}
|
|
121
|
+
this._notifyOnDataDebounced()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import message from '../../common/message'
|
|
2
|
+
import ExternalLink from '../../common/external-link.jsx'
|
|
3
|
+
import { isWin } from '../../../common/constants.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reacting to config changes that cannot be handled by a re-render: they need
|
|
7
|
+
* the live xterm instance (font/renderer) or the shell integration sequence.
|
|
8
|
+
*/
|
|
9
|
+
export const configMixin = {
|
|
10
|
+
getValue (props, type, name) {
|
|
11
|
+
return type === 'glob'
|
|
12
|
+
? props.config[name]
|
|
13
|
+
: props.tab[name] || props.config[name]
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
checkConfigChange (prevProps, props) {
|
|
17
|
+
for (const k of this.terminalConfigProps) {
|
|
18
|
+
const { name, type } = k
|
|
19
|
+
const prev = this.getValue(prevProps, type, name)
|
|
20
|
+
const curr = this.getValue(props, type, name)
|
|
21
|
+
if (
|
|
22
|
+
prev !== curr
|
|
23
|
+
) {
|
|
24
|
+
this.term.options[name] = curr
|
|
25
|
+
if (['fontFamily', 'fontSize'].includes(name)) {
|
|
26
|
+
this.onResize()
|
|
27
|
+
}
|
|
28
|
+
if (name === 'fontSize') {
|
|
29
|
+
this.originalFontSize = curr
|
|
30
|
+
this.setState({ fontSizeChanged: false })
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Handle renderer type changes (dom <-> webGL) by reloading the
|
|
36
|
+
// renderer and refreshing the theme so the background color is
|
|
37
|
+
// correct for the new renderer.
|
|
38
|
+
if (
|
|
39
|
+
prevProps.config.rendererType !== props.config.rendererType &&
|
|
40
|
+
this.term
|
|
41
|
+
) {
|
|
42
|
+
this.reloadWebglRenderer('renderer type change')
|
|
43
|
+
.then(() => this.applyTerminalTheme())
|
|
44
|
+
.catch(e => console.error('renderer type change failed', e))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check for shell integration related config changes
|
|
48
|
+
const prevShowSuggestions = prevProps.config.showCmdSuggestions
|
|
49
|
+
const currShowSuggestions = props.config.showCmdSuggestions
|
|
50
|
+
const prevSftpFollow = prevProps.sftpPathFollowSsh
|
|
51
|
+
const currSftpFollow = props.sftpPathFollowSsh
|
|
52
|
+
const prevRestoreTerminal = prevProps.config.restoreTerminalSessionOnReload
|
|
53
|
+
const currRestoreTerminal = props.config.restoreTerminalSessionOnReload
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
(!prevShowSuggestions && currShowSuggestions) ||
|
|
57
|
+
(!prevSftpFollow && currSftpFollow) ||
|
|
58
|
+
(!prevRestoreTerminal && currRestoreTerminal)
|
|
59
|
+
) {
|
|
60
|
+
// Config was toggled to true, try to inject shell integration if not already done
|
|
61
|
+
if (this.startupQueue.canInjectShellIntegration() && !this.startupQueue.shellInjected) {
|
|
62
|
+
this.startupQueue.enqueueShellIntegration(currSftpFollow)
|
|
63
|
+
} else if (this.startupQueue.shellInjected && currSftpFollow) {
|
|
64
|
+
this.getCwd()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
!prevSftpFollow &&
|
|
69
|
+
currSftpFollow &&
|
|
70
|
+
this.isLocal() &&
|
|
71
|
+
isWin
|
|
72
|
+
) {
|
|
73
|
+
return this.warnSftpFollowUnsupported()
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
warnSftpFollowUnsupported () {
|
|
78
|
+
message.warning(
|
|
79
|
+
<span>
|
|
80
|
+
Fish shell/windows shell is not supported for SFTP follow SSH path feature. See: <ExternalLink to='https://github.com/electerm/electerm/wiki/Warning-about-sftp-follow-ssh-path-function'>wiki</ExternalLink>
|
|
81
|
+
</span>
|
|
82
|
+
, 7)
|
|
83
|
+
}
|
|
84
|
+
}
|