(function() { /** * Nexus Terminal — snippet / command library. * Provides a panel for managing and sending reusable commands to active sessions. */ const { createElement, escapeHtml, dismissContextMenus } = window.NexusTerminal; class SnippetLibrary { /** * @param {App} app */ constructor(app) { this.app = app; /** @type {Array} */ this.snippets = []; this.searchQuery = ''; } // ── Public API ─────────────────────────────────────────────────── /** * Opens the snippet library modal. */ async open() { await this._loadSnippets(); this._showModal(); } /** * Sends a snippet command to the active session. * @param {string} command */ sendToActiveSession(command) { const sessionId = this.app.state.activeSessionId; if (!sessionId) { this.app.notifications.error('No active session'); return; } const session = this.app.state.getSession(sessionId); if (!session || session.state !== 'connected') { this.app.notifications.error('Session is not connected'); return; } try { const b64 = btoa(command + '\n'); window.go.api.Service.WriteSession(sessionId, b64); this.app.notifications.info('Snippet sent'); } catch (err) { this.app.notifications.error('Failed to send snippet: ' + err); } } // ── Data ───────────────────────────────────────────────────────── /** @private */ async _loadSnippets() { try { this.snippets = await window.go.api.Service.ListSnippets() || []; } catch (err) { this.snippets = []; console.error('Failed to load snippets:', err); } } // ── Modal ──────────────────────────────────────────────────────── /** @private */ _showModal() { const backdrop = createElement('div', 'modal-backdrop'); const modal = createElement('div', 'modal modal-lg'); modal.innerHTML = ` `; backdrop.appendChild(modal); document.body.appendChild(backdrop); requestAnimationFrame(() => { backdrop.classList.add('is-visible'); modal.classList.add('is-visible'); }); const close = () => { backdrop.classList.remove('is-visible'); modal.classList.remove('is-visible'); setTimeout(() => backdrop.remove(), 200); }; modal.querySelector('.modal-close').addEventListener('click', close); backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(); }); // Search const searchInput = modal.querySelector('.snippet-search'); searchInput.addEventListener('input', () => { this.searchQuery = searchInput.value.trim().toLowerCase(); this._renderList(modal, close); }); // New snippet button modal.querySelector('.snippet-add-btn').addEventListener('click', () => { this._showEditor(modal, null, close); }); this._renderList(modal, close); } /** @private */ _renderList(modal, closeFn) { const listEl = modal.querySelector('.snippet-list'); if (!listEl) return; listEl.innerHTML = ''; const filtered = this.snippets.filter((sn) => { if (!this.searchQuery) return true; return sn.name.toLowerCase().includes(this.searchQuery) || sn.command.toLowerCase().includes(this.searchQuery) || (sn.description || '').toLowerCase().includes(this.searchQuery); }); if (filtered.length === 0) { listEl.innerHTML = '
No snippets found
'; return; } for (const sn of filtered) { const item = createElement('div', 'snippet-item', { dataset: { id: sn.id }, }); item.style.cssText = 'padding:var(--space-2);border-radius:var(--radius-md);cursor:pointer;border:1px solid var(--border-subtle);'; item.innerHTML = `
${escapeHtml(sn.name)}
${escapeHtml(sn.command)}
`; item.addEventListener('click', () => this._showDetail(modal, sn, closeFn)); item.addEventListener('mousedown', (e) => e.preventDefault()); item.addEventListener('mouseenter', () => { item.style.background = 'var(--bg-hover)'; }); item.addEventListener('mouseleave', () => { item.style.background = ''; }); listEl.appendChild(item); } } /** @private */ _showDetail(modal, snippet, closeFn) { const detailPanel = modal.querySelector('.snippet-detail-panel'); if (!detailPanel) return; detailPanel.innerHTML = ''; const nameEl = createElement('div', '', { innerHTML: `${escapeHtml(snippet.name)}`, }); detailPanel.appendChild(nameEl); if (snippet.description) { const descEl = createElement('div', '', { textContent: snippet.description, }); descEl.style.cssText = 'font-size:var(--text-sm);color:var(--text-secondary);'; detailPanel.appendChild(descEl); } const cmdEl = createElement('pre', '', { textContent: snippet.command, }); cmdEl.style.cssText = 'flex:1;font-family:var(--font-mono);font-size:var(--text-sm);background:var(--bg-base);padding:var(--space-3);border-radius:var(--radius-md);overflow:auto;color:var(--text-primary);margin:0;white-space:pre-wrap;word-break:break-all;'; detailPanel.appendChild(cmdEl); // Action buttons const actions = createElement('div', '', {}); actions.style.cssText = 'display:flex;gap:var(--space-2);'; const sendBtn = createElement('button', 'btn btn-sm btn-primary', { textContent: 'Send to Session' }); sendBtn.addEventListener('click', () => { this.sendToActiveSession(snippet.command); }); const editBtn = createElement('button', 'btn btn-sm btn-secondary', { textContent: 'Edit' }); editBtn.addEventListener('click', () => { this._showEditor(modal, snippet, closeFn); }); const deleteBtn = createElement('button', 'btn btn-sm btn-danger', { textContent: 'Delete' }); deleteBtn.addEventListener('click', async () => { try { await window.go.api.Service.DeleteSnippet(snippet.id); this.snippets = this.snippets.filter((s) => s.id !== snippet.id); this._renderList(modal, closeFn); detailPanel.innerHTML = '
Snippet deleted
'; } catch (err) { this.app.notifications.error('Failed to delete: ' + err); } }); actions.appendChild(sendBtn); actions.appendChild(editBtn); actions.appendChild(deleteBtn); detailPanel.appendChild(actions); } /** @private */ _showEditor(modal, snippet, closeFn) { const detailPanel = modal.querySelector('.snippet-detail-panel'); if (!detailPanel) return; detailPanel.innerHTML = ''; const isNew = !snippet; const title = isNew ? 'New Snippet' : 'Edit Snippet'; detailPanel.innerHTML = `
${title}
`; detailPanel.querySelector('.snippet-save-btn').addEventListener('click', async () => { const name = detailPanel.querySelector('.snippet-ed-name').value.trim(); const command = detailPanel.querySelector('.snippet-ed-cmd').value; const description = detailPanel.querySelector('.snippet-ed-desc').value.trim(); const folder = detailPanel.querySelector('.snippet-ed-folder').value.trim(); if (!name) { this.app.notifications.error('Name is required'); return; } if (!command) { this.app.notifications.error('Command is required'); return; } try { const data = { id: isNew ? '' : snippet.id, name, command, description, folder, tags: isNew ? [] : (snippet.tags || []), }; let saved; if (isNew) { saved = await window.go.api.Service.CreateSnippet(data); this.snippets.push(saved); } else { saved = await window.go.api.Service.UpdateSnippet(data); const idx = this.snippets.findIndex((s) => s.id === saved.id); if (idx !== -1) this.snippets[idx] = saved; } this._renderList(modal, closeFn); this._showDetail(modal, saved, closeFn); this.app.notifications.info(isNew ? 'Snippet created' : 'Snippet updated'); } catch (err) { this.app.notifications.error('Failed to save snippet: ' + err); } }); detailPanel.querySelector('.snippet-cancel-btn').addEventListener('click', () => { if (snippet) { this._showDetail(modal, snippet, closeFn); } else { detailPanel.innerHTML = '
Select a snippet or create a new one
'; } }); } } window.NexusTerminal = window.NexusTerminal || {}; window.NexusTerminal.SnippetLibrary = SnippetLibrary; })();