Refactor SnippetLibrary and State classes for improved readability and consistency
- Updated formatting and indentation in snippets.js for better code clarity. - Enhanced error handling and notifications in sendToActiveSession method. - Improved variable resolution logic for command snippets. - Added new methods for managing session state in state.js. - Introduced new split pane functionality in terminal.js for better user experience. - Enhanced modal handling and user notifications across various components.
This commit is contained in:
+40
-11
@@ -604,7 +604,9 @@
|
|||||||
*/
|
*/
|
||||||
selectBroadcastTargets() {
|
selectBroadcastTargets() {
|
||||||
const { createElement, escapeHtml } = window.NexusTerminal;
|
const { createElement, escapeHtml } = window.NexusTerminal;
|
||||||
const sessions = this.state.getAllSessions().filter(
|
const sessions = this.state
|
||||||
|
.getAllSessions()
|
||||||
|
.filter(
|
||||||
(s) => s.state === "connected" && s.id !== this.state.activeSessionId,
|
(s) => s.state === "connected" && s.id !== this.state.activeSessionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -620,8 +622,13 @@
|
|||||||
let listHtml = "";
|
let listHtml = "";
|
||||||
for (const s of sessions) {
|
for (const s of sessions) {
|
||||||
const profile = this.state.getProfile(s.profileId);
|
const profile = this.state.getProfile(s.profileId);
|
||||||
const label = profile ? (profile.displayName || profile.host) : s.id.substring(0, 8);
|
const label = profile
|
||||||
const checked = currentTargets.size === 0 || currentTargets.has(s.id) ? "checked" : "";
|
? profile.displayName || profile.host
|
||||||
|
: s.id.substring(0, 8);
|
||||||
|
const checked =
|
||||||
|
currentTargets.size === 0 || currentTargets.has(s.id)
|
||||||
|
? "checked"
|
||||||
|
: "";
|
||||||
listHtml += `
|
listHtml += `
|
||||||
<label style="display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) 0;font-size:var(--text-sm);cursor:pointer;">
|
<label style="display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) 0;font-size:var(--text-sm);cursor:pointer;">
|
||||||
<input type="checkbox" class="broadcast-target-check" data-session-id="${s.id}" ${checked} />
|
<input type="checkbox" class="broadcast-target-check" data-session-id="${s.id}" ${checked} />
|
||||||
@@ -663,10 +670,14 @@
|
|||||||
|
|
||||||
modal.querySelector(".modal-close").addEventListener("click", close);
|
modal.querySelector(".modal-close").addEventListener("click", close);
|
||||||
modal.querySelector(".broadcast-cancel").addEventListener("click", close);
|
modal.querySelector(".broadcast-cancel").addEventListener("click", close);
|
||||||
backdrop.addEventListener("click", (e) => { if (e.target === backdrop) close(); });
|
backdrop.addEventListener("click", (e) => {
|
||||||
|
if (e.target === backdrop) close();
|
||||||
|
});
|
||||||
|
|
||||||
modal.querySelector(".broadcast-all").addEventListener("click", () => {
|
modal.querySelector(".broadcast-all").addEventListener("click", () => {
|
||||||
modal.querySelectorAll(".broadcast-target-check").forEach((cb) => { cb.checked = true; });
|
modal.querySelectorAll(".broadcast-target-check").forEach((cb) => {
|
||||||
|
cb.checked = true;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
modal.querySelector(".broadcast-apply").addEventListener("click", () => {
|
modal.querySelector(".broadcast-apply").addEventListener("click", () => {
|
||||||
@@ -682,11 +693,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If all are selected, use null (broadcast to all)
|
// If all are selected, use null (broadcast to all)
|
||||||
this.state.broadcastTargets = selected.size === sessions.length ? null : selected;
|
this.state.broadcastTargets =
|
||||||
|
selected.size === sessions.length ? null : selected;
|
||||||
if (!this.state.broadcastMode) {
|
if (!this.state.broadcastMode) {
|
||||||
this.toggleBroadcast();
|
this.toggleBroadcast();
|
||||||
} else {
|
} else {
|
||||||
const targetLabel = selected.size === sessions.length ? "all sessions" : selected.size + " sessions";
|
const targetLabel =
|
||||||
|
selected.size === sessions.length
|
||||||
|
? "all sessions"
|
||||||
|
: selected.size + " sessions";
|
||||||
this.notifications.info("Broadcasting to " + targetLabel);
|
this.notifications.info("Broadcasting to " + targetLabel);
|
||||||
}
|
}
|
||||||
close();
|
close();
|
||||||
@@ -1001,7 +1016,11 @@
|
|||||||
|
|
||||||
// Latency indicator — only show for connected SSH sessions.
|
// Latency indicator — only show for connected SSH sessions.
|
||||||
if (latencyEl) {
|
if (latencyEl) {
|
||||||
if (activeSession && activeSession.state === "connected" && activeSession.protocol === "ssh") {
|
if (
|
||||||
|
activeSession &&
|
||||||
|
activeSession.state === "connected" &&
|
||||||
|
activeSession.protocol === "ssh"
|
||||||
|
) {
|
||||||
this._startLatencyPing(activeSession.id);
|
this._startLatencyPing(activeSession.id);
|
||||||
} else {
|
} else {
|
||||||
this._stopLatencyPing();
|
this._stopLatencyPing();
|
||||||
@@ -1043,12 +1062,20 @@
|
|||||||
const el = document.getElementById("status-latency");
|
const el = document.getElementById("status-latency");
|
||||||
if (el && this._latencySessionID === sessionID) {
|
if (el && this._latencySessionID === sessionID) {
|
||||||
el.textContent = ms + " ms";
|
el.textContent = ms + " ms";
|
||||||
el.className = ms < 100 ? "latency-good" : ms < 250 ? "latency-warn" : "latency-bad";
|
el.className =
|
||||||
|
ms < 100
|
||||||
|
? "latency-good"
|
||||||
|
: ms < 250
|
||||||
|
? "latency-warn"
|
||||||
|
: "latency-bad";
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Session may have disconnected — clear indicator.
|
// Session may have disconnected — clear indicator.
|
||||||
const el = document.getElementById("status-latency");
|
const el = document.getElementById("status-latency");
|
||||||
if (el) { el.textContent = ""; el.className = ""; }
|
if (el) {
|
||||||
|
el.textContent = "";
|
||||||
|
el.className = "";
|
||||||
|
}
|
||||||
this._stopLatencyPing();
|
this._stopLatencyPing();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1370,7 +1397,9 @@
|
|||||||
const re = new RegExp(query, caseSensitive ? "g" : "gi");
|
const re = new RegExp(query, caseSensitive ? "g" : "gi");
|
||||||
const m = text.match(re);
|
const m = text.match(re);
|
||||||
if (m) total += m.length;
|
if (m) total += m.length;
|
||||||
} catch (_) { /* invalid regex */ }
|
} catch (_) {
|
||||||
|
/* invalid regex */
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const searchText = caseSensitive ? text : text.toLowerCase();
|
const searchText = caseSensitive ? text : text.toLowerCase();
|
||||||
const searchQuery = caseSensitive ? query : query.toLowerCase();
|
const searchQuery = caseSensitive ? query : query.toLowerCase();
|
||||||
|
|||||||
@@ -194,6 +194,20 @@ class CommandPalette {
|
|||||||
|
|
||||||
// Terminal actions (only when a session is active)
|
// Terminal actions (only when a session is active)
|
||||||
if (this.app.state.activeSessionId) {
|
if (this.app.state.activeSessionId) {
|
||||||
|
cmds.push({
|
||||||
|
id: 'split-vertical',
|
||||||
|
label: 'Split Pane Right',
|
||||||
|
section: 'Terminal',
|
||||||
|
shortcut: '⌘⇧D',
|
||||||
|
action: () => this.app.splitPane('vertical'),
|
||||||
|
});
|
||||||
|
cmds.push({
|
||||||
|
id: 'split-horizontal',
|
||||||
|
label: 'Split Pane Down',
|
||||||
|
section: 'Terminal',
|
||||||
|
shortcut: '⌘⇧E',
|
||||||
|
action: () => this.app.splitPane('horizontal'),
|
||||||
|
});
|
||||||
cmds.push({
|
cmds.push({
|
||||||
id: 'clear-terminal',
|
id: 'clear-terminal',
|
||||||
label: 'Clear Terminal',
|
label: 'Clear Terminal',
|
||||||
|
|||||||
+75
-24
@@ -236,7 +236,9 @@
|
|||||||
actions.appendChild(goToBtn);
|
actions.appendChild(goToBtn);
|
||||||
|
|
||||||
var bookmarkBtn = this._toolbarBtn("★", "Bookmarks", "");
|
var bookmarkBtn = this._toolbarBtn("★", "Bookmarks", "");
|
||||||
bookmarkBtn.addEventListener("click", () => this._showBookmarks(sessionId));
|
bookmarkBtn.addEventListener("click", () =>
|
||||||
|
this._showBookmarks(sessionId),
|
||||||
|
);
|
||||||
actions.appendChild(bookmarkBtn);
|
actions.appendChild(bookmarkBtn);
|
||||||
|
|
||||||
var mkdirBtn = this._toolbarBtn("+", "New folder", "is-accent");
|
var mkdirBtn = this._toolbarBtn("+", "New folder", "is-accent");
|
||||||
@@ -496,12 +498,17 @@
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Single click without modifier — clear all, select this one
|
// Single click without modifier — clear all, select this one
|
||||||
state.el.querySelectorAll(".sftp-file-row.is-selected").forEach(function (r) {
|
state.el
|
||||||
|
.querySelectorAll(".sftp-file-row.is-selected")
|
||||||
|
.forEach(function (r) {
|
||||||
r.classList.remove("is-selected");
|
r.classList.remove("is-selected");
|
||||||
});
|
});
|
||||||
|
|
||||||
// If clicking the same entry (and it was the only selection), deselect
|
// If clicking the same entry (and it was the only selection), deselect
|
||||||
if (state.selectedFiles.size === 1 && state.selectedFiles.has(entry.path)) {
|
if (
|
||||||
|
state.selectedFiles.size === 1 &&
|
||||||
|
state.selectedFiles.has(entry.path)
|
||||||
|
) {
|
||||||
state.selectedFiles.clear();
|
state.selectedFiles.clear();
|
||||||
state.selected = null;
|
state.selected = null;
|
||||||
var ctxBar = this._getCtxBar(sessionId);
|
var ctxBar = this._getCtxBar(sessionId);
|
||||||
@@ -540,14 +547,23 @@
|
|||||||
|
|
||||||
var label = createElement("span", "sftp-ctx-label", {
|
var label = createElement("span", "sftp-ctx-label", {
|
||||||
textContent: isSingle ? entry.name : count + " items selected",
|
textContent: isSingle ? entry.name : count + " items selected",
|
||||||
title: isSingle ? entry.path : entries.map(function (e) { return e.name; }).join(", "),
|
title: isSingle
|
||||||
|
? entry.path
|
||||||
|
: entries
|
||||||
|
.map(function (e) {
|
||||||
|
return e.name;
|
||||||
|
})
|
||||||
|
.join(", "),
|
||||||
});
|
});
|
||||||
ctxBar.appendChild(label);
|
ctxBar.appendChild(label);
|
||||||
|
|
||||||
// Download button — only for files (single or batch)
|
// Download button — only for files (single or batch)
|
||||||
var downloadableFiles = entries.filter(function (e) { return !e.isDir; });
|
var downloadableFiles = entries.filter(function (e) {
|
||||||
|
return !e.isDir;
|
||||||
|
});
|
||||||
if (downloadableFiles.length > 0) {
|
if (downloadableFiles.length > 0) {
|
||||||
var dlLabel = downloadableFiles.length === 1
|
var dlLabel =
|
||||||
|
downloadableFiles.length === 1
|
||||||
? "↓ Download"
|
? "↓ Download"
|
||||||
: "↓ Download (" + downloadableFiles.length + ")";
|
: "↓ Download (" + downloadableFiles.length + ")";
|
||||||
var dlBtn = createElement("button", "sftp-ctx-btn is-accent", {
|
var dlBtn = createElement("button", "sftp-ctx-btn is-accent", {
|
||||||
@@ -664,14 +680,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (failed === 0) {
|
if (failed === 0) {
|
||||||
this.app.notifications.success("All " + files.length + " files downloaded");
|
this.app.notifications.success(
|
||||||
|
"All " + files.length + " files downloaded",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_confirmDeleteBatch(sessionId, entries) {
|
_confirmDeleteBatch(sessionId, entries) {
|
||||||
this._showConfirmModal({
|
this._showConfirmModal({
|
||||||
title: "Delete " + entries.length + " Items",
|
title: "Delete " + entries.length + " Items",
|
||||||
message: "Permanently delete <strong>" + entries.length + " selected items</strong>?",
|
message:
|
||||||
|
"Permanently delete <strong>" +
|
||||||
|
entries.length +
|
||||||
|
" selected items</strong>?",
|
||||||
confirmLabel: "Delete All",
|
confirmLabel: "Delete All",
|
||||||
isDanger: true,
|
isDanger: true,
|
||||||
onConfirm: function () {
|
onConfirm: function () {
|
||||||
@@ -913,13 +934,25 @@
|
|||||||
|
|
||||||
var listHtml = "";
|
var listHtml = "";
|
||||||
if (bookmarks.length === 0) {
|
if (bookmarks.length === 0) {
|
||||||
listHtml = '<div style="color:var(--text-tertiary);font-size:var(--text-sm);text-align:center;padding:var(--space-3);">No bookmarks yet. Navigate to a directory and click "Bookmark Current".</div>';
|
listHtml =
|
||||||
|
'<div style="color:var(--text-tertiary);font-size:var(--text-sm);text-align:center;padding:var(--space-3);">No bookmarks yet. Navigate to a directory and click "Bookmark Current".</div>';
|
||||||
} else {
|
} else {
|
||||||
for (var i = 0; i < bookmarks.length; i++) {
|
for (var i = 0; i < bookmarks.length; i++) {
|
||||||
listHtml += '<div class="sftp-bookmark-row" data-index="' + i + '" data-path="' + escapeHtml(bookmarks[i]) + '">';
|
listHtml +=
|
||||||
listHtml += '<span style="flex:1;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escapeHtml(bookmarks[i]) + '</span>';
|
'<div class="sftp-bookmark-row" data-index="' +
|
||||||
listHtml += '<button class="btn btn-xs btn-ghost sftp-bookmark-del" data-index="' + i + '" title="Remove">✕</button>';
|
i +
|
||||||
listHtml += '</div>';
|
'" data-path="' +
|
||||||
|
escapeHtml(bookmarks[i]) +
|
||||||
|
'">';
|
||||||
|
listHtml +=
|
||||||
|
'<span style="flex:1;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' +
|
||||||
|
escapeHtml(bookmarks[i]) +
|
||||||
|
"</span>";
|
||||||
|
listHtml +=
|
||||||
|
'<button class="btn btn-xs btn-ghost sftp-bookmark-del" data-index="' +
|
||||||
|
i +
|
||||||
|
'" title="Remove">✕</button>';
|
||||||
|
listHtml += "</div>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -927,14 +960,16 @@
|
|||||||
'<div class="modal-header">' +
|
'<div class="modal-header">' +
|
||||||
'<span class="modal-title">Directory Bookmarks</span>' +
|
'<span class="modal-title">Directory Bookmarks</span>' +
|
||||||
'<button class="modal-close btn btn-icon btn-xs btn-ghost">✕</button>' +
|
'<button class="modal-close btn btn-icon btn-xs btn-ghost">✕</button>' +
|
||||||
'</div>' +
|
"</div>" +
|
||||||
'<div class="modal-body">' +
|
'<div class="modal-body">' +
|
||||||
'<div class="sftp-bookmark-list">' + listHtml + '</div>' +
|
'<div class="sftp-bookmark-list">' +
|
||||||
'</div>' +
|
listHtml +
|
||||||
|
"</div>" +
|
||||||
|
"</div>" +
|
||||||
'<div class="modal-footer">' +
|
'<div class="modal-footer">' +
|
||||||
'<button class="btn btn-sm btn-ghost sftp-modal-cancel">Close</button>' +
|
'<button class="btn btn-sm btn-ghost sftp-modal-cancel">Close</button>' +
|
||||||
'<button class="btn btn-sm btn-primary sftp-bookmark-add">★ Bookmark Current</button>' +
|
'<button class="btn btn-sm btn-primary sftp-bookmark-add">★ Bookmark Current</button>' +
|
||||||
'</div>';
|
"</div>";
|
||||||
|
|
||||||
backdrop.appendChild(modal);
|
backdrop.appendChild(modal);
|
||||||
document.body.appendChild(backdrop);
|
document.body.appendChild(backdrop);
|
||||||
@@ -950,12 +985,18 @@
|
|||||||
closed = true;
|
closed = true;
|
||||||
backdrop.classList.remove("is-visible");
|
backdrop.classList.remove("is-visible");
|
||||||
modal.classList.remove("is-visible");
|
modal.classList.remove("is-visible");
|
||||||
setTimeout(function () { backdrop.remove(); }, 200);
|
setTimeout(function () {
|
||||||
|
backdrop.remove();
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
modal.querySelector(".modal-close").addEventListener("click", close);
|
modal.querySelector(".modal-close").addEventListener("click", close);
|
||||||
modal.querySelector(".sftp-modal-cancel").addEventListener("click", close);
|
modal
|
||||||
backdrop.addEventListener("click", function (e) { if (e.target === backdrop) close(); });
|
.querySelector(".sftp-modal-cancel")
|
||||||
|
.addEventListener("click", close);
|
||||||
|
backdrop.addEventListener("click", function (e) {
|
||||||
|
if (e.target === backdrop) close();
|
||||||
|
});
|
||||||
|
|
||||||
// Navigate to bookmark on click
|
// Navigate to bookmark on click
|
||||||
modal.querySelectorAll(".sftp-bookmark-row").forEach(function (row) {
|
modal.querySelectorAll(".sftp-bookmark-row").forEach(function (row) {
|
||||||
@@ -965,7 +1006,9 @@
|
|||||||
close();
|
close();
|
||||||
self._navigate(sessionId, path);
|
self._navigate(sessionId, path);
|
||||||
});
|
});
|
||||||
row.addEventListener("mousedown", function (e) { e.preventDefault(); });
|
row.addEventListener("mousedown", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete bookmark
|
// Delete bookmark
|
||||||
@@ -980,7 +1023,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add current directory as bookmark
|
// Add current directory as bookmark
|
||||||
modal.querySelector(".sftp-bookmark-add").addEventListener("click", function () {
|
modal
|
||||||
|
.querySelector(".sftp-bookmark-add")
|
||||||
|
.addEventListener("click", function () {
|
||||||
self._addBookmark(sessionId, state.cwd);
|
self._addBookmark(sessionId, state.cwd);
|
||||||
close();
|
close();
|
||||||
self._showBookmarks(sessionId);
|
self._showBookmarks(sessionId);
|
||||||
@@ -1001,7 +1046,10 @@
|
|||||||
var bookmarks = this._getBookmarks(sessionId);
|
var bookmarks = this._getBookmarks(sessionId);
|
||||||
if (bookmarks.indexOf(path) === -1) {
|
if (bookmarks.indexOf(path) === -1) {
|
||||||
bookmarks.push(path);
|
bookmarks.push(path);
|
||||||
localStorage.setItem("sftp-bookmarks-" + sessionId, JSON.stringify(bookmarks));
|
localStorage.setItem(
|
||||||
|
"sftp-bookmarks-" + sessionId,
|
||||||
|
JSON.stringify(bookmarks),
|
||||||
|
);
|
||||||
this.app.notifications.success("Bookmarked: " + path);
|
this.app.notifications.success("Bookmarked: " + path);
|
||||||
} else {
|
} else {
|
||||||
this.app.notifications.info("Already bookmarked");
|
this.app.notifications.info("Already bookmarked");
|
||||||
@@ -1011,7 +1059,10 @@
|
|||||||
_removeBookmark(sessionId, index) {
|
_removeBookmark(sessionId, index) {
|
||||||
var bookmarks = this._getBookmarks(sessionId);
|
var bookmarks = this._getBookmarks(sessionId);
|
||||||
bookmarks.splice(index, 1);
|
bookmarks.splice(index, 1);
|
||||||
localStorage.setItem("sftp-bookmarks-" + sessionId, JSON.stringify(bookmarks));
|
localStorage.setItem(
|
||||||
|
"sftp-bookmarks-" + sessionId,
|
||||||
|
JSON.stringify(bookmarks),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Transfer progress queue ─────────────────────────────────── */
|
/* ── Transfer progress queue ─────────────────────────────────── */
|
||||||
|
|||||||
+130
-92
@@ -4,7 +4,8 @@
|
|||||||
* Provides a panel for managing and sending reusable commands to active sessions.
|
* Provides a panel for managing and sending reusable commands to active sessions.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { createElement, escapeHtml, dismissContextMenus } = window.NexusTerminal;
|
const { createElement, escapeHtml, dismissContextMenus } =
|
||||||
|
window.NexusTerminal;
|
||||||
|
|
||||||
class SnippetLibrary {
|
class SnippetLibrary {
|
||||||
/**
|
/**
|
||||||
@@ -14,7 +15,7 @@ class SnippetLibrary {
|
|||||||
this.app = app;
|
this.app = app;
|
||||||
/** @type {Array<Object>} */
|
/** @type {Array<Object>} */
|
||||||
this.snippets = [];
|
this.snippets = [];
|
||||||
this.searchQuery = '';
|
this.searchQuery = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Public API ───────────────────────────────────────────────────
|
// ── Public API ───────────────────────────────────────────────────
|
||||||
@@ -36,12 +37,12 @@ class SnippetLibrary {
|
|||||||
sendToActiveSession(command) {
|
sendToActiveSession(command) {
|
||||||
const sessionId = this.app.state.activeSessionId;
|
const sessionId = this.app.state.activeSessionId;
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
this.app.notifications.error('No active session');
|
this.app.notifications.error("No active session");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const session = this.app.state.getSession(sessionId);
|
const session = this.app.state.getSession(sessionId);
|
||||||
if (!session || session.state !== 'connected') {
|
if (!session || session.state !== "connected") {
|
||||||
this.app.notifications.error('Session is not connected');
|
this.app.notifications.error("Session is not connected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,9 +56,9 @@ class SnippetLibrary {
|
|||||||
|
|
||||||
// Resolve built-in variables first
|
// Resolve built-in variables first
|
||||||
const builtins = {
|
const builtins = {
|
||||||
hostname: (session.host || 'unknown'),
|
hostname: session.host || "unknown",
|
||||||
date: new Date().toISOString().split('T')[0],
|
date: new Date().toISOString().split("T")[0],
|
||||||
time: new Date().toTimeString().split(' ')[0],
|
time: new Date().toTimeString().split(" ")[0],
|
||||||
timestamp: Math.floor(Date.now() / 1000).toString(),
|
timestamp: Math.floor(Date.now() / 1000).toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ class SnippetLibrary {
|
|||||||
for (const varName of matches) {
|
for (const varName of matches) {
|
||||||
const lower = varName.toLowerCase();
|
const lower = varName.toLowerCase();
|
||||||
if (builtins[lower] !== undefined) {
|
if (builtins[lower] !== undefined) {
|
||||||
resolved = resolved.replace('{{' + varName + '}}', builtins[lower]);
|
resolved = resolved.replace("{{" + varName + "}}", builtins[lower]);
|
||||||
} else if (!userVars.includes(varName)) {
|
} else if (!userVars.includes(varName)) {
|
||||||
userVars.push(varName);
|
userVars.push(varName);
|
||||||
}
|
}
|
||||||
@@ -81,7 +82,7 @@ class SnippetLibrary {
|
|||||||
this._promptVariables(userVars, (values) => {
|
this._promptVariables(userVars, (values) => {
|
||||||
let final = resolved;
|
let final = resolved;
|
||||||
for (let i = 0; i < userVars.length; i++) {
|
for (let i = 0; i < userVars.length; i++) {
|
||||||
final = final.split('{{' + userVars[i] + '}}').join(values[i] || '');
|
final = final.split("{{" + userVars[i] + "}}").join(values[i] || "");
|
||||||
}
|
}
|
||||||
this._sendCommand(sessionId, final);
|
this._sendCommand(sessionId, final);
|
||||||
});
|
});
|
||||||
@@ -90,20 +91,20 @@ class SnippetLibrary {
|
|||||||
/** @private */
|
/** @private */
|
||||||
_sendCommand(sessionId, command) {
|
_sendCommand(sessionId, command) {
|
||||||
try {
|
try {
|
||||||
const b64 = btoa(command + '\n');
|
const b64 = btoa(command + "\n");
|
||||||
window.go.api.Service.WriteSession(sessionId, b64);
|
window.go.api.Service.WriteSession(sessionId, b64);
|
||||||
this.app.notifications.info('Snippet sent');
|
this.app.notifications.info("Snippet sent");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.app.notifications.error('Failed to send snippet: ' + err);
|
this.app.notifications.error("Failed to send snippet: " + err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
_promptVariables(varNames, onConfirm) {
|
_promptVariables(varNames, onConfirm) {
|
||||||
const backdrop = createElement('div', 'modal-backdrop');
|
const backdrop = createElement("div", "modal-backdrop");
|
||||||
const modal = createElement('div', 'modal modal-sm');
|
const modal = createElement("div", "modal modal-sm");
|
||||||
|
|
||||||
let inputsHtml = '';
|
let inputsHtml = "";
|
||||||
for (const v of varNames) {
|
for (const v of varNames) {
|
||||||
inputsHtml += `
|
inputsHtml += `
|
||||||
<div class="form-group" style="margin-bottom:var(--space-2);">
|
<div class="form-group" style="margin-bottom:var(--space-2);">
|
||||||
@@ -128,36 +129,40 @@ class SnippetLibrary {
|
|||||||
backdrop.appendChild(modal);
|
backdrop.appendChild(modal);
|
||||||
document.body.appendChild(backdrop);
|
document.body.appendChild(backdrop);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
backdrop.classList.add('is-visible');
|
backdrop.classList.add("is-visible");
|
||||||
modal.classList.add('is-visible');
|
modal.classList.add("is-visible");
|
||||||
const firstInput = modal.querySelector('.snippet-var-input');
|
const firstInput = modal.querySelector(".snippet-var-input");
|
||||||
if (firstInput) firstInput.focus();
|
if (firstInput) firstInput.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
backdrop.classList.remove('is-visible');
|
backdrop.classList.remove("is-visible");
|
||||||
modal.classList.remove('is-visible');
|
modal.classList.remove("is-visible");
|
||||||
setTimeout(() => backdrop.remove(), 200);
|
setTimeout(() => backdrop.remove(), 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
modal.querySelector('.modal-close').addEventListener('click', close);
|
modal.querySelector(".modal-close").addEventListener("click", close);
|
||||||
modal.querySelector('.snippet-var-cancel').addEventListener('click', close);
|
modal
|
||||||
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(); });
|
.querySelector(".snippet-var-cancel")
|
||||||
|
.addEventListener("click", close);
|
||||||
|
backdrop.addEventListener("click", (e) => {
|
||||||
|
if (e.target === backdrop) close();
|
||||||
|
});
|
||||||
|
|
||||||
modal.querySelector('.snippet-var-send').addEventListener('click', () => {
|
modal.querySelector(".snippet-var-send").addEventListener("click", () => {
|
||||||
const inputs = modal.querySelectorAll('.snippet-var-input');
|
const inputs = modal.querySelectorAll(".snippet-var-input");
|
||||||
const values = Array.from(inputs).map((inp) => inp.value);
|
const values = Array.from(inputs).map((inp) => inp.value);
|
||||||
close();
|
close();
|
||||||
onConfirm(values);
|
onConfirm(values);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enter to send
|
// Enter to send
|
||||||
modal.addEventListener('keydown', (e) => {
|
modal.addEventListener("keydown", (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
modal.querySelector('.snippet-var-send').click();
|
modal.querySelector(".snippet-var-send").click();
|
||||||
}
|
}
|
||||||
if (e.key === 'Escape') close();
|
if (e.key === "Escape") close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,10 +171,10 @@ class SnippetLibrary {
|
|||||||
/** @private */
|
/** @private */
|
||||||
async _loadSnippets() {
|
async _loadSnippets() {
|
||||||
try {
|
try {
|
||||||
this.snippets = await window.go.api.Service.ListSnippets() || [];
|
this.snippets = (await window.go.api.Service.ListSnippets()) || [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.snippets = [];
|
this.snippets = [];
|
||||||
console.error('Failed to load snippets:', err);
|
console.error("Failed to load snippets:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,8 +182,8 @@ class SnippetLibrary {
|
|||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
_showModal() {
|
_showModal() {
|
||||||
const backdrop = createElement('div', 'modal-backdrop');
|
const backdrop = createElement("div", "modal-backdrop");
|
||||||
const modal = createElement('div', 'modal modal-lg');
|
const modal = createElement("div", "modal modal-lg");
|
||||||
|
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -204,28 +209,30 @@ class SnippetLibrary {
|
|||||||
backdrop.appendChild(modal);
|
backdrop.appendChild(modal);
|
||||||
document.body.appendChild(backdrop);
|
document.body.appendChild(backdrop);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
backdrop.classList.add('is-visible');
|
backdrop.classList.add("is-visible");
|
||||||
modal.classList.add('is-visible');
|
modal.classList.add("is-visible");
|
||||||
});
|
});
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
backdrop.classList.remove('is-visible');
|
backdrop.classList.remove("is-visible");
|
||||||
modal.classList.remove('is-visible');
|
modal.classList.remove("is-visible");
|
||||||
setTimeout(() => backdrop.remove(), 200);
|
setTimeout(() => backdrop.remove(), 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
modal.querySelector('.modal-close').addEventListener('click', close);
|
modal.querySelector(".modal-close").addEventListener("click", close);
|
||||||
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(); });
|
backdrop.addEventListener("click", (e) => {
|
||||||
|
if (e.target === backdrop) close();
|
||||||
|
});
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
const searchInput = modal.querySelector('.snippet-search');
|
const searchInput = modal.querySelector(".snippet-search");
|
||||||
searchInput.addEventListener('input', () => {
|
searchInput.addEventListener("input", () => {
|
||||||
this.searchQuery = searchInput.value.trim().toLowerCase();
|
this.searchQuery = searchInput.value.trim().toLowerCase();
|
||||||
this._renderList(modal, close);
|
this._renderList(modal, close);
|
||||||
});
|
});
|
||||||
|
|
||||||
// New snippet button
|
// New snippet button
|
||||||
modal.querySelector('.snippet-add-btn').addEventListener('click', () => {
|
modal.querySelector(".snippet-add-btn").addEventListener("click", () => {
|
||||||
this._showEditor(modal, null, close);
|
this._showEditor(modal, null, close);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -234,87 +241,106 @@ class SnippetLibrary {
|
|||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
_renderList(modal, closeFn) {
|
_renderList(modal, closeFn) {
|
||||||
const listEl = modal.querySelector('.snippet-list');
|
const listEl = modal.querySelector(".snippet-list");
|
||||||
if (!listEl) return;
|
if (!listEl) return;
|
||||||
listEl.innerHTML = '';
|
listEl.innerHTML = "";
|
||||||
|
|
||||||
const filtered = this.snippets.filter((sn) => {
|
const filtered = this.snippets.filter((sn) => {
|
||||||
if (!this.searchQuery) return true;
|
if (!this.searchQuery) return true;
|
||||||
return sn.name.toLowerCase().includes(this.searchQuery) ||
|
return (
|
||||||
|
sn.name.toLowerCase().includes(this.searchQuery) ||
|
||||||
sn.command.toLowerCase().includes(this.searchQuery) ||
|
sn.command.toLowerCase().includes(this.searchQuery) ||
|
||||||
(sn.description || '').toLowerCase().includes(this.searchQuery);
|
(sn.description || "").toLowerCase().includes(this.searchQuery)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
if (filtered.length === 0) {
|
||||||
listEl.innerHTML = '<div style="color:var(--text-tertiary);font-size:var(--text-sm);padding:var(--space-3);text-align:center;">No snippets found</div>';
|
listEl.innerHTML =
|
||||||
|
'<div style="color:var(--text-tertiary);font-size:var(--text-sm);padding:var(--space-3);text-align:center;">No snippets found</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const sn of filtered) {
|
for (const sn of filtered) {
|
||||||
const item = createElement('div', 'snippet-item', {
|
const item = createElement("div", "snippet-item", {
|
||||||
dataset: { id: sn.id },
|
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.style.cssText =
|
||||||
|
"padding:var(--space-2);border-radius:var(--radius-md);cursor:pointer;border:1px solid var(--border-subtle);";
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<div style="font-weight:var(--weight-medium);font-size:var(--text-sm);color:var(--text-primary);">${escapeHtml(sn.name)}</div>
|
<div style="font-weight:var(--weight-medium);font-size:var(--text-sm);color:var(--text-primary);">${escapeHtml(sn.name)}</div>
|
||||||
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-secondary);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(sn.command)}</div>
|
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-secondary);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(sn.command)}</div>
|
||||||
`;
|
`;
|
||||||
item.addEventListener('click', () => this._showDetail(modal, sn, closeFn));
|
item.addEventListener("click", () =>
|
||||||
item.addEventListener('mousedown', (e) => e.preventDefault());
|
this._showDetail(modal, sn, closeFn),
|
||||||
item.addEventListener('mouseenter', () => { item.style.background = 'var(--bg-hover)'; });
|
);
|
||||||
item.addEventListener('mouseleave', () => { item.style.background = ''; });
|
item.addEventListener("mousedown", (e) => e.preventDefault());
|
||||||
|
item.addEventListener("mouseenter", () => {
|
||||||
|
item.style.background = "var(--bg-hover)";
|
||||||
|
});
|
||||||
|
item.addEventListener("mouseleave", () => {
|
||||||
|
item.style.background = "";
|
||||||
|
});
|
||||||
listEl.appendChild(item);
|
listEl.appendChild(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
_showDetail(modal, snippet, closeFn) {
|
_showDetail(modal, snippet, closeFn) {
|
||||||
const detailPanel = modal.querySelector('.snippet-detail-panel');
|
const detailPanel = modal.querySelector(".snippet-detail-panel");
|
||||||
if (!detailPanel) return;
|
if (!detailPanel) return;
|
||||||
detailPanel.innerHTML = '';
|
detailPanel.innerHTML = "";
|
||||||
|
|
||||||
const nameEl = createElement('div', '', {
|
const nameEl = createElement("div", "", {
|
||||||
innerHTML: `<strong style="font-size:var(--text-base);">${escapeHtml(snippet.name)}</strong>`,
|
innerHTML: `<strong style="font-size:var(--text-base);">${escapeHtml(snippet.name)}</strong>`,
|
||||||
});
|
});
|
||||||
detailPanel.appendChild(nameEl);
|
detailPanel.appendChild(nameEl);
|
||||||
|
|
||||||
if (snippet.description) {
|
if (snippet.description) {
|
||||||
const descEl = createElement('div', '', {
|
const descEl = createElement("div", "", {
|
||||||
textContent: snippet.description,
|
textContent: snippet.description,
|
||||||
});
|
});
|
||||||
descEl.style.cssText = 'font-size:var(--text-sm);color:var(--text-secondary);';
|
descEl.style.cssText =
|
||||||
|
"font-size:var(--text-sm);color:var(--text-secondary);";
|
||||||
detailPanel.appendChild(descEl);
|
detailPanel.appendChild(descEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cmdEl = createElement('pre', '', {
|
const cmdEl = createElement("pre", "", {
|
||||||
textContent: snippet.command,
|
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;';
|
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);
|
detailPanel.appendChild(cmdEl);
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons
|
||||||
const actions = createElement('div', '', {});
|
const actions = createElement("div", "", {});
|
||||||
actions.style.cssText = 'display:flex;gap:var(--space-2);';
|
actions.style.cssText = "display:flex;gap:var(--space-2);";
|
||||||
|
|
||||||
const sendBtn = createElement('button', 'btn btn-sm btn-primary', { textContent: 'Send to Session' });
|
const sendBtn = createElement("button", "btn btn-sm btn-primary", {
|
||||||
sendBtn.addEventListener('click', () => {
|
textContent: "Send to Session",
|
||||||
|
});
|
||||||
|
sendBtn.addEventListener("click", () => {
|
||||||
this.sendToActiveSession(snippet.command);
|
this.sendToActiveSession(snippet.command);
|
||||||
});
|
});
|
||||||
|
|
||||||
const editBtn = createElement('button', 'btn btn-sm btn-secondary', { textContent: 'Edit' });
|
const editBtn = createElement("button", "btn btn-sm btn-secondary", {
|
||||||
editBtn.addEventListener('click', () => {
|
textContent: "Edit",
|
||||||
|
});
|
||||||
|
editBtn.addEventListener("click", () => {
|
||||||
this._showEditor(modal, snippet, closeFn);
|
this._showEditor(modal, snippet, closeFn);
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteBtn = createElement('button', 'btn btn-sm btn-danger', { textContent: 'Delete' });
|
const deleteBtn = createElement("button", "btn btn-sm btn-danger", {
|
||||||
deleteBtn.addEventListener('click', async () => {
|
textContent: "Delete",
|
||||||
|
});
|
||||||
|
deleteBtn.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
await window.go.api.Service.DeleteSnippet(snippet.id);
|
await window.go.api.Service.DeleteSnippet(snippet.id);
|
||||||
this.snippets = this.snippets.filter((s) => s.id !== snippet.id);
|
this.snippets = this.snippets.filter((s) => s.id !== snippet.id);
|
||||||
this._renderList(modal, closeFn);
|
this._renderList(modal, closeFn);
|
||||||
detailPanel.innerHTML = '<div class="snippet-detail-empty" style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--text-tertiary);font-size:var(--text-sm);">Snippet deleted</div>';
|
detailPanel.innerHTML =
|
||||||
|
'<div class="snippet-detail-empty" style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--text-tertiary);font-size:var(--text-sm);">Snippet deleted</div>';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.app.notifications.error('Failed to delete: ' + err);
|
this.app.notifications.error("Failed to delete: " + err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -326,30 +352,30 @@ class SnippetLibrary {
|
|||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
_showEditor(modal, snippet, closeFn) {
|
_showEditor(modal, snippet, closeFn) {
|
||||||
const detailPanel = modal.querySelector('.snippet-detail-panel');
|
const detailPanel = modal.querySelector(".snippet-detail-panel");
|
||||||
if (!detailPanel) return;
|
if (!detailPanel) return;
|
||||||
detailPanel.innerHTML = '';
|
detailPanel.innerHTML = "";
|
||||||
|
|
||||||
const isNew = !snippet;
|
const isNew = !snippet;
|
||||||
const title = isNew ? 'New Snippet' : 'Edit Snippet';
|
const title = isNew ? "New Snippet" : "Edit Snippet";
|
||||||
|
|
||||||
detailPanel.innerHTML = `
|
detailPanel.innerHTML = `
|
||||||
<div style="font-weight:var(--weight-medium);font-size:var(--text-sm);">${title}</div>
|
<div style="font-weight:var(--weight-medium);font-size:var(--text-sm);">${title}</div>
|
||||||
<div class="form-group" style="margin-bottom:var(--space-2);">
|
<div class="form-group" style="margin-bottom:var(--space-2);">
|
||||||
<label class="form-label">Name</label>
|
<label class="form-label">Name</label>
|
||||||
<input type="text" class="form-input snippet-ed-name" value="${isNew ? '' : escapeHtml(snippet.name)}" />
|
<input type="text" class="form-input snippet-ed-name" value="${isNew ? "" : escapeHtml(snippet.name)}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom:var(--space-2);">
|
<div class="form-group" style="margin-bottom:var(--space-2);">
|
||||||
<label class="form-label">Description</label>
|
<label class="form-label">Description</label>
|
||||||
<input type="text" class="form-input snippet-ed-desc" value="${isNew ? '' : escapeHtml(snippet.description || '')}" />
|
<input type="text" class="form-input snippet-ed-desc" value="${isNew ? "" : escapeHtml(snippet.description || "")}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="flex:1;display:flex;flex-direction:column;">
|
<div class="form-group" style="flex:1;display:flex;flex-direction:column;">
|
||||||
<label class="form-label">Command</label>
|
<label class="form-label">Command</label>
|
||||||
<textarea class="form-input snippet-ed-cmd" style="flex:1;font-family:var(--font-mono);font-size:var(--text-sm);resize:none;">${isNew ? '' : escapeHtml(snippet.command)}</textarea>
|
<textarea class="form-input snippet-ed-cmd" style="flex:1;font-family:var(--font-mono);font-size:var(--text-sm);resize:none;">${isNew ? "" : escapeHtml(snippet.command)}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom:var(--space-2);">
|
<div class="form-group" style="margin-bottom:var(--space-2);">
|
||||||
<label class="form-label">Folder</label>
|
<label class="form-label">Folder</label>
|
||||||
<input type="text" class="form-input snippet-ed-folder" value="${isNew ? '' : escapeHtml(snippet.folder || '')}" placeholder="e.g. Networking, Debugging" />
|
<input type="text" class="form-input snippet-ed-folder" value="${isNew ? "" : escapeHtml(snippet.folder || "")}" placeholder="e.g. Networking, Debugging" />
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:var(--space-2);">
|
<div style="display:flex;gap:var(--space-2);">
|
||||||
<button class="btn btn-sm btn-primary snippet-save-btn">Save</button>
|
<button class="btn btn-sm btn-primary snippet-save-btn">Save</button>
|
||||||
@@ -357,29 +383,37 @@ class SnippetLibrary {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
detailPanel.querySelector('.snippet-save-btn').addEventListener('click', async () => {
|
detailPanel
|
||||||
const name = detailPanel.querySelector('.snippet-ed-name').value.trim();
|
.querySelector(".snippet-save-btn")
|
||||||
const command = detailPanel.querySelector('.snippet-ed-cmd').value;
|
.addEventListener("click", async () => {
|
||||||
const description = detailPanel.querySelector('.snippet-ed-desc').value.trim();
|
const name = detailPanel
|
||||||
const folder = detailPanel.querySelector('.snippet-ed-folder').value.trim();
|
.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) {
|
if (!name) {
|
||||||
this.app.notifications.error('Name is required');
|
this.app.notifications.error("Name is required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!command) {
|
if (!command) {
|
||||||
this.app.notifications.error('Command is required');
|
this.app.notifications.error("Command is required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
id: isNew ? '' : snippet.id,
|
id: isNew ? "" : snippet.id,
|
||||||
name,
|
name,
|
||||||
command,
|
command,
|
||||||
description,
|
description,
|
||||||
folder,
|
folder,
|
||||||
tags: isNew ? [] : (snippet.tags || []),
|
tags: isNew ? [] : snippet.tags || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
let saved;
|
let saved;
|
||||||
@@ -394,17 +428,22 @@ class SnippetLibrary {
|
|||||||
|
|
||||||
this._renderList(modal, closeFn);
|
this._renderList(modal, closeFn);
|
||||||
this._showDetail(modal, saved, closeFn);
|
this._showDetail(modal, saved, closeFn);
|
||||||
this.app.notifications.info(isNew ? 'Snippet created' : 'Snippet updated');
|
this.app.notifications.info(
|
||||||
|
isNew ? "Snippet created" : "Snippet updated",
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.app.notifications.error('Failed to save snippet: ' + err);
|
this.app.notifications.error("Failed to save snippet: " + err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
detailPanel.querySelector('.snippet-cancel-btn').addEventListener('click', () => {
|
detailPanel
|
||||||
|
.querySelector(".snippet-cancel-btn")
|
||||||
|
.addEventListener("click", () => {
|
||||||
if (snippet) {
|
if (snippet) {
|
||||||
this._showDetail(modal, snippet, closeFn);
|
this._showDetail(modal, snippet, closeFn);
|
||||||
} else {
|
} else {
|
||||||
detailPanel.innerHTML = '<div class="snippet-detail-empty" style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--text-tertiary);font-size:var(--text-sm);">Select a snippet or create a new one</div>';
|
detailPanel.innerHTML =
|
||||||
|
'<div class="snippet-detail-empty" style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--text-tertiary);font-size:var(--text-sm);">Select a snippet or create a new one</div>';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -412,5 +451,4 @@ class SnippetLibrary {
|
|||||||
|
|
||||||
window.NexusTerminal = window.NexusTerminal || {};
|
window.NexusTerminal = window.NexusTerminal || {};
|
||||||
window.NexusTerminal.SnippetLibrary = SnippetLibrary;
|
window.NexusTerminal.SnippetLibrary = SnippetLibrary;
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
+19
-19
@@ -87,7 +87,7 @@ class State {
|
|||||||
*/
|
*/
|
||||||
setProfiles(profiles) {
|
setProfiles(profiles) {
|
||||||
this.profiles = profiles;
|
this.profiles = profiles;
|
||||||
this.emit('profiles:changed', this.profiles);
|
this.emit("profiles:changed", this.profiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,8 +96,8 @@ class State {
|
|||||||
*/
|
*/
|
||||||
addProfile(profile) {
|
addProfile(profile) {
|
||||||
this.profiles.push(profile);
|
this.profiles.push(profile);
|
||||||
this.emit('profiles:changed', this.profiles);
|
this.emit("profiles:changed", this.profiles);
|
||||||
this.emit('profile:added', profile);
|
this.emit("profile:added", profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,8 +109,8 @@ class State {
|
|||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
this.profiles[idx] = profile;
|
this.profiles[idx] = profile;
|
||||||
}
|
}
|
||||||
this.emit('profiles:changed', this.profiles);
|
this.emit("profiles:changed", this.profiles);
|
||||||
this.emit('profile:updated', profile);
|
this.emit("profile:updated", profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,8 +119,8 @@ class State {
|
|||||||
*/
|
*/
|
||||||
removeProfile(id) {
|
removeProfile(id) {
|
||||||
this.profiles = this.profiles.filter((p) => p.id !== id);
|
this.profiles = this.profiles.filter((p) => p.id !== id);
|
||||||
this.emit('profiles:changed', this.profiles);
|
this.emit("profiles:changed", this.profiles);
|
||||||
this.emit('profile:removed', id);
|
this.emit("profile:removed", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,8 +140,8 @@ class State {
|
|||||||
*/
|
*/
|
||||||
addSession(sessionInfo) {
|
addSession(sessionInfo) {
|
||||||
this.sessions.set(sessionInfo.id, sessionInfo);
|
this.sessions.set(sessionInfo.id, sessionInfo);
|
||||||
this.emit('sessions:changed', this.sessions);
|
this.emit("sessions:changed", this.sessions);
|
||||||
this.emit('session:added', sessionInfo);
|
this.emit("session:added", sessionInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,8 +153,8 @@ class State {
|
|||||||
const existing = this.sessions.get(id);
|
const existing = this.sessions.get(id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
Object.assign(existing, updates);
|
Object.assign(existing, updates);
|
||||||
this.emit('sessions:changed', this.sessions);
|
this.emit("sessions:changed", this.sessions);
|
||||||
this.emit('session:updated', existing);
|
this.emit("session:updated", existing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,10 +167,11 @@ class State {
|
|||||||
if (this.activeSessionId === id) {
|
if (this.activeSessionId === id) {
|
||||||
// Activate the most recent remaining session, or null
|
// Activate the most recent remaining session, or null
|
||||||
const remaining = [...this.sessions.keys()];
|
const remaining = [...this.sessions.keys()];
|
||||||
this.activeSessionId = remaining.length > 0 ? remaining[remaining.length - 1] : null;
|
this.activeSessionId =
|
||||||
|
remaining.length > 0 ? remaining[remaining.length - 1] : null;
|
||||||
}
|
}
|
||||||
this.emit('sessions:changed', this.sessions);
|
this.emit("sessions:changed", this.sessions);
|
||||||
this.emit('session:removed', id);
|
this.emit("session:removed", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,7 +189,7 @@ class State {
|
|||||||
*/
|
*/
|
||||||
setActiveSession(id) {
|
setActiveSession(id) {
|
||||||
this.activeSessionId = id;
|
this.activeSessionId = id;
|
||||||
this.emit('session:activated', id);
|
this.emit("session:activated", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -207,7 +208,7 @@ class State {
|
|||||||
*/
|
*/
|
||||||
setPreferences(prefs) {
|
setPreferences(prefs) {
|
||||||
this.preferences = prefs;
|
this.preferences = prefs;
|
||||||
this.emit('preferences:changed', this.preferences);
|
this.emit("preferences:changed", this.preferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Secret store ─────────────────────────────────────────────────
|
// ── Secret store ─────────────────────────────────────────────────
|
||||||
@@ -218,7 +219,7 @@ class State {
|
|||||||
*/
|
*/
|
||||||
setSecretStoreState(unlocked) {
|
setSecretStoreState(unlocked) {
|
||||||
this.secretStoreUnlocked = unlocked;
|
this.secretStoreUnlocked = unlocked;
|
||||||
this.emit('secretStore:changed', unlocked);
|
this.emit("secretStore:changed", unlocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -227,11 +228,10 @@ class State {
|
|||||||
*/
|
*/
|
||||||
setSecretStoreInitialised(initialised) {
|
setSecretStoreInitialised(initialised) {
|
||||||
this.secretStoreInitialised = initialised;
|
this.secretStoreInitialised = initialised;
|
||||||
this.emit('secretStore:init', initialised);
|
this.emit("secretStore:init", initialised);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.NexusTerminal = window.NexusTerminal || {};
|
window.NexusTerminal = window.NexusTerminal || {};
|
||||||
window.NexusTerminal.State = State;
|
window.NexusTerminal.State = State;
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
+54
-16
@@ -574,6 +574,17 @@
|
|||||||
action: () => this._showPortForwardPanel(sessionId),
|
action: () => this._showPortForwardPanel(sessionId),
|
||||||
},
|
},
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
|
{
|
||||||
|
label: "Split Right",
|
||||||
|
shortcut: modKey + "⇧D",
|
||||||
|
action: () => this.app.splitPane("vertical"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Split Down",
|
||||||
|
shortcut: modKey + "⇧E",
|
||||||
|
action: () => this.app.splitPane("horizontal"),
|
||||||
|
},
|
||||||
|
{ divider: true },
|
||||||
{
|
{
|
||||||
label: "Export Output",
|
label: "Export Output",
|
||||||
action: () => this._exportTerminalOutput(sessionId, term),
|
action: () => this._exportTerminalOutput(sessionId, term),
|
||||||
@@ -1236,7 +1247,9 @@
|
|||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
this.app.notifications.success("Terminal output exported & copied to clipboard");
|
this.app.notifications.success(
|
||||||
|
"Terminal output exported & copied to clipboard",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1248,7 +1261,9 @@
|
|||||||
const { createElement, escapeHtml } = window.NexusTerminal;
|
const { createElement, escapeHtml } = window.NexusTerminal;
|
||||||
const session = this.app.state.getSession(sessionId);
|
const session = this.app.state.getSession(sessionId);
|
||||||
if (!session || session.protocol !== "ssh") {
|
if (!session || session.protocol !== "ssh") {
|
||||||
this.app.notifications.error("Port forwarding is only available for SSH sessions");
|
this.app.notifications.error(
|
||||||
|
"Port forwarding is only available for SSH sessions",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1297,24 +1312,30 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
modal.querySelector(".modal-close").addEventListener("click", close);
|
modal.querySelector(".modal-close").addEventListener("click", close);
|
||||||
backdrop.addEventListener("click", (e) => { if (e.target === backdrop) close(); });
|
backdrop.addEventListener("click", (e) => {
|
||||||
|
if (e.target === backdrop) close();
|
||||||
|
});
|
||||||
|
|
||||||
const listEl = modal.querySelector(".portfwd-list");
|
const listEl = modal.querySelector(".portfwd-list");
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
const renderForwards = async () => {
|
const renderForwards = async () => {
|
||||||
try {
|
try {
|
||||||
const forwards = await window.go.api.Service.ListPortForwards(sessionId) || [];
|
const forwards =
|
||||||
|
(await window.go.api.Service.ListPortForwards(sessionId)) || [];
|
||||||
listEl.innerHTML = "";
|
listEl.innerHTML = "";
|
||||||
if (forwards.length === 0) {
|
if (forwards.length === 0) {
|
||||||
listEl.innerHTML = '<div style="color:var(--text-tertiary);font-size:var(--text-sm);padding:var(--space-2);">No active port forwards</div>';
|
listEl.innerHTML =
|
||||||
|
'<div style="color:var(--text-tertiary);font-size:var(--text-sm);padding:var(--space-2);">No active port forwards</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const fwd of forwards) {
|
for (const fwd of forwards) {
|
||||||
const row = createElement("div", "", {});
|
const row = createElement("div", "", {});
|
||||||
row.style.cssText = "display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) 0;border-bottom:1px solid var(--border-subtle);";
|
row.style.cssText =
|
||||||
|
"display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) 0;border-bottom:1px solid var(--border-subtle);";
|
||||||
const dirLabel = fwd.direction === "local" ? "L" : "R";
|
const dirLabel = fwd.direction === "local" ? "L" : "R";
|
||||||
const dirColor = fwd.direction === "local" ? "var(--accent)" : "var(--warning)";
|
const dirColor =
|
||||||
|
fwd.direction === "local" ? "var(--accent)" : "var(--warning)";
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<span style="font-size:var(--text-xs);font-weight:var(--weight-semibold);color:${dirColor};width:16px;">${dirLabel}</span>
|
<span style="font-size:var(--text-xs);font-weight:var(--weight-semibold);color:${dirColor};width:16px;">${dirLabel}</span>
|
||||||
<span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-primary);flex:1;">
|
<span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-primary);flex:1;">
|
||||||
@@ -1322,21 +1343,26 @@
|
|||||||
</span>
|
</span>
|
||||||
${fwd.label ? `<span style="font-size:var(--text-xs);color:var(--text-tertiary);">${escapeHtml(fwd.label)}</span>` : ""}
|
${fwd.label ? `<span style="font-size:var(--text-xs);color:var(--text-tertiary);">${escapeHtml(fwd.label)}</span>` : ""}
|
||||||
`;
|
`;
|
||||||
const stopBtn = createElement("button", "btn btn-xs btn-danger", { textContent: "Stop" });
|
const stopBtn = createElement("button", "btn btn-xs btn-danger", {
|
||||||
|
textContent: "Stop",
|
||||||
|
});
|
||||||
stopBtn.addEventListener("click", async () => {
|
stopBtn.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
await window.go.api.Service.StopPortForward(sessionId, fwd.id);
|
await window.go.api.Service.StopPortForward(sessionId, fwd.id);
|
||||||
self.app.notifications.info("Forward stopped");
|
self.app.notifications.info("Forward stopped");
|
||||||
renderForwards();
|
renderForwards();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
self.app.notifications.error("Failed to stop: " + (err.message || err));
|
self.app.notifications.error(
|
||||||
|
"Failed to stop: " + (err.message || err),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
row.appendChild(stopBtn);
|
row.appendChild(stopBtn);
|
||||||
listEl.appendChild(row);
|
listEl.appendChild(row);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
listEl.innerHTML = '<div style="color:var(--error);font-size:var(--text-sm);">Failed to load forwards</div>';
|
listEl.innerHTML =
|
||||||
|
'<div style="color:var(--error);font-size:var(--text-sm);">Failed to load forwards</div>';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1345,14 +1371,24 @@
|
|||||||
// Add forward button
|
// Add forward button
|
||||||
modal.querySelector(".pf-add-btn").addEventListener("click", async () => {
|
modal.querySelector(".pf-add-btn").addEventListener("click", async () => {
|
||||||
const direction = modal.querySelector(".pf-direction").value;
|
const direction = modal.querySelector(".pf-direction").value;
|
||||||
const bindHost = modal.querySelector(".pf-bind-host").value.trim() || "127.0.0.1";
|
const bindHost =
|
||||||
const bindPort = parseInt(modal.querySelector(".pf-bind-port").value, 10);
|
modal.querySelector(".pf-bind-host").value.trim() || "127.0.0.1";
|
||||||
const destHost = modal.querySelector(".pf-dest-host").value.trim() || "localhost";
|
const bindPort = parseInt(
|
||||||
const destPort = parseInt(modal.querySelector(".pf-dest-port").value, 10);
|
modal.querySelector(".pf-bind-port").value,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
const destHost =
|
||||||
|
modal.querySelector(".pf-dest-host").value.trim() || "localhost";
|
||||||
|
const destPort = parseInt(
|
||||||
|
modal.querySelector(".pf-dest-port").value,
|
||||||
|
10,
|
||||||
|
);
|
||||||
const label = modal.querySelector(".pf-label").value.trim();
|
const label = modal.querySelector(".pf-label").value.trim();
|
||||||
|
|
||||||
if (!bindPort || !destPort) {
|
if (!bindPort || !destPort) {
|
||||||
self.app.notifications.error("Bind port and destination port are required");
|
self.app.notifications.error(
|
||||||
|
"Bind port and destination port are required",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1373,7 +1409,9 @@
|
|||||||
modal.querySelector(".pf-label").value = "";
|
modal.querySelector(".pf-label").value = "";
|
||||||
renderForwards();
|
renderForwards();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
self.app.notifications.error("Failed to start forward: " + (err.message || err));
|
self.app.notifications.error(
|
||||||
|
"Failed to start forward: " + (err.message || err),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user