esp32tool 1.6.6 → 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/apple-touch-icon.png +0 -0
- package/css/style.css +325 -25
- package/dist/util/console-color.js +3 -3
- package/dist/util/timestamp-transformer.js +24 -1
- package/electron/cli-main.cjs +19 -19
- package/electron/main.cjs +167 -148
- package/electron/preload.js +16 -18
- 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 +21 -12
- package/js/hex-editor.js +216 -163
- package/js/improv.js +59 -21
- package/js/nvs-editor.js +1189 -182
- package/js/script.js +1048 -845
- package/js/util/console-color.js +3 -3
- package/js/util/timestamp-transformer.js +24 -1
- package/js/webusb-serial.js +1075 -950
- package/package.cli.json +2 -2
- package/package.json +11 -12
- package/screenshots/desktop.png +0 -0
- package/screenshots/mobile.png +0 -0
- package/src/util/console-color.ts +3 -3
- package/src/util/timestamp-transformer.ts +27 -1
- package/sw.js +1 -1
package/js/hex-editor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ESP32Tool Flash Hex Editor
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* A full-screen hex editor for viewing and editing flash memory content.
|
|
5
5
|
* Features:
|
|
6
6
|
* - Dual-pane display: HEX (left) + ASCII (right)
|
|
@@ -26,10 +26,10 @@ export class HexEditor {
|
|
|
26
26
|
this.rowHeight = 20;
|
|
27
27
|
this.selectedOffset = -1;
|
|
28
28
|
this.editingPane = null; // 'hex' or 'ascii'
|
|
29
|
-
this.editBuffer =
|
|
29
|
+
this.editBuffer = ""; // partial hex nibble during hex editing
|
|
30
30
|
|
|
31
31
|
// Search state
|
|
32
|
-
this.searchMatches = [];
|
|
32
|
+
this.searchMatches = []; // array of byte offsets
|
|
33
33
|
this.currentMatchIdx = -1;
|
|
34
34
|
this._searchMatchLength = 0;
|
|
35
35
|
this._searchAbort = null; // AbortController for cancelling in-progress search
|
|
@@ -82,12 +82,12 @@ export class HexEditor {
|
|
|
82
82
|
this.currentMatchIdx = -1;
|
|
83
83
|
this.selectedOffset = 0;
|
|
84
84
|
this.editingPane = null;
|
|
85
|
-
this.editBuffer =
|
|
85
|
+
this.editBuffer = "";
|
|
86
86
|
|
|
87
87
|
this._buildUI();
|
|
88
|
-
this.container.classList.remove(
|
|
89
|
-
document.body.classList.add(
|
|
90
|
-
document.addEventListener(
|
|
88
|
+
this.container.classList.remove("hidden");
|
|
89
|
+
document.body.classList.add("hexeditor-active");
|
|
90
|
+
document.addEventListener("keydown", this._boundHandleKeyDown);
|
|
91
91
|
|
|
92
92
|
this._calculateLayout();
|
|
93
93
|
this._render();
|
|
@@ -104,9 +104,9 @@ export class HexEditor {
|
|
|
104
104
|
|
|
105
105
|
/** Close hex editor */
|
|
106
106
|
close() {
|
|
107
|
-
this.container.classList.add(
|
|
108
|
-
document.body.classList.remove(
|
|
109
|
-
document.removeEventListener(
|
|
107
|
+
this.container.classList.add("hidden");
|
|
108
|
+
document.body.classList.remove("hexeditor-active");
|
|
109
|
+
document.removeEventListener("keydown", this._boundHandleKeyDown);
|
|
110
110
|
if (this._resizeObserver) {
|
|
111
111
|
this._resizeObserver.disconnect();
|
|
112
112
|
this._resizeObserver = null;
|
|
@@ -115,23 +115,23 @@ export class HexEditor {
|
|
|
115
115
|
this._searchAbort.abort();
|
|
116
116
|
this._searchAbort = null;
|
|
117
117
|
}
|
|
118
|
-
this.container.innerHTML =
|
|
118
|
+
this.container.innerHTML = "";
|
|
119
119
|
if (this.onClose) this.onClose();
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/** Show loading overlay */
|
|
123
123
|
showProgress(text, percent) {
|
|
124
124
|
if (this._progressOverlay) {
|
|
125
|
-
this._progressOverlay.classList.remove(
|
|
125
|
+
this._progressOverlay.classList.remove("hidden");
|
|
126
126
|
this._progressText.textContent = text;
|
|
127
|
-
this._progressBarInner.style.width = percent +
|
|
127
|
+
this._progressBarInner.style.width = percent + "%";
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/** Hide loading overlay */
|
|
132
132
|
hideProgress() {
|
|
133
133
|
if (this._progressOverlay) {
|
|
134
|
-
this._progressOverlay.classList.add(
|
|
134
|
+
this._progressOverlay.classList.add("hidden");
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -162,18 +162,19 @@ export class HexEditor {
|
|
|
162
162
|
</div>
|
|
163
163
|
</div>
|
|
164
164
|
`;
|
|
165
|
-
this._progressOverlay = this.container.querySelector(
|
|
166
|
-
this._progressText = this.container.querySelector(
|
|
167
|
-
this._progressBarInner = this.container.querySelector(
|
|
165
|
+
this._progressOverlay = this.container.querySelector("#hexedProgress");
|
|
166
|
+
this._progressText = this.container.querySelector("#hexedProgressText");
|
|
167
|
+
this._progressBarInner = this.container.querySelector("#hexedProgressBar");
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
_buildUI() {
|
|
171
171
|
const totalSize = this.data ? this.data.length : 0;
|
|
172
|
-
const sizeStr =
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
const sizeStr =
|
|
173
|
+
totalSize >= 1024 * 1024
|
|
174
|
+
? (totalSize / (1024 * 1024)).toFixed(1) + " MB"
|
|
175
|
+
: totalSize >= 1024
|
|
176
|
+
? (totalSize / 1024).toFixed(1) + " KB"
|
|
177
|
+
: totalSize + " B";
|
|
177
178
|
|
|
178
179
|
this.container.innerHTML = `
|
|
179
180
|
<div class="hexeditor-toolbar">
|
|
@@ -222,47 +223,58 @@ export class HexEditor {
|
|
|
222
223
|
`;
|
|
223
224
|
|
|
224
225
|
// Cache DOM references
|
|
225
|
-
this._viewport = this.container.querySelector(
|
|
226
|
-
this._scrollContent = this.container.querySelector(
|
|
227
|
-
this._statusOffset = this.container.querySelector(
|
|
228
|
-
this._statusValue = this.container.querySelector(
|
|
229
|
-
this._statusModified = this.container.querySelector(
|
|
230
|
-
this._searchInput = this.container.querySelector(
|
|
231
|
-
this._searchMode = this.container.querySelector(
|
|
232
|
-
this._searchInfo = this.container.querySelector(
|
|
233
|
-
this._gotoInput = this.container.querySelector(
|
|
234
|
-
this._progressOverlay = this.container.querySelector(
|
|
235
|
-
this._progressText = this.container.querySelector(
|
|
236
|
-
this._progressBarInner = this.container.querySelector(
|
|
237
|
-
this._butWrite = this.container.querySelector(
|
|
226
|
+
this._viewport = this.container.querySelector("#hexedViewport");
|
|
227
|
+
this._scrollContent = this.container.querySelector("#hexedScrollContent");
|
|
228
|
+
this._statusOffset = this.container.querySelector("#hexedStatusOffset");
|
|
229
|
+
this._statusValue = this.container.querySelector("#hexedStatusValue");
|
|
230
|
+
this._statusModified = this.container.querySelector("#hexedStatusModified");
|
|
231
|
+
this._searchInput = this.container.querySelector("#hexedSearch");
|
|
232
|
+
this._searchMode = this.container.querySelector("#hexedSearchMode");
|
|
233
|
+
this._searchInfo = this.container.querySelector("#hexedSearchInfo");
|
|
234
|
+
this._gotoInput = this.container.querySelector("#hexedGoto");
|
|
235
|
+
this._progressOverlay = this.container.querySelector("#hexedProgress");
|
|
236
|
+
this._progressText = this.container.querySelector("#hexedProgressText");
|
|
237
|
+
this._progressBarInner = this.container.querySelector("#hexedProgressBar");
|
|
238
|
+
this._butWrite = this.container.querySelector("#hexedWrite");
|
|
238
239
|
|
|
239
240
|
// Event listeners
|
|
240
|
-
this.container
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
241
|
+
this.container
|
|
242
|
+
.querySelector("#hexedClose")
|
|
243
|
+
.addEventListener("click", () => {
|
|
244
|
+
if (this.hasModifications()) {
|
|
245
|
+
if (!confirm("You have unsaved modifications. Close anyway?")) return;
|
|
246
|
+
}
|
|
247
|
+
this.close();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
this._butWrite.addEventListener("click", () => this._handleWrite());
|
|
251
|
+
|
|
252
|
+
this.container
|
|
253
|
+
.querySelector("#hexedUndoAll")
|
|
254
|
+
.addEventListener("click", () => {
|
|
255
|
+
if (this.modifiedOffsets.size === 0) return;
|
|
256
|
+
if (!confirm(`Undo all ${this.modifiedOffsets.size} modifications?`))
|
|
257
|
+
return;
|
|
258
|
+
for (const offset of this.modifiedOffsets) {
|
|
259
|
+
this.data[offset] = this.originalData[offset];
|
|
260
|
+
}
|
|
261
|
+
this.modifiedOffsets.clear();
|
|
262
|
+
this._render();
|
|
263
|
+
this._updateStatus();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
this.container
|
|
267
|
+
.querySelector("#hexedSearchBtn")
|
|
268
|
+
.addEventListener("click", () => this._doSearch());
|
|
269
|
+
this.container
|
|
270
|
+
.querySelector("#hexedSearchPrev")
|
|
271
|
+
.addEventListener("click", () => this._navigateSearch(-1));
|
|
272
|
+
this.container
|
|
273
|
+
.querySelector("#hexedSearchNext")
|
|
274
|
+
.addEventListener("click", () => this._navigateSearch(1));
|
|
275
|
+
|
|
276
|
+
this._searchInput.addEventListener("keydown", (e) => {
|
|
277
|
+
if (e.key === "Enter") {
|
|
266
278
|
e.preventDefault();
|
|
267
279
|
if (this.searchMatches.length > 0) {
|
|
268
280
|
this._navigateSearch(1);
|
|
@@ -273,23 +285,30 @@ export class HexEditor {
|
|
|
273
285
|
});
|
|
274
286
|
|
|
275
287
|
// Clear search on input change
|
|
276
|
-
this._searchInput.addEventListener(
|
|
288
|
+
this._searchInput.addEventListener("input", () => {
|
|
277
289
|
this.searchMatches = [];
|
|
278
290
|
this.currentMatchIdx = -1;
|
|
279
|
-
this._searchInfo.textContent =
|
|
291
|
+
this._searchInfo.textContent = "";
|
|
280
292
|
this._render();
|
|
281
293
|
});
|
|
282
294
|
|
|
283
|
-
this.container
|
|
284
|
-
|
|
285
|
-
|
|
295
|
+
this.container
|
|
296
|
+
.querySelector("#hexedGotoBtn")
|
|
297
|
+
.addEventListener("click", () => this._doGoto());
|
|
298
|
+
this._gotoInput.addEventListener("keydown", (e) => {
|
|
299
|
+
if (e.key === "Enter") {
|
|
300
|
+
e.preventDefault();
|
|
301
|
+
this._doGoto();
|
|
302
|
+
}
|
|
286
303
|
});
|
|
287
304
|
|
|
288
305
|
// Virtual scroll
|
|
289
|
-
this._viewport.addEventListener(
|
|
306
|
+
this._viewport.addEventListener("scroll", () => this._onScroll());
|
|
290
307
|
|
|
291
308
|
// Click handler for cells
|
|
292
|
-
this._scrollContent.addEventListener(
|
|
309
|
+
this._scrollContent.addEventListener("mousedown", (e) =>
|
|
310
|
+
this._handleCellClick(e),
|
|
311
|
+
);
|
|
293
312
|
}
|
|
294
313
|
|
|
295
314
|
// ──────────────────── Layout & Virtual Scroll ────────────────────
|
|
@@ -300,8 +319,8 @@ export class HexEditor {
|
|
|
300
319
|
const totalHeight = this._totalRows * this.rowHeight;
|
|
301
320
|
|
|
302
321
|
// Set scroll height
|
|
303
|
-
this._scrollContent.style.height = totalHeight +
|
|
304
|
-
this._scrollContent.style.position =
|
|
322
|
+
this._scrollContent.style.height = totalHeight + "px";
|
|
323
|
+
this._scrollContent.style.position = "relative";
|
|
305
324
|
|
|
306
325
|
// Calculate visible rows
|
|
307
326
|
const vpHeight = this._viewport.clientHeight;
|
|
@@ -320,7 +339,8 @@ export class HexEditor {
|
|
|
320
339
|
_scrollToOffset(byteOffset) {
|
|
321
340
|
const row = Math.floor(byteOffset / this.bytesPerRow);
|
|
322
341
|
const vpHeight = this._viewport.clientHeight;
|
|
323
|
-
const targetScroll =
|
|
342
|
+
const targetScroll =
|
|
343
|
+
row * this.rowHeight - vpHeight / 2 + this.rowHeight / 2;
|
|
324
344
|
this._viewport.scrollTop = Math.max(0, targetScroll);
|
|
325
345
|
this._visibleStart = Math.floor(this._viewport.scrollTop / this.rowHeight);
|
|
326
346
|
this._render();
|
|
@@ -338,11 +358,11 @@ export class HexEditor {
|
|
|
338
358
|
const fragment = document.createDocumentFragment();
|
|
339
359
|
|
|
340
360
|
// Container for positioned rows
|
|
341
|
-
const wrapper = document.createElement(
|
|
342
|
-
wrapper.style.position =
|
|
343
|
-
wrapper.style.top =
|
|
344
|
-
wrapper.style.left =
|
|
345
|
-
wrapper.style.right =
|
|
361
|
+
const wrapper = document.createElement("div");
|
|
362
|
+
wrapper.style.position = "absolute";
|
|
363
|
+
wrapper.style.top = start * this.rowHeight + "px";
|
|
364
|
+
wrapper.style.left = "0";
|
|
365
|
+
wrapper.style.right = "0";
|
|
346
366
|
|
|
347
367
|
for (let row = start; row < end; row++) {
|
|
348
368
|
const rowEl = this._createRow(row);
|
|
@@ -351,39 +371,45 @@ export class HexEditor {
|
|
|
351
371
|
|
|
352
372
|
// Replace content (keep scroll height div)
|
|
353
373
|
// Remove old rendered wrapper if present
|
|
354
|
-
const oldWrapper = this._scrollContent.querySelector(
|
|
374
|
+
const oldWrapper = this._scrollContent.querySelector(".hex-rows-wrapper");
|
|
355
375
|
if (oldWrapper) oldWrapper.remove();
|
|
356
|
-
wrapper.className =
|
|
376
|
+
wrapper.className = "hex-rows-wrapper";
|
|
357
377
|
this._scrollContent.appendChild(wrapper);
|
|
358
378
|
}
|
|
359
379
|
|
|
360
380
|
_createRow(rowIndex) {
|
|
361
|
-
const row = document.createElement(
|
|
362
|
-
row.className =
|
|
381
|
+
const row = document.createElement("div");
|
|
382
|
+
row.className = "hexeditor-row";
|
|
363
383
|
const byteStart = rowIndex * this.bytesPerRow;
|
|
364
384
|
|
|
365
385
|
// Highlight row if selected offset is in this row
|
|
366
|
-
if (
|
|
367
|
-
|
|
386
|
+
if (
|
|
387
|
+
this.selectedOffset >= byteStart &&
|
|
388
|
+
this.selectedOffset < byteStart + this.bytesPerRow
|
|
389
|
+
) {
|
|
390
|
+
row.classList.add("highlight-row");
|
|
368
391
|
}
|
|
369
392
|
|
|
370
393
|
// Address
|
|
371
|
-
const addr = document.createElement(
|
|
372
|
-
addr.className =
|
|
373
|
-
addr.textContent = (this.baseAddress + byteStart)
|
|
394
|
+
const addr = document.createElement("span");
|
|
395
|
+
addr.className = "hexeditor-addr";
|
|
396
|
+
addr.textContent = (this.baseAddress + byteStart)
|
|
397
|
+
.toString(16)
|
|
398
|
+
.toUpperCase()
|
|
399
|
+
.padStart(8, "0");
|
|
374
400
|
row.appendChild(addr);
|
|
375
401
|
|
|
376
402
|
// Hex cells
|
|
377
|
-
const hexDiv = document.createElement(
|
|
378
|
-
hexDiv.className =
|
|
403
|
+
const hexDiv = document.createElement("span");
|
|
404
|
+
hexDiv.className = "hexeditor-hex";
|
|
379
405
|
|
|
380
406
|
// Separator
|
|
381
|
-
const sep = document.createElement(
|
|
382
|
-
sep.className =
|
|
407
|
+
const sep = document.createElement("span");
|
|
408
|
+
sep.className = "hexeditor-sep";
|
|
383
409
|
|
|
384
410
|
// ASCII cells
|
|
385
|
-
const asciiDiv = document.createElement(
|
|
386
|
-
asciiDiv.className =
|
|
411
|
+
const asciiDiv = document.createElement("span");
|
|
412
|
+
asciiDiv.className = "hexeditor-ascii";
|
|
387
413
|
|
|
388
414
|
const bytesInRow = Math.min(this.bytesPerRow, this.data.length - byteStart);
|
|
389
415
|
|
|
@@ -391,58 +417,58 @@ export class HexEditor {
|
|
|
391
417
|
const offset = byteStart + i;
|
|
392
418
|
|
|
393
419
|
// Hex cell
|
|
394
|
-
const hexCell = document.createElement(
|
|
395
|
-
hexCell.className =
|
|
420
|
+
const hexCell = document.createElement("span");
|
|
421
|
+
hexCell.className = "hex-cell";
|
|
396
422
|
hexCell.dataset.offset = offset;
|
|
397
|
-
hexCell.dataset.pane =
|
|
423
|
+
hexCell.dataset.pane = "hex";
|
|
398
424
|
|
|
399
425
|
// ASCII cell
|
|
400
|
-
const asciiCell = document.createElement(
|
|
401
|
-
asciiCell.className =
|
|
426
|
+
const asciiCell = document.createElement("span");
|
|
427
|
+
asciiCell.className = "ascii-cell";
|
|
402
428
|
asciiCell.dataset.offset = offset;
|
|
403
|
-
asciiCell.dataset.pane =
|
|
429
|
+
asciiCell.dataset.pane = "ascii";
|
|
404
430
|
|
|
405
431
|
if (i < bytesInRow) {
|
|
406
432
|
const byte = this.data[offset];
|
|
407
|
-
const hexStr = byte.toString(16).toUpperCase().padStart(2,
|
|
433
|
+
const hexStr = byte.toString(16).toUpperCase().padStart(2, "0");
|
|
408
434
|
hexCell.textContent = hexStr;
|
|
409
435
|
|
|
410
436
|
// Color classes
|
|
411
|
-
if (byte === 0x00) hexCell.classList.add(
|
|
412
|
-
else if (byte ===
|
|
437
|
+
if (byte === 0x00) hexCell.classList.add("zero");
|
|
438
|
+
else if (byte === 0xff) hexCell.classList.add("ff");
|
|
413
439
|
|
|
414
440
|
// ASCII char
|
|
415
|
-
if (byte >= 0x20 && byte <=
|
|
441
|
+
if (byte >= 0x20 && byte <= 0x7e) {
|
|
416
442
|
asciiCell.textContent = String.fromCharCode(byte);
|
|
417
443
|
} else {
|
|
418
|
-
asciiCell.textContent =
|
|
419
|
-
asciiCell.classList.add(
|
|
444
|
+
asciiCell.textContent = "·";
|
|
445
|
+
asciiCell.classList.add("non-printable");
|
|
420
446
|
}
|
|
421
447
|
|
|
422
448
|
// Modified?
|
|
423
449
|
if (this.modifiedOffsets.has(offset)) {
|
|
424
|
-
hexCell.classList.add(
|
|
425
|
-
asciiCell.classList.add(
|
|
450
|
+
hexCell.classList.add("modified");
|
|
451
|
+
asciiCell.classList.add("modified");
|
|
426
452
|
}
|
|
427
453
|
|
|
428
454
|
// Selected?
|
|
429
455
|
if (offset === this.selectedOffset) {
|
|
430
|
-
hexCell.classList.add(
|
|
431
|
-
asciiCell.classList.add(
|
|
456
|
+
hexCell.classList.add("selected");
|
|
457
|
+
asciiCell.classList.add("selected");
|
|
432
458
|
}
|
|
433
459
|
|
|
434
460
|
// Search match?
|
|
435
461
|
if (this._isSearchMatch(offset)) {
|
|
436
|
-
hexCell.classList.add(
|
|
437
|
-
asciiCell.classList.add(
|
|
462
|
+
hexCell.classList.add("search-match");
|
|
463
|
+
asciiCell.classList.add("search-match");
|
|
438
464
|
}
|
|
439
465
|
if (this._isCurrentSearchMatch(offset)) {
|
|
440
|
-
hexCell.classList.add(
|
|
441
|
-
asciiCell.classList.add(
|
|
466
|
+
hexCell.classList.add("search-current");
|
|
467
|
+
asciiCell.classList.add("search-current");
|
|
442
468
|
}
|
|
443
469
|
} else {
|
|
444
|
-
hexCell.textContent =
|
|
445
|
-
asciiCell.textContent =
|
|
470
|
+
hexCell.textContent = " ";
|
|
471
|
+
asciiCell.textContent = " ";
|
|
446
472
|
}
|
|
447
473
|
|
|
448
474
|
hexDiv.appendChild(hexCell);
|
|
@@ -458,7 +484,7 @@ export class HexEditor {
|
|
|
458
484
|
// ──────────────────── Selection & Editing ────────────────────
|
|
459
485
|
|
|
460
486
|
_handleCellClick(e) {
|
|
461
|
-
const cell = e.target.closest(
|
|
487
|
+
const cell = e.target.closest("[data-offset]");
|
|
462
488
|
if (!cell) return;
|
|
463
489
|
|
|
464
490
|
const offset = parseInt(cell.dataset.offset);
|
|
@@ -466,7 +492,7 @@ export class HexEditor {
|
|
|
466
492
|
|
|
467
493
|
this.selectedOffset = offset;
|
|
468
494
|
this.editingPane = cell.dataset.pane;
|
|
469
|
-
this.editBuffer =
|
|
495
|
+
this.editBuffer = "";
|
|
470
496
|
this._render();
|
|
471
497
|
this._updateStatus();
|
|
472
498
|
|
|
@@ -476,64 +502,68 @@ export class HexEditor {
|
|
|
476
502
|
|
|
477
503
|
_handleKeyDown(e) {
|
|
478
504
|
// Only handle when hex editor is visible
|
|
479
|
-
if (this.container.classList.contains(
|
|
505
|
+
if (this.container.classList.contains("hidden")) return;
|
|
480
506
|
|
|
481
507
|
// Don't intercept when focus is in search/goto inputs
|
|
482
|
-
if (e.target.tagName ===
|
|
508
|
+
if (e.target.tagName === "INPUT" || e.target.tagName === "SELECT") return;
|
|
483
509
|
|
|
484
510
|
const offset = this.selectedOffset;
|
|
485
511
|
if (offset < 0 || !this.data) return;
|
|
486
512
|
|
|
487
513
|
// Navigation keys
|
|
488
514
|
switch (e.key) {
|
|
489
|
-
case
|
|
515
|
+
case "ArrowRight":
|
|
490
516
|
e.preventDefault();
|
|
491
517
|
this._selectOffset(Math.min(offset + 1, this.data.length - 1));
|
|
492
518
|
return;
|
|
493
|
-
case
|
|
519
|
+
case "ArrowLeft":
|
|
494
520
|
e.preventDefault();
|
|
495
521
|
this._selectOffset(Math.max(offset - 1, 0));
|
|
496
522
|
return;
|
|
497
|
-
case
|
|
523
|
+
case "ArrowDown":
|
|
498
524
|
e.preventDefault();
|
|
499
|
-
this._selectOffset(
|
|
525
|
+
this._selectOffset(
|
|
526
|
+
Math.min(offset + this.bytesPerRow, this.data.length - 1),
|
|
527
|
+
);
|
|
500
528
|
return;
|
|
501
|
-
case
|
|
529
|
+
case "ArrowUp":
|
|
502
530
|
e.preventDefault();
|
|
503
531
|
this._selectOffset(Math.max(offset - this.bytesPerRow, 0));
|
|
504
532
|
return;
|
|
505
|
-
case
|
|
533
|
+
case "PageDown":
|
|
506
534
|
e.preventDefault();
|
|
507
|
-
this._selectOffset(
|
|
535
|
+
this._selectOffset(
|
|
536
|
+
Math.min(offset + this.bytesPerRow * 16, this.data.length - 1),
|
|
537
|
+
);
|
|
508
538
|
return;
|
|
509
|
-
case
|
|
539
|
+
case "PageUp":
|
|
510
540
|
e.preventDefault();
|
|
511
541
|
this._selectOffset(Math.max(offset - this.bytesPerRow * 16, 0));
|
|
512
542
|
return;
|
|
513
|
-
case
|
|
543
|
+
case "Home":
|
|
514
544
|
if (e.ctrlKey || e.metaKey) {
|
|
515
545
|
e.preventDefault();
|
|
516
546
|
this._selectOffset(0);
|
|
517
547
|
}
|
|
518
548
|
return;
|
|
519
|
-
case
|
|
549
|
+
case "End":
|
|
520
550
|
if (e.ctrlKey || e.metaKey) {
|
|
521
551
|
e.preventDefault();
|
|
522
552
|
this._selectOffset(this.data.length - 1);
|
|
523
553
|
}
|
|
524
554
|
return;
|
|
525
|
-
case
|
|
555
|
+
case "Tab":
|
|
526
556
|
e.preventDefault();
|
|
527
|
-
this.editingPane = this.editingPane ===
|
|
528
|
-
this.editBuffer =
|
|
557
|
+
this.editingPane = this.editingPane === "hex" ? "ascii" : "hex";
|
|
558
|
+
this.editBuffer = "";
|
|
529
559
|
this._render();
|
|
530
560
|
return;
|
|
531
561
|
}
|
|
532
562
|
|
|
533
563
|
// Editing
|
|
534
|
-
if (this.editingPane ===
|
|
564
|
+
if (this.editingPane === "hex") {
|
|
535
565
|
this._handleHexEdit(e);
|
|
536
|
-
} else if (this.editingPane ===
|
|
566
|
+
} else if (this.editingPane === "ascii") {
|
|
537
567
|
this._handleAsciiEdit(e);
|
|
538
568
|
}
|
|
539
569
|
}
|
|
@@ -547,9 +577,11 @@ export class HexEditor {
|
|
|
547
577
|
if (this.editBuffer.length === 2) {
|
|
548
578
|
const newByte = parseInt(this.editBuffer, 16);
|
|
549
579
|
this._setByte(this.selectedOffset, newByte);
|
|
550
|
-
this.editBuffer =
|
|
580
|
+
this.editBuffer = "";
|
|
551
581
|
// Move to next byte
|
|
552
|
-
this._selectOffset(
|
|
582
|
+
this._selectOffset(
|
|
583
|
+
Math.min(this.selectedOffset + 1, this.data.length - 1),
|
|
584
|
+
);
|
|
553
585
|
} else {
|
|
554
586
|
// Show partial edit feedback – re-render to update status
|
|
555
587
|
this._updateStatus();
|
|
@@ -561,7 +593,7 @@ export class HexEditor {
|
|
|
561
593
|
e.preventDefault();
|
|
562
594
|
|
|
563
595
|
const newByte = e.key.charCodeAt(0);
|
|
564
|
-
if (newByte < 0x20 || newByte >
|
|
596
|
+
if (newByte < 0x20 || newByte > 0x7e) return; // only printable ASCII
|
|
565
597
|
|
|
566
598
|
this._setByte(this.selectedOffset, newByte);
|
|
567
599
|
this._selectOffset(Math.min(this.selectedOffset + 1, this.data.length - 1));
|
|
@@ -569,7 +601,8 @@ export class HexEditor {
|
|
|
569
601
|
|
|
570
602
|
_setByte(offset, value) {
|
|
571
603
|
if (offset < 0 || offset >= this.data.length) return;
|
|
572
|
-
if (this.data[offset] === value && !this.modifiedOffsets.has(offset))
|
|
604
|
+
if (this.data[offset] === value && !this.modifiedOffsets.has(offset))
|
|
605
|
+
return;
|
|
573
606
|
|
|
574
607
|
this.data[offset] = value;
|
|
575
608
|
|
|
@@ -586,7 +619,7 @@ export class HexEditor {
|
|
|
586
619
|
|
|
587
620
|
_selectOffset(offset) {
|
|
588
621
|
this.selectedOffset = offset;
|
|
589
|
-
this.editBuffer =
|
|
622
|
+
this.editBuffer = "";
|
|
590
623
|
|
|
591
624
|
// Ensure visible
|
|
592
625
|
const row = Math.floor(offset / this.bytesPerRow);
|
|
@@ -613,10 +646,14 @@ export class HexEditor {
|
|
|
613
646
|
const mode = this._searchMode.value;
|
|
614
647
|
let searchBytes;
|
|
615
648
|
|
|
616
|
-
if (mode ===
|
|
649
|
+
if (mode === "hex") {
|
|
617
650
|
// Parse hex string: allow spaces, commas
|
|
618
|
-
const cleaned = query.replace(/[,\s]/g,
|
|
619
|
-
if (
|
|
651
|
+
const cleaned = query.replace(/[,\s]/g, "");
|
|
652
|
+
if (
|
|
653
|
+
!/^[0-9a-fA-F]*$/.test(cleaned) ||
|
|
654
|
+
cleaned.length === 0 ||
|
|
655
|
+
cleaned.length % 2 !== 0
|
|
656
|
+
) {
|
|
620
657
|
this._searchInfo.textContent = 'Invalid hex (e.g. "48 65 6C 6C 6F")';
|
|
621
658
|
return;
|
|
622
659
|
}
|
|
@@ -643,7 +680,7 @@ export class HexEditor {
|
|
|
643
680
|
|
|
644
681
|
this.searchMatches = [];
|
|
645
682
|
this.currentMatchIdx = -1;
|
|
646
|
-
this._searchInfo.textContent =
|
|
683
|
+
this._searchInfo.textContent = "Searching...";
|
|
647
684
|
this._render();
|
|
648
685
|
|
|
649
686
|
// Non-blocking chunked search to keep UI responsive
|
|
@@ -655,7 +692,7 @@ export class HexEditor {
|
|
|
655
692
|
|
|
656
693
|
if (this.searchMatches.length > 0) {
|
|
657
694
|
// Jump to first match at or after current selection
|
|
658
|
-
let idx = this.searchMatches.findIndex(m => m >= this.selectedOffset);
|
|
695
|
+
let idx = this.searchMatches.findIndex((m) => m >= this.selectedOffset);
|
|
659
696
|
if (idx === -1) idx = 0;
|
|
660
697
|
this.currentMatchIdx = idx;
|
|
661
698
|
this._searchInfo.textContent = `${idx + 1} / ${this.searchMatches.length} matches`;
|
|
@@ -663,7 +700,7 @@ export class HexEditor {
|
|
|
663
700
|
this._scrollToOffset(this.selectedOffset);
|
|
664
701
|
} else {
|
|
665
702
|
this.currentMatchIdx = -1;
|
|
666
|
-
this._searchInfo.textContent =
|
|
703
|
+
this._searchInfo.textContent = "No matches found";
|
|
667
704
|
}
|
|
668
705
|
|
|
669
706
|
this._render();
|
|
@@ -692,7 +729,10 @@ export class HexEditor {
|
|
|
692
729
|
}
|
|
693
730
|
|
|
694
731
|
const processChunk = () => {
|
|
695
|
-
if (signal.aborted) {
|
|
732
|
+
if (signal.aborted) {
|
|
733
|
+
resolve([]);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
696
736
|
|
|
697
737
|
const end = Math.min(pos + CHUNK, len);
|
|
698
738
|
while (pos <= end) {
|
|
@@ -724,8 +764,10 @@ export class HexEditor {
|
|
|
724
764
|
if (this.searchMatches.length === 0) return;
|
|
725
765
|
|
|
726
766
|
this.currentMatchIdx += direction;
|
|
727
|
-
if (this.currentMatchIdx >= this.searchMatches.length)
|
|
728
|
-
|
|
767
|
+
if (this.currentMatchIdx >= this.searchMatches.length)
|
|
768
|
+
this.currentMatchIdx = 0;
|
|
769
|
+
if (this.currentMatchIdx < 0)
|
|
770
|
+
this.currentMatchIdx = this.searchMatches.length - 1;
|
|
729
771
|
|
|
730
772
|
this.selectedOffset = this.searchMatches[this.currentMatchIdx];
|
|
731
773
|
this._searchInfo.textContent = `${this.currentMatchIdx + 1} / ${this.searchMatches.length} matches`;
|
|
@@ -737,11 +779,15 @@ export class HexEditor {
|
|
|
737
779
|
_isSearchMatch(offset) {
|
|
738
780
|
if (this.searchMatches.length === 0) return false;
|
|
739
781
|
const len = this._searchMatchLength || 1;
|
|
740
|
-
return this.searchMatches.some(m => offset >= m && offset < m + len);
|
|
782
|
+
return this.searchMatches.some((m) => offset >= m && offset < m + len);
|
|
741
783
|
}
|
|
742
784
|
|
|
743
785
|
_isCurrentSearchMatch(offset) {
|
|
744
|
-
if (
|
|
786
|
+
if (
|
|
787
|
+
this.currentMatchIdx < 0 ||
|
|
788
|
+
this.currentMatchIdx >= this.searchMatches.length
|
|
789
|
+
)
|
|
790
|
+
return false;
|
|
745
791
|
const m = this.searchMatches[this.currentMatchIdx];
|
|
746
792
|
const len = this._searchMatchLength || 1;
|
|
747
793
|
return offset >= m && offset < m + len;
|
|
@@ -750,15 +796,17 @@ export class HexEditor {
|
|
|
750
796
|
// ──────────────────── Go To Address ────────────────────
|
|
751
797
|
|
|
752
798
|
_doGoto() {
|
|
753
|
-
const val = this._gotoInput.value.trim().replace(/^0x/i,
|
|
799
|
+
const val = this._gotoInput.value.trim().replace(/^0x/i, "");
|
|
754
800
|
const addr = parseInt(val, 16);
|
|
755
801
|
if (isNaN(addr)) return;
|
|
756
802
|
|
|
757
803
|
// Convert absolute address to offset
|
|
758
804
|
const offset = addr >= this.baseAddress ? addr - this.baseAddress : addr;
|
|
759
805
|
if (offset < 0 || offset >= this.data.length) {
|
|
760
|
-
this._gotoInput.style.borderColor =
|
|
761
|
-
setTimeout(() => {
|
|
806
|
+
this._gotoInput.style.borderColor = "#c62828";
|
|
807
|
+
setTimeout(() => {
|
|
808
|
+
this._gotoInput.style.borderColor = "";
|
|
809
|
+
}, 1000);
|
|
762
810
|
return;
|
|
763
811
|
}
|
|
764
812
|
|
|
@@ -773,28 +821,29 @@ export class HexEditor {
|
|
|
773
821
|
const off = this.selectedOffset;
|
|
774
822
|
const absAddr = this.baseAddress + off;
|
|
775
823
|
const byte = this.data[off];
|
|
776
|
-
const hexStr = byte.toString(16).toUpperCase().padStart(2,
|
|
824
|
+
const hexStr = byte.toString(16).toUpperCase().padStart(2, "0");
|
|
777
825
|
const dec = byte;
|
|
778
|
-
const bin = byte.toString(2).padStart(8,
|
|
779
|
-
const chr =
|
|
826
|
+
const bin = byte.toString(2).padStart(8, "0");
|
|
827
|
+
const chr =
|
|
828
|
+
byte >= 0x20 && byte <= 0x7e ? `'${String.fromCharCode(byte)}'` : "-";
|
|
829
|
+
|
|
830
|
+
this._statusOffset.textContent = `Offset: 0x${absAddr.toString(16).toUpperCase().padStart(8, "0")} (${off})`;
|
|
780
831
|
|
|
781
|
-
this._statusOffset.textContent = `Offset: 0x${absAddr.toString(16).toUpperCase().padStart(8, '0')} (${off})`;
|
|
782
|
-
|
|
783
832
|
let valueStr = `Hex: 0x${hexStr} | Dec: ${dec} | Bin: ${bin} | Char: ${chr}`;
|
|
784
833
|
if (this.editBuffer.length > 0) {
|
|
785
834
|
valueStr += ` [typing: ${this.editBuffer}_]`;
|
|
786
835
|
}
|
|
787
836
|
this._statusValue.textContent = valueStr;
|
|
788
837
|
} else {
|
|
789
|
-
this._statusOffset.textContent =
|
|
790
|
-
this._statusValue.textContent =
|
|
838
|
+
this._statusOffset.textContent = "Offset: -";
|
|
839
|
+
this._statusValue.textContent = "Value: -";
|
|
791
840
|
}
|
|
792
841
|
|
|
793
842
|
if (this.modifiedOffsets.size > 0) {
|
|
794
843
|
this._statusModified.textContent = `● ${this.modifiedOffsets.size} byte(s) modified`;
|
|
795
844
|
this._butWrite.disabled = false;
|
|
796
845
|
} else {
|
|
797
|
-
this._statusModified.textContent =
|
|
846
|
+
this._statusModified.textContent = "";
|
|
798
847
|
this._butWrite.disabled = true;
|
|
799
848
|
}
|
|
800
849
|
}
|
|
@@ -804,18 +853,22 @@ export class HexEditor {
|
|
|
804
853
|
async _handleWrite() {
|
|
805
854
|
if (this.modifiedOffsets.size === 0) return;
|
|
806
855
|
if (!this.onWriteFlash) {
|
|
807
|
-
alert(
|
|
856
|
+
alert("Write handler not configured");
|
|
808
857
|
return;
|
|
809
858
|
}
|
|
810
859
|
|
|
811
860
|
const count = this.modifiedOffsets.size;
|
|
812
|
-
if (
|
|
861
|
+
if (
|
|
862
|
+
!confirm(
|
|
863
|
+
`Write ${count} modified byte(s) to flash?\n\nThis will erase and reprogram affected sectors.`,
|
|
864
|
+
)
|
|
865
|
+
) {
|
|
813
866
|
return;
|
|
814
867
|
}
|
|
815
868
|
|
|
816
869
|
try {
|
|
817
870
|
this._butWrite.disabled = true;
|
|
818
|
-
this.showProgress(
|
|
871
|
+
this.showProgress("Writing changes to flash...", 0);
|
|
819
872
|
await this.onWriteFlash(this.data, this.modifiedOffsets);
|
|
820
873
|
|
|
821
874
|
// Update original snapshot after successful write
|
|
@@ -827,7 +880,7 @@ export class HexEditor {
|
|
|
827
880
|
} catch (err) {
|
|
828
881
|
this.hideProgress();
|
|
829
882
|
this._butWrite.disabled = false;
|
|
830
|
-
alert(
|
|
883
|
+
alert("Write failed: " + (err.message || err));
|
|
831
884
|
}
|
|
832
885
|
}
|
|
833
886
|
}
|