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);
}
}
/* -----------------------------------------------------------------------
+12 -9
View File
@@ -76,10 +76,11 @@
/* -----------------------------------------------------------------------
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;
@@ -109,10 +110,10 @@
/* -----------------------------------------------------------------------
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
@@ -146,7 +147,9 @@
Global input overrides — disable macOS/WebKit auto-capitalisation,
autocorrect, and spellcheck on all text fields.
----------------------------------------------------------------------- */
input, textarea, select {
input,
textarea,
select {
autocapitalize: none;
autocorrect: off;
spellcheck: false;
+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.");
+42 -13
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,11 +219,16 @@
list.style.display = "";
});
form.querySelector(".sshkey-create-btn").addEventListener("click", async () => {
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 bits = parseInt(
form.querySelector(".sshkey-bits-select").value,
10,
);
const passphrase = form.querySelector(".sshkey-pass-input").value;
if (!name) {
@@ -221,8 +237,15 @@
}
try {
await window.go.api.Service.GenerateSSHKey(name, keyType, bits, passphrase);
this.app.notifications.success(`Key "${name}" generated successfully`);
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();
@@ -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);
});
+275 -276
View File
@@ -1,348 +1,347 @@
(function() {
/**
(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 = {
const THEMES = {
charcoal: {
label: 'Charcoal (Default)',
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',
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',
"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',
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',
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',
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',
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',
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',
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',
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',
"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',
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',
"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',
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',
"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',
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',
"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',
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',
"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',
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',
"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',
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'];
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() {
function listTerminalThemes() {
return Object.entries(THEMES).map(([value, entry]) => ({
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;
})();
+33 -12
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
.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;
+152 -109
View File
@@ -1,120 +1,132 @@
(function() {
/**
(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) {
function $(selector) {
return document.querySelector(selector);
}
}
/**
/**
* Shorthand for document.querySelectorAll.
* @param {string} selector - CSS selector.
* @returns {NodeListOf<Element>}
*/
function $$(selector) {
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 = {}) {
function createElement(tag, className, attrs = {}) {
const el = document.createElement(tag);
if (className) {
el.className = className;
}
for (const [key, value] of Object.entries(attrs)) {
if (key === 'textContent') {
if (key === "textContent") {
el.textContent = value;
} else if (key === 'innerHTML') {
} else if (key === "innerHTML") {
el.innerHTML = value;
} else if (key === 'dataset') {
} else if (key === "dataset") {
for (const [dKey, dVal] of Object.entries(value)) {
el.dataset[dKey] = dVal;
}
} else if (key.startsWith('on') && typeof value === 'function') {
} 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 '';
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 '';
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}`;
}
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) {
function debounce(fn, ms) {
let timer = null;
return function (...args) {
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) {
function fuzzyMatch(text, query) {
if (!query) return { match: true, score: 1 };
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
@@ -140,153 +152,184 @@ function fuzzyMatch(text, query) {
}
return { match: true, score };
}
}
/**
/**
* Base64-encodes a string.
* @param {string} str
* @returns {string}
*/
function base64Encode(str) {
function base64Encode(str) {
return btoa(str);
}
}
/**
/**
* Base64-decodes a string.
* @param {string} str
* @returns {string}
*/
function base64Decode(str) {
function base64Decode(str) {
return atob(str);
}
}
/**
/**
* Generates a new UUID v4 using the browser crypto API.
* @returns {string}
*/
function generateId() {
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'];
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) + '…';
}
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());
}
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) {
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 '';
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) {
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') {
if (node.tagName === "INPUT" || node.tagName === "TEXTAREA") {
disableAutoCapitalise(node);
}
if (node.querySelectorAll) {
node.querySelectorAll('input, textarea').forEach(disableAutoCapitalise);
node
.querySelectorAll("input, textarea")
.forEach(disableAutoCapitalise);
}
}
}
});
inputObserver.observe(document.body, { childList: true, subtree: true });
});
inputObserver.observe(document.body, { childList: true, subtree: true });
})();
+12 -4
View File
@@ -91,16 +91,24 @@
});
ctrlAltDel.addEventListener("click", () => this._sendCtrlAltDel());
const clipboardBtn = createElement("button", "btn btn-sm btn-vnc-action", {
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", {
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'));