Files
toolLooper/src/terminalService.js

137 lines
4.2 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: 'bash> '
},
});
this.ptyProcess.onData((data) => {
const str = String(data);
// Normalize CRLF to LF to avoid double-handling \r and \n
const normalized = str.replace(/\r\n/g, '\n');
for (let i = 0; i < normalized.length; i += 1) {
const ch = normalized[i];
if (ch === '\n') {
// Line feed completes the current line
this.lines.push(this.partial);
this.partial = '';
} else if (ch === '\r') {
// Standalone carriage return: simulate return to start of line (overwrite)
this.partial = '';
} else if (ch === '\b' || ch === '\x7f') {
// Backspace or DEL: remove last char if present
if (this.partial.length > 0) {
this.partial = this.partial.slice(0, -1);
}
} else {
this.partial += ch;
}
}
// 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;
}
this.started = false;
} catch {
// ignore
}
}
restart() {
try { this.dispose(); } catch {}
try { this.start(); } catch {}
}
}
const terminalService = new TerminalService();
export default terminalService;