@oakoliver/bubbles
v1.0.3
Published
Pre-built TUI components for TypeScript — zero-dependency port of Charmbracelet's Bubbles
Maintainers
Readme
@oakoliver/bubbles
Pre-built TUI components for TypeScript. A pure TypeScript port of charmbracelet/bubbles with zero dependencies.
Built on top of @oakoliver/bubbletea (Elm Architecture) and @oakoliver/lipgloss (terminal styling).
Features
- 15 ready-to-use TUI components
- Zero runtime dependencies (peer dependencies on lipgloss + bubbletea)
- Full Elm Architecture: each component has
update(msg)andview() - Keybinding system with help text generation
- ESM and CommonJS builds with full TypeScript declarations
Install
npm install @oakoliver/bubbles @oakoliver/lipgloss @oakoliver/bubbleteaComponents
Input
| Component | Description | |-----------|-------------| | TextInput | Single-line text input with cursor, echo modes, suggestions, and completion | | TextArea | Multi-line text editor with word wrapping, line numbers, and soft wrap |
Display
| Component | Description | |-----------|-------------| | Spinner | Animated spinner with 12 built-in styles (Line, Dot, MiniDot, Jump, Pulse, Points, Globe, Moon, Monkey, Meter, Hamburger, Ellipsis) | | Progress | Progress bar with spring-physics animation | | Table | Tabular data with headers, row selection, and scrollable viewport | | Viewport | Scrollable content viewer with soft wrapping, gutter functions, and horizontal scroll |
Navigation
| Component | Description |
|-----------|-------------|
| List | Full-featured list browser with fuzzy filtering, pagination, and customizable delegates |
| Paginator | Page navigation with Arabic (1/5) or Dots (●○○) display |
| FilePicker | File system browser using Node.js fs |
Utility
| Component | Description |
|-----------|-------------|
| Help | Auto-generated help view from keybindings (short and full modes) |
| Key | Keybinding system — Binding class with matches(), withKeys(), withHelp() |
| Cursor | Virtual blinking cursor with focus/blur and multiple blink modes |
| Timer | Countdown timer with start/stop/toggle |
| Stopwatch | Count-up stopwatch with start/stop/toggle/reset |
Quick Start
import {
SpinnerModel, newSpinner, Line,
ProgressModel, newProgress,
TextInputModel, newTextInput,
ViewportModel, newViewport, withWidth, withHeight,
} from '@oakoliver/bubbles';Spinner
import { SpinnerModel, newSpinner, Dot } from '@oakoliver/bubbles';
import type { Msg, Cmd } from '@oakoliver/bubbletea';
const spinner = newSpinner(Dot);
const view = spinner.view(); // "⣾"
// In your update loop:
const [updated, cmd] = spinner.update(msg);Text Input
import { TextInputModel, newTextInput } from '@oakoliver/bubbles';
const input = newTextInput();
input.placeholder = 'Type something...';
input.charLimit = 100;
input.focus();
const view = input.view();
const [updated, cmd] = input.update(msg);
const value = input.value();Text Area
import { TextAreaModel, newTextarea } from '@oakoliver/bubbles';
const ta = newTextarea();
ta.placeholder = 'Enter your message...';
ta.setWidth(80);
ta.setHeight(10);
ta.focus();
const view = ta.view();
const [updated, cmd] = ta.update(msg);Progress Bar
import { ProgressModel, newProgress } from '@oakoliver/bubbles';
const bar = newProgress();
bar.setPercent(0.75);
const view = bar.view(); // "████████████████████░░░░░░░"Viewport
import { ViewportModel, newViewport, withWidth, withHeight } from '@oakoliver/bubbles';
const vp = newViewport(withWidth(80), withHeight(24));
vp.setContent(longString);
const view = vp.view();
const [updated, cmd] = vp.update(msg);Table
import { TableModel, newTable, withTableColumns, withTableRows } from '@oakoliver/bubbles';
const table = newTable(
withTableColumns([
{ title: 'Name', width: 20 },
{ title: 'Age', width: 5 },
]),
withTableRows([
['Alice', '30'],
['Bob', '25'],
]),
);
table.focus();
const view = table.view();List
import { ListModel, newList } from '@oakoliver/bubbles';
import type { ListItem, ListItemDelegate } from '@oakoliver/bubbles';
class MyItem implements ListItem {
constructor(public name: string) {}
filterValue() { return this.name; }
}
const list = newList(items, delegate, 80, 24);
const view = list.view();Help
import { HelpModel, newHelp, newBinding, withKeys, withHelp } from '@oakoliver/bubbles';
const quit = newBinding(withKeys('q', 'ctrl+c'), withHelp('q', 'quit'));
const keyMap = {
shortHelp: () => [quit],
fullHelp: () => [[quit]],
};
const help = newHelp();
help.view(keyMap); // "q quit"Keybindings
import { Binding, newBinding, withKeys, withHelp, withDisabled, matches } from '@oakoliver/bubbles';
const quit = newBinding(
withKeys('q', 'ctrl+c'),
withHelp('q', 'quit'),
);
// In update():
if (matches(msg, quit)) {
return [model, Quit];
}Paginator
import { PaginatorModel, newPaginator, Arabic, Dots } from '@oakoliver/bubbles';
const pager = newPaginator();
pager.type = Dots;
pager.totalPages = 5;
pager.page = 2;
pager.view(); // "○ ○ ● ○ ○"Timer & Stopwatch
import { TimerModel, newTimer, StopwatchModel, newStopwatch } from '@oakoliver/bubbles';
// Countdown
const timer = newTimer(30000); // 30 seconds
const [t, cmd] = timer.update(msg);
// Count up
const sw = newStopwatch();
const [s, cmd2] = sw.update(msg);Keybinding System
The key module provides a composable keybinding system used by all components:
import { Binding, newBinding, withKeys, withHelp, withDisabled, matches } from '@oakoliver/bubbles';
// Create a binding
const save = newBinding(
withKeys('ctrl+s'),
withHelp('ctrl+s', 'save file'),
);
// Check if a key message matches
if (matches(keyMsg, save)) { /* ... */ }
// Disable/enable at runtime
save.setEnabled(false);API Pattern
Every component follows the Bubbletea Elm Architecture:
// Create
const model = newComponent(options);
// Update on each message
const [updated, cmd] = model.update(msg);
// Render
const output = model.view();Components are immutable-style — update() returns a [Model, Cmd | null] tuple.
Attribution
This is a TypeScript port of bubbles by Charmbracelet, Inc., licensed under MIT.
License
MIT
