mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 19:05:11 +00:00
- Create Terminal interface abstracting stdin/stdout operations for dependency injection - Implement ProcessTerminal for production use with process.stdin/stdout - Implement VirtualTerminal using @xterm/headless for accurate terminal emulation in tests - Fix TypeScript imports for @xterm/headless module - Move all component files to src/components/ directory for better organization - Add comprehensive test suite with async/await patterns for proper render timing - Fix critical TUI differential rendering bug when components grow in height - Issue: Old content wasn't properly cleared when component line count increased - Solution: Clear each old line individually before redrawing, ensure cursor at line start - Add test verifying terminal content preservation and text editor growth behavior - Update tsconfig.json to include test files in type checking - Add benchmark test comparing single vs double buffer performance The implementation successfully reduces flicker by only updating changed lines rather than clearing entire sections. Both TUI implementations maintain the same interface for backward compatibility.
161 lines
No EOL
3.4 KiB
TypeScript
161 lines
No EOL
3.4 KiB
TypeScript
import xterm from '@xterm/headless';
|
|
import type { Terminal as XtermTerminalType } from '@xterm/headless';
|
|
import { Terminal } from '../src/terminal.js';
|
|
|
|
// Extract Terminal class from the module
|
|
const XtermTerminal = xterm.Terminal;
|
|
|
|
/**
|
|
* Virtual terminal for testing using xterm.js for accurate terminal emulation
|
|
*/
|
|
export class VirtualTerminal implements Terminal {
|
|
private xterm: XtermTerminalType;
|
|
private inputHandler?: (data: string) => void;
|
|
private resizeHandler?: () => void;
|
|
private _columns: number;
|
|
private _rows: number;
|
|
|
|
constructor(columns = 80, rows = 24) {
|
|
this._columns = columns;
|
|
this._rows = rows;
|
|
|
|
// Create xterm instance with specified dimensions
|
|
this.xterm = new XtermTerminal({
|
|
cols: columns,
|
|
rows: rows,
|
|
// Disable all interactive features for testing
|
|
disableStdin: true,
|
|
allowProposedApi: true,
|
|
});
|
|
}
|
|
|
|
start(onInput: (data: string) => void, onResize: () => void): void {
|
|
this.inputHandler = onInput;
|
|
this.resizeHandler = onResize;
|
|
// No need for raw mode in virtual terminal
|
|
}
|
|
|
|
stop(): void {
|
|
this.inputHandler = undefined;
|
|
this.resizeHandler = undefined;
|
|
}
|
|
|
|
write(data: string): void {
|
|
this.xterm.write(data);
|
|
}
|
|
|
|
get columns(): number {
|
|
return this._columns;
|
|
}
|
|
|
|
get rows(): number {
|
|
return this._rows;
|
|
}
|
|
|
|
// Test-specific methods not in Terminal interface
|
|
|
|
/**
|
|
* Simulate keyboard input
|
|
*/
|
|
sendInput(data: string): void {
|
|
if (this.inputHandler) {
|
|
this.inputHandler(data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resize the terminal
|
|
*/
|
|
resize(columns: number, rows: number): void {
|
|
this._columns = columns;
|
|
this._rows = rows;
|
|
this.xterm.resize(columns, rows);
|
|
if (this.resizeHandler) {
|
|
this.resizeHandler();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait for all pending writes to complete. Viewport and scroll buffer will be updated.
|
|
*/
|
|
async flush(): Promise<void> {
|
|
// Write an empty string to ensure all previous writes are flushed
|
|
return new Promise<void>((resolve) => {
|
|
this.xterm.write('', () => resolve());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Flush and get viewport - convenience method for tests
|
|
*/
|
|
async flushAndGetViewport(): Promise<string[]> {
|
|
await this.flush();
|
|
return this.getViewport();
|
|
}
|
|
|
|
/**
|
|
* Get the visible viewport (what's currently on screen)
|
|
* Note: You should use getViewportAfterWrite() for testing after writing data
|
|
*/
|
|
getViewport(): string[] {
|
|
const lines: string[] = [];
|
|
const buffer = this.xterm.buffer.active;
|
|
|
|
// Get only the visible lines (viewport)
|
|
for (let i = 0; i < this.xterm.rows; i++) {
|
|
const line = buffer.getLine(buffer.viewportY + i);
|
|
if (line) {
|
|
lines.push(line.translateToString(true));
|
|
} else {
|
|
lines.push('');
|
|
}
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
/**
|
|
* Get the entire scroll buffer
|
|
*/
|
|
getScrollBuffer(): string[] {
|
|
const lines: string[] = [];
|
|
const buffer = this.xterm.buffer.active;
|
|
|
|
// Get all lines in the buffer (including scrollback)
|
|
for (let i = 0; i < buffer.length; i++) {
|
|
const line = buffer.getLine(i);
|
|
if (line) {
|
|
lines.push(line.translateToString(true));
|
|
} else {
|
|
lines.push('');
|
|
}
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
/**
|
|
* Clear the terminal viewport
|
|
*/
|
|
clear(): void {
|
|
this.xterm.clear();
|
|
}
|
|
|
|
/**
|
|
* Reset the terminal completely
|
|
*/
|
|
reset(): void {
|
|
this.xterm.reset();
|
|
}
|
|
|
|
/**
|
|
* Get cursor position
|
|
*/
|
|
getCursorPosition(): { x: number; y: number } {
|
|
const buffer = this.xterm.buffer.active;
|
|
return {
|
|
x: buffer.cursorX,
|
|
y: buffer.cursorY
|
|
};
|
|
}
|
|
} |