(function () { /** * Nexus Terminal — SSH Key Manager. * Panel for listing, generating, copying, and deleting SSH key pairs. */ const { createElement, dismissContextMenus } = window.NexusTerminal; class SSHKeyManager { constructor(app) { this.app = app; this._backdrop = null; } // ── Public API ─────────────────────────────────────────────────── open() { if (this._backdrop) return; this._render(); this._loadKeys(); } 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); } } // ── Rendering ──────────────────────────────────────────────────── _render() { const backdrop = createElement("div", "modal-backdrop"); const modal = createElement("div", "modal modal-lg"); modal.innerHTML = ` `; backdrop.appendChild(modal); document.body.appendChild(backdrop); this._backdrop = backdrop; requestAnimationFrame(() => { backdrop.classList.add("is-visible"); modal.classList.add("is-visible"); }); // Events modal .querySelector(".modal-close") .addEventListener("click", () => this.close()); backdrop.addEventListener("click", (e) => { if (e.target === backdrop) this.close(); }); modal .querySelector(".sshkey-generate-btn") .addEventListener("click", () => this._showGenerateForm()); } async _loadKeys() { const list = this._backdrop.querySelector(".sshkey-list"); if (!list) return; list.innerHTML = '
Loading keys…
'; try { const keys = await window.NexusTerminal.wailsService.ListSSHKeyPairs(); if (keys.length === 0) { list.innerHTML = '
No SSH keys found in ~/.ssh/
'; return; } list.innerHTML = ""; for (const key of keys) { list.appendChild(this._renderKeyRow(key)); } } catch (err) { list.innerHTML = ""; list.appendChild( createElement("div", "sshkey-error", { textContent: "Failed to load keys: " + String(err), }), ); } } _renderKeyRow(key) { const row = createElement("div", "sshkey-row"); const info = createElement("div", "sshkey-info"); const name = createElement("div", "sshkey-name", { textContent: key.name, }); const meta = createElement("div", "sshkey-meta", { textContent: key.type.toUpperCase() + (key.bits ? ` ${key.bits}` : ""), }); info.appendChild(name); info.appendChild(meta); const actions = createElement("div", "sshkey-actions"); const copyBtn = createElement("button", "btn btn-xs", { textContent: "Copy Public Key", }); copyBtn.addEventListener("mousedown", (e) => e.preventDefault()); copyBtn.addEventListener("click", () => this._copyPublicKey(key)); const deleteBtn = createElement("button", "btn btn-xs btn-danger", { textContent: "Delete", }); deleteBtn.addEventListener("mousedown", (e) => e.preventDefault()); deleteBtn.addEventListener("click", () => this._confirmDelete(key)); actions.appendChild(copyBtn); actions.appendChild(deleteBtn); row.appendChild(info); row.appendChild(actions); return row; } async _copyPublicKey(key) { try { const pubKey = await window.NexusTerminal.wailsService.GetSSHPublicKey(key.name); if (window.runtime && window.runtime.ClipboardSetText) { window.runtime.ClipboardSetText(pubKey); } else { await navigator.clipboard.writeText(pubKey); } this.app.notifications.success("Public key copied to clipboard"); } catch (err) { this.app.notifications.error("Failed to copy: " + err); } } _confirmDelete(key) { const confirmed = this._showConfirm( "Delete Key", `Delete key pair "${key.name}"? This cannot be undone.`, "Delete", ); confirmed.then((ok) => { if (ok) this._deleteKey(key); }); } async _deleteKey(key) { try { await window.NexusTerminal.wailsService.DeleteSSHKey(key.name); this.app.notifications.success(`Key "${key.name}" deleted`); this._loadKeys(); } catch (err) { this.app.notifications.error("Delete failed: " + err); } } // ── Generate form ──────────────────────────────────────────────── _showGenerateForm() { const list = this._backdrop.querySelector(".sshkey-list"); if (!list) return; const form = createElement("div", "sshkey-generate-form"); form.innerHTML = `
`; // Insert form above the key list list.parentNode.insertBefore(form, list); list.style.display = "none"; // Show bits field for RSA const typeSelect = form.querySelector(".sshkey-type-select"); const bitsGroup = form.querySelector(".sshkey-bits-group"); typeSelect.addEventListener("change", () => { bitsGroup.style.display = typeSelect.value === "rsa" ? "" : "none"; }); form.querySelector(".sshkey-cancel-btn").addEventListener("click", () => { form.remove(); list.style.display = ""; }); form .querySelector(".sshkey-create-btn") .addEventListener("click", async () => { const nameInput = form.querySelector(".sshkey-name-input"); const name = nameInput.value.trim(); const keyType = typeSelect.value; const bits = parseInt( form.querySelector(".sshkey-bits-select").value, 10, ); const passphrase = form.querySelector(".sshkey-pass-input").value; if (!name) { this.app.notifications.error("Key name is required"); return; } try { await window.NexusTerminal.wailsService.GenerateSSHKey( name, keyType, bits, passphrase, ); this.app.notifications.success( `Key "${name}" generated successfully`, ); form.remove(); list.style.display = ""; this._loadKeys(); } catch (err) { this.app.notifications.error("Generation failed: " + err); } }); form.querySelector(".sshkey-name-input").focus(); } // ── Confirmation helper ────────────────────────────────────────── _showConfirm(title, message, confirmLabel) { return new Promise((resolve) => { const backdrop = createElement("div", "modal-backdrop"); const modal = createElement("div", "modal modal-sm"); const header = createElement("div", "modal-header"); header.appendChild( createElement("h3", "modal-title", { textContent: title }), ); header.appendChild( createElement("button", "btn btn-icon btn-xs modal-close", { textContent: "x", type: "button", "aria-label": "Close", }), ); const body = createElement("div", "modal-body"); const messageEl = createElement("p", "", { textContent: message }); messageEl.style.fontSize = "var(--text-sm)"; messageEl.style.color = "var(--text-secondary)"; body.appendChild(messageEl); const footer = createElement("div", "modal-footer"); footer.appendChild( createElement("button", "btn btn-sm confirm-cancel", { textContent: "Cancel", type: "button", }), ); footer.appendChild( createElement("button", "btn btn-sm btn-danger confirm-ok", { textContent: confirmLabel, type: "button", }), ); modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer); backdrop.appendChild(modal); document.body.appendChild(backdrop); requestAnimationFrame(() => { backdrop.classList.add("is-visible"); modal.classList.add("is-visible"); }); const close = (val) => { backdrop.classList.remove("is-visible"); modal.classList.remove("is-visible"); setTimeout(() => backdrop.remove(), 200); resolve(val); }; modal .querySelector(".modal-close") .addEventListener("click", () => close(false)); modal .querySelector(".confirm-cancel") .addEventListener("click", () => close(false)); modal .querySelector(".confirm-ok") .addEventListener("click", () => close(true)); backdrop.addEventListener("click", (e) => { if (e.target === backdrop) close(false); }); }); } } window.NexusTerminal = window.NexusTerminal || {}; window.NexusTerminal.SSHKeyManager = SSHKeyManager; })();