From adbe0c9b4fa634e84e413a316573c358533127cd Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 30 Dec 2025 23:22:41 +0100 Subject: [PATCH] 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 --- packages/tui/CHANGELOG.md | 10 ++ packages/tui/README.md | 264 ++++++++++++++++++++++++++++++-------- 2 files changed, 224 insertions(+), 50 deletions(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index e1c04859..e3ae4435 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,16 @@ ## [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 - Markdown component now renders HTML tags as plain text instead of silently dropping them ([#359](https://github.com/badlogic/pi-mono/issues/359)) diff --git a/packages/tui/README.md b/packages/tui/README.md index f8066ac7..5e88344b 100644 --- a/packages/tui/README.md +++ b/packages/tui/README.md @@ -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) - **Bracketed Paste Mode**: Handles large pastes correctly with markers for >10 line pastes - **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 - **Autocomplete Support**: File paths and slash commands @@ -26,10 +27,10 @@ const tui = new TUI(terminal); // Add components tui.addChild(new Text("Welcome to my app!")); -const editor = new Editor(); +const editor = new Editor(editorTheme); editor.onSubmit = (text) => { - console.log("Submitted:", text); - tui.addChild(new Text(`You said: ${text}`)); + console.log("Submitted:", text); + tui.addChild(new Text(`You said: ${text}`)); }; tui.addChild(editor); @@ -50,6 +51,9 @@ tui.removeChild(component); tui.start(); tui.stop(); tui.requestRender(); // Request a re-render + +// Global debug key handler (Shift+Ctrl+D) +tui.onDebug = () => console.log("Debug triggered"); ``` ### Component Interface @@ -58,8 +62,9 @@ All components implement: ```typescript interface Component { - render(width: number): string[]; - handleInput?(data: string): void; + render(width: number): string[]; + handleInput?(data: string): void; + invalidate?(): void; } ``` @@ -81,11 +86,11 @@ Container that applies padding and background color to all children. ```typescript const box = new Box( - 1, // paddingX (default: 1) - 1, // paddingY (default: 1) - (text) => chalk.bgGray(text) // optional background function + 1, // paddingX (default: 1) + 1, // paddingY (default: 1) + (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 ``` @@ -94,8 +99,26 @@ box.setBgFn((text) => chalk.bgBlue(text)); // Change background dynamically Displays multi-line text with word wrapping and padding. ```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.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 @@ -106,14 +129,17 @@ Single-line text input with horizontal scrolling. const input = new Input(); input.onSubmit = (value) => console.log(value); input.setValue("initial"); +input.getValue(); ``` **Key Bindings:** - `Enter` - Submit - `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+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 ### Editor @@ -121,11 +147,17 @@ input.setValue("initial"); Multi-line text editor with autocomplete, file completion, and paste handling. ```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.onChange = (text) => console.log("Changed:", text); editor.disableSubmit = true; // Disable submit temporarily editor.setAutocompleteProvider(provider); +editor.borderColor = (s) => chalk.blue(s); // Change border dynamically ``` **Features:** @@ -146,24 +178,50 @@ editor.setAutocompleteProvider(provider); ### Markdown -Renders markdown with syntax highlighting and optional background colors. +Renders markdown with syntax highlighting and theming support. ```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( - "# Hello\n\nSome **bold** text", - bgColor, // optional: "bgRed", "bgBlue", etc. - fgColor, // optional: "white", "cyan", etc. - customBgRgb, // optional: { r: 52, g: 53, b: 65 } - paddingX, // optional: default 1 - paddingY // optional: default 1 + "# Hello\n\nSome **bold** text", + 1, // paddingX + 1, // paddingY + theme, // MarkdownTheme + defaultStyle // optional DefaultTextStyle ); md.setText("Updated markdown"); ``` **Features:** - Headings, bold, italic, code blocks, lists, links, blockquotes -- Syntax highlighting with chalk -- Optional background colors (including custom RGB) +- HTML tags rendered as plain text +- Optional syntax highlighting via `highlightCode` - Padding support - Render caching for performance @@ -172,8 +230,14 @@ md.setText("Updated markdown"); Animated loading spinner. ```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.setMessage("Still loading..."); loader.stop(); ``` @@ -182,19 +246,78 @@ loader.stop(); Interactive selection list with keyboard navigation. ```typescript -const list = new SelectList([ - { value: "opt1", label: "Option 1", description: "First option" }, - { value: "opt2", label: "Option 2", description: "Second option" }, -], 5); // maxVisible +interface SelectItem { + value: string; + label: string; + 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.onCancel = () => console.log("Cancelled"); +list.onSelectionChange = (item) => console.log("Highlighted:", item); list.setFilter("opt"); // Filter items ``` **Controls:** - 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 ### 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. ```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( - base64Data, // base64-encoded image data - "image/png", // MIME type - { fallbackColor: (s) => s }, // theme for fallback text - { maxWidthCells: 60 } // optional: limit width + base64Data, // base64-encoded image data + "image/png", // MIME type + theme, // ImageTheme + options // optional ImageOptions ); tui.addChild(image); ``` @@ -233,12 +364,12 @@ Supports both slash commands and file paths. import { CombinedAutocompleteProvider } from "@mariozechner/pi-tui"; const provider = new CombinedAutocompleteProvider( - [ - { name: "help", description: "Show help" }, - { name: "clear", description: "Clear screen" }, - { name: "delete", description: "Delete last message" }, - ], - process.cwd() // base path for file completion + [ + { name: "help", description: "Show help" }, + { name: "clear", description: "Clear screen" }, + { name: "delete", description: "Delete last message" }, + ], + process.cwd() // base path for file completion ); editor.setAutocompleteProvider(provider); @@ -250,6 +381,27 @@ editor.setAutocompleteProvider(provider); - Works with `~/`, `./`, `../`, and `@` 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 The TUI uses three rendering strategies: @@ -266,17 +418,17 @@ The TUI works with any object implementing the `Terminal` interface: ```typescript interface Terminal { - start(onInput: (data: string) => void, onResize: () => void): void; - stop(): void; - write(data: string): void; - get columns(): number; - get rows(): number; - moveBy(lines: number): void; - hideCursor(): void; - showCursor(): void; - clearLine(): void; - clearFromCursor(): void; - clearScreen(): void; + start(onInput: (data: string) => void, onResize: () => void): void; + stop(): void; + write(data: string): void; + get columns(): number; + get rows(): number; + moveBy(lines: number): void; + hideCursor(): void; + showCursor(): void; + clearLine(): void; + clearFromCursor(): void; + clearScreen(): void; } ``` @@ -284,6 +436,18 @@ interface Terminal { - `ProcessTerminal` - Uses `process.stdin/stdout` - `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 See `test/chat-simple.ts` for a complete chat interface example with: