charmed_rust
Build beautiful terminal UIs in Rust with Charm's elegance.
Quick Start • Components • Styling • SSH Apps • FAQ
# Add to Cargo.toml
[]
= { = "charmed-bubbletea", = "0.1.0" }
= { = "charmed-lipgloss", = "0.1.0" }
= { = "charmed-bubbles", = "0.1.0" }
Crates are published under the charmed-* package names on crates.io, while the Rust
crate names remain bubbletea, lipgloss, bubbles, etc. Use package = "charmed-…"
to keep the idiomatic crate names in your code.
TL;DR
The Problem: Building terminal UIs in Rust means fighting raw ANSI codes, wrestling with complex ncurses bindings, or cobbling together half-finished abstractions. Go developers get Charm's elegant ecosystem—beautiful styles, functional architecture, polished components—while Rust developers suffer.
The Solution: charmed_rust ports the entire Charm ecosystem to Rust. Same elegant APIs. Same beautiful output. Rust's type safety, zero-cost abstractions, and fearless concurrency.
Why charmed_rust?
| Feature | What You Get |
|---|---|
| Elm Architecture | Pure update and view functions. Testable. Predictable. No spaghetti. |
| CSS-like Styling | Borders, colors, padding, margins, alignment—lipgloss feels like CSS for terminals |
| 16 Components | Text inputs, lists, tables, spinners, viewports, file pickers—all ready to use |
| Spring Animations | harmonica gives you physics-based motion: springs, projectiles, smooth easing |
| Markdown in Terminal | glamour renders beautiful Markdown with syntax highlighting and themes |
| SSH App Framework | wish serves TUI apps over SSH with middleware patterns—build BBS-style apps |
| 100% Safe Rust | #![forbid(unsafe_code)] everywhere. No segfaults. No data races. Ever. |
Quick Example
use ;
use Style;
Output:
╭────────────────╮
│ Count: 42 │
╰────────────────╯
Press + to increment, - to decrement, q to quit. That's it.
Architecture
┌────────────────────────────────────────────────────────────┐
│ Applications │
│ glow (Markdown Reader) huh (Interactive Forms) │
└────────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ bubbles │ │ glamour │ │ wish │
│ (TUI Components) │ │ (Markdown) │ │ (SSH Framework) │
│ - textinput │ │ - themes │ │ - middleware │
│ - list, table │ │ - word wrap │ │ - sessions │
│ - viewport │ │ - syntax │ │ - PTY support │
│ - spinner │ │ │ │ │
│ - filepicker │ │ │ │ │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│ │ │
└───────────────────┼───────────────────┘
▼
┌────────────────────────────────────────────────────────────┐
│ bubbletea │
│ (Elm Architecture TUI Framework) │
│ Model trait • Message passing • Commands • Event loop │
└────────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ lipgloss │ │ harmonica │ │ charmed_log │
│ (Terminal CSS) │ │ (Animations) │ │ (Logging) │
│ - colors │ │ - spring physics │ │ - text/json │
│ - borders │ │ - projectile │ │ - styled output │
│ - layout │ │ - frame timing │ │ - levels │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│
▼
┌──────────────────┐
│ crossterm │
│ (Terminal I/O) │
└──────────────────┘
Crate Reference
| Crate | Purpose | LOC |
|---|---|---|
| bubbletea | Elm Architecture TUI framework | ~2,300 |
| lipgloss | Terminal styling (colors, borders, padding, alignment) | ~3,000 |
| bubbles | 16 pre-built TUI components | ~8,200 |
| glamour | Markdown rendering with themes | ~1,600 |
| harmonica | Spring physics animations, projectile motion | ~1,100 |
| wish | SSH application framework | ~1,700 |
| huh | Interactive forms and prompts | ~2,700 |
| charmed_log | Structured logging with styled output | ~1,000 |
| glow | Markdown reader CLI | ~160 |
Crates.io Package Names
Crates are published with a charmed- prefix on crates.io to avoid name
collisions, while the Rust crate names remain the same:
| Rust Crate | crates.io Package |
|---|---|
bubbletea |
charmed-bubbletea |
bubbletea_macros |
charmed-bubbletea-macros |
lipgloss |
charmed-lipgloss |
harmonica |
charmed-harmonica |
glamour |
charmed-glamour |
bubbles |
charmed-bubbles |
huh |
charmed-huh |
wish |
charmed-wish |
charmed_log |
charmed-log |
glow |
charmed-glow |
demo_showcase |
charmed-demo-showcase |
charmed_wasm |
charmed-wasm |
Design Philosophy
1. Functional Core, Imperative Shell
The Elm Architecture keeps your logic pure:
// Pure: state + message → new state + effects
// Pure: state → string
Side effects happen through Cmd values. Your business logic stays testable.
2. Composition Over Inheritance
Components are Models. Nest them freely:
3. CSS-like Styling
If you know CSS, you know lipgloss:
let card = new
.border // border-radius
.border_foreground // border-color
.padding // padding: 1em 2em
.margin // margin: 1em
.width // width: 40ch
.align; // text-align: center
4. Zero Unsafe Code
Every crate: #![forbid(unsafe_code)]. Memory safety isn't optional.
5. Go Conformance
Same inputs → same outputs as Go Charm. Migration is seamless. Conformance tests verify compatibility.
Comparison
| Feature | charmed_rust | Go Charm | ratatui | ncurses-rs |
|---|---|---|---|---|
| Architecture | Elm (functional) | Elm (functional) | Immediate mode | Imperative |
| Styling | CSS-like | CSS-like | Widget props | Raw attrs |
| Type Safety | Compile-time | Runtime | Compile-time | Minimal |
| Async | Native tokio | Goroutines | Manual | None |
| Memory Safety | Guaranteed | GC | Depends | Unsafe |
| Components | 16 included | 16 included | 20+ | Manual |
| SSH Framework | ✅ | ✅ | ❌ | ❌ |
| Markdown | ✅ | ✅ | ❌ | ❌ |
| Learning Curve | Moderate | Moderate | Steep | Steep |
Choose charmed_rust when:
- You want Go Charm's elegance with Rust's performance and safety
- You're porting a Go Charm app to Rust
- You prefer functional/Elm-style over immediate mode
- You need SSH-served TUIs
Consider alternatives when:
- You need maximum widget variety (ratatui has more)
- You prefer immediate-mode rendering
- You need ncurses compatibility for legacy systems
Installation
From crates.io (Recommended)
# Cargo.toml
[]
= { = "charmed-bubbletea", = "0.1.0" }
= { = "charmed-lipgloss", = "0.1.0" }
= { = "charmed-bubbles", = "0.1.0" }
= { = "charmed-glamour", = "0.1.0" }
= { = "charmed-harmonica", = "0.1.0" }
= { = "charmed-wish", = "0.1.0" }
= { = "charmed-huh", = "0.1.0" }
= { = "charmed-log", = "0.1.0" }
From Git (Bleeding Edge)
# Cargo.toml
[]
= { = "charmed-bubbletea", = "https://github.com/Dicklesworthstone/charmed_rust" }
= { = "charmed-lipgloss", = "https://github.com/Dicklesworthstone/charmed_rust" }
= { = "charmed-bubbles", = "https://github.com/Dicklesworthstone/charmed_rust" }
= { = "charmed-glamour", = "https://github.com/Dicklesworthstone/charmed_rust" }
= { = "charmed-harmonica", = "https://github.com/Dicklesworthstone/charmed_rust" }
= { = "charmed-wish", = "https://github.com/Dicklesworthstone/charmed_rust" }
= { = "charmed-huh", = "https://github.com/Dicklesworthstone/charmed_rust" }
= { = "charmed-log", = "https://github.com/Dicklesworthstone/charmed_rust" }
With Async Support
= { = "charmed-bubbletea", = "0.1.0", = ["async"] }
= { = "1", = ["rt-multi-thread", "macros"] }
From Source
# Run the Markdown reader
Requirements
- Rust 1.85+ (nightly required for Edition 2024)
- Platforms: Linux, macOS, Windows
Quick Start
1. Create Project
&&
2. Add Dependencies
[]
= { = "charmed-bubbletea", = "0.1.0" }
= { = "charmed-lipgloss", = "0.1.0" }
3. Implement Model
use ;
use Style;
4. Run
lipgloss Styling Examples
Text Styling
use ;
// Bold pink text
let heading = new.bold.foreground;
println!;
// Underlined with background
let highlight = new
.underline
.background
.foreground;
Boxes and Borders
use ;
let card = new
.border
.border_foreground
.padding
.margin;
println!;
Output:
╭────────────────────╮
│ │
│ Content in a card │
│ │
╰────────────────────╯
Adaptive Colors
// Automatically picks color based on terminal background
let adaptive = new
.foreground; // dark bg, light bg
Layout
use ;
// Side-by-side columns
let left = new.width.render;
let right = new.width.render;
let row = join_horizontal;
// Stacked sections
let header = new.bold.render;
let body = "Body content";
let layout = join_vertical;
// Center content in a box
let centered = place;
Theming
lipgloss includes a theming system for consistent colors across your application:
use ;
use Arc;
// Use a built-in preset
let ctx = new;
// Create styles that use semantic color slots
let title = new
.foreground
.bold;
let error = new
.foreground;
println!;
println!;
// Switch themes at runtime - styles update automatically
ctx.set_preset;
Built-in Themes:
Dark/Light- Default themesDracula- Popular purple-tinted dark themeNord- Arctic-inspired paletteCatppuccin- Pastel themes (Latte, Frappe, Macchiato, Mocha)
Semantic Color Slots:
| Slot | Purpose |
|---|---|
Background |
Main background |
Foreground |
Default text |
Primary |
Buttons, links, headings |
Error |
Error messages |
Success |
Confirmations |
Warning |
Alerts |
Muted |
Disabled/placeholder text |
Border |
UI borders |
Load custom themes from files:
use Theme;
// From TOML, JSON, or YAML
let theme = from_file?;
// Validate accessibility
if !theme.check_contrast_aa
See Theming Tutorial for complete documentation.
bubbles Components
TextInput
use TextInput;
let mut input = new;
input.set_placeholder;
input.set_char_limit;
input.set_width;
input.focus;
// In update():
input.update;
// In view():
input.view
List with Filtering
use ;
let items = vec!;
let mut list = new;
list.set_show_filter; // Enable fuzzy search
Table
use ;
let table = new
.columns
.rows
.focused;
Spinner
use ;
use Style;
let spinner = new
.spinner_type
.style;
// Tick it in update() with a timer message
Progress Bar
use Progress;
let progress = new
.width
.show_percentage;
progress.view_as // 75% complete
Viewport (Scrollable)
use Viewport;
let mut viewport = new;
viewport.set_content;
// Scroll with:
viewport.line_down;
viewport.line_up;
viewport.half_page_down;
viewport.goto_top;
File Picker
use FilePicker;
let mut picker = new;
picker.set_current_directory;
picker.set_show_hidden;
picker.set_allowed_types;
Stopwatch & Timer
use Stopwatch;
use Timer;
use Duration;
let stopwatch = new; // Counts up
let timer = new; // 5-minute countdown
Paginator
use Paginator;
let mut paginator = new;
paginator.set_total_pages;
paginator.set_per_page;
glamour Markdown Rendering
use ;
let markdown = r#"
# Hello World
This is **bold** and *italic* text.
```rust
fn main() {
println!("Code blocks work too!");
}
- Lists
- Are
- Supported "#;
// Render with default theme let output = render(markdown)?; println!("{}", output);
// Or with a specific theme let themed = glamour::render_with_theme(markdown, Theme::dark())?;
### Available Themes
- `Theme::dark()` - For dark terminal backgrounds
- `Theme::light()` - For light terminal backgrounds
- `Theme::dracula()` - Dracula color scheme
- `Theme::ascii()` - Plain ASCII, no colors
- `Theme::notty()` - No formatting (for piping)
---
## wish SSH Framework
Build SSH-accessible TUI applications:
```rust
use wish::{Server, Session, Handler};
use bubbletea::{Program, Model};
struct MyApp { /* ... */ }
impl Model for MyApp { /* ... */ }
struct MyHandler;
impl Handler for MyHandler {
fn handle(&self, session: Session) {
// Each SSH connection gets its own TUI instance
let app = MyApp::new();
Program::new(app)
.with_input(session.stdin())
.with_output(session.stdout())
.run()
.unwrap();
}
}
fn main() {
Server::new()
.address("0.0.0.0:2222")
.host_key_path("./host_key")
.handler(MyHandler)
.run()
.unwrap();
}
Users connect with: ssh -p 2222 localhost
Async Support
Enable tokio-based async for non-blocking I/O:
[]
= { = "charmed-bubbletea", = "0.1.0", = ["async"] }
= { = "1", = ["rt-multi-thread", "macros"] }
use ;
use Duration;
// Async command for HTTP fetch
// Async timer (non-blocking)
async
Demo Showcase
The flagship demo shows off all functionality in charmed_rust:
Options
| Flag | Description |
|---|---|
--theme <name> |
Color theme: dark, light, dracula, nord, catppuccin-* |
--seed <n> |
Seed for reproducible randomness |
--no-alt-screen |
Stay in main terminal buffer |
--no-mouse |
Disable mouse support |
--help |
Show all available options |
Examples
# Run with Nord theme
# Run with Dracula theme
# Reproducible demo state
What It Demonstrates
- Multi-page navigation - Dashboard, Services, Jobs, Logs, Docs, Files, Wizard, Settings
- Theme switching - Switch themes at runtime with keyboard shortcuts
- All bubbles components - Lists, tables, spinners, progress bars, text inputs, viewports
- Markdown rendering - glamour with syntax highlighting
- Form interactions - Interactive forms via huh
- Spring animations - Smooth physics-based animations via harmonica
- File browser - Navigate the filesystem
- Full keyboard & mouse support
glow CLI Reference
glow is a terminal Markdown reader built on glamour:
# Read a file
# Read from stdin
|
# With specific theme
# Pager mode (for long documents)
Options
| Flag | Description |
|---|---|
--theme <name> |
Color theme: dark, light, dracula, ascii |
--pager |
Enable scrollable pager mode |
--width <n> |
Set render width (default: terminal width) |
--style <path> |
Load custom style JSON |
Configuration
Environment Variables
| Variable | Description | Default |
|---|---|---|
COLORTERM |
Color support (truecolor, 256) |
auto-detect |
NO_COLOR |
Disable all colors if set | unset |
GLAMOUR_STYLE |
Default glamour theme | dark |
TERM |
Terminal type | auto-detect |
Programmatic Configuration
use ;
// Force 256-color mode
let renderer = new
.color_profile;
// Disable colors entirely
let renderer = new
.color_profile;
Troubleshooting
"failed to select a version for bubbletea"
Use the published package name and alias it to the bubbletea crate:
# ❌ Wrong
= "0.1"
# ✅ Correct
= { = "charmed-bubbletea", = "0.1.0" }
Terminal not restoring after crash
bubbletea uses alternate screen mode. Reset with:
# or
Colors not showing
Check true color support:
Fallback to 256 colors:
let color = ansi256; // Pink in 256-color palette
"cannot find trait Model"
Add the import:
use Model;
Windows: garbled output
Use Windows Terminal, not cmd.exe. Or enable virtual terminal:
// crossterm handles this, but needs a modern terminal
Viewport not scrolling
Ensure you're forwarding key messages:
SSH connections rejected
Check host key permissions:
Limitations
Current State
| Capability | Status | Notes |
|---|---|---|
| crates.io | ✅ Published | Install via charmed-* packages |
| Nightly Rust | Required | Edition 2024 |
| SSH (wish) | ⚠️ Beta | Framework ready, deps maturing |
| Mouse drag | ⚠️ Limited | Click/scroll work, selection needs terminal support |
| Complex Unicode | ⚠️ Basic | unicode-width handles most cases |
| Windows SSH | ⚠️ Untested | Linux/macOS verified |
Not Planned
- Built-in syntax highlighting:
glamourdetects code blocks but delegates highlighting tosyntect - GUI rendering: Terminal only—use
eguioricedfor GUI - ncurses compatibility: Clean break from legacy
FAQ
Why "charmed_rust"?
Charm + Rust = charmed_rust. The TUIs are pretty charming too.
Can I use lipgloss without bubbletea?
Yes. It's completely standalone:
use Style;
println!;
Is this API-compatible with Go Charm?
Semantically yes. Method names follow Rust conventions (set_width vs Width), but the patterns are identical.
How do I test TUI apps?
Use headless mode:
let model = new.without_renderer.run?;
assert_eq!;
Or test update/view directly:
let mut app = default;
app.update;
assert!;
Does it work in Docker/CI?
Yes. Use without_renderer() or set TERM=dumb for non-interactive environments.
How do I handle window resize?
Subscribe to resize events:
Can I use custom fonts/icons?
The terminal controls fonts. Use Nerd Fonts for icons:
let icon = ""; // Nerd Font: nf-fa-folder
How do I debug render issues?
Log the view output:
Or use charmed_log to file:
init_file?;
debug!;
Conformance Testing
Verify behavior matches Go Charm:
# All conformance tests
# Specific crates
Fixtures captured from Go reference implementations live in tests/conformance/go_reference/.
Performance
Benchmarks run on Apple M2:
| Operation | Time |
|---|---|
| Style creation | ~50ns |
| Simple render | ~200ns |
| Complex layout (10 boxes) | ~2μs |
| Markdown page render | ~500μs |
| Message dispatch | ~100ns |
Memory: Typical TUI app uses 2-5MB RSS.
Run benchmarks:
About Contributions
Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via gh and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
License
MIT License - see LICENSE for details.
Built with Rust. Inspired by Charm.