Fix for pasting
This commit is contained in:
+402
-351
@@ -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;
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user