esp32tool 1.6.4 → 1.6.5

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.
Binary file
package/dist/console.d.ts CHANGED
@@ -4,10 +4,14 @@ export declare class ESP32ToolConsole {
4
4
  private cancelConnection?;
5
5
  private containerElement;
6
6
  private allowInput;
7
+ private commandHistory;
8
+ private historyIndex;
9
+ private currentInput;
7
10
  constructor(port: SerialPort, containerElement: HTMLElement, allowInput?: boolean);
8
11
  logs(): string;
9
12
  init(): Promise<void>;
10
13
  private _connect;
14
+ private _navigateHistory;
11
15
  private _sendCommand;
12
16
  clear(): void;
13
17
  reset(): Promise<void>;
package/dist/console.js CHANGED
@@ -1,7 +1,12 @@
1
1
  import { ColoredConsole, coloredConsoleStyles } from "./util/console-color.js";
2
2
  import { LineBreakTransformer } from "./util/line-break-transformer.js";
3
+ import { TimestampTransformer } from "./util/timestamp-transformer.js";
3
4
  export class ESP32ToolConsole {
4
5
  constructor(port, containerElement, allowInput = true) {
6
+ // Command history buffer
7
+ this.commandHistory = [];
8
+ this.historyIndex = -1;
9
+ this.currentInput = "";
5
10
  this.port = port;
6
11
  this.containerElement = containerElement;
7
12
  this.allowInput = allowInput;
@@ -137,6 +142,20 @@ export class ESP32ToolConsole {
137
142
  ev.stopPropagation();
138
143
  this._sendCommand();
139
144
  }
145
+ else if (ev.key === "ArrowUp") {
146
+ ev.preventDefault();
147
+ this._navigateHistory(1, input);
148
+ }
149
+ else if (ev.key === "ArrowDown") {
150
+ ev.preventDefault();
151
+ this._navigateHistory(-1, input);
152
+ }
153
+ else {
154
+ // User is editing — reset history navigation to live input
155
+ if (this.historyIndex !== -1) {
156
+ this.historyIndex = -1;
157
+ }
158
+ }
140
159
  });
141
160
  }
142
161
  // Start connection
@@ -174,10 +193,10 @@ export class ESP32ToolConsole {
174
193
  signal: abortSignal,
175
194
  })
176
195
  .pipeThrough(new TransformStream(new LineBreakTransformer()))
196
+ .pipeThrough(new TransformStream(new TimestampTransformer()))
177
197
  .pipeTo(new WritableStream({
178
198
  write: (chunk) => {
179
- const cleaned = chunk.replace(/\r\n$/, "\n");
180
- this.console.addLine(cleaned);
199
+ this.console.addLine(chunk);
181
200
  },
182
201
  }));
183
202
  if (!abortSignal.aborted) {
@@ -196,9 +215,42 @@ export class ESP32ToolConsole {
196
215
  console.log("Finished console read loop");
197
216
  }
198
217
  }
218
+ _navigateHistory(direction, input) {
219
+ if (this.commandHistory.length === 0)
220
+ return;
221
+ // Save current unsent input before navigating away
222
+ if (this.historyIndex === -1) {
223
+ this.currentInput = input.value;
224
+ }
225
+ const nextIndex = this.historyIndex + direction;
226
+ if (nextIndex < 0) {
227
+ // Back to unsent draft
228
+ this.historyIndex = -1;
229
+ input.value = this.currentInput;
230
+ }
231
+ else if (nextIndex < this.commandHistory.length) {
232
+ this.historyIndex = nextIndex;
233
+ input.value = this.commandHistory[this.historyIndex];
234
+ }
235
+ // Move cursor to end
236
+ const len = input.value.length;
237
+ input.setSelectionRange(len, len);
238
+ }
199
239
  async _sendCommand() {
200
240
  const input = this.containerElement.querySelector(".esp32tool-console-input");
201
241
  const command = input.value;
242
+ if (command.trim() !== "") {
243
+ // Avoid consecutive duplicates, cap at 100
244
+ if (this.commandHistory[0] !== command) {
245
+ this.commandHistory.unshift(command);
246
+ if (this.commandHistory.length > 100) {
247
+ this.commandHistory.pop();
248
+ }
249
+ }
250
+ }
251
+ // Reset history navigation state
252
+ this.historyIndex = -1;
253
+ this.currentInput = "";
202
254
  if (!this.port.writable) {
203
255
  this.console.addLine("Terminal disconnected: port not writable");
204
256
  return;
@@ -6,14 +6,19 @@ interface ConsoleState {
6
6
  foregroundColor: string | null;
7
7
  backgroundColor: string | null;
8
8
  carriageReturn: boolean;
9
+ lines: string[];
9
10
  secret: boolean;
11
+ blink: boolean;
12
+ rapidBlink: boolean;
10
13
  }
11
14
  export declare class ColoredConsole {
12
15
  targetElement: HTMLElement;
13
16
  state: ConsoleState;
14
17
  constructor(targetElement: HTMLElement);
15
18
  logs(): string;
19
+ processLine(line: string): Element;
20
+ processLines(): void;
16
21
  addLine(line: string): void;
17
22
  }
18
- export declare const coloredConsoleStyles = "\n .log {\n flex: 1;\n background-color: #1c1c1c;\n font-family: \"SFMono-Regular\", Consolas, \"Liberation Mono\", Menlo, Courier,\n monospace;\n font-size: 12px;\n padding: 16px;\n overflow: auto;\n line-height: 1.45;\n border-radius: 3px;\n white-space: pre-wrap;\n overflow-wrap: break-word;\n color: #ddd;\n }\n\n .log-bold {\n font-weight: bold;\n }\n .log-italic {\n font-style: italic;\n }\n .log-underline {\n text-decoration: underline;\n }\n .log-strikethrough {\n text-decoration: line-through;\n }\n .log-underline.log-strikethrough {\n text-decoration: underline line-through;\n }\n .log-secret {\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n }\n .log-secret-redacted {\n opacity: 0;\n width: 1px;\n font-size: 1px;\n }\n .log-fg-black {\n color: rgb(128, 128, 128);\n }\n .log-fg-red {\n color: rgb(255, 0, 0);\n }\n .log-fg-green {\n color: rgb(0, 255, 0);\n }\n .log-fg-yellow {\n color: rgb(255, 255, 0);\n }\n .log-fg-blue {\n color: rgb(0, 0, 255);\n }\n .log-fg-magenta {\n color: rgb(255, 0, 255);\n }\n .log-fg-cyan {\n color: rgb(0, 255, 255);\n }\n .log-fg-white {\n color: rgb(187, 187, 187);\n }\n .log-bg-black {\n background-color: rgb(0, 0, 0);\n }\n .log-bg-red {\n background-color: rgb(255, 0, 0);\n }\n .log-bg-green {\n background-color: rgb(0, 255, 0);\n }\n .log-bg-yellow {\n background-color: rgb(255, 255, 0);\n }\n .log-bg-blue {\n background-color: rgb(0, 0, 255);\n }\n .log-bg-magenta {\n background-color: rgb(255, 0, 255);\n }\n .log-bg-cyan {\n background-color: rgb(0, 255, 255);\n }\n .log-bg-white {\n background-color: rgb(255, 255, 255);\n }\n";
23
+ export declare const coloredConsoleStyles = "\n .log {\n flex: 1;\n background-color: #1c1c1c;\n font-family: \"SFMono-Regular\", Consolas, \"Liberation Mono\", Menlo, Courier,\n monospace;\n font-size: 12px;\n padding: 16px;\n overflow: auto;\n line-height: 1.45;\n border-radius: 3px;\n white-space: pre-wrap;\n overflow-wrap: break-word;\n color: #ddd;\n }\n\n .log-bold {\n font-weight: bold;\n }\n .log-italic {\n font-style: italic;\n }\n .log-underline {\n text-decoration: underline;\n }\n .log-strikethrough {\n text-decoration: line-through;\n }\n .log-underline.log-strikethrough {\n text-decoration: underline line-through;\n }\n .log-blink {\n animation: blink 1s step-end infinite;\n }\n .log-rapid-blink {\n animation: blink 0.4s step-end infinite;\n }\n @keyframes blink {\n 50% {\n opacity: 0;\n }\n }\n .log-secret {\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n }\n .log-secret-redacted {\n opacity: 0;\n width: 1px;\n font-size: 1px;\n }\n .log-fg-black {\n color: rgb(128, 128, 128);\n }\n .log-fg-red {\n color: rgb(255, 0, 0);\n }\n .log-fg-green {\n color: rgb(0, 255, 0);\n }\n .log-fg-yellow {\n color: rgb(255, 255, 0);\n }\n .log-fg-blue {\n color: rgb(0, 0, 255);\n }\n .log-fg-magenta {\n color: rgb(255, 0, 255);\n }\n .log-fg-cyan {\n color: rgb(0, 255, 255);\n }\n .log-fg-white {\n color: rgb(187, 187, 187);\n }\n .log-bg-black {\n background-color: rgb(0, 0, 0);\n }\n .log-bg-red {\n background-color: rgb(255, 0, 0);\n }\n .log-bg-green {\n background-color: rgb(0, 255, 0);\n }\n .log-bg-yellow {\n background-color: rgb(255, 255, 0);\n }\n .log-bg-blue {\n background-color: rgb(0, 0, 255);\n }\n .log-bg-magenta {\n background-color: rgb(255, 0, 255);\n }\n .log-bg-cyan {\n background-color: rgb(0, 255, 255);\n }\n .log-bg-white {\n background-color: rgb(255, 255, 255);\n }\n";
19
24
  export {};
@@ -9,33 +9,25 @@ export class ColoredConsole {
9
9
  foregroundColor: null,
10
10
  backgroundColor: null,
11
11
  carriageReturn: false,
12
+ lines: [],
12
13
  secret: false,
14
+ blink: false,
15
+ rapidBlink: false,
13
16
  };
14
17
  }
15
18
  logs() {
19
+ if (this.state.lines.length > 0) {
20
+ this.processLines();
21
+ }
16
22
  return this.targetElement.innerText;
17
23
  }
18
- addLine(line) {
24
+ processLine(line) {
19
25
  // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequences
20
26
  // eslint-disable-next-line no-control-regex
21
27
  const re = /(?:\x1B|\\x1B)(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1B\\))/g;
22
28
  let i = 0;
23
- if (this.state.carriageReturn) {
24
- if (line !== "\n") {
25
- // don't remove if \r\n
26
- if (this.targetElement.lastChild) {
27
- this.targetElement.removeChild(this.targetElement.lastChild);
28
- }
29
- }
30
- this.state.carriageReturn = false;
31
- }
32
- const hasBareCR = line.endsWith("\r") && !line.endsWith("\r\n");
33
- if (hasBareCR) {
34
- this.state.carriageReturn = true;
35
- }
36
29
  const lineSpan = document.createElement("span");
37
30
  lineSpan.classList.add("line");
38
- this.targetElement.appendChild(lineSpan);
39
31
  const addSpan = (content) => {
40
32
  if (content === "")
41
33
  return;
@@ -50,6 +42,10 @@ export class ColoredConsole {
50
42
  span.classList.add("log-strikethrough");
51
43
  if (this.state.secret)
52
44
  span.classList.add("log-secret");
45
+ if (this.state.blink)
46
+ span.classList.add("log-blink");
47
+ if (this.state.rapidBlink)
48
+ span.classList.add("log-rapid-blink");
53
49
  if (this.state.foregroundColor !== null)
54
50
  span.classList.add(`log-fg-${this.state.foregroundColor}`);
55
51
  if (this.state.backgroundColor !== null)
@@ -83,6 +79,8 @@ export class ColoredConsole {
83
79
  this.state.foregroundColor = null;
84
80
  this.state.backgroundColor = null;
85
81
  this.state.secret = false;
82
+ this.state.blink = false;
83
+ this.state.rapidBlink = false;
86
84
  break;
87
85
  case 1:
88
86
  this.state.bold = true;
@@ -94,10 +92,15 @@ export class ColoredConsole {
94
92
  this.state.underline = true;
95
93
  break;
96
94
  case 5:
97
- this.state.secret = true;
95
+ this.state.blink = true;
96
+ this.state.rapidBlink = false;
98
97
  break;
99
98
  case 6:
100
- this.state.secret = false;
99
+ this.state.rapidBlink = true;
100
+ this.state.blink = false;
101
+ break;
102
+ case 8:
103
+ this.state.secret = true;
101
104
  break;
102
105
  case 9:
103
106
  this.state.strikethrough = true;
@@ -111,6 +114,13 @@ export class ColoredConsole {
111
114
  case 24:
112
115
  this.state.underline = false;
113
116
  break;
117
+ case 25:
118
+ this.state.blink = false;
119
+ this.state.rapidBlink = false;
120
+ break;
121
+ case 28:
122
+ this.state.secret = false;
123
+ break;
114
124
  case 29:
115
125
  this.state.strikethrough = false;
116
126
  break;
@@ -141,6 +151,9 @@ export class ColoredConsole {
141
151
  case 39:
142
152
  this.state.foregroundColor = null;
143
153
  break;
154
+ case 40:
155
+ this.state.backgroundColor = "black";
156
+ break;
144
157
  case 41:
145
158
  this.state.backgroundColor = "red";
146
159
  break;
@@ -162,23 +175,60 @@ export class ColoredConsole {
162
175
  case 47:
163
176
  this.state.backgroundColor = "white";
164
177
  break;
165
- case 40:
166
- this.state.backgroundColor = "black";
167
- break;
168
178
  case 49:
169
179
  this.state.backgroundColor = null;
170
180
  break;
171
181
  }
172
182
  }
173
183
  }
184
+ addSpan(line.substring(i));
185
+ return lineSpan;
186
+ }
187
+ processLines() {
174
188
  const atBottom = this.targetElement.scrollTop >
175
189
  this.targetElement.scrollHeight - this.targetElement.offsetHeight - 50;
176
- addSpan(line.substring(i));
190
+ const prevCarriageReturn = this.state.carriageReturn;
191
+ const fragment = document.createDocumentFragment();
192
+ if (this.state.lines.length === 0) {
193
+ return;
194
+ }
195
+ for (const line of this.state.lines) {
196
+ // A lone \r is a pure carriage-return signal — update state but don't
197
+ // create a DOM node for it (it has no renderable content).
198
+ if (line === "\r") {
199
+ this.state.carriageReturn = true;
200
+ continue;
201
+ }
202
+ if (this.state.carriageReturn && line !== "\n") {
203
+ if (fragment.childElementCount) {
204
+ fragment.removeChild(fragment.lastChild);
205
+ }
206
+ }
207
+ const hadCarriageReturn = line.endsWith("\r");
208
+ fragment.appendChild(this.processLine(line.replace(/\r/g, "")));
209
+ this.state.carriageReturn = hadCarriageReturn;
210
+ }
211
+ if (prevCarriageReturn &&
212
+ fragment.childElementCount > 0 &&
213
+ this.targetElement.lastChild) {
214
+ this.targetElement.replaceChild(fragment, this.targetElement.lastChild);
215
+ }
216
+ else {
217
+ this.targetElement.appendChild(fragment);
218
+ }
219
+ this.state.lines = [];
177
220
  // Keep scroll at bottom
178
221
  if (atBottom) {
179
222
  this.targetElement.scrollTop = this.targetElement.scrollHeight;
180
223
  }
181
224
  }
225
+ addLine(line) {
226
+ // Processing of lines is deferred for performance reasons
227
+ if (this.state.lines.length === 0) {
228
+ setTimeout(() => this.processLines(), 0);
229
+ }
230
+ this.state.lines.push(line);
231
+ }
182
232
  }
183
233
  export const coloredConsoleStyles = `
184
234
  .log {
@@ -211,6 +261,17 @@ export const coloredConsoleStyles = `
211
261
  .log-underline.log-strikethrough {
212
262
  text-decoration: underline line-through;
213
263
  }
264
+ .log-blink {
265
+ animation: blink 1s step-end infinite;
266
+ }
267
+ .log-rapid-blink {
268
+ animation: blink 0.4s step-end infinite;
269
+ }
270
+ @keyframes blink {
271
+ 50% {
272
+ opacity: 0;
273
+ }
274
+ }
214
275
  .log-secret {
215
276
  -webkit-user-select: none;
216
277
  -moz-user-select: none;
@@ -5,10 +5,24 @@ export class LineBreakTransformer {
5
5
  transform(chunk, controller) {
6
6
  // Append new chunks to existing chunks.
7
7
  this.chunks += chunk;
8
- // For each line breaks in chunks, send the parsed lines out.
9
- const lines = this.chunks.split("\r\n");
10
- this.chunks = lines.pop();
11
- lines.forEach((line) => controller.enqueue(line + "\r\n"));
8
+ // Split on \r\n, lone \r, or lone \n — capturing the separator so we can
9
+ // distinguish a lone \r (overwrite intent) from a normal newline.
10
+ const re = /\r\n|\r|\n/g;
11
+ let lastIndex = 0;
12
+ let match;
13
+ while ((match = re.exec(this.chunks)) !== null) {
14
+ // If this is a lone \r at the very end of the buffer, leave it so it can
15
+ // be combined with a possible following \n in the next chunk.
16
+ if (match[0] === "\r" && match.index === this.chunks.length - 1) {
17
+ break;
18
+ }
19
+ const line = this.chunks.substring(lastIndex, match.index);
20
+ // Emit with \r suffix only for lone \r (overwrite), \n for everything else.
21
+ const suffix = match[0] === "\r" ? "\r" : "\n";
22
+ controller.enqueue(line + suffix);
23
+ lastIndex = re.lastIndex;
24
+ }
25
+ this.chunks = this.chunks.substring(lastIndex);
12
26
  }
13
27
  flush(controller) {
14
28
  // When the stream is closed, flush any remaining chunks out.
@@ -0,0 +1,5 @@
1
+ export declare class TimestampTransformer implements Transformer<string, string> {
2
+ private deviceHasTimestamps;
3
+ transform(chunk: string, controller: TransformStreamDefaultController<string>): void;
4
+ reset(): void;
5
+ }
@@ -0,0 +1,39 @@
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
+ //
5
+ // Covered formats:
6
+ // (123456) FreeRTOS ms-tick e.g. "(12345) "
7
+ // [HH:MM:SS] wall-clock bracket
8
+ // [HH:MM:SS.mmm] wall-clock bracket with millis
9
+ // I (1234) tag: ESP-IDF log level + tick e.g. "I (1234) wifi: ..."
10
+ // HH:MM:SS.mmm plain wall-clock
11
+ const DEVICE_TIMESTAMP_RE = /^\s*(?:\(\d+\)\s|\[\d{2}:\d{2}:\d{2}(?:\.\d+)?\]|[DIWEACV] \(\d+\) \w|(?:\d{2}:){2}\d{2}\.\d)/;
12
+ export class TimestampTransformer {
13
+ constructor() {
14
+ this.deviceHasTimestamps = false;
15
+ }
16
+ transform(chunk, controller) {
17
+ // Pass through pure newline / empty sentinel unchanged so that
18
+ // carriage-return overwrite logic in console-color.ts still works.
19
+ if (chunk === "" || chunk === "\n" || chunk === "\r") {
20
+ controller.enqueue(chunk);
21
+ return;
22
+ }
23
+ if (!this.deviceHasTimestamps && DEVICE_TIMESTAMP_RE.test(chunk)) {
24
+ this.deviceHasTimestamps = true;
25
+ }
26
+ if (this.deviceHasTimestamps) {
27
+ controller.enqueue(chunk);
28
+ return;
29
+ }
30
+ const date = new Date();
31
+ const h = date.getHours().toString().padStart(2, "0");
32
+ const m = date.getMinutes().toString().padStart(2, "0");
33
+ const s = date.getSeconds().toString().padStart(2, "0");
34
+ controller.enqueue(`[${h}:${m}:${s}] ${chunk}`);
35
+ }
36
+ reset() {
37
+ this.deviceHasTimestamps = false;
38
+ }
39
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/icons/icon-72.png CHANGED
Binary file
package/icons/icon-96.png CHANGED
Binary file
package/js/console.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ColoredConsole, coloredConsoleStyles } from "./util/console-color.js";
2
2
  import { LineBreakTransformer } from "./util/line-break-transformer.js";
3
+ import { TimestampTransformer } from "./util/timestamp-transformer.js";
3
4
  import { ImprovDialog } from "./improv.js";
4
5
 
5
6
  export class ESP32ToolConsole {
@@ -16,6 +17,11 @@ export class ESP32ToolConsole {
16
17
  this.allowInput = allowInput;
17
18
  this.console = null;
18
19
  this.cancelConnection = null;
20
+ // Command history buffer — keep in sync with src/console.ts
21
+ // (history logic is duplicated there; update both files together)
22
+ this.commandHistory = [];
23
+ this.historyIndex = -1;
24
+ this.currentInput = "";
19
25
  }
20
26
 
21
27
  logs() {
@@ -180,6 +186,16 @@ export class ESP32ToolConsole {
180
186
  ev.preventDefault();
181
187
  ev.stopPropagation();
182
188
  this._sendCommand();
189
+ } else if (ev.key === "ArrowUp") {
190
+ ev.preventDefault();
191
+ this._navigateHistory(1, input);
192
+ } else if (ev.key === "ArrowDown") {
193
+ ev.preventDefault();
194
+ this._navigateHistory(-1, input);
195
+ } else {
196
+ if (this.historyIndex !== -1) {
197
+ this.historyIndex = -1;
198
+ }
183
199
  }
184
200
  });
185
201
  }
@@ -220,16 +236,16 @@ export class ESP32ToolConsole {
220
236
  signal: abortSignal,
221
237
  })
222
238
  .pipeThrough(new TransformStream(new LineBreakTransformer()))
239
+ .pipeThrough(new TransformStream(new TimestampTransformer()))
223
240
  .pipeTo(
224
241
  new WritableStream({
225
242
  write: (chunk) => {
226
- const cleaned = chunk.replace(/\r\n$/, "\n");
227
- this.console.addLine(cleaned);
243
+ this.console.addLine(chunk);
228
244
 
229
245
  if (!bootloaderDetected && lineCount < 30) {
230
246
  lineCount++;
231
247
  for (const pat of BOOTLOADER_PATTERNS) {
232
- if (pat.test(cleaned)) {
248
+ if (pat.test(chunk)) {
233
249
  bootloaderDetected = true;
234
250
  this.containerElement.dispatchEvent(
235
251
  new CustomEvent("console-bootloader", { bubbles: true })
@@ -259,9 +275,42 @@ export class ESP32ToolConsole {
259
275
  }
260
276
  }
261
277
 
278
+ _navigateHistory(direction, input) {
279
+ if (this.commandHistory.length === 0) return;
280
+
281
+ if (this.historyIndex === -1) {
282
+ this.currentInput = input.value;
283
+ }
284
+
285
+ const nextIndex = this.historyIndex + direction;
286
+
287
+ if (nextIndex < 0) {
288
+ this.historyIndex = -1;
289
+ input.value = this.currentInput;
290
+ } else if (nextIndex < this.commandHistory.length) {
291
+ this.historyIndex = nextIndex;
292
+ input.value = this.commandHistory[this.historyIndex];
293
+ }
294
+
295
+ const len = input.value.length;
296
+ input.setSelectionRange(len, len);
297
+ }
298
+
262
299
  async _sendCommand() {
263
300
  const input = this.containerElement.querySelector(".esp32tool-console-input");
264
301
  const command = input.value;
302
+
303
+ if (command.trim() !== "") {
304
+ if (this.commandHistory[0] !== command) {
305
+ this.commandHistory.unshift(command);
306
+ if (this.commandHistory.length > 100) {
307
+ this.commandHistory.pop();
308
+ }
309
+ }
310
+ }
311
+ this.historyIndex = -1;
312
+ this.currentInput = "";
313
+
265
314
  if (!this.port.writable) {
266
315
  this.console.addLine("Terminal disconnected: port not writable");
267
316
  return;