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:
Mario Zechner 2025-12-30 23:22:41 +01:00
parent 9b2d22d26d
commit adbe0c9b4f
2 changed files with 224 additions and 50 deletions

View file

@ -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))

View file

@ -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: