From 3ff04feb922a09694bc984e17c7226bcc0036eae Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Thu, 21 May 2026 12:53:08 +0100 Subject: [PATCH] Refactor: Improve code readability and formatting across multiple files - Enhanced formatting in triggers.js for better readability, including consistent line breaks and indentation. - Updated utility functions in utils.js for improved clarity and organization, ensuring consistent formatting and spacing. - Refactored VNC button creation in vnc.js to enhance readability with better structured code. - Modified SQL migration script to improve readability by aligning SQL statements and enhancing the structure of the INSERT statement. --- frontend/scripts/bundle-novnc.js | 13 +- frontend/src/css/tabs.css | 17 +- frontend/src/css/tokens.css | 201 +++--- frontend/src/css/vnc.css | 4 +- frontend/src/js/app.js | 8 +- frontend/src/js/profiles.js | 14 +- frontend/src/js/sshkeys.js | 87 ++- frontend/src/js/themes.js | 629 +++++++++--------- frontend/src/js/triggers.js | 53 +- frontend/src/js/utils.js | 467 +++++++------ frontend/src/js/vnc.js | 24 +- .../storage/migrations/004_vnc_protocol.sql | 86 ++- 12 files changed, 891 insertions(+), 712 deletions(-) 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