mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Extract Claudeman's local echo overlay into a reusable xterm.js addon at packages/xterm-zerolag-input/. Provides instant keystroke feedback via a DOM overlay, eliminating perceived input latency over high-RTT connections (SSH, mobile, cloud IDEs). - Zero dependencies, compatible with xterm v5.x and @xterm/xterm v5.4+ - Configurable prompt detection (character, regex, or custom function) - Flushed text tracking for tab-switch / deferred echo scenarios - Per-character grid-aligned rendering matching xterm's canvas output - 56 tests passing (prompt finder, overlay renderer, full addon lifecycle) - Dual CJS/ESM build with full TypeScript declarations Claudeman source is unchanged — migration to consume this lib is a separate follow-up. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
151 lines
5.9 KiB
TypeScript
151 lines
5.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { createMockTerminal } from './helpers.js';
|
|
import { findPrompt, readTextAfterPrompt } from '../src/prompt-finder.js';
|
|
import type { XtermTerminal, PromptFinder } from '../src/types.js';
|
|
|
|
function term(lines: string[]) {
|
|
return createMockTerminal({ buffer: { lines } });
|
|
}
|
|
|
|
describe('findPrompt', () => {
|
|
describe('character strategy', () => {
|
|
it('finds $ prompt at column 0', () => {
|
|
const { terminal, cleanup } = term(['output line', '$ ls -la']);
|
|
const finder: PromptFinder = { type: 'character', char: '$' };
|
|
const pos = findPrompt(terminal as unknown as XtermTerminal, finder);
|
|
expect(pos).toEqual({ row: 1, col: 0 });
|
|
cleanup();
|
|
});
|
|
|
|
it('finds > prompt', () => {
|
|
const { terminal, cleanup } = term(['> hello']);
|
|
const finder: PromptFinder = { type: 'character', char: '>' };
|
|
const pos = findPrompt(terminal as unknown as XtermTerminal, finder);
|
|
expect(pos).toEqual({ row: 0, col: 0 });
|
|
cleanup();
|
|
});
|
|
|
|
it('finds prompt with prefix (user@host)', () => {
|
|
const { terminal, cleanup } = term(['user@host:~$ command']);
|
|
const finder: PromptFinder = { type: 'character', char: '$' };
|
|
const pos = findPrompt(terminal as unknown as XtermTerminal, finder);
|
|
expect(pos).toEqual({ row: 0, col: 11 });
|
|
cleanup();
|
|
});
|
|
|
|
it('scans bottom-up and returns lowest match', () => {
|
|
const { terminal, cleanup } = term([
|
|
'$ old prompt',
|
|
'output',
|
|
'$ current prompt',
|
|
]);
|
|
const finder: PromptFinder = { type: 'character', char: '$' };
|
|
const pos = findPrompt(terminal as unknown as XtermTerminal, finder);
|
|
expect(pos).toEqual({ row: 2, col: 0 });
|
|
cleanup();
|
|
});
|
|
|
|
it('returns null when no prompt found', () => {
|
|
const { terminal, cleanup } = term(['no prompt here', 'or here']);
|
|
const finder: PromptFinder = { type: 'character', char: '$' };
|
|
const pos = findPrompt(terminal as unknown as XtermTerminal, finder);
|
|
expect(pos).toBeNull();
|
|
cleanup();
|
|
});
|
|
|
|
it('finds Unicode prompt character', () => {
|
|
const { terminal, cleanup } = term(['\u276f hello']);
|
|
const finder: PromptFinder = { type: 'character', char: '\u276f' };
|
|
const pos = findPrompt(terminal as unknown as XtermTerminal, finder);
|
|
expect(pos).toEqual({ row: 0, col: 0 });
|
|
cleanup();
|
|
});
|
|
});
|
|
|
|
describe('regex strategy', () => {
|
|
it('finds regex prompt', () => {
|
|
const { terminal, cleanup } = term(['user@host:~/dir$ ls']);
|
|
const finder: PromptFinder = { type: 'regex', pattern: /\$/ };
|
|
const pos = findPrompt(terminal as unknown as XtermTerminal, finder);
|
|
expect(pos).not.toBeNull();
|
|
expect(pos!.col).toBe(15);
|
|
cleanup();
|
|
});
|
|
|
|
it('matches complex PS1 patterns', () => {
|
|
const { terminal, cleanup } = term(['(venv) user % cmd']);
|
|
const finder: PromptFinder = { type: 'regex', pattern: /%/ };
|
|
const pos = findPrompt(terminal as unknown as XtermTerminal, finder);
|
|
expect(pos).not.toBeNull();
|
|
expect(pos!.col).toBe(12);
|
|
cleanup();
|
|
});
|
|
|
|
it('returns null on no match', () => {
|
|
const { terminal, cleanup } = term(['just output']);
|
|
const finder: PromptFinder = { type: 'regex', pattern: /\$\s*$/ };
|
|
const pos = findPrompt(terminal as unknown as XtermTerminal, finder);
|
|
expect(pos).toBeNull();
|
|
cleanup();
|
|
});
|
|
});
|
|
|
|
describe('custom strategy', () => {
|
|
it('uses custom finder function', () => {
|
|
const { terminal, cleanup } = term(['anything']);
|
|
const finder: PromptFinder = {
|
|
type: 'custom',
|
|
find: () => ({ row: 5, col: 10 }),
|
|
};
|
|
const pos = findPrompt(terminal as unknown as XtermTerminal, finder);
|
|
expect(pos).toEqual({ row: 5, col: 10 });
|
|
cleanup();
|
|
});
|
|
|
|
it('handles null from custom finder', () => {
|
|
const { terminal, cleanup } = term(['anything']);
|
|
const finder: PromptFinder = {
|
|
type: 'custom',
|
|
find: () => null,
|
|
};
|
|
const pos = findPrompt(terminal as unknown as XtermTerminal, finder);
|
|
expect(pos).toBeNull();
|
|
cleanup();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('readTextAfterPrompt', () => {
|
|
it('reads text after prompt with offset', () => {
|
|
const { terminal, cleanup } = term(['$ hello world']);
|
|
const prompt = { row: 0, col: 0 };
|
|
const text = readTextAfterPrompt(terminal as unknown as XtermTerminal, prompt, 2);
|
|
expect(text).toBe('hello world');
|
|
cleanup();
|
|
});
|
|
|
|
it('returns empty string for empty prompt line', () => {
|
|
const { terminal, cleanup } = term(['$ ']);
|
|
const prompt = { row: 0, col: 0 };
|
|
const text = readTextAfterPrompt(terminal as unknown as XtermTerminal, prompt, 2);
|
|
expect(text).toBe('');
|
|
cleanup();
|
|
});
|
|
|
|
it('trims trailing whitespace', () => {
|
|
const { terminal, cleanup } = term(['$ hello ']);
|
|
const prompt = { row: 0, col: 0 };
|
|
const text = readTextAfterPrompt(terminal as unknown as XtermTerminal, prompt, 2);
|
|
expect(text).toBe('hello');
|
|
cleanup();
|
|
});
|
|
|
|
it('handles offset for complex prompts', () => {
|
|
const { terminal, cleanup } = term(['user@host:~$ ls -la']);
|
|
const prompt = { row: 0, col: 11 };
|
|
const text = readTextAfterPrompt(terminal as unknown as XtermTerminal, prompt, 2);
|
|
expect(text).toBe('ls -la');
|
|
cleanup();
|
|
});
|
|
});
|