Refactor SnippetLibrary and State classes for improved readability and consistency

- Updated formatting and indentation in snippets.js for better code clarity.
- Enhanced error handling and notifications in sendToActiveSession method.
- Improved variable resolution logic for command snippets.
- Added new methods for managing session state in state.js.
- Introduced new split pane functionality in terminal.js for better user experience.
- Enhanced modal handling and user notifications across various components.
This commit is contained in:
Andy Dixon
2026-05-21 12:15:49 +01:00
parent 421bdb9ee8
commit 523d9b02b4
6 changed files with 588 additions and 418 deletions
+40 -11
View File
@@ -604,7 +604,9 @@
*/ */
selectBroadcastTargets() { selectBroadcastTargets() {
const { createElement, escapeHtml } = window.NexusTerminal; const { createElement, escapeHtml } = window.NexusTerminal;
const sessions = this.state.getAllSessions().filter( const sessions = this.state
.getAllSessions()
.filter(
(s) => s.state === "connected" && s.id !== this.state.activeSessionId, (s) => s.state === "connected" && s.id !== this.state.activeSessionId,
); );
@@ -620,8 +622,13 @@
let listHtml = ""; let listHtml = "";
for (const s of sessions) { for (const s of sessions) {
const profile = this.state.getProfile(s.profileId); const profile = this.state.getProfile(s.profileId);
const label = profile ? (profile.displayName || profile.host) : s.id.substring(0, 8); const label = profile
const checked = currentTargets.size === 0 || currentTargets.has(s.id) ? "checked" : ""; ? profile.displayName || profile.host
: s.id.substring(0, 8);
const checked =
currentTargets.size === 0 || currentTargets.has(s.id)
? "checked"
: "";
listHtml += ` listHtml += `
<label style="display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) 0;font-size:var(--text-sm);cursor:pointer;"> <label style="display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) 0;font-size:var(--text-sm);cursor:pointer;">
<input type="checkbox" class="broadcast-target-check" data-session-id="${s.id}" ${checked} /> <input type="checkbox" class="broadcast-target-check" data-session-id="${s.id}" ${checked} />
@@ -663,10 +670,14 @@
modal.querySelector(".modal-close").addEventListener("click", close); modal.querySelector(".modal-close").addEventListener("click", close);
modal.querySelector(".broadcast-cancel").addEventListener("click", close); modal.querySelector(".broadcast-cancel").addEventListener("click", close);
backdrop.addEventListener("click", (e) => { if (e.target === backdrop) close(); }); backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) close();
});
modal.querySelector(".broadcast-all").addEventListener("click", () => { modal.querySelector(".broadcast-all").addEventListener("click", () => {
modal.querySelectorAll(".broadcast-target-check").forEach((cb) => { cb.checked = true; }); modal.querySelectorAll(".broadcast-target-check").forEach((cb) => {
cb.checked = true;
});
}); });
modal.querySelector(".broadcast-apply").addEventListener("click", () => { modal.querySelector(".broadcast-apply").addEventListener("click", () => {
@@ -682,11 +693,15 @@
} }
// If all are selected, use null (broadcast to all) // If all are selected, use null (broadcast to all)
this.state.broadcastTargets = selected.size === sessions.length ? null : selected; this.state.broadcastTargets =
selected.size === sessions.length ? null : selected;
if (!this.state.broadcastMode) { if (!this.state.broadcastMode) {
this.toggleBroadcast(); this.toggleBroadcast();
} else { } else {
const targetLabel = selected.size === sessions.length ? "all sessions" : selected.size + " sessions"; const targetLabel =
selected.size === sessions.length
? "all sessions"
: selected.size + " sessions";
this.notifications.info("Broadcasting to " + targetLabel); this.notifications.info("Broadcasting to " + targetLabel);
} }
close(); close();
@@ -1001,7 +1016,11 @@
// Latency indicator — only show for connected SSH sessions. // Latency indicator — only show for connected SSH sessions.
if (latencyEl) { if (latencyEl) {
if (activeSession && activeSession.state === "connected" && activeSession.protocol === "ssh") { if (
activeSession &&
activeSession.state === "connected" &&
activeSession.protocol === "ssh"
) {
this._startLatencyPing(activeSession.id); this._startLatencyPing(activeSession.id);
} else { } else {
this._stopLatencyPing(); this._stopLatencyPing();
@@ -1043,12 +1062,20 @@
const el = document.getElementById("status-latency"); const el = document.getElementById("status-latency");
if (el && this._latencySessionID === sessionID) { if (el && this._latencySessionID === sessionID) {
el.textContent = ms + " ms"; el.textContent = ms + " ms";
el.className = ms < 100 ? "latency-good" : ms < 250 ? "latency-warn" : "latency-bad"; el.className =
ms < 100
? "latency-good"
: ms < 250
? "latency-warn"
: "latency-bad";
} }
} catch (_) { } catch (_) {
// Session may have disconnected — clear indicator. // Session may have disconnected — clear indicator.
const el = document.getElementById("status-latency"); const el = document.getElementById("status-latency");
if (el) { el.textContent = ""; el.className = ""; } if (el) {
el.textContent = "";
el.className = "";
}
this._stopLatencyPing(); this._stopLatencyPing();
} }
}; };
@@ -1370,7 +1397,9 @@
const re = new RegExp(query, caseSensitive ? "g" : "gi"); const re = new RegExp(query, caseSensitive ? "g" : "gi");
const m = text.match(re); const m = text.match(re);
if (m) total += m.length; if (m) total += m.length;
} catch (_) { /* invalid regex */ } } catch (_) {
/* invalid regex */
}
} else { } else {
const searchText = caseSensitive ? text : text.toLowerCase(); const searchText = caseSensitive ? text : text.toLowerCase();
const searchQuery = caseSensitive ? query : query.toLowerCase(); const searchQuery = caseSensitive ? query : query.toLowerCase();
+14
View File
@@ -194,6 +194,20 @@ class CommandPalette {
// Terminal actions (only when a session is active) // Terminal actions (only when a session is active)
if (this.app.state.activeSessionId) { if (this.app.state.activeSessionId) {
cmds.push({
id: 'split-vertical',
label: 'Split Pane Right',
section: 'Terminal',
shortcut: '⌘⇧D',
action: () => this.app.splitPane('vertical'),
});
cmds.push({
id: 'split-horizontal',
label: 'Split Pane Down',
section: 'Terminal',
shortcut: '⌘⇧E',
action: () => this.app.splitPane('horizontal'),
});
cmds.push({ cmds.push({
id: 'clear-terminal', id: 'clear-terminal',
label: 'Clear Terminal', label: 'Clear Terminal',
+75 -24
View File
@@ -236,7 +236,9 @@
actions.appendChild(goToBtn); actions.appendChild(goToBtn);
var bookmarkBtn = this._toolbarBtn("★", "Bookmarks", ""); var bookmarkBtn = this._toolbarBtn("★", "Bookmarks", "");
bookmarkBtn.addEventListener("click", () => this._showBookmarks(sessionId)); bookmarkBtn.addEventListener("click", () =>
this._showBookmarks(sessionId),
);
actions.appendChild(bookmarkBtn); actions.appendChild(bookmarkBtn);
var mkdirBtn = this._toolbarBtn("+", "New folder", "is-accent"); var mkdirBtn = this._toolbarBtn("+", "New folder", "is-accent");
@@ -496,12 +498,17 @@
} }
} else { } else {
// Single click without modifier — clear all, select this one // Single click without modifier — clear all, select this one
state.el.querySelectorAll(".sftp-file-row.is-selected").forEach(function (r) { state.el
.querySelectorAll(".sftp-file-row.is-selected")
.forEach(function (r) {
r.classList.remove("is-selected"); r.classList.remove("is-selected");
}); });
// If clicking the same entry (and it was the only selection), deselect // If clicking the same entry (and it was the only selection), deselect
if (state.selectedFiles.size === 1 && state.selectedFiles.has(entry.path)) { if (
state.selectedFiles.size === 1 &&
state.selectedFiles.has(entry.path)
) {
state.selectedFiles.clear(); state.selectedFiles.clear();
state.selected = null; state.selected = null;
var ctxBar = this._getCtxBar(sessionId); var ctxBar = this._getCtxBar(sessionId);
@@ -540,14 +547,23 @@
var label = createElement("span", "sftp-ctx-label", { var label = createElement("span", "sftp-ctx-label", {
textContent: isSingle ? entry.name : count + " items selected", textContent: isSingle ? entry.name : count + " items selected",
title: isSingle ? entry.path : entries.map(function (e) { return e.name; }).join(", "), title: isSingle
? entry.path
: entries
.map(function (e) {
return e.name;
})
.join(", "),
}); });
ctxBar.appendChild(label); ctxBar.appendChild(label);
// Download button — only for files (single or batch) // Download button — only for files (single or batch)
var downloadableFiles = entries.filter(function (e) { return !e.isDir; }); var downloadableFiles = entries.filter(function (e) {
return !e.isDir;
});
if (downloadableFiles.length > 0) { if (downloadableFiles.length > 0) {
var dlLabel = downloadableFiles.length === 1 var dlLabel =
downloadableFiles.length === 1
? "↓ Download" ? "↓ Download"
: "↓ Download (" + downloadableFiles.length + ")"; : "↓ Download (" + downloadableFiles.length + ")";
var dlBtn = createElement("button", "sftp-ctx-btn is-accent", { var dlBtn = createElement("button", "sftp-ctx-btn is-accent", {
@@ -664,14 +680,19 @@
} }
} }
if (failed === 0) { if (failed === 0) {
this.app.notifications.success("All " + files.length + " files downloaded"); this.app.notifications.success(
"All " + files.length + " files downloaded",
);
} }
} }
_confirmDeleteBatch(sessionId, entries) { _confirmDeleteBatch(sessionId, entries) {
this._showConfirmModal({ this._showConfirmModal({
title: "Delete " + entries.length + " Items", title: "Delete " + entries.length + " Items",
message: "Permanently delete <strong>" + entries.length + " selected items</strong>?", message:
"Permanently delete <strong>" +
entries.length +
" selected items</strong>?",
confirmLabel: "Delete All", confirmLabel: "Delete All",
isDanger: true, isDanger: true,
onConfirm: function () { onConfirm: function () {
@@ -913,13 +934,25 @@
var listHtml = ""; var listHtml = "";
if (bookmarks.length === 0) { if (bookmarks.length === 0) {
listHtml = '<div style="color:var(--text-tertiary);font-size:var(--text-sm);text-align:center;padding:var(--space-3);">No bookmarks yet. Navigate to a directory and click "Bookmark Current".</div>'; listHtml =
'<div style="color:var(--text-tertiary);font-size:var(--text-sm);text-align:center;padding:var(--space-3);">No bookmarks yet. Navigate to a directory and click "Bookmark Current".</div>';
} else { } else {
for (var i = 0; i < bookmarks.length; i++) { for (var i = 0; i < bookmarks.length; i++) {
listHtml += '<div class="sftp-bookmark-row" data-index="' + i + '" data-path="' + escapeHtml(bookmarks[i]) + '">'; listHtml +=
listHtml += '<span style="flex:1;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escapeHtml(bookmarks[i]) + '</span>'; '<div class="sftp-bookmark-row" data-index="' +
listHtml += '<button class="btn btn-xs btn-ghost sftp-bookmark-del" data-index="' + i + '" title="Remove">✕</button>'; i +
listHtml += '</div>'; '" data-path="' +
escapeHtml(bookmarks[i]) +
'">';
listHtml +=
'<span style="flex:1;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' +
escapeHtml(bookmarks[i]) +
"</span>";
listHtml +=
'<button class="btn btn-xs btn-ghost sftp-bookmark-del" data-index="' +
i +
'" title="Remove">✕</button>';
listHtml += "</div>";
} }
} }
@@ -927,14 +960,16 @@
'<div class="modal-header">' + '<div class="modal-header">' +
'<span class="modal-title">Directory Bookmarks</span>' + '<span class="modal-title">Directory Bookmarks</span>' +
'<button class="modal-close btn btn-icon btn-xs btn-ghost">✕</button>' + '<button class="modal-close btn btn-icon btn-xs btn-ghost">✕</button>' +
'</div>' + "</div>" +
'<div class="modal-body">' + '<div class="modal-body">' +
'<div class="sftp-bookmark-list">' + listHtml + '</div>' + '<div class="sftp-bookmark-list">' +
'</div>' + listHtml +
"</div>" +
"</div>" +
'<div class="modal-footer">' + '<div class="modal-footer">' +
'<button class="btn btn-sm btn-ghost sftp-modal-cancel">Close</button>' + '<button class="btn btn-sm btn-ghost sftp-modal-cancel">Close</button>' +
'<button class="btn btn-sm btn-primary sftp-bookmark-add">★ Bookmark Current</button>' + '<button class="btn btn-sm btn-primary sftp-bookmark-add">★ Bookmark Current</button>' +
'</div>'; "</div>";
backdrop.appendChild(modal); backdrop.appendChild(modal);
document.body.appendChild(backdrop); document.body.appendChild(backdrop);
@@ -950,12 +985,18 @@
closed = true; closed = true;
backdrop.classList.remove("is-visible"); backdrop.classList.remove("is-visible");
modal.classList.remove("is-visible"); modal.classList.remove("is-visible");
setTimeout(function () { backdrop.remove(); }, 200); setTimeout(function () {
backdrop.remove();
}, 200);
} }
modal.querySelector(".modal-close").addEventListener("click", close); modal.querySelector(".modal-close").addEventListener("click", close);
modal.querySelector(".sftp-modal-cancel").addEventListener("click", close); modal
backdrop.addEventListener("click", function (e) { if (e.target === backdrop) close(); }); .querySelector(".sftp-modal-cancel")
.addEventListener("click", close);
backdrop.addEventListener("click", function (e) {
if (e.target === backdrop) close();
});
// Navigate to bookmark on click // Navigate to bookmark on click
modal.querySelectorAll(".sftp-bookmark-row").forEach(function (row) { modal.querySelectorAll(".sftp-bookmark-row").forEach(function (row) {
@@ -965,7 +1006,9 @@
close(); close();
self._navigate(sessionId, path); self._navigate(sessionId, path);
}); });
row.addEventListener("mousedown", function (e) { e.preventDefault(); }); row.addEventListener("mousedown", function (e) {
e.preventDefault();
});
}); });
// Delete bookmark // Delete bookmark
@@ -980,7 +1023,9 @@
}); });
// Add current directory as bookmark // Add current directory as bookmark
modal.querySelector(".sftp-bookmark-add").addEventListener("click", function () { modal
.querySelector(".sftp-bookmark-add")
.addEventListener("click", function () {
self._addBookmark(sessionId, state.cwd); self._addBookmark(sessionId, state.cwd);
close(); close();
self._showBookmarks(sessionId); self._showBookmarks(sessionId);
@@ -1001,7 +1046,10 @@
var bookmarks = this._getBookmarks(sessionId); var bookmarks = this._getBookmarks(sessionId);
if (bookmarks.indexOf(path) === -1) { if (bookmarks.indexOf(path) === -1) {
bookmarks.push(path); bookmarks.push(path);
localStorage.setItem("sftp-bookmarks-" + sessionId, JSON.stringify(bookmarks)); localStorage.setItem(
"sftp-bookmarks-" + sessionId,
JSON.stringify(bookmarks),
);
this.app.notifications.success("Bookmarked: " + path); this.app.notifications.success("Bookmarked: " + path);
} else { } else {
this.app.notifications.info("Already bookmarked"); this.app.notifications.info("Already bookmarked");
@@ -1011,7 +1059,10 @@
_removeBookmark(sessionId, index) { _removeBookmark(sessionId, index) {
var bookmarks = this._getBookmarks(sessionId); var bookmarks = this._getBookmarks(sessionId);
bookmarks.splice(index, 1); bookmarks.splice(index, 1);
localStorage.setItem("sftp-bookmarks-" + sessionId, JSON.stringify(bookmarks)); localStorage.setItem(
"sftp-bookmarks-" + sessionId,
JSON.stringify(bookmarks),
);
} }
/* ── Transfer progress queue ─────────────────────────────────── */ /* ── Transfer progress queue ─────────────────────────────────── */
+136 -98
View File
@@ -1,12 +1,13 @@
(function() { (function () {
/** /**
* Nexus Terminal — snippet / command library. * Nexus Terminal — snippet / command library.
* Provides a panel for managing and sending reusable commands to active sessions. * Provides a panel for managing and sending reusable commands to active sessions.
*/ */
const { createElement, escapeHtml, dismissContextMenus } = window.NexusTerminal; const { createElement, escapeHtml, dismissContextMenus } =
window.NexusTerminal;
class SnippetLibrary { class SnippetLibrary {
/** /**
* @param {App} app * @param {App} app
*/ */
@@ -14,7 +15,7 @@ class SnippetLibrary {
this.app = app; this.app = app;
/** @type {Array<Object>} */ /** @type {Array<Object>} */
this.snippets = []; this.snippets = [];
this.searchQuery = ''; this.searchQuery = "";
} }
// ── Public API ─────────────────────────────────────────────────── // ── Public API ───────────────────────────────────────────────────
@@ -36,12 +37,12 @@ class SnippetLibrary {
sendToActiveSession(command) { sendToActiveSession(command) {
const sessionId = this.app.state.activeSessionId; const sessionId = this.app.state.activeSessionId;
if (!sessionId) { if (!sessionId) {
this.app.notifications.error('No active session'); this.app.notifications.error("No active session");
return; return;
} }
const session = this.app.state.getSession(sessionId); const session = this.app.state.getSession(sessionId);
if (!session || session.state !== 'connected') { if (!session || session.state !== "connected") {
this.app.notifications.error('Session is not connected'); this.app.notifications.error("Session is not connected");
return; return;
} }
@@ -55,9 +56,9 @@ class SnippetLibrary {
// Resolve built-in variables first // Resolve built-in variables first
const builtins = { const builtins = {
hostname: (session.host || 'unknown'), hostname: session.host || "unknown",
date: new Date().toISOString().split('T')[0], date: new Date().toISOString().split("T")[0],
time: new Date().toTimeString().split(' ')[0], time: new Date().toTimeString().split(" ")[0],
timestamp: Math.floor(Date.now() / 1000).toString(), timestamp: Math.floor(Date.now() / 1000).toString(),
}; };
@@ -66,7 +67,7 @@ class SnippetLibrary {
for (const varName of matches) { for (const varName of matches) {
const lower = varName.toLowerCase(); const lower = varName.toLowerCase();
if (builtins[lower] !== undefined) { if (builtins[lower] !== undefined) {
resolved = resolved.replace('{{' + varName + '}}', builtins[lower]); resolved = resolved.replace("{{" + varName + "}}", builtins[lower]);
} else if (!userVars.includes(varName)) { } else if (!userVars.includes(varName)) {
userVars.push(varName); userVars.push(varName);
} }
@@ -81,7 +82,7 @@ class SnippetLibrary {
this._promptVariables(userVars, (values) => { this._promptVariables(userVars, (values) => {
let final = resolved; let final = resolved;
for (let i = 0; i < userVars.length; i++) { for (let i = 0; i < userVars.length; i++) {
final = final.split('{{' + userVars[i] + '}}').join(values[i] || ''); final = final.split("{{" + userVars[i] + "}}").join(values[i] || "");
} }
this._sendCommand(sessionId, final); this._sendCommand(sessionId, final);
}); });
@@ -90,20 +91,20 @@ class SnippetLibrary {
/** @private */ /** @private */
_sendCommand(sessionId, command) { _sendCommand(sessionId, command) {
try { try {
const b64 = btoa(command + '\n'); const b64 = btoa(command + "\n");
window.go.api.Service.WriteSession(sessionId, b64); window.go.api.Service.WriteSession(sessionId, b64);
this.app.notifications.info('Snippet sent'); this.app.notifications.info("Snippet sent");
} catch (err) { } catch (err) {
this.app.notifications.error('Failed to send snippet: ' + err); this.app.notifications.error("Failed to send snippet: " + err);
} }
} }
/** @private */ /** @private */
_promptVariables(varNames, onConfirm) { _promptVariables(varNames, onConfirm) {
const backdrop = createElement('div', 'modal-backdrop'); const backdrop = createElement("div", "modal-backdrop");
const modal = createElement('div', 'modal modal-sm'); const modal = createElement("div", "modal modal-sm");
let inputsHtml = ''; let inputsHtml = "";
for (const v of varNames) { for (const v of varNames) {
inputsHtml += ` inputsHtml += `
<div class="form-group" style="margin-bottom:var(--space-2);"> <div class="form-group" style="margin-bottom:var(--space-2);">
@@ -128,36 +129,40 @@ class SnippetLibrary {
backdrop.appendChild(modal); backdrop.appendChild(modal);
document.body.appendChild(backdrop); document.body.appendChild(backdrop);
requestAnimationFrame(() => { requestAnimationFrame(() => {
backdrop.classList.add('is-visible'); backdrop.classList.add("is-visible");
modal.classList.add('is-visible'); modal.classList.add("is-visible");
const firstInput = modal.querySelector('.snippet-var-input'); const firstInput = modal.querySelector(".snippet-var-input");
if (firstInput) firstInput.focus(); if (firstInput) firstInput.focus();
}); });
const close = () => { const close = () => {
backdrop.classList.remove('is-visible'); backdrop.classList.remove("is-visible");
modal.classList.remove('is-visible'); modal.classList.remove("is-visible");
setTimeout(() => backdrop.remove(), 200); setTimeout(() => backdrop.remove(), 200);
}; };
modal.querySelector('.modal-close').addEventListener('click', close); modal.querySelector(".modal-close").addEventListener("click", close);
modal.querySelector('.snippet-var-cancel').addEventListener('click', close); modal
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(); }); .querySelector(".snippet-var-cancel")
.addEventListener("click", close);
backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) close();
});
modal.querySelector('.snippet-var-send').addEventListener('click', () => { modal.querySelector(".snippet-var-send").addEventListener("click", () => {
const inputs = modal.querySelectorAll('.snippet-var-input'); const inputs = modal.querySelectorAll(".snippet-var-input");
const values = Array.from(inputs).map((inp) => inp.value); const values = Array.from(inputs).map((inp) => inp.value);
close(); close();
onConfirm(values); onConfirm(values);
}); });
// Enter to send // Enter to send
modal.addEventListener('keydown', (e) => { modal.addEventListener("keydown", (e) => {
if (e.key === 'Enter') { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
modal.querySelector('.snippet-var-send').click(); modal.querySelector(".snippet-var-send").click();
} }
if (e.key === 'Escape') close(); if (e.key === "Escape") close();
}); });
} }
@@ -166,10 +171,10 @@ class SnippetLibrary {
/** @private */ /** @private */
async _loadSnippets() { async _loadSnippets() {
try { try {
this.snippets = await window.go.api.Service.ListSnippets() || []; this.snippets = (await window.go.api.Service.ListSnippets()) || [];
} catch (err) { } catch (err) {
this.snippets = []; this.snippets = [];
console.error('Failed to load snippets:', err); console.error("Failed to load snippets:", err);
} }
} }
@@ -177,8 +182,8 @@ class SnippetLibrary {
/** @private */ /** @private */
_showModal() { _showModal() {
const backdrop = createElement('div', 'modal-backdrop'); const backdrop = createElement("div", "modal-backdrop");
const modal = createElement('div', 'modal modal-lg'); const modal = createElement("div", "modal modal-lg");
modal.innerHTML = ` modal.innerHTML = `
<div class="modal-header"> <div class="modal-header">
@@ -204,28 +209,30 @@ class SnippetLibrary {
backdrop.appendChild(modal); backdrop.appendChild(modal);
document.body.appendChild(backdrop); document.body.appendChild(backdrop);
requestAnimationFrame(() => { requestAnimationFrame(() => {
backdrop.classList.add('is-visible'); backdrop.classList.add("is-visible");
modal.classList.add('is-visible'); modal.classList.add("is-visible");
}); });
const close = () => { const close = () => {
backdrop.classList.remove('is-visible'); backdrop.classList.remove("is-visible");
modal.classList.remove('is-visible'); modal.classList.remove("is-visible");
setTimeout(() => backdrop.remove(), 200); setTimeout(() => backdrop.remove(), 200);
}; };
modal.querySelector('.modal-close').addEventListener('click', close); modal.querySelector(".modal-close").addEventListener("click", close);
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(); }); backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) close();
});
// Search // Search
const searchInput = modal.querySelector('.snippet-search'); const searchInput = modal.querySelector(".snippet-search");
searchInput.addEventListener('input', () => { searchInput.addEventListener("input", () => {
this.searchQuery = searchInput.value.trim().toLowerCase(); this.searchQuery = searchInput.value.trim().toLowerCase();
this._renderList(modal, close); this._renderList(modal, close);
}); });
// New snippet button // New snippet button
modal.querySelector('.snippet-add-btn').addEventListener('click', () => { modal.querySelector(".snippet-add-btn").addEventListener("click", () => {
this._showEditor(modal, null, close); this._showEditor(modal, null, close);
}); });
@@ -234,87 +241,106 @@ class SnippetLibrary {
/** @private */ /** @private */
_renderList(modal, closeFn) { _renderList(modal, closeFn) {
const listEl = modal.querySelector('.snippet-list'); const listEl = modal.querySelector(".snippet-list");
if (!listEl) return; if (!listEl) return;
listEl.innerHTML = ''; listEl.innerHTML = "";
const filtered = this.snippets.filter((sn) => { const filtered = this.snippets.filter((sn) => {
if (!this.searchQuery) return true; if (!this.searchQuery) return true;
return sn.name.toLowerCase().includes(this.searchQuery) || return (
sn.name.toLowerCase().includes(this.searchQuery) ||
sn.command.toLowerCase().includes(this.searchQuery) || sn.command.toLowerCase().includes(this.searchQuery) ||
(sn.description || '').toLowerCase().includes(this.searchQuery); (sn.description || "").toLowerCase().includes(this.searchQuery)
);
}); });
if (filtered.length === 0) { if (filtered.length === 0) {
listEl.innerHTML = '<div style="color:var(--text-tertiary);font-size:var(--text-sm);padding:var(--space-3);text-align:center;">No snippets found</div>'; listEl.innerHTML =
'<div style="color:var(--text-tertiary);font-size:var(--text-sm);padding:var(--space-3);text-align:center;">No snippets found</div>';
return; return;
} }
for (const sn of filtered) { for (const sn of filtered) {
const item = createElement('div', 'snippet-item', { const item = createElement("div", "snippet-item", {
dataset: { id: sn.id }, dataset: { id: sn.id },
}); });
item.style.cssText = 'padding:var(--space-2);border-radius:var(--radius-md);cursor:pointer;border:1px solid var(--border-subtle);'; item.style.cssText =
"padding:var(--space-2);border-radius:var(--radius-md);cursor:pointer;border:1px solid var(--border-subtle);";
item.innerHTML = ` item.innerHTML = `
<div style="font-weight:var(--weight-medium);font-size:var(--text-sm);color:var(--text-primary);">${escapeHtml(sn.name)}</div> <div style="font-weight:var(--weight-medium);font-size:var(--text-sm);color:var(--text-primary);">${escapeHtml(sn.name)}</div>
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-secondary);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(sn.command)}</div> <div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-secondary);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(sn.command)}</div>
`; `;
item.addEventListener('click', () => this._showDetail(modal, sn, closeFn)); item.addEventListener("click", () =>
item.addEventListener('mousedown', (e) => e.preventDefault()); this._showDetail(modal, sn, closeFn),
item.addEventListener('mouseenter', () => { item.style.background = 'var(--bg-hover)'; }); );
item.addEventListener('mouseleave', () => { item.style.background = ''; }); item.addEventListener("mousedown", (e) => e.preventDefault());
item.addEventListener("mouseenter", () => {
item.style.background = "var(--bg-hover)";
});
item.addEventListener("mouseleave", () => {
item.style.background = "";
});
listEl.appendChild(item); listEl.appendChild(item);
} }
} }
/** @private */ /** @private */
_showDetail(modal, snippet, closeFn) { _showDetail(modal, snippet, closeFn) {
const detailPanel = modal.querySelector('.snippet-detail-panel'); const detailPanel = modal.querySelector(".snippet-detail-panel");
if (!detailPanel) return; if (!detailPanel) return;
detailPanel.innerHTML = ''; detailPanel.innerHTML = "";
const nameEl = createElement('div', '', { const nameEl = createElement("div", "", {
innerHTML: `<strong style="font-size:var(--text-base);">${escapeHtml(snippet.name)}</strong>`, innerHTML: `<strong style="font-size:var(--text-base);">${escapeHtml(snippet.name)}</strong>`,
}); });
detailPanel.appendChild(nameEl); detailPanel.appendChild(nameEl);
if (snippet.description) { if (snippet.description) {
const descEl = createElement('div', '', { const descEl = createElement("div", "", {
textContent: snippet.description, textContent: snippet.description,
}); });
descEl.style.cssText = 'font-size:var(--text-sm);color:var(--text-secondary);'; descEl.style.cssText =
"font-size:var(--text-sm);color:var(--text-secondary);";
detailPanel.appendChild(descEl); detailPanel.appendChild(descEl);
} }
const cmdEl = createElement('pre', '', { const cmdEl = createElement("pre", "", {
textContent: snippet.command, textContent: snippet.command,
}); });
cmdEl.style.cssText = 'flex:1;font-family:var(--font-mono);font-size:var(--text-sm);background:var(--bg-base);padding:var(--space-3);border-radius:var(--radius-md);overflow:auto;color:var(--text-primary);margin:0;white-space:pre-wrap;word-break:break-all;'; cmdEl.style.cssText =
"flex:1;font-family:var(--font-mono);font-size:var(--text-sm);background:var(--bg-base);padding:var(--space-3);border-radius:var(--radius-md);overflow:auto;color:var(--text-primary);margin:0;white-space:pre-wrap;word-break:break-all;";
detailPanel.appendChild(cmdEl); detailPanel.appendChild(cmdEl);
// Action buttons // Action buttons
const actions = createElement('div', '', {}); const actions = createElement("div", "", {});
actions.style.cssText = 'display:flex;gap:var(--space-2);'; actions.style.cssText = "display:flex;gap:var(--space-2);";
const sendBtn = createElement('button', 'btn btn-sm btn-primary', { textContent: 'Send to Session' }); const sendBtn = createElement("button", "btn btn-sm btn-primary", {
sendBtn.addEventListener('click', () => { textContent: "Send to Session",
});
sendBtn.addEventListener("click", () => {
this.sendToActiveSession(snippet.command); this.sendToActiveSession(snippet.command);
}); });
const editBtn = createElement('button', 'btn btn-sm btn-secondary', { textContent: 'Edit' }); const editBtn = createElement("button", "btn btn-sm btn-secondary", {
editBtn.addEventListener('click', () => { textContent: "Edit",
});
editBtn.addEventListener("click", () => {
this._showEditor(modal, snippet, closeFn); this._showEditor(modal, snippet, closeFn);
}); });
const deleteBtn = createElement('button', 'btn btn-sm btn-danger', { textContent: 'Delete' }); const deleteBtn = createElement("button", "btn btn-sm btn-danger", {
deleteBtn.addEventListener('click', async () => { textContent: "Delete",
});
deleteBtn.addEventListener("click", async () => {
try { try {
await window.go.api.Service.DeleteSnippet(snippet.id); await window.go.api.Service.DeleteSnippet(snippet.id);
this.snippets = this.snippets.filter((s) => s.id !== snippet.id); this.snippets = this.snippets.filter((s) => s.id !== snippet.id);
this._renderList(modal, closeFn); this._renderList(modal, closeFn);
detailPanel.innerHTML = '<div class="snippet-detail-empty" style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--text-tertiary);font-size:var(--text-sm);">Snippet deleted</div>'; detailPanel.innerHTML =
'<div class="snippet-detail-empty" style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--text-tertiary);font-size:var(--text-sm);">Snippet deleted</div>';
} catch (err) { } catch (err) {
this.app.notifications.error('Failed to delete: ' + err); this.app.notifications.error("Failed to delete: " + err);
} }
}); });
@@ -326,30 +352,30 @@ class SnippetLibrary {
/** @private */ /** @private */
_showEditor(modal, snippet, closeFn) { _showEditor(modal, snippet, closeFn) {
const detailPanel = modal.querySelector('.snippet-detail-panel'); const detailPanel = modal.querySelector(".snippet-detail-panel");
if (!detailPanel) return; if (!detailPanel) return;
detailPanel.innerHTML = ''; detailPanel.innerHTML = "";
const isNew = !snippet; const isNew = !snippet;
const title = isNew ? 'New Snippet' : 'Edit Snippet'; const title = isNew ? "New Snippet" : "Edit Snippet";
detailPanel.innerHTML = ` detailPanel.innerHTML = `
<div style="font-weight:var(--weight-medium);font-size:var(--text-sm);">${title}</div> <div style="font-weight:var(--weight-medium);font-size:var(--text-sm);">${title}</div>
<div class="form-group" style="margin-bottom:var(--space-2);"> <div class="form-group" style="margin-bottom:var(--space-2);">
<label class="form-label">Name</label> <label class="form-label">Name</label>
<input type="text" class="form-input snippet-ed-name" value="${isNew ? '' : escapeHtml(snippet.name)}" /> <input type="text" class="form-input snippet-ed-name" value="${isNew ? "" : escapeHtml(snippet.name)}" />
</div> </div>
<div class="form-group" style="margin-bottom:var(--space-2);"> <div class="form-group" style="margin-bottom:var(--space-2);">
<label class="form-label">Description</label> <label class="form-label">Description</label>
<input type="text" class="form-input snippet-ed-desc" value="${isNew ? '' : escapeHtml(snippet.description || '')}" /> <input type="text" class="form-input snippet-ed-desc" value="${isNew ? "" : escapeHtml(snippet.description || "")}" />
</div> </div>
<div class="form-group" style="flex:1;display:flex;flex-direction:column;"> <div class="form-group" style="flex:1;display:flex;flex-direction:column;">
<label class="form-label">Command</label> <label class="form-label">Command</label>
<textarea class="form-input snippet-ed-cmd" style="flex:1;font-family:var(--font-mono);font-size:var(--text-sm);resize:none;">${isNew ? '' : escapeHtml(snippet.command)}</textarea> <textarea class="form-input snippet-ed-cmd" style="flex:1;font-family:var(--font-mono);font-size:var(--text-sm);resize:none;">${isNew ? "" : escapeHtml(snippet.command)}</textarea>
</div> </div>
<div class="form-group" style="margin-bottom:var(--space-2);"> <div class="form-group" style="margin-bottom:var(--space-2);">
<label class="form-label">Folder</label> <label class="form-label">Folder</label>
<input type="text" class="form-input snippet-ed-folder" value="${isNew ? '' : escapeHtml(snippet.folder || '')}" placeholder="e.g. Networking, Debugging" /> <input type="text" class="form-input snippet-ed-folder" value="${isNew ? "" : escapeHtml(snippet.folder || "")}" placeholder="e.g. Networking, Debugging" />
</div> </div>
<div style="display:flex;gap:var(--space-2);"> <div style="display:flex;gap:var(--space-2);">
<button class="btn btn-sm btn-primary snippet-save-btn">Save</button> <button class="btn btn-sm btn-primary snippet-save-btn">Save</button>
@@ -357,29 +383,37 @@ class SnippetLibrary {
</div> </div>
`; `;
detailPanel.querySelector('.snippet-save-btn').addEventListener('click', async () => { detailPanel
const name = detailPanel.querySelector('.snippet-ed-name').value.trim(); .querySelector(".snippet-save-btn")
const command = detailPanel.querySelector('.snippet-ed-cmd').value; .addEventListener("click", async () => {
const description = detailPanel.querySelector('.snippet-ed-desc').value.trim(); const name = detailPanel
const folder = detailPanel.querySelector('.snippet-ed-folder').value.trim(); .querySelector(".snippet-ed-name")
.value.trim();
const command = detailPanel.querySelector(".snippet-ed-cmd").value;
const description = detailPanel
.querySelector(".snippet-ed-desc")
.value.trim();
const folder = detailPanel
.querySelector(".snippet-ed-folder")
.value.trim();
if (!name) { if (!name) {
this.app.notifications.error('Name is required'); this.app.notifications.error("Name is required");
return; return;
} }
if (!command) { if (!command) {
this.app.notifications.error('Command is required'); this.app.notifications.error("Command is required");
return; return;
} }
try { try {
const data = { const data = {
id: isNew ? '' : snippet.id, id: isNew ? "" : snippet.id,
name, name,
command, command,
description, description,
folder, folder,
tags: isNew ? [] : (snippet.tags || []), tags: isNew ? [] : snippet.tags || [],
}; };
let saved; let saved;
@@ -394,23 +428,27 @@ class SnippetLibrary {
this._renderList(modal, closeFn); this._renderList(modal, closeFn);
this._showDetail(modal, saved, closeFn); this._showDetail(modal, saved, closeFn);
this.app.notifications.info(isNew ? 'Snippet created' : 'Snippet updated'); this.app.notifications.info(
isNew ? "Snippet created" : "Snippet updated",
);
} catch (err) { } catch (err) {
this.app.notifications.error('Failed to save snippet: ' + err); this.app.notifications.error("Failed to save snippet: " + err);
} }
}); });
detailPanel.querySelector('.snippet-cancel-btn').addEventListener('click', () => { detailPanel
.querySelector(".snippet-cancel-btn")
.addEventListener("click", () => {
if (snippet) { if (snippet) {
this._showDetail(modal, snippet, closeFn); this._showDetail(modal, snippet, closeFn);
} else { } else {
detailPanel.innerHTML = '<div class="snippet-detail-empty" style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--text-tertiary);font-size:var(--text-sm);">Select a snippet or create a new one</div>'; detailPanel.innerHTML =
'<div class="snippet-detail-empty" style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--text-tertiary);font-size:var(--text-sm);">Select a snippet or create a new one</div>';
} }
}); });
} }
} }
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.SnippetLibrary = SnippetLibrary;
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.SnippetLibrary = SnippetLibrary;
})(); })();
+25 -25
View File
@@ -1,11 +1,11 @@
(function() { (function () {
/** /**
* Nexus Terminal — application state management. * Nexus Terminal — application state management.
* Provides a centralised store for profiles, sessions, and preferences, * Provides a centralised store for profiles, sessions, and preferences,
* emitting change events so the UI stays in sync. * emitting change events so the UI stays in sync.
*/ */
class State { class State {
constructor() { constructor() {
/** @type {Array<Object>} */ /** @type {Array<Object>} */
this.profiles = []; this.profiles = [];
@@ -87,7 +87,7 @@ class State {
*/ */
setProfiles(profiles) { setProfiles(profiles) {
this.profiles = profiles; this.profiles = profiles;
this.emit('profiles:changed', this.profiles); this.emit("profiles:changed", this.profiles);
} }
/** /**
@@ -96,8 +96,8 @@ class State {
*/ */
addProfile(profile) { addProfile(profile) {
this.profiles.push(profile); this.profiles.push(profile);
this.emit('profiles:changed', this.profiles); this.emit("profiles:changed", this.profiles);
this.emit('profile:added', profile); this.emit("profile:added", profile);
} }
/** /**
@@ -109,8 +109,8 @@ class State {
if (idx !== -1) { if (idx !== -1) {
this.profiles[idx] = profile; this.profiles[idx] = profile;
} }
this.emit('profiles:changed', this.profiles); this.emit("profiles:changed", this.profiles);
this.emit('profile:updated', profile); this.emit("profile:updated", profile);
} }
/** /**
@@ -119,8 +119,8 @@ class State {
*/ */
removeProfile(id) { removeProfile(id) {
this.profiles = this.profiles.filter((p) => p.id !== id); this.profiles = this.profiles.filter((p) => p.id !== id);
this.emit('profiles:changed', this.profiles); this.emit("profiles:changed", this.profiles);
this.emit('profile:removed', id); this.emit("profile:removed", id);
} }
/** /**
@@ -140,8 +140,8 @@ class State {
*/ */
addSession(sessionInfo) { addSession(sessionInfo) {
this.sessions.set(sessionInfo.id, sessionInfo); this.sessions.set(sessionInfo.id, sessionInfo);
this.emit('sessions:changed', this.sessions); this.emit("sessions:changed", this.sessions);
this.emit('session:added', sessionInfo); this.emit("session:added", sessionInfo);
} }
/** /**
@@ -153,8 +153,8 @@ class State {
const existing = this.sessions.get(id); const existing = this.sessions.get(id);
if (existing) { if (existing) {
Object.assign(existing, updates); Object.assign(existing, updates);
this.emit('sessions:changed', this.sessions); this.emit("sessions:changed", this.sessions);
this.emit('session:updated', existing); this.emit("session:updated", existing);
} }
} }
@@ -167,10 +167,11 @@ class State {
if (this.activeSessionId === id) { if (this.activeSessionId === id) {
// Activate the most recent remaining session, or null // Activate the most recent remaining session, or null
const remaining = [...this.sessions.keys()]; const remaining = [...this.sessions.keys()];
this.activeSessionId = remaining.length > 0 ? remaining[remaining.length - 1] : null; this.activeSessionId =
remaining.length > 0 ? remaining[remaining.length - 1] : null;
} }
this.emit('sessions:changed', this.sessions); this.emit("sessions:changed", this.sessions);
this.emit('session:removed', id); this.emit("session:removed", id);
} }
/** /**
@@ -188,7 +189,7 @@ class State {
*/ */
setActiveSession(id) { setActiveSession(id) {
this.activeSessionId = id; this.activeSessionId = id;
this.emit('session:activated', id); this.emit("session:activated", id);
} }
/** /**
@@ -207,7 +208,7 @@ class State {
*/ */
setPreferences(prefs) { setPreferences(prefs) {
this.preferences = prefs; this.preferences = prefs;
this.emit('preferences:changed', this.preferences); this.emit("preferences:changed", this.preferences);
} }
// ── Secret store ───────────────────────────────────────────────── // ── Secret store ─────────────────────────────────────────────────
@@ -218,7 +219,7 @@ class State {
*/ */
setSecretStoreState(unlocked) { setSecretStoreState(unlocked) {
this.secretStoreUnlocked = unlocked; this.secretStoreUnlocked = unlocked;
this.emit('secretStore:changed', unlocked); this.emit("secretStore:changed", unlocked);
} }
/** /**
@@ -227,11 +228,10 @@ class State {
*/ */
setSecretStoreInitialised(initialised) { setSecretStoreInitialised(initialised) {
this.secretStoreInitialised = initialised; this.secretStoreInitialised = initialised;
this.emit('secretStore:init', initialised); this.emit("secretStore:init", initialised);
}
} }
}
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.State = State;
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.State = State;
})(); })();
+54 -16
View File
@@ -574,6 +574,17 @@
action: () => this._showPortForwardPanel(sessionId), action: () => this._showPortForwardPanel(sessionId),
}, },
{ divider: true }, { divider: true },
{
label: "Split Right",
shortcut: modKey + "⇧D",
action: () => this.app.splitPane("vertical"),
},
{
label: "Split Down",
shortcut: modKey + "⇧E",
action: () => this.app.splitPane("horizontal"),
},
{ divider: true },
{ {
label: "Export Output", label: "Export Output",
action: () => this._exportTerminalOutput(sessionId, term), action: () => this._exportTerminalOutput(sessionId, term),
@@ -1236,7 +1247,9 @@
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
this.app.notifications.success("Terminal output exported & copied to clipboard"); this.app.notifications.success(
"Terminal output exported & copied to clipboard",
);
} }
/** /**
@@ -1248,7 +1261,9 @@
const { createElement, escapeHtml } = window.NexusTerminal; const { createElement, escapeHtml } = window.NexusTerminal;
const session = this.app.state.getSession(sessionId); const session = this.app.state.getSession(sessionId);
if (!session || session.protocol !== "ssh") { if (!session || session.protocol !== "ssh") {
this.app.notifications.error("Port forwarding is only available for SSH sessions"); this.app.notifications.error(
"Port forwarding is only available for SSH sessions",
);
return; return;
} }
@@ -1297,24 +1312,30 @@
}; };
modal.querySelector(".modal-close").addEventListener("click", close); modal.querySelector(".modal-close").addEventListener("click", close);
backdrop.addEventListener("click", (e) => { if (e.target === backdrop) close(); }); backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) close();
});
const listEl = modal.querySelector(".portfwd-list"); const listEl = modal.querySelector(".portfwd-list");
const self = this; const self = this;
const renderForwards = async () => { const renderForwards = async () => {
try { try {
const forwards = await window.go.api.Service.ListPortForwards(sessionId) || []; const forwards =
(await window.go.api.Service.ListPortForwards(sessionId)) || [];
listEl.innerHTML = ""; listEl.innerHTML = "";
if (forwards.length === 0) { if (forwards.length === 0) {
listEl.innerHTML = '<div style="color:var(--text-tertiary);font-size:var(--text-sm);padding:var(--space-2);">No active port forwards</div>'; listEl.innerHTML =
'<div style="color:var(--text-tertiary);font-size:var(--text-sm);padding:var(--space-2);">No active port forwards</div>';
return; return;
} }
for (const fwd of forwards) { for (const fwd of forwards) {
const row = createElement("div", "", {}); const row = createElement("div", "", {});
row.style.cssText = "display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) 0;border-bottom:1px solid var(--border-subtle);"; row.style.cssText =
"display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) 0;border-bottom:1px solid var(--border-subtle);";
const dirLabel = fwd.direction === "local" ? "L" : "R"; const dirLabel = fwd.direction === "local" ? "L" : "R";
const dirColor = fwd.direction === "local" ? "var(--accent)" : "var(--warning)"; const dirColor =
fwd.direction === "local" ? "var(--accent)" : "var(--warning)";
row.innerHTML = ` row.innerHTML = `
<span style="font-size:var(--text-xs);font-weight:var(--weight-semibold);color:${dirColor};width:16px;">${dirLabel}</span> <span style="font-size:var(--text-xs);font-weight:var(--weight-semibold);color:${dirColor};width:16px;">${dirLabel}</span>
<span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-primary);flex:1;"> <span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-primary);flex:1;">
@@ -1322,21 +1343,26 @@
</span> </span>
${fwd.label ? `<span style="font-size:var(--text-xs);color:var(--text-tertiary);">${escapeHtml(fwd.label)}</span>` : ""} ${fwd.label ? `<span style="font-size:var(--text-xs);color:var(--text-tertiary);">${escapeHtml(fwd.label)}</span>` : ""}
`; `;
const stopBtn = createElement("button", "btn btn-xs btn-danger", { textContent: "Stop" }); const stopBtn = createElement("button", "btn btn-xs btn-danger", {
textContent: "Stop",
});
stopBtn.addEventListener("click", async () => { stopBtn.addEventListener("click", async () => {
try { try {
await window.go.api.Service.StopPortForward(sessionId, fwd.id); await window.go.api.Service.StopPortForward(sessionId, fwd.id);
self.app.notifications.info("Forward stopped"); self.app.notifications.info("Forward stopped");
renderForwards(); renderForwards();
} catch (err) { } catch (err) {
self.app.notifications.error("Failed to stop: " + (err.message || err)); self.app.notifications.error(
"Failed to stop: " + (err.message || err),
);
} }
}); });
row.appendChild(stopBtn); row.appendChild(stopBtn);
listEl.appendChild(row); listEl.appendChild(row);
} }
} catch (err) { } catch (err) {
listEl.innerHTML = '<div style="color:var(--error);font-size:var(--text-sm);">Failed to load forwards</div>'; listEl.innerHTML =
'<div style="color:var(--error);font-size:var(--text-sm);">Failed to load forwards</div>';
} }
}; };
@@ -1345,14 +1371,24 @@
// Add forward button // Add forward button
modal.querySelector(".pf-add-btn").addEventListener("click", async () => { modal.querySelector(".pf-add-btn").addEventListener("click", async () => {
const direction = modal.querySelector(".pf-direction").value; const direction = modal.querySelector(".pf-direction").value;
const bindHost = modal.querySelector(".pf-bind-host").value.trim() || "127.0.0.1"; const bindHost =
const bindPort = parseInt(modal.querySelector(".pf-bind-port").value, 10); modal.querySelector(".pf-bind-host").value.trim() || "127.0.0.1";
const destHost = modal.querySelector(".pf-dest-host").value.trim() || "localhost"; const bindPort = parseInt(
const destPort = parseInt(modal.querySelector(".pf-dest-port").value, 10); modal.querySelector(".pf-bind-port").value,
10,
);
const destHost =
modal.querySelector(".pf-dest-host").value.trim() || "localhost";
const destPort = parseInt(
modal.querySelector(".pf-dest-port").value,
10,
);
const label = modal.querySelector(".pf-label").value.trim(); const label = modal.querySelector(".pf-label").value.trim();
if (!bindPort || !destPort) { if (!bindPort || !destPort) {
self.app.notifications.error("Bind port and destination port are required"); self.app.notifications.error(
"Bind port and destination port are required",
);
return; return;
} }
@@ -1373,7 +1409,9 @@
modal.querySelector(".pf-label").value = ""; modal.querySelector(".pf-label").value = "";
renderForwards(); renderForwards();
} catch (err) { } catch (err) {
self.app.notifications.error("Failed to start forward: " + (err.message || err)); self.app.notifications.error(
"Failed to start forward: " + (err.message || err),
);
} }
}); });
} }