Files
toolLooper/src/terminalService.js

124 lines
3.7 KiB
JavaScript

import EventEmitter from 'events';
import pty from 'node-pty';
class TerminalService extends EventEmitter {
constructor() {
super();
this.lines = [];
this.partial = '';
this.ptyProcess = null;
this.started = false;
this.maxLines = 1000;
}
start() {
if (this.started) return;
const shell = process.platform === 'win32' ? 'powershell.exe' : 'bash';
const cols = process.stdout && process.stdout.columns ? process.stdout.columns : 120;
const rows = process.stdout && process.stdout.rows ? process.stdout.rows : 30;
const isWindows = process.platform === 'win32';
const userShell = process.env.SHELL && !isWindows ? process.env.SHELL : null;
const shellPath = userShell || (isWindows ? 'powershell.exe' : '/bin/bash');
const args = ['--rcfile','rc'];
this.ptyProcess = pty.spawn(shellPath, args, {
name: 'xterm-256color',
cols,
rows,
cwd: process.cwd(),
env: {
...process.env,
TERM: 'xterm-256color',
PS1: '> '
},
});
this.ptyProcess.onData((data) => {
// Normalize line endings
const normalized = String(data).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const parts = normalized.split('\n');
// First part joins the existing partial
if (parts.length > 0) {
this.partial += parts[0];
}
// For each subsequent part before the last, we have completed lines
for (let i = 1; i < parts.length; i += 1) {
this.lines.push(this.partial);
this.partial = parts[i];
}
// Enforce max lines buffer
if (this.lines.length > this.maxLines) {
this.lines.splice(0, this.lines.length - this.maxLines);
}
// Emit lines including current partial to ensure prompts (no trailing newline) are visible
const display = this.partial ? [...this.lines, this.partial] : this.lines.slice();
this.emit('update', display);
});
// Resize with the host TTY
const onResize = () => {
try {
const newCols = process.stdout.columns || cols;
const newRows = process.stdout.rows || rows;
this.ptyProcess.resize(newCols, newRows);
} catch {
// ignore
}
};
if (process.stdout && process.stdout.on) {
process.stdout.on('resize', onResize);
}
this.ptyProcess.onExit(({ exitCode, signal }) => {
this.emit('exit', { exitCode, signal });
});
this.started = true;
}
getLines() {
return this.lines.slice();
}
write(input) {
if (!this.ptyProcess) return;
this.ptyProcess.write(input);
}
resize(columns, rows) {
if (!this.ptyProcess) return;
try {
const cols = Math.max(1, Number(columns) || 1);
const r = rows ? Math.max(1, Number(rows) || 1) : undefined;
if (r !== undefined) {
this.ptyProcess.resize(cols, r);
} else {
this.ptyProcess.resize(cols, this.ptyProcess.rows || 24);
}
} catch {
// ignore
}
}
dispose() {
try {
if (this.ptyProcess) {
this.ptyProcess.kill();
this.ptyProcess = null;
}
} catch {
// ignore
}
}
}
const terminalService = new TerminalService();
export default terminalService;