@electerm/electerm-react 5.3.15 → 5.5.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 (149) hide show
  1. package/README.md +246 -12
  2. package/client/common/choose-save-folder.js +21 -2
  3. package/client/common/constants.js +23 -6
  4. package/client/common/create-title.jsx +6 -0
  5. package/client/common/default-setting.js +14 -0
  6. package/client/common/init-setting-item.js +5 -0
  7. package/client/common/platform.js +24 -0
  8. package/client/components/ai/agent-tool-call-card.jsx +10 -2
  9. package/client/components/ai/agent.js +4 -1
  10. package/client/components/ai/ai-attachments.js +87 -0
  11. package/client/components/ai/ai-chat-empty.jsx +43 -0
  12. package/client/components/ai/ai-chat-entry.jsx +13 -4
  13. package/client/components/ai/ai-chat-history-item.jsx +28 -4
  14. package/client/components/ai/ai-chat-history.jsx +2 -1
  15. package/client/components/ai/ai-chat.jsx +104 -13
  16. package/client/components/ai/ai-config-props.js +4 -130
  17. package/client/components/ai/ai-config.jsx +135 -15
  18. package/client/components/ai/ai-presets.js +173 -0
  19. package/client/components/ai/ai.styl +83 -0
  20. package/client/components/bg/css-overwrite.jsx +11 -0
  21. package/client/components/bookmark-form/bookmark-schema.js +4 -0
  22. package/client/components/bookmark-form/common/fields.jsx +3 -0
  23. package/client/components/bookmark-form/config/common-fields.js +12 -0
  24. package/client/components/bookmark-form/config/local.js +3 -2
  25. package/client/components/bookmark-form/config/serial.js +3 -2
  26. package/client/components/bookmark-form/config/ssh.js +2 -1
  27. package/client/components/bookmark-form/config/telnet.js +3 -2
  28. package/client/components/bookmark-form/index.jsx +10 -8
  29. package/client/components/common/item-filter.jsx +76 -0
  30. package/client/components/common/item-filter.styl +54 -0
  31. package/client/components/common/lazy-boundary.jsx +34 -0
  32. package/client/components/common/opacity.jsx +3 -1
  33. package/client/components/footer/footer-entry.jsx +35 -5
  34. package/client/components/footer/footer.styl +8 -1
  35. package/client/components/layout/layout.jsx +23 -31
  36. package/client/components/layout/pixed.js +3 -0
  37. package/client/components/main/main.jsx +80 -28
  38. package/client/components/main/term-fullscreen.styl +9 -3
  39. package/client/components/quick-commands/qm.styl +3 -2
  40. package/client/components/rdp/file-transfer.js +4 -4
  41. package/client/components/remote-monitor/monitor-details.jsx +458 -0
  42. package/client/components/remote-monitor/monitor-details.styl +87 -0
  43. package/client/components/remote-monitor/monitor-model.js +764 -0
  44. package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
  45. package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
  46. package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
  47. package/client/components/remote-monitor/session-monitor.js +361 -0
  48. package/client/components/remote-monitor/use-monitor-details.js +10 -0
  49. package/client/components/remote-monitor/visibility.js +18 -0
  50. package/client/components/session/session-control.jsx +29 -2
  51. package/client/components/session/session.jsx +40 -18
  52. package/client/components/session/sessions.jsx +5 -2
  53. package/client/components/setting-panel/col.jsx +32 -6
  54. package/client/components/setting-panel/list.styl +12 -0
  55. package/client/components/setting-panel/setting-common.jsx +25 -56
  56. package/client/components/setting-panel/setting-mobile-breadcrumb.jsx +29 -0
  57. package/client/components/setting-panel/setting-modal.jsx +64 -44
  58. package/client/components/setting-panel/setting-terminal.jsx +7 -2
  59. package/client/components/setting-panel/setting-wrap.jsx +0 -3
  60. package/client/components/setting-panel/setting-wrap.styl +80 -11
  61. package/client/components/setting-panel/setting.styl +1 -1
  62. package/client/components/setting-panel/tab-triggers.jsx +71 -0
  63. package/client/components/sftp/file-compare-modal.jsx +6 -3
  64. package/client/components/sftp/file-item.jsx +5 -3
  65. package/client/components/sftp/owner-list.js +11 -2
  66. package/client/components/side-panel-r/side-panel-r.jsx +2 -2
  67. package/client/components/sidebar/index.jsx +28 -21
  68. package/client/components/sidebar/left-sidebar-icons.jsx +1 -1
  69. package/client/components/sidebar/sidebar.styl +16 -0
  70. package/client/components/sys-menu/menu-btn.jsx +30 -1
  71. package/client/components/tabs/index.jsx +2 -2
  72. package/client/components/tabs/tab-title.jsx +3 -2
  73. package/client/components/tabs/tab.jsx +5 -1
  74. package/client/components/tabs/tabs.styl +4 -0
  75. package/client/components/terminal/attach-addon-custom.js +295 -49
  76. package/client/components/terminal/automation/index.js +64 -0
  77. package/client/components/terminal/automation/keys.js +49 -0
  78. package/client/components/terminal/automation/stream-matcher.js +82 -0
  79. package/client/components/terminal/automation/strip-ansi.js +19 -0
  80. package/client/components/terminal/automation/trigger-engine.js +233 -0
  81. package/client/components/terminal/automation/trigger-presets.js +110 -0
  82. package/client/components/terminal/command-tracker-addon.js +31 -1
  83. package/client/components/terminal/mixins/index.js +27 -0
  84. package/client/components/terminal/mixins/term-attach.js +123 -0
  85. package/client/components/terminal/mixins/term-config.jsx +84 -0
  86. package/client/components/terminal/mixins/term-content.js +88 -0
  87. package/client/components/terminal/mixins/term-context-menu.jsx +291 -0
  88. package/client/components/terminal/mixins/term-file-drop.js +149 -0
  89. package/client/components/terminal/mixins/term-init.js +122 -0
  90. package/client/components/terminal/mixins/term-log.jsx +96 -0
  91. package/client/components/terminal/mixins/term-resize.js +87 -0
  92. package/client/components/terminal/mixins/term-socket.js +433 -0
  93. package/client/components/terminal/mixins/term-suggestions.js +224 -0
  94. package/client/components/terminal/mixins/term-theme.js +190 -0
  95. package/client/components/terminal/mixins/term-touch.js +136 -0
  96. package/client/components/terminal/shell-detect.js +37 -0
  97. package/client/components/terminal/shell.js +0 -28
  98. package/client/components/terminal/shortcut-bar-entry.jsx +32 -27
  99. package/client/components/terminal/shortcut-bar.jsx +4 -6
  100. package/client/components/terminal/shortcut-bar.styl +4 -0
  101. package/client/components/terminal/ssh-reload-state.js +56 -0
  102. package/client/components/terminal/startup-queue.js +379 -0
  103. package/client/components/terminal/terminal-interactive.jsx +32 -0
  104. package/client/components/terminal/terminal.jsx +102 -2007
  105. package/client/components/terminal/transfer-client-base.js +5 -29
  106. package/client/components/terminal/trzsz-client.js +1 -3
  107. package/client/components/terminal/xmodem-client.js +1 -3
  108. package/client/components/terminal/xterm-loader.js +7 -0
  109. package/client/components/terminal/zmodem-client.js +1 -3
  110. package/client/components/terminal-info/base.jsx +18 -70
  111. package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
  112. package/client/components/terminal-info/terminal-info.jsx +50 -59
  113. package/client/components/terminal-info/terminal-info.styl +6 -49
  114. package/client/components/text-editor/open-text-editor.js +30 -0
  115. package/client/components/text-editor/text-editor-entry.jsx +17 -6
  116. package/client/components/text-editor/text-editor.jsx +14 -0
  117. package/client/components/theme/theme-form.jsx +22 -0
  118. package/client/components/tree-list/bookmark-transport.jsx +7 -31
  119. package/client/components/tree-list/tree-list.styl +13 -0
  120. package/client/components/triggers/trigger-editor.jsx +246 -0
  121. package/client/components/triggers/trigger-form.jsx +200 -0
  122. package/client/components/triggers/trigger-session-modal.jsx +199 -0
  123. package/client/components/triggers/trigger-setting-list.jsx +89 -0
  124. package/client/components/triggers/trigger-transport.jsx +60 -0
  125. package/client/components/triggers/triggers-field.jsx +19 -0
  126. package/client/components/widgets/widgets-list.jsx +11 -1
  127. package/client/css/mobile.styl +9 -1
  128. package/client/store/common.js +12 -1
  129. package/client/store/init-state.js +14 -0
  130. package/client/store/mcp-handler.js +158 -30
  131. package/client/store/setting.js +20 -0
  132. package/client/store/sidebar.js +12 -0
  133. package/client/store/store.js +2 -0
  134. package/client/store/sync.js +45 -9
  135. package/client/store/system-menu.js +2 -0
  136. package/client/store/tab.js +41 -3
  137. package/client/store/trigger.js +118 -0
  138. package/client/store/watch.js +13 -5
  139. package/client/views/index.pug +3 -0
  140. package/package.json +1 -1
  141. package/client/components/terminal-info/activity.jsx +0 -57
  142. package/client/components/terminal-info/data-cols-parser.jsx +0 -54
  143. package/client/components/terminal-info/disk.jsx +0 -45
  144. package/client/components/terminal-info/format-disks.js +0 -45
  145. package/client/components/terminal-info/network.jsx +0 -121
  146. package/client/components/terminal-info/resource.jsx +0 -99
  147. package/client/components/terminal-info/run-cmd.jsx +0 -263
  148. package/client/components/terminal-info/sys-info.jsx +0 -56
  149. package/client/components/terminal-info/up.jsx +0 -17
@@ -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
+ }