mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix: add suppressBufferDetection, comprehensive docs and tests
Second deep audit comparing all 43 integration points in Claudeman's app.js against the library API found one critical gap: - Add suppressBufferDetection() — needed when switching to sessions with UI framework text (Ink) after the prompt that would be falsely detected. Claudeman sets _bufferDetectDone=true externally; this method provides the public equivalent. Tests: 74 total (+13 new) - Tab-switch save/restore pattern (flushed state roundtrip) - suppressBufferDetection blocks explicit, implicit (addChar), and cascade (removeChar) detection paths - clear() resets suppression - Methods safe before activate() and after dispose() - addChar implicit buffer detection on first keystroke - refreshFont with flushed-only text README: rewritten from 207 to 459 lines with: - removeChar cascade explanation with return value table - Flushed text concept explained - Integration patterns: buffered, char-at-a-time, tab switching, tab completion, Ink/TUI frameworks, SSE reconnect, resize, font - Architecture diagram (keypress → DOM overlay → PTY echo flow) - Prompt column locking, text wrapping, render cache, scroll awareness - All known limitations documented Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,8 @@ When using xterm.js over a remote connection (SSH web clients, cloud IDEs, mobil
|
||||
npm install xterm-zerolag-input
|
||||
```
|
||||
|
||||
Zero runtime dependencies. Compatible with both `xterm` (pre-5.4) and `@xterm/xterm` (5.4+).
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
@@ -44,12 +46,10 @@ terminal.onData((data) => {
|
||||
if (source === 'flushed') ws.send(data); // only backspace text already in PTY
|
||||
} else if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
||||
zerolag.addChar(data);
|
||||
// Don't send to server yet — wait for Enter
|
||||
// Or send immediately if your app uses char-at-a-time mode
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Re-render after terminal output (optional, for frameworks like Ink)
|
||||
// 3. Re-render after terminal output (optional, for full-screen TUI frameworks)
|
||||
terminal.onWriteParsed(() => {
|
||||
if (zerolag.hasPending) zerolag.rerender();
|
||||
});
|
||||
@@ -57,10 +57,11 @@ terminal.onWriteParsed(() => {
|
||||
|
||||
## Prompt Detection
|
||||
|
||||
The addon needs to know where user input starts on the terminal line. Three strategies are supported:
|
||||
The addon needs to know where user input starts on the terminal line. It scans the terminal buffer bottom-up looking for the prompt. Three strategies are supported:
|
||||
|
||||
### Character (default)
|
||||
Scans bottom-up for a single character:
|
||||
|
||||
Scans bottom-up for a single character. Uses `lastIndexOf` to find the rightmost occurrence on each line.
|
||||
|
||||
```typescript
|
||||
// Bash: user@host:~$
|
||||
@@ -76,21 +77,23 @@ Scans bottom-up for a single character:
|
||||
{ type: 'character', char: '>', offset: 2 }
|
||||
```
|
||||
|
||||
The `offset` is how many characters after the marker the input begins (e.g., `"$ "` = 2).
|
||||
The `offset` is how many characters after the marker the user input begins (e.g., `"$ "` = 2, `">"` with no space = 1).
|
||||
|
||||
### Regex
|
||||
For complex prompts:
|
||||
|
||||
For complex prompts. The global (`g`) flag is safely stripped to prevent `lastIndex` mutation across renders.
|
||||
|
||||
```typescript
|
||||
// Match end-of-prompt patterns
|
||||
// Match dollar sign at end of prompt
|
||||
{ type: 'regex', pattern: /\$\s*$/, offset: 2 }
|
||||
|
||||
// Match specific prompt format
|
||||
// Match specific virtualenv prompt
|
||||
{ type: 'regex', pattern: /\(venv\)\s+\w+\s+%/, offset: 2 }
|
||||
```
|
||||
|
||||
### Custom
|
||||
Full control:
|
||||
|
||||
Full control — provide your own function:
|
||||
|
||||
```typescript
|
||||
{
|
||||
@@ -98,64 +101,93 @@ Full control:
|
||||
offset: 0,
|
||||
find: (terminal) => {
|
||||
// Your logic here — return { row, col } or null
|
||||
// row is viewport-relative (0 = top of viewport)
|
||||
return { row: terminal.rows - 1, col: 0 };
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
## API Reference
|
||||
|
||||
### `ZerolagInputAddon`
|
||||
|
||||
Implements xterm.js `ITerminalAddon`. Load via `terminal.loadAddon(addon)`.
|
||||
|
||||
The addon does **not** hook `terminal.onData()` — you wire your own input handler and call the methods below. This gives you full control over which keystrokes are echoed vs forwarded.
|
||||
|
||||
#### Input Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `addChar(char)` | Add a single printable character to the overlay |
|
||||
| `appendText(text)` | Append multiple characters (e.g., paste) |
|
||||
| `removeChar(): 'pending' \| 'flushed' \| false` | Remove last char. Returns source (`'pending'` = unsent, `'flushed'` = send backspace to PTY) or `false` |
|
||||
| `clear()` | Clear all state and hide overlay |
|
||||
| `addChar(char)` | Add a single printable character to the overlay. Call for `charCode >= 32, length === 1`. On first keystroke after empty state, auto-detects existing buffer text as flushed. |
|
||||
| `appendText(text)` | Append multiple characters at once (paste). Same auto-detection as `addChar`. |
|
||||
| `removeChar()` | Remove last character. See [removeChar Cascade](#removechar-cascade) below. |
|
||||
| `clear()` | Clear all state (pending + flushed), hide overlay. Call on Enter, Ctrl+C, Escape, or any action that submits/cancels input. Resets buffer detection guard. |
|
||||
|
||||
#### removeChar Cascade
|
||||
|
||||
`removeChar()` returns `'pending' | 'flushed' | false` indicating the source of the removed character:
|
||||
|
||||
```
|
||||
Step 1: pendingText non-empty → pop last char → return 'pending'
|
||||
Step 2: flushed text exists → decrement flushed → return 'flushed'
|
||||
Step 3: both empty → detect buffer → return 'flushed' (if found)
|
||||
Step 4: nothing found → → return false
|
||||
```
|
||||
|
||||
**How to use the return value:**
|
||||
|
||||
| Return | Meaning | Action |
|
||||
|--------|---------|--------|
|
||||
| `'pending'` | Removed a character that was never sent to PTY | Do nothing — no backspace needed |
|
||||
| `'flushed'` | Removed a character that was already sent to PTY | Send `\x7f` (backspace) to PTY |
|
||||
| `false` | Nothing to remove | Do nothing |
|
||||
|
||||
Step 3 handles tab completion and arrow-key edits: if the user tabs to complete a command and immediately hits backspace, the overlay detects the completed text from the terminal buffer and removes from it.
|
||||
|
||||
#### Flushed Text Tracking
|
||||
|
||||
For scenarios where text has been sent to the PTY but the echo hasn't arrived yet (e.g., tab switching between sessions):
|
||||
"Flushed" text is text that has been sent to the PTY but whose echo hasn't arrived in the terminal buffer yet. This happens during:
|
||||
- **Tab switching**: Pending overlay text is flushed to PTY before switching, then restored as flushed on switch-back
|
||||
- **Tab completion**: Shell fills text on the prompt; overlay syncs via `detectBufferText()`
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `setFlushed(count, text)` | Mark characters as sent-but-unacknowledged |
|
||||
| `getFlushed()` | Get `{ count, text }` of flushed state |
|
||||
| `clearFlushed()` | Clear flushed state (echo arrived) |
|
||||
| `setFlushed(count, text)` | Mark characters as sent-but-unacknowledged. Triggers a render. |
|
||||
| `getFlushed()` | Returns `{ count: number, text: string }`. |
|
||||
| `clearFlushed()` | Clear flushed state (call when server echo has arrived). |
|
||||
|
||||
#### Buffer Detection
|
||||
|
||||
The overlay can scan the terminal buffer for text that already exists after the prompt but wasn't typed through the overlay (tab completion, arrow-key edits, shell history).
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `detectBufferText()` | Scan buffer for text after prompt. Returns the detected text string, or `null`. If found, sets it as flushed text. Guarded: only runs once per `clear()` cycle. |
|
||||
| `resetBufferDetection()` | Re-enable detection (e.g., after tab completion response arrives). |
|
||||
| `suppressBufferDetection()` | Prevent detection until next `clear()`. Use when switching to a session whose buffer has UI framework text (e.g., Ink status bars) after the prompt marker that would be falsely detected. |
|
||||
|
||||
#### Rendering
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `rerender()` | Force re-render at current prompt position |
|
||||
| `refreshFont()` | Re-read font properties after size/theme change |
|
||||
|
||||
#### Buffer Detection
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `detectBufferText()` | Scan buffer for text after prompt; returns detected text or `null` |
|
||||
| `resetBufferDetection()` | Allow re-detection (auto-reset on `clear()`) |
|
||||
| `rerender()` | Force re-render at current prompt position. Clears the render cache so the DOM is rebuilt. Call after terminal buffer reloads, full-screen redraws, or SSE reconnects. |
|
||||
| `refreshFont()` | Re-read font properties (family, size, weight, letter-spacing, colors) from the terminal and re-render. Call after font size changes or theme switches. |
|
||||
|
||||
#### Prompt Utilities
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `findPrompt()` | Find prompt position using configured strategy |
|
||||
| `readPromptText()` | Read text after prompt marker |
|
||||
| `findPrompt()` | Find prompt position using the configured strategy. Returns `{ row, col }` (viewport-relative) or `null`. |
|
||||
| `readPromptText()` | Read text after the prompt marker on the prompt line. Returns the text or `null`. |
|
||||
|
||||
#### State
|
||||
#### State Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `pendingText` | `string` | Characters typed but not acknowledged |
|
||||
| `hasPending` | `boolean` | Whether overlay has any content |
|
||||
| `state` | `ZerolagInputState` | Full read-only state snapshot |
|
||||
| `pendingText` | `string` | Characters typed but not acknowledged. Read-only. |
|
||||
| `hasPending` | `boolean` | `true` if overlay has any content (pending or flushed). |
|
||||
| `state` | `ZerolagInputState` | Read-only snapshot: `{ pendingText, flushedLength, flushedText, visible, promptPosition }`. Safe to call before `activate()` (returns `visible: false`). |
|
||||
|
||||
### Options
|
||||
|
||||
@@ -163,24 +195,154 @@ For scenarios where text has been sent to the PTY but the echo hasn't arrived ye
|
||||
interface ZerolagInputOptions {
|
||||
prompt?: PromptFinder; // Default: { type: 'character', char: '>', offset: 2 }
|
||||
zIndex?: number; // Default: 7
|
||||
backgroundColor?: string; // Default: from terminal theme
|
||||
foregroundColor?: string; // Default: from terminal theme
|
||||
backgroundColor?: string; // Default: from terminal theme, then '#0d0d0d'
|
||||
foregroundColor?: string; // Default: from computed .xterm-rows style, then theme, then '#eeeeee'
|
||||
showCursor?: boolean; // Default: true
|
||||
cursorColor?: string; // Default: from terminal theme
|
||||
cursorColor?: string; // Default: from terminal theme cursor, then '#e0e0e0'
|
||||
scrollDebounceMs?: number; // Default: 50
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Patterns
|
||||
|
||||
### Buffered Input (hold until Enter)
|
||||
|
||||
The quick start example above uses buffered mode — characters accumulate in the overlay and are sent on Enter. This is common for remote shells where you want to batch input.
|
||||
|
||||
### Char-at-a-Time (send immediately)
|
||||
|
||||
For applications that need each keystroke sent immediately:
|
||||
|
||||
```typescript
|
||||
terminal.onData((data) => {
|
||||
if (data === '\r') {
|
||||
zerolag.clear();
|
||||
ws.send('\r');
|
||||
} else if (data === '\x7f') {
|
||||
zerolag.removeChar(); // always 'pending' since nothing is buffered
|
||||
ws.send(data);
|
||||
} else if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
||||
zerolag.addChar(data);
|
||||
ws.send(data);
|
||||
// The overlay shows the char immediately; the PTY echo will arrive later
|
||||
// and the overlay continues showing until the next rerender()
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Tab Switching (multi-session)
|
||||
|
||||
When your app has multiple terminal sessions in tabs:
|
||||
|
||||
```typescript
|
||||
function switchToSession(newSessionId: string) {
|
||||
// 1. Save current overlay state
|
||||
const pending = zerolag.pendingText;
|
||||
const { count, text } = zerolag.getFlushed();
|
||||
if (pending) {
|
||||
sendToPty(currentSessionId, pending); // flush unsent text to PTY
|
||||
}
|
||||
const totalCount = count + pending.length;
|
||||
const totalText = text + pending;
|
||||
savedFlushed.set(currentSessionId, { count: totalCount, text: totalText });
|
||||
zerolag.clear();
|
||||
|
||||
// 2. Switch terminal buffer to new session
|
||||
loadSessionBuffer(newSessionId);
|
||||
|
||||
// 3. Restore overlay state for new session
|
||||
const saved = savedFlushed.get(newSessionId);
|
||||
if (saved) {
|
||||
zerolag.suppressBufferDetection(); // prevent false detection of UI text
|
||||
zerolag.setFlushed(saved.count, saved.text);
|
||||
} else {
|
||||
zerolag.suppressBufferDetection(); // fresh session, no flushed state
|
||||
}
|
||||
|
||||
// 4. Re-render after buffer loads
|
||||
terminal.write('', () => {
|
||||
zerolag.rerender(); // finds prompt, positions overlay correctly
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Tab Completion Detection
|
||||
|
||||
After sending a Tab key to the PTY, you can detect whether the shell completed text:
|
||||
|
||||
```typescript
|
||||
// Before Tab: snapshot baseline
|
||||
const baseline = zerolag.readPromptText();
|
||||
zerolag.clear();
|
||||
sendToPty('\t');
|
||||
|
||||
// After PTY response arrives:
|
||||
zerolag.resetBufferDetection();
|
||||
const detected = zerolag.detectBufferText();
|
||||
if (detected && detected !== baseline) {
|
||||
// Tab completion occurred — overlay now shows the completed text
|
||||
}
|
||||
```
|
||||
|
||||
### Full-Screen TUI Frameworks (Ink, Blessed)
|
||||
|
||||
Frameworks like Ink redraw the entire screen on state changes, which can move the prompt. Re-render the overlay after each terminal write:
|
||||
|
||||
```typescript
|
||||
terminal.onWriteParsed(() => {
|
||||
if (zerolag.hasPending) zerolag.rerender();
|
||||
});
|
||||
```
|
||||
|
||||
### SSE/WebSocket Reconnect
|
||||
|
||||
After a connection drop and reconnect, the terminal buffer reloads. Preserve overlay text across reconnects:
|
||||
|
||||
```typescript
|
||||
function onReconnect() {
|
||||
// Buffer reloaded — re-render overlay at new prompt position
|
||||
zerolag.rerender();
|
||||
}
|
||||
```
|
||||
|
||||
### Terminal Resize
|
||||
|
||||
After the terminal is resized (columns/rows change), cell dimensions change:
|
||||
|
||||
```typescript
|
||||
fitAddon.fit();
|
||||
zerolag.rerender(); // recalculates cell dimensions and prompt position
|
||||
```
|
||||
|
||||
### Font Size / Theme Changes
|
||||
|
||||
```typescript
|
||||
terminal.options.fontSize = 18;
|
||||
zerolag.refreshFont(); // re-caches font properties, re-renders
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. A `<div>` overlay is inserted into xterm.js's `.xterm-screen` element at z-index 7
|
||||
2. Each character is rendered as an absolutely-positioned `<span>` on the terminal's cell grid
|
||||
3. Cell dimensions are read from xterm.js's render service (private API on v5, public on v7+)
|
||||
4. Font properties (family, size, weight, letter-spacing) are cached from the terminal's computed styles
|
||||
5. The overlay is hidden when the user scrolls away from the bottom of the terminal
|
||||
6. A render cache (`renderKey`) prevents redundant DOM rebuilds at 60fps
|
||||
### Architecture
|
||||
|
||||
### xterm.js DOM Structure
|
||||
```
|
||||
User types 'a' addChar('a') DOM overlay Instant feedback
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
Keyboard ─────► ZerolagAddon ─────► <span> at ─────► User sees 'a'
|
||||
│ grid pos immediately
|
||||
│
|
||||
│ (meanwhile, 200ms later...)
|
||||
│
|
||||
▼
|
||||
PTY echo 'a' ─────► xterm.js canvas ─────► Canvas shows 'a'
|
||||
(overlay still on top,
|
||||
cleared on next clear())
|
||||
```
|
||||
|
||||
### DOM Overlay Positioning
|
||||
|
||||
The overlay is a `<div>` inserted into xterm.js's `.xterm-screen` element:
|
||||
|
||||
```
|
||||
div.xterm-screen (position: relative)
|
||||
@@ -188,19 +350,62 @@ div.xterm-screen (position: relative)
|
||||
├── div.xterm-rows (z-index: auto)
|
||||
├── div.xterm-selection (z-index: 1)
|
||||
├── div.xterm-decoration-container (z-index: 6-7)
|
||||
└── div.zerolag-overlay (z-index: 7) ← our overlay
|
||||
└── div[zerolag overlay] (z-index: 7) ← our overlay
|
||||
```
|
||||
|
||||
Each character is rendered as an absolutely-positioned `<span>` on the terminal's cell grid:
|
||||
|
||||
```
|
||||
left = charIndex * cellWidth (CSS pixels)
|
||||
top = lineIndex * cellHeight (CSS pixels)
|
||||
width = cellWidth (one cell per character)
|
||||
```
|
||||
|
||||
Cell dimensions are read from xterm.js:
|
||||
- **v5.x**: `terminal._core._renderService.dimensions.css.cell` (private API)
|
||||
- **v7+**: `terminal.dimensions.css.cell` (public API, auto-detected)
|
||||
|
||||
### Font Matching
|
||||
|
||||
The overlay matches the terminal's font rendering by:
|
||||
1. Caching `fontFamily`, `fontSize`, `fontWeight` from `terminal.options`
|
||||
2. Reading `letterSpacing` from the computed style of `.xterm-rows`
|
||||
3. Applying `-webkit-font-smoothing: antialiased` (matches canvas grayscale rendering)
|
||||
4. Disabling ligatures via `font-feature-settings: 'liga' 0, 'calt' 0`
|
||||
5. Using `text-rendering: geometricPrecision` for consistent glyph sizing
|
||||
|
||||
### Render Cache
|
||||
|
||||
A render key based on `displayText:startCol:row:col:totalCols:flushedOffset` prevents redundant DOM rebuilds. The cache is cleared by `rerender()`, `refreshFont()`, and internally on state changes that affect the display.
|
||||
|
||||
### Scroll Awareness
|
||||
|
||||
The overlay hides when the user scrolls away from the bottom of the terminal (where the prompt lives). A scroll listener on `.xterm-viewport` detects `viewportY !== baseY` and hides the overlay. When the user scrolls back to the bottom, a debounced re-render (50ms default) repositions the overlay.
|
||||
|
||||
### Prompt Column Locking
|
||||
|
||||
When flushed text exists, the prompt column is locked to prevent visual jitter from full-screen redraws that temporarily shift the prompt marker. The row is allowed to change (output scrolls the prompt down), but the column stays fixed until the flushed state is cleared.
|
||||
|
||||
### Text Wrapping
|
||||
|
||||
Long input that exceeds the terminal width is wrapped at column boundaries:
|
||||
- Line 0: `(totalCols - startCol)` characters (starts after prompt)
|
||||
- Line 1+: `totalCols` characters (starts at column 0)
|
||||
|
||||
This matches xterm.js's character-level wrapping behavior.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **xterm.js v5.x**: Uses private API `terminal._core._renderService.dimensions` for cell sizing
|
||||
- **xterm.js v7+** (future): Will automatically use public `terminal.dimensions` API
|
||||
- **xterm.js v5.x**: Uses private API `terminal._core._renderService.dimensions` for cell sizing. Fully supported.
|
||||
- **xterm.js v7+** (future): Will automatically use public `terminal.dimensions` API when available.
|
||||
- **Renderers**: Best results with the canvas/WebGL renderer. DOM renderer works but the overlay is redundant since DOM text is already positioned identically.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- **Canvas/WebGL renderer**: Minor sub-pixel font differences between DOM overlay and canvas text are possible. Best results with the DOM renderer.
|
||||
- **Unicode/emoji**: Multi-byte characters (emoji, CJK) are not echoed (they have varying cell widths that are hard to predict client-side).
|
||||
- **Misprediction**: If the server processes input differently than expected (e.g., password prompts that don't echo), the overlay will show characters that aren't actually there. Call `clear()` when you detect such cases.
|
||||
- **Canvas/WebGL font mismatch**: Minor sub-pixel differences between DOM overlay text and canvas-rendered text are possible. The per-character absolute positioning minimizes this, but it's not pixel-identical on all platforms.
|
||||
- **Unicode/emoji**: Multi-byte characters (emoji, CJK) are not reliably echoed — they occupy variable cell widths that can't be predicted client-side. The overlay renders them at single-cell width, causing misalignment.
|
||||
- **Misprediction**: If the server processes input differently than expected (e.g., password prompts that suppress echo), the overlay shows characters that aren't actually displayed. Call `clear()` when you detect such cases.
|
||||
- **Prompt character in output**: If the prompt character appears in command output (e.g., `$` in a log message), the overlay may position at the wrong location. Use a more specific regex or custom finder to avoid this.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -347,6 +347,20 @@ export class ZerolagInputAddon implements XtermAddon {
|
||||
this._bufferDetectDone = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress buffer detection until the next `clear()` or
|
||||
* `resetBufferDetection()` call.
|
||||
*
|
||||
* Use case: When switching to a session whose buffer contains UI
|
||||
* framework text (e.g., Ink status bars) after the prompt marker,
|
||||
* `detectBufferText()` would falsely pick up that text as user input.
|
||||
* Call this after switching to prevent false detection until the user
|
||||
* actually presses Enter (which calls `clear()` and re-enables detection).
|
||||
*/
|
||||
suppressBufferDetection(): void {
|
||||
this._bufferDetectDone = true;
|
||||
}
|
||||
|
||||
// ─── Prompt utilities ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -261,6 +261,45 @@ describe('ZerolagInputAddon', () => {
|
||||
expect(text).toBe('text');
|
||||
});
|
||||
|
||||
it('suppressBufferDetection prevents detection', () => {
|
||||
const { addon } = tracked(['$ some UI text']);
|
||||
addon.suppressBufferDetection();
|
||||
|
||||
// Detection should be blocked
|
||||
const text = addon.detectBufferText();
|
||||
expect(text).toBeNull();
|
||||
expect(addon.getFlushed().count).toBe(0);
|
||||
});
|
||||
|
||||
it('suppressBufferDetection also blocks implicit detection in addChar', () => {
|
||||
const { addon } = tracked(['$ ink status bar']);
|
||||
addon.suppressBufferDetection();
|
||||
|
||||
// addChar calls _detectBufferText internally on first keystroke
|
||||
addon.addChar('x');
|
||||
// Should have only 'x' pending, NOT the buffer text as flushed
|
||||
expect(addon.pendingText).toBe('x');
|
||||
expect(addon.getFlushed().count).toBe(0);
|
||||
});
|
||||
|
||||
it('suppressBufferDetection also blocks detection in removeChar cascade', () => {
|
||||
const { addon } = tracked(['$ buffer text']);
|
||||
addon.suppressBufferDetection();
|
||||
|
||||
// removeChar step 3 calls _detectBufferText — should be blocked
|
||||
expect(addon.removeChar()).toBe(false);
|
||||
expect(addon.getFlushed().count).toBe(0);
|
||||
});
|
||||
|
||||
it('clear resets suppression (re-enables detection)', () => {
|
||||
const { addon } = tracked(['$ text']);
|
||||
addon.suppressBufferDetection();
|
||||
addon.clear(); // resets _bufferDetectDone to false
|
||||
|
||||
const text = addon.detectBufferText();
|
||||
expect(text).toBe('text');
|
||||
});
|
||||
|
||||
it('clear resets buffer detection guard', () => {
|
||||
const { addon } = tracked(['$ text']);
|
||||
addon.detectBufferText();
|
||||
@@ -325,5 +364,129 @@ describe('ZerolagInputAddon', () => {
|
||||
expect(() => addon.rerender()).not.toThrow();
|
||||
expect(addon.hasPending).toBe(true);
|
||||
});
|
||||
|
||||
it('refreshFont re-renders flushed-only text', () => {
|
||||
const { addon } = tracked();
|
||||
addon.setFlushed(3, 'abc');
|
||||
expect(() => addon.refreshFont()).not.toThrow();
|
||||
expect(addon.hasPending).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tab-switch save/restore pattern', () => {
|
||||
it('save pending + flushed, restore as flushed', () => {
|
||||
const { addon } = tracked(['$ ']);
|
||||
|
||||
// User types some text
|
||||
addon.addChar('h');
|
||||
addon.addChar('i');
|
||||
expect(addon.pendingText).toBe('hi');
|
||||
|
||||
// Tab switch: save state
|
||||
const pending = addon.pendingText;
|
||||
const { count: flushedCount, text: flushedText } = addon.getFlushed();
|
||||
const totalCount = flushedCount + pending.length;
|
||||
const totalText = flushedText + pending;
|
||||
addon.clear();
|
||||
|
||||
// Simulate PTY send of pending text (app would do this)
|
||||
// ...
|
||||
|
||||
// Tab switch back: restore as flushed (text is now in PTY)
|
||||
addon.suppressBufferDetection(); // prevent false Ink detection
|
||||
addon.setFlushed(totalCount, totalText);
|
||||
expect(addon.getFlushed()).toEqual({ count: 2, text: 'hi' });
|
||||
expect(addon.hasPending).toBe(true); // has flushed content
|
||||
|
||||
// Backspace should return 'flushed' (text is in PTY)
|
||||
const source = addon.removeChar();
|
||||
expect(source).toBe('flushed');
|
||||
expect(addon.getFlushed().count).toBe(1);
|
||||
});
|
||||
|
||||
it('save with existing flushed + pending', () => {
|
||||
const { addon } = tracked(['$ ']);
|
||||
|
||||
// Set flushed from previous restore, then user types more
|
||||
addon.setFlushed(3, 'abc');
|
||||
addon.addChar('d');
|
||||
addon.addChar('e');
|
||||
|
||||
// Save state
|
||||
const pending = addon.pendingText;
|
||||
const { count, text } = addon.getFlushed();
|
||||
expect(pending).toBe('de');
|
||||
expect(count).toBe(3);
|
||||
expect(text).toBe('abc');
|
||||
|
||||
// Combined for restore
|
||||
const totalCount = count + pending.length;
|
||||
const totalText = text + pending;
|
||||
addon.clear();
|
||||
|
||||
// Restore
|
||||
addon.setFlushed(totalCount, totalText);
|
||||
expect(addon.getFlushed()).toEqual({ count: 5, text: 'abcde' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods before activate / after dispose', () => {
|
||||
it('addChar accumulates but does not crash before activate', () => {
|
||||
const addon = new ZerolagInputAddon();
|
||||
addon.addChar('a');
|
||||
addon.addChar('b');
|
||||
expect(addon.pendingText).toBe('ab');
|
||||
expect(addon.hasPending).toBe(true);
|
||||
// No dispose needed — never activated, no DOM
|
||||
});
|
||||
|
||||
it('removeChar works on pending text before activate', () => {
|
||||
const addon = new ZerolagInputAddon();
|
||||
addon.addChar('x');
|
||||
expect(addon.removeChar()).toBe('pending');
|
||||
expect(addon.pendingText).toBe('');
|
||||
});
|
||||
|
||||
it('clear works before activate', () => {
|
||||
const addon = new ZerolagInputAddon();
|
||||
addon.addChar('x');
|
||||
addon.clear();
|
||||
expect(addon.pendingText).toBe('');
|
||||
expect(addon.hasPending).toBe(false);
|
||||
});
|
||||
|
||||
it('all methods safe after dispose', () => {
|
||||
const { addon, mock } = tracked();
|
||||
addon.dispose();
|
||||
expect(() => addon.addChar('x')).not.toThrow();
|
||||
expect(() => addon.removeChar()).not.toThrow();
|
||||
expect(() => addon.clear()).not.toThrow();
|
||||
expect(() => addon.rerender()).not.toThrow();
|
||||
expect(() => addon.refreshFont()).not.toThrow();
|
||||
expect(addon.findPrompt()).toBeNull();
|
||||
expect(addon.readPromptText()).toBeNull();
|
||||
expect(addon.detectBufferText()).toBeNull();
|
||||
mock.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addChar implicit buffer detection', () => {
|
||||
it('first keystroke detects existing buffer text as flushed', () => {
|
||||
const { addon } = tracked(['$ existing']);
|
||||
// First addChar should trigger _detectBufferText
|
||||
addon.addChar('!');
|
||||
expect(addon.pendingText).toBe('!');
|
||||
expect(addon.getFlushed().count).toBe(8); // 'existing'
|
||||
expect(addon.getFlushed().text).toBe('existing');
|
||||
});
|
||||
|
||||
it('second keystroke does NOT re-detect', () => {
|
||||
const { addon } = tracked(['$ existing']);
|
||||
addon.addChar('a');
|
||||
addon.addChar('b');
|
||||
// Should NOT detect again — flushed from first char remains
|
||||
expect(addon.pendingText).toBe('ab');
|
||||
expect(addon.getFlushed().count).toBe(8); // still 'existing'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user