@mariozechner/pi-tui 0.5.48 → 0.7.0
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 +166 -475
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +2 -0
- package/dist/autocomplete.js.map +1 -1
- package/dist/components/{text-editor.d.ts → editor.d.ts} +9 -5
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/{text-editor.js → editor.js} +125 -70
- package/dist/components/editor.js.map +1 -0
- package/dist/components/input.d.ts +14 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +120 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/{loading-animation.d.ts → loader.d.ts} +5 -5
- package/dist/components/loader.d.ts.map +1 -0
- package/dist/components/{loading-animation.js → loader.js} +13 -10
- package/dist/components/loader.js.map +1 -0
- package/dist/components/markdown.d.ts +46 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +499 -0
- package/dist/components/markdown.js.map +1 -0
- package/dist/components/select-list.d.ts +3 -3
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +24 -16
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/spacer.d.ts +11 -0
- package/dist/components/spacer.d.ts.map +1 -0
- package/dist/components/spacer.js +20 -0
- package/dist/components/spacer.js.map +1 -0
- package/dist/components/text.d.ts +26 -0
- package/dist/components/text.d.ts.map +1 -0
- package/dist/components/text.js +141 -0
- package/dist/components/text.js.map +1 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -12
- package/dist/index.js.map +1 -1
- package/dist/terminal.d.ts +12 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +33 -3
- package/dist/terminal.js.map +1 -1
- package/dist/tui.d.ts +30 -52
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +131 -337
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +15 -0
- package/dist/utils.js.map +1 -0
- package/package.json +6 -5
- package/dist/components/loading-animation.d.ts.map +0 -1
- package/dist/components/loading-animation.js.map +0 -1
- package/dist/components/markdown-component.d.ts +0 -15
- package/dist/components/markdown-component.d.ts.map +0 -1
- package/dist/components/markdown-component.js +0 -247
- package/dist/components/markdown-component.js.map +0 -1
- package/dist/components/text-component.d.ts +0 -14
- package/dist/components/text-component.d.ts.map +0 -1
- package/dist/components/text-component.js +0 -90
- package/dist/components/text-component.js.map +0 -1
- package/dist/components/text-editor.d.ts.map +0 -1
- package/dist/components/text-editor.js.map +0 -1
- package/dist/components/whitespace-component.d.ts +0 -13
- package/dist/components/whitespace-component.d.ts.map +0 -1
- package/dist/components/whitespace-component.js +0 -22
- package/dist/components/whitespace-component.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,559 +1,261 @@
|
|
|
1
1
|
# @mariozechner/pi-tui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Minimal terminal UI framework with differential rendering and synchronized output for flicker-free interactive CLI applications.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
7
|
+
- **Differential Rendering**: Three-strategy rendering system that only updates what changed
|
|
8
|
+
- **Synchronized Output**: Uses CSI 2026 for atomic screen updates (no flicker)
|
|
9
|
+
- **Bracketed Paste Mode**: Handles large pastes correctly with markers for >10 line pastes
|
|
10
|
+
- **Component-based**: Simple Component interface with render() method
|
|
11
|
+
- **Built-in Components**: Text, Input, Editor, Markdown, Loader, SelectList, Spacer
|
|
12
|
+
- **Autocomplete Support**: File paths and slash commands
|
|
12
13
|
|
|
13
14
|
## Quick Start
|
|
14
15
|
|
|
15
16
|
```typescript
|
|
16
|
-
import { TUI,
|
|
17
|
-
|
|
18
|
-
// Create TUI manager
|
|
19
|
-
const ui = new TUI();
|
|
20
|
-
|
|
21
|
-
// Create components
|
|
22
|
-
const header = new TextComponent("🚀 My TUI App");
|
|
23
|
-
const chatContainer = new Container();
|
|
24
|
-
const editor = new TextEditor();
|
|
25
|
-
|
|
26
|
-
// Add components to UI
|
|
27
|
-
ui.addChild(header);
|
|
28
|
-
ui.addChild(chatContainer);
|
|
29
|
-
ui.addChild(editor);
|
|
30
|
-
|
|
31
|
-
// Set focus to the editor
|
|
32
|
-
ui.setFocus(editor);
|
|
33
|
-
|
|
34
|
-
// Handle editor submissions
|
|
35
|
-
editor.onSubmit = (text: string) => {
|
|
36
|
-
if (text.trim()) {
|
|
37
|
-
const message = new TextComponent(`💬 ${text}`);
|
|
38
|
-
chatContainer.addChild(message);
|
|
39
|
-
// Note: Container automatically calls requestRender when children change
|
|
40
|
-
}
|
|
41
|
-
};
|
|
17
|
+
import { TUI, Text, Editor, ProcessTerminal } from "@mariozechner/pi-tui";
|
|
42
18
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
```
|
|
19
|
+
// Create terminal
|
|
20
|
+
const terminal = new ProcessTerminal();
|
|
46
21
|
|
|
47
|
-
|
|
22
|
+
// Create TUI
|
|
23
|
+
const tui = new TUI(terminal);
|
|
48
24
|
|
|
49
|
-
|
|
25
|
+
// Add components
|
|
26
|
+
tui.addChild(new Text("Welcome to my app!"));
|
|
50
27
|
|
|
51
|
-
|
|
28
|
+
const editor = new Editor();
|
|
29
|
+
editor.onSubmit = (text) => {
|
|
30
|
+
console.log("Submitted:", text);
|
|
31
|
+
tui.addChild(new Text(`You said: ${text}`));
|
|
32
|
+
};
|
|
33
|
+
tui.addChild(editor);
|
|
52
34
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
35
|
+
// Start
|
|
36
|
+
tui.start();
|
|
37
|
+
```
|
|
60
38
|
|
|
61
|
-
|
|
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
|
|
39
|
+
## Core API
|
|
68
40
|
|
|
69
|
-
###
|
|
41
|
+
### TUI
|
|
70
42
|
|
|
71
|
-
|
|
43
|
+
Main container that manages components and rendering.
|
|
72
44
|
|
|
73
45
|
```typescript
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
46
|
+
const tui = new TUI(terminal);
|
|
47
|
+
tui.addChild(component);
|
|
48
|
+
tui.removeChild(component);
|
|
49
|
+
tui.start();
|
|
50
|
+
tui.stop();
|
|
51
|
+
tui.requestRender(); // Request a re-render
|
|
78
52
|
```
|
|
79
53
|
|
|
80
|
-
###
|
|
54
|
+
### Component Interface
|
|
81
55
|
|
|
82
|
-
|
|
56
|
+
All components implement:
|
|
83
57
|
|
|
84
58
|
```typescript
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
### TextComponent
|
|
100
|
-
|
|
101
|
-
Simple text display with automatic word wrapping.
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
const text = new TextComponent("Hello World", { top: 1, bottom: 1 });
|
|
105
|
-
text.setText("Updated text");
|
|
59
|
+
interface Component {
|
|
60
|
+
render(width: number): string[];
|
|
61
|
+
handleInput?(data: string): void;
|
|
62
|
+
}
|
|
106
63
|
```
|
|
107
64
|
|
|
108
|
-
|
|
65
|
+
## Built-in Components
|
|
109
66
|
|
|
110
|
-
|
|
67
|
+
### Container
|
|
111
68
|
|
|
112
|
-
|
|
69
|
+
Groups child components.
|
|
113
70
|
|
|
114
71
|
```typescript
|
|
115
|
-
new
|
|
72
|
+
const container = new Container();
|
|
73
|
+
container.addChild(component);
|
|
74
|
+
container.removeChild(component);
|
|
116
75
|
```
|
|
117
76
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
- `setText(text)` - Update markdown content
|
|
121
|
-
- `render(width)` - Render parsed markdown
|
|
122
|
-
|
|
123
|
-
**Features:**
|
|
124
|
-
|
|
125
|
-
- **Headings**: Styled with colors and formatting
|
|
126
|
-
- **Code blocks**: Syntax highlighting with gray background
|
|
127
|
-
- **Lists**: Bullet points (•) and numbered lists
|
|
128
|
-
- **Emphasis**: **Bold** and _italic_ text
|
|
129
|
-
- **Links**: Underlined with URL display
|
|
130
|
-
- **Blockquotes**: Styled with left border
|
|
131
|
-
- **Inline code**: Highlighted with background
|
|
132
|
-
- **Horizontal rules**: Terminal-width separator lines
|
|
133
|
-
- Differential rendering for performance
|
|
134
|
-
|
|
135
|
-
### SelectList
|
|
136
|
-
|
|
137
|
-
Interactive selection component for choosing from options.
|
|
77
|
+
### Text
|
|
138
78
|
|
|
139
|
-
|
|
79
|
+
Displays multi-line text with word wrapping and padding.
|
|
140
80
|
|
|
141
81
|
```typescript
|
|
142
|
-
new
|
|
143
|
-
|
|
144
|
-
interface SelectItem {
|
|
145
|
-
value: string;
|
|
146
|
-
label: string;
|
|
147
|
-
description?: string;
|
|
148
|
-
}
|
|
82
|
+
const text = new Text("Hello World", paddingX, paddingY); // defaults: 1, 1
|
|
83
|
+
text.setText("Updated text");
|
|
149
84
|
```
|
|
150
85
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
- `onSelect?: (item: SelectItem) => void` - Called when item is selected
|
|
154
|
-
- `onCancel?: () => void` - Called when selection is cancelled
|
|
155
|
-
|
|
156
|
-
**Methods:**
|
|
157
|
-
|
|
158
|
-
- `setFilter(filter)` - Filter items by value
|
|
159
|
-
- `getSelectedItem()` - Get currently selected item
|
|
160
|
-
- `handleInput(keyData)` - Handle keyboard navigation
|
|
161
|
-
- `render(width)` - Render the selection list
|
|
162
|
-
|
|
163
|
-
**Features:**
|
|
164
|
-
|
|
165
|
-
- Keyboard navigation (arrow keys, Enter)
|
|
166
|
-
- Search/filter functionality
|
|
167
|
-
- Scrolling for long lists
|
|
168
|
-
- Custom option rendering with descriptions
|
|
169
|
-
- Visual selection indicator (→)
|
|
170
|
-
- Scroll position indicator
|
|
171
|
-
|
|
172
|
-
### Autocomplete System
|
|
173
|
-
|
|
174
|
-
Comprehensive autocomplete system supporting slash commands and file paths.
|
|
86
|
+
### Input
|
|
175
87
|
|
|
176
|
-
|
|
88
|
+
Single-line text input with horizontal scrolling.
|
|
177
89
|
|
|
178
90
|
```typescript
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
cursorLine: number,
|
|
183
|
-
cursorCol: number,
|
|
184
|
-
): {
|
|
185
|
-
items: AutocompleteItem[];
|
|
186
|
-
prefix: string;
|
|
187
|
-
} | null;
|
|
188
|
-
|
|
189
|
-
applyCompletion(
|
|
190
|
-
lines: string[],
|
|
191
|
-
cursorLine: number,
|
|
192
|
-
cursorCol: number,
|
|
193
|
-
item: AutocompleteItem,
|
|
194
|
-
prefix: string,
|
|
195
|
-
): {
|
|
196
|
-
lines: string[];
|
|
197
|
-
cursorLine: number;
|
|
198
|
-
cursorCol: number;
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
interface AutocompleteItem {
|
|
203
|
-
value: string;
|
|
204
|
-
label: string;
|
|
205
|
-
description?: string;
|
|
206
|
-
}
|
|
91
|
+
const input = new Input();
|
|
92
|
+
input.onSubmit = (value) => console.log(value);
|
|
93
|
+
input.setValue("initial");
|
|
207
94
|
```
|
|
208
95
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
Built-in provider supporting slash commands and file completion.
|
|
96
|
+
### Editor
|
|
212
97
|
|
|
213
|
-
|
|
98
|
+
Multi-line text editor with autocomplete, file completion, and paste handling.
|
|
214
99
|
|
|
215
100
|
```typescript
|
|
216
|
-
new
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
interface SlashCommand {
|
|
222
|
-
name: string;
|
|
223
|
-
description?: string;
|
|
224
|
-
getArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;
|
|
225
|
-
}
|
|
101
|
+
const editor = new Editor();
|
|
102
|
+
editor.onSubmit = (text) => console.log(text);
|
|
103
|
+
editor.onChange = (text) => console.log("Changed:", text);
|
|
104
|
+
editor.disableSubmit = true; // Disable submit temporarily
|
|
105
|
+
editor.setAutocompleteProvider(provider);
|
|
226
106
|
```
|
|
227
107
|
|
|
228
108
|
**Features:**
|
|
109
|
+
- Multi-line editing with word wrap
|
|
110
|
+
- Slash command autocomplete (type `/`)
|
|
111
|
+
- File path autocomplete (press `Tab`)
|
|
112
|
+
- Large paste handling (>10 lines creates `[paste #1 +50 lines]` marker)
|
|
113
|
+
- Horizontal lines above/below editor
|
|
114
|
+
- Fake cursor rendering (hidden real cursor)
|
|
229
115
|
|
|
230
|
-
**
|
|
231
|
-
|
|
232
|
-
-
|
|
233
|
-
-
|
|
234
|
-
-
|
|
235
|
-
-
|
|
236
|
-
|
|
237
|
-
**File Completion:**
|
|
238
|
-
|
|
239
|
-
- `Tab` key triggers file completion
|
|
240
|
-
- `@` prefix for file attachments
|
|
241
|
-
- Home directory expansion (`~/`)
|
|
242
|
-
- Relative and absolute path support
|
|
243
|
-
- Directory-first sorting
|
|
244
|
-
- Filters to attachable files for `@` prefix
|
|
245
|
-
|
|
246
|
-
**Path Patterns:**
|
|
247
|
-
|
|
248
|
-
- `./` and `../` - Relative paths
|
|
249
|
-
- `~/` - Home directory
|
|
250
|
-
- `@path` - File attachment syntax
|
|
251
|
-
- Tab completion from any context
|
|
252
|
-
|
|
253
|
-
**Methods:**
|
|
254
|
-
|
|
255
|
-
- `getSuggestions()` - Get completions for current context
|
|
256
|
-
- `getForceFileSuggestions()` - Force file completion (Tab key)
|
|
257
|
-
- `shouldTriggerFileCompletion()` - Check if file completion should trigger
|
|
258
|
-
- `applyCompletion()` - Apply selected completion
|
|
259
|
-
|
|
260
|
-
## Surgical Differential Rendering
|
|
261
|
-
|
|
262
|
-
The TUI uses a three-strategy rendering system that minimizes redraws to only what's necessary:
|
|
263
|
-
|
|
264
|
-
### Rendering Strategies
|
|
265
|
-
|
|
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
|
|
270
|
-
|
|
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
|
|
275
|
-
|
|
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
|
|
116
|
+
**Key Bindings:**
|
|
117
|
+
- `Enter` - Submit
|
|
118
|
+
- `Shift+Enter` or `Ctrl+Enter` - New line
|
|
119
|
+
- `Tab` - Autocomplete
|
|
120
|
+
- `Ctrl+K` - Delete line
|
|
121
|
+
- `Ctrl+A` / `Ctrl+E` - Line start/end
|
|
122
|
+
- Arrow keys, Backspace, Delete work as expected
|
|
280
123
|
|
|
281
|
-
###
|
|
124
|
+
### Markdown
|
|
282
125
|
|
|
283
|
-
|
|
126
|
+
Renders markdown with syntax highlighting and optional background colors.
|
|
284
127
|
|
|
285
128
|
```typescript
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
129
|
+
const md = new Markdown(
|
|
130
|
+
"# Hello\n\nSome **bold** text",
|
|
131
|
+
bgColor, // optional: "bgRed", "bgBlue", etc.
|
|
132
|
+
fgColor, // optional: "white", "cyan", etc.
|
|
133
|
+
customBgRgb, // optional: { r: 52, g: 53, b: 65 }
|
|
134
|
+
paddingX, // optional: default 1
|
|
135
|
+
paddingY // optional: default 1
|
|
136
|
+
);
|
|
137
|
+
md.setText("Updated markdown");
|
|
296
138
|
```
|
|
297
139
|
|
|
298
|
-
|
|
140
|
+
**Features:**
|
|
141
|
+
- Headings, bold, italic, code blocks, lists, links, blockquotes
|
|
142
|
+
- Syntax highlighting with chalk
|
|
143
|
+
- Optional background colors (including custom RGB)
|
|
144
|
+
- Padding support
|
|
145
|
+
- Render caching for performance
|
|
299
146
|
|
|
300
|
-
###
|
|
147
|
+
### Loader
|
|
301
148
|
|
|
302
|
-
|
|
149
|
+
Animated loading spinner.
|
|
303
150
|
|
|
304
151
|
```typescript
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
console.log(`Average per render: ${ui.getAverageLinesRedrawn()}`);
|
|
152
|
+
const loader = new Loader(tui, "Loading...");
|
|
153
|
+
loader.start();
|
|
154
|
+
loader.stop();
|
|
309
155
|
```
|
|
310
156
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
## Examples
|
|
314
|
-
|
|
315
|
-
Run the example applications in the `test/` directory:
|
|
316
|
-
|
|
317
|
-
```bash
|
|
318
|
-
# Chat application with slash commands and autocomplete
|
|
319
|
-
npx tsx test/chat-app.ts
|
|
320
|
-
|
|
321
|
-
# File browser with navigation
|
|
322
|
-
npx tsx test/file-browser.ts
|
|
157
|
+
### SelectList
|
|
323
158
|
|
|
324
|
-
|
|
325
|
-
npx tsx test/multi-layout.ts
|
|
159
|
+
Interactive selection list with keyboard navigation.
|
|
326
160
|
|
|
327
|
-
|
|
328
|
-
|
|
161
|
+
```typescript
|
|
162
|
+
const list = new SelectList([
|
|
163
|
+
{ value: "opt1", label: "Option 1", description: "First option" },
|
|
164
|
+
{ value: "opt2", label: "Option 2", description: "Second option" },
|
|
165
|
+
], 5); // maxVisible
|
|
166
|
+
|
|
167
|
+
list.onSelect = (item) => console.log("Selected:", item);
|
|
168
|
+
list.onCancel = () => console.log("Cancelled");
|
|
169
|
+
list.setFilter("opt"); // Filter items
|
|
329
170
|
```
|
|
330
171
|
|
|
331
|
-
|
|
172
|
+
**Controls:**
|
|
173
|
+
- Arrow keys: Navigate
|
|
174
|
+
- Enter or Tab: Select
|
|
175
|
+
- Escape: Cancel
|
|
332
176
|
|
|
333
|
-
|
|
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
|
|
177
|
+
### Spacer
|
|
337
178
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
### Core Types
|
|
179
|
+
Empty lines for vertical spacing.
|
|
341
180
|
|
|
342
181
|
```typescript
|
|
343
|
-
|
|
344
|
-
lines: string[];
|
|
345
|
-
changed: boolean;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
interface ContainerRenderResult extends ComponentRenderResult {
|
|
349
|
-
keepLines: number;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
interface Component {
|
|
353
|
-
render(width: number): ComponentRenderResult;
|
|
354
|
-
handleInput?(keyData: string): void;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
interface Padding {
|
|
358
|
-
top?: number;
|
|
359
|
-
bottom?: number;
|
|
360
|
-
left?: number;
|
|
361
|
-
right?: number;
|
|
362
|
-
}
|
|
182
|
+
const spacer = new Spacer(2); // 2 empty lines (default: 1)
|
|
363
183
|
```
|
|
364
184
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
```typescript
|
|
368
|
-
interface AutocompleteItem {
|
|
369
|
-
value: string;
|
|
370
|
-
label: string;
|
|
371
|
-
description?: string;
|
|
372
|
-
}
|
|
185
|
+
## Autocomplete
|
|
373
186
|
|
|
374
|
-
|
|
375
|
-
name: string;
|
|
376
|
-
description?: string;
|
|
377
|
-
getArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;
|
|
378
|
-
}
|
|
187
|
+
### CombinedAutocompleteProvider
|
|
379
188
|
|
|
380
|
-
|
|
381
|
-
getSuggestions(
|
|
382
|
-
lines: string[],
|
|
383
|
-
cursorLine: number,
|
|
384
|
-
cursorCol: number,
|
|
385
|
-
): {
|
|
386
|
-
items: AutocompleteItem[];
|
|
387
|
-
prefix: string;
|
|
388
|
-
} | null;
|
|
389
|
-
|
|
390
|
-
applyCompletion(
|
|
391
|
-
lines: string[],
|
|
392
|
-
cursorLine: number,
|
|
393
|
-
cursorCol: number,
|
|
394
|
-
item: AutocompleteItem,
|
|
395
|
-
prefix: string,
|
|
396
|
-
): {
|
|
397
|
-
lines: string[];
|
|
398
|
-
cursorLine: number;
|
|
399
|
-
cursorCol: number;
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
### Selection Types
|
|
189
|
+
Supports both slash commands and file paths.
|
|
405
190
|
|
|
406
191
|
```typescript
|
|
407
|
-
|
|
408
|
-
value: string;
|
|
409
|
-
label: string;
|
|
410
|
-
description?: string;
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
## Testing
|
|
415
|
-
|
|
416
|
-
### Running Tests
|
|
417
|
-
|
|
418
|
-
```bash
|
|
419
|
-
# Run all tests
|
|
420
|
-
npm test
|
|
192
|
+
import { CombinedAutocompleteProvider } from "@mariozechner/pi-tui";
|
|
421
193
|
|
|
422
|
-
|
|
423
|
-
|
|
194
|
+
const provider = new CombinedAutocompleteProvider(
|
|
195
|
+
[
|
|
196
|
+
{ name: "help", description: "Show help" },
|
|
197
|
+
{ name: "clear", description: "Clear screen" },
|
|
198
|
+
{ name: "delete", description: "Delete last message" },
|
|
199
|
+
],
|
|
200
|
+
process.cwd() // base path for file completion
|
|
201
|
+
);
|
|
424
202
|
|
|
425
|
-
|
|
426
|
-
npm test -- --test-name-pattern="preserves existing"
|
|
203
|
+
editor.setAutocompleteProvider(provider);
|
|
427
204
|
```
|
|
428
205
|
|
|
429
|
-
|
|
206
|
+
**Features:**
|
|
207
|
+
- Type `/` to see slash commands
|
|
208
|
+
- Press `Tab` for file path completion
|
|
209
|
+
- Works with `~/`, `./`, `../`, and `@` prefix
|
|
210
|
+
- Filters to attachable files for `@` prefix
|
|
430
211
|
|
|
431
|
-
|
|
212
|
+
## Differential Rendering
|
|
432
213
|
|
|
433
|
-
|
|
434
|
-
import { VirtualTerminal } from "./test/virtual-terminal.js";
|
|
435
|
-
import { TUI, TextComponent } from "../src/index.js";
|
|
214
|
+
The TUI uses three rendering strategies:
|
|
436
215
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
ui.start();
|
|
216
|
+
1. **First Render**: Output all lines without clearing scrollback
|
|
217
|
+
2. **Width Changed or Change Above Viewport**: Clear screen and full re-render
|
|
218
|
+
3. **Normal Update**: Move cursor to first changed line, clear to end, render changed lines
|
|
441
219
|
|
|
442
|
-
|
|
220
|
+
All updates are wrapped in **synchronized output** (`\x1b[?2026h` ... `\x1b[?2026l`) for atomic, flicker-free rendering.
|
|
443
221
|
|
|
444
|
-
|
|
445
|
-
await new Promise(resolve => process.nextTick(resolve));
|
|
222
|
+
## Terminal Interface
|
|
446
223
|
|
|
447
|
-
|
|
448
|
-
const viewport = await terminal.flushAndGetViewport();
|
|
449
|
-
assert.strictEqual(viewport[0], "Hello");
|
|
224
|
+
The TUI works with any object implementing the `Terminal` interface:
|
|
450
225
|
|
|
451
|
-
|
|
452
|
-
|
|
226
|
+
```typescript
|
|
227
|
+
interface Terminal {
|
|
228
|
+
start(onInput: (data: string) => void, onResize: () => void): void;
|
|
229
|
+
stop(): void;
|
|
230
|
+
write(data: string): void;
|
|
231
|
+
get columns(): number;
|
|
232
|
+
get rows(): number;
|
|
233
|
+
moveBy(lines: number): void;
|
|
234
|
+
hideCursor(): void;
|
|
235
|
+
showCursor(): void;
|
|
236
|
+
clearLine(): void;
|
|
237
|
+
clearFromCursor(): void;
|
|
238
|
+
clearScreen(): void;
|
|
239
|
+
}
|
|
453
240
|
```
|
|
454
241
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
2. **Use VirtualTerminal** for accurate terminal emulation
|
|
459
|
-
3. **Key testing patterns**:
|
|
242
|
+
**Built-in implementations:**
|
|
243
|
+
- `ProcessTerminal` - Uses `process.stdin/stdout`
|
|
244
|
+
- `VirtualTerminal` - For testing (uses `@xterm/headless`)
|
|
460
245
|
|
|
461
|
-
|
|
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
|
-
```
|
|
246
|
+
## Example
|
|
503
247
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
-
|
|
507
|
-
-
|
|
508
|
-
-
|
|
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:
|
|
248
|
+
See `test/chat-simple.ts` for a complete chat interface example with:
|
|
249
|
+
- Markdown messages with custom background colors
|
|
250
|
+
- Loading spinner during responses
|
|
251
|
+
- Editor with autocomplete and slash commands
|
|
252
|
+
- Spacers between messages
|
|
547
253
|
|
|
254
|
+
Run it:
|
|
548
255
|
```bash
|
|
549
|
-
npx tsx test/
|
|
256
|
+
npx tsx test/chat-simple.ts
|
|
550
257
|
```
|
|
551
258
|
|
|
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
259
|
## Development
|
|
558
260
|
|
|
559
261
|
```bash
|
|
@@ -563,17 +265,6 @@ npm install
|
|
|
563
265
|
# Run type checking
|
|
564
266
|
npm run check
|
|
565
267
|
|
|
566
|
-
# Run
|
|
567
|
-
|
|
568
|
-
```
|
|
569
|
-
|
|
570
|
-
**Debugging:**
|
|
571
|
-
Enable logging to see detailed component behavior:
|
|
572
|
-
|
|
573
|
-
```typescript
|
|
574
|
-
ui.configureLogging({
|
|
575
|
-
enabled: true,
|
|
576
|
-
level: "debug", // "error" | "warn" | "info" | "debug"
|
|
577
|
-
logFile: "tui-debug.log",
|
|
578
|
-
});
|
|
268
|
+
# Run the demo
|
|
269
|
+
npx tsx test/chat-simple.ts
|
|
579
270
|
```
|