Fix for pasting

This commit is contained in:
Andy Dixon
2026-06-02 10:47:59 +01:00
parent 2eed4ea548
commit 74f16bfd0d
3 changed files with 404 additions and 353 deletions
+402 -351
View File
@@ -1,30 +1,31 @@
(function() { (function () {
/** /**
* Nexus Terminal — RAW TCP data viewer. * Nexus Terminal — RAW TCP data viewer.
* Provides text, hex, and split display modes for raw socket data, * Provides text, hex, and split display modes for raw socket data,
* along with a send bar, pause/resume, and capture saving. * along with a send bar, pause/resume, and capture saving.
*/ */
const { createElement, escapeHtml, formatTimestamp, formatBytes } = window.NexusTerminal; const { createElement, escapeHtml, formatTimestamp, formatBytes } =
window.NexusTerminal;
/** /**
* Maximum number of data entries retained in the scrollback buffer. * Maximum number of data entries retained in the scrollback buffer.
* @type {number} * @type {number}
*/ */
const MAX_BUFFER_ENTRIES = 5000; const MAX_BUFFER_ENTRIES = 5000;
class RawViewer { class RawViewer {
/** /**
* @param {App} app * @param {App} app
*/ */
constructor(app) { constructor(app) {
this.app = app; this.app = app;
/** /**
* Map of sessionId → viewer state. * Map of sessionId → viewer state.
* @type {Map<string, Object>} * @type {Map<string, Object>}
*/ */
this.viewers = new Map(); this.viewers = new Map();
} }
// ── Public API ─────────────────────────────────────────────────── // ── Public API ───────────────────────────────────────────────────
@@ -36,71 +37,85 @@ class RawViewer {
* @param {Object} profile * @param {Object} profile
*/ */
createViewer(sessionId, containerId, profile) { createViewer(sessionId, containerId, profile) {
const container = document.getElementById(containerId); const container = document.getElementById(containerId);
if (!container) return; if (!container) return;
const state = { const state = {
mode: 'text', // text | hex | split mode: "text", // text | hex | split
paused: false, paused: false,
buffer: [], buffer: [],
totalBytes: 0, totalBytes: 0,
sendFormat: 'text', // text | hex | escaped sendFormat: "text", // text | hex | escaped
}; };
// ── Toolbar ────────────────────────────────────────────── // ── Toolbar ──────────────────────────────────────────────
const toolbar = createElement('div', 'raw-toolbar'); const toolbar = createElement("div", "raw-toolbar");
// Mode toggle // Mode toggle
const modeGroup = createElement('div', 'raw-mode-group'); const modeGroup = createElement("div", "raw-mode-group");
for (const mode of ['text', 'hex', 'split']) { for (const mode of ["text", "hex", "split"]) {
const btn = createElement('button', `raw-mode-btn ${mode === state.mode ? 'active' : ''}`, { const btn = createElement(
textContent: mode.charAt(0).toUpperCase() + mode.slice(1), "button",
dataset: { mode }, `raw-mode-btn ${mode === state.mode ? "active" : ""}`,
}); {
btn.addEventListener('click', () => this._setMode(sessionId, mode)); textContent: mode.charAt(0).toUpperCase() + mode.slice(1),
modeGroup.appendChild(btn); dataset: { mode },
} },
toolbar.appendChild(modeGroup); );
btn.addEventListener("click", () => this._setMode(sessionId, mode));
modeGroup.appendChild(btn);
}
toolbar.appendChild(modeGroup);
// Controls // Controls
const controls = createElement('div', 'raw-controls'); const controls = createElement("div", "raw-controls");
const pauseBtn = createElement('button', 'btn btn-sm btn-outline raw-pause', { textContent: '⏸ Pause' }); const pauseBtn = createElement(
pauseBtn.addEventListener('click', () => this.togglePause(sessionId)); "button",
controls.appendChild(pauseBtn); "btn btn-sm btn-outline raw-pause",
{ textContent: "⏸ Pause" },
);
pauseBtn.addEventListener("click", () => this.togglePause(sessionId));
controls.appendChild(pauseBtn);
const clearBtn = createElement('button', 'btn btn-sm btn-outline', { textContent: '✕ Clear' }); const clearBtn = createElement("button", "btn btn-sm btn-outline", {
clearBtn.addEventListener('click', () => this.clearBuffer(sessionId)); textContent: "✕ Clear",
controls.appendChild(clearBtn); });
clearBtn.addEventListener("click", () => this.clearBuffer(sessionId));
controls.appendChild(clearBtn);
const saveBtn = createElement('button', 'btn btn-sm btn-outline', { textContent: '💾 Save' }); const saveBtn = createElement("button", "btn btn-sm btn-outline", {
saveBtn.addEventListener('click', () => this.saveCapture(sessionId)); textContent: "💾 Save",
controls.appendChild(saveBtn); });
saveBtn.addEventListener("click", () => this.saveCapture(sessionId));
controls.appendChild(saveBtn);
// Byte counter // Byte counter
const counter = createElement('span', 'raw-counter', { textContent: '0 B' }); const counter = createElement("span", "raw-counter", {
controls.appendChild(counter); textContent: "0 B",
});
controls.appendChild(counter);
toolbar.appendChild(controls); toolbar.appendChild(controls);
container.appendChild(toolbar); container.appendChild(toolbar);
// ── Data display ───────────────────────────────────────── // ── Data display ─────────────────────────────────────────
const display = createElement('div', 'raw-display'); const display = createElement("div", "raw-display");
container.appendChild(display); container.appendChild(display);
// ── Send bar ───────────────────────────────────────────── // ── Send bar ─────────────────────────────────────────────
const sendBar = this._renderSendBar(sessionId, profile); const sendBar = this._renderSendBar(sessionId, profile);
container.appendChild(sendBar); container.appendChild(sendBar);
this.viewers.set(sessionId, { this.viewers.set(sessionId, {
...state, ...state,
container, container,
display, display,
toolbar, toolbar,
pauseBtn, pauseBtn,
counter, counter,
profile, profile,
}); });
} }
/** /**
@@ -108,45 +123,45 @@ class RawViewer {
* @param {Object} payload - { sessionID, data, direction, timestamp } * @param {Object} payload - { sessionID, data, direction, timestamp }
*/ */
handleData(payload) { handleData(payload) {
const viewer = this.viewers.get(payload.sessionID); const viewer = this.viewers.get(payload.sessionID);
if (!viewer) return; if (!viewer) return;
// Decode base64 payload // Decode base64 payload
let rawStr; let rawStr;
try { try {
rawStr = atob(payload.data); rawStr = atob(payload.data);
} catch (_) { } catch (_) {
rawStr = payload.data || ''; rawStr = payload.data || "";
} }
const bytes = new Uint8Array(rawStr.length); const bytes = new Uint8Array(rawStr.length);
for (let i = 0; i < rawStr.length; i++) { for (let i = 0; i < rawStr.length; i++) {
bytes[i] = rawStr.charCodeAt(i); bytes[i] = rawStr.charCodeAt(i);
} }
const entry = { const entry = {
direction: payload.direction || 'recv', // 'send' | 'recv' direction: payload.direction || "recv", // 'send' | 'recv'
timestamp: payload.timestamp ? new Date(payload.timestamp) : new Date(), timestamp: payload.timestamp ? new Date(payload.timestamp) : new Date(),
data: rawStr, data: rawStr,
bytes, bytes,
}; };
viewer.buffer.push(entry); viewer.buffer.push(entry);
viewer.totalBytes += bytes.length; viewer.totalBytes += bytes.length;
// Trim buffer if it exceeds the maximum // Trim buffer if it exceeds the maximum
if (viewer.buffer.length > MAX_BUFFER_ENTRIES) { if (viewer.buffer.length > MAX_BUFFER_ENTRIES) {
viewer.buffer.splice(0, viewer.buffer.length - MAX_BUFFER_ENTRIES); viewer.buffer.splice(0, viewer.buffer.length - MAX_BUFFER_ENTRIES);
} }
// Update counter // Update counter
viewer.counter.textContent = formatBytes(viewer.totalBytes); viewer.counter.textContent = formatBytes(viewer.totalBytes);
// Render unless paused // Render unless paused
if (!viewer.paused) { if (!viewer.paused) {
this._renderEntry(viewer, entry); this._renderEntry(viewer, entry);
this._scrollToBottom(viewer); this._scrollToBottom(viewer);
} }
} }
/** /**
@@ -154,17 +169,17 @@ class RawViewer {
* @param {string} sessionId * @param {string} sessionId
*/ */
togglePause(sessionId) { togglePause(sessionId) {
const viewer = this.viewers.get(sessionId); const viewer = this.viewers.get(sessionId);
if (!viewer) return; if (!viewer) return;
viewer.paused = !viewer.paused; viewer.paused = !viewer.paused;
viewer.pauseBtn.textContent = viewer.paused ? '▶ Resume' : '⏸ Pause'; viewer.pauseBtn.textContent = viewer.paused ? "▶ Resume" : "⏸ Pause";
viewer.pauseBtn.classList.toggle('active', viewer.paused); viewer.pauseBtn.classList.toggle("active", viewer.paused);
// On resume, flush any buffered entries // On resume, flush any buffered entries
if (!viewer.paused) { if (!viewer.paused) {
this._renderAllEntries(viewer); this._renderAllEntries(viewer);
} }
} }
/** /**
@@ -172,13 +187,13 @@ class RawViewer {
* @param {string} sessionId * @param {string} sessionId
*/ */
clearBuffer(sessionId) { clearBuffer(sessionId) {
const viewer = this.viewers.get(sessionId); const viewer = this.viewers.get(sessionId);
if (!viewer) return; if (!viewer) return;
viewer.buffer = []; viewer.buffer = [];
viewer.totalBytes = 0; viewer.totalBytes = 0;
viewer.display.innerHTML = ''; viewer.display.innerHTML = "";
viewer.counter.textContent = '0 B'; viewer.counter.textContent = "0 B";
} }
/** /**
@@ -186,24 +201,24 @@ class RawViewer {
* @param {string} sessionId * @param {string} sessionId
*/ */
async saveCapture(sessionId) { async saveCapture(sessionId) {
const viewer = this.viewers.get(sessionId); const viewer = this.viewers.get(sessionId);
if (!viewer || viewer.buffer.length === 0) { if (!viewer || viewer.buffer.length === 0) {
this.app.notifications.warning('Nothing to save'); this.app.notifications.warning("Nothing to save");
return; return;
} }
try { try {
const lines = viewer.buffer.map((e) => { const lines = viewer.buffer.map((e) => {
const ts = formatTimestamp(e.timestamp); const ts = formatTimestamp(e.timestamp);
const dir = e.direction === 'send' ? 'TX' : 'RX'; const dir = e.direction === "send" ? "TX" : "RX";
return `[${ts}] [${dir}] ${e.data}`; return `[${ts}] [${dir}] ${e.data}`;
}); });
const content = lines.join('\n'); const content = lines.join("\n");
await window.go.api.Service.SaveSessionCapture(sessionId, content); await window.go.api.Service.SaveSessionCapture(sessionId, content);
this.app.notifications.success('Capture saved'); this.app.notifications.success("Capture saved");
} catch (err) { } catch (err) {
this.app.notifications.error('Save failed: ' + err); this.app.notifications.error("Save failed: " + err);
} }
} }
/** /**
@@ -211,49 +226,50 @@ class RawViewer {
* @param {string} sessionId * @param {string} sessionId
*/ */
async send(sessionId) { async send(sessionId) {
const viewer = this.viewers.get(sessionId); const viewer = this.viewers.get(sessionId);
if (!viewer) return; if (!viewer) return;
const input = viewer.container.querySelector('.raw-send-input'); const input = viewer.container.querySelector(".raw-send-input");
const formatSelect = viewer.container.querySelector('.raw-send-format'); const formatSelect = viewer.container.querySelector(".raw-send-format");
if (!input) return; if (!input) return;
const rawValue = input.value; const rawValue = input.value;
if (!rawValue) return; if (!rawValue) return;
const format = formatSelect?.value || 'text'; const format = formatSelect?.value || "text";
let dataToSend; let dataToSend;
switch (format) { switch (format) {
case 'hex': case "hex":
dataToSend = this._hexToString(rawValue); dataToSend = this._hexToString(rawValue);
break; break;
case 'escaped': case "escaped":
dataToSend = this._unescapeString(rawValue); dataToSend = this._unescapeString(rawValue);
break; break;
default: default:
dataToSend = rawValue; dataToSend = rawValue;
} }
try { try {
// For hex/escaped, data is already raw bytes (0-255) so btoa is correct. // For hex/escaped, data is already raw bytes (0-255) so btoa is correct.
// For text mode, UTF-8 encode so non-ASCII characters are transmitted properly. // For text mode, UTF-8 encode so non-ASCII characters are transmitted properly.
const b64 = (format === 'hex' || format === 'escaped') const b64 =
? btoa(dataToSend) format === "hex" || format === "escaped"
: window.NexusTerminal.utf8ToBase64(dataToSend); ? btoa(dataToSend)
await window.go.api.Service.WriteSession(sessionId, b64); : window.NexusTerminal.utf8ToBase64(dataToSend);
input.value = ''; await window.go.api.Service.WriteSession(sessionId, b64);
input.value = "";
// Also show in viewer as sent data // Also show in viewer as sent data
this.handleData({ this.handleData({
sessionID: sessionId, sessionID: sessionId,
data: b64, data: b64,
direction: 'send', direction: "send",
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
} catch (err) { } catch (err) {
this.app.notifications.error('Send failed: ' + err); this.app.notifications.error("Send failed: " + err);
} }
} }
/** /**
@@ -261,7 +277,7 @@ class RawViewer {
* @param {string} sessionId * @param {string} sessionId
*/ */
destroyViewer(sessionId) { destroyViewer(sessionId) {
this.viewers.delete(sessionId); this.viewers.delete(sessionId);
} }
// ── Rendering ──────────────────────────────────────────────────── // ── Rendering ────────────────────────────────────────────────────
@@ -274,76 +290,97 @@ class RawViewer {
* @private * @private
*/ */
_renderSendBar(sessionId, profile) { _renderSendBar(sessionId, profile) {
const bar = createElement('div', 'raw-send-bar'); const bar = createElement("div", "raw-send-bar");
// Format selector // Format selector
const formatSelect = createElement('select', 'form-select form-select-sm raw-send-format'); const formatSelect = createElement(
for (const [value, label] of [['text', 'Text'], ['hex', 'Hex'], ['escaped', 'Escaped']]) { "select",
const opt = createElement('option', '', { value, textContent: label }); "form-select form-select-sm raw-send-format",
formatSelect.appendChild(opt); );
} for (const [value, label] of [
bar.appendChild(formatSelect); ["text", "Text"],
["hex", "Hex"],
["escaped", "Escaped"],
]) {
const opt = createElement("option", "", { value, textContent: label });
formatSelect.appendChild(opt);
}
bar.appendChild(formatSelect);
// Input // Input
const input = createElement('input', 'form-input raw-send-input', { const input = createElement("input", "form-input raw-send-input", {
type: 'text', type: "text",
placeholder: 'Type data to send…', placeholder: "Type data to send…",
});
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") this.send(sessionId);
});
bar.appendChild(input);
// Quick-insert buttons for common control bytes
const quickBtns = createElement("div", "raw-quick-btns");
const controlBytes = [
{ label: "NUL", value: "\\x00" },
{ label: "CR", value: "\\r" },
{ label: "LF", value: "\\n" },
{ label: "ETX", value: "\\x03" },
{ label: "EOT", value: "\\x04" },
];
for (const cb of controlBytes) {
const btn = createElement("button", "btn btn-xs btn-outline", {
textContent: cb.label,
title: cb.value,
}); });
input.addEventListener('keydown', (e) => { btn.addEventListener("click", () => {
if (e.key === 'Enter') this.send(sessionId); input.value += cb.value;
input.focus();
}); });
bar.appendChild(input); quickBtns.appendChild(btn);
}
bar.appendChild(quickBtns);
// Quick-insert buttons for common control bytes // Payload preset dropdown
const quickBtns = createElement('div', 'raw-quick-btns'); const presets = profile?.raw?.payloadPresets || [];
const controlBytes = [ if (presets.length > 0) {
{ label: 'NUL', value: '\\x00' }, const presetSelect = createElement(
{ label: 'CR', value: '\\r' }, "select",
{ label: 'LF', value: '\\n' }, "form-select form-select-sm raw-preset-select",
{ label: 'ETX', value: '\\x03' }, );
{ label: 'EOT', value: '\\x04' }, const defaultOpt = createElement("option", "", {
]; value: "",
for (const cb of controlBytes) { textContent: "Presets…",
const btn = createElement('button', 'btn btn-xs btn-outline', { textContent: cb.label, title: cb.value }); });
btn.addEventListener('click', () => { presetSelect.appendChild(defaultOpt);
input.value += cb.value; for (const ps of presets) {
input.focus(); const opt = createElement("option", "", {
}); value: ps.payload,
quickBtns.appendChild(btn); textContent: ps.name,
});
opt.dataset.format = ps.format;
presetSelect.appendChild(opt);
} }
bar.appendChild(quickBtns); presetSelect.addEventListener("change", () => {
if (presetSelect.value) {
// Payload preset dropdown input.value = presetSelect.value;
const presets = profile?.raw?.payloadPresets || []; const selOpt = presetSelect.selectedOptions[0];
if (presets.length > 0) { if (selOpt?.dataset.format) {
const presetSelect = createElement('select', 'form-select form-select-sm raw-preset-select'); formatSelect.value = selOpt.dataset.format;
const defaultOpt = createElement('option', '', { value: '', textContent: 'Presets…' });
presetSelect.appendChild(defaultOpt);
for (const ps of presets) {
const opt = createElement('option', '', { value: ps.payload, textContent: ps.name });
opt.dataset.format = ps.format;
presetSelect.appendChild(opt);
} }
presetSelect.addEventListener('change', () => { presetSelect.value = "";
if (presetSelect.value) { input.focus();
input.value = presetSelect.value; }
const selOpt = presetSelect.selectedOptions[0]; });
if (selOpt?.dataset.format) { bar.appendChild(presetSelect);
formatSelect.value = selOpt.dataset.format; }
}
presetSelect.value = '';
input.focus();
}
});
bar.appendChild(presetSelect);
}
// Send button // Send button
const sendBtn = createElement('button', 'btn btn-sm btn-primary', { textContent: 'Send' }); const sendBtn = createElement("button", "btn btn-sm btn-primary", {
sendBtn.addEventListener('click', () => this.send(sessionId)); textContent: "Send",
bar.appendChild(sendBtn); });
sendBtn.addEventListener("click", () => this.send(sessionId));
bar.appendChild(sendBtn);
return bar; return bar;
} }
/** /**
@@ -353,18 +390,18 @@ class RawViewer {
* @private * @private
*/ */
_renderEntry(viewer, entry) { _renderEntry(viewer, entry) {
let el; let el;
switch (viewer.mode) { switch (viewer.mode) {
case 'hex': case "hex":
el = this._renderHexView(entry); el = this._renderHexView(entry);
break; break;
case 'split': case "split":
el = this._renderSplitView(entry, viewer.profile); el = this._renderSplitView(entry, viewer.profile);
break; break;
default: default:
el = this._renderTextView(entry, viewer.profile); el = this._renderTextView(entry, viewer.profile);
} }
viewer.display.appendChild(el); viewer.display.appendChild(el);
} }
/** /**
@@ -373,11 +410,11 @@ class RawViewer {
* @private * @private
*/ */
_renderAllEntries(viewer) { _renderAllEntries(viewer) {
viewer.display.innerHTML = ''; viewer.display.innerHTML = "";
for (const entry of viewer.buffer) { for (const entry of viewer.buffer) {
this._renderEntry(viewer, entry); this._renderEntry(viewer, entry);
} }
this._scrollToBottom(viewer); this._scrollToBottom(viewer);
} }
/** /**
@@ -388,21 +425,24 @@ class RawViewer {
* @private * @private
*/ */
_renderTextView(entry, profile) { _renderTextView(entry, profile) {
const row = createElement('div', `raw-entry raw-entry-${entry.direction}`); const row = createElement(
const raw = profile?.raw || {}; "div",
`raw-entry raw-entry-${entry.direction}`,
);
const raw = profile?.raw || {};
let prefix = ''; let prefix = "";
if (raw.showTimestamps) { if (raw.showTimestamps) {
prefix += `[${formatTimestamp(entry.timestamp)}] `; prefix += `[${formatTimestamp(entry.timestamp)}] `;
} }
if (raw.showDirection) { if (raw.showDirection) {
prefix += entry.direction === 'send' ? '[TX] ' : '[RX] '; prefix += entry.direction === "send" ? "[TX] " : "[RX] ";
} }
const pre = createElement('pre', 'raw-text'); const pre = createElement("pre", "raw-text");
pre.textContent = prefix + entry.data; pre.textContent = prefix + entry.data;
row.appendChild(pre); row.appendChild(pre);
return row; return row;
} }
/** /**
@@ -412,11 +452,14 @@ class RawViewer {
* @private * @private
*/ */
_renderHexView(entry) { _renderHexView(entry) {
const row = createElement('div', `raw-entry raw-entry-${entry.direction}`); const row = createElement(
const pre = createElement('pre', 'raw-hex'); "div",
pre.textContent = this.formatHexDump(entry.bytes); `raw-entry raw-entry-${entry.direction}`,
row.appendChild(pre); );
return row; const pre = createElement("pre", "raw-hex");
pre.textContent = this.formatHexDump(entry.bytes);
row.appendChild(pre);
return row;
} }
/** /**
@@ -427,30 +470,34 @@ class RawViewer {
* @private * @private
*/ */
_renderSplitView(entry, profile) { _renderSplitView(entry, profile) {
const row = createElement('div', `raw-entry raw-entry-split raw-entry-${entry.direction}`); const row = createElement(
const raw = profile?.raw || {}; "div",
`raw-entry raw-entry-split raw-entry-${entry.direction}`,
);
const raw = profile?.raw || {};
// Direction/timestamp header // Direction/timestamp header
const header = createElement('div', 'raw-split-header'); const header = createElement("div", "raw-split-header");
let headerText = ''; let headerText = "";
if (raw.showTimestamps) headerText += formatTimestamp(entry.timestamp) + ' '; if (raw.showTimestamps)
headerText += entry.direction === 'send' ? '▲ TX' : '▼ RX'; headerText += formatTimestamp(entry.timestamp) + " ";
header.textContent = headerText; headerText += entry.direction === "send" ? "▲ TX" : "▼ RX";
row.appendChild(header); header.textContent = headerText;
row.appendChild(header);
// Split panes // Split panes
const panes = createElement('div', 'raw-split-panes'); const panes = createElement("div", "raw-split-panes");
const textPane = createElement('pre', 'raw-text raw-split-text'); const textPane = createElement("pre", "raw-text raw-split-text");
textPane.textContent = entry.data; textPane.textContent = entry.data;
panes.appendChild(textPane); panes.appendChild(textPane);
const hexPane = createElement('pre', 'raw-hex raw-split-hex'); const hexPane = createElement("pre", "raw-hex raw-split-hex");
hexPane.textContent = this.formatHexDump(entry.bytes); hexPane.textContent = this.formatHexDump(entry.bytes);
panes.appendChild(hexPane); panes.appendChild(hexPane);
row.appendChild(panes); row.appendChild(panes);
return row; return row;
} }
/** /**
@@ -460,39 +507,39 @@ class RawViewer {
* @returns {string} * @returns {string}
*/ */
formatHexDump(bytes) { formatHexDump(bytes) {
const lines = []; const lines = [];
const bytesPerLine = 16; const bytesPerLine = 16;
for (let offset = 0; offset < bytes.length; offset += bytesPerLine) { for (let offset = 0; offset < bytes.length; offset += bytesPerLine) {
const chunk = bytes.slice(offset, offset + bytesPerLine); const chunk = bytes.slice(offset, offset + bytesPerLine);
// Offset column // Offset column
const offsetStr = offset.toString(16).padStart(8, '0'); const offsetStr = offset.toString(16).padStart(8, "0");
// Hex column // Hex column
const hexParts = []; const hexParts = [];
for (let i = 0; i < bytesPerLine; i++) { for (let i = 0; i < bytesPerLine; i++) {
if (i < chunk.length) { if (i < chunk.length) {
hexParts.push(chunk[i].toString(16).padStart(2, '0')); hexParts.push(chunk[i].toString(16).padStart(2, "0"));
} else { } else {
hexParts.push(' '); hexParts.push(" ");
} }
// Extra space after 8th byte for readability // Extra space after 8th byte for readability
if (i === 7) hexParts.push(''); if (i === 7) hexParts.push("");
} }
const hexStr = hexParts.join(' '); const hexStr = hexParts.join(" ");
// ASCII column // ASCII column
let ascii = ''; let ascii = "";
for (let i = 0; i < chunk.length; i++) { for (let i = 0; i < chunk.length; i++) {
const b = chunk[i]; const b = chunk[i];
ascii += (b >= 0x20 && b <= 0x7e) ? String.fromCharCode(b) : '.'; ascii += b >= 0x20 && b <= 0x7e ? String.fromCharCode(b) : ".";
}
lines.push(`${offsetStr} ${hexStr} |${ascii}|`);
} }
return lines.join('\n'); lines.push(`${offsetStr} ${hexStr} |${ascii}|`);
}
return lines.join("\n");
} }
// ── Internal helpers ───────────────────────────────────────────── // ── Internal helpers ─────────────────────────────────────────────
@@ -504,17 +551,17 @@ class RawViewer {
* @private * @private
*/ */
_setMode(sessionId, mode) { _setMode(sessionId, mode) {
const viewer = this.viewers.get(sessionId); const viewer = this.viewers.get(sessionId);
if (!viewer) return; if (!viewer) return;
viewer.mode = mode; viewer.mode = mode;
// Update active button // Update active button
viewer.toolbar.querySelectorAll('.raw-mode-btn').forEach((btn) => { viewer.toolbar.querySelectorAll(".raw-mode-btn").forEach((btn) => {
btn.classList.toggle('active', btn.dataset.mode === mode); btn.classList.toggle("active", btn.dataset.mode === mode);
}); });
this._renderAllEntries(viewer); this._renderAllEntries(viewer);
} }
/** /**
@@ -523,9 +570,9 @@ class RawViewer {
* @private * @private
*/ */
_scrollToBottom(viewer) { _scrollToBottom(viewer) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
viewer.display.scrollTop = viewer.display.scrollHeight; viewer.display.scrollTop = viewer.display.scrollHeight;
}); });
} }
/** /**
@@ -535,12 +582,12 @@ class RawViewer {
* @private * @private
*/ */
_hexToString(hex) { _hexToString(hex) {
const cleaned = hex.replace(/\s+/g, ''); const cleaned = hex.replace(/\s+/g, "");
let str = ''; let str = "";
for (let i = 0; i < cleaned.length; i += 2) { for (let i = 0; i < cleaned.length; i += 2) {
str += String.fromCharCode(parseInt(cleaned.substring(i, i + 2), 16)); str += String.fromCharCode(parseInt(cleaned.substring(i, i + 2), 16));
} }
return str; return str;
} }
/** /**
@@ -550,25 +597,29 @@ class RawViewer {
* @private * @private
*/ */
_unescapeString(str) { _unescapeString(str) {
return str.replace(/\\(n|r|t|0|x[0-9a-fA-F]{2}|\\)/g, (match, seq) => { return str.replace(/\\(n|r|t|0|x[0-9a-fA-F]{2}|\\)/g, (match, seq) => {
switch (seq) { switch (seq) {
case 'n': return '\n'; case "n":
case 'r': return '\r'; return "\n";
case 't': return '\t'; case "r":
case '0': return '\0'; return "\r";
case '\\': return '\\'; case "t":
default: return "\t";
// \\xHH case "0":
if (seq.startsWith('x')) { return "\0";
return String.fromCharCode(parseInt(seq.slice(1), 16)); case "\\":
} return "\\";
return match; default:
// \\xHH
if (seq.startsWith("x")) {
return String.fromCharCode(parseInt(seq.slice(1), 16));
} }
}); return match;
}
});
} }
} }
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.RawViewer = RawViewer;
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.RawViewer = RawViewer;
})(); })();
+1 -1
View File
@@ -14,7 +14,7 @@
*/ */
function utf8ToBase64(str) { function utf8ToBase64(str) {
const bytes = new TextEncoder().encode(str); const bytes = new TextEncoder().encode(str);
let binary = ''; let binary = "";
for (let i = 0; i < bytes.length; i++) { for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]); binary += String.fromCharCode(bytes[i]);
} }
+1 -1
View File
@@ -171,7 +171,7 @@
*/ */
function utf8ToBase64(str) { function utf8ToBase64(str) {
const bytes = new TextEncoder().encode(str); const bytes = new TextEncoder().encode(str);
let binary = ''; let binary = "";
for (let i = 0; i < bytes.length; i++) { for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]); binary += String.fromCharCode(bytes[i]);
} }