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
<script type="module"> tags.

- Remove all import/export statements from JS files
- Register each module's exports on window.NexusTerminal
- Destructure dependencies from window.NexusTerminal at file top
- Load all scripts in dependency order in index.html as plain <script> tags
This commit is contained in:
2026-04-15 00:00:00 +01:00
parent 909657a4b1
commit fca177c384
13 changed files with 145 additions and 101 deletions
+17 -37
View File
@@ -51,40 +51,8 @@
</div>
<nav class="sidebar-nav" id="sidebar-nav">
<!-- Favourites -->
<div class="sidebar-section" id="favourites-section">
<button class="sidebar-section-header" data-section="favourites">
<span class="section-icon">★</span>
<span>Favourites</span>
<span class="section-count" id="favourites-count">0</span>
</button>
<div class="sidebar-list" id="favourites-list">
<div class="sidebar-list-empty">No favourites yet</div>
</div>
</div>
<!-- Recent -->
<div class="sidebar-section" id="recent-section">
<button class="sidebar-section-header" data-section="recent">
<span class="section-icon">⟳</span>
<span>Recent</span>
</button>
<div class="sidebar-list" id="recent-list">
<div class="sidebar-list-empty">No recent sessions</div>
</div>
</div>
<!-- All Connections -->
<div class="sidebar-section" id="all-section">
<button class="sidebar-section-header" data-section="all">
<span class="section-icon">⊞</span>
<span>All Connections</span>
<span class="section-count" id="all-count">0</span>
</button>
<div class="sidebar-list" id="all-list">
<div class="sidebar-list-empty">No saved connections</div>
</div>
</div>
<!-- Dynamically managed by sidebar.js -->
<div id="sidebar-profiles" class="sidebar-profiles"></div>
</nav>
<div class="sidebar-footer">
@@ -162,7 +130,8 @@
</div>
</div>
<!-- Session containers are injected here dynamically -->
<!-- Session panels are injected here dynamically by tabs.js -->
<div id="panel-container" class="panel-container"></div>
</main>
<!-- ============================================================
@@ -192,7 +161,18 @@
<script src="/src/js/lib/xterm-addon-search.min.js"></script>
<script src="/src/js/lib/xterm-addon-web-links.min.js"></script>
<!-- Application entry point -->
<script type="module" src="/src/js/app.js"></script>
<!-- Application scripts (loaded in dependency order) -->
<script src="/src/js/utils.js"></script>
<script src="/src/js/state.js"></script>
<script src="/src/js/notifications.js"></script>
<script src="/src/js/sidebar.js"></script>
<script src="/src/js/tabs.js"></script>
<script src="/src/js/terminal.js"></script>
<script src="/src/js/profiles.js"></script>
<script src="/src/js/raw.js"></script>
<script src="/src/js/settings.js"></script>
<script src="/src/js/commands.js"></script>
<script src="/src/js/secrets.js"></script>
<script src="/src/js/app.js"></script>
</body>
</html>
+43 -15
View File
@@ -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<void>}
* @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);
+5 -2
View File
@@ -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;
+5 -2
View File
@@ -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;
+5 -2
View File
@@ -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;
+5 -2
View File
@@ -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;
+5 -2
View File
@@ -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;
+5 -2
View File
@@ -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;
+5 -2
View File
@@ -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;
+4 -1
View File
@@ -4,7 +4,7 @@
* emitting change events so the UI stays in sync.
*/
export class State {
class State {
constructor() {
/** @type {Array<Object>} */
this.profiles = [];
@@ -223,3 +223,6 @@ export class State {
this.emit('secretStore:init', initialised);
}
}
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.State = State;
+24 -18
View File
@@ -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;
+4 -1
View File
@@ -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;
+18 -15
View File
@@ -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<Element>}
*/
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 = {
'&': '&amp;',
@@ -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 });