@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,224 @@
1
+ import { debounce } from 'lodash-es'
2
+ import { refsStatic } from '../../common/ref.js'
3
+
4
+ /**
5
+ * Command suggestions: reading the current input straight from the terminal
6
+ * buffer, positioning the dropdown at the cursor, and the password prompt
7
+ * variant triggered by the shell integration OSC sequences.
8
+ */
9
+ export const suggestionsMixin = {
10
+ getCursorPosition () {
11
+ if (!this.term) return null
12
+
13
+ // Get the active buffer and cursor position
14
+ const buffer = this.term.buffer.active
15
+ const cursorRow = buffer.cursorY
16
+ const cursorCol = buffer.cursorX
17
+
18
+ // Get dimensions from term element
19
+ const termElement = this.term.element
20
+ if (!termElement) return null
21
+
22
+ // Get the exact position of the terminal element
23
+ const termRect = termElement.getBoundingClientRect()
24
+
25
+ // Calculate cell dimensions
26
+ const cellWidth = termRect.width / this.term.cols
27
+ const cellHeight = termRect.height / this.term.rows
28
+
29
+ // Calculate absolute position relative to terminal element
30
+ const left = Math.floor(termRect.left + (cursorCol * cellWidth))
31
+ const top = Math.floor(termRect.top + ((cursorRow + 1) * cellHeight))
32
+
33
+ return {
34
+ cellWidth,
35
+ cellHeight,
36
+ left,
37
+ top
38
+ }
39
+ },
40
+
41
+ closeSuggestions () {
42
+ refsStatic
43
+ .get('terminal-suggestions')
44
+ ?.closeSuggestions()
45
+ },
46
+
47
+ openSuggestions (cursorPos, data) {
48
+ refsStatic
49
+ .get('terminal-suggestions')
50
+ ?.openSuggestions(cursorPos, data)
51
+ },
52
+
53
+ /**
54
+ * Read current input directly from terminal buffer
55
+ * This is more reliable than tracking character-by-character
56
+ */
57
+ getCurrentInput () {
58
+ if (!this.term) return ''
59
+
60
+ const buffer = this.term.buffer.active
61
+ const cursorY = buffer.cursorY
62
+ const cursorX = buffer.cursorX
63
+
64
+ // Get the current line from buffer (baseY + cursorY gives absolute position)
65
+ const absoluteY = buffer.baseY + cursorY
66
+ const line = buffer.getLine(absoluteY)
67
+ if (!line) return ''
68
+
69
+ // Get text from start of line up to cursor position
70
+ const lineText = line.translateToString(true, 0, cursorX)
71
+
72
+ // Try to extract command after prompt
73
+ // Common prompt endings with trailing space
74
+ const promptEndings = ['$ ', '# ', '> ', '% ', '] ', ') ']
75
+
76
+ let commandStart = 0
77
+ for (const ending of promptEndings) {
78
+ const idx = lineText.lastIndexOf(ending)
79
+ if (idx !== -1 && idx + ending.length > commandStart) {
80
+ commandStart = idx + ending.length
81
+ }
82
+ }
83
+
84
+ return lineText.slice(commandStart)
85
+ },
86
+
87
+ setCurrentInput (value) {
88
+ this.currentInput = value
89
+ },
90
+
91
+ /**
92
+ * Handle special input events for command history tracking
93
+ * The actual input reading is done via getCurrentInput from buffer
94
+ */
95
+ handleInputEvent (d) {
96
+ // Handle Enter - add command to history
97
+ if (d === '\r' || d === '\n') {
98
+ const currentCmd = this.getCurrentInput()
99
+ if (currentCmd && currentCmd.trim() && this.shouldUseManualHistory()) {
100
+ window.store.addCmdHistory(currentCmd.trim())
101
+ }
102
+ if (currentCmd && currentCmd.trim() === 'exit') {
103
+ this.userTypeExit = true
104
+ }
105
+ this.closeSuggestions()
106
+ }
107
+ },
108
+
109
+ onData (d) {
110
+ this.handleInputEvent(d)
111
+ // Skip normal suggestion logic when in password mode
112
+ const suggestions = refsStatic.get('terminal-suggestions')
113
+ if (suggestions?.state?.passwordMode) {
114
+ if (d === '\r' || d === '\n') {
115
+ this.closeSuggestions()
116
+ }
117
+ return
118
+ }
119
+ if (this.props.config.showCmdSuggestions) {
120
+ if (d === '\r' || d === '\n') {
121
+ this.closeSuggestions()
122
+ return
123
+ }
124
+ // Debounce the suggestion opening to avoid expensive work
125
+ // (buffer read + getBoundingClientRect + React re-render) on every keystroke
126
+ this._debouncedOpenSuggestions()
127
+ } else {
128
+ this.closeSuggestions()
129
+ }
130
+ },
131
+
132
+ /**
133
+ * Called by AttachAddonCustom after data is written to the terminal buffer.
134
+ * This fires after server echo arrives, so getCurrentInput() reflects the
135
+ * latest state. We trigger a debounced suggestion refresh so the dropdown
136
+ * updates correctly after backspace, delete, and other edits that rely on
137
+ * server-side echo to update the buffer.
138
+ */
139
+ onTerminalWrite () {
140
+ if (!this.props.config.showCmdSuggestions) {
141
+ return
142
+ }
143
+ const suggestions = refsStatic.get('terminal-suggestions')
144
+ if (suggestions?.state?.showSuggestions && !suggestions?.state?.passwordMode) {
145
+ this._debouncedOpenSuggestions()
146
+ }
147
+ },
148
+
149
+ // One debounced runner per terminal, so a keystroke does not trigger a
150
+ // buffer read + layout + re-render for every character.
151
+ _debouncedOpenSuggestions () {
152
+ if (!this._openSuggestionsDebounced) {
153
+ this._openSuggestionsDebounced = debounce(() => {
154
+ const data = this.getCurrentInput()
155
+ if (!data) {
156
+ this.closeSuggestions()
157
+ return
158
+ }
159
+ const cursorPos = this.getCursorPosition()
160
+ this.openSuggestions(cursorPos, data)
161
+ }, 80)
162
+ }
163
+ this._openSuggestionsDebounced()
164
+ },
165
+
166
+ onPasswordPromptDetected () {
167
+ window.store.notifyTabPasswordPrompt(this.props.tab.id)
168
+ if (!this.props.config.showCmdSuggestions) {
169
+ return
170
+ }
171
+ const cursorPos = this.getCursorPosition()
172
+ if (cursorPos) {
173
+ refsStatic
174
+ .get('terminal-suggestions')
175
+ ?.openPasswordSuggestions(cursorPos)
176
+ }
177
+ },
178
+
179
+ onPasswordPromptCancelled () {
180
+ window.store.clearTabPasswordPrompt(this.props.tab.id)
181
+ const suggestions = refsStatic.get('terminal-suggestions')
182
+ if (suggestions?.state?.passwordMode) {
183
+ suggestions.closeSuggestions()
184
+ }
185
+ },
186
+
187
+ getCmd () {
188
+ return this.cmdAddon.getCurrentCommand()
189
+ },
190
+
191
+ getCwd () {
192
+ // Use shell integration CWD if available
193
+ if (this.cmdAddon && this.cmdAddon.hasShellIntegration()) {
194
+ const cwd = this.cmdAddon.getCwd()
195
+ if (cwd) {
196
+ this.setCwd(cwd)
197
+ return cwd
198
+ }
199
+ }
200
+ // Fallback: no longer needed with shell integration
201
+ return ''
202
+ },
203
+
204
+ setCwd (cwd) {
205
+ this.props.setCwd(cwd, this.state.id)
206
+ },
207
+
208
+ shouldUseManualHistory () {
209
+ return !this.cmdAddon || !this.cmdAddon.hasShellIntegration()
210
+ },
211
+
212
+ parse (rawText) {
213
+ let result = ''
214
+ const len = rawText.length
215
+ for (let i = 0; i < len; i++) {
216
+ if (rawText[i] === '\b') {
217
+ result = result.slice(0, -1)
218
+ } else {
219
+ result += rawText[i]
220
+ }
221
+ }
222
+ return result
223
+ }
224
+ }
@@ -0,0 +1,190 @@
1
+ import deepCopy from 'json-deep-copy'
2
+ import { loadWebglAddon } from '../xterm-loader.js'
3
+ import { rendererTypes } from '../../../common/constants.js'
4
+ import {
5
+ createRendererThemeConfig,
6
+ handleTerminalColorQuery,
7
+ blendSelectionOverBackground
8
+ } from '../terminal-color-query.mjs'
9
+
10
+ /**
11
+ * Renderer (DOM / WebGL) and theming, including the OSC 10/11 colour query
12
+ * handlers that let shells ask the terminal for its background colour.
13
+ */
14
+ export const themeMixin = {
15
+ async loadRenderer (term, config) {
16
+ // xterm 6.x: only the built-in DOM renderer and the WebGL addon exist
17
+ // (the canvas renderer addon was removed in 6.x). 'dom' = no addon loaded
18
+ // (built-in DOM renderer). Legacy 'canvas' settings fall back to DOM.
19
+ if (config.rendererType === rendererTypes.webGL) {
20
+ try {
21
+ const WebglAddon = await loadWebglAddon()
22
+ const webglAddon = new WebglAddon()
23
+ this.webglAddon = webglAddon
24
+ // On macOS native fullscreen the GPU/WebGL context can be lost when
25
+ // the window migrates across Spaces. Without a listener xterm keeps
26
+ // drawing into a dead context and every terminal goes black while the
27
+ // rest of the UI stays alive. Rebuild the addon to recover.
28
+ this.webglContextLossDisposable = webglAddon.onContextLoss(this.handleWebglContextLoss)
29
+ term.loadAddon(webglAddon)
30
+ } catch (e) {
31
+ console.error('render with webgl failed, fallback to dom renderer')
32
+ console.error(e)
33
+ // built-in DOM renderer is used (no addon loaded)
34
+ this.webglAddon = null
35
+ }
36
+ }
37
+ },
38
+
39
+ reloadWebglRenderer (reason = 'reload') {
40
+ console.warn(`webgl renderer ${reason}, rebuilding`)
41
+ try {
42
+ this.webglContextLossDisposable?.dispose?.()
43
+ this.webglContextLossDisposable = null
44
+ } catch (e) {
45
+ console.error(e)
46
+ }
47
+ try {
48
+ this.webglAddon?.dispose?.()
49
+ } catch (e) {
50
+ console.error(e)
51
+ }
52
+ this.webglAddon = null
53
+ const { term } = this
54
+ const { config } = this.props
55
+ return this.loadRenderer(term, config)
56
+ .then(() => {
57
+ term.refresh(0, term.rows - 1)
58
+ })
59
+ .catch(e => {
60
+ console.error(`webgl renderer ${reason} failed`, e)
61
+ })
62
+ },
63
+
64
+ handleWebglContextLoss (webglAddon = this.webglAddon) {
65
+ if (this.webglRecovering || !webglAddon) {
66
+ return
67
+ }
68
+ this.webglRecovering = true
69
+ this.reloadWebglRenderer('context loss')
70
+ .finally(() => {
71
+ this.webglRecovering = false
72
+ })
73
+ },
74
+
75
+ disposeTerminalColorQueryHandlers () {
76
+ this.terminalColorQueryDisposables.forEach(disposable => disposable?.dispose?.())
77
+ this.terminalColorQueryDisposables.length = 0
78
+ },
79
+
80
+ getVisibleTerminalBackground () {
81
+ const uiThemeConfig = window.store?.getUiThemeConfig?.() || {}
82
+ // The store value (uiThemeConfig.main) is always immediately up-to-date
83
+ // when the theme changes, because it reads directly from store.config.theme.
84
+ // The CSS --main variable lags behind because UiTheme's useEffect runs
85
+ // asynchronously after componentDidUpdate. So we prioritise the store
86
+ // value, and only fall back to CSS (for custom-CSS edge cases) or the
87
+ // terminal theme background (last resort).
88
+ const root = document.documentElement
89
+ const cssMain = root && window.getComputedStyle
90
+ ? window.getComputedStyle(root).getPropertyValue('--main').trim()
91
+ : ''
92
+ return uiThemeConfig.main || cssMain || this.props.themeConfig.background
93
+ },
94
+
95
+ getVisibleTerminalForeground () {
96
+ const uiThemeConfig = window.store?.getUiThemeConfig?.() || {}
97
+ return uiThemeConfig.text
98
+ },
99
+
100
+ registerTerminalColorQueryHandlers (term, themeConfig = {}) {
101
+ this.disposeTerminalColorQueryHandlers()
102
+ if (!term?.parser?.registerOscHandler) {
103
+ return
104
+ }
105
+ const background = this.getVisibleTerminalBackground()
106
+ const foregroundFallback = this.getVisibleTerminalForeground()
107
+ this.terminalColorQueryDisposables.push(
108
+ term.parser.registerOscHandler(10, data => {
109
+ return handleTerminalColorQuery(term, 10, themeConfig.foreground, foregroundFallback, data)
110
+ }),
111
+ term.parser.registerOscHandler(11, data => {
112
+ return handleTerminalColorQuery(term, 11, background, themeConfig.background, data)
113
+ })
114
+ )
115
+ },
116
+
117
+ getRendererThemeConfig (themeConfig = this.props.themeConfig) {
118
+ return createRendererThemeConfig(
119
+ deepCopy(themeConfig),
120
+ this.props.config.rendererType,
121
+ this.getVisibleTerminalBackground()
122
+ )
123
+ },
124
+
125
+ /**
126
+ * Apply the current renderer theme to the terminal and trigger a repaint.
127
+ * When `deferred` is true (WebGL mode), a second repaint is scheduled on
128
+ * the next animation frame so the theme picks up CSS --main changes that
129
+ * UiTheme's useEffect applies asynchronously after componentDidUpdate.
130
+ * The optional `term` parameter is used during initTerminal, where
131
+ * this.term hasn't been assigned yet.
132
+ */
133
+ applyTerminalTheme (deferred = false, term = this.term) {
134
+ if (!term || this.onClose) {
135
+ return
136
+ }
137
+ term.options.theme = this.getRendererThemeConfig(this.props.themeConfig)
138
+ this.fixSelectionColors(term)
139
+ term.refresh(0, term.rows - 1)
140
+ if (deferred && this.props.config.rendererType === rendererTypes.webGL) {
141
+ window.cancelAnimationFrame(this.timers.themeRaf)
142
+ this.timers.themeRaf = window.requestAnimationFrame(() => {
143
+ if (!this.term || this.onClose) {
144
+ return
145
+ }
146
+ this.term.options.theme = this.getRendererThemeConfig(this.props.themeConfig)
147
+ this.fixSelectionColors(this.term)
148
+ this.term.refresh(0, this.term.rows - 1)
149
+ })
150
+ }
151
+ },
152
+
153
+ /**
154
+ * xterm computes its DOM selection colour as blend(theme.background,
155
+ * selectionBackground). The terminal background is forced transparent
156
+ * (rgba(0,0,0,0)) so electerm's own CSS background / image shows through,
157
+ * which makes the blend run over transparent-black. Recompute the selection
158
+ * colours over the real visible background, from the *configured* theme
159
+ * colours — xterm's internal selectionBackgroundTransparent forces opaque
160
+ * colours down to 0.3 alpha (xterm#2737), which would make every selection
161
+ * translucent even when the user configured an opaque one.
162
+ */
163
+ fixSelectionColors (term) {
164
+ const themeConfig = this.props.themeConfig || {}
165
+ const themeService = term?._core?._themeService
166
+ if (!themeService?.modifyColors) {
167
+ return
168
+ }
169
+ const visibleBackground = this.getVisibleTerminalBackground()
170
+ // xterm's DEFAULT_SELECTION, used when the theme omits a selection color
171
+ const selectionFallback = 'rgba(255, 255, 255, 0.3)'
172
+ const inactiveFallback = themeConfig.selectionBackground || selectionFallback
173
+ themeService.modifyColors((c) => {
174
+ const active = blendSelectionOverBackground(
175
+ visibleBackground,
176
+ themeConfig.selectionBackground || selectionFallback
177
+ )
178
+ if (active) {
179
+ c.selectionBackgroundOpaque = active
180
+ }
181
+ const inactive = blendSelectionOverBackground(
182
+ visibleBackground,
183
+ themeConfig.selectionInactiveBackground || inactiveFallback
184
+ )
185
+ if (inactive) {
186
+ c.selectionInactiveBackgroundOpaque = inactive
187
+ }
188
+ })
189
+ }
190
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Mobile touch support.
3
+ * On touch devices, long-press should (1) select the word under the finger
4
+ * and (2) open the context menu — mirroring desktop right-click behaviour.
5
+ * xterm.js only uses touch events for scrolling, so we add explicit
6
+ * long-press detection here.
7
+ */
8
+ export const touchMixin = {
9
+ onTouchStart (e) {
10
+ if (e.touches.length !== 1) {
11
+ return
12
+ }
13
+ const touch = e.touches[0]
14
+ this.touchStartPos = {
15
+ clientX: touch.clientX,
16
+ clientY: touch.clientY,
17
+ target: e.currentTarget
18
+ }
19
+ this.longPressFired = false
20
+ clearTimeout(this.longPressTimer)
21
+ this.longPressTimer = setTimeout(() => {
22
+ this.handleLongPress()
23
+ }, this.longPressThreshold)
24
+ },
25
+
26
+ onTouchMove (e) {
27
+ if (!this.touchStartPos) {
28
+ return
29
+ }
30
+ const touch = e.touches[0]
31
+ const dx = touch.clientX - this.touchStartPos.clientX
32
+ const dy = touch.clientY - this.touchStartPos.clientY
33
+ if (Math.sqrt(dx * dx + dy * dy) > this.longPressMoveTolerance) {
34
+ clearTimeout(this.longPressTimer)
35
+ this.longPressTimer = null
36
+ this.touchStartPos = null
37
+ }
38
+ },
39
+
40
+ onTouchEnd () {
41
+ const wasTap = this.touchStartPos && !this.longPressFired
42
+ clearTimeout(this.longPressTimer)
43
+ this.longPressTimer = null
44
+ this.touchStartPos = null
45
+ // xterm's own touch (Gesture) handler calls preventDefault()/stopPropagation()
46
+ // on touchstart/touchend, which suppresses the synthesised mousedown xterm
47
+ // relies on to focus its hidden helper textarea. As a result a tap never
48
+ // focuses the terminal on touch devices, so the soft keyboard never opens
49
+ // and you cannot type. Focus explicitly on a clean tap (not a long-press,
50
+ // not a scroll) so mobile input works. The focus() runs synchronously inside
51
+ // this user-gesture handler, so iOS/Android will show the keyboard.
52
+ if (wasTap && this.term) {
53
+ this.term.focus()
54
+ }
55
+ },
56
+
57
+ handleLongPress () {
58
+ if (!this.touchStartPos || this.state.loading) {
59
+ return
60
+ }
61
+ this.longPressFired = true
62
+ const { clientX, clientY, target } = this.touchStartPos
63
+
64
+ // Select the word at the touch position (same as desktop right-click word
65
+ // select) so the user can immediately copy or act on it.
66
+ this.selectWordAt(clientX, clientY)
67
+
68
+ // Respect pasteWhenContextMenu: when enabled, long-press pastes instead
69
+ // of showing the menu (same as desktop right-click).
70
+ if (this.props.config.pasteWhenContextMenu) {
71
+ this.onPaste()
72
+ return
73
+ }
74
+
75
+ // Dispatch a synthetic contextmenu event so antd Dropdown's contextMenu
76
+ // trigger opens the menu at the exact finger position.
77
+ const event = new MouseEvent('contextmenu', {
78
+ bubbles: true,
79
+ cancelable: true,
80
+ clientX,
81
+ clientY
82
+ })
83
+ target.dispatchEvent(event)
84
+ },
85
+
86
+ selectWordAt (clientX, clientY) {
87
+ if (!this.term) {
88
+ return
89
+ }
90
+ const termElement = this.term.element
91
+ if (!termElement) {
92
+ return
93
+ }
94
+ const rect = termElement.getBoundingClientRect()
95
+ const x = clientX - rect.left
96
+ const y = clientY - rect.top
97
+ if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
98
+ return
99
+ }
100
+
101
+ const cellWidth = rect.width / this.term.cols
102
+ const cellHeight = rect.height / this.term.rows
103
+ const col = Math.floor(x / cellWidth)
104
+ const row = Math.floor(y / cellHeight)
105
+
106
+ const buffer = this.term.buffer.active
107
+ const line = buffer.getLine(row)
108
+ if (!line) {
109
+ return
110
+ }
111
+
112
+ const text = line.translateToString(true)
113
+ const wordSeparator = this.props.config.terminalWordSeparator ||
114
+ ' ./\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~ ?'
115
+
116
+ // If the touched cell is empty or a separator, nothing to select
117
+ if (col >= text.length || wordSeparator.includes(text[col])) {
118
+ return
119
+ }
120
+
121
+ // Find word start
122
+ let start = col
123
+ while (start > 0 && !wordSeparator.includes(text[start - 1])) {
124
+ start--
125
+ }
126
+ // Find word end
127
+ let end = col
128
+ while (end < text.length && !wordSeparator.includes(text[end])) {
129
+ end++
130
+ }
131
+
132
+ if (end > start) {
133
+ this.term.select(start, row, end - start)
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Remote shell detection.
3
+ *
4
+ * Kept out of shell.js on purpose: it needs the network (runCmd), which drags
5
+ * the whole app runtime into the import graph. shell.js stays pure so the
6
+ * startup queue can be imported (and unit tested) on its own.
7
+ */
8
+ import { runCmd } from './terminal-apis.js'
9
+ import { detectShellType } from './shell.js'
10
+
11
+ export async function detectRemoteShell (pid) {
12
+ // SSH exec runs under the account shell, so prefer the configured shell path
13
+ // instead of probing for any shell binary installed on the host.
14
+ const cmd = 'printf "%s\n" "$SHELL"'
15
+
16
+ // { silent: true } so this best-effort probe does not emit a transport-level
17
+ // fetch warning; a single, clearer warning is logged below if it fails.
18
+ const r = await runCmd(pid, cmd, { silent: true })
19
+ .catch((err) => {
20
+ // Non-fatal: the interactive shell already opened, so the terminal keeps
21
+ // working. We just can't inject OSC 633 shell integration for command
22
+ // tracking. This commonly happens when the server limits concurrent
23
+ // sessions (sshd MaxSessions) or the account uses a forced/restricted
24
+ // command, which rejects the auxiliary exec channel with
25
+ // "(SSH) Channel open failure: open failed".
26
+ console.warn('detectRemoteShell: exec channel rejected by server, shell integration disabled, falling back to sh —', err?.message || err)
27
+ return 'sh'
28
+ })
29
+
30
+ const shell = r.trim().toLowerCase()
31
+
32
+ if (!shell) {
33
+ return 'sh'
34
+ }
35
+
36
+ return detectShellType(shell)
37
+ }
@@ -15,7 +15,6 @@
15
15
  */
16
16
 
17
17
  /* eslint-disable no-template-curly-in-string, no-useless-escape */
18
- import { runCmd } from './terminal-apis.js'
19
18
 
20
19
  /**
21
20
  * Get inline shell integration command for bash (one-liner format)
@@ -153,30 +152,3 @@ export function getShellIntegrationCommand (shellType = 'bash') {
153
152
  const cmd = getInlineShellIntegration(shellType)
154
153
  return wrapSilent(cmd, shellType)
155
154
  }
156
- export async function detectRemoteShell (pid) {
157
- // SSH exec runs under the account shell, so prefer the configured shell path
158
- // instead of probing for any shell binary installed on the host.
159
- const cmd = 'printf "%s\n" "$SHELL"'
160
-
161
- // { silent: true } so this best-effort probe does not emit a transport-level
162
- // fetch warning; a single, clearer warning is logged below if it fails.
163
- const r = await runCmd(pid, cmd, { silent: true })
164
- .catch((err) => {
165
- // Non-fatal: the interactive shell already opened, so the terminal keeps
166
- // working. We just can't inject OSC 633 shell integration for command
167
- // tracking. This commonly happens when the server limits concurrent
168
- // sessions (sshd MaxSessions) or the account uses a forced/restricted
169
- // command, which rejects the auxiliary exec channel with
170
- // "(SSH) Channel open failure: open failed".
171
- console.warn('detectRemoteShell: exec channel rejected by server, shell integration disabled, falling back to sh —', err?.message || err)
172
- return 'sh'
173
- })
174
-
175
- const shell = r.trim().toLowerCase()
176
-
177
- if (!shell) {
178
- return 'sh'
179
- }
180
-
181
- return detectShellType(shell)
182
- }