deckide 3.5.24 → 3.5.25
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/dist/routes/terminals.js +25 -8
- package/dist/utils/utf8.js +71 -0
- package/package.json +1 -1
package/dist/routes/terminals.js
CHANGED
|
@@ -5,7 +5,7 @@ import { TERMINAL_BUFFER_LIMIT } from '../config.js';
|
|
|
5
5
|
import { createHttpError, handleError, readJson } from '../utils/error.js';
|
|
6
6
|
import { getDefaultShell } from '../utils/shell.js';
|
|
7
7
|
import { saveTerminal, deleteTerminal as deleteTerminalFromDb } from '../utils/database.js';
|
|
8
|
-
import { alignToUtf8Start } from '../utils/utf8.js';
|
|
8
|
+
import { alignToUtf8Start, skipPartialEscapeSequence } from '../utils/utf8.js';
|
|
9
9
|
const DEFAULT_TERMINAL_TITLE = 'ターミナル';
|
|
10
10
|
const MAX_SOCKET_BUFFERED_AMOUNT = 1024 * 1024;
|
|
11
11
|
export function createTerminalRouter(db, decks, terminals) {
|
|
@@ -21,15 +21,22 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
21
21
|
if (chunk.length >= TERMINAL_BUFFER_LIMIT) {
|
|
22
22
|
let cutPos = chunk.length - TERMINAL_BUFFER_LIMIT;
|
|
23
23
|
cutPos = alignToUtf8Start(chunk, cutPos);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
let retained = Buffer.from(chunk.subarray(cutPos));
|
|
25
|
+
// Skip partial escape sequence at the new start
|
|
26
|
+
const escSkip = skipPartialEscapeSequence(retained, 0);
|
|
27
|
+
if (escSkip > 0) {
|
|
28
|
+
retained = Buffer.from(retained.subarray(escSkip));
|
|
29
|
+
}
|
|
30
|
+
session.bufferBase += session.bufferLength + cutPos + escSkip;
|
|
31
|
+
session.bufferChunks = [retained];
|
|
32
|
+
session.bufferLength = retained.length;
|
|
28
33
|
return;
|
|
29
34
|
}
|
|
30
35
|
session.bufferChunks.push(Buffer.from(chunk));
|
|
31
36
|
session.bufferLength += chunk.length;
|
|
37
|
+
let trimmed = false;
|
|
32
38
|
while (session.bufferLength > TERMINAL_BUFFER_LIMIT && session.bufferChunks.length > 0) {
|
|
39
|
+
trimmed = true;
|
|
33
40
|
const overflow = session.bufferLength - TERMINAL_BUFFER_LIMIT;
|
|
34
41
|
const firstChunk = session.bufferChunks[0];
|
|
35
42
|
if (firstChunk.length <= overflow) {
|
|
@@ -43,9 +50,9 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
43
50
|
session.bufferBase += cutPos;
|
|
44
51
|
session.bufferLength -= cutPos;
|
|
45
52
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
if (trimmed && session.bufferChunks.length > 0) {
|
|
54
|
+
// After removing whole chunks, the new first chunk may start with
|
|
55
|
+
// orphaned UTF-8 continuation bytes from a character that spanned chunks.
|
|
49
56
|
const first = session.bufferChunks[0];
|
|
50
57
|
const skip = alignToUtf8Start(first, 0);
|
|
51
58
|
if (skip > 0) {
|
|
@@ -53,6 +60,16 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
53
60
|
session.bufferBase += skip;
|
|
54
61
|
session.bufferLength -= skip;
|
|
55
62
|
}
|
|
63
|
+
// Also skip past any partial ANSI CSI escape sequence at the start.
|
|
64
|
+
// Without this, orphaned parameter bytes like "100;50m" are rendered
|
|
65
|
+
// as literal text and shift all cursor positions.
|
|
66
|
+
const cur = session.bufferChunks[0];
|
|
67
|
+
const escSkip = skipPartialEscapeSequence(cur, 0);
|
|
68
|
+
if (escSkip > 0) {
|
|
69
|
+
session.bufferChunks[0] = Buffer.from(cur.subarray(escSkip));
|
|
70
|
+
session.bufferBase += escSkip;
|
|
71
|
+
session.bufferLength -= escSkip;
|
|
72
|
+
}
|
|
56
73
|
}
|
|
57
74
|
}
|
|
58
75
|
function getUniqueTerminalTitle(deckId, requestedTitle) {
|
package/dist/utils/utf8.js
CHANGED
|
@@ -53,3 +53,74 @@ export function alignToUtf8End(buf, offset) {
|
|
|
53
53
|
// The character is incomplete — exclude it
|
|
54
54
|
return pos;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Skip past a partial ANSI CSI escape sequence at the start of a buffer.
|
|
58
|
+
*
|
|
59
|
+
* When a terminal buffer is trimmed from the front, the cut point may land
|
|
60
|
+
* inside a CSI sequence (e.g. "\x1b[38;2;100;50m"). The ESC and '[' are
|
|
61
|
+
* discarded, leaving orphaned parameter bytes like "100;50m" that xterm.js
|
|
62
|
+
* would render as literal text, shifting all subsequent cursor positions.
|
|
63
|
+
*
|
|
64
|
+
* This function detects two patterns:
|
|
65
|
+
* 1. Buffer starts with '[' followed by CSI parameter / intermediate /
|
|
66
|
+
* final bytes → partial CSI whose ESC was trimmed.
|
|
67
|
+
* 2. Buffer starts with CSI parameter bytes (digits, ';') containing at
|
|
68
|
+
* least one ';', ending with a final byte → partial CSI whose
|
|
69
|
+
* "ESC [" was trimmed.
|
|
70
|
+
*
|
|
71
|
+
* Returns the number of bytes to skip (0 if no partial sequence detected).
|
|
72
|
+
*/
|
|
73
|
+
export function skipPartialEscapeSequence(buf, offset) {
|
|
74
|
+
if (offset >= buf.length)
|
|
75
|
+
return 0;
|
|
76
|
+
const limit = Math.min(buf.length, offset + 128);
|
|
77
|
+
let pos = offset;
|
|
78
|
+
// Pattern 1: starts with '[' (CSI intro without preceding ESC)
|
|
79
|
+
if (buf[pos] === 0x5B /* '[' */ && pos + 1 < limit) {
|
|
80
|
+
const next = buf[pos + 1];
|
|
81
|
+
// Only treat as CSI if next byte is a parameter (0x30-0x3F) or
|
|
82
|
+
// intermediate (0x20-0x2F) byte — avoids false positives like "[user@host"
|
|
83
|
+
if ((next >= 0x30 && next <= 0x3F) || (next >= 0x20 && next <= 0x2F)) {
|
|
84
|
+
pos++; // skip '['
|
|
85
|
+
while (pos < limit) {
|
|
86
|
+
const c = buf[pos];
|
|
87
|
+
if ((c >= 0x30 && c <= 0x3F) || (c >= 0x20 && c <= 0x2F)) {
|
|
88
|
+
pos++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (c >= 0x40 && c <= 0x7E) {
|
|
92
|
+
// CSI final byte — skip it and we're done
|
|
93
|
+
return (pos + 1) - offset;
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
// Pattern 2: starts with CSI parameter bytes (digits, ';', etc.)
|
|
102
|
+
const b = buf[pos];
|
|
103
|
+
if (b < 0x30 || b > 0x3F)
|
|
104
|
+
return 0;
|
|
105
|
+
let hasSemicolon = false;
|
|
106
|
+
while (pos < limit) {
|
|
107
|
+
const c = buf[pos];
|
|
108
|
+
if (c >= 0x30 && c <= 0x3F) {
|
|
109
|
+
if (c === 0x3B)
|
|
110
|
+
hasSemicolon = true;
|
|
111
|
+
pos++;
|
|
112
|
+
}
|
|
113
|
+
else if (c >= 0x20 && c <= 0x2F) {
|
|
114
|
+
pos++;
|
|
115
|
+
}
|
|
116
|
+
else if (c >= 0x40 && c <= 0x7E) {
|
|
117
|
+
// CSI final byte — only skip if we saw at least one semicolon
|
|
118
|
+
// (avoids false positives like "5m" at the start of normal text)
|
|
119
|
+
return hasSemicolon ? (pos + 1) - offset : 0;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return 0;
|
|
126
|
+
}
|