refactor styles
This commit is contained in:
parent
7fd4968a35
commit
66e0c96b75
26
index.html
26
index.html
|
|
@ -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
46
main.ts
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
142
renderer.ts
142
renderer.ts
|
|
@ -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");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
85
styles.css
85
styles.css
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue