@electerm/electerm-react 3.15.175 → 3.15.186
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/common/bookmark-schemas.js +2 -0
- package/client/common/drop-position.js +59 -0
- package/client/components/bookmark-form/bookmark-schema.js +2 -0
- package/client/components/bookmark-form/common/connection-hopping.jsx +33 -7
- package/client/components/bookmark-form/common/ssh-tunnel-form.jsx +25 -7
- package/client/components/bookmark-form/common/ssh-tunnels.jsx +1 -1
- package/client/components/bookmark-form/config/serial.js +14 -0
- package/client/components/main/term-fullscreen.styl +5 -0
- package/client/components/quick-commands/on-drop.js +6 -1
- package/client/components/quick-commands/quick-commands-box.jsx +18 -2
- package/client/components/quick-commands/quick-commands-list-form.jsx +15 -4
- package/client/components/quick-commands/quick-commands-list.jsx +18 -2
- package/client/components/session/session-control.jsx +331 -0
- package/client/components/session/session-control.styl +38 -0
- package/client/components/session/session.jsx +41 -246
- package/client/components/session/session.styl +1 -0
- package/client/components/sftp/address-bookmark-item.jsx +15 -2
- package/client/components/sftp/code-compare-utils.js +260 -0
- package/client/components/sftp/code-compare.jsx +183 -0
- package/client/components/sftp/code-compare.styl +13 -0
- package/client/components/sftp/file-compare-modal.jsx +87 -42
- package/client/components/shortcuts/shortcut-handler.js +20 -0
- package/client/components/sidebar/transport-ui.jsx +21 -14
- package/client/components/tabs/index.jsx +18 -3
- package/client/components/tabs/tab.jsx +27 -8
- package/client/components/tabs/tabs.styl +8 -0
- package/client/components/terminal/attach-addon-custom.js +147 -36
- package/client/components/terminal/highlight-addon.js +26 -8
- package/client/components/terminal/term-search.jsx +1 -1
- package/client/components/terminal/terminal.jsx +193 -2
- package/client/components/terminal/terminal.styl +0 -2
- package/client/components/terminal-info/run-cmd.jsx +1 -1
- package/client/components/tree-list/tree-list.jsx +57 -39
- package/client/css/basic.styl +14 -0
- package/client/css/mobile.styl +31 -9
- package/client/store/item.js +10 -6
- package/package.json +1 -1
|
@@ -214,6 +214,26 @@ export function shortcutExtend (Cls) {
|
|
|
214
214
|
this.term.buffer.active.type === 'alternate'
|
|
215
215
|
) {
|
|
216
216
|
return true
|
|
217
|
+
} else if (
|
|
218
|
+
(key === 'ArrowLeft' || key === 'ArrowRight') &&
|
|
219
|
+
type === 'keydown' &&
|
|
220
|
+
altKey &&
|
|
221
|
+
!ctrlKey &&
|
|
222
|
+
!shiftKey &&
|
|
223
|
+
!metaKey
|
|
224
|
+
) {
|
|
225
|
+
// Restore xterm 5.5.0 behavior: convert Alt/Option + Left/Right into
|
|
226
|
+
// readline-compatible word-movement sequences.
|
|
227
|
+
// xterm 6.x dropped the platform-specific override in evaluateKeyboardEvent,
|
|
228
|
+
// which now sends CSI modifier sequences (e.g. ESC[1;3D) that old shells
|
|
229
|
+
// do not bind to backward-word / forward-word, causing trailing chars
|
|
230
|
+
// ("D", ";3D") to leak as literal input.
|
|
231
|
+
const seq = key === 'ArrowLeft'
|
|
232
|
+
? (isMacJs ? '\x1bb' : '\x1b[1;5D')
|
|
233
|
+
: (isMacJs ? '\x1bf' : '\x1b[1;5C')
|
|
234
|
+
sendInputData(this, seq)
|
|
235
|
+
this.term?.scrollToBottom()
|
|
236
|
+
return false
|
|
217
237
|
}
|
|
218
238
|
|
|
219
239
|
if (
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { action } from 'manate'
|
|
14
14
|
import { addClass, removeClass } from '../../common/class'
|
|
15
15
|
import { refsStatic } from '../common/ref'
|
|
16
|
+
import { isDropAfterHalf, setDropIndicator, clearDropIndicator } from '../../common/drop-position'
|
|
16
17
|
import './transfer.styl'
|
|
17
18
|
|
|
18
19
|
const e = window.translate
|
|
@@ -70,28 +71,28 @@ export default function Transporter (props) {
|
|
|
70
71
|
addClass(dom.current, onDragCls)
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
function
|
|
74
|
+
function updateDropIndicator (e) {
|
|
74
75
|
clearCls()
|
|
75
|
-
|
|
76
|
+
setDropIndicator(dom.current, isDropAfterHalf(e, dom.current))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function onDragEnter (e) {
|
|
80
|
+
e.preventDefault()
|
|
81
|
+
updateDropIndicator(e)
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
function onDragExit () {
|
|
79
85
|
// debug('ondragexit')
|
|
80
|
-
// let {target} = e
|
|
81
|
-
// removeClass(target, 'sftp-dragover')
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
function onDragLeave (
|
|
85
|
-
|
|
86
|
-
const { target } = e
|
|
87
|
-
removeClass(target, onDragOverCls)
|
|
88
|
+
function onDragLeave () {
|
|
89
|
+
clearDropIndicator(dom.current)
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
function onDragOver (e) {
|
|
91
93
|
// debug('ondragover')
|
|
92
|
-
// debug(e.target)
|
|
93
|
-
// removeClass(dom.current, 'sftp-dragover')
|
|
94
94
|
e.preventDefault()
|
|
95
|
+
updateDropIndicator(e)
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
function onDragStart (e) {
|
|
@@ -103,6 +104,7 @@ export default function Transporter (props) {
|
|
|
103
104
|
|
|
104
105
|
function onDrop (e) {
|
|
105
106
|
e.preventDefault()
|
|
107
|
+
clearDropIndicator(dom.current)
|
|
106
108
|
const { target } = e
|
|
107
109
|
if (!target) {
|
|
108
110
|
return
|
|
@@ -126,15 +128,19 @@ export default function Transporter (props) {
|
|
|
126
128
|
|
|
127
129
|
const arr = window.store.fileTransfers
|
|
128
130
|
const indexFrom = arr.findIndex(t => t.id === fromId)
|
|
129
|
-
|
|
131
|
+
const indexDrop = arr.findIndex(t => t.id === dropId)
|
|
132
|
+
// bottom half of the target row => insert after it, so the last
|
|
133
|
+
// transfer can receive a drop (append to the end of the queue).
|
|
134
|
+
const insertAfter = isDropAfterHalf(e, onDropTab)
|
|
130
135
|
if (indexFrom >= 0 && indexDrop >= 0) {
|
|
131
136
|
// Reorder tabs and update batch
|
|
132
137
|
action(function () {
|
|
133
138
|
const [tr] = arr.splice(indexFrom, 1)
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
let insertIndex = insertAfter ? indexDrop + 1 : indexDrop
|
|
140
|
+
if (indexFrom < insertIndex) {
|
|
141
|
+
insertIndex = insertIndex - 1
|
|
136
142
|
}
|
|
137
|
-
arr.splice(
|
|
143
|
+
arr.splice(insertIndex, 0, tr)
|
|
138
144
|
})()
|
|
139
145
|
}
|
|
140
146
|
}
|
|
@@ -142,6 +148,7 @@ export default function Transporter (props) {
|
|
|
142
148
|
function onDragEnd (e) {
|
|
143
149
|
removeClass(dom.current, onDragCls)
|
|
144
150
|
clearCls()
|
|
151
|
+
clearDropIndicator(dom.current)
|
|
145
152
|
e && e.dataTransfer && e.dataTransfer.clearData()
|
|
146
153
|
}
|
|
147
154
|
const isTransfer = typeTo !== typeFrom
|
|
@@ -386,6 +386,23 @@ export default class Tabs extends Component {
|
|
|
386
386
|
)
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
+
renderMobileTabsInner (currentTabEl) {
|
|
390
|
+
const inner = (
|
|
391
|
+
<div className='tabs-inner mobile-tabs-inner'>
|
|
392
|
+
{currentTabEl}
|
|
393
|
+
</div>
|
|
394
|
+
)
|
|
395
|
+
const { config } = this.props
|
|
396
|
+
if (config.useSystemTitleBar) {
|
|
397
|
+
return inner
|
|
398
|
+
}
|
|
399
|
+
return (
|
|
400
|
+
<AppDrag>
|
|
401
|
+
{inner}
|
|
402
|
+
</AppDrag>
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
389
406
|
renderMobileTabs () {
|
|
390
407
|
const { tabs = [], currentBatchTabId } = this.props
|
|
391
408
|
const currentTab = tabs.find(t => t.id === currentBatchTabId)
|
|
@@ -410,9 +427,7 @@ export default class Tabs extends Component {
|
|
|
410
427
|
: null
|
|
411
428
|
return (
|
|
412
429
|
<div className='tabs mobile-tabs' ref={this.tabsRef}>
|
|
413
|
-
|
|
414
|
-
{currentTabEl}
|
|
415
|
-
</div>
|
|
430
|
+
{this.renderMobileTabsInner(currentTabEl)}
|
|
416
431
|
{this.renderMobileTabsExtra(items)}
|
|
417
432
|
{this.renderWindowControl()}
|
|
418
433
|
</div>
|
|
@@ -25,10 +25,12 @@ import isDark from '../../common/is-color-dark'
|
|
|
25
25
|
import { action } from 'manate'
|
|
26
26
|
import iconsMap from '../sys-menu/icons-map.jsx'
|
|
27
27
|
import { shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
|
|
28
|
+
import { isDropAfterHalf } from '../../common/drop-position'
|
|
28
29
|
|
|
29
30
|
const e = window.translate
|
|
30
31
|
const onDragCls = 'ondrag-tab'
|
|
31
32
|
const onDragOverCls = 'dragover-tab'
|
|
33
|
+
const onDragOverAfterCls = 'dragover-tab-after'
|
|
32
34
|
|
|
33
35
|
class Tab extends Component {
|
|
34
36
|
constructor (props) {
|
|
@@ -110,6 +112,9 @@ class Tab extends Component {
|
|
|
110
112
|
document.querySelectorAll('.' + onDragOverCls).forEach((d) => {
|
|
111
113
|
removeClass(d, onDragOverCls)
|
|
112
114
|
})
|
|
115
|
+
document.querySelectorAll('.' + onDragOverAfterCls).forEach((d) => {
|
|
116
|
+
removeClass(d, onDragOverAfterCls)
|
|
117
|
+
})
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
handleClick = (e) => {
|
|
@@ -120,9 +125,8 @@ class Tab extends Component {
|
|
|
120
125
|
addClass(this.tabRef.current, onDragCls)
|
|
121
126
|
}
|
|
122
127
|
|
|
123
|
-
onDragEnter = () => {
|
|
124
|
-
this.
|
|
125
|
-
addClass(this.tabRef.current, onDragOverCls)
|
|
128
|
+
onDragEnter = (e) => {
|
|
129
|
+
this.applyDragOverCls(e)
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
onDragExit = () => {
|
|
@@ -135,13 +139,22 @@ class Tab extends Component {
|
|
|
135
139
|
// debug('ondragleave')
|
|
136
140
|
const { target } = e
|
|
137
141
|
removeClass(target, onDragOverCls)
|
|
142
|
+
removeClass(target, onDragOverAfterCls)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Pick the before/after indicator based on which half of the tab
|
|
146
|
+
// the pointer is in, so the drop line shows where it will actually go.
|
|
147
|
+
applyDragOverCls = e => {
|
|
148
|
+
this.clearCls()
|
|
149
|
+
const insertAfter = isDropAfterHalf(e, this.tabRef.current, true)
|
|
150
|
+
addClass(this.tabRef.current, insertAfter ? onDragOverAfterCls : onDragOverCls)
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
onDragOver = e => {
|
|
141
154
|
// debug('ondragover')
|
|
142
155
|
// debug(e.target)
|
|
143
|
-
// removeClass(this.tabRef.current, 'sftp-dragover')
|
|
144
156
|
e.preventDefault()
|
|
157
|
+
this.applyDragOverCls(e)
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
onDragStart = e => {
|
|
@@ -178,7 +191,9 @@ class Tab extends Component {
|
|
|
178
191
|
const { id } = fromTab
|
|
179
192
|
const storeTabs = window.store.tabs
|
|
180
193
|
const indexFrom = storeTabs.findIndex(t => t.id === id)
|
|
181
|
-
|
|
194
|
+
const indexDrop = storeTabs.findIndex(t => t.id === dropId)
|
|
195
|
+
// tabs are laid out horizontally: right half of the target => insert after
|
|
196
|
+
const insertAfter = isDropAfterHalf(e, onDropTab, true)
|
|
182
197
|
|
|
183
198
|
if (indexFrom >= 0 && indexDrop >= 0) {
|
|
184
199
|
const targetTab = storeTabs[indexDrop]
|
|
@@ -197,10 +212,13 @@ class Tab extends Component {
|
|
|
197
212
|
action(function () {
|
|
198
213
|
const [tab] = storeTabs.splice(indexFrom, 1)
|
|
199
214
|
tab.batch = targetTab.batch // Update the batch to match target tab's batch
|
|
200
|
-
|
|
201
|
-
|
|
215
|
+
// insertAfter lets a tab land after the target, so the last tab
|
|
216
|
+
// can receive a drop (insertIndex can reach storeTabs.length).
|
|
217
|
+
let insertIndex = insertAfter ? indexDrop + 1 : indexDrop
|
|
218
|
+
if (indexFrom < insertIndex) {
|
|
219
|
+
insertIndex = insertIndex - 1
|
|
202
220
|
}
|
|
203
|
-
storeTabs.splice(
|
|
221
|
+
storeTabs.splice(insertIndex, 0, tab)
|
|
204
222
|
})()
|
|
205
223
|
window.store.focus()
|
|
206
224
|
}
|
|
@@ -216,6 +234,7 @@ class Tab extends Component {
|
|
|
216
234
|
|
|
217
235
|
onDragEnd = e => {
|
|
218
236
|
removeClass(this.tabRef.current, onDragCls)
|
|
237
|
+
removeClass(this.tabRef.current, onDragOverAfterCls)
|
|
219
238
|
this.clearCls()
|
|
220
239
|
e && e.dataTransfer && e.dataTransfer.clearData()
|
|
221
240
|
}
|
|
@@ -68,6 +68,14 @@
|
|
|
68
68
|
width 1px
|
|
69
69
|
border 1px dashed var(--text-dark)
|
|
70
70
|
height 36px
|
|
71
|
+
&.dragover-tab-after::after
|
|
72
|
+
position absolute
|
|
73
|
+
content ''
|
|
74
|
+
right -2px
|
|
75
|
+
top 0
|
|
76
|
+
width 1px
|
|
77
|
+
border 1px dashed var(--text-dark)
|
|
78
|
+
height 36px
|
|
71
79
|
@keyframes blink
|
|
72
80
|
0%
|
|
73
81
|
background-color #e0e0e0
|
|
@@ -23,6 +23,32 @@ export default class AttachAddonCustom {
|
|
|
23
23
|
this._passwordPromptDetected = false
|
|
24
24
|
this._pendingEchoCheck = null
|
|
25
25
|
this._echoCheckTimer = null
|
|
26
|
+
// Write coalescing: terminal output is buffered and flushed on a short
|
|
27
|
+
// timer so that heavy bursts (e.g. `yum install` download progress, which
|
|
28
|
+
// rewrites the same line with \r + clear-line thousands of times per
|
|
29
|
+
// second) collapse into a few term.write() calls per frame instead of
|
|
30
|
+
// blocking the main thread on every WebSocket message.
|
|
31
|
+
this._writeBuffer = []
|
|
32
|
+
this._bufferBytes = 0
|
|
33
|
+
this._flushScheduled = false
|
|
34
|
+
this._flushTimer = null
|
|
35
|
+
// Coalescing window. Output is flushed at most once per interval.
|
|
36
|
+
// 16ms ~= one frame; low enough that interactive echo feels instant,
|
|
37
|
+
// high enough to merge a multi-MB/s flood into ~60 writes/s.
|
|
38
|
+
this._flushIntervalMs = 16
|
|
39
|
+
// Time of the last actual flush. Drives the interactive fast path in
|
|
40
|
+
// _enqueueWrite: output arriving after an idle gap (keystroke echo,
|
|
41
|
+
// command result) is flushed immediately instead of paying the
|
|
42
|
+
// coalescing delay. 0 = the first chunk ever flushes immediately.
|
|
43
|
+
this._lastFlushTime = 0
|
|
44
|
+
// Soft cap on buffered-but-unflushed bytes. Under a sustained flood the
|
|
45
|
+
// producer outruns the renderer; once pending output exceeds this we drop
|
|
46
|
+
// the OLDEST data (preserving the newest, visible tail and the line
|
|
47
|
+
// currently being rewritten). Normal interactive output is many orders of
|
|
48
|
+
// magnitude smaller and is never dropped.
|
|
49
|
+
this._maxBufferBytes = 256 * 1024
|
|
50
|
+
this._droppedBytes = 0
|
|
51
|
+
this._droppedWarned = false
|
|
26
52
|
}
|
|
27
53
|
|
|
28
54
|
_initBase = async () => {
|
|
@@ -182,27 +208,11 @@ export default class AttachAddonCustom {
|
|
|
182
208
|
}
|
|
183
209
|
}
|
|
184
210
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
str = decoder.decode(data instanceof ArrayBuffer ? data : new Uint8Array(data))
|
|
191
|
-
} catch (e) {
|
|
192
|
-
str = ''
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (this.checkForShellIntegration(str)) {
|
|
197
|
-
this.onShellIntegrationDetected()
|
|
198
|
-
return
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
this.suppressedData.push(data)
|
|
202
|
-
return
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Password prompt detection on output
|
|
211
|
+
// Decode once, synchronously. The previous implementation routed binary
|
|
212
|
+
// chunks through `new Blob()` + `FileReader.readAsArrayBuffer()` and decoded
|
|
213
|
+
// in an async `onRead` callback; that async round-trip (plus a per-chunk
|
|
214
|
+
// Blob allocation) showed up in the profile and fragmented main-thread work.
|
|
215
|
+
// We already hold a TextDecoder, so decode inline like the suppression path.
|
|
206
216
|
let str = data
|
|
207
217
|
if (typeof data !== 'string') {
|
|
208
218
|
try {
|
|
@@ -213,6 +223,18 @@ export default class AttachAddonCustom {
|
|
|
213
223
|
str = ''
|
|
214
224
|
}
|
|
215
225
|
}
|
|
226
|
+
|
|
227
|
+
if (this.outputSuppressed) {
|
|
228
|
+
if (this.checkForShellIntegration(str)) {
|
|
229
|
+
this.onShellIntegrationDetected()
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
this.suppressedData.push(data)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Prompt/echo detection runs per chunk (cheap) so password prompts and
|
|
237
|
+
// shell integration are still detected promptly.
|
|
216
238
|
this._handleEchoDetection(str)
|
|
217
239
|
if (this._checkPasswordPrompt(str) && !this._passwordPromptDetected) {
|
|
218
240
|
this._passwordPromptDetected = true
|
|
@@ -222,27 +244,109 @@ export default class AttachAddonCustom {
|
|
|
222
244
|
}, 100)
|
|
223
245
|
}
|
|
224
246
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
247
|
+
// Coalesce the actual write (see _enqueueWrite). notifyOnData /
|
|
248
|
+
// onTerminalWrite fire once per flush instead of once per chunk.
|
|
249
|
+
this._enqueueWrite(str)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Buffer decoded output and flush it on a short timer so a burst of messages
|
|
253
|
+
// becomes a single (size-capped) term.write() call, keeping the main thread
|
|
254
|
+
// free to handle UI events and input between flushes.
|
|
255
|
+
_enqueueWrite = (str) => {
|
|
256
|
+
if (!this.term || !str) {
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
this._writeBuffer.push(str)
|
|
260
|
+
this._bufferBytes += str.length
|
|
261
|
+
if (this._bufferBytes > this._maxBufferBytes) {
|
|
262
|
+
this._dropOldestUntil()
|
|
263
|
+
}
|
|
264
|
+
// A hidden window gets its timers throttled by Chromium (Electron's
|
|
265
|
+
// backgroundThrottling defaults to true), which would stall the flush
|
|
266
|
+
// timer and force the byte cap to drop real output. Nothing is painted
|
|
267
|
+
// while hidden, so coalescing buys nothing — flush synchronously.
|
|
268
|
+
if (document.hidden) {
|
|
269
|
+
clearTimeout(this._flushTimer)
|
|
270
|
+
this._flushWrites()
|
|
232
271
|
return
|
|
233
272
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
273
|
+
// Interactive fast path: if the last flush was longer ago than the
|
|
274
|
+
// coalescing window (keystroke echo, command result after idle), flush
|
|
275
|
+
// immediately so typing adds zero latency. Only output arriving inside
|
|
276
|
+
// a burst window pays the coalescing delay — which is exactly the flood
|
|
277
|
+
// case coalescing exists for.
|
|
278
|
+
const elapsed = Date.now() - this._lastFlushTime
|
|
279
|
+
if (elapsed >= this._flushIntervalMs) {
|
|
280
|
+
clearTimeout(this._flushTimer)
|
|
281
|
+
this._flushWrites()
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
if (!this._flushScheduled) {
|
|
285
|
+
this._flushScheduled = true
|
|
286
|
+
this._flushTimer = setTimeout(this._flushWrites, this._flushIntervalMs - elapsed)
|
|
287
|
+
}
|
|
238
288
|
}
|
|
239
289
|
|
|
240
|
-
|
|
241
|
-
|
|
290
|
+
// Under heavy flood the producer outruns the renderer; once pending output
|
|
291
|
+
// exceeds the cap we drop the OLDEST chunks (preserving the newest, visible
|
|
292
|
+
// tail and the line currently being rewritten). The first kept chunk is
|
|
293
|
+
// trimmed to the next newline so we never render a half line / broken escape.
|
|
294
|
+
_dropOldestUntil = () => {
|
|
295
|
+
const buf = this._writeBuffer
|
|
296
|
+
let kept = 0
|
|
297
|
+
let cutIdx = buf.length
|
|
298
|
+
for (let i = buf.length - 1; i >= 0; i--) {
|
|
299
|
+
if (kept + buf[i].length > this._maxBufferBytes) {
|
|
300
|
+
cutIdx = i + 1
|
|
301
|
+
break
|
|
302
|
+
}
|
|
303
|
+
kept += buf[i].length
|
|
304
|
+
}
|
|
305
|
+
if (cutIdx <= 0) {
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
let dropped = 0
|
|
309
|
+
for (let i = 0; i < cutIdx; i++) {
|
|
310
|
+
dropped += buf[i].length
|
|
311
|
+
}
|
|
312
|
+
if (cutIdx < buf.length) {
|
|
313
|
+
const first = buf[cutIdx]
|
|
314
|
+
const nl = first.indexOf('\n')
|
|
315
|
+
if (nl >= 0 && nl < first.length - 1) {
|
|
316
|
+
dropped += nl + 1
|
|
317
|
+
buf[cutIdx] = first.slice(nl + 1)
|
|
318
|
+
} else {
|
|
319
|
+
dropped += buf[cutIdx].length
|
|
320
|
+
cutIdx += 1
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
this._writeBuffer = buf.slice(cutIdx)
|
|
324
|
+
this._bufferBytes -= dropped
|
|
325
|
+
this._droppedBytes += dropped
|
|
326
|
+
if (!this._droppedWarned) {
|
|
327
|
+
this._droppedWarned = true
|
|
328
|
+
console.warn('[AttachAddon] Heavy output detected; coalescing writes and dropping intermediate output to keep the UI responsive.')
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
_flushWrites = () => {
|
|
333
|
+
this._flushScheduled = false
|
|
334
|
+
this._flushTimer = null
|
|
335
|
+
const buf = this._writeBuffer
|
|
336
|
+
if (!buf.length || !this.term) {
|
|
337
|
+
this._writeBuffer = []
|
|
338
|
+
this._bufferBytes = 0
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
this._writeBuffer = []
|
|
342
|
+
this._bufferBytes = 0
|
|
343
|
+
const data = buf.length === 1 ? buf[0] : buf.join('')
|
|
242
344
|
const { term } = this
|
|
345
|
+
this._lastFlushTime = Date.now()
|
|
346
|
+
term.write(data)
|
|
347
|
+
// Notify parent that the terminal buffer has been updated (echo received),
|
|
348
|
+
// once per flush instead of once per chunk.
|
|
243
349
|
term?.parent?.notifyOnData()
|
|
244
|
-
const str = this.decoder.decode(data)
|
|
245
|
-
term?.write(str)
|
|
246
350
|
term?.parent?.onTerminalWrite?.()
|
|
247
351
|
}
|
|
248
352
|
|
|
@@ -324,6 +428,13 @@ export default class AttachAddonCustom {
|
|
|
324
428
|
this._stopKeepalive()
|
|
325
429
|
clearTimeout(this._echoCheckTimer)
|
|
326
430
|
this._echoCheckTimer = null
|
|
431
|
+
if (this._flushTimer) {
|
|
432
|
+
clearTimeout(this._flushTimer)
|
|
433
|
+
this._flushTimer = null
|
|
434
|
+
}
|
|
435
|
+
this._flushScheduled = false
|
|
436
|
+
this._writeBuffer = []
|
|
437
|
+
this._bufferBytes = 0
|
|
327
438
|
this.term = null
|
|
328
439
|
this._disposables.forEach(d => d.dispose())
|
|
329
440
|
this._disposables.length = 0
|
|
@@ -2,6 +2,18 @@ export class KeywordHighlighterAddon {
|
|
|
2
2
|
constructor (keywords) {
|
|
3
3
|
this.keywords = keywords || []
|
|
4
4
|
this.compiledPatterns = this.compilePatterns()
|
|
5
|
+
// Writes larger than this bypass keyword highlighting. Highlighting is meant
|
|
6
|
+
// for interactive command output; a large chunk is a command flood or
|
|
7
|
+
// download progress where scanning every byte (and rebuilding the result
|
|
8
|
+
// string) is pure overhead. Coupled with write coalescing in the attach
|
|
9
|
+
// addon, flood output skips this path entirely.
|
|
10
|
+
this.maxHighlightLength = 4096
|
|
11
|
+
// Compile the ANSI/OSC splitter once instead of allocating a new RegExp on
|
|
12
|
+
// every write (this was the #1 allocation in the hot path under load).
|
|
13
|
+
const ESC = String.fromCharCode(27) // \x1b
|
|
14
|
+
const BEL = String.fromCharCode(7) // \x07
|
|
15
|
+
// eslint-disable-next-line no-control-regex
|
|
16
|
+
this._ansiPattern = new RegExp('(' + ESC + '\\][^' + BEL + ESC + ']*(?:' + BEL + '|' + ESC + '\\\\)|' + ESC + '\\[\\??[0-9;]*[A-Za-z])', 'g')
|
|
5
17
|
}
|
|
6
18
|
|
|
7
19
|
// Pre-compile all regex patterns once for better performance
|
|
@@ -54,11 +66,8 @@ export class KeywordHighlighterAddon {
|
|
|
54
66
|
|
|
55
67
|
// Split text into segments: ANSI/OSC sequences vs plain text
|
|
56
68
|
// Match OSC sequences (ESC ] ... BEL or ESC ] ... ESC \) and CSI sequences (ESC [ ... letter)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const BEL = String.fromCharCode(7) // \x07
|
|
60
|
-
// eslint-disable-next-line no-control-regex
|
|
61
|
-
const ansiPattern = new RegExp('(' + ESC + '\\][^' + BEL + ESC + ']*(?:' + BEL + '|' + ESC + '\\\\)|' + ESC + '\\[\\??[0-9;]*[A-Za-z])', 'g')
|
|
69
|
+
const ansiPattern = this._ansiPattern
|
|
70
|
+
ansiPattern.lastIndex = 0
|
|
62
71
|
|
|
63
72
|
const segments = []
|
|
64
73
|
let lastIndex = 0
|
|
@@ -142,9 +151,18 @@ export class KeywordHighlighterAddon {
|
|
|
142
151
|
if (self.containsDcsSequence(data)) {
|
|
143
152
|
return self.originalWrite(data)
|
|
144
153
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
// displayRaw applies to every write regardless of size: raw mode shows
|
|
155
|
+
// escape sequences as text, so skipping escape() for large chunks would
|
|
156
|
+
// let raw ESC bytes be interpreted by xterm instead of displayed.
|
|
157
|
+
if (terminal.displayRaw) {
|
|
158
|
+
return self.originalWrite(self.escape(data))
|
|
159
|
+
}
|
|
160
|
+
// Skip keyword highlighting for large writes (command floods, download
|
|
161
|
+
// progress). Highlighting targets interactive command output.
|
|
162
|
+
if (data.length > self.maxHighlightLength) {
|
|
163
|
+
return self.originalWrite(data)
|
|
164
|
+
}
|
|
165
|
+
self.originalWrite(self.highlightKeywords(data))
|
|
148
166
|
}
|
|
149
167
|
}
|
|
150
168
|
|
|
@@ -70,7 +70,7 @@ export default class TermSearch extends PureComponent {
|
|
|
70
70
|
} = this.props
|
|
71
71
|
refs.get('term-' + activeTabId)
|
|
72
72
|
?.searchPrev(
|
|
73
|
-
searchTerm
|
|
73
|
+
typeof searchTerm === 'string' ? searchTerm : this.props.termSearch,
|
|
74
74
|
copy(termSearchOptions)
|
|
75
75
|
)
|
|
76
76
|
}
|