refactor styles

This commit is contained in:
Amrit Subramanian 2025-10-02 18:33:22 -04:00
parent 7fd4968a35
commit 66e0c96b75
4 changed files with 192 additions and 107 deletions

View file

@ -16,21 +16,21 @@
<body class="h-screen overflow-hidden bg-gray-900"> <body class="h-screen overflow-hidden bg-gray-900">
<div class="flex h-full"> <div class="flex h-full">
<!-- Sidebar --> <!-- Sidebar -->
<div class="w-64 bg-gray-800 border-r border-gray-700 flex flex-col"> <div class="sidebar">
<div class="p-4 border-b border-gray-700"> <div class="sidebar-header">
<h1 class="text-white font-semibold text-lg">FleetCode</h1> <h1 class="sidebar-title">FleetCode</h1>
</div> </div>
<div class="flex-1 overflow-y-auto"> <div class="sidebar-content">
<div class="p-2"> <div class="sidebar-section">
<div class="text-xs text-gray-400 px-2 py-1 mb-1">TERMINALS</div> <div class="sidebar-section-title">SESSIONS</div>
<div id="terminal-list"></div> <div id="session-list"></div>
</div> </div>
</div> </div>
<div class="p-3 border-t border-gray-700"> <div class="sidebar-footer">
<button id="new-terminal" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm font-medium transition"> <button id="new-session" class="btn-primary">
+ New Terminal + New Session
</button> </button>
</div> </div>
</div> </div>
@ -38,10 +38,10 @@
<!-- Main content --> <!-- Main content -->
<div class="flex-1 flex flex-col bg-black"> <div class="flex-1 flex flex-col bg-black">
<!-- Tabs --> <!-- Tabs -->
<div id="tabs" class="flex bg-gray-900 border-b border-gray-700 overflow-x-auto"></div> <div id="tabs" class="tabs-container"></div>
<!-- Terminal container --> <!-- Session container -->
<div id="terminal-container" class="flex-1 relative"></div> <div id="session-container" class="flex-1 relative"></div>
</div> </div>
</div> </div>

46
main.ts
View file

@ -3,11 +3,11 @@ import * as pty from "node-pty";
import * as os from "os"; import * as os from "os";
let mainWindow: BrowserWindow; let mainWindow: BrowserWindow;
const terminals = new Map<string, pty.IPty>(); const sessions = new Map<string, pty.IPty>();
// Create new terminal // Create new session
ipcMain.on("create-terminal", (event) => { ipcMain.on("create-session", (event) => {
const terminalId = `terminal-${Date.now()}`; const sessionId = `session-${Date.now()}`;
const shell = os.platform() === "darwin" ? "zsh" : "bash"; const shell = os.platform() === "darwin" ? "zsh" : "bash";
const ptyProcess = pty.spawn(shell, [], { const ptyProcess = pty.spawn(shell, [], {
@ -18,37 +18,37 @@ ipcMain.on("create-terminal", (event) => {
env: process.env, env: process.env,
}); });
terminals.set(terminalId, ptyProcess); sessions.set(sessionId, ptyProcess);
ptyProcess.onData((data) => { ptyProcess.onData((data) => {
mainWindow.webContents.send("terminal-output", terminalId, data); mainWindow.webContents.send("session-output", sessionId, data);
}); });
event.reply("terminal-created", terminalId); event.reply("session-created", sessionId);
}); });
// Handle terminal input // Handle session input
ipcMain.on("terminal-input", (_event, terminalId: string, data: string) => { ipcMain.on("session-input", (_event, sessionId: string, data: string) => {
const terminal = terminals.get(terminalId); const session = sessions.get(sessionId);
if (terminal) { if (session) {
terminal.write(data); session.write(data);
} }
}); });
// Handle terminal resize // Handle session resize
ipcMain.on("terminal-resize", (_event, terminalId: string, cols: number, rows: number) => { ipcMain.on("session-resize", (_event, sessionId: string, cols: number, rows: number) => {
const terminal = terminals.get(terminalId); const session = sessions.get(sessionId);
if (terminal) { if (session) {
terminal.resize(cols, rows); session.resize(cols, rows);
} }
}); });
// Handle terminal close // Handle session close
ipcMain.on("close-terminal", (_event, terminalId: string) => { ipcMain.on("close-session", (_event, sessionId: string) => {
const terminal = terminals.get(terminalId); const session = sessions.get(sessionId);
if (terminal) { if (session) {
terminal.kill(); session.kill();
terminals.delete(terminalId); sessions.delete(sessionId);
} }
}); });

View file

@ -2,7 +2,7 @@ import { ipcRenderer } from "electron";
import { Terminal } from "xterm"; import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit"; import { FitAddon } from "xterm-addon-fit";
interface TerminalSession { interface Session {
id: string; id: string;
terminal: Terminal; terminal: Terminal;
fitAddon: FitAddon; fitAddon: FitAddon;
@ -10,10 +10,10 @@ interface TerminalSession {
name: string; name: string;
} }
const sessions = new Map<string, TerminalSession>(); const sessions = new Map<string, Session>();
let activeTerminalId: string | null = null; let activeSessionId: string | null = null;
function createTerminalSession(terminalId: string, name: string) { function createSession(sessionId: string, name: string) {
const term = new Terminal({ const term = new Terminal({
cursorBlink: true, cursorBlink: true,
fontSize: 14, fontSize: 14,
@ -27,46 +27,46 @@ function createTerminalSession(terminalId: string, name: string) {
const fitAddon = new FitAddon(); const fitAddon = new FitAddon();
term.loadAddon(fitAddon); term.loadAddon(fitAddon);
const terminalElement = document.createElement("div"); const sessionElement = document.createElement("div");
terminalElement.className = "absolute inset-0 hidden"; sessionElement.className = "session-wrapper";
terminalElement.id = `term-${terminalId}`; sessionElement.id = `session-${sessionId}`;
const container = document.getElementById("terminal-container"); const container = document.getElementById("session-container");
if (container) { if (container) {
container.appendChild(terminalElement); container.appendChild(sessionElement);
} }
term.open(terminalElement); term.open(sessionElement);
fitAddon.fit(); fitAddon.fit();
term.onData((data) => { term.onData((data) => {
ipcRenderer.send("terminal-input", terminalId, data); ipcRenderer.send("session-input", sessionId, data);
}); });
const session: TerminalSession = { const session: Session = {
id: terminalId, id: sessionId,
terminal: term, terminal: term,
fitAddon, fitAddon,
element: terminalElement, element: sessionElement,
name, name,
}; };
sessions.set(terminalId, session); sessions.set(sessionId, session);
// Add to sidebar // Add to sidebar
addToSidebar(terminalId, name); addToSidebar(sessionId, name);
// Add tab // Add tab
addTab(terminalId, name); addTab(sessionId, name);
// Switch to this terminal // Switch to this session
switchToTerminal(terminalId); switchToSession(sessionId);
// Handle resize // Handle resize
const resizeHandler = () => { const resizeHandler = () => {
if (activeTerminalId === terminalId) { if (activeSessionId === sessionId) {
fitAddon.fit(); fitAddon.fit();
ipcRenderer.send("terminal-resize", terminalId, term.cols, term.rows); ipcRenderer.send("session-resize", sessionId, term.cols, term.rows);
} }
}; };
window.addEventListener("resize", resizeHandler); window.addEventListener("resize", resizeHandler);
@ -74,127 +74,127 @@ function createTerminalSession(terminalId: string, name: string) {
return session; return session;
} }
function addToSidebar(terminalId: string, name: string) { function addToSidebar(sessionId: string, name: string) {
const list = document.getElementById("terminal-list"); const list = document.getElementById("session-list");
if (!list) return; if (!list) return;
const item = document.createElement("div"); const item = document.createElement("div");
item.id = `sidebar-${terminalId}`; item.id = `sidebar-${sessionId}`;
item.className = "px-3 py-2 rounded cursor-pointer hover:bg-gray-700 text-gray-300 text-sm flex items-center justify-between group"; item.className = "session-list-item";
item.innerHTML = ` item.innerHTML = `
<span class="truncate">${name}</span> <span class="truncate">${name}</span>
<button class="close-btn opacity-0 group-hover:opacity-100 text-gray-500 hover:text-red-500 ml-2" data-id="${terminalId}">×</button> <button class="session-close-btn" data-id="${sessionId}">×</button>
`; `;
item.addEventListener("click", (e) => { item.addEventListener("click", (e) => {
if (!(e.target as HTMLElement).classList.contains("close-btn")) { if (!(e.target as HTMLElement).classList.contains("session-close-btn")) {
switchToTerminal(terminalId); switchToSession(sessionId);
} }
}); });
const closeBtn = item.querySelector(".close-btn"); const closeBtn = item.querySelector(".session-close-btn");
closeBtn?.addEventListener("click", (e) => { closeBtn?.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
closeTerminal(terminalId); closeSession(sessionId);
}); });
list.appendChild(item); list.appendChild(item);
} }
function addTab(terminalId: string, name: string) { function addTab(sessionId: string, name: string) {
const tabsContainer = document.getElementById("tabs"); const tabsContainer = document.getElementById("tabs");
if (!tabsContainer) return; if (!tabsContainer) return;
const tab = document.createElement("div"); const tab = document.createElement("div");
tab.id = `tab-${terminalId}`; tab.id = `tab-${sessionId}`;
tab.className = "px-4 py-2 border-r border-gray-700 cursor-pointer hover:bg-gray-800 flex items-center space-x-2 min-w-max"; tab.className = "tab";
tab.innerHTML = ` tab.innerHTML = `
<span class="text-sm text-gray-300">${name}</span> <span class="tab-name">${name}</span>
<button class="close-tab-btn text-gray-500 hover:text-red-500" data-id="${terminalId}">×</button> <button class="tab-close-btn" data-id="${sessionId}">×</button>
`; `;
tab.addEventListener("click", (e) => { tab.addEventListener("click", (e) => {
if (!(e.target as HTMLElement).classList.contains("close-tab-btn")) { if (!(e.target as HTMLElement).classList.contains("tab-close-btn")) {
switchToTerminal(terminalId); switchToSession(sessionId);
} }
}); });
const closeBtn = tab.querySelector(".close-tab-btn"); const closeBtn = tab.querySelector(".tab-close-btn");
closeBtn?.addEventListener("click", (e) => { closeBtn?.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
closeTerminal(terminalId); closeSession(sessionId);
}); });
tabsContainer.appendChild(tab); tabsContainer.appendChild(tab);
} }
function switchToTerminal(terminalId: string) { function switchToSession(sessionId: string) {
// Hide all terminals // Hide all sessions
sessions.forEach((session, id) => { sessions.forEach((session, id) => {
session.element.classList.add("hidden"); session.element.classList.remove("active");
document.getElementById(`tab-${id}`)?.classList.remove("bg-gray-800", "border-b-2", "border-blue-500"); document.getElementById(`tab-${id}`)?.classList.remove("active");
document.getElementById(`sidebar-${id}`)?.classList.remove("bg-gray-700"); document.getElementById(`sidebar-${id}`)?.classList.remove("active");
}); });
// Show active terminal // Show active session
const session = sessions.get(terminalId); const session = sessions.get(sessionId);
if (session) { if (session) {
session.element.classList.remove("hidden"); session.element.classList.add("active");
document.getElementById(`tab-${terminalId}`)?.classList.add("bg-gray-800", "border-b-2", "border-blue-500"); document.getElementById(`tab-${sessionId}`)?.classList.add("active");
document.getElementById(`sidebar-${terminalId}`)?.classList.add("bg-gray-700"); document.getElementById(`sidebar-${sessionId}`)?.classList.add("active");
activeTerminalId = terminalId; activeSessionId = sessionId;
// Focus and resize // Focus and resize
session.terminal.focus(); session.terminal.focus();
setTimeout(() => { setTimeout(() => {
session.fitAddon.fit(); session.fitAddon.fit();
ipcRenderer.send("terminal-resize", terminalId, session.terminal.cols, session.terminal.rows); ipcRenderer.send("session-resize", sessionId, session.terminal.cols, session.terminal.rows);
}, 0); }, 0);
} }
} }
function closeTerminal(terminalId: string) { function closeSession(sessionId: string) {
const session = sessions.get(terminalId); const session = sessions.get(sessionId);
if (!session) return; if (!session) return;
// Remove from UI // Remove from UI
session.element.remove(); session.element.remove();
document.getElementById(`tab-${terminalId}`)?.remove(); document.getElementById(`tab-${sessionId}`)?.remove();
document.getElementById(`sidebar-${terminalId}`)?.remove(); document.getElementById(`sidebar-${sessionId}`)?.remove();
// Dispose terminal // Dispose terminal
session.terminal.dispose(); session.terminal.dispose();
sessions.delete(terminalId); sessions.delete(sessionId);
// Close in main process // Close in main process
ipcRenderer.send("close-terminal", terminalId); ipcRenderer.send("close-session", sessionId);
// Switch to another terminal // Switch to another session
if (activeTerminalId === terminalId) { if (activeSessionId === sessionId) {
const remainingSessions = Array.from(sessions.keys()); const remainingSessions = Array.from(sessions.keys());
if (remainingSessions.length > 0) { if (remainingSessions.length > 0) {
switchToTerminal(remainingSessions[0]); switchToSession(remainingSessions[0]);
} else { } else {
activeTerminalId = null; activeSessionId = null;
} }
} }
} }
// Handle terminal output // Handle session output
ipcRenderer.on("terminal-output", (_event, terminalId: string, data: string) => { ipcRenderer.on("session-output", (_event, sessionId: string, data: string) => {
const session = sessions.get(terminalId); const session = sessions.get(sessionId);
if (session) { if (session) {
session.terminal.write(data); session.terminal.write(data);
} }
}); });
// Handle terminal created // Handle session created
ipcRenderer.on("terminal-created", (_event, terminalId: string) => { ipcRenderer.on("session-created", (_event, sessionId: string) => {
const name = `Terminal ${sessions.size + 1}`; const name = `Session ${sessions.size + 1}`;
createTerminalSession(terminalId, name); createSession(sessionId, name);
}); });
// New terminal button // New session button
document.getElementById("new-terminal")?.addEventListener("click", () => { document.getElementById("new-session")?.addEventListener("click", () => {
ipcRenderer.send("create-terminal"); ipcRenderer.send("create-session");
}); });

View file

@ -1,3 +1,88 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer components {
/* Sidebar Styles */
.sidebar {
@apply w-64 bg-gray-800 border-r border-gray-700 flex flex-col;
}
.sidebar-header {
@apply p-4 border-b border-gray-700;
}
.sidebar-title {
@apply text-white font-semibold text-lg;
}
.sidebar-content {
@apply flex-1 overflow-y-auto;
}
.sidebar-section {
@apply p-2;
}
.sidebar-section-title {
@apply text-xs text-gray-400 px-2 py-1 mb-1;
}
.sidebar-footer {
@apply p-3 border-t border-gray-700;
}
/* Session List Item */
.session-list-item {
@apply px-3 py-2 rounded cursor-pointer hover:bg-gray-700 text-gray-300 text-sm flex items-center justify-between;
}
.session-list-item.active {
@apply bg-gray-700;
}
.session-close-btn {
@apply text-gray-500 hover:text-red-500 ml-2;
opacity: 0;
transition: opacity 0.2s;
}
.session-list-item:hover .session-close-btn {
opacity: 1;
}
/* Tabs */
.tabs-container {
@apply flex bg-gray-900 border-b border-gray-700 overflow-x-auto;
}
.tab {
@apply px-4 py-2 border-r border-gray-700 cursor-pointer hover:bg-gray-800 flex items-center space-x-2 min-w-max;
}
.tab.active {
@apply bg-gray-800 border-b-2 border-blue-500;
}
.tab-name {
@apply text-sm text-gray-300;
}
.tab-close-btn {
@apply text-gray-500 hover:text-red-500;
}
/* Button */
.btn-primary {
@apply w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm font-medium transition;
}
/* Session Container */
.session-wrapper {
@apply absolute inset-0 hidden;
}
.session-wrapper.active {
@apply block;
}
}