137 lines
4.2 KiB
JavaScript
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;
|
|
|
|
|