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