201 lines
5.1 KiB
TypeScript
201 lines
5.1 KiB
TypeScript
import { ipcRenderer } from "electron";
|
||
import { Terminal } from "xterm";
|
||
import { FitAddon } from "xterm-addon-fit";
|
||
|
||
interface Session {
|
||
id: string;
|
||
terminal: Terminal;
|
||
fitAddon: FitAddon;
|
||
element: HTMLDivElement;
|
||
name: string;
|
||
}
|
||
|
||
const sessions = new Map<string, Session>();
|
||
let activeSessionId: string | null = null;
|
||
|
||
function createSession(sessionId: string, name: string) {
|
||
const term = new Terminal({
|
||
cursorBlink: true,
|
||
fontSize: 14,
|
||
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
||
theme: {
|
||
background: "#000000",
|
||
foreground: "#ffffff",
|
||
},
|
||
});
|
||
|
||
const fitAddon = new FitAddon();
|
||
term.loadAddon(fitAddon);
|
||
|
||
const sessionElement = document.createElement("div");
|
||
sessionElement.className = "session-wrapper";
|
||
sessionElement.id = `session-${sessionId}`;
|
||
|
||
const container = document.getElementById("session-container");
|
||
if (container) {
|
||
container.appendChild(sessionElement);
|
||
}
|
||
|
||
term.open(sessionElement);
|
||
fitAddon.fit();
|
||
|
||
term.onData((data) => {
|
||
ipcRenderer.send("session-input", sessionId, data);
|
||
});
|
||
|
||
const session: Session = {
|
||
id: sessionId,
|
||
terminal: term,
|
||
fitAddon,
|
||
element: sessionElement,
|
||
name,
|
||
};
|
||
|
||
sessions.set(sessionId, session);
|
||
|
||
// Add to sidebar
|
||
addToSidebar(sessionId, name);
|
||
|
||
// Add tab
|
||
addTab(sessionId, name);
|
||
|
||
// Switch to this session
|
||
switchToSession(sessionId);
|
||
|
||
// Handle resize
|
||
const resizeHandler = () => {
|
||
if (activeSessionId === sessionId) {
|
||
fitAddon.fit();
|
||
ipcRenderer.send("session-resize", sessionId, term.cols, term.rows);
|
||
}
|
||
};
|
||
window.addEventListener("resize", resizeHandler);
|
||
|
||
return session;
|
||
}
|
||
|
||
function addToSidebar(sessionId: string, name: string) {
|
||
const list = document.getElementById("session-list");
|
||
if (!list) return;
|
||
|
||
const item = document.createElement("div");
|
||
item.id = `sidebar-${sessionId}`;
|
||
item.className = "session-list-item";
|
||
item.innerHTML = `
|
||
<span class="truncate">${name}</span>
|
||
<button class="session-close-btn" data-id="${sessionId}">×</button>
|
||
`;
|
||
|
||
item.addEventListener("click", (e) => {
|
||
if (!(e.target as HTMLElement).classList.contains("session-close-btn")) {
|
||
switchToSession(sessionId);
|
||
}
|
||
});
|
||
|
||
const closeBtn = item.querySelector(".session-close-btn");
|
||
closeBtn?.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
closeSession(sessionId);
|
||
});
|
||
|
||
list.appendChild(item);
|
||
}
|
||
|
||
function addTab(sessionId: string, name: string) {
|
||
const tabsContainer = document.getElementById("tabs");
|
||
if (!tabsContainer) return;
|
||
|
||
const tab = document.createElement("div");
|
||
tab.id = `tab-${sessionId}`;
|
||
tab.className = "tab";
|
||
tab.innerHTML = `
|
||
<span class="tab-name">${name}</span>
|
||
<button class="tab-close-btn" data-id="${sessionId}">×</button>
|
||
`;
|
||
|
||
tab.addEventListener("click", (e) => {
|
||
if (!(e.target as HTMLElement).classList.contains("tab-close-btn")) {
|
||
switchToSession(sessionId);
|
||
}
|
||
});
|
||
|
||
const closeBtn = tab.querySelector(".tab-close-btn");
|
||
closeBtn?.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
closeSession(sessionId);
|
||
});
|
||
|
||
tabsContainer.appendChild(tab);
|
||
}
|
||
|
||
function switchToSession(sessionId: string) {
|
||
// Hide all sessions
|
||
sessions.forEach((session, id) => {
|
||
session.element.classList.remove("active");
|
||
document.getElementById(`tab-${id}`)?.classList.remove("active");
|
||
document.getElementById(`sidebar-${id}`)?.classList.remove("active");
|
||
});
|
||
|
||
// Show active session
|
||
const session = sessions.get(sessionId);
|
||
if (session) {
|
||
session.element.classList.add("active");
|
||
document.getElementById(`tab-${sessionId}`)?.classList.add("active");
|
||
document.getElementById(`sidebar-${sessionId}`)?.classList.add("active");
|
||
activeSessionId = sessionId;
|
||
|
||
// Focus and resize
|
||
session.terminal.focus();
|
||
setTimeout(() => {
|
||
session.fitAddon.fit();
|
||
ipcRenderer.send("session-resize", sessionId, session.terminal.cols, session.terminal.rows);
|
||
}, 0);
|
||
}
|
||
}
|
||
|
||
function closeSession(sessionId: string) {
|
||
const session = sessions.get(sessionId);
|
||
if (!session) return;
|
||
|
||
// Remove from UI
|
||
session.element.remove();
|
||
document.getElementById(`tab-${sessionId}`)?.remove();
|
||
document.getElementById(`sidebar-${sessionId}`)?.remove();
|
||
|
||
// Dispose terminal
|
||
session.terminal.dispose();
|
||
sessions.delete(sessionId);
|
||
|
||
// Close in main process
|
||
ipcRenderer.send("close-session", sessionId);
|
||
|
||
// Switch to another session
|
||
if (activeSessionId === sessionId) {
|
||
const remainingSessions = Array.from(sessions.keys());
|
||
if (remainingSessions.length > 0) {
|
||
switchToSession(remainingSessions[0]);
|
||
} else {
|
||
activeSessionId = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Handle session output
|
||
ipcRenderer.on("session-output", (_event, sessionId: string, data: string) => {
|
||
const session = sessions.get(sessionId);
|
||
if (session) {
|
||
session.terminal.write(data);
|
||
}
|
||
});
|
||
|
||
// Handle session created
|
||
ipcRenderer.on("session-created", (_event, sessionId: string) => {
|
||
const name = `Session ${sessions.size + 1}`;
|
||
createSession(sessionId, name);
|
||
});
|
||
|
||
// New session button
|
||
document.getElementById("new-session")?.addEventListener("click", () => {
|
||
ipcRenderer.send("create-session");
|
||
});
|