(function () { /** * Nexus Terminal — Trigger Rules Engine. * Monitors terminal output and fires actions (highlight, notify, sound) * when patterns match. Rules are stored in localStorage. */ const { createElement } = window.NexusTerminal; const STORAGE_KEY = "nexus:trigger-rules"; class TriggerEngine { constructor(app) { this.app = app; this._rules = this._load(); this._backdrop = null; // Buffer per session to accumulate partial lines this._buffers = new Map(); } // ── Public API ─────────────────────────────────────────────────── /** * Checks incoming terminal data against all enabled rules. * Called from the session:data event handler. * @param {string} sessionId * @param {string} text - decoded terminal text */ check(sessionId, text) { if (this._rules.length === 0) return; // Accumulate text and check line by line const buf = (this._buffers.get(sessionId) || "") + text; const lines = buf.split("\n"); // Keep the last incomplete line in the buffer this._buffers.set(sessionId, lines.pop()); for (const line of lines) { this._checkLine(sessionId, line); } } /** * Opens the trigger rules management panel. */ open() { if (this._backdrop) return; this._render(); } /** * Closes the trigger rules panel. */ close() { if (this._backdrop) { this._backdrop.classList.remove("is-visible"); const modal = this._backdrop.querySelector(".modal"); if (modal) modal.classList.remove("is-visible"); setTimeout(() => { if (this._backdrop) { this._backdrop.remove(); this._backdrop = null; } }, 200); } } /** * Cleans up session buffer on disconnect. * @param {string} sessionId */ cleanup(sessionId) { this._buffers.delete(sessionId); } // ── Rule matching ──────────────────────────────────────────────── _checkLine(sessionId, line) { // Strip ANSI escape sequences for matching const clean = line.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, ""); for (const rule of this._rules) { if (!rule.enabled) continue; let matched = false; if (rule.isRegex) { try { const re = new RegExp(rule.pattern, rule.caseSensitive ? "" : "i"); matched = re.test(clean); } catch (_) { // Invalid regex — skip } } else { const haystack = rule.caseSensitive ? clean : clean.toLowerCase(); const needle = rule.caseSensitive ? rule.pattern : rule.pattern.toLowerCase(); matched = haystack.includes(needle); } if (!matched) continue; // 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}`, ); } if (rule.action === "highlight" || rule.action === "both") { // Flash the tab briefly 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); } } if (rule.action === "sound") { this._playBeep(); } } } _playBeep() { try { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 880; gain.gain.value = 0.1; osc.start(); osc.stop(ctx.currentTime + 0.15); } catch (_) { // Audio not available — silent } } // ── Storage ────────────────────────────────────────────────────── _load() { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : []; } catch (_) { return []; } } _save() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(this._rules)); } catch (_) { // localStorage full or unavailable } } // ── UI ─────────────────────────────────────────────────────────── _render() { const backdrop = createElement("div", "modal-backdrop"); const modal = createElement("div", "modal modal-lg"); modal.innerHTML = `
Rules match against terminal output and fire actions when a pattern is detected.