esp32tool 1.6.5 → 1.6.7

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/package.cli.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esp32tool",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "ESP32Tool - Standalone command-line tool (build-time config only)",
@@ -23,7 +23,7 @@
23
23
  "usb": "^2.17.0"
24
24
  },
25
25
  "devDependencies": {
26
- "electron": "^39.2.5"
26
+ "electron": "^42.0.1"
27
27
  },
28
28
  "_comment": "This file is ONLY used during CLI builds. It is temporarily swapped with package.json by build-electron-cli.cjs, then restored. Never publish this file to npm. Keep version in sync with main package.json when releasing."
29
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esp32tool",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "type": "module",
5
5
  "description": "Flash & Read ESP devices using WebSerial, Electron, and also Android mobile via WebUSB",
6
6
  "main": "electron/main.cjs",
@@ -24,7 +24,7 @@
24
24
  "build": "npm run prebuild && node update-sw-version.cjs && tsc --skipLibCheck && tsc -p tsconfig.util.json && rollup -c && node -e \"const fs=require('fs'); fs.readdirSync('dist/web').filter(f=>f.endsWith('.js')).forEach(f=>fs.copyFileSync('dist/web/'+f,'js/modules/'+f)); fs.renameSync('js/modules/index.js','js/modules/esptool.js');\" && node fix-cli-imports.cjs",
25
25
  "build:binary": "node build-single-binary.cjs",
26
26
  "build:cli-electron": "node build-electron-cli.cjs",
27
- "format": "npm exec -- prettier --write src",
27
+ "format": "npm exec -- prettier --write src \"js/*.js\" electron css",
28
28
  "dev:clean": "node -e \"const fs=require('fs'); fs.rmSync('dist',{recursive:true,force:true});\"",
29
29
  "dev:tsc-once": "tsc",
30
30
  "dev:tsc": "tsc --watch",
@@ -50,27 +50,26 @@
50
50
  "@electron-forge/maker-zip": "^7.11.1",
51
51
  "@electron-forge/plugin-auto-unpack-natives": "^7.11.1",
52
52
  "@electron/fuses": "^2.1.1",
53
- "@eslint/js": "^9.39.3",
53
+ "@eslint/js": "^9.39.4",
54
54
  "@rollup/plugin-json": "^6.1.0",
55
- "@rollup/plugin-node-resolve": "^16.0.0",
55
+ "@rollup/plugin-node-resolve": "^16.0.3",
56
56
  "@rollup/plugin-terser": "^1.0.0",
57
57
  "@rollup/plugin-typescript": "^12.3.0",
58
- "@types/node": "^25.5.2",
58
+ "@types/node": "^25.7.0",
59
59
  "@types/pako": "^2.0.4",
60
- "@types/serialport": "^10.2.0",
61
- "@types/w3c-web-serial": "^1.0.7",
60
+ "@types/w3c-web-serial": "^1.0.8",
62
61
  "archiver": "^7.0.1",
63
- "electron": "^41.1.0",
62
+ "electron": "^42.0.1",
64
63
  "electron-squirrel-startup": "^1.0.1",
65
- "eslint": "^10.2.0",
64
+ "eslint": "^10.3.0",
66
65
  "eslint-config-prettier": "^10.1.8",
67
66
  "eslint-plugin-prettier": "^5.5.5",
68
67
  "npm-run-all": "^4.1.5",
69
- "prettier": "^3.8.1",
70
- "rollup": "^4.60.1",
68
+ "prettier": "^3.8.3",
69
+ "rollup": "^4.60.3",
71
70
  "serve": "^14.2.6",
72
71
  "typescript": "^5.9.3",
73
- "typescript-eslint": "^8.58.0"
72
+ "typescript-eslint": "^8.59.1"
74
73
  },
75
74
  "dependencies": {
76
75
  "pako": "^2.1.0",
@@ -78,11 +77,11 @@
78
77
  "usb": "^2.17.0"
79
78
  },
80
79
  "overrides": {
81
- "tmp": "^0.2.4",
82
- "tar": "^7.5.6",
83
- "minimatch": "^10.2.1",
80
+ "tmp": "^0.2.5",
81
+ "tar": "^7.5.13",
82
+ "minimatch": "^10.2.5",
84
83
  "serve": {
85
- "ajv": "^8.18.0"
84
+ "ajv": "^8.20.0"
86
85
  },
87
86
  "@electron/asar": "^4.0.1",
88
87
  "serialize-javascript": "^7.0.3",
Binary file
Binary file
@@ -1,3 +1,58 @@
1
+ const ANSI_256: string[] = (() => {
2
+ const t: string[] = [];
3
+ // Standard colors 0-7
4
+ t[0] = "rgb(0,0,0)";
5
+ t[1] = "rgb(128,0,0)";
6
+ t[2] = "rgb(0,128,0)";
7
+ t[3] = "rgb(128,128,0)";
8
+ t[4] = "rgb(0,0,128)";
9
+ t[5] = "rgb(128,0,128)";
10
+ t[6] = "rgb(0,128,128)";
11
+ t[7] = "rgb(192,192,192)";
12
+ // Bright colors 8-15
13
+ t[8] = "rgb(128,128,128)";
14
+ t[9] = "rgb(255,0,0)";
15
+ t[10] = "rgb(0,255,0)";
16
+ t[11] = "rgb(255,255,0)";
17
+ t[12] = "rgb(99,153,255)";
18
+ t[13] = "rgb(255,0,255)";
19
+ t[14] = "rgb(0,255,255)";
20
+ t[15] = "rgb(255,255,255)";
21
+ // 6x6x6 color cube 16-231
22
+ for (let i = 0; i < 216; i++) {
23
+ const r = Math.floor(i / 36);
24
+ const g = Math.floor((i % 36) / 6);
25
+ const b = i % 6;
26
+ t[16 + i] =
27
+ "rgb(" +
28
+ (r ? r * 40 + 55 : 0) +
29
+ "," +
30
+ (g ? g * 40 + 55 : 0) +
31
+ "," +
32
+ (b ? b * 40 + 55 : 0) +
33
+ ")";
34
+ }
35
+ // Grayscale ramp 232-255
36
+ for (let i = 0; i < 24; i++) {
37
+ const v = i * 10 + 8;
38
+ t[232 + i] = "rgb(" + v + "," + v + "," + v + ")";
39
+ }
40
+ return t;
41
+ })();
42
+
43
+ // Maps 256-color indices 0–7 to the named CSS class tokens so that
44
+ // \x1b[38;5;1m renders the same red as \x1b[31m.
45
+ const ANSI_NAMED: (string | null)[] = [
46
+ "black",
47
+ "red",
48
+ "green",
49
+ "yellow",
50
+ "blue",
51
+ "magenta",
52
+ "cyan",
53
+ "white",
54
+ ];
55
+
1
56
  interface ConsoleState {
2
57
  bold: boolean;
3
58
  italic: boolean;
@@ -5,6 +60,10 @@ interface ConsoleState {
5
60
  strikethrough: boolean;
6
61
  foregroundColor: string | null;
7
62
  backgroundColor: string | null;
63
+ fgRgb: string | null;
64
+ bgRgb: string | null;
65
+ dim: boolean;
66
+ reverse: boolean;
8
67
  carriageReturn: boolean;
9
68
  lines: string[];
10
69
  secret: boolean;
@@ -20,6 +79,10 @@ export class ColoredConsole {
20
79
  strikethrough: false,
21
80
  foregroundColor: null,
22
81
  backgroundColor: null,
82
+ fgRgb: null,
83
+ bgRgb: null,
84
+ dim: false,
85
+ reverse: false,
23
86
  carriageReturn: false,
24
87
  lines: [],
25
88
  secret: false,
@@ -39,27 +102,57 @@ export class ColoredConsole {
39
102
  processLine(line: string): Element {
40
103
  // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequences
41
104
  // eslint-disable-next-line no-control-regex
42
- const re = /(?:\x1B|\\x1B)(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1B\\))/g;
105
+ const re = /(?:\x1B|\\x1B)(?:\[(.*?)([@-~])|\].*?(?:\x07|\x1B\\))/g;
43
106
  let i = 0;
44
107
 
45
108
  const lineSpan = document.createElement("span");
46
109
  lineSpan.classList.add("line");
47
110
 
48
111
  const addSpan = (content: string) => {
49
- if (content === "") return;
50
-
51
112
  const span = document.createElement("span");
52
113
  if (this.state.bold) span.classList.add("log-bold");
114
+ if (this.state.dim) span.classList.add("log-dim");
53
115
  if (this.state.italic) span.classList.add("log-italic");
54
116
  if (this.state.underline) span.classList.add("log-underline");
55
117
  if (this.state.strikethrough) span.classList.add("log-strikethrough");
56
118
  if (this.state.secret) span.classList.add("log-secret");
57
119
  if (this.state.blink) span.classList.add("log-blink");
58
120
  if (this.state.rapidBlink) span.classList.add("log-rapid-blink");
59
- if (this.state.foregroundColor !== null)
60
- span.classList.add(`log-fg-${this.state.foregroundColor}`);
61
- if (this.state.backgroundColor !== null)
62
- span.classList.add(`log-bg-${this.state.backgroundColor}`);
121
+
122
+ // Resolve colors with reverse-video support
123
+ let fgRgb = this.state.fgRgb;
124
+ let bgRgb = this.state.bgRgb;
125
+ let fg = this.state.foregroundColor;
126
+ let bg = this.state.backgroundColor;
127
+
128
+ if (this.state.reverse) {
129
+ fgRgb = this.state.bgRgb;
130
+ bgRgb = this.state.fgRgb;
131
+ fg = this.state.backgroundColor;
132
+ bg = this.state.foregroundColor;
133
+ // When one side is unset, fill in the terminal defaults so the
134
+ // swap is always visible (fg default=#ddd, bg default=#1c1c1c).
135
+ if (!fgRgb && !fg && !bgRgb && !bg) {
136
+ span.classList.add("log-reverse");
137
+ } else {
138
+ if (!fgRgb && !fg) fgRgb = "rgb(28,28,28)";
139
+ if (!bgRgb && !bg) bgRgb = "rgb(221,221,221)";
140
+ }
141
+ }
142
+
143
+ // Inline rgb() style takes priority over CSS class
144
+ if (fgRgb) {
145
+ span.style.color = fgRgb;
146
+ } else if (fg !== null) {
147
+ span.classList.add(`log-fg-${fg}`);
148
+ }
149
+
150
+ if (bgRgb) {
151
+ span.style.backgroundColor = bgRgb;
152
+ } else if (bg !== null) {
153
+ span.classList.add(`log-bg-${bg}`);
154
+ }
155
+
63
156
  span.appendChild(document.createTextNode(content));
64
157
  lineSpan.appendChild(span);
65
158
 
@@ -79,18 +172,40 @@ export class ColoredConsole {
79
172
  addSpan(line.substring(i, j));
80
173
  i = j + match[0].length;
81
174
 
82
- if (match[1] === undefined) continue;
175
+ // Only process SGR sequences (final byte 'm'); skip cursor, erase, etc.
176
+ if (match[1] === undefined || match[2] !== "m") continue;
83
177
 
84
- for (const colorCode of match[1].split(";")) {
85
- switch (parseInt(colorCode)) {
178
+ const rawCodes = match[1] === "" ? [""] : match[1].split(";");
179
+ const codes = [];
180
+ let invalidSgr = false;
181
+ for (const rawCode of rawCodes) {
182
+ if (rawCode === "") {
183
+ codes.push(0);
184
+ continue;
185
+ }
186
+ if (!/^\d+$/.test(rawCode)) {
187
+ invalidSgr = true;
188
+ break;
189
+ }
190
+ codes.push(Number(rawCode));
191
+ }
192
+ if (invalidSgr) continue;
193
+
194
+ let ci = 0;
195
+ while (ci < codes.length) {
196
+ const code = codes[ci];
197
+ switch (code) {
86
198
  case 0:
87
- // reset
88
199
  this.state.bold = false;
200
+ this.state.dim = false;
89
201
  this.state.italic = false;
90
202
  this.state.underline = false;
91
203
  this.state.strikethrough = false;
92
204
  this.state.foregroundColor = null;
93
205
  this.state.backgroundColor = null;
206
+ this.state.fgRgb = null;
207
+ this.state.bgRgb = null;
208
+ this.state.reverse = false;
94
209
  this.state.secret = false;
95
210
  this.state.blink = false;
96
211
  this.state.rapidBlink = false;
@@ -98,6 +213,9 @@ export class ColoredConsole {
98
213
  case 1:
99
214
  this.state.bold = true;
100
215
  break;
216
+ case 2:
217
+ this.state.dim = true;
218
+ break;
101
219
  case 3:
102
220
  this.state.italic = true;
103
221
  break;
@@ -112,6 +230,9 @@ export class ColoredConsole {
112
230
  this.state.rapidBlink = true;
113
231
  this.state.blink = false;
114
232
  break;
233
+ case 7:
234
+ this.state.reverse = true;
235
+ break;
115
236
  case 8:
116
237
  this.state.secret = true;
117
238
  break;
@@ -120,6 +241,7 @@ export class ColoredConsole {
120
241
  break;
121
242
  case 22:
122
243
  this.state.bold = false;
244
+ this.state.dim = false;
123
245
  break;
124
246
  case 23:
125
247
  this.state.italic = false;
@@ -131,6 +253,9 @@ export class ColoredConsole {
131
253
  this.state.blink = false;
132
254
  this.state.rapidBlink = false;
133
255
  break;
256
+ case 27:
257
+ this.state.reverse = false;
258
+ break;
134
259
  case 28:
135
260
  this.state.secret = false;
136
261
  break;
@@ -139,59 +264,206 @@ export class ColoredConsole {
139
264
  break;
140
265
  case 30:
141
266
  this.state.foregroundColor = "black";
267
+ this.state.fgRgb = null;
142
268
  break;
143
269
  case 31:
144
270
  this.state.foregroundColor = "red";
271
+ this.state.fgRgb = null;
145
272
  break;
146
273
  case 32:
147
274
  this.state.foregroundColor = "green";
275
+ this.state.fgRgb = null;
148
276
  break;
149
277
  case 33:
150
278
  this.state.foregroundColor = "yellow";
279
+ this.state.fgRgb = null;
151
280
  break;
152
281
  case 34:
153
282
  this.state.foregroundColor = "blue";
283
+ this.state.fgRgb = null;
154
284
  break;
155
285
  case 35:
156
286
  this.state.foregroundColor = "magenta";
287
+ this.state.fgRgb = null;
157
288
  break;
158
289
  case 36:
159
290
  this.state.foregroundColor = "cyan";
291
+ this.state.fgRgb = null;
160
292
  break;
161
293
  case 37:
162
294
  this.state.foregroundColor = "white";
295
+ this.state.fgRgb = null;
296
+ break;
297
+ case 38:
298
+ // Extended foreground: 38;5;n (256-color) or 38;2;r;g;b (true-color)
299
+ if (ci + 1 < codes.length) {
300
+ if (codes[ci + 1] === 5) {
301
+ if (ci + 2 < codes.length) {
302
+ const idx = codes[ci + 2];
303
+ if (idx >= 0 && idx <= 7 && ANSI_NAMED[idx]) {
304
+ this.state.foregroundColor = ANSI_NAMED[idx];
305
+ this.state.fgRgb = null;
306
+ } else if (idx >= 0 && idx <= 255 && ANSI_256[idx]) {
307
+ this.state.foregroundColor = null;
308
+ this.state.fgRgb = ANSI_256[idx];
309
+ }
310
+ ci += 2;
311
+ } else {
312
+ ci += 1;
313
+ }
314
+ } else if (codes[ci + 1] === 2) {
315
+ if (ci + 4 < codes.length) {
316
+ this.state.foregroundColor = null;
317
+ const r = Math.max(0, Math.min(255, codes[ci + 2]));
318
+ const g = Math.max(0, Math.min(255, codes[ci + 3]));
319
+ const b = Math.max(0, Math.min(255, codes[ci + 4]));
320
+ this.state.fgRgb = "rgb(" + r + "," + g + "," + b + ")";
321
+ ci += 4;
322
+ } else {
323
+ ci = codes.length - 1;
324
+ }
325
+ }
326
+ }
163
327
  break;
164
328
  case 39:
165
329
  this.state.foregroundColor = null;
330
+ this.state.fgRgb = null;
166
331
  break;
167
332
  case 40:
168
333
  this.state.backgroundColor = "black";
334
+ this.state.bgRgb = null;
169
335
  break;
170
336
  case 41:
171
337
  this.state.backgroundColor = "red";
338
+ this.state.bgRgb = null;
172
339
  break;
173
340
  case 42:
174
341
  this.state.backgroundColor = "green";
342
+ this.state.bgRgb = null;
175
343
  break;
176
344
  case 43:
177
345
  this.state.backgroundColor = "yellow";
346
+ this.state.bgRgb = null;
178
347
  break;
179
348
  case 44:
180
349
  this.state.backgroundColor = "blue";
350
+ this.state.bgRgb = null;
181
351
  break;
182
352
  case 45:
183
353
  this.state.backgroundColor = "magenta";
354
+ this.state.bgRgb = null;
184
355
  break;
185
356
  case 46:
186
357
  this.state.backgroundColor = "cyan";
358
+ this.state.bgRgb = null;
187
359
  break;
188
360
  case 47:
189
361
  this.state.backgroundColor = "white";
362
+ this.state.bgRgb = null;
363
+ break;
364
+ case 48:
365
+ // Extended background: 48;5;n (256-color) or 48;2;r;g;b (true-color)
366
+ if (ci + 1 < codes.length) {
367
+ if (codes[ci + 1] === 5) {
368
+ if (ci + 2 < codes.length) {
369
+ const idx = codes[ci + 2];
370
+ if (idx >= 0 && idx <= 7 && ANSI_NAMED[idx]) {
371
+ this.state.backgroundColor = ANSI_NAMED[idx];
372
+ this.state.bgRgb = null;
373
+ } else if (idx >= 0 && idx <= 255 && ANSI_256[idx]) {
374
+ this.state.backgroundColor = null;
375
+ this.state.bgRgb = ANSI_256[idx];
376
+ }
377
+ ci += 2;
378
+ } else {
379
+ ci += 1;
380
+ }
381
+ } else if (codes[ci + 1] === 2) {
382
+ if (ci + 4 < codes.length) {
383
+ this.state.backgroundColor = null;
384
+ const r = Math.max(0, Math.min(255, codes[ci + 2]));
385
+ const g = Math.max(0, Math.min(255, codes[ci + 3]));
386
+ const b = Math.max(0, Math.min(255, codes[ci + 4]));
387
+ this.state.bgRgb = "rgb(" + r + "," + g + "," + b + ")";
388
+ ci += 4;
389
+ } else {
390
+ ci = codes.length - 1;
391
+ }
392
+ }
393
+ }
190
394
  break;
191
395
  case 49:
192
396
  this.state.backgroundColor = null;
397
+ this.state.bgRgb = null;
398
+ break;
399
+ // Bright foreground colors
400
+ case 90:
401
+ this.state.foregroundColor = null;
402
+ this.state.fgRgb = ANSI_256[8];
403
+ break;
404
+ case 91:
405
+ this.state.foregroundColor = null;
406
+ this.state.fgRgb = ANSI_256[9];
407
+ break;
408
+ case 92:
409
+ this.state.foregroundColor = null;
410
+ this.state.fgRgb = ANSI_256[10];
411
+ break;
412
+ case 93:
413
+ this.state.foregroundColor = null;
414
+ this.state.fgRgb = ANSI_256[11];
415
+ break;
416
+ case 94:
417
+ this.state.foregroundColor = null;
418
+ this.state.fgRgb = ANSI_256[12];
419
+ break;
420
+ case 95:
421
+ this.state.foregroundColor = null;
422
+ this.state.fgRgb = ANSI_256[13];
423
+ break;
424
+ case 96:
425
+ this.state.foregroundColor = null;
426
+ this.state.fgRgb = ANSI_256[14];
427
+ break;
428
+ case 97:
429
+ this.state.foregroundColor = null;
430
+ this.state.fgRgb = ANSI_256[15];
431
+ break;
432
+ // Bright background colors
433
+ case 100:
434
+ this.state.backgroundColor = null;
435
+ this.state.bgRgb = ANSI_256[8];
436
+ break;
437
+ case 101:
438
+ this.state.backgroundColor = null;
439
+ this.state.bgRgb = ANSI_256[9];
440
+ break;
441
+ case 102:
442
+ this.state.backgroundColor = null;
443
+ this.state.bgRgb = ANSI_256[10];
444
+ break;
445
+ case 103:
446
+ this.state.backgroundColor = null;
447
+ this.state.bgRgb = ANSI_256[11];
448
+ break;
449
+ case 104:
450
+ this.state.backgroundColor = null;
451
+ this.state.bgRgb = ANSI_256[12];
452
+ break;
453
+ case 105:
454
+ this.state.backgroundColor = null;
455
+ this.state.bgRgb = ANSI_256[13];
456
+ break;
457
+ case 106:
458
+ this.state.backgroundColor = null;
459
+ this.state.bgRgb = ANSI_256[14];
460
+ break;
461
+ case 107:
462
+ this.state.backgroundColor = null;
463
+ this.state.bgRgb = ANSI_256[15];
193
464
  break;
194
465
  }
466
+ ci++;
195
467
  }
196
468
  }
197
469
  addSpan(line.substring(i));
@@ -210,8 +482,6 @@ export class ColoredConsole {
210
482
  }
211
483
 
212
484
  for (const line of this.state.lines) {
213
- // A lone \r is a pure carriage-return signal — update state but don't
214
- // create a DOM node for it (it has no renderable content).
215
485
  if (line === "\r") {
216
486
  this.state.carriageReturn = true;
217
487
  continue;
@@ -238,14 +508,12 @@ export class ColoredConsole {
238
508
 
239
509
  this.state.lines = [];
240
510
 
241
- // Keep scroll at bottom
242
511
  if (atBottom) {
243
512
  this.targetElement.scrollTop = this.targetElement.scrollHeight;
244
513
  }
245
514
  }
246
515
 
247
516
  addLine(line: string) {
248
- // Processing of lines is deferred for performance reasons
249
517
  if (this.state.lines.length === 0) {
250
518
  setTimeout(() => this.processLines(), 0);
251
519
  }
@@ -272,6 +540,9 @@ export const coloredConsoleStyles = `
272
540
  .log-bold {
273
541
  font-weight: bold;
274
542
  }
543
+ .log-dim {
544
+ opacity: 0.5;
545
+ }
275
546
  .log-italic {
276
547
  font-style: italic;
277
548
  }
@@ -306,6 +577,10 @@ export const coloredConsoleStyles = `
306
577
  width: 1px;
307
578
  font-size: 1px;
308
579
  }
580
+ .log-reverse {
581
+ background: #ddd;
582
+ color: #1c1c1c;
583
+ }
309
584
  .log-fg-black {
310
585
  color: rgb(128, 128, 128);
311
586
  }
@@ -1,15 +1,18 @@
1
- // Matches lines that already carry a wall-clock or tick timestamp so we don't
2
- // add a redundant one. Intentionally does NOT match bare log-level prefixes
3
- // like ESPHome's [I][tag:line]: those have no time information.
4
- //
1
+ // Matches lines that already carry a wall-clock timestamp so we don't add a
2
+ // redundant one. Only real wall-clock formats are matched — tick-based
3
+ // formats like FreeRTOS "(12345)" or ESP-IDF "I (15) boot:" are NOT matched
4
+ // because they don't carry time-of-day information
5
5
  // Covered formats:
6
- // (123456) FreeRTOS ms-tick e.g. "(12345) "
7
6
  // [HH:MM:SS] wall-clock bracket
8
7
  // [HH:MM:SS.mmm] wall-clock bracket with millis
9
- // I (1234) tag: ESP-IDF log level + tick e.g. "I (1234) wifi: ..."
10
8
  // HH:MM:SS.mmm plain wall-clock
11
9
  const DEVICE_TIMESTAMP_RE =
12
- /^\s*(?:\(\d+\)\s|\[\d{2}:\d{2}:\d{2}(?:\.\d+)?\]|[DIWEACV] \(\d+\) \w|(?:\d{2}:){2}\d{2}\.\d)/;
10
+ /^\s*(?:\[\d{2}:\d{2}:\d{2}(?:\.\d+)?\]|(?:\d{2}:){2}\d{2}\.\d)/;
11
+
12
+ // Matches leading ANSI SGR (color/style) codes at the start of a string
13
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequences
14
+ // eslint-disable-next-line no-control-regex
15
+ const LEADING_ANSI_RE = /^(\x1b\[(?:\d+;)*\d*m)+/;
13
16
 
14
17
  export class TimestampTransformer implements Transformer<string, string> {
15
18
  private deviceHasTimestamps = false;
@@ -34,11 +37,32 @@ export class TimestampTransformer implements Transformer<string, string> {
34
37
  return;
35
38
  }
36
39
 
40
+ // Extract leading ANSI codes to preserve them across line splits
41
+ const ansiMatch = chunk.match(LEADING_ANSI_RE);
42
+ const leadingAnsi = ansiMatch ? ansiMatch[0] : "";
43
+ const contentWithoutAnsi = leadingAnsi
44
+ ? chunk.slice(leadingAnsi.length)
45
+ : chunk;
46
+
37
47
  const date = new Date();
38
48
  const h = date.getHours().toString().padStart(2, "0");
39
49
  const m = date.getMinutes().toString().padStart(2, "0");
40
50
  const s = date.getSeconds().toString().padStart(2, "0");
41
- controller.enqueue(`[${h}:${m}:${s}] ${chunk}`);
51
+ const timestamp = `[${h}:${m}:${s}]`;
52
+
53
+ // For multi-line chunks, we need to preserve ANSI codes on each line
54
+ // Split on newlines, but keep the newline characters
55
+ const lines = contentWithoutAnsi.split(/(\r?\n)/);
56
+ let result = "";
57
+ for (const part of lines) {
58
+ if (part === "\n" || part === "\r\n") {
59
+ result += part;
60
+ } else if (part !== "") {
61
+ result += leadingAnsi + timestamp + " " + part;
62
+ }
63
+ }
64
+
65
+ controller.enqueue(result);
42
66
  }
43
67
 
44
68
  reset() {
package/sw.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Service Worker for ESP32Tool PWA
2
- const CACHE_NAME = 'esp32tool-v1.6.5';
2
+ const CACHE_NAME = 'esp32tool-v1.6.7';
3
3
  const RUNTIME_CACHE = 'esp32tool-runtime';
4
4
 
5
5
  // Core files to cache on install (relative paths work for any deployment path)