vibe-code termjs
This commit is contained in:
parent
bf697bd98e
commit
d11e71e7a6
|
|
@ -9,7 +9,8 @@
|
|||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"electron-squirrel-startup": "^1.0.1"
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"node-pty": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
|
|
@ -8895,6 +8896,12 @@
|
|||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.23.1",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.1.tgz",
|
||||
"integrity": "sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
|
|
@ -8989,6 +8996,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-pty": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
|
||||
"integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nan": "^2.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
"vite": "^7.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-squirrel-startup": "^1.0.1"
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"node-pty": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ export class Terminal implements Displayable {
|
|||
currentTitle: string = "Terminal";
|
||||
del: () => void;
|
||||
dom: HTMLElement;
|
||||
private terminalId: string | null = null;
|
||||
private fitAddon: FitAddon;
|
||||
private resizeObserver: ResizeObserver;
|
||||
private unsubTerminalData?: () => void;
|
||||
private unsubTerminalExit?: () => void;
|
||||
|
||||
setDeleteFunction(del: () => void): void {
|
||||
this.del = del;
|
||||
|
|
@ -19,28 +24,104 @@ export class Terminal implements Displayable {
|
|||
}
|
||||
|
||||
constructor() {
|
||||
this.term = new xterm.Terminal();
|
||||
this.term = new xterm.Terminal({
|
||||
// cursorBlink: true,
|
||||
// fontSize: 14,
|
||||
// fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
||||
});
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
this.term.loadAddon(fitAddon);
|
||||
this.fitAddon = new FitAddon();
|
||||
this.term.loadAddon(this.fitAddon);
|
||||
|
||||
this.dom = v.div({ class: "h-full w-full" });
|
||||
this.dom = v.div({ class: "h-full w-lg resize-x overflow-x-hidden" });
|
||||
const loaded = van.state(false);
|
||||
|
||||
van.derive(() => {
|
||||
if (loaded.val) {
|
||||
this.term.open(this.dom);
|
||||
fitAddon.fit();
|
||||
this.term.writeln("Welcome to the terminal!");
|
||||
this.initializeTerminal();
|
||||
}
|
||||
});
|
||||
loaded.val = true;
|
||||
}
|
||||
|
||||
private async initializeTerminal() {
|
||||
this.term.open(this.dom);
|
||||
|
||||
// Create terminal in main process
|
||||
try {
|
||||
this.terminalId = await window.electronAPI.createTerminal();
|
||||
|
||||
// Set up data handling (subscribe/unsubscribe)
|
||||
this.unsubTerminalData = window.electronAPI.onTerminalData(
|
||||
(id, data) => {
|
||||
if (id === this.terminalId) {
|
||||
this.term.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.unsubTerminalExit = window.electronAPI.onTerminalExit(
|
||||
(id, exitCode, signal) => {
|
||||
if (id === this.terminalId) {
|
||||
this.term.writeln(
|
||||
`\r\n[Process exited with code ${exitCode}]`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Handle user input
|
||||
this.term.onData((data) => {
|
||||
if (this.terminalId) {
|
||||
window.electronAPI.writeToTerminal(this.terminalId, data);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up resize handling
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.handleResize();
|
||||
});
|
||||
this.resizeObserver.observe(this.dom);
|
||||
|
||||
// Initial fit
|
||||
setTimeout(() => {
|
||||
this.handleResize();
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize terminal:", error);
|
||||
this.term.writeln(
|
||||
"Failed to initialize terminal. Check console for details.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleResize() {
|
||||
if (
|
||||
this.terminalId &&
|
||||
this.dom.clientWidth > 0 &&
|
||||
this.dom.clientHeight > 0
|
||||
) {
|
||||
console.log("Old size: ", this.term.rows, this.term.cols);
|
||||
this.fitAddon.fit();
|
||||
const { cols, rows } = this.term;
|
||||
console.log("New size: ", rows, cols);
|
||||
window.electronAPI.resizeTerminal(this.terminalId, cols, rows);
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.term.focus();
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.terminalId) {
|
||||
window.electronAPI.closeTerminal(this.terminalId);
|
||||
}
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
if (this.unsubTerminalData) this.unsubTerminalData();
|
||||
if (this.unsubTerminalExit) this.unsubTerminalExit();
|
||||
this.term.dispose();
|
||||
this.del();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
/// <reference types="@electron-forge/plugin-vite/vite-env" />
|
||||
|
||||
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
|
||||
declare const MAIN_WINDOW_VITE_NAME: string;
|
||||
|
|
@ -8,8 +8,10 @@ import {
|
|||
getOpenedFiles,
|
||||
showConfirmDialog,
|
||||
} from "./fileOperations";
|
||||
import { terminalManager } from "./pty";
|
||||
import path from "node:path";
|
||||
import started from "electron-squirrel-startup";
|
||||
/// <reference types="./forge-vite-env.d.ts" />
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
if (started) {
|
||||
|
|
@ -94,6 +96,51 @@ app.whenReady().then(() => {
|
|||
},
|
||||
);
|
||||
|
||||
// Terminal handlers
|
||||
ipcMain.handle(
|
||||
"terminal:create",
|
||||
async (event, shell?: string, args?: string[]) => {
|
||||
const terminalId = terminalManager.createTerminal(shell, args);
|
||||
|
||||
// Set up data forwarding to the renderer
|
||||
terminalManager.on("terminal-data", (id, data) => {
|
||||
if (id === terminalId) {
|
||||
event.sender.send("terminal:data", id, data);
|
||||
}
|
||||
});
|
||||
|
||||
terminalManager.on("terminal-exit", (id, exitCode, signal) => {
|
||||
if (id === terminalId) {
|
||||
event.sender.send("terminal:exit", id, exitCode, signal);
|
||||
}
|
||||
});
|
||||
|
||||
return terminalId;
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"terminal:resize",
|
||||
async (event, id: string, cols: number, rows: number) => {
|
||||
return terminalManager.resizeTerminal(id, cols, rows);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"terminal:write",
|
||||
async (event, id: string, data: string) => {
|
||||
return terminalManager.writeToTerminal(id, data);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle("terminal:close", async (event, id: string) => {
|
||||
return terminalManager.closeTerminal(id);
|
||||
});
|
||||
|
||||
ipcMain.handle("terminal:list", async () => {
|
||||
return terminalManager.listTerminals();
|
||||
});
|
||||
|
||||
createWindow();
|
||||
if (process.platform === "darwin") {
|
||||
app.on("activate", function () {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import * as pty from "node-pty";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
export interface TerminalInstance {
|
||||
ptyProcess: pty.IPty;
|
||||
}
|
||||
|
||||
export class TerminalManager extends EventEmitter {
|
||||
private terminals: Map<string, TerminalInstance> = new Map();
|
||||
private nextId: number = 1;
|
||||
|
||||
createTerminal(shell?: string, args?: string[]): string {
|
||||
const id = `terminal-${this.nextId++}`;
|
||||
|
||||
// Default shell based on platform
|
||||
const defaultShell =
|
||||
process.platform === "win32" ? "powershell.exe" : "/bin/bash";
|
||||
const shellToUse = shell || defaultShell;
|
||||
|
||||
const ptyProcess = pty.spawn(shellToUse, args || [], {
|
||||
name: "xterm-color",
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
const terminal: TerminalInstance = {
|
||||
ptyProcess,
|
||||
};
|
||||
|
||||
ptyProcess.onData((data) => {
|
||||
this.emit("terminal-data", id, data);
|
||||
});
|
||||
|
||||
ptyProcess.onExit(({ exitCode, signal }) => {
|
||||
this.emit("terminal-exit", id, exitCode, signal);
|
||||
this.terminals.delete(id);
|
||||
});
|
||||
|
||||
this.terminals.set(id, terminal);
|
||||
return id;
|
||||
}
|
||||
|
||||
getTerminal(id: string): TerminalInstance | undefined {
|
||||
return this.terminals.get(id);
|
||||
}
|
||||
|
||||
resizeTerminal(id: string, cols: number, rows: number): boolean {
|
||||
const terminal = this.terminals.get(id);
|
||||
if (terminal) {
|
||||
terminal.ptyProcess.resize(cols, rows);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
writeToTerminal(id: string, data: string): boolean {
|
||||
const terminal = this.terminals.get(id);
|
||||
if (terminal) {
|
||||
terminal.ptyProcess.write(data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
closeTerminal(id: string): boolean {
|
||||
const terminal = this.terminals.get(id);
|
||||
if (terminal) {
|
||||
terminal.ptyProcess.kill();
|
||||
this.terminals.delete(id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
listTerminals(): string[] {
|
||||
return Array.from(this.terminals.keys());
|
||||
}
|
||||
}
|
||||
|
||||
export const terminalManager = new TerminalManager();
|
||||
|
|
@ -4,6 +4,26 @@
|
|||
import { contextBridge, ipcRenderer } from "electron";
|
||||
import type { FolderTree } from "./types/global";
|
||||
|
||||
// Centralized routing for terminal events: keep a single ipcRenderer listener
|
||||
// and forward events to subscribed callbacks. Each `onTerminal*` returns an
|
||||
// unsubscribe function so individual renderer components can remove only
|
||||
// their own listeners.
|
||||
const terminalDataCallbacks = new Set<(id: string, data: string) => void>();
|
||||
const terminalExitCallbacks = new Set<
|
||||
(id: string, exitCode: number, signal: string) => void
|
||||
>();
|
||||
|
||||
ipcRenderer.on("terminal:data", (_ev, id: string, data: string) => {
|
||||
for (const cb of terminalDataCallbacks) cb(id, data);
|
||||
});
|
||||
|
||||
ipcRenderer.on(
|
||||
"terminal:exit",
|
||||
(_ev, id: string, exitCode: number, signal: string) => {
|
||||
for (const cb of terminalExitCallbacks) cb(id, exitCode, signal);
|
||||
},
|
||||
);
|
||||
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
openFolder: () =>
|
||||
ipcRenderer.invoke("dialog:openFolder") as Promise<FolderTree | null>,
|
||||
|
|
@ -43,4 +63,38 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||
title,
|
||||
buttons,
|
||||
) as Promise<string>,
|
||||
|
||||
// Terminal operations
|
||||
createTerminal: (shell?: string, args?: string[]) =>
|
||||
ipcRenderer.invoke("terminal:create", shell, args) as Promise<string>,
|
||||
|
||||
resizeTerminal: (id: string, cols: number, rows: number) =>
|
||||
ipcRenderer.invoke(
|
||||
"terminal:resize",
|
||||
id,
|
||||
cols,
|
||||
rows,
|
||||
) as Promise<boolean>,
|
||||
|
||||
writeToTerminal: (id: string, data: string) =>
|
||||
ipcRenderer.invoke("terminal:write", id, data) as Promise<boolean>,
|
||||
|
||||
closeTerminal: (id: string) =>
|
||||
ipcRenderer.invoke("terminal:close", id) as Promise<boolean>,
|
||||
|
||||
listTerminals: () =>
|
||||
ipcRenderer.invoke("terminal:list") as Promise<string[]>,
|
||||
|
||||
// Terminal events (subscribe/unsubscribe)
|
||||
onTerminalData: (callback: (id: string, data: string) => void) => {
|
||||
terminalDataCallbacks.add(callback);
|
||||
return () => terminalDataCallbacks.delete(callback);
|
||||
},
|
||||
|
||||
onTerminalExit: (
|
||||
callback: (id: string, exitCode: number, signal: string) => void,
|
||||
) => {
|
||||
terminalExitCallbacks.add(callback);
|
||||
return () => terminalExitCallbacks.delete(callback);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,6 +34,31 @@ declare global {
|
|||
title: string,
|
||||
buttons: string[],
|
||||
) => Promise<string>;
|
||||
|
||||
// Terminal operations
|
||||
createTerminal: (
|
||||
shell?: string,
|
||||
args?: string[],
|
||||
) => Promise<string>;
|
||||
resizeTerminal: (
|
||||
id: string,
|
||||
cols: number,
|
||||
rows: number,
|
||||
) => Promise<boolean>;
|
||||
writeToTerminal: (id: string, data: string) => Promise<boolean>;
|
||||
closeTerminal: (id: string) => Promise<boolean>;
|
||||
listTerminals: () => Promise<string[]>;
|
||||
onTerminalData: (
|
||||
callback: (id: string, data: string) => void,
|
||||
) => () => void;
|
||||
onTerminalExit: (
|
||||
callback: (
|
||||
id: string,
|
||||
exitCode: number,
|
||||
signal: string,
|
||||
) => void,
|
||||
) => () => void;
|
||||
removeAllTerminalListeners: () => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,12 @@ import tailwindcss from "@tailwindcss/vite";
|
|||
// https://vitejs.dev/config
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss()],
|
||||
optimizeDeps: {
|
||||
exclude: ["node-pty"],
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ["node-pty"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue