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.
- package/apple-touch-icon.png +0 -0
- package/dist/console.d.ts +4 -0
- package/dist/console.js +54 -2
- package/dist/util/console-color.d.ts +6 -1
- package/dist/util/console-color.js +82 -21
- package/dist/util/line-break-transformer.js +18 -4
- package/dist/util/timestamp-transformer.d.ts +5 -0
- package/dist/util/timestamp-transformer.js +39 -0
- package/icons/icon-128.png +0 -0
- package/icons/icon-144.png +0 -0
- package/icons/icon-152.png +0 -0
- package/icons/icon-192.png +0 -0
- package/icons/icon-384.png +0 -0
- package/icons/icon-512.png +0 -0
- package/icons/icon-72.png +0 -0
- package/icons/icon-96.png +0 -0
- package/js/console.js +52 -3
- package/js/util/console-color.js +236 -212
- package/js/util/line-break-transformer.js +29 -17
- package/js/util/timestamp-transformer.js +39 -0
- package/package.cli.json +1 -1
- package/package.json +7 -6
- package/screenshots/desktop.png +0 -0
- package/screenshots/mobile.png +0 -0
- package/src/console.ts +57 -2
- package/src/util/console-color.ts +89 -23
- package/src/util/line-break-transformer.ts +18 -4
- package/src/util/timestamp-transformer.ts +47 -0
- package/sw.js +1 -1
- package/tsconfig.util.json +9 -0
package/apple-touch-icon.png
CHANGED
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
95
|
+
this.state.blink = true;
|
|
96
|
+
this.state.rapidBlink = false;
|
|
98
97
|
break;
|
|
99
98
|
case 6:
|
|
100
|
-
this.state.
|
|
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
|
-
|
|
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
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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,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
|
+
}
|
package/icons/icon-128.png
CHANGED
|
Binary file
|
package/icons/icon-144.png
CHANGED
|
Binary file
|
package/icons/icon-152.png
CHANGED
|
Binary file
|
package/icons/icon-192.png
CHANGED
|
Binary file
|
package/icons/icon-384.png
CHANGED
|
Binary file
|
package/icons/icon-512.png
CHANGED
|
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
|
-
|
|
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(
|
|
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;
|