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> </div>
<nav class="sidebar-nav" id="sidebar-nav"> <nav class="sidebar-nav" id="sidebar-nav">
<!-- Favourites --> <!-- Dynamically managed by sidebar.js -->
<div class="sidebar-section" id="favourites-section"> <div id="sidebar-profiles" class="sidebar-profiles"></div>
<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>
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">
@@ -162,7 +130,8 @@
</div> </div>
</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> </main>
<!-- ============================================================ <!-- ============================================================
@@ -192,7 +161,18 @@
<script src="/src/js/lib/xterm-addon-search.min.js"></script> <script src="/src/js/lib/xterm-addon-search.min.js"></script>
<script src="/src/js/lib/xterm-addon-web-links.min.js"></script> <script src="/src/js/lib/xterm-addon-web-links.min.js"></script>
<!-- Application entry point --> <!-- Application scripts (loaded in dependency order) -->
<script type="module" src="/src/js/app.js"></script> <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> </body>
</html> </html>
+43 -15
View File
@@ -3,17 +3,7 @@
* Initialises the application shell, loads profiles, and sets up event listeners. * Initialises the application shell, loads profiles, and sets up event listeners.
*/ */
import { State } from './state.js'; const { State, Sidebar, TabManager, TerminalManager, ProfileEditor, RawViewer, Notifications, Settings, CommandPalette, SecretPrompt, stateLabel } = window.NexusTerminal;
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';
class App { class App {
constructor() { constructor() {
@@ -31,18 +21,51 @@ class App {
/** /**
* Bootstraps the application after the DOM is ready. * 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() { 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.loadProfiles();
await this.loadPreferences(); await this.loadPreferences();
await this.checkSecretStore(); await this.checkSecretStore();
this.setupEvents();
this.setupKeyboard();
this.setupUI();
this.updateStatusBar(); 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 ───────────────────────────────────────────────── // ── Data loading ─────────────────────────────────────────────────
/** /**
@@ -94,8 +117,13 @@ class App {
/** /**
* Registers listeners for backend events emitted via the Wails runtime. * Registers listeners for backend events emitted via the Wails runtime.
* Guarded: silently skips if the runtime is unavailable.
*/ */
setupEvents() { 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) // Incoming session data (terminal output)
window.runtime.EventsOn('session:data', (payload) => { window.runtime.EventsOn('session:data', (payload) => {
this.terminals.handleData(payload.sessionID, payload.data); this.terminals.handleData(payload.sessionID, payload.data);
+5 -2
View File
@@ -4,9 +4,9 @@
* profile connections, and utility actions. * 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 * @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. * 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. */ /** Map of notification type to SVG icon path data. */
const ICONS = { 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', 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() { constructor() {
/** @type {HTMLElement} */ /** @type {HTMLElement} */
this.container = document.getElementById('notifications'); this.container = document.getElementById('notifications');
@@ -122,3 +122,6 @@ export class Notifications {
}, 500); }, 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. * 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. * Default values mirroring the Go backend defaults.
@@ -53,7 +53,7 @@ const DEFAULTS = {
}, },
}; };
export class ProfileEditor { class ProfileEditor {
/** /**
* @param {import('./app.js').App} app * @param {import('./app.js').App} app
*/ */
@@ -886,3 +886,6 @@ export class ProfileEditor {
return Object.entries(envVars).map(([k, v]) => `${k}=${v}`).join('\n'); 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. * 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. * 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; const MAX_BUFFER_ENTRIES = 5000;
export class RawViewer { class RawViewer {
/** /**
* @param {import('./app.js').App} app * @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. * and SSH host-key verification confirmations.
*/ */
import { createElement, escapeHtml } from './utils.js'; const { createElement, escapeHtml } = window.NexusTerminal;
export class SecretPrompt { class SecretPrompt {
constructor(app) { constructor(app) {
/** @type {import('./app.js').App} */ /** @type {import('./app.js').App} */
this.app = 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. * 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. */ /** Settings tab definitions. */
const TABS = [ const TABS = [
@@ -15,7 +15,7 @@ const TABS = [
{ key: 'about', label: 'About' }, { key: 'about', label: 'About' },
]; ];
export class Settings { class Settings {
/** /**
* @param {import('./app.js').App} app * @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. * 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. */ /** Maximum number of profiles shown in the "Recent" section. */
const RECENT_LIMIT = 5; const RECENT_LIMIT = 5;
export class Sidebar { class Sidebar {
/** /**
* @param {import('./app.js').App} app * @param {import('./app.js').App} app
*/ */
@@ -333,3 +333,6 @@ export class Sidebar {
.map((r) => r.profile); .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. * emitting change events so the UI stays in sync.
*/ */
export class State { class State {
constructor() { constructor() {
/** @type {Array<Object>} */ /** @type {Array<Object>} */
this.profiles = []; this.profiles = [];
@@ -223,3 +223,6 @@ export class State {
this.emit('secretStore:init', initialised); 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. * 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 * @param {import('./app.js').App} app
*/ */
constructor(app) { constructor(app) {
this.app = 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'); this.tabBar = document.getElementById('tab-bar');
/** @type {HTMLElement} The container for terminal/viewer panels. */ /** @type {HTMLElement} The container for terminal/viewer panels. */
@@ -56,7 +59,7 @@ export class TabManager {
if (idx !== -1) this.order.splice(idx, 1); if (idx !== -1) this.order.splice(idx, 1);
// Remove tab element // 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(); if (tabEl) tabEl.remove();
// Remove panel // Remove panel
@@ -86,7 +89,7 @@ export class TabManager {
this.app.state.setActiveSession(sessionId); this.app.state.setActiveSession(sessionId);
// Highlight active tab // Highlight active tab
this.tabBar.querySelectorAll('.tab').forEach((t) => { this.tabList.querySelectorAll('.tab').forEach((t) => {
t.classList.toggle('tab-active', t.dataset.sessionId === sessionId); t.classList.toggle('tab-active', t.dataset.sessionId === sessionId);
}); });
@@ -146,7 +149,7 @@ export class TabManager {
*/ */
renameTab(sessionId, name) { renameTab(sessionId, name) {
this.app.state.updateSession(sessionId, { tabName: 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) { if (tabEl) {
tabEl.querySelector('.tab-name').textContent = name; tabEl.querySelector('.tab-name').textContent = name;
} }
@@ -216,7 +219,7 @@ export class TabManager {
updateSessionState(sessionId, state, error) { updateSessionState(sessionId, state, error) {
this.app.state.updateSession(sessionId, { state, error: 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; if (!tabEl) return;
const dot = tabEl.querySelector('.tab-status'); const dot = tabEl.querySelector('.tab-status');
@@ -383,7 +386,7 @@ export class TabManager {
this.showContextMenu(sessionInfo.id, e.clientX, e.clientY); this.showContextMenu(sessionInfo.id, e.clientX, e.clientY);
}); });
this.tabBar.appendChild(tab); this.tabList.appendChild(tab);
} }
// ── Internal helpers ───────────────────────────────────────────── // ── Internal helpers ─────────────────────────────────────────────
@@ -405,8 +408,8 @@ export class TabManager {
}); });
for (const id of sorted) { for (const id of sorted) {
const el = this.tabBar.querySelector(`[data-session-id="${id}"]`); const el = this.tabList.querySelector(`[data-session-id="${id}"]`);
if (el) this.tabBar.appendChild(el); if (el) this.tabList.appendChild(el);
} }
} }
@@ -417,7 +420,7 @@ export class TabManager {
*/ */
_updateTabStyles(sessionId) { _updateTabStyles(sessionId) {
const session = this.app.state.getSession(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) { if (tabEl && session) {
tabEl.classList.toggle('tab-pinned', !!session.pinned); tabEl.classList.toggle('tab-pinned', !!session.pinned);
} }
@@ -484,11 +487,11 @@ export class TabManager {
* @private * @private
*/ */
_setupDragAndDrop() { _setupDragAndDrop() {
if (!this.tabBar) return; if (!this.tabList) return;
let draggedId = null; let draggedId = null;
this.tabBar.addEventListener('dragstart', (e) => { this.tabList.addEventListener('dragstart', (e) => {
const tab = e.target.closest('.tab'); const tab = e.target.closest('.tab');
if (!tab) return; if (!tab) return;
draggedId = tab.dataset.sessionId; draggedId = tab.dataset.sessionId;
@@ -496,7 +499,7 @@ export class TabManager {
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
}); });
this.tabBar.addEventListener('dragover', (e) => { this.tabList.addEventListener('dragover', (e) => {
e.preventDefault(); e.preventDefault();
e.dataTransfer.dropEffect = 'move'; e.dataTransfer.dropEffect = 'move';
@@ -509,14 +512,14 @@ export class TabManager {
target.classList.toggle('tab-drop-after', e.clientX >= midX); 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'); const target = e.target.closest('.tab');
if (target) { if (target) {
target.classList.remove('tab-drop-before', 'tab-drop-after'); target.classList.remove('tab-drop-before', 'tab-drop-after');
} }
}); });
this.tabBar.addEventListener('drop', (e) => { this.tabList.addEventListener('drop', (e) => {
e.preventDefault(); e.preventDefault();
const target = e.target.closest('.tab'); const target = e.target.closest('.tab');
if (!target || !draggedId) return; if (!target || !draggedId) return;
@@ -536,11 +539,14 @@ export class TabManager {
this._reorderDOM(); this._reorderDOM();
}); });
this.tabBar.addEventListener('dragend', () => { this.tabList.addEventListener('dragend', () => {
this.tabBar.querySelectorAll('.tab').forEach((t) => { this.tabList.querySelectorAll('.tab').forEach((t) => {
t.classList.remove('tab-dragging', 'tab-drop-before', 'tab-drop-after'); t.classList.remove('tab-dragging', 'tab-drop-before', 'tab-drop-after');
}); });
draggedId = null; draggedId = null;
}); });
} }
} }
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.TabManager = TabManager;
+4 -1
View File
@@ -6,7 +6,7 @@
/* global Terminal, FitAddon, SearchAddon, WebLinksAddon */ /* global Terminal, FitAddon, SearchAddon, WebLinksAddon */
export class TerminalManager { class TerminalManager {
/** /**
* @param {import('./app.js').App} app * @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. * @param {string} selector - CSS selector.
* @returns {Element|null} * @returns {Element|null}
*/ */
export function $(selector) { function $(selector) {
return document.querySelector(selector); return document.querySelector(selector);
} }
@@ -17,7 +17,7 @@ export function $(selector) {
* @param {string} selector - CSS selector. * @param {string} selector - CSS selector.
* @returns {NodeListOf<Element>} * @returns {NodeListOf<Element>}
*/ */
export function $$(selector) { function $$(selector) {
return document.querySelectorAll(selector); return document.querySelectorAll(selector);
} }
@@ -28,7 +28,7 @@ export function $$(selector) {
* @param {Object} [attrs] - Attribute key/value pairs. * @param {Object} [attrs] - Attribute key/value pairs.
* @returns {HTMLElement} * @returns {HTMLElement}
*/ */
export function createElement(tag, className, attrs = {}) { function createElement(tag, className, attrs = {}) {
const el = document.createElement(tag); const el = document.createElement(tag);
if (className) { if (className) {
el.className = className; el.className = className;
@@ -56,7 +56,7 @@ export function createElement(tag, className, attrs = {}) {
* @param {string} str - Raw string. * @param {string} str - Raw string.
* @returns {string} Escaped string safe for innerHTML. * @returns {string} Escaped string safe for innerHTML.
*/ */
export function escapeHtml(str) { function escapeHtml(str) {
if (!str) return ''; if (!str) return '';
const map = { const map = {
'&': '&amp;', '&': '&amp;',
@@ -73,7 +73,7 @@ export function escapeHtml(str) {
* @param {Date|string} date * @param {Date|string} date
* @returns {string} e.g. "14:32:07" or "2 Jan 14:32". * @returns {string} e.g. "14:32:07" or "2 Jan 14:32".
*/ */
export function formatTimestamp(date) { function formatTimestamp(date) {
if (!date) return ''; if (!date) return '';
const d = date instanceof Date ? date : new Date(date); const d = date instanceof Date ? date : new Date(date);
if (isNaN(d.getTime())) return ''; if (isNaN(d.getTime())) return '';
@@ -98,7 +98,7 @@ export function formatTimestamp(date) {
* @param {number} ms - Delay in milliseconds. * @param {number} ms - Delay in milliseconds.
* @returns {Function} * @returns {Function}
*/ */
export function debounce(fn, ms) { function debounce(fn, ms) {
let timer = null; let timer = null;
return function (...args) { return function (...args) {
clearTimeout(timer); clearTimeout(timer);
@@ -113,7 +113,7 @@ export function debounce(fn, ms) {
* @param {string} query * @param {string} query
* @returns {{ match: boolean, score: number }} * @returns {{ match: boolean, score: number }}
*/ */
export function fuzzyMatch(text, query) { function fuzzyMatch(text, query) {
if (!query) return { match: true, score: 1 }; if (!query) return { match: true, score: 1 };
const lowerText = text.toLowerCase(); const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase(); const lowerQuery = query.toLowerCase();
@@ -146,7 +146,7 @@ export function fuzzyMatch(text, query) {
* @param {string} str * @param {string} str
* @returns {string} * @returns {string}
*/ */
export function base64Encode(str) { function base64Encode(str) {
return btoa(str); return btoa(str);
} }
@@ -155,7 +155,7 @@ export function base64Encode(str) {
* @param {string} str * @param {string} str
* @returns {string} * @returns {string}
*/ */
export function base64Decode(str) { function base64Decode(str) {
return atob(str); return atob(str);
} }
@@ -163,7 +163,7 @@ export function base64Decode(str) {
* Generates a new UUID v4 using the browser crypto API. * Generates a new UUID v4 using the browser crypto API.
* @returns {string} * @returns {string}
*/ */
export function generateId() { function generateId() {
return crypto.randomUUID(); return crypto.randomUUID();
} }
@@ -172,7 +172,7 @@ export function generateId() {
* @param {number} bytes * @param {number} bytes
* @returns {string} e.g. "1.23 KB" * @returns {string} e.g. "1.23 KB"
*/ */
export function formatBytes(bytes) { function formatBytes(bytes) {
if (bytes === 0) return '0 B'; if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB']; const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const k = 1024; const k = 1024;
@@ -187,7 +187,7 @@ export function formatBytes(bytes) {
* @param {number} len * @param {number} len
* @returns {string} * @returns {string}
*/ */
export function truncate(str, len) { function truncate(str, len) {
if (!str || str.length <= len) return str || ''; if (!str || str.length <= len) return str || '';
return str.slice(0, len) + '…'; return str.slice(0, len) + '…';
} }
@@ -195,7 +195,7 @@ export function truncate(str, len) {
/** /**
* Removes any open context menus from the DOM. * Removes any open context menus from the DOM.
*/ */
export function dismissContextMenus() { function dismissContextMenus() {
document.querySelectorAll('.context-menu').forEach((m) => m.remove()); document.querySelectorAll('.context-menu').forEach((m) => m.remove());
} }
@@ -204,7 +204,7 @@ export function dismissContextMenus() {
* @param {string} protocol - 'ssh', 'telnet', or 'raw'. * @param {string} protocol - 'ssh', 'telnet', or 'raw'.
* @returns {string} * @returns {string}
*/ */
export function protocolColour(protocol) { function protocolColour(protocol) {
switch (protocol) { switch (protocol) {
case 'ssh': return 'protocol-ssh'; case 'ssh': return 'protocol-ssh';
case 'telnet': return 'protocol-telnet'; case 'telnet': return 'protocol-telnet';
@@ -218,7 +218,7 @@ export function protocolColour(protocol) {
* @param {string} state * @param {string} state
* @returns {string} * @returns {string}
*/ */
export function stateLabel(state) { function stateLabel(state) {
switch (state) { switch (state) {
case 'connecting': return 'Connecting'; case 'connecting': return 'Connecting';
case 'connected': return 'Connected'; case 'connected': return 'Connected';
@@ -227,3 +227,6 @@ export function stateLabel(state) {
default: return state || 'Unknown'; 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 });