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 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);
+13 -4
View File
@@ -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);
}
}
/* -----------------------------------------------------------------------
+102 -99
View File
@@ -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;
}
+3 -1
View File
@@ -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 {
+6 -2
View File
@@ -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());
+9 -5
View File
@@ -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.");
+58 -29
View File
@@ -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 = '<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;
}
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);
});
+314 -315
View File
@@ -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;
})();
+37 -16
View File
@@ -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 = '<div class="trigger-empty">No trigger rules defined</div>';
list.innerHTML =
'<div class="trigger-empty">No trigger rules defined</div>';
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 = `
<div class="form-group">
<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 class="form-group">
<label class="form-label">Pattern</label>
@@ -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;
+255 -212
View File
@@ -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<Element>}
*/
function $$(selector) {
/**
* Shorthand for document.querySelectorAll.
* @param {string} selector - CSS selector.
* @returns {NodeListOf<Element>}
*/
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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;',
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;",
};
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 });
})();
+16 -8
View File
@@ -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", {
@@ -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'));