From fca177c3841cc1d834718b2758eb1526598e852e Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Wed, 15 Apr 2026 00:00:00 +0100 Subject: [PATCH] Convert ES modules to plain scripts using window.NexusTerminal namespace Replace ES module import/export with a global namespace pattern to avoid silent failures in the Wails v2 WKWebView which does not reliably support + + + + + + + + + + + + + diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js index 8623263..f4a587d 100644 --- a/frontend/src/js/app.js +++ b/frontend/src/js/app.js @@ -3,17 +3,7 @@ * Initialises the application shell, loads profiles, and sets up event listeners. */ -import { State } from './state.js'; -import { Sidebar } from './sidebar.js'; -import { TabManager } from './tabs.js'; -import { TerminalManager } from './terminal.js'; -import { ProfileEditor } from './profiles.js'; -import { RawViewer } from './raw.js'; -import { Notifications } from './notifications.js'; -import { Settings } from './settings.js'; -import { CommandPalette } from './commands.js'; -import { SecretPrompt } from './secrets.js'; -import { stateLabel } from './utils.js'; +const { State, Sidebar, TabManager, TerminalManager, ProfileEditor, RawViewer, Notifications, Settings, CommandPalette, SecretPrompt, stateLabel } = window.NexusTerminal; class App { constructor() { @@ -31,18 +21,51 @@ class App { /** * Bootstraps the application after the DOM is ready. + * UI event binding happens synchronously so buttons always work. + * Backend data loading waits for the Wails runtime to be available. */ async init() { + // Phase 1 — bind all UI event handlers immediately (DOM-only, no backend). + this.setupUI(); + this.setupKeyboard(); + this.updateStatusBar(); + + // Phase 2 — wait for the Wails runtime, then load backend data and bind events. + await this._waitForWailsRuntime(); + this.setupEvents(); + await this.loadProfiles(); await this.loadPreferences(); await this.checkSecretStore(); - - this.setupEvents(); - this.setupKeyboard(); - this.setupUI(); this.updateStatusBar(); } + /** + * Polls for `window.go` and `window.runtime` to become available. + * Wails injects these into the webview; on fast machines they are + * present before DOMContentLoaded, but on slower starts there may + * be a brief delay. + * @returns {Promise} + * @private + */ + _waitForWailsRuntime() { + return new Promise((resolve) => { + const maxWait = 5000; + const start = Date.now(); + const check = () => { + if (window.go && window.runtime) { + resolve(); + } else if (Date.now() - start < maxWait) { + setTimeout(check, 50); + } else { + console.warn('Wails runtime not detected after ' + maxWait + 'ms — proceeding anyway'); + resolve(); + } + }; + check(); + }); + } + // ── Data loading ───────────────────────────────────────────────── /** @@ -94,8 +117,13 @@ class App { /** * Registers listeners for backend events emitted via the Wails runtime. + * Guarded: silently skips if the runtime is unavailable. */ setupEvents() { + if (!window.runtime || typeof window.runtime.EventsOn !== 'function') { + console.warn('window.runtime not available — Wails event listeners not bound'); + return; + } // Incoming session data (terminal output) window.runtime.EventsOn('session:data', (payload) => { this.terminals.handleData(payload.sessionID, payload.data); diff --git a/frontend/src/js/commands.js b/frontend/src/js/commands.js index 677401d..8cea917 100644 --- a/frontend/src/js/commands.js +++ b/frontend/src/js/commands.js @@ -4,9 +4,9 @@ * profile connections, and utility actions. */ -import { createElement, escapeHtml, fuzzyMatch, $ } from './utils.js'; +const { createElement, escapeHtml, fuzzyMatch, $ } = window.NexusTerminal; -export class CommandPalette { +class CommandPalette { /** * @param {import('./app.js').App} app */ @@ -379,3 +379,6 @@ export class CommandPalette { } } } + +window.NexusTerminal = window.NexusTerminal || {}; +window.NexusTerminal.CommandPalette = CommandPalette; diff --git a/frontend/src/js/notifications.js b/frontend/src/js/notifications.js index a507ad5..044b719 100644 --- a/frontend/src/js/notifications.js +++ b/frontend/src/js/notifications.js @@ -4,7 +4,7 @@ * in the top-right corner of the viewport. */ -import { createElement, escapeHtml } from './utils.js'; +const { createElement, escapeHtml } = window.NexusTerminal; /** Map of notification type to SVG icon path data. */ const ICONS = { @@ -14,7 +14,7 @@ const ICONS = { info: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z', }; -export class Notifications { +class Notifications { constructor() { /** @type {HTMLElement} */ this.container = document.getElementById('notifications'); @@ -122,3 +122,6 @@ export class Notifications { }, 500); } } + +window.NexusTerminal = window.NexusTerminal || {}; +window.NexusTerminal.Notifications = Notifications; diff --git a/frontend/src/js/profiles.js b/frontend/src/js/profiles.js index 9359668..b44f544 100644 --- a/frontend/src/js/profiles.js +++ b/frontend/src/js/profiles.js @@ -4,7 +4,7 @@ * Dynamically switches form sections based on the selected protocol. */ -import { createElement, escapeHtml, generateId, $ } from './utils.js'; +const { createElement, escapeHtml, generateId, $ } = window.NexusTerminal; /** * Default values mirroring the Go backend defaults. @@ -53,7 +53,7 @@ const DEFAULTS = { }, }; -export class ProfileEditor { +class ProfileEditor { /** * @param {import('./app.js').App} app */ @@ -886,3 +886,6 @@ export class ProfileEditor { return Object.entries(envVars).map(([k, v]) => `${k}=${v}`).join('\n'); } } + +window.NexusTerminal = window.NexusTerminal || {}; +window.NexusTerminal.ProfileEditor = ProfileEditor; diff --git a/frontend/src/js/raw.js b/frontend/src/js/raw.js index 405d7de..aeafab0 100644 --- a/frontend/src/js/raw.js +++ b/frontend/src/js/raw.js @@ -4,7 +4,7 @@ * along with a send bar, pause/resume, and capture saving. */ -import { createElement, escapeHtml, formatTimestamp, formatBytes } from './utils.js'; +const { createElement, escapeHtml, formatTimestamp, formatBytes } = window.NexusTerminal; /** * Maximum number of data entries retained in the scrollback buffer. @@ -12,7 +12,7 @@ import { createElement, escapeHtml, formatTimestamp, formatBytes } from './utils */ const MAX_BUFFER_ENTRIES = 5000; -export class RawViewer { +class RawViewer { /** * @param {import('./app.js').App} app */ @@ -562,3 +562,6 @@ export class RawViewer { }); } } + +window.NexusTerminal = window.NexusTerminal || {}; +window.NexusTerminal.RawViewer = RawViewer; diff --git a/frontend/src/js/secrets.js b/frontend/src/js/secrets.js index c7e5768..4fb1e23 100644 --- a/frontend/src/js/secrets.js +++ b/frontend/src/js/secrets.js @@ -4,9 +4,9 @@ * and SSH host-key verification confirmations. */ -import { createElement, escapeHtml } from './utils.js'; +const { createElement, escapeHtml } = window.NexusTerminal; -export class SecretPrompt { +class SecretPrompt { constructor(app) { /** @type {import('./app.js').App} */ this.app = app; @@ -298,3 +298,6 @@ export class SecretPrompt { } } } + +window.NexusTerminal = window.NexusTerminal || {}; +window.NexusTerminal.SecretPrompt = SecretPrompt; diff --git a/frontend/src/js/settings.js b/frontend/src/js/settings.js index 3e9aba2..87ce898 100644 --- a/frontend/src/js/settings.js +++ b/frontend/src/js/settings.js @@ -4,7 +4,7 @@ * terminal defaults, security, known-hosts management, and about info. */ -import { createElement, escapeHtml, formatTimestamp, $ } from './utils.js'; +const { createElement, escapeHtml, formatTimestamp, $ } = window.NexusTerminal; /** Settings tab definitions. */ const TABS = [ @@ -15,7 +15,7 @@ const TABS = [ { key: 'about', label: 'About' }, ]; -export class Settings { +class Settings { /** * @param {import('./app.js').App} app */ @@ -461,3 +461,6 @@ export class Settings { } } } + +window.NexusTerminal = window.NexusTerminal || {}; +window.NexusTerminal.Settings = Settings; diff --git a/frontend/src/js/sidebar.js b/frontend/src/js/sidebar.js index 65fb3d9..24aa6a2 100644 --- a/frontend/src/js/sidebar.js +++ b/frontend/src/js/sidebar.js @@ -4,12 +4,12 @@ * and context menus. */ -import { createElement, escapeHtml, fuzzyMatch, protocolColour, dismissContextMenus, truncate } from './utils.js'; +const { createElement, escapeHtml, fuzzyMatch, protocolColour, dismissContextMenus, truncate } = window.NexusTerminal; /** Maximum number of profiles shown in the "Recent" section. */ const RECENT_LIMIT = 5; -export class Sidebar { +class Sidebar { /** * @param {import('./app.js').App} app */ @@ -333,3 +333,6 @@ export class Sidebar { .map((r) => r.profile); } } + +window.NexusTerminal = window.NexusTerminal || {}; +window.NexusTerminal.Sidebar = Sidebar; diff --git a/frontend/src/js/state.js b/frontend/src/js/state.js index fe8933f..da4fbdf 100644 --- a/frontend/src/js/state.js +++ b/frontend/src/js/state.js @@ -4,7 +4,7 @@ * emitting change events so the UI stays in sync. */ -export class State { +class State { constructor() { /** @type {Array} */ this.profiles = []; @@ -223,3 +223,6 @@ export class State { this.emit('secretStore:init', initialised); } } + +window.NexusTerminal = window.NexusTerminal || {}; +window.NexusTerminal.State = State; diff --git a/frontend/src/js/tabs.js b/frontend/src/js/tabs.js index a58d051..c03e168 100644 --- a/frontend/src/js/tabs.js +++ b/frontend/src/js/tabs.js @@ -4,16 +4,19 @@ * pinning tabs, and context menus. */ -import { createElement, escapeHtml, protocolColour, stateLabel, dismissContextMenus } from './utils.js'; +const { createElement, escapeHtml, protocolColour, stateLabel, dismissContextMenus } = window.NexusTerminal; -export class TabManager { +class TabManager { /** * @param {import('./app.js').App} app */ constructor(app) { this.app = app; - /** @type {HTMLElement} The tab bar container. */ + /** @type {HTMLElement} The inner tab list where tab elements are inserted. */ + this.tabList = document.getElementById('tab-list'); + + /** @type {HTMLElement} The outer tab bar wrapper (used for drag-and-drop). */ this.tabBar = document.getElementById('tab-bar'); /** @type {HTMLElement} The container for terminal/viewer panels. */ @@ -56,7 +59,7 @@ export class TabManager { if (idx !== -1) this.order.splice(idx, 1); // Remove tab element - const tabEl = this.tabBar.querySelector(`[data-session-id="${sessionId}"]`); + const tabEl = this.tabList.querySelector(`[data-session-id="${sessionId}"]`); if (tabEl) tabEl.remove(); // Remove panel @@ -86,7 +89,7 @@ export class TabManager { this.app.state.setActiveSession(sessionId); // Highlight active tab - this.tabBar.querySelectorAll('.tab').forEach((t) => { + this.tabList.querySelectorAll('.tab').forEach((t) => { t.classList.toggle('tab-active', t.dataset.sessionId === sessionId); }); @@ -146,7 +149,7 @@ export class TabManager { */ renameTab(sessionId, name) { this.app.state.updateSession(sessionId, { tabName: name }); - const tabEl = this.tabBar.querySelector(`[data-session-id="${sessionId}"]`); + const tabEl = this.tabList.querySelector(`[data-session-id="${sessionId}"]`); if (tabEl) { tabEl.querySelector('.tab-name').textContent = name; } @@ -216,7 +219,7 @@ export class TabManager { updateSessionState(sessionId, state, error) { this.app.state.updateSession(sessionId, { state, error: error || '' }); - const tabEl = this.tabBar.querySelector(`[data-session-id="${sessionId}"]`); + const tabEl = this.tabList.querySelector(`[data-session-id="${sessionId}"]`); if (!tabEl) return; const dot = tabEl.querySelector('.tab-status'); @@ -383,7 +386,7 @@ export class TabManager { this.showContextMenu(sessionInfo.id, e.clientX, e.clientY); }); - this.tabBar.appendChild(tab); + this.tabList.appendChild(tab); } // ── Internal helpers ───────────────────────────────────────────── @@ -405,8 +408,8 @@ export class TabManager { }); for (const id of sorted) { - const el = this.tabBar.querySelector(`[data-session-id="${id}"]`); - if (el) this.tabBar.appendChild(el); + const el = this.tabList.querySelector(`[data-session-id="${id}"]`); + if (el) this.tabList.appendChild(el); } } @@ -417,7 +420,7 @@ export class TabManager { */ _updateTabStyles(sessionId) { const session = this.app.state.getSession(sessionId); - const tabEl = this.tabBar.querySelector(`[data-session-id="${sessionId}"]`); + const tabEl = this.tabList.querySelector(`[data-session-id="${sessionId}"]`); if (tabEl && session) { tabEl.classList.toggle('tab-pinned', !!session.pinned); } @@ -484,11 +487,11 @@ export class TabManager { * @private */ _setupDragAndDrop() { - if (!this.tabBar) return; + if (!this.tabList) return; let draggedId = null; - this.tabBar.addEventListener('dragstart', (e) => { + this.tabList.addEventListener('dragstart', (e) => { const tab = e.target.closest('.tab'); if (!tab) return; draggedId = tab.dataset.sessionId; @@ -496,7 +499,7 @@ export class TabManager { e.dataTransfer.effectAllowed = 'move'; }); - this.tabBar.addEventListener('dragover', (e) => { + this.tabList.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; @@ -509,14 +512,14 @@ export class TabManager { target.classList.toggle('tab-drop-after', e.clientX >= midX); }); - this.tabBar.addEventListener('dragleave', (e) => { + this.tabList.addEventListener('dragleave', (e) => { const target = e.target.closest('.tab'); if (target) { target.classList.remove('tab-drop-before', 'tab-drop-after'); } }); - this.tabBar.addEventListener('drop', (e) => { + this.tabList.addEventListener('drop', (e) => { e.preventDefault(); const target = e.target.closest('.tab'); if (!target || !draggedId) return; @@ -536,11 +539,14 @@ export class TabManager { this._reorderDOM(); }); - this.tabBar.addEventListener('dragend', () => { - this.tabBar.querySelectorAll('.tab').forEach((t) => { + this.tabList.addEventListener('dragend', () => { + this.tabList.querySelectorAll('.tab').forEach((t) => { t.classList.remove('tab-dragging', 'tab-drop-before', 'tab-drop-after'); }); draggedId = null; }); } } + +window.NexusTerminal = window.NexusTerminal || {}; +window.NexusTerminal.TabManager = TabManager; diff --git a/frontend/src/js/terminal.js b/frontend/src/js/terminal.js index c166509..d69b27f 100644 --- a/frontend/src/js/terminal.js +++ b/frontend/src/js/terminal.js @@ -6,7 +6,7 @@ /* global Terminal, FitAddon, SearchAddon, WebLinksAddon */ -export class TerminalManager { +class TerminalManager { /** * @param {import('./app.js').App} app */ @@ -284,3 +284,6 @@ export class TerminalManager { }; } } + +window.NexusTerminal = window.NexusTerminal || {}; +window.NexusTerminal.TerminalManager = TerminalManager; diff --git a/frontend/src/js/utils.js b/frontend/src/js/utils.js index 7e7d174..d180947 100644 --- a/frontend/src/js/utils.js +++ b/frontend/src/js/utils.js @@ -8,7 +8,7 @@ * @param {string} selector - CSS selector. * @returns {Element|null} */ -export function $(selector) { +function $(selector) { return document.querySelector(selector); } @@ -17,7 +17,7 @@ export function $(selector) { * @param {string} selector - CSS selector. * @returns {NodeListOf} */ -export function $$(selector) { +function $$(selector) { return document.querySelectorAll(selector); } @@ -28,7 +28,7 @@ export function $$(selector) { * @param {Object} [attrs] - Attribute key/value pairs. * @returns {HTMLElement} */ -export function createElement(tag, className, attrs = {}) { +function createElement(tag, className, attrs = {}) { const el = document.createElement(tag); if (className) { el.className = className; @@ -56,7 +56,7 @@ export function createElement(tag, className, attrs = {}) { * @param {string} str - Raw string. * @returns {string} Escaped string safe for innerHTML. */ -export function escapeHtml(str) { +function escapeHtml(str) { if (!str) return ''; const map = { '&': '&', @@ -73,7 +73,7 @@ export function escapeHtml(str) { * @param {Date|string} date * @returns {string} e.g. "14:32:07" or "2 Jan 14:32". */ -export function formatTimestamp(date) { +function formatTimestamp(date) { if (!date) return ''; const d = date instanceof Date ? date : new Date(date); if (isNaN(d.getTime())) return ''; @@ -98,7 +98,7 @@ export function formatTimestamp(date) { * @param {number} ms - Delay in milliseconds. * @returns {Function} */ -export function debounce(fn, ms) { +function debounce(fn, ms) { let timer = null; return function (...args) { clearTimeout(timer); @@ -113,7 +113,7 @@ export function debounce(fn, ms) { * @param {string} query * @returns {{ match: boolean, score: number }} */ -export function fuzzyMatch(text, query) { +function fuzzyMatch(text, query) { if (!query) return { match: true, score: 1 }; const lowerText = text.toLowerCase(); const lowerQuery = query.toLowerCase(); @@ -146,7 +146,7 @@ export function fuzzyMatch(text, query) { * @param {string} str * @returns {string} */ -export function base64Encode(str) { +function base64Encode(str) { return btoa(str); } @@ -155,7 +155,7 @@ export function base64Encode(str) { * @param {string} str * @returns {string} */ -export function base64Decode(str) { +function base64Decode(str) { return atob(str); } @@ -163,7 +163,7 @@ export function base64Decode(str) { * Generates a new UUID v4 using the browser crypto API. * @returns {string} */ -export function generateId() { +function generateId() { return crypto.randomUUID(); } @@ -172,7 +172,7 @@ export function generateId() { * @param {number} bytes * @returns {string} e.g. "1.23 KB" */ -export function formatBytes(bytes) { +function formatBytes(bytes) { if (bytes === 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; const k = 1024; @@ -187,7 +187,7 @@ export function formatBytes(bytes) { * @param {number} len * @returns {string} */ -export function truncate(str, len) { +function truncate(str, len) { if (!str || str.length <= len) return str || ''; return str.slice(0, len) + '…'; } @@ -195,7 +195,7 @@ export function truncate(str, len) { /** * Removes any open context menus from the DOM. */ -export function dismissContextMenus() { +function dismissContextMenus() { document.querySelectorAll('.context-menu').forEach((m) => m.remove()); } @@ -204,7 +204,7 @@ export function dismissContextMenus() { * @param {string} protocol - 'ssh', 'telnet', or 'raw'. * @returns {string} */ -export function protocolColour(protocol) { +function protocolColour(protocol) { switch (protocol) { case 'ssh': return 'protocol-ssh'; case 'telnet': return 'protocol-telnet'; @@ -218,7 +218,7 @@ export function protocolColour(protocol) { * @param {string} state * @returns {string} */ -export function stateLabel(state) { +function stateLabel(state) { switch (state) { case 'connecting': return 'Connecting'; case 'connected': return 'Connected'; @@ -227,3 +227,6 @@ export function stateLabel(state) { 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 });