diff --git a/frontend/scripts/bundle-novnc.js b/frontend/scripts/bundle-novnc.js
index 1bd6a5e..6b21f10 100644
--- a/frontend/scripts/bundle-novnc.js
+++ b/frontend/scripts/bundle-novnc.js
@@ -11,7 +11,14 @@ const esbuild = require("esbuild");
const path = require("path");
const entryPoint = path.resolve(__dirname, "novnc-entry.js");
-const outFile = path.resolve(__dirname, "..", "src", "js", "lib", "novnc.min.js");
+const outFile = path.resolve(
+ __dirname,
+ "..",
+ "src",
+ "js",
+ "lib",
+ "novnc.min.js",
+);
esbuild
.build({
@@ -26,7 +33,9 @@ esbuild
footer: { js: "})();" },
})
.then(() => {
- console.log("[bundle-novnc] noVNC bundled successfully → src/js/lib/novnc.min.js");
+ console.log(
+ "[bundle-novnc] noVNC bundled successfully → src/js/lib/novnc.min.js",
+ );
})
.catch((err) => {
console.error("[bundle-novnc] Failed:", err.message);
diff --git a/frontend/src/css/tabs.css b/frontend/src/css/tabs.css
index ceb6af3..f79bc35 100644
--- a/frontend/src/css/tabs.css
+++ b/frontend/src/css/tabs.css
@@ -191,10 +191,19 @@
}
@keyframes trigger-flash {
- 0%, 100% { background: var(--bg-hover); }
- 15% { background: rgba(239, 83, 80, 0.25); }
- 30% { background: var(--bg-hover); }
- 45% { background: rgba(239, 83, 80, 0.15); }
+ 0%,
+ 100% {
+ background: var(--bg-hover);
+ }
+ 15% {
+ background: rgba(239, 83, 80, 0.25);
+ }
+ 30% {
+ background: var(--bg-hover);
+ }
+ 45% {
+ background: rgba(239, 83, 80, 0.15);
+ }
}
/* -----------------------------------------------------------------------
diff --git a/frontend/src/css/tokens.css b/frontend/src/css/tokens.css
index be1bed2..fedfee4 100644
--- a/frontend/src/css/tokens.css
+++ b/frontend/src/css/tokens.css
@@ -5,150 +5,153 @@
========================================================================== */
:root {
- /* -----------------------------------------------------------------------
+ /* -----------------------------------------------------------------------
Colour palette — dark charcoal theme
----------------------------------------------------------------------- */
- --bg-base: #1c1c1c; /* deepest background */
- --bg-surface: #232323; /* primary surface (sidebar, bars) */
- --bg-elevated: #2a2a2a; /* elevated panels, cards */
- --bg-overlay: #303030; /* overlays, dropdowns */
- --bg-hover: #363636; /* hover state backgrounds */
- --bg-active: #3c3c3c; /* active / pressed state */
- --bg-input: #1e1e1e; /* input field backgrounds */
+ --bg-base: #1c1c1c; /* deepest background */
+ --bg-surface: #232323; /* primary surface (sidebar, bars) */
+ --bg-elevated: #2a2a2a; /* elevated panels, cards */
+ --bg-overlay: #303030; /* overlays, dropdowns */
+ --bg-hover: #363636; /* hover state backgrounds */
+ --bg-active: #3c3c3c; /* active / pressed state */
+ --bg-input: #1e1e1e; /* input field backgrounds */
- /* -----------------------------------------------------------------------
+ /* -----------------------------------------------------------------------
Border colours
----------------------------------------------------------------------- */
- --border-subtle: rgba(255, 255, 255, 0.07);
- --border-default: rgba(255, 255, 255, 0.12);
- --border-strong: rgba(255, 255, 255, 0.18);
- --border-focus: var(--accent);
+ --border-subtle: rgba(255, 255, 255, 0.07);
+ --border-default: rgba(255, 255, 255, 0.12);
+ --border-strong: rgba(255, 255, 255, 0.18);
+ --border-focus: var(--accent);
- /* -----------------------------------------------------------------------
+ /* -----------------------------------------------------------------------
Text colours
----------------------------------------------------------------------- */
- --text-primary: #e4e4e4;
- --text-secondary: #a0a0a0;
- --text-tertiary: #707070;
- --text-disabled: #4a4a4a;
- --text-inverse: #1c1c1c;
- --sidebar-group-title: #b89d5f;
+ --text-primary: #e4e4e4;
+ --text-secondary: #a0a0a0;
+ --text-tertiary: #707070;
+ --text-disabled: #4a4a4a;
+ --text-inverse: #1c1c1c;
+ --sidebar-group-title: #b89d5f;
- /* -----------------------------------------------------------------------
+ /* -----------------------------------------------------------------------
Accent colours — teal / cyan
----------------------------------------------------------------------- */
- --accent: #4ecdc4;
- --accent-hover: #45b7af;
- --accent-muted: rgba(78, 205, 196, 0.12);
- --accent-strong: rgba(78, 205, 196, 0.22);
+ --accent: #4ecdc4;
+ --accent-hover: #45b7af;
+ --accent-muted: rgba(78, 205, 196, 0.12);
+ --accent-strong: rgba(78, 205, 196, 0.22);
- /* -----------------------------------------------------------------------
+ /* -----------------------------------------------------------------------
Status colours
----------------------------------------------------------------------- */
- --success: #66bb6a;
- --warning: #ffa726;
- --error: #ef5350;
- --info: #42a5f5;
+ --success: #66bb6a;
+ --warning: #ffa726;
+ --error: #ef5350;
+ --info: #42a5f5;
- /* -----------------------------------------------------------------------
+ /* -----------------------------------------------------------------------
Protocol badge colours
----------------------------------------------------------------------- */
- --protocol-ssh: #4ecdc4;
- --protocol-telnet: #ffa726;
- --protocol-raw: #ab47bc;
- --protocol-github: #7c8aff;
- --protocol-vnc: #e85d4a;
+ --protocol-ssh: #4ecdc4;
+ --protocol-telnet: #ffa726;
+ --protocol-raw: #ab47bc;
+ --protocol-github: #7c8aff;
+ --protocol-vnc: #e85d4a;
- /* -----------------------------------------------------------------------
+ /* -----------------------------------------------------------------------
Spacing scale (4 px base unit)
----------------------------------------------------------------------- */
- --space-1: 4px;
- --space-2: 8px;
- --space-3: 12px;
- --space-4: 16px;
- --space-5: 20px;
- --space-6: 24px;
- --space-8: 32px;
- --space-10: 40px;
- --space-12: 48px;
- --space-16: 64px;
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-5: 20px;
+ --space-6: 24px;
+ --space-8: 32px;
+ --space-10: 40px;
+ --space-12: 48px;
+ --space-16: 64px;
- /* -----------------------------------------------------------------------
+ /* -----------------------------------------------------------------------
Typography
----------------------------------------------------------------------- */
- --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI',
- system-ui, sans-serif;
- --font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco,
- 'Cascadia Code', monospace;
+ --font-sans:
+ "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,
+ sans-serif;
+ --font-mono:
+ "JetBrains Mono", "Fira Code", "SF Mono", Monaco, "Cascadia Code", monospace;
- --text-xs: 11px;
- --text-sm: 12px;
- --text-base: 13px;
- --text-md: 14px;
- --text-lg: 16px;
- --text-xl: 18px;
- --text-2xl: 22px;
+ --text-xs: 11px;
+ --text-sm: 12px;
+ --text-base: 13px;
+ --text-md: 14px;
+ --text-lg: 16px;
+ --text-xl: 18px;
+ --text-2xl: 22px;
- --leading-tight: 1.25;
- --leading-normal: 1.5;
- --leading-relaxed: 1.625;
+ --leading-tight: 1.25;
+ --leading-normal: 1.5;
+ --leading-relaxed: 1.625;
- --weight-normal: 400;
- --weight-medium: 500;
- --weight-semibold: 600;
+ --weight-normal: 400;
+ --weight-medium: 500;
+ --weight-semibold: 600;
- /* -----------------------------------------------------------------------
+ /* -----------------------------------------------------------------------
Border radius
----------------------------------------------------------------------- */
- --radius-sm: 3px;
- --radius-md: 5px;
- --radius-lg: 8px;
- --radius-xl: 12px;
- --radius-full: 9999px;
+ --radius-sm: 3px;
+ --radius-md: 5px;
+ --radius-lg: 8px;
+ --radius-xl: 12px;
+ --radius-full: 9999px;
- /* -----------------------------------------------------------------------
+ /* -----------------------------------------------------------------------
Shadows
----------------------------------------------------------------------- */
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.30);
- --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.40);
- --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.50);
- --shadow-xl: 0 8px 32px rgba(0, 0, 0, 0.60);
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
+ --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4);
+ --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.5);
+ --shadow-xl: 0 8px 32px rgba(0, 0, 0, 0.6);
- /* -----------------------------------------------------------------------
+ /* -----------------------------------------------------------------------
Transitions
----------------------------------------------------------------------- */
- --transition-fast: 100ms ease;
- --transition-normal: 200ms ease;
- --transition-slow: 300ms ease;
+ --transition-fast: 100ms ease;
+ --transition-normal: 200ms ease;
+ --transition-slow: 300ms ease;
- /* -----------------------------------------------------------------------
+ /* -----------------------------------------------------------------------
Z-index layers
----------------------------------------------------------------------- */
- --z-base: 0;
- --z-sticky: 100;
- --z-dropdown: 200;
- --z-modal-backdrop: 300;
- --z-modal: 400;
- --z-notification: 500;
- --z-tooltip: 600;
+ --z-base: 0;
+ --z-sticky: 100;
+ --z-dropdown: 200;
+ --z-modal-backdrop: 300;
+ --z-modal: 400;
+ --z-notification: 500;
+ --z-tooltip: 600;
- /* -----------------------------------------------------------------------
+ /* -----------------------------------------------------------------------
Layout
----------------------------------------------------------------------- */
- --sidebar-width: 260px;
- --sidebar-width-collapsed: 48px;
- --tab-bar-height: 36px;
- --toolbar-height: 40px;
- --status-bar-height: 24px;
+ --sidebar-width: 260px;
+ --sidebar-width-collapsed: 48px;
+ --tab-bar-height: 36px;
+ --toolbar-height: 40px;
+ --status-bar-height: 24px;
}
/* -----------------------------------------------------------------------
Global input overrides — disable macOS/WebKit auto-capitalisation,
autocorrect, and spellcheck on all text fields.
----------------------------------------------------------------------- */
-input, textarea, select {
- autocapitalize: none;
- autocorrect: off;
- spellcheck: false;
- text-transform: none;
+input,
+textarea,
+select {
+ autocapitalize: none;
+ autocorrect: off;
+ spellcheck: false;
+ text-transform: none;
}
diff --git a/frontend/src/css/vnc.css b/frontend/src/css/vnc.css
index 0a3c6dc..64016aa 100644
--- a/frontend/src/css/vnc.css
+++ b/frontend/src/css/vnc.css
@@ -40,7 +40,9 @@
border: 1px solid var(--border-subtle);
color: var(--text-secondary);
cursor: pointer;
- transition: background 0.15s, color 0.15s;
+ transition:
+ background 0.15s,
+ color 0.15s;
}
.btn-vnc-action:hover {
diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js
index 2d288be..d2d508e 100644
--- a/frontend/src/js/app.js
+++ b/frontend/src/js/app.js
@@ -168,7 +168,9 @@
try {
const raw = atob(payload.data);
this.triggers.check(payload.sessionID, raw);
- } catch (_) { /* noop */ }
+ } catch (_) {
+ /* noop */
+ }
});
// Session state transitions (connecting → connected → reconnecting → disconnected)
@@ -529,7 +531,9 @@
setupUI() {
var btnEl = document.getElementById("btn-new-connection");
this._bindClick("btn-new-connection", () => this.profileEditor.openNew());
- this._bindClick("btn-command-palette", () => this.commandPalette.toggle());
+ this._bindClick("btn-command-palette", () =>
+ this.commandPalette.toggle(),
+ );
this._bindClick("btn-settings", () => this.settings.open());
this._bindClick("btn-import", () => this.importProfiles());
this._bindClick("btn-export", () => this.exportProfiles());
diff --git a/frontend/src/js/profiles.js b/frontend/src/js/profiles.js
index 04d4016..15a3357 100644
--- a/frontend/src/js/profiles.js
+++ b/frontend/src/js/profiles.js
@@ -206,14 +206,16 @@
);
break;
case "vnc":
- this._renderVNCSections(profile).forEach((s) =>
- body.appendChild(s),
- );
+ this._renderVNCSections(profile).forEach((s) => body.appendChild(s));
break;
}
// Terminal section (SSH and Telnet only)
- if (profile.protocol !== "raw" && profile.protocol !== "github" && profile.protocol !== "vnc") {
+ if (
+ profile.protocol !== "raw" &&
+ profile.protocol !== "github" &&
+ profile.protocol !== "vnc"
+ ) {
body.appendChild(this._renderTerminalSection(profile));
}
@@ -2288,7 +2290,9 @@
if (profile.protocol === "vnc") {
const vncHost = (profile.vnc && profile.vnc.vncHost) || profile.host;
if (!vncHost || !vncHost.trim()) {
- errors.push("VNC Host is required (in General or VNC Server section).");
+ errors.push(
+ "VNC Host is required (in General or VNC Server section).",
+ );
}
} else if (!profile.host || !profile.host.trim()) {
errors.push("Host is required.");
diff --git a/frontend/src/js/sshkeys.js b/frontend/src/js/sshkeys.js
index 4ea08dd..695bc13 100644
--- a/frontend/src/js/sshkeys.js
+++ b/frontend/src/js/sshkeys.js
@@ -63,11 +63,15 @@
});
// Events
- modal.querySelector(".modal-close").addEventListener("click", () => this.close());
+ modal
+ .querySelector(".modal-close")
+ .addEventListener("click", () => this.close());
backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) this.close();
});
- modal.querySelector(".sshkey-generate-btn").addEventListener("click", () => this._showGenerateForm());
+ modal
+ .querySelector(".sshkey-generate-btn")
+ .addEventListener("click", () => this._showGenerateForm());
}
async _loadKeys() {
@@ -79,7 +83,8 @@
try {
const keys = await window.go.api.Service.ListSSHKeyPairs();
if (keys.length === 0) {
- list.innerHTML = '
No SSH keys found in ~/.ssh/
';
+ list.innerHTML =
+ 'No SSH keys found in ~/.ssh/
';
return;
}
list.innerHTML = "";
@@ -95,7 +100,9 @@
const row = createElement("div", "sshkey-row");
const info = createElement("div", "sshkey-info");
- const name = createElement("div", "sshkey-name", { textContent: key.name });
+ const name = createElement("div", "sshkey-name", {
+ textContent: key.name,
+ });
const meta = createElement("div", "sshkey-meta", {
textContent: key.type.toUpperCase() + (key.bits ? ` ${key.bits}` : ""),
});
@@ -104,11 +111,15 @@
const actions = createElement("div", "sshkey-actions");
- const copyBtn = createElement("button", "btn btn-xs", { textContent: "Copy Public Key" });
+ const copyBtn = createElement("button", "btn btn-xs", {
+ textContent: "Copy Public Key",
+ });
copyBtn.addEventListener("mousedown", (e) => e.preventDefault());
copyBtn.addEventListener("click", () => this._copyPublicKey(key));
- const deleteBtn = createElement("button", "btn btn-xs btn-danger", { textContent: "Delete" });
+ const deleteBtn = createElement("button", "btn btn-xs btn-danger", {
+ textContent: "Delete",
+ });
deleteBtn.addEventListener("mousedown", (e) => e.preventDefault());
deleteBtn.addEventListener("click", () => this._confirmDelete(key));
@@ -208,28 +219,40 @@
list.style.display = "";
});
- form.querySelector(".sshkey-create-btn").addEventListener("click", async () => {
- const nameInput = form.querySelector(".sshkey-name-input");
- const name = nameInput.value.trim();
- const keyType = typeSelect.value;
- const bits = parseInt(form.querySelector(".sshkey-bits-select").value, 10);
- const passphrase = form.querySelector(".sshkey-pass-input").value;
+ form
+ .querySelector(".sshkey-create-btn")
+ .addEventListener("click", async () => {
+ const nameInput = form.querySelector(".sshkey-name-input");
+ const name = nameInput.value.trim();
+ const keyType = typeSelect.value;
+ const bits = parseInt(
+ form.querySelector(".sshkey-bits-select").value,
+ 10,
+ );
+ const passphrase = form.querySelector(".sshkey-pass-input").value;
- if (!name) {
- this.app.notifications.error("Key name is required");
- return;
- }
+ if (!name) {
+ this.app.notifications.error("Key name is required");
+ return;
+ }
- try {
- await window.go.api.Service.GenerateSSHKey(name, keyType, bits, passphrase);
- this.app.notifications.success(`Key "${name}" generated successfully`);
- form.remove();
- list.style.display = "";
- this._loadKeys();
- } catch (err) {
- this.app.notifications.error("Generation failed: " + err);
- }
- });
+ try {
+ await window.go.api.Service.GenerateSSHKey(
+ name,
+ keyType,
+ bits,
+ passphrase,
+ );
+ this.app.notifications.success(
+ `Key "${name}" generated successfully`,
+ );
+ form.remove();
+ list.style.display = "";
+ this._loadKeys();
+ } catch (err) {
+ this.app.notifications.error("Generation failed: " + err);
+ }
+ });
form.querySelector(".sshkey-name-input").focus();
}
@@ -266,9 +289,15 @@
setTimeout(() => backdrop.remove(), 200);
resolve(val);
};
- modal.querySelector(".modal-close").addEventListener("click", () => close(false));
- modal.querySelector(".confirm-cancel").addEventListener("click", () => close(false));
- modal.querySelector(".confirm-ok").addEventListener("click", () => close(true));
+ modal
+ .querySelector(".modal-close")
+ .addEventListener("click", () => close(false));
+ modal
+ .querySelector(".confirm-cancel")
+ .addEventListener("click", () => close(false));
+ modal
+ .querySelector(".confirm-ok")
+ .addEventListener("click", () => close(true));
backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) close(false);
});
diff --git a/frontend/src/js/themes.js b/frontend/src/js/themes.js
index 528554b..629278c 100644
--- a/frontend/src/js/themes.js
+++ b/frontend/src/js/themes.js
@@ -1,348 +1,347 @@
-(function() {
-/**
- * Nexus Terminal — terminal colour themes.
- * Provides a collection of preset xterm.js colour schemes and a manager
- * for applying them to terminal instances.
- */
+(function () {
+ /**
+ * Nexus Terminal — terminal colour themes.
+ * Provides a collection of preset xterm.js colour schemes and a manager
+ * for applying them to terminal instances.
+ */
-/**
- * Preset terminal colour themes.
- * Each entry contains the full xterm.js theme object.
- */
-const THEMES = {
+ /**
+ * Preset terminal colour themes.
+ * Each entry contains the full xterm.js theme object.
+ */
+ const THEMES = {
charcoal: {
- label: 'Charcoal (Default)',
- theme: {
- background: '#1c1c1c',
- foreground: '#e4e4e4',
- cursor: '#4ecdc4',
- cursorAccent: '#1c1c1c',
- selectionBackground: 'rgba(78, 205, 196, 0.3)',
- selectionForeground: '#ffffff',
- black: '#1c1c1c',
- red: '#ef5350',
- green: '#66bb6a',
- yellow: '#ffa726',
- blue: '#42a5f5',
- magenta: '#ab47bc',
- cyan: '#4ecdc4',
- white: '#e4e4e4',
- brightBlack: '#555555',
- brightRed: '#ff7043',
- brightGreen: '#81c784',
- brightYellow: '#ffcc02',
- brightBlue: '#64b5f6',
- brightMagenta: '#ce93d8',
- brightCyan: '#80deea',
- brightWhite: '#ffffff',
- },
+ label: "Charcoal (Default)",
+ theme: {
+ background: "#1c1c1c",
+ foreground: "#e4e4e4",
+ cursor: "#4ecdc4",
+ cursorAccent: "#1c1c1c",
+ selectionBackground: "rgba(78, 205, 196, 0.3)",
+ selectionForeground: "#ffffff",
+ black: "#1c1c1c",
+ red: "#ef5350",
+ green: "#66bb6a",
+ yellow: "#ffa726",
+ blue: "#42a5f5",
+ magenta: "#ab47bc",
+ cyan: "#4ecdc4",
+ white: "#e4e4e4",
+ brightBlack: "#555555",
+ brightRed: "#ff7043",
+ brightGreen: "#81c784",
+ brightYellow: "#ffcc02",
+ brightBlue: "#64b5f6",
+ brightMagenta: "#ce93d8",
+ brightCyan: "#80deea",
+ brightWhite: "#ffffff",
+ },
},
- 'solarized-dark': {
- label: 'Solarized Dark',
- theme: {
- background: '#002b36',
- foreground: '#839496',
- cursor: '#93a1a1',
- cursorAccent: '#002b36',
- selectionBackground: 'rgba(147, 161, 161, 0.3)',
- selectionForeground: '#fdf6e3',
- black: '#073642',
- red: '#dc322f',
- green: '#859900',
- yellow: '#b58900',
- blue: '#268bd2',
- magenta: '#d33682',
- cyan: '#2aa198',
- white: '#eee8d5',
- brightBlack: '#586e75',
- brightRed: '#cb4b16',
- brightGreen: '#586e75',
- brightYellow: '#657b83',
- brightBlue: '#839496',
- brightMagenta: '#6c71c4',
- brightCyan: '#93a1a1',
- brightWhite: '#fdf6e3',
- },
+ "solarized-dark": {
+ label: "Solarized Dark",
+ theme: {
+ background: "#002b36",
+ foreground: "#839496",
+ cursor: "#93a1a1",
+ cursorAccent: "#002b36",
+ selectionBackground: "rgba(147, 161, 161, 0.3)",
+ selectionForeground: "#fdf6e3",
+ black: "#073642",
+ red: "#dc322f",
+ green: "#859900",
+ yellow: "#b58900",
+ blue: "#268bd2",
+ magenta: "#d33682",
+ cyan: "#2aa198",
+ white: "#eee8d5",
+ brightBlack: "#586e75",
+ brightRed: "#cb4b16",
+ brightGreen: "#586e75",
+ brightYellow: "#657b83",
+ brightBlue: "#839496",
+ brightMagenta: "#6c71c4",
+ brightCyan: "#93a1a1",
+ brightWhite: "#fdf6e3",
+ },
},
monokai: {
- label: 'Monokai',
- theme: {
- background: '#272822',
- foreground: '#f8f8f2',
- cursor: '#f8f8f0',
- cursorAccent: '#272822',
- selectionBackground: 'rgba(73, 72, 62, 0.6)',
- selectionForeground: '#f8f8f2',
- black: '#272822',
- red: '#f92672',
- green: '#a6e22e',
- yellow: '#f4bf75',
- blue: '#66d9ef',
- magenta: '#ae81ff',
- cyan: '#a1efe4',
- white: '#f8f8f2',
- brightBlack: '#75715e',
- brightRed: '#f92672',
- brightGreen: '#a6e22e',
- brightYellow: '#f4bf75',
- brightBlue: '#66d9ef',
- brightMagenta: '#ae81ff',
- brightCyan: '#a1efe4',
- brightWhite: '#f9f8f5',
- },
+ label: "Monokai",
+ theme: {
+ background: "#272822",
+ foreground: "#f8f8f2",
+ cursor: "#f8f8f0",
+ cursorAccent: "#272822",
+ selectionBackground: "rgba(73, 72, 62, 0.6)",
+ selectionForeground: "#f8f8f2",
+ black: "#272822",
+ red: "#f92672",
+ green: "#a6e22e",
+ yellow: "#f4bf75",
+ blue: "#66d9ef",
+ magenta: "#ae81ff",
+ cyan: "#a1efe4",
+ white: "#f8f8f2",
+ brightBlack: "#75715e",
+ brightRed: "#f92672",
+ brightGreen: "#a6e22e",
+ brightYellow: "#f4bf75",
+ brightBlue: "#66d9ef",
+ brightMagenta: "#ae81ff",
+ brightCyan: "#a1efe4",
+ brightWhite: "#f9f8f5",
+ },
},
nord: {
- label: 'Nord',
- theme: {
- background: '#2e3440',
- foreground: '#d8dee9',
- cursor: '#d8dee9',
- cursorAccent: '#2e3440',
- selectionBackground: 'rgba(136, 192, 208, 0.3)',
- selectionForeground: '#eceff4',
- black: '#3b4252',
- red: '#bf616a',
- green: '#a3be8c',
- yellow: '#ebcb8b',
- blue: '#81a1c1',
- magenta: '#b48ead',
- cyan: '#88c0d0',
- white: '#e5e9f0',
- brightBlack: '#4c566a',
- brightRed: '#bf616a',
- brightGreen: '#a3be8c',
- brightYellow: '#ebcb8b',
- brightBlue: '#81a1c1',
- brightMagenta: '#b48ead',
- brightCyan: '#8fbcbb',
- brightWhite: '#eceff4',
- },
+ label: "Nord",
+ theme: {
+ background: "#2e3440",
+ foreground: "#d8dee9",
+ cursor: "#d8dee9",
+ cursorAccent: "#2e3440",
+ selectionBackground: "rgba(136, 192, 208, 0.3)",
+ selectionForeground: "#eceff4",
+ black: "#3b4252",
+ red: "#bf616a",
+ green: "#a3be8c",
+ yellow: "#ebcb8b",
+ blue: "#81a1c1",
+ magenta: "#b48ead",
+ cyan: "#88c0d0",
+ white: "#e5e9f0",
+ brightBlack: "#4c566a",
+ brightRed: "#bf616a",
+ brightGreen: "#a3be8c",
+ brightYellow: "#ebcb8b",
+ brightBlue: "#81a1c1",
+ brightMagenta: "#b48ead",
+ brightCyan: "#8fbcbb",
+ brightWhite: "#eceff4",
+ },
},
dracula: {
- label: 'Dracula',
- theme: {
- background: '#282a36',
- foreground: '#f8f8f2',
- cursor: '#f8f8f2',
- cursorAccent: '#282a36',
- selectionBackground: 'rgba(68, 71, 90, 0.6)',
- selectionForeground: '#f8f8f2',
- black: '#21222c',
- red: '#ff5555',
- green: '#50fa7b',
- yellow: '#f1fa8c',
- blue: '#bd93f9',
- magenta: '#ff79c6',
- cyan: '#8be9fd',
- white: '#f8f8f2',
- brightBlack: '#6272a4',
- brightRed: '#ff6e6e',
- brightGreen: '#69ff94',
- brightYellow: '#ffffa5',
- brightBlue: '#d6acff',
- brightMagenta: '#ff92df',
- brightCyan: '#a4ffff',
- brightWhite: '#ffffff',
- },
+ label: "Dracula",
+ theme: {
+ background: "#282a36",
+ foreground: "#f8f8f2",
+ cursor: "#f8f8f2",
+ cursorAccent: "#282a36",
+ selectionBackground: "rgba(68, 71, 90, 0.6)",
+ selectionForeground: "#f8f8f2",
+ black: "#21222c",
+ red: "#ff5555",
+ green: "#50fa7b",
+ yellow: "#f1fa8c",
+ blue: "#bd93f9",
+ magenta: "#ff79c6",
+ cyan: "#8be9fd",
+ white: "#f8f8f2",
+ brightBlack: "#6272a4",
+ brightRed: "#ff6e6e",
+ brightGreen: "#69ff94",
+ brightYellow: "#ffffa5",
+ brightBlue: "#d6acff",
+ brightMagenta: "#ff92df",
+ brightCyan: "#a4ffff",
+ brightWhite: "#ffffff",
+ },
},
- 'one-dark': {
- label: 'One Dark',
- theme: {
- background: '#282c34',
- foreground: '#abb2bf',
- cursor: '#528bff',
- cursorAccent: '#282c34',
- selectionBackground: 'rgba(82, 139, 255, 0.3)',
- selectionForeground: '#abb2bf',
- black: '#282c34',
- red: '#e06c75',
- green: '#98c379',
- yellow: '#e5c07b',
- blue: '#61afef',
- magenta: '#c678dd',
- cyan: '#56b6c2',
- white: '#abb2bf',
- brightBlack: '#5c6370',
- brightRed: '#e06c75',
- brightGreen: '#98c379',
- brightYellow: '#e5c07b',
- brightBlue: '#61afef',
- brightMagenta: '#c678dd',
- brightCyan: '#56b6c2',
- brightWhite: '#ffffff',
- },
+ "one-dark": {
+ label: "One Dark",
+ theme: {
+ background: "#282c34",
+ foreground: "#abb2bf",
+ cursor: "#528bff",
+ cursorAccent: "#282c34",
+ selectionBackground: "rgba(82, 139, 255, 0.3)",
+ selectionForeground: "#abb2bf",
+ black: "#282c34",
+ red: "#e06c75",
+ green: "#98c379",
+ yellow: "#e5c07b",
+ blue: "#61afef",
+ magenta: "#c678dd",
+ cyan: "#56b6c2",
+ white: "#abb2bf",
+ brightBlack: "#5c6370",
+ brightRed: "#e06c75",
+ brightGreen: "#98c379",
+ brightYellow: "#e5c07b",
+ brightBlue: "#61afef",
+ brightMagenta: "#c678dd",
+ brightCyan: "#56b6c2",
+ brightWhite: "#ffffff",
+ },
},
- 'gruvbox-dark': {
- label: 'Gruvbox Dark',
- theme: {
- background: '#282828',
- foreground: '#ebdbb2',
- cursor: '#ebdbb2',
- cursorAccent: '#282828',
- selectionBackground: 'rgba(214, 93, 14, 0.3)',
- selectionForeground: '#ebdbb2',
- black: '#282828',
- red: '#cc241d',
- green: '#98971a',
- yellow: '#d79921',
- blue: '#458588',
- magenta: '#b16286',
- cyan: '#689d6a',
- white: '#a89984',
- brightBlack: '#928374',
- brightRed: '#fb4934',
- brightGreen: '#b8bb26',
- brightYellow: '#fabd2f',
- brightBlue: '#83a598',
- brightMagenta: '#d3869b',
- brightCyan: '#8ec07c',
- brightWhite: '#ebdbb2',
- },
+ "gruvbox-dark": {
+ label: "Gruvbox Dark",
+ theme: {
+ background: "#282828",
+ foreground: "#ebdbb2",
+ cursor: "#ebdbb2",
+ cursorAccent: "#282828",
+ selectionBackground: "rgba(214, 93, 14, 0.3)",
+ selectionForeground: "#ebdbb2",
+ black: "#282828",
+ red: "#cc241d",
+ green: "#98971a",
+ yellow: "#d79921",
+ blue: "#458588",
+ magenta: "#b16286",
+ cyan: "#689d6a",
+ white: "#a89984",
+ brightBlack: "#928374",
+ brightRed: "#fb4934",
+ brightGreen: "#b8bb26",
+ brightYellow: "#fabd2f",
+ brightBlue: "#83a598",
+ brightMagenta: "#d3869b",
+ brightCyan: "#8ec07c",
+ brightWhite: "#ebdbb2",
+ },
},
- 'tokyo-night': {
- label: 'Tokyo Night',
- theme: {
- background: '#1a1b26',
- foreground: '#a9b1d6',
- cursor: '#c0caf5',
- cursorAccent: '#1a1b26',
- selectionBackground: 'rgba(41, 46, 66, 0.9)',
- selectionForeground: '#c0caf5',
- black: '#15161e',
- red: '#f7768e',
- green: '#9ece6a',
- yellow: '#e0af68',
- blue: '#7aa2f7',
- magenta: '#bb9af7',
- cyan: '#7dcfff',
- white: '#a9b1d6',
- brightBlack: '#414868',
- brightRed: '#f7768e',
- brightGreen: '#9ece6a',
- brightYellow: '#e0af68',
- brightBlue: '#7aa2f7',
- brightMagenta: '#bb9af7',
- brightCyan: '#7dcfff',
- brightWhite: '#c0caf5',
- },
+ "tokyo-night": {
+ label: "Tokyo Night",
+ theme: {
+ background: "#1a1b26",
+ foreground: "#a9b1d6",
+ cursor: "#c0caf5",
+ cursorAccent: "#1a1b26",
+ selectionBackground: "rgba(41, 46, 66, 0.9)",
+ selectionForeground: "#c0caf5",
+ black: "#15161e",
+ red: "#f7768e",
+ green: "#9ece6a",
+ yellow: "#e0af68",
+ blue: "#7aa2f7",
+ magenta: "#bb9af7",
+ cyan: "#7dcfff",
+ white: "#a9b1d6",
+ brightBlack: "#414868",
+ brightRed: "#f7768e",
+ brightGreen: "#9ece6a",
+ brightYellow: "#e0af68",
+ brightBlue: "#7aa2f7",
+ brightMagenta: "#bb9af7",
+ brightCyan: "#7dcfff",
+ brightWhite: "#c0caf5",
+ },
},
- 'catppuccin-mocha': {
- label: 'Catppuccin Mocha',
- theme: {
- background: '#1e1e2e',
- foreground: '#cdd6f4',
- cursor: '#f5e0dc',
- cursorAccent: '#1e1e2e',
- selectionBackground: 'rgba(88, 91, 112, 0.5)',
- selectionForeground: '#cdd6f4',
- black: '#45475a',
- red: '#f38ba8',
- green: '#a6e3a1',
- yellow: '#f9e2af',
- blue: '#89b4fa',
- magenta: '#f5c2e7',
- cyan: '#94e2d5',
- white: '#bac2de',
- brightBlack: '#585b70',
- brightRed: '#f38ba8',
- brightGreen: '#a6e3a1',
- brightYellow: '#f9e2af',
- brightBlue: '#89b4fa',
- brightMagenta: '#f5c2e7',
- brightCyan: '#94e2d5',
- brightWhite: '#a6adc8',
- },
+ "catppuccin-mocha": {
+ label: "Catppuccin Mocha",
+ theme: {
+ background: "#1e1e2e",
+ foreground: "#cdd6f4",
+ cursor: "#f5e0dc",
+ cursorAccent: "#1e1e2e",
+ selectionBackground: "rgba(88, 91, 112, 0.5)",
+ selectionForeground: "#cdd6f4",
+ black: "#45475a",
+ red: "#f38ba8",
+ green: "#a6e3a1",
+ yellow: "#f9e2af",
+ blue: "#89b4fa",
+ magenta: "#f5c2e7",
+ cyan: "#94e2d5",
+ white: "#bac2de",
+ brightBlack: "#585b70",
+ brightRed: "#f38ba8",
+ brightGreen: "#a6e3a1",
+ brightYellow: "#f9e2af",
+ brightBlue: "#89b4fa",
+ brightMagenta: "#f5c2e7",
+ brightCyan: "#94e2d5",
+ brightWhite: "#a6adc8",
+ },
},
- 'rose-pine': {
- label: 'Rosé Pine',
- theme: {
- background: '#191724',
- foreground: '#e0def4',
- cursor: '#524f67',
- cursorAccent: '#e0def4',
- selectionBackground: 'rgba(110, 106, 134, 0.4)',
- selectionForeground: '#e0def4',
- black: '#26233a',
- red: '#eb6f92',
- green: '#31748f',
- yellow: '#f6c177',
- blue: '#9ccfd8',
- magenta: '#c4a7e7',
- cyan: '#ebbcba',
- white: '#e0def4',
- brightBlack: '#6e6a86',
- brightRed: '#eb6f92',
- brightGreen: '#31748f',
- brightYellow: '#f6c177',
- brightBlue: '#9ccfd8',
- brightMagenta: '#c4a7e7',
- brightCyan: '#ebbcba',
- brightWhite: '#e0def4',
- },
+ "rose-pine": {
+ label: "Rosé Pine",
+ theme: {
+ background: "#191724",
+ foreground: "#e0def4",
+ cursor: "#524f67",
+ cursorAccent: "#e0def4",
+ selectionBackground: "rgba(110, 106, 134, 0.4)",
+ selectionForeground: "#e0def4",
+ black: "#26233a",
+ red: "#eb6f92",
+ green: "#31748f",
+ yellow: "#f6c177",
+ blue: "#9ccfd8",
+ magenta: "#c4a7e7",
+ cyan: "#ebbcba",
+ white: "#e0def4",
+ brightBlack: "#6e6a86",
+ brightRed: "#eb6f92",
+ brightGreen: "#31748f",
+ brightYellow: "#f6c177",
+ brightBlue: "#9ccfd8",
+ brightMagenta: "#c4a7e7",
+ brightCyan: "#ebbcba",
+ brightWhite: "#e0def4",
+ },
},
- 'solarized-light': {
- label: 'Solarized Light',
- theme: {
- background: '#fdf6e3',
- foreground: '#657b83',
- cursor: '#586e75',
- cursorAccent: '#fdf6e3',
- selectionBackground: 'rgba(7, 54, 66, 0.2)',
- selectionForeground: '#073642',
- black: '#073642',
- red: '#dc322f',
- green: '#859900',
- yellow: '#b58900',
- blue: '#268bd2',
- magenta: '#d33682',
- cyan: '#2aa198',
- white: '#eee8d5',
- brightBlack: '#002b36',
- brightRed: '#cb4b16',
- brightGreen: '#586e75',
- brightYellow: '#657b83',
- brightBlue: '#839496',
- brightMagenta: '#6c71c4',
- brightCyan: '#93a1a1',
- brightWhite: '#fdf6e3',
- },
+ "solarized-light": {
+ label: "Solarized Light",
+ theme: {
+ background: "#fdf6e3",
+ foreground: "#657b83",
+ cursor: "#586e75",
+ cursorAccent: "#fdf6e3",
+ selectionBackground: "rgba(7, 54, 66, 0.2)",
+ selectionForeground: "#073642",
+ black: "#073642",
+ red: "#dc322f",
+ green: "#859900",
+ yellow: "#b58900",
+ blue: "#268bd2",
+ magenta: "#d33682",
+ cyan: "#2aa198",
+ white: "#eee8d5",
+ brightBlack: "#002b36",
+ brightRed: "#cb4b16",
+ brightGreen: "#586e75",
+ brightYellow: "#657b83",
+ brightBlue: "#839496",
+ brightMagenta: "#6c71c4",
+ brightCyan: "#93a1a1",
+ brightWhite: "#fdf6e3",
+ },
},
-};
+ };
-/**
- * Returns the theme object for the given theme name.
- * Falls back to 'charcoal' if not found.
- * @param {string} name
- * @returns {Object} xterm.js theme object
- */
-function getTerminalTheme(name) {
- const entry = THEMES[name || 'charcoal'];
+ /**
+ * Returns the theme object for the given theme name.
+ * Falls back to 'charcoal' if not found.
+ * @param {string} name
+ * @returns {Object} xterm.js theme object
+ */
+ function getTerminalTheme(name) {
+ const entry = THEMES[name || "charcoal"];
return entry ? entry.theme : THEMES.charcoal.theme;
-}
+ }
-/**
- * Returns all theme names and labels for building a selector.
- * @returns {Array<{value: string, label: string}>}
- */
-function listTerminalThemes() {
+ /**
+ * Returns all theme names and labels for building a selector.
+ * @returns {Array<{value: string, label: string}>}
+ */
+ function listTerminalThemes() {
return Object.entries(THEMES).map(([value, entry]) => ({
- value,
- label: entry.label,
+ value,
+ label: entry.label,
}));
-}
-
-window.NexusTerminal = window.NexusTerminal || {};
-window.NexusTerminal.getTerminalTheme = getTerminalTheme;
-window.NexusTerminal.listTerminalThemes = listTerminalThemes;
+ }
+ window.NexusTerminal = window.NexusTerminal || {};
+ window.NexusTerminal.getTerminalTheme = getTerminalTheme;
+ window.NexusTerminal.listTerminalThemes = listTerminalThemes;
})();
diff --git a/frontend/src/js/triggers.js b/frontend/src/js/triggers.js
index e9a2dc9..e8b3d95 100644
--- a/frontend/src/js/triggers.js
+++ b/frontend/src/js/triggers.js
@@ -92,7 +92,9 @@
}
} else {
const haystack = rule.caseSensitive ? clean : clean.toLowerCase();
- const needle = rule.caseSensitive ? rule.pattern : rule.pattern.toLowerCase();
+ const needle = rule.caseSensitive
+ ? rule.pattern
+ : rule.pattern.toLowerCase();
matched = haystack.includes(needle);
}
@@ -101,13 +103,19 @@
// Fire actions
if (rule.action === "notify" || rule.action === "both") {
const session = this.app.state.getSession(sessionId);
- const label = session ? (session.tabName || session.profileName) : sessionId;
- this.app.notifications.info(`[${label}] Trigger: ${rule.name || rule.pattern}`);
+ const label = session
+ ? session.tabName || session.profileName
+ : sessionId;
+ this.app.notifications.info(
+ `[${label}] Trigger: ${rule.name || rule.pattern}`,
+ );
}
if (rule.action === "highlight" || rule.action === "both") {
// Flash the tab briefly
- const tabEl = document.querySelector(`.tab[data-session-id="${sessionId}"]`);
+ const tabEl = document.querySelector(
+ `.tab[data-session-id="${sessionId}"]`,
+ );
if (tabEl) {
tabEl.classList.add("tab-trigger-flash");
setTimeout(() => tabEl.classList.remove("tab-trigger-flash"), 2000);
@@ -186,11 +194,15 @@
modal.classList.add("is-visible");
});
- modal.querySelector(".modal-close").addEventListener("click", () => this.close());
+ modal
+ .querySelector(".modal-close")
+ .addEventListener("click", () => this.close());
backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) this.close();
});
- modal.querySelector(".trigger-add-btn").addEventListener("click", () => this._addRule());
+ modal
+ .querySelector(".trigger-add-btn")
+ .addEventListener("click", () => this._addRule());
this._renderList();
}
@@ -201,7 +213,8 @@
list.innerHTML = "";
if (this._rules.length === 0) {
- list.innerHTML = 'No trigger rules defined
';
+ list.innerHTML =
+ 'No trigger rules defined
';
return;
}
@@ -236,11 +249,15 @@
});
const actions = createElement("div", "trigger-actions");
- const editBtn = createElement("button", "btn btn-xs", { textContent: "Edit" });
+ const editBtn = createElement("button", "btn btn-xs", {
+ textContent: "Edit",
+ });
editBtn.addEventListener("mousedown", (e) => e.preventDefault());
editBtn.addEventListener("click", () => this._editRule(index));
- const delBtn = createElement("button", "btn btn-xs btn-danger", { textContent: "✕" });
+ const delBtn = createElement("button", "btn btn-xs btn-danger", {
+ textContent: "✕",
+ });
delBtn.addEventListener("mousedown", (e) => e.preventDefault());
delBtn.addEventListener("click", () => {
this._rules.splice(index, 1);
@@ -287,7 +304,7 @@
form.innerHTML = `
-
+
@@ -325,14 +342,18 @@
list.parentNode.insertBefore(form, list);
- form.querySelector(".trigger-cancel-btn").addEventListener("click", () => {
- form.remove();
- list.style.display = "";
- if (toolbar) toolbar.style.display = "";
- });
+ form
+ .querySelector(".trigger-cancel-btn")
+ .addEventListener("click", () => {
+ form.remove();
+ list.style.display = "";
+ if (toolbar) toolbar.style.display = "";
+ });
form.querySelector(".trigger-save-btn").addEventListener("click", () => {
- const pattern = form.querySelector(".trigger-pattern-input").value.trim();
+ const pattern = form
+ .querySelector(".trigger-pattern-input")
+ .value.trim();
if (!pattern) {
this.app.notifications.error("Pattern is required");
return;
diff --git a/frontend/src/js/utils.js b/frontend/src/js/utils.js
index fe1059e..8e94e65 100644
--- a/frontend/src/js/utils.js
+++ b/frontend/src/js/utils.js
@@ -1,120 +1,132 @@
-(function() {
-/**
- * Nexus Terminal — utility functions.
- * General-purpose helpers used throughout the application.
- */
+(function () {
+ /**
+ * Nexus Terminal — utility functions.
+ * General-purpose helpers used throughout the application.
+ */
-/**
- * Shorthand for document.querySelector.
- * @param {string} selector - CSS selector.
- * @returns {Element|null}
- */
-function $(selector) {
+ /**
+ * Shorthand for document.querySelector.
+ * @param {string} selector - CSS selector.
+ * @returns {Element|null}
+ */
+ function $(selector) {
return document.querySelector(selector);
-}
+ }
-/**
- * Shorthand for document.querySelectorAll.
- * @param {string} selector - CSS selector.
- * @returns {NodeListOf}
- */
-function $$(selector) {
+ /**
+ * Shorthand for document.querySelectorAll.
+ * @param {string} selector - CSS selector.
+ * @returns {NodeListOf}
+ */
+ function $$(selector) {
return document.querySelectorAll(selector);
-}
+ }
-/**
- * Creates a DOM element with optional class name and attributes.
- * @param {string} tag - HTML tag name.
- * @param {string} [className] - Space-separated class names.
- * @param {Object} [attrs] - Attribute key/value pairs.
- * @returns {HTMLElement}
- */
-function createElement(tag, className, attrs = {}) {
+ /**
+ * Creates a DOM element with optional class name and attributes.
+ * @param {string} tag - HTML tag name.
+ * @param {string} [className] - Space-separated class names.
+ * @param {Object} [attrs] - Attribute key/value pairs.
+ * @returns {HTMLElement}
+ */
+ function createElement(tag, className, attrs = {}) {
const el = document.createElement(tag);
if (className) {
- el.className = className;
+ el.className = className;
}
for (const [key, value] of Object.entries(attrs)) {
- if (key === 'textContent') {
- el.textContent = value;
- } else if (key === 'innerHTML') {
- el.innerHTML = value;
- } else if (key === 'dataset') {
- for (const [dKey, dVal] of Object.entries(value)) {
- el.dataset[dKey] = dVal;
- }
- } else if (key.startsWith('on') && typeof value === 'function') {
- el.addEventListener(key.slice(2).toLowerCase(), value);
- } else {
- el.setAttribute(key, value);
+ if (key === "textContent") {
+ el.textContent = value;
+ } else if (key === "innerHTML") {
+ el.innerHTML = value;
+ } else if (key === "dataset") {
+ for (const [dKey, dVal] of Object.entries(value)) {
+ el.dataset[dKey] = dVal;
}
+ } else if (key.startsWith("on") && typeof value === "function") {
+ el.addEventListener(key.slice(2).toLowerCase(), value);
+ } else {
+ el.setAttribute(key, value);
+ }
}
return el;
-}
+ }
-/**
- * Escapes HTML special characters to prevent XSS.
- * @param {string} str - Raw string.
- * @returns {string} Escaped string safe for innerHTML.
- */
-function escapeHtml(str) {
- if (!str) return '';
+ /**
+ * Escapes HTML special characters to prevent XSS.
+ * @param {string} str - Raw string.
+ * @returns {string} Escaped string safe for innerHTML.
+ */
+ function escapeHtml(str) {
+ if (!str) return "";
const map = {
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": ''',
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': """,
+ "'": "'",
};
return str.replace(/[&<>"']/g, (ch) => map[ch]);
-}
+ }
-/**
- * Formats a Date (or ISO string) into a concise timestamp.
- * @param {Date|string} date
- * @returns {string} e.g. "14:32:07" or "2 Jan 14:32".
- */
-function formatTimestamp(date) {
- if (!date) return '';
+ /**
+ * Formats a Date (or ISO string) into a concise timestamp.
+ * @param {Date|string} date
+ * @returns {string} e.g. "14:32:07" or "2 Jan 14:32".
+ */
+ function formatTimestamp(date) {
+ if (!date) return "";
const d = date instanceof Date ? date : new Date(date);
- if (isNaN(d.getTime())) return '';
+ if (isNaN(d.getTime())) return "";
const now = new Date();
const isToday = d.toDateString() === now.toDateString();
- const hours = String(d.getHours()).padStart(2, '0');
- const mins = String(d.getMinutes()).padStart(2, '0');
- const secs = String(d.getSeconds()).padStart(2, '0');
+ const hours = String(d.getHours()).padStart(2, "0");
+ const mins = String(d.getMinutes()).padStart(2, "0");
+ const secs = String(d.getSeconds()).padStart(2, "0");
if (isToday) {
- return `${hours}:${mins}:${secs}`;
+ return `${hours}:${mins}:${secs}`;
}
- const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
- 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ const months = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
return `${d.getDate()} ${months[d.getMonth()]} ${hours}:${mins}`;
-}
+ }
-/**
- * Returns a debounced version of the given function.
- * @param {Function} fn
- * @param {number} ms - Delay in milliseconds.
- * @returns {Function}
- */
-function debounce(fn, ms) {
+ /**
+ * Returns a debounced version of the given function.
+ * @param {Function} fn
+ * @param {number} ms - Delay in milliseconds.
+ * @returns {Function}
+ */
+ function debounce(fn, ms) {
let timer = null;
return function (...args) {
- clearTimeout(timer);
- timer = setTimeout(() => fn.apply(this, args), ms);
+ clearTimeout(timer);
+ timer = setTimeout(() => fn.apply(this, args), ms);
};
-}
+ }
-/**
- * Performs a simple fuzzy match — checks whether every character in
- * the query appears in order within the text (case-insensitive).
- * @param {string} text
- * @param {string} query
- * @returns {{ match: boolean, score: number }}
- */
-function fuzzyMatch(text, query) {
+ /**
+ * Performs a simple fuzzy match — checks whether every character in
+ * the query appears in order within the text (case-insensitive).
+ * @param {string} text
+ * @param {string} query
+ * @returns {{ match: boolean, score: number }}
+ */
+ function fuzzyMatch(text, query) {
if (!query) return { match: true, score: 1 };
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
@@ -124,169 +136,200 @@ function fuzzyMatch(text, query) {
let consecutiveBonus = 0;
for (let qi = 0; qi < lowerQuery.length; qi++) {
- const ch = lowerQuery[qi];
- const foundAt = lowerText.indexOf(ch, textIdx);
- if (foundAt === -1) {
- return { match: false, score: 0 };
- }
- // Reward consecutive character matches and early matches
- if (foundAt === textIdx) {
- consecutiveBonus += 1;
- } else {
- consecutiveBonus = 0;
- }
- score += 1 + consecutiveBonus + (foundAt === 0 ? 2 : 0);
- textIdx = foundAt + 1;
+ const ch = lowerQuery[qi];
+ const foundAt = lowerText.indexOf(ch, textIdx);
+ if (foundAt === -1) {
+ return { match: false, score: 0 };
+ }
+ // Reward consecutive character matches and early matches
+ if (foundAt === textIdx) {
+ consecutiveBonus += 1;
+ } else {
+ consecutiveBonus = 0;
+ }
+ score += 1 + consecutiveBonus + (foundAt === 0 ? 2 : 0);
+ textIdx = foundAt + 1;
}
return { match: true, score };
-}
+ }
-/**
- * Base64-encodes a string.
- * @param {string} str
- * @returns {string}
- */
-function base64Encode(str) {
+ /**
+ * Base64-encodes a string.
+ * @param {string} str
+ * @returns {string}
+ */
+ function base64Encode(str) {
return btoa(str);
-}
+ }
-/**
- * Base64-decodes a string.
- * @param {string} str
- * @returns {string}
- */
-function base64Decode(str) {
+ /**
+ * Base64-decodes a string.
+ * @param {string} str
+ * @returns {string}
+ */
+ function base64Decode(str) {
return atob(str);
-}
+ }
-/**
- * Generates a new UUID v4 using the browser crypto API.
- * @returns {string}
- */
-function generateId() {
+ /**
+ * Generates a new UUID v4 using the browser crypto API.
+ * @returns {string}
+ */
+ function generateId() {
return crypto.randomUUID();
-}
+ }
-/**
- * Formats a byte count into a human-friendly string.
- * @param {number} bytes
- * @returns {string} e.g. "1.23 KB"
- */
-function formatBytes(bytes) {
- if (bytes === 0) return '0 B';
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+ /**
+ * Formats a byte count into a human-friendly string.
+ * @param {number} bytes
+ * @returns {string} e.g. "1.23 KB"
+ */
+ function formatBytes(bytes) {
+ if (bytes === 0) return "0 B";
+ const units = ["B", "KB", "MB", "GB", "TB"];
const k = 1024;
const i = Math.floor(Math.log(bytes) / Math.log(k));
const value = bytes / Math.pow(k, i);
return `${value.toFixed(i === 0 ? 0 : 2)} ${units[i]}`;
-}
+ }
-/**
- * Truncates a string to the given length, appending an ellipsis if needed.
- * @param {string} str
- * @param {number} len
- * @returns {string}
- */
-function truncate(str, len) {
- if (!str || str.length <= len) return str || '';
- return str.slice(0, len) + '…';
-}
+ /**
+ * Truncates a string to the given length, appending an ellipsis if needed.
+ * @param {string} str
+ * @param {number} len
+ * @returns {string}
+ */
+ function truncate(str, len) {
+ if (!str || str.length <= len) return str || "";
+ return str.slice(0, len) + "…";
+ }
-/**
- * Removes any open context menus from the DOM.
- */
-function dismissContextMenus() {
- document.querySelectorAll('.context-menu').forEach((m) => m.remove());
-}
+ /**
+ * Removes any open context menus from the DOM.
+ */
+ function dismissContextMenus() {
+ document.querySelectorAll(".context-menu").forEach((m) => m.remove());
+ }
-/**
- * Returns the protocol badge colour class.
- * @param {string} protocol - 'ssh', 'telnet', 'raw', or 'github'.
- * @returns {string}
- */
-function protocolColour(protocol) {
+ /**
+ * Returns the protocol badge colour class.
+ * @param {string} protocol - 'ssh', 'telnet', 'raw', or 'github'.
+ * @returns {string}
+ */
+ function protocolColour(protocol) {
switch (protocol) {
- case 'ssh': return 'proto-ssh';
- case 'telnet': return 'proto-telnet';
- case 'raw': return 'proto-raw';
- case 'github': return 'proto-github';
- case 'vnc': return 'proto-vnc';
- default: return '';
+ case "ssh":
+ return "proto-ssh";
+ case "telnet":
+ return "proto-telnet";
+ case "raw":
+ return "proto-raw";
+ case "github":
+ return "proto-github";
+ case "vnc":
+ return "proto-vnc";
+ default:
+ return "";
}
-}
+ }
-/**
- * Returns a human-friendly relative time string for an ISO date.
- * @param {string} isoDate - ISO 8601 date string.
- * @returns {string} e.g. "3 hours ago", "2 days ago".
- */
-function relativeTime(isoDate) {
- if (!isoDate) return '';
+ /**
+ * Returns a human-friendly relative time string for an ISO date.
+ * @param {string} isoDate - ISO 8601 date string.
+ * @returns {string} e.g. "3 hours ago", "2 days ago".
+ */
+ function relativeTime(isoDate) {
+ if (!isoDate) return "";
const d = new Date(isoDate);
- if (isNaN(d.getTime())) return '';
+ if (isNaN(d.getTime())) return "";
const now = Date.now();
const diff = now - d.getTime();
const secs = Math.floor(diff / 1000);
- if (secs < 60) return 'just now';
+ if (secs < 60) return "just now";
const mins = Math.floor(secs / 60);
- if (mins < 60) return mins === 1 ? '1 minute ago' : `${mins} minutes ago`;
+ if (mins < 60) return mins === 1 ? "1 minute ago" : `${mins} minutes ago`;
const hrs = Math.floor(mins / 60);
- if (hrs < 24) return hrs === 1 ? '1 hour ago' : `${hrs} hours ago`;
+ if (hrs < 24) return hrs === 1 ? "1 hour ago" : `${hrs} hours ago`;
const days = Math.floor(hrs / 24);
- if (days < 30) return days === 1 ? '1 day ago' : `${days} days ago`;
+ if (days < 30) return days === 1 ? "1 day ago" : `${days} days ago`;
const months = Math.floor(days / 30);
- if (months < 12) return months === 1 ? '1 month ago' : `${months} months ago`;
+ if (months < 12)
+ return months === 1 ? "1 month ago" : `${months} months ago`;
const years = Math.floor(months / 12);
- return years === 1 ? '1 year ago' : `${years} years ago`;
-}
+ return years === 1 ? "1 year ago" : `${years} years ago`;
+ }
-/**
- * Returns a human-friendly label for a session state.
- * @param {string} state
- * @returns {string}
- */
-function stateLabel(state) {
+ /**
+ * Returns a human-friendly label for a session state.
+ * @param {string} state
+ * @returns {string}
+ */
+ function stateLabel(state) {
switch (state) {
- case 'connecting': return 'Connecting';
- case 'connected': return 'Connected';
- case 'disconnected': return 'Disconnected';
- case 'reconnecting': return 'Reconnecting';
- case 'error': return 'Error';
- default: return state || 'Unknown';
+ case "connecting":
+ return "Connecting";
+ case "connected":
+ return "Connected";
+ case "disconnected":
+ return "Disconnected";
+ case "reconnecting":
+ return "Reconnecting";
+ case "error":
+ return "Error";
+ default:
+ return state || "Unknown";
}
-}
+ }
-window.NexusTerminal = window.NexusTerminal || {};
-Object.assign(window.NexusTerminal, { $, $$, createElement, escapeHtml, formatTimestamp, debounce, fuzzyMatch, base64Encode, base64Decode, generateId, formatBytes, truncate, dismissContextMenus, protocolColour, stateLabel, relativeTime });
+ window.NexusTerminal = window.NexusTerminal || {};
+ Object.assign(window.NexusTerminal, {
+ $,
+ $$,
+ createElement,
+ escapeHtml,
+ formatTimestamp,
+ debounce,
+ fuzzyMatch,
+ base64Encode,
+ base64Decode,
+ generateId,
+ formatBytes,
+ truncate,
+ dismissContextMenus,
+ protocolColour,
+ stateLabel,
+ relativeTime,
+ });
-/* ── Disable macOS/WebKit auto-capitalisation on all text inputs ───────── */
-function disableAutoCapitalise(el) {
- el.setAttribute('autocapitalize', 'none');
- el.setAttribute('autocorrect', 'off');
- el.setAttribute('spellcheck', 'false');
-}
+ /* ── Disable macOS/WebKit auto-capitalisation on all text inputs ───────── */
+ function disableAutoCapitalise(el) {
+ el.setAttribute("autocapitalize", "none");
+ el.setAttribute("autocorrect", "off");
+ el.setAttribute("spellcheck", "false");
+ }
-// Apply to existing inputs
-document.querySelectorAll('input, textarea').forEach(disableAutoCapitalise);
+ // Apply to existing inputs
+ document.querySelectorAll("input, textarea").forEach(disableAutoCapitalise);
-// Observe for dynamically added inputs
-var inputObserver = new MutationObserver(function(mutations) {
+ // Observe for dynamically added inputs
+ var inputObserver = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
- var nodes = mutations[i].addedNodes;
- for (var j = 0; j < nodes.length; j++) {
- var node = nodes[j];
- if (node.nodeType !== 1) continue;
- if (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') {
- disableAutoCapitalise(node);
- }
- if (node.querySelectorAll) {
- node.querySelectorAll('input, textarea').forEach(disableAutoCapitalise);
- }
+ var nodes = mutations[i].addedNodes;
+ for (var j = 0; j < nodes.length; j++) {
+ var node = nodes[j];
+ if (node.nodeType !== 1) continue;
+ if (node.tagName === "INPUT" || node.tagName === "TEXTAREA") {
+ disableAutoCapitalise(node);
}
+ if (node.querySelectorAll) {
+ node
+ .querySelectorAll("input, textarea")
+ .forEach(disableAutoCapitalise);
+ }
+ }
}
-});
-inputObserver.observe(document.body, { childList: true, subtree: true });
-
+ });
+ inputObserver.observe(document.body, { childList: true, subtree: true });
})();
diff --git a/frontend/src/js/vnc.js b/frontend/src/js/vnc.js
index a00bc53..1f8495d 100644
--- a/frontend/src/js/vnc.js
+++ b/frontend/src/js/vnc.js
@@ -91,16 +91,24 @@
});
ctrlAltDel.addEventListener("click", () => this._sendCtrlAltDel());
- const clipboardBtn = createElement("button", "btn btn-sm btn-vnc-action", {
- textContent: "Clipboard",
- title: "Sync clipboard",
- });
+ const clipboardBtn = createElement(
+ "button",
+ "btn btn-sm btn-vnc-action",
+ {
+ textContent: "Clipboard",
+ title: "Sync clipboard",
+ },
+ );
clipboardBtn.addEventListener("click", () => this._syncClipboard());
- const fullscreenBtn = createElement("button", "btn btn-sm btn-vnc-action", {
- textContent: "Fullscreen",
- title: "Toggle fullscreen",
- });
+ const fullscreenBtn = createElement(
+ "button",
+ "btn btn-sm btn-vnc-action",
+ {
+ textContent: "Fullscreen",
+ title: "Toggle fullscreen",
+ },
+ );
fullscreenBtn.addEventListener("click", () => this._toggleFullscreen());
this.statusEl = createElement("span", "vnc-status", {
diff --git a/internal/storage/migrations/004_vnc_protocol.sql b/internal/storage/migrations/004_vnc_protocol.sql
index 3939dd6..6710d49 100644
--- a/internal/storage/migrations/004_vnc_protocol.sql
+++ b/internal/storage/migrations/004_vnc_protocol.sql
@@ -1,12 +1,13 @@
-- Migration 004: Add VNC protocol support
-- Adds the 'vnc' value to the protocol CHECK constraint and a vnc_config column.
-
-- Step 1: Create the new table with the updated constraint.
CREATE TABLE IF NOT EXISTS profiles_new (
id TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
description TEXT DEFAULT '',
- protocol TEXT NOT NULL CHECK(protocol IN ('ssh', 'telnet', 'raw', 'github', 'vnc')),
+ protocol TEXT NOT NULL CHECK(
+ protocol IN ('ssh', 'telnet', 'raw', 'github', 'vnc')
+ ),
host TEXT NOT NULL,
port INTEGER NOT NULL,
username TEXT DEFAULT '',
@@ -32,26 +33,73 @@ CREATE TABLE IF NOT EXISTS profiles_new (
);
-- Step 2: Copy existing data (vnc_config will be NULL for legacy rows).
-INSERT OR IGNORE INTO profiles_new (
- id, display_name, description, protocol, host, port, username,
- tags, folder, environment, notes, favourite, last_connected,
- connect_timeout, read_timeout,
- terminal_font_family, terminal_font_size, terminal_scrollback, terminal_cursor_style,
- ssh_config, telnet_config, raw_config, github_config, vnc_config,
- created_at, updated_at
-)
+INSERT
+ OR IGNORE INTO profiles_new (
+ id,
+ display_name,
+ description,
+ protocol,
+ host,
+ port,
+ username,
+ tags,
+ folder,
+ environment,
+ notes,
+ favourite,
+ last_connected,
+ connect_timeout,
+ read_timeout,
+ terminal_font_family,
+ terminal_font_size,
+ terminal_scrollback,
+ terminal_cursor_style,
+ ssh_config,
+ telnet_config,
+ raw_config,
+ github_config,
+ vnc_config,
+ created_at,
+ updated_at
+ )
SELECT
- id, display_name, description, protocol, host, port, username,
- tags, folder, environment, notes, favourite, last_connected,
- connect_timeout, read_timeout,
- terminal_font_family, terminal_font_size, terminal_scrollback, terminal_cursor_style,
- ssh_config, telnet_config, raw_config, github_config, NULL,
- created_at, updated_at
-FROM profiles;
+ id,
+ display_name,
+ description,
+ protocol,
+ host,
+ port,
+ username,
+ tags,
+ folder,
+ environment,
+ notes,
+ favourite,
+ last_connected,
+ connect_timeout,
+ read_timeout,
+ terminal_font_family,
+ terminal_font_size,
+ terminal_scrollback,
+ terminal_cursor_style,
+ ssh_config,
+ telnet_config,
+ raw_config,
+ github_config,
+ NULL,
+ created_at,
+ updated_at
+FROM
+ profiles;
-- Step 3: Drop old table and rename.
DROP TABLE profiles;
-ALTER TABLE profiles_new RENAME TO profiles;
+
+ALTER TABLE
+ profiles_new RENAME TO profiles;
-- Step 4: Record the migration version.
-INSERT INTO schema_version (version, applied_at) VALUES (4, datetime('now'));
+INSERT INTO
+ schema_version (version, applied_at)
+VALUES
+ (4, datetime('now'));
\ No newline at end of file