@mariozechner/pi-tui 0.5.8 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +238 -314
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +0 -34
- package/dist/autocomplete.js.map +1 -1
- package/dist/components/markdown-component.d.ts.map +1 -1
- package/dist/components/markdown-component.js +27 -6
- package/dist/components/markdown-component.js.map +1 -1
- package/dist/components/text-editor.d.ts.map +1 -1
- package/dist/components/text-editor.js +0 -81
- package/dist/components/text-editor.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/tui.d.ts +4 -5
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +116 -131
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/dist/logger.d.ts +0 -23
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -83
- package/dist/logger.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# @mariozechner/pi-tui
|
|
2
2
|
|
|
3
|
-
Terminal UI framework with differential rendering for building interactive CLI applications.
|
|
3
|
+
Terminal UI framework with surgical differential rendering for building flicker-free interactive CLI applications.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Differential Rendering**:
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
7
|
+
- **Surgical Differential Rendering**: Three-strategy system that minimizes redraws to 1-2 lines for typical updates
|
|
8
|
+
- **Scrollback Buffer Preservation**: Correctly maintains terminal history when content exceeds viewport
|
|
9
|
+
- **Zero Flicker**: Components like text editors remain perfectly still while other parts update
|
|
10
|
+
- **Interactive Components**: Text editor with autocomplete, selection lists, markdown rendering
|
|
11
|
+
- **Composable Architecture**: Container-based component system with automatic lifecycle management
|
|
11
12
|
|
|
12
13
|
## Quick Start
|
|
13
14
|
|
|
@@ -35,7 +36,7 @@ editor.onSubmit = (text: string) => {
|
|
|
35
36
|
if (text.trim()) {
|
|
36
37
|
const message = new TextComponent(`💬 ${text}`);
|
|
37
38
|
chatContainer.addChild(message);
|
|
38
|
-
|
|
39
|
+
// Note: Container automatically calls requestRender when children change
|
|
39
40
|
}
|
|
40
41
|
};
|
|
41
42
|
|
|
@@ -47,135 +48,63 @@ ui.start();
|
|
|
47
48
|
|
|
48
49
|
### TUI
|
|
49
50
|
|
|
50
|
-
Main TUI manager that handles
|
|
51
|
+
Main TUI manager with surgical differential rendering that handles input and component lifecycle.
|
|
51
52
|
|
|
52
|
-
**
|
|
53
|
+
**Key Features:**
|
|
54
|
+
- **Three rendering strategies**: Automatically selects optimal approach
|
|
55
|
+
- Surgical: Updates only changed lines (1-2 lines typical)
|
|
56
|
+
- Partial: Re-renders from first change when structure shifts
|
|
57
|
+
- Full: Complete re-render when changes are above viewport
|
|
58
|
+
- **Performance metrics**: Built-in tracking via `getLinesRedrawn()` and `getAverageLinesRedrawn()`
|
|
59
|
+
- **Terminal abstraction**: Works with any Terminal interface implementation
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
- `
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
58
|
-
- `stop()` -
|
|
59
|
-
- `requestRender()` -
|
|
60
|
-
- `configureLogging(config)` -
|
|
61
|
-
- `cleanupSentinels()` - Remove placeholder components after removal operations
|
|
62
|
-
- `findComponent(component)` - Check if a component exists in the hierarchy (private)
|
|
63
|
-
- `findInContainer(container, component)` - Search for component in container (private)
|
|
61
|
+
**Methods:**
|
|
62
|
+
- `addChild(component)` - Add a component
|
|
63
|
+
- `removeChild(component)` - Remove a component
|
|
64
|
+
- `setFocus(component)` - Set keyboard focus
|
|
65
|
+
- `start()` / `stop()` - Lifecycle management
|
|
66
|
+
- `requestRender()` - Queue re-render (automatically debounced)
|
|
67
|
+
- `configureLogging(config)` - Enable debug logging
|
|
64
68
|
|
|
65
69
|
### Container
|
|
66
70
|
|
|
67
|
-
Component that manages child components
|
|
68
|
-
|
|
69
|
-
**Constructor:**
|
|
71
|
+
Component that manages child components. Automatically triggers re-renders when children change.
|
|
70
72
|
|
|
71
73
|
```typescript
|
|
72
|
-
new Container(
|
|
74
|
+
const container = new Container();
|
|
75
|
+
container.addChild(new TextComponent("Child 1"));
|
|
76
|
+
container.removeChild(component);
|
|
77
|
+
container.clear();
|
|
73
78
|
```
|
|
74
79
|
|
|
75
|
-
**Methods:**
|
|
76
|
-
|
|
77
|
-
- `addChild(component)` - Add a child component
|
|
78
|
-
- `removeChild(component)` - Remove a child component
|
|
79
|
-
- `getChild(index)` - Get a specific child component
|
|
80
|
-
- `getChildCount()` - Get the number of child components
|
|
81
|
-
- `clear()` - Remove all child components
|
|
82
|
-
- `setParentTui(tui)` - Set the parent TUI reference
|
|
83
|
-
- `cleanupSentinels()` - Clean up removed component placeholders
|
|
84
|
-
- `render(width)` - Render all child components (returns ContainerRenderResult)
|
|
85
|
-
|
|
86
80
|
### TextEditor
|
|
87
81
|
|
|
88
|
-
Interactive multiline text editor with
|
|
89
|
-
|
|
90
|
-
**Constructor:**
|
|
82
|
+
Interactive multiline text editor with autocomplete support.
|
|
91
83
|
|
|
92
84
|
```typescript
|
|
93
|
-
new TextEditor(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
interface TextEditorConfig {
|
|
100
|
-
// Configuration options for text editor
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
editor.configure(config: Partial<TextEditorConfig>)
|
|
85
|
+
const editor = new TextEditor();
|
|
86
|
+
editor.setText("Initial text");
|
|
87
|
+
editor.onSubmit = (text) => console.log("Submitted:", text);
|
|
88
|
+
editor.setAutocompleteProvider(provider);
|
|
104
89
|
```
|
|
105
90
|
|
|
106
|
-
**
|
|
107
|
-
|
|
108
|
-
- `
|
|
109
|
-
- `
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
- `getText()` - Get current text content
|
|
114
|
-
- `setText(text)` - Set text content and move cursor to end
|
|
115
|
-
- `setAutocompleteProvider(provider)` - Set autocomplete provider for Tab completion
|
|
116
|
-
- `render(width)` - Render the editor with current state
|
|
117
|
-
- `handleInput(data)` - Process keyboard input
|
|
118
|
-
|
|
119
|
-
**Keyboard Shortcuts:**
|
|
120
|
-
|
|
121
|
-
**Navigation:**
|
|
122
|
-
|
|
123
|
-
- `Arrow Keys` - Move cursor
|
|
124
|
-
- `Home` / `Ctrl+A` - Move to start of line
|
|
125
|
-
- `End` / `Ctrl+E` - Move to end of line
|
|
126
|
-
|
|
127
|
-
**Editing:**
|
|
128
|
-
|
|
129
|
-
- `Backspace` - Delete character before cursor
|
|
130
|
-
- `Delete` / `Fn+Backspace` - Delete character at cursor
|
|
131
|
-
- `Ctrl+K` - Delete current line
|
|
132
|
-
- `Enter` - Submit text (calls onSubmit)
|
|
133
|
-
- `Shift+Enter` / `Option+Enter` - Add new line
|
|
134
|
-
- `Tab` - Trigger autocomplete
|
|
135
|
-
|
|
136
|
-
**Autocomplete (when active):**
|
|
137
|
-
|
|
138
|
-
- `Tab` - Apply selected completion
|
|
139
|
-
- `Arrow Up/Down` - Navigate suggestions
|
|
140
|
-
- `Escape` - Cancel autocomplete
|
|
141
|
-
- `Enter` - Cancel autocomplete and submit
|
|
142
|
-
|
|
143
|
-
**Paste Detection:**
|
|
144
|
-
|
|
145
|
-
- Automatically handles multi-line paste
|
|
146
|
-
- Converts tabs to 4 spaces
|
|
147
|
-
- Filters non-printable characters
|
|
91
|
+
**Key Bindings:**
|
|
92
|
+
- `Enter` - Submit text
|
|
93
|
+
- `Shift+Enter` - New line
|
|
94
|
+
- `Tab` - Autocomplete
|
|
95
|
+
- `Ctrl+K` - Delete line
|
|
96
|
+
- `Ctrl+A/E` - Start/end of line
|
|
97
|
+
- Arrow keys, Backspace, Delete work as expected
|
|
148
98
|
|
|
149
99
|
### TextComponent
|
|
150
100
|
|
|
151
|
-
Simple text
|
|
152
|
-
|
|
153
|
-
**Constructor:**
|
|
101
|
+
Simple text display with automatic word wrapping.
|
|
154
102
|
|
|
155
103
|
```typescript
|
|
156
|
-
new TextComponent(
|
|
157
|
-
|
|
158
|
-
interface Padding {
|
|
159
|
-
top?: number;
|
|
160
|
-
bottom?: number;
|
|
161
|
-
left?: number;
|
|
162
|
-
right?: number;
|
|
163
|
-
}
|
|
104
|
+
const text = new TextComponent("Hello World", { top: 1, bottom: 1 });
|
|
105
|
+
text.setText("Updated text");
|
|
164
106
|
```
|
|
165
107
|
|
|
166
|
-
**Methods:**
|
|
167
|
-
|
|
168
|
-
- `setText(text)` - Update the text content
|
|
169
|
-
- `getText()` - Get current text content
|
|
170
|
-
- `render(width)` - Render with word wrapping
|
|
171
|
-
|
|
172
|
-
**Features:**
|
|
173
|
-
|
|
174
|
-
- Automatic text wrapping to fit terminal width
|
|
175
|
-
- Configurable padding on all sides
|
|
176
|
-
- Preserves line breaks in source text
|
|
177
|
-
- Uses differential rendering to avoid unnecessary updates
|
|
178
|
-
|
|
179
108
|
### MarkdownComponent
|
|
180
109
|
|
|
181
110
|
Renders markdown content with syntax highlighting and proper formatting.
|
|
@@ -328,219 +257,83 @@ interface SlashCommand {
|
|
|
328
257
|
- `shouldTriggerFileCompletion()` - Check if file completion should trigger
|
|
329
258
|
- `applyCompletion()` - Apply selected completion
|
|
330
259
|
|
|
331
|
-
## Differential Rendering
|
|
332
|
-
|
|
333
|
-
The core concept: components return `{lines: string[], changed: boolean, keepLines?: number}`:
|
|
334
|
-
|
|
335
|
-
- `lines`: All lines the component should display
|
|
336
|
-
- `changed`: Whether the component has changed since last render
|
|
337
|
-
- `keepLines`: (Containers only) How many lines from the beginning are unchanged
|
|
338
|
-
|
|
339
|
-
**How it works:**
|
|
340
|
-
|
|
341
|
-
1. TUI calculates total unchanged lines from top (`keepLines`)
|
|
342
|
-
2. Moves cursor up by `(totalLines - keepLines)` positions
|
|
343
|
-
3. Clears from cursor position down with `\x1b[0J`
|
|
344
|
-
4. Prints only the changing lines: `result.lines.slice(keepLines)`
|
|
345
|
-
|
|
346
|
-
This approach minimizes screen updates and provides smooth performance even with large amounts of text.
|
|
347
|
-
|
|
348
|
-
**Important:** Don't add extra cursor positioning after printing - it interferes with terminal scrolling and causes rendering artifacts.
|
|
260
|
+
## Surgical Differential Rendering
|
|
349
261
|
|
|
350
|
-
|
|
262
|
+
The TUI uses a three-strategy rendering system that minimizes redraws to only what's necessary:
|
|
351
263
|
|
|
352
|
-
###
|
|
264
|
+
### Rendering Strategies
|
|
353
265
|
|
|
354
|
-
|
|
355
|
-
|
|
266
|
+
1. **Surgical Updates** (most common)
|
|
267
|
+
- When: Only content changes, same line counts, all changes in viewport
|
|
268
|
+
- Action: Updates only specific changed lines (typically 1-2 lines)
|
|
269
|
+
- Example: Loading spinner animation, updating status text
|
|
356
270
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
271
|
+
2. **Partial Re-render**
|
|
272
|
+
- When: Line count changes or structural changes within viewport
|
|
273
|
+
- Action: Clears from first change to end of screen, re-renders tail
|
|
274
|
+
- Example: Adding new messages to a chat, expanding text editor
|
|
360
275
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
{
|
|
366
|
-
name: "attach",
|
|
367
|
-
description: "Attach a file",
|
|
368
|
-
getArgumentCompletions: (prefix) => {
|
|
369
|
-
// Return file suggestions for attach command
|
|
370
|
-
return null; // Use default file completion
|
|
371
|
-
},
|
|
372
|
-
},
|
|
373
|
-
]);
|
|
374
|
-
|
|
375
|
-
editor.setAutocompleteProvider(autocompleteProvider);
|
|
376
|
-
|
|
377
|
-
editor.onSubmit = (text) => {
|
|
378
|
-
// Handle slash commands
|
|
379
|
-
if (text.startsWith("/")) {
|
|
380
|
-
const [command, ...args] = text.slice(1).split(" ");
|
|
381
|
-
if (command === "clear") {
|
|
382
|
-
chatHistory.clear();
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
if (command === "help") {
|
|
386
|
-
const help = new MarkdownComponent(`
|
|
387
|
-
## Available Commands
|
|
388
|
-
- \`/clear\` - Clear chat history
|
|
389
|
-
- \`/help\` - Show this help
|
|
390
|
-
- \`/attach <file>\` - Attach a file
|
|
391
|
-
`);
|
|
392
|
-
chatHistory.addChild(help);
|
|
393
|
-
ui.requestRender();
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
276
|
+
3. **Full Re-render**
|
|
277
|
+
- When: Changes occur above the viewport (in scrollback buffer)
|
|
278
|
+
- Action: Clears scrollback and screen, renders everything fresh
|
|
279
|
+
- Example: Content exceeds viewport and early components change
|
|
397
280
|
|
|
398
|
-
|
|
399
|
-
const message = new MarkdownComponent(`**You:** ${text}`);
|
|
400
|
-
chatHistory.addChild(message);
|
|
281
|
+
### How Components Participate
|
|
401
282
|
|
|
402
|
-
|
|
403
|
-
setTimeout(() => {
|
|
404
|
-
const response = new MarkdownComponent(`**AI:** Response to "${text}"`);
|
|
405
|
-
chatHistory.addChild(response);
|
|
406
|
-
ui.requestRender();
|
|
407
|
-
}, 1000);
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
ui.addChild(chatHistory);
|
|
411
|
-
ui.addChild(editor);
|
|
412
|
-
ui.setFocus(editor);
|
|
413
|
-
ui.start();
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
### File Browser
|
|
283
|
+
Components implement the simple `Component` interface:
|
|
417
284
|
|
|
418
285
|
```typescript
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const ui = new TUI();
|
|
424
|
-
let currentPath = process.cwd();
|
|
425
|
-
|
|
426
|
-
function createFileList(path: string) {
|
|
427
|
-
const entries = readdirSync(path).map((entry) => {
|
|
428
|
-
const fullPath = join(path, entry);
|
|
429
|
-
const isDir = statSync(fullPath).isDirectory();
|
|
430
|
-
return {
|
|
431
|
-
value: entry,
|
|
432
|
-
label: entry,
|
|
433
|
-
description: isDir ? "directory" : "file",
|
|
434
|
-
};
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
// Add parent directory option
|
|
438
|
-
if (path !== "/") {
|
|
439
|
-
entries.unshift({
|
|
440
|
-
value: "..",
|
|
441
|
-
label: "..",
|
|
442
|
-
description: "parent directory",
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
return entries;
|
|
286
|
+
interface ComponentRenderResult {
|
|
287
|
+
lines: string[]; // The lines to display
|
|
288
|
+
changed: boolean; // Whether content changed since last render
|
|
447
289
|
}
|
|
448
290
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const fileList = new SelectList(entries, 10);
|
|
454
|
-
|
|
455
|
-
fileList.onSelect = (item) => {
|
|
456
|
-
if (item.value === "..") {
|
|
457
|
-
currentPath = join(currentPath, "..");
|
|
458
|
-
showDirectory(currentPath);
|
|
459
|
-
} else if (item.description === "directory") {
|
|
460
|
-
currentPath = join(currentPath, item.value);
|
|
461
|
-
showDirectory(currentPath);
|
|
462
|
-
} else {
|
|
463
|
-
console.log(`Selected file: ${join(currentPath, item.value)}`);
|
|
464
|
-
ui.stop();
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
ui.addChild(fileList);
|
|
469
|
-
ui.setFocus(fileList);
|
|
291
|
+
interface Component {
|
|
292
|
+
readonly id: number; // Unique ID for tracking
|
|
293
|
+
render(width: number): ComponentRenderResult;
|
|
294
|
+
handleInput?(keyData: string): void;
|
|
470
295
|
}
|
|
471
|
-
|
|
472
|
-
showDirectory(currentPath);
|
|
473
|
-
ui.start();
|
|
474
296
|
```
|
|
475
297
|
|
|
476
|
-
|
|
298
|
+
The TUI tracks component IDs and line positions to determine the optimal strategy automatically.
|
|
477
299
|
|
|
478
|
-
|
|
479
|
-
import { TUI, Container, TextComponent, TextEditor, MarkdownComponent } from "@mariozechner/pi-tui";
|
|
300
|
+
### Performance Metrics
|
|
480
301
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
// Create layout containers
|
|
484
|
-
const header = new TextComponent("📝 Advanced TUI Demo", { bottom: 1 });
|
|
485
|
-
const mainContent = new Container();
|
|
486
|
-
const sidebar = new Container();
|
|
487
|
-
const footer = new TextComponent("Press Ctrl+C to exit", { top: 1 });
|
|
302
|
+
Monitor rendering efficiency:
|
|
488
303
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
// Main content area
|
|
496
|
-
const chatArea = new Container();
|
|
497
|
-
const inputArea = new TextEditor();
|
|
304
|
+
```typescript
|
|
305
|
+
const ui = new TUI();
|
|
306
|
+
// After some rendering...
|
|
307
|
+
console.log(`Total lines redrawn: ${ui.getLinesRedrawn()}`);
|
|
308
|
+
console.log(`Average per render: ${ui.getAverageLinesRedrawn()}`);
|
|
309
|
+
```
|
|
498
310
|
|
|
499
|
-
|
|
500
|
-
chatArea.addChild(
|
|
501
|
-
new MarkdownComponent(`
|
|
502
|
-
# Welcome to the TUI Demo
|
|
311
|
+
Typical performance: 1-2 lines redrawn for animations, 0 for static content.
|
|
503
312
|
|
|
504
|
-
|
|
313
|
+
## Examples
|
|
505
314
|
|
|
506
|
-
|
|
507
|
-
- **Sidebar**: File list (simulated)
|
|
508
|
-
- **Chat Area**: Scrollable message history
|
|
509
|
-
- **Input**: Interactive text editor
|
|
510
|
-
- **Footer**: Status information
|
|
315
|
+
Run the example applications in the `test/` directory:
|
|
511
316
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
317
|
+
```bash
|
|
318
|
+
# Chat application with slash commands and autocomplete
|
|
319
|
+
npx tsx test/chat-app.ts
|
|
515
320
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const message = new MarkdownComponent(`
|
|
519
|
-
**${new Date().toLocaleTimeString()}:** ${text}
|
|
520
|
-
`);
|
|
521
|
-
chatArea.addChild(message);
|
|
522
|
-
ui.requestRender();
|
|
523
|
-
}
|
|
524
|
-
};
|
|
321
|
+
# File browser with navigation
|
|
322
|
+
npx tsx test/file-browser.ts
|
|
525
323
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
mainContent.addChild(inputArea);
|
|
324
|
+
# Multi-component layout demo
|
|
325
|
+
npx tsx test/multi-layout.ts
|
|
529
326
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
ui.setFocus(inputArea);
|
|
327
|
+
# Performance benchmark with animation
|
|
328
|
+
npx tsx test/bench.ts
|
|
329
|
+
```
|
|
534
330
|
|
|
535
|
-
|
|
536
|
-
ui.configureLogging({
|
|
537
|
-
enabled: true,
|
|
538
|
-
level: "info",
|
|
539
|
-
logFile: "tui-debug.log",
|
|
540
|
-
});
|
|
331
|
+
### Example Descriptions
|
|
541
332
|
|
|
542
|
-
|
|
543
|
-
|
|
333
|
+
- **chat-app.ts** - Chat interface with slash commands (/clear, /help, /attach) and autocomplete
|
|
334
|
+
- **file-browser.ts** - Interactive file browser with directory navigation
|
|
335
|
+
- **multi-layout.ts** - Complex layout with header, sidebar, main content, and footer
|
|
336
|
+
- **bench.ts** - Performance test with animation showing surgical rendering efficiency
|
|
544
337
|
|
|
545
338
|
## Interfaces and Types
|
|
546
339
|
|
|
@@ -618,28 +411,161 @@ interface SelectItem {
|
|
|
618
411
|
}
|
|
619
412
|
```
|
|
620
413
|
|
|
621
|
-
##
|
|
414
|
+
## Testing
|
|
415
|
+
|
|
416
|
+
### Running Tests
|
|
622
417
|
|
|
623
418
|
```bash
|
|
624
|
-
#
|
|
625
|
-
npm
|
|
419
|
+
# Run all tests
|
|
420
|
+
npm test
|
|
626
421
|
|
|
627
|
-
#
|
|
628
|
-
npm
|
|
422
|
+
# Run specific test file
|
|
423
|
+
npm test -- test/tui-rendering.test.ts
|
|
629
424
|
|
|
630
|
-
# Run
|
|
631
|
-
npm
|
|
425
|
+
# Run tests matching a pattern
|
|
426
|
+
npm test -- --test-name-pattern="preserves existing"
|
|
632
427
|
```
|
|
633
428
|
|
|
634
|
-
|
|
635
|
-
|
|
429
|
+
### Test Infrastructure
|
|
430
|
+
|
|
431
|
+
The TUI uses a **VirtualTerminal** for testing that provides accurate terminal emulation via `@xterm/headless`:
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
import { VirtualTerminal } from "./test/virtual-terminal.js";
|
|
435
|
+
import { TUI, TextComponent } from "../src/index.js";
|
|
436
|
+
|
|
437
|
+
test("my TUI test", async () => {
|
|
438
|
+
const terminal = new VirtualTerminal(80, 24);
|
|
439
|
+
const ui = new TUI(terminal);
|
|
440
|
+
ui.start();
|
|
441
|
+
|
|
442
|
+
ui.addChild(new TextComponent("Hello"));
|
|
443
|
+
|
|
444
|
+
// Wait for render
|
|
445
|
+
await new Promise(resolve => process.nextTick(resolve));
|
|
446
|
+
|
|
447
|
+
// Get rendered output
|
|
448
|
+
const viewport = await terminal.flushAndGetViewport();
|
|
449
|
+
assert.strictEqual(viewport[0], "Hello");
|
|
450
|
+
|
|
451
|
+
ui.stop();
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Writing a New Test
|
|
456
|
+
|
|
457
|
+
1. **Create test file** in `test/` directory with `.test.ts` extension
|
|
458
|
+
2. **Use VirtualTerminal** for accurate terminal emulation
|
|
459
|
+
3. **Key testing patterns**:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { test, describe } from "node:test";
|
|
463
|
+
import assert from "node:assert";
|
|
464
|
+
import { VirtualTerminal } from "./virtual-terminal.js";
|
|
465
|
+
import { TUI, Container, TextComponent } from "../src/index.js";
|
|
466
|
+
|
|
467
|
+
describe("My Feature", () => {
|
|
468
|
+
test("should handle dynamic content", async () => {
|
|
469
|
+
const terminal = new VirtualTerminal(80, 24);
|
|
470
|
+
const ui = new TUI(terminal);
|
|
471
|
+
ui.start();
|
|
472
|
+
|
|
473
|
+
// Setup components
|
|
474
|
+
const container = new Container();
|
|
475
|
+
ui.addChild(container);
|
|
476
|
+
|
|
477
|
+
// Initial render
|
|
478
|
+
await new Promise(resolve => process.nextTick(resolve));
|
|
479
|
+
await terminal.flush();
|
|
480
|
+
|
|
481
|
+
// Check viewport (visible content)
|
|
482
|
+
let viewport = terminal.getViewport();
|
|
483
|
+
assert.strictEqual(viewport.length, 24);
|
|
484
|
+
|
|
485
|
+
// Check scrollback buffer (all content including history)
|
|
486
|
+
let scrollBuffer = terminal.getScrollBuffer();
|
|
487
|
+
|
|
488
|
+
// Simulate user input
|
|
489
|
+
terminal.sendInput("Hello");
|
|
490
|
+
|
|
491
|
+
// Wait for processing
|
|
492
|
+
await new Promise(resolve => process.nextTick(resolve));
|
|
493
|
+
await terminal.flush();
|
|
494
|
+
|
|
495
|
+
// Verify changes
|
|
496
|
+
viewport = terminal.getViewport();
|
|
497
|
+
// ... assertions
|
|
498
|
+
|
|
499
|
+
ui.stop();
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### VirtualTerminal API
|
|
505
|
+
|
|
506
|
+
- `new VirtualTerminal(columns, rows)` - Create terminal with dimensions
|
|
507
|
+
- `write(data)` - Write ANSI sequences to terminal
|
|
508
|
+
- `sendInput(data)` - Simulate keyboard input
|
|
509
|
+
- `flush()` - Wait for all writes to complete
|
|
510
|
+
- `getViewport()` - Get visible lines (what user sees)
|
|
511
|
+
- `getScrollBuffer()` - Get all lines including scrollback
|
|
512
|
+
- `flushAndGetViewport()` - Convenience method
|
|
513
|
+
- `getCursorPosition()` - Get cursor row/column
|
|
514
|
+
- `resize(columns, rows)` - Resize terminal
|
|
515
|
+
|
|
516
|
+
### Testing Best Practices
|
|
517
|
+
|
|
518
|
+
1. **Always flush after renders**: Terminal writes are async
|
|
519
|
+
```typescript
|
|
520
|
+
await new Promise(resolve => process.nextTick(resolve));
|
|
521
|
+
await terminal.flush();
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
2. **Test both viewport and scrollback**: Ensure content preservation
|
|
525
|
+
```typescript
|
|
526
|
+
const viewport = terminal.getViewport(); // Visible content
|
|
527
|
+
const scrollBuffer = terminal.getScrollBuffer(); // All content
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
3. **Use exact string matching**: Don't trim() - whitespace matters
|
|
531
|
+
```typescript
|
|
532
|
+
assert.strictEqual(viewport[0], "Expected text"); // Good
|
|
533
|
+
assert.strictEqual(viewport[0].trim(), "Expected"); // Bad
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
4. **Test rendering strategies**: Verify surgical vs partial vs full
|
|
537
|
+
```typescript
|
|
538
|
+
const beforeLines = ui.getLinesRedrawn();
|
|
539
|
+
// Make change...
|
|
540
|
+
const afterLines = ui.getLinesRedrawn();
|
|
541
|
+
assert.strictEqual(afterLines - beforeLines, 1); // Only 1 line changed
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Performance Testing
|
|
545
|
+
|
|
546
|
+
Use `test/bench.ts` as a template for performance testing:
|
|
636
547
|
|
|
637
548
|
```bash
|
|
638
|
-
|
|
639
|
-
npx tsx test/demo.ts
|
|
549
|
+
npx tsx test/bench.ts
|
|
640
550
|
```
|
|
641
551
|
|
|
642
|
-
|
|
552
|
+
Monitor real-time performance metrics:
|
|
553
|
+
- Render count and timing
|
|
554
|
+
- Lines redrawn per render
|
|
555
|
+
- Visual verification of flicker-free updates
|
|
556
|
+
|
|
557
|
+
## Development
|
|
558
|
+
|
|
559
|
+
```bash
|
|
560
|
+
# Install dependencies (from monorepo root)
|
|
561
|
+
npm install
|
|
562
|
+
|
|
563
|
+
# Run type checking
|
|
564
|
+
npm run check
|
|
565
|
+
|
|
566
|
+
# Run tests
|
|
567
|
+
npm test
|
|
568
|
+
```
|
|
643
569
|
|
|
644
570
|
**Debugging:**
|
|
645
571
|
Enable logging to see detailed component behavior:
|
|
@@ -651,5 +577,3 @@ ui.configureLogging({
|
|
|
651
577
|
logFile: "tui-debug.log",
|
|
652
578
|
});
|
|
653
579
|
```
|
|
654
|
-
|
|
655
|
-
Check the log file to debug rendering issues, input handling, and component lifecycle.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"autocomplete.d.ts","sourceRoot":"","sources":["../src/autocomplete.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"autocomplete.d.ts","sourceRoot":"","sources":["../src/autocomplete.ts"],"names":[],"mappings":"AAuFA,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,sBAAsB,CAAC,CAAC,cAAc,EAAE,MAAM,GAAG,gBAAgB,EAAE,GAAG,IAAI,CAAC;CAC3E;AAED,MAAM,WAAW,oBAAoB;IAGpC,cAAc,CACb,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GACf;QACF,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAC1B,MAAM,EAAE,MAAM,CAAC;KACf,GAAG,IAAI,CAAC;IAIT,eAAe,CACd,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,MAAM,GACZ;QACF,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;KAClB,CAAC;CACF;AAGD,qBAAa,4BAA6B,YAAW,oBAAoB;IACxE,OAAO,CAAC,QAAQ,CAAsC;IACtD,OAAO,CAAC,QAAQ,CAAS;gBAEb,QAAQ,GAAE,CAAC,YAAY,GAAG,gBAAgB,CAAC,EAAO,EAAE,QAAQ,GAAE,MAAsB;IAKhG,cAAc,CACb,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GACf;QAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAqEvD,eAAe,CACd,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,MAAM,GACZ;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IA6D7D,OAAO,CAAC,iBAAiB;IA0CzB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,kBAAkB;IAwI1B,uBAAuB,CACtB,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GACf;QAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAyBvD,2BAA2B,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;CAW5F"}
|