mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 01:00:24 +00:00
Update tui CHANGELOG and README
- Add changelog entries for new key detection functions and TUI.onDebug - Completely rewrite README with accurate documentation: - All components documented (added TruncatedText, SettingsList) - All theme interfaces documented (EditorTheme, MarkdownTheme, SelectListTheme, SettingsListTheme, ImageTheme) - Fixed incorrect constructor signatures - Added key detection utilities section - Added utilities section
This commit is contained in:
parent
9b2d22d26d
commit
adbe0c9b4f
2 changed files with 224 additions and 50 deletions
|
|
@ -2,6 +2,16 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `isShiftCtrlO()` key detection function for Shift+Ctrl+O (Kitty protocol)
|
||||||
|
- `isShiftCtrlD()` key detection function for Shift+Ctrl+D (Kitty protocol)
|
||||||
|
- `TUI.onDebug` callback for global debug key handling (Shift+Ctrl+D)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- README.md completely rewritten with accurate component documentation, theme interfaces, and examples
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Markdown component now renders HTML tags as plain text instead of silently dropping them ([#359](https://github.com/badlogic/pi-mono/issues/359))
|
- Markdown component now renders HTML tags as plain text instead of silently dropping them ([#359](https://github.com/badlogic/pi-mono/issues/359))
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ Minimal terminal UI framework with differential rendering and synchronized outpu
|
||||||
- **Synchronized Output**: Uses CSI 2026 for atomic screen updates (no flicker)
|
- **Synchronized Output**: Uses CSI 2026 for atomic screen updates (no flicker)
|
||||||
- **Bracketed Paste Mode**: Handles large pastes correctly with markers for >10 line pastes
|
- **Bracketed Paste Mode**: Handles large pastes correctly with markers for >10 line pastes
|
||||||
- **Component-based**: Simple Component interface with render() method
|
- **Component-based**: Simple Component interface with render() method
|
||||||
- **Built-in Components**: Text, Input, Editor, Markdown, Loader, SelectList, Spacer, Image, Box, Container
|
- **Theme Support**: Components accept theme interfaces for customizable styling
|
||||||
|
- **Built-in Components**: Text, TruncatedText, Input, Editor, Markdown, Loader, SelectList, SettingsList, Spacer, Image, Box, Container
|
||||||
- **Inline Images**: Renders images in terminals that support Kitty or iTerm2 graphics protocols
|
- **Inline Images**: Renders images in terminals that support Kitty or iTerm2 graphics protocols
|
||||||
- **Autocomplete Support**: File paths and slash commands
|
- **Autocomplete Support**: File paths and slash commands
|
||||||
|
|
||||||
|
|
@ -26,10 +27,10 @@ const tui = new TUI(terminal);
|
||||||
// Add components
|
// Add components
|
||||||
tui.addChild(new Text("Welcome to my app!"));
|
tui.addChild(new Text("Welcome to my app!"));
|
||||||
|
|
||||||
const editor = new Editor();
|
const editor = new Editor(editorTheme);
|
||||||
editor.onSubmit = (text) => {
|
editor.onSubmit = (text) => {
|
||||||
console.log("Submitted:", text);
|
console.log("Submitted:", text);
|
||||||
tui.addChild(new Text(`You said: ${text}`));
|
tui.addChild(new Text(`You said: ${text}`));
|
||||||
};
|
};
|
||||||
tui.addChild(editor);
|
tui.addChild(editor);
|
||||||
|
|
||||||
|
|
@ -50,6 +51,9 @@ tui.removeChild(component);
|
||||||
tui.start();
|
tui.start();
|
||||||
tui.stop();
|
tui.stop();
|
||||||
tui.requestRender(); // Request a re-render
|
tui.requestRender(); // Request a re-render
|
||||||
|
|
||||||
|
// Global debug key handler (Shift+Ctrl+D)
|
||||||
|
tui.onDebug = () => console.log("Debug triggered");
|
||||||
```
|
```
|
||||||
|
|
||||||
### Component Interface
|
### Component Interface
|
||||||
|
|
@ -58,8 +62,9 @@ All components implement:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface Component {
|
interface Component {
|
||||||
render(width: number): string[];
|
render(width: number): string[];
|
||||||
handleInput?(data: string): void;
|
handleInput?(data: string): void;
|
||||||
|
invalidate?(): void;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -81,11 +86,11 @@ Container that applies padding and background color to all children.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const box = new Box(
|
const box = new Box(
|
||||||
1, // paddingX (default: 1)
|
1, // paddingX (default: 1)
|
||||||
1, // paddingY (default: 1)
|
1, // paddingY (default: 1)
|
||||||
(text) => chalk.bgGray(text) // optional background function
|
(text) => chalk.bgGray(text) // optional background function
|
||||||
);
|
);
|
||||||
box.addChild(new Text("Content", 0, 0));
|
box.addChild(new Text("Content"));
|
||||||
box.setBgFn((text) => chalk.bgBlue(text)); // Change background dynamically
|
box.setBgFn((text) => chalk.bgBlue(text)); // Change background dynamically
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -94,8 +99,26 @@ box.setBgFn((text) => chalk.bgBlue(text)); // Change background dynamically
|
||||||
Displays multi-line text with word wrapping and padding.
|
Displays multi-line text with word wrapping and padding.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const text = new Text("Hello World", paddingX, paddingY); // defaults: 1, 1
|
const text = new Text(
|
||||||
|
"Hello World", // text content
|
||||||
|
1, // paddingX (default: 1)
|
||||||
|
1, // paddingY (default: 1)
|
||||||
|
(text) => chalk.bgGray(text) // optional background function
|
||||||
|
);
|
||||||
text.setText("Updated text");
|
text.setText("Updated text");
|
||||||
|
text.setCustomBgFn((text) => chalk.bgBlue(text));
|
||||||
|
```
|
||||||
|
|
||||||
|
### TruncatedText
|
||||||
|
|
||||||
|
Single-line text that truncates to fit viewport width. Useful for status lines and headers.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const truncated = new TruncatedText(
|
||||||
|
"This is a very long line that will be truncated...",
|
||||||
|
0, // paddingX (default: 0)
|
||||||
|
0 // paddingY (default: 0)
|
||||||
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Input
|
### Input
|
||||||
|
|
@ -106,14 +129,17 @@ Single-line text input with horizontal scrolling.
|
||||||
const input = new Input();
|
const input = new Input();
|
||||||
input.onSubmit = (value) => console.log(value);
|
input.onSubmit = (value) => console.log(value);
|
||||||
input.setValue("initial");
|
input.setValue("initial");
|
||||||
|
input.getValue();
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key Bindings:**
|
**Key Bindings:**
|
||||||
- `Enter` - Submit
|
- `Enter` - Submit
|
||||||
- `Ctrl+A` / `Ctrl+E` - Line start/end
|
- `Ctrl+A` / `Ctrl+E` - Line start/end
|
||||||
- `Ctrl+W` or `Option+Backspace` - Delete word backwards
|
- `Ctrl+W` or `Alt+Backspace` - Delete word backwards
|
||||||
- `Ctrl+U` - Delete to start of line
|
- `Ctrl+U` - Delete to start of line
|
||||||
- `Ctrl+K` - Delete to end of line
|
- `Ctrl+K` - Delete to end of line
|
||||||
|
- `Ctrl+Left` / `Ctrl+Right` - Word navigation
|
||||||
|
- `Alt+Left` / `Alt+Right` - Word navigation
|
||||||
- Arrow keys, Backspace, Delete work as expected
|
- Arrow keys, Backspace, Delete work as expected
|
||||||
|
|
||||||
### Editor
|
### Editor
|
||||||
|
|
@ -121,11 +147,17 @@ input.setValue("initial");
|
||||||
Multi-line text editor with autocomplete, file completion, and paste handling.
|
Multi-line text editor with autocomplete, file completion, and paste handling.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const editor = new Editor();
|
interface EditorTheme {
|
||||||
|
borderColor: (str: string) => string;
|
||||||
|
selectList: SelectListTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = new Editor(theme);
|
||||||
editor.onSubmit = (text) => console.log(text);
|
editor.onSubmit = (text) => console.log(text);
|
||||||
editor.onChange = (text) => console.log("Changed:", text);
|
editor.onChange = (text) => console.log("Changed:", text);
|
||||||
editor.disableSubmit = true; // Disable submit temporarily
|
editor.disableSubmit = true; // Disable submit temporarily
|
||||||
editor.setAutocompleteProvider(provider);
|
editor.setAutocompleteProvider(provider);
|
||||||
|
editor.borderColor = (s) => chalk.blue(s); // Change border dynamically
|
||||||
```
|
```
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
@ -146,24 +178,50 @@ editor.setAutocompleteProvider(provider);
|
||||||
|
|
||||||
### Markdown
|
### Markdown
|
||||||
|
|
||||||
Renders markdown with syntax highlighting and optional background colors.
|
Renders markdown with syntax highlighting and theming support.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
interface MarkdownTheme {
|
||||||
|
heading: (text: string) => string;
|
||||||
|
link: (text: string) => string;
|
||||||
|
linkUrl: (text: string) => string;
|
||||||
|
code: (text: string) => string;
|
||||||
|
codeBlock: (text: string) => string;
|
||||||
|
codeBlockBorder: (text: string) => string;
|
||||||
|
quote: (text: string) => string;
|
||||||
|
quoteBorder: (text: string) => string;
|
||||||
|
hr: (text: string) => string;
|
||||||
|
listBullet: (text: string) => string;
|
||||||
|
bold: (text: string) => string;
|
||||||
|
italic: (text: string) => string;
|
||||||
|
strikethrough: (text: string) => string;
|
||||||
|
underline: (text: string) => string;
|
||||||
|
highlightCode?: (code: string, lang?: string) => string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DefaultTextStyle {
|
||||||
|
color?: (text: string) => string;
|
||||||
|
bgColor?: (text: string) => string;
|
||||||
|
bold?: boolean;
|
||||||
|
italic?: boolean;
|
||||||
|
strikethrough?: boolean;
|
||||||
|
underline?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const md = new Markdown(
|
const md = new Markdown(
|
||||||
"# Hello\n\nSome **bold** text",
|
"# Hello\n\nSome **bold** text",
|
||||||
bgColor, // optional: "bgRed", "bgBlue", etc.
|
1, // paddingX
|
||||||
fgColor, // optional: "white", "cyan", etc.
|
1, // paddingY
|
||||||
customBgRgb, // optional: { r: 52, g: 53, b: 65 }
|
theme, // MarkdownTheme
|
||||||
paddingX, // optional: default 1
|
defaultStyle // optional DefaultTextStyle
|
||||||
paddingY // optional: default 1
|
|
||||||
);
|
);
|
||||||
md.setText("Updated markdown");
|
md.setText("Updated markdown");
|
||||||
```
|
```
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- Headings, bold, italic, code blocks, lists, links, blockquotes
|
- Headings, bold, italic, code blocks, lists, links, blockquotes
|
||||||
- Syntax highlighting with chalk
|
- HTML tags rendered as plain text
|
||||||
- Optional background colors (including custom RGB)
|
- Optional syntax highlighting via `highlightCode`
|
||||||
- Padding support
|
- Padding support
|
||||||
- Render caching for performance
|
- Render caching for performance
|
||||||
|
|
||||||
|
|
@ -172,8 +230,14 @@ md.setText("Updated markdown");
|
||||||
Animated loading spinner.
|
Animated loading spinner.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const loader = new Loader(tui, "Loading...");
|
const loader = new Loader(
|
||||||
|
tui, // TUI instance for render updates
|
||||||
|
(s) => chalk.cyan(s), // spinner color function
|
||||||
|
(s) => chalk.gray(s), // message color function
|
||||||
|
"Loading..." // message (default: "Loading...")
|
||||||
|
);
|
||||||
loader.start();
|
loader.start();
|
||||||
|
loader.setMessage("Still loading...");
|
||||||
loader.stop();
|
loader.stop();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -182,19 +246,78 @@ loader.stop();
|
||||||
Interactive selection list with keyboard navigation.
|
Interactive selection list with keyboard navigation.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const list = new SelectList([
|
interface SelectItem {
|
||||||
{ value: "opt1", label: "Option 1", description: "First option" },
|
value: string;
|
||||||
{ value: "opt2", label: "Option 2", description: "Second option" },
|
label: string;
|
||||||
], 5); // maxVisible
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectListTheme {
|
||||||
|
selectedPrefix: (text: string) => string;
|
||||||
|
selectedText: (text: string) => string;
|
||||||
|
description: (text: string) => string;
|
||||||
|
scrollInfo: (text: string) => string;
|
||||||
|
noMatch: (text: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = new SelectList(
|
||||||
|
[
|
||||||
|
{ value: "opt1", label: "Option 1", description: "First option" },
|
||||||
|
{ value: "opt2", label: "Option 2", description: "Second option" },
|
||||||
|
],
|
||||||
|
5, // maxVisible
|
||||||
|
theme // SelectListTheme
|
||||||
|
);
|
||||||
|
|
||||||
list.onSelect = (item) => console.log("Selected:", item);
|
list.onSelect = (item) => console.log("Selected:", item);
|
||||||
list.onCancel = () => console.log("Cancelled");
|
list.onCancel = () => console.log("Cancelled");
|
||||||
|
list.onSelectionChange = (item) => console.log("Highlighted:", item);
|
||||||
list.setFilter("opt"); // Filter items
|
list.setFilter("opt"); // Filter items
|
||||||
```
|
```
|
||||||
|
|
||||||
**Controls:**
|
**Controls:**
|
||||||
- Arrow keys: Navigate
|
- Arrow keys: Navigate
|
||||||
- Enter or Tab: Select
|
- Enter: Select
|
||||||
|
- Escape: Cancel
|
||||||
|
|
||||||
|
### SettingsList
|
||||||
|
|
||||||
|
Settings panel with value cycling and submenus.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface SettingItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
currentValue: string;
|
||||||
|
values?: string[]; // If provided, Enter/Space cycles through these
|
||||||
|
submenu?: (currentValue: string, done: (selectedValue?: string) => void) => Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsListTheme {
|
||||||
|
label: (text: string, selected: boolean) => string;
|
||||||
|
value: (text: string, selected: boolean) => string;
|
||||||
|
description: (text: string) => string;
|
||||||
|
cursor: string;
|
||||||
|
hint: (text: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = new SettingsList(
|
||||||
|
[
|
||||||
|
{ id: "theme", label: "Theme", currentValue: "dark", values: ["dark", "light"] },
|
||||||
|
{ id: "model", label: "Model", currentValue: "gpt-4", submenu: (val, done) => modelSelector },
|
||||||
|
],
|
||||||
|
10, // maxVisible
|
||||||
|
theme, // SettingsListTheme
|
||||||
|
(id, newValue) => console.log(`${id} changed to ${newValue}`),
|
||||||
|
() => console.log("Cancelled")
|
||||||
|
);
|
||||||
|
settings.updateValue("theme", "light");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Controls:**
|
||||||
|
- Arrow keys: Navigate
|
||||||
|
- Enter/Space: Activate (cycle value or open submenu)
|
||||||
- Escape: Cancel
|
- Escape: Cancel
|
||||||
|
|
||||||
### Spacer
|
### Spacer
|
||||||
|
|
@ -210,13 +333,21 @@ const spacer = new Spacer(2); // 2 empty lines (default: 1)
|
||||||
Renders images inline for terminals that support the Kitty graphics protocol (Kitty, Ghostty, WezTerm) or iTerm2 inline images. Falls back to a text placeholder on unsupported terminals.
|
Renders images inline for terminals that support the Kitty graphics protocol (Kitty, Ghostty, WezTerm) or iTerm2 inline images. Falls back to a text placeholder on unsupported terminals.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Image } from "@mariozechner/pi-tui";
|
interface ImageTheme {
|
||||||
|
fallbackColor: (str: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageOptions {
|
||||||
|
maxWidthCells?: number;
|
||||||
|
maxHeightCells?: number;
|
||||||
|
filename?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const image = new Image(
|
const image = new Image(
|
||||||
base64Data, // base64-encoded image data
|
base64Data, // base64-encoded image data
|
||||||
"image/png", // MIME type
|
"image/png", // MIME type
|
||||||
{ fallbackColor: (s) => s }, // theme for fallback text
|
theme, // ImageTheme
|
||||||
{ maxWidthCells: 60 } // optional: limit width
|
options // optional ImageOptions
|
||||||
);
|
);
|
||||||
tui.addChild(image);
|
tui.addChild(image);
|
||||||
```
|
```
|
||||||
|
|
@ -233,12 +364,12 @@ Supports both slash commands and file paths.
|
||||||
import { CombinedAutocompleteProvider } from "@mariozechner/pi-tui";
|
import { CombinedAutocompleteProvider } from "@mariozechner/pi-tui";
|
||||||
|
|
||||||
const provider = new CombinedAutocompleteProvider(
|
const provider = new CombinedAutocompleteProvider(
|
||||||
[
|
[
|
||||||
{ name: "help", description: "Show help" },
|
{ name: "help", description: "Show help" },
|
||||||
{ name: "clear", description: "Clear screen" },
|
{ name: "clear", description: "Clear screen" },
|
||||||
{ name: "delete", description: "Delete last message" },
|
{ name: "delete", description: "Delete last message" },
|
||||||
],
|
],
|
||||||
process.cwd() // base path for file completion
|
process.cwd() // base path for file completion
|
||||||
);
|
);
|
||||||
|
|
||||||
editor.setAutocompleteProvider(provider);
|
editor.setAutocompleteProvider(provider);
|
||||||
|
|
@ -250,6 +381,27 @@ editor.setAutocompleteProvider(provider);
|
||||||
- Works with `~/`, `./`, `../`, and `@` prefix
|
- Works with `~/`, `./`, `../`, and `@` prefix
|
||||||
- Filters to attachable files for `@` prefix
|
- Filters to attachable files for `@` prefix
|
||||||
|
|
||||||
|
## Key Detection
|
||||||
|
|
||||||
|
Helper functions for detecting keyboard input (supports Kitty keyboard protocol):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
isEnter, isEscape, isTab, isShiftTab,
|
||||||
|
isArrowUp, isArrowDown, isArrowLeft, isArrowRight,
|
||||||
|
isCtrlA, isCtrlC, isCtrlE, isCtrlK, isCtrlO, isCtrlP,
|
||||||
|
isCtrlLeft, isCtrlRight, isAltLeft, isAltRight,
|
||||||
|
isShiftEnter, isAltEnter,
|
||||||
|
isShiftCtrlO, isShiftCtrlD, isShiftCtrlP,
|
||||||
|
isBackspace, isDelete, isHome, isEnd,
|
||||||
|
// ... and more
|
||||||
|
} from "@mariozechner/pi-tui";
|
||||||
|
|
||||||
|
if (isCtrlC(data)) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Differential Rendering
|
## Differential Rendering
|
||||||
|
|
||||||
The TUI uses three rendering strategies:
|
The TUI uses three rendering strategies:
|
||||||
|
|
@ -266,17 +418,17 @@ The TUI works with any object implementing the `Terminal` interface:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface Terminal {
|
interface Terminal {
|
||||||
start(onInput: (data: string) => void, onResize: () => void): void;
|
start(onInput: (data: string) => void, onResize: () => void): void;
|
||||||
stop(): void;
|
stop(): void;
|
||||||
write(data: string): void;
|
write(data: string): void;
|
||||||
get columns(): number;
|
get columns(): number;
|
||||||
get rows(): number;
|
get rows(): number;
|
||||||
moveBy(lines: number): void;
|
moveBy(lines: number): void;
|
||||||
hideCursor(): void;
|
hideCursor(): void;
|
||||||
showCursor(): void;
|
showCursor(): void;
|
||||||
clearLine(): void;
|
clearLine(): void;
|
||||||
clearFromCursor(): void;
|
clearFromCursor(): void;
|
||||||
clearScreen(): void;
|
clearScreen(): void;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -284,6 +436,18 @@ interface Terminal {
|
||||||
- `ProcessTerminal` - Uses `process.stdin/stdout`
|
- `ProcessTerminal` - Uses `process.stdin/stdout`
|
||||||
- `VirtualTerminal` - For testing (uses `@xterm/headless`)
|
- `VirtualTerminal` - For testing (uses `@xterm/headless`)
|
||||||
|
|
||||||
|
## Utilities
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
||||||
|
|
||||||
|
// Get visible width of string (ignoring ANSI codes)
|
||||||
|
const width = visibleWidth("\x1b[31mHello\x1b[0m"); // 5
|
||||||
|
|
||||||
|
// Truncate string to width (preserving ANSI codes)
|
||||||
|
const truncated = truncateToWidth("Hello World", 8); // "Hello..."
|
||||||
|
```
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
See `test/chat-simple.ts` for a complete chat interface example with:
|
See `test/chat-simple.ts` for a complete chat interface example with:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue