typescript-virtual-container 1.5.8 → 1.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,9 +15,11 @@ export class WebTermRenderer {
15
15
  rows;
16
16
  cols;
17
17
  screen;
18
+ scrollback = [];
18
19
  curRow = 0;
19
20
  curCol = 0;
20
21
  cursorVisible = true;
22
+ _cleared = false;
21
23
  // Current SGR state
22
24
  bold = false;
23
25
  reverse = false;
@@ -66,8 +68,34 @@ export class WebTermRenderer {
66
68
  this.handleCsi(seq, cmd);
67
69
  i = j + 1;
68
70
  }
71
+ else if (next === "]") {
72
+ // OSC (Operating System Command) — terminator is BEL (\x07) or ST (ESC \)
73
+ // Must consume fully or the payload prints as raw text and corrupts SGR state.
74
+ let j = i + 2;
75
+ while (j < this.buf.length) {
76
+ if (this.buf[j] === "\x07") {
77
+ j++;
78
+ break;
79
+ }
80
+ if (this.buf[j] === "\x1b" && this.buf[j + 1] === "\\") {
81
+ j += 2;
82
+ break;
83
+ }
84
+ j++;
85
+ }
86
+ // If terminator not yet received, wait for more data
87
+ if (j >= this.buf.length && this.buf[j - 1] !== "\x07")
88
+ break;
89
+ i = j;
90
+ }
91
+ else if (next === "O") {
92
+ // SS3 — single extra byte (F1-F4, cursor keys in application mode)
93
+ if (i + 2 >= this.buf.length)
94
+ break; // wait for more data
95
+ i += 3; // ESC O <cmd>
96
+ }
69
97
  else {
70
- i += 2; // skip unknown ESC sequence
98
+ i += 2; // skip unknown 2-char ESC sequence
71
99
  }
72
100
  }
73
101
  else if (ch === "\r") {
@@ -75,7 +103,12 @@ export class WebTermRenderer {
75
103
  i++;
76
104
  }
77
105
  else if (ch === "\n") {
78
- this.curRow = Math.min(this.curRow + 1, this.rows - 1);
106
+ if (this.curRow < this.rows - 1) {
107
+ this.curRow++;
108
+ }
109
+ else {
110
+ this.scrollUp();
111
+ }
79
112
  i++;
80
113
  }
81
114
  else if (ch.charCodeAt(0) >= 32) {
@@ -116,15 +149,6 @@ export class WebTermRenderer {
116
149
  }
117
150
  return;
118
151
  }
119
- if (cmd === "J") {
120
- const mode = seq === "" ? 0 : Number.parseInt(seq, 10);
121
- if (mode === 2) {
122
- this.screen = this.makeScreen();
123
- this.curRow = 0;
124
- this.curCol = 0;
125
- }
126
- return;
127
- }
128
152
  if (cmd === "m") {
129
153
  this.handleSgr(seq);
130
154
  return;
@@ -137,6 +161,57 @@ export class WebTermRenderer {
137
161
  this.cursorVisible = true;
138
162
  return;
139
163
  }
164
+ // Cursor movement (relative)
165
+ if (cmd === "A") {
166
+ const n = Number.parseInt(seq || "1", 10) || 1;
167
+ this.curRow = Math.max(0, this.curRow - n);
168
+ return;
169
+ }
170
+ if (cmd === "B") {
171
+ const n = Number.parseInt(seq || "1", 10) || 1;
172
+ this.curRow = Math.min(this.rows - 1, this.curRow + n);
173
+ return;
174
+ }
175
+ if (cmd === "C") {
176
+ const n = Number.parseInt(seq || "1", 10) || 1;
177
+ this.curCol = Math.min(this.cols - 1, this.curCol + n);
178
+ return;
179
+ }
180
+ if (cmd === "D") {
181
+ const n = Number.parseInt(seq || "1", 10) || 1;
182
+ this.curCol = Math.max(0, this.curCol - n);
183
+ return;
184
+ }
185
+ // Cursor column absolute
186
+ if (cmd === "G") {
187
+ const n = Number.parseInt(seq || "1", 10) || 1;
188
+ this.curCol = Math.max(0, Math.min(n - 1, this.cols - 1));
189
+ return;
190
+ }
191
+ // Erase display modes 0/1
192
+ if (cmd === "J") {
193
+ const mode = seq === "" ? 0 : Number.parseInt(seq, 10);
194
+ if (mode === 0) {
195
+ for (let c = this.curCol; c < this.cols; c++)
196
+ this.screen[this.curRow][c] = makeCell();
197
+ for (let r = this.curRow + 1; r < this.rows; r++)
198
+ this.screen[r] = Array.from({ length: this.cols }, () => makeCell());
199
+ }
200
+ else if (mode === 1) {
201
+ for (let r = 0; r < this.curRow; r++)
202
+ this.screen[r] = Array.from({ length: this.cols }, () => makeCell());
203
+ for (let c = 0; c <= this.curCol; c++)
204
+ this.screen[this.curRow][c] = makeCell();
205
+ }
206
+ else if (mode === 2) {
207
+ this.screen = this.makeScreen();
208
+ this.scrollback = [];
209
+ this.curRow = 0;
210
+ this.curCol = 0;
211
+ this._cleared = true;
212
+ }
213
+ return;
214
+ }
140
215
  }
141
216
  handleSgr(seq) {
142
217
  const codes = seq === "" ? [0] : seq.split(";").map((n) => Number.parseInt(n || "0", 10));
@@ -202,9 +277,24 @@ export class WebTermRenderer {
202
277
  i++;
203
278
  }
204
279
  }
280
+ scrollUp() {
281
+ const line = this.screen.shift();
282
+ this.scrollback.push(line);
283
+ if (this.scrollback.length > 1000)
284
+ this.scrollback.shift();
285
+ this.screen.push(Array.from({ length: this.cols }, () => makeCell()));
286
+ // curRow stays at rows-1 (bottom)
287
+ }
205
288
  putChar(ch) {
206
- if (this.curRow >= this.rows || this.curCol >= this.cols)
207
- return;
289
+ if (this.curCol >= this.cols) {
290
+ this.curCol = 0;
291
+ if (this.curRow < this.rows - 1) {
292
+ this.curRow++;
293
+ }
294
+ else {
295
+ this.scrollUp();
296
+ }
297
+ }
208
298
  this.screen[this.curRow][this.curCol] = makeCell({
209
299
  ch,
210
300
  bold: this.bold,
@@ -213,11 +303,6 @@ export class WebTermRenderer {
213
303
  bg: this.bg,
214
304
  });
215
305
  this.curCol++;
216
- if (this.curCol >= this.cols) {
217
- this.curCol = 0;
218
- if (this.curRow < this.rows - 1)
219
- this.curRow++;
220
- }
221
306
  }
222
307
  makeScreen(rows = this.rows, cols = this.cols) {
223
308
  return Array.from({ length: rows }, () => Array.from({ length: cols }, () => makeCell()));
@@ -235,14 +320,60 @@ export class WebTermRenderer {
235
320
  let fg = cell.fg ?? "#ccc";
236
321
  let bg = cell.bg ?? "transparent";
237
322
  if (cell.reverse) {
238
- [fg, bg] = [bg === "transparent" ? "#000" : bg, fg];
323
+ [fg, bg] = [bg === "transparent" ? "#000" : bg, fg === "transparent" ? "#000" : fg];
239
324
  }
240
- if (cell.bold && !cell.fg)
241
- fg = "#fff";
242
325
  if (isCursor) {
243
- [fg, bg] = [bg === "transparent" ? "#000" : bg, fg === "#ccc" ? "#ccc" : fg];
244
- bg = "#ccc";
245
- fg = "#000";
326
+ // Isoler le curseur dans son propre span pour éviter que sa couleur
327
+ // inversée ne déborde sur les cellules vides adjacentes.
328
+ if (spanOpen) {
329
+ html += "</span>";
330
+ spanOpen = false;
331
+ lastStyle = "";
332
+ }
333
+ const curFg = bg === "transparent" ? "#000" : bg;
334
+ const boldPart = cell.bold ? "font-weight:bold;" : "";
335
+ html += `<span style="color:${curFg};background:#ccc;${boldPart}">${escHtml(cell.ch)}</span>`;
336
+ }
337
+ else {
338
+ const style = `color:${fg};background:${bg};${cell.bold ? "font-weight:bold;" : ""}`;
339
+ if (style !== lastStyle) {
340
+ if (spanOpen)
341
+ html += "</span>";
342
+ html += `<span style="${style}">`;
343
+ spanOpen = true;
344
+ lastStyle = style;
345
+ }
346
+ html += escHtml(cell.ch);
347
+ }
348
+ }
349
+ if (spanOpen)
350
+ html += "</span>";
351
+ if (r < this.rows - 1)
352
+ html += "\n";
353
+ }
354
+ return html;
355
+ }
356
+ get cursorRow() { return this.curRow; }
357
+ get cursorCol() { return this.curCol; }
358
+ get isCursorVisible() { return this.cursorVisible; }
359
+ /** Returns true (once) if CSI 2J was received since last call. */
360
+ consumeCleared() {
361
+ const v = this._cleared;
362
+ this._cleared = false;
363
+ return v;
364
+ }
365
+ get scrollbackLength() { return this.scrollback.length; }
366
+ clearScrollback() { this.scrollback = []; }
367
+ renderScrollbackHtml() {
368
+ let html = "";
369
+ for (const row of this.scrollback) {
370
+ let spanOpen = false;
371
+ let lastStyle = "";
372
+ for (const cell of row) {
373
+ let fg = cell.fg ?? "#ccc";
374
+ let bg = cell.bg ?? "transparent";
375
+ if (cell.reverse) {
376
+ [fg, bg] = [bg === "transparent" ? "#000" : bg, fg === "transparent" ? "#000" : fg];
246
377
  }
247
378
  const style = `color:${fg};background:${bg};${cell.bold ? "font-weight:bold;" : ""}`;
248
379
  if (style !== lastStyle) {
@@ -256,15 +387,18 @@ export class WebTermRenderer {
256
387
  }
257
388
  if (spanOpen)
258
389
  html += "</span>";
259
- if (r < this.rows - 1)
260
- html += "\n";
390
+ html += "\n";
261
391
  }
262
392
  return html;
263
393
  }
264
- get cursorRow() { return this.curRow; }
265
- get cursorCol() { return this.curCol; }
266
- get isCursorVisible() { return this.cursorVisible; }
267
394
  }
395
+ // const ANSI_NORMAL_TO_BRIGHT: Record<string, string> = {
396
+ // "#000": "#555", "#c00": "#f55", "#0c0": "#5f5", "#cc0": "#ff5",
397
+ // "#00c": "#55f", "#c0c": "#f5f", "#0cc": "#5ff", "#ccc": "#fff",
398
+ // };
399
+ // function boldBright(fg: string): string {
400
+ // return ANSI_NORMAL_TO_BRIGHT[fg] ?? fg;
401
+ // }
268
402
  function escHtml(ch) {
269
403
  if (ch === "&")
270
404
  return "&amp;";
@@ -27,6 +27,8 @@ export interface CommandResult {
27
27
  openEditor?: NanoEditorSession;
28
28
  /** Request opening built-in htop-like screen. */
29
29
  openHtop?: boolean;
30
+ /** Request opening built-in Pac-Man game. */
31
+ openPacman?: boolean;
30
32
  /** Request sudo password challenge flow. */
31
33
  sudoChallenge?: SudoChallenge;
32
34
  /** Request a generic password challenge (adduser, passwd). */
@@ -36,6 +36,8 @@ export interface Statement {
36
36
  op?: LogicalOp;
37
37
  /** Optional next statement in sequence. */
38
38
  next?: Statement;
39
+ /** Run in background (trailing &). */
40
+ background?: boolean;
39
41
  }
40
42
  /** Top-level parse result for a script. */
41
43
  export interface Script {
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
7
- "version": "1.5.8",
7
+ "version": "1.5.9",
8
8
  "files": [
9
9
  "dist/",
10
10
  "README.md",
@@ -37,7 +37,7 @@
37
37
  "deploy:npm": "bun publish --access public",
38
38
  "bench": "rm -rf .benchmark-shells/ && bun benchmark-virtualshell.ts",
39
39
  "benchmark": "bun benchmark-virtualshell.ts > benchmark-results.txt",
40
- "web-build": "bunx esbuild src/web.ts --bundle --platform=browser --format=esm --target=es2020 --outfile=builds/web.min.js --tree-shaking=true --minify",
40
+ "web-build": "bunx esbuild src/web.ts --bundle --platform=browser --format=esm --target=es2020 --outfile=builds/web.min.js --tree-shaking=true --minify",
41
41
  "web-build-iife": "bunx esbuild src/web.ts --bundle --platform=browser --format=iife --target=es2020 --outfile=builds/web-iife.min.js --tree-shaking=true --minify --global-name=WebShellLib",
42
42
  "example-build": "bun run web-build && cp builds/web.min.js examples/web.min.js",
43
43
  "example-serve": "cd examples && bun server.js",