124 lines
3.7 KiB
JavaScript
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;
|
|
|
|
|