Skip to main content

Minimal Python Charm stack — raw terminal TUI framework

Project description

snaptui

A zero-dependency Python TUI framework that ports the Charm stack's approach to terminal UIs: the Elm Architecture, raw terminal I/O, and ANSI escape sequences.

Why this exists

Python's TUI options are either curses-based (complex, stateful, platform-dependent) or heavy frameworks like Textual (hundreds of dependencies, CSS layout engine, async runtime). The Go ecosystem solved this with the Charm stack: a set of composable libraries that render TUIs by writing ANSI escape sequences directly to stdout. This makes rendering fast and the architecture simple -- your entire UI is a function that returns a string.

snaptui brings that approach to Python:

  • No curses. Reads raw bytes from stdin, writes ANSI sequences to stdout.
  • No dependencies. Uses only the Python standard library (termios, select, signal, fcntl).
  • Elm Architecture. Your app is init(), update(msg), view() -- nothing else.
  • Fast rendering. A line-diff renderer only redraws what changed, just like Bubble Tea.

How the Elm Architecture works

from snaptui import Model, Cmd, KeyMsg, WindowSizeMsg, quit_cmd

class Counter:
    def __init__(self):
        self.count = 0

    def init(self) -> Cmd:
        return None

    def update(self, msg) -> tuple['Counter', Cmd]:
        if isinstance(msg, KeyMsg):
            if msg.key == 'q':
                return self, quit_cmd
            elif msg.key == 'up':
                self.count += 1
            elif msg.key == 'down':
                self.count -= 1
        return self, None

    def view(self) -> str:
        return f"Count: {self.count}\n\nup/down to change, q to quit"

The program calls view() after every update(), diffs the output, and writes only the changed lines. There's no widget tree, no layout engine, no virtual DOM -- just a string.

Why escape sequences make this fast

Traditional TUI frameworks (curses, Textual) maintain an internal screen buffer and compute cell-by-cell diffs. snaptui skips all of that:

  1. Your view() returns a string with embedded ANSI codes for colors, bold, etc.
  2. The renderer splits it into lines and compares against the previous frame.
  3. Only changed lines get written using cursor-movement escape sequences.
  4. One flush() per frame -- the terminal handles the rest.

Because ANSI rendering is done by the terminal emulator (which is GPU-accelerated in modern terminals like Ghostty, Kitty, WezTerm), the Python side only needs to produce and diff strings. There's no pixel math, no redraw-the-whole-screen, no intermediate buffer.

Why not cell-level diffing?

Bubble Tea v2 introduced a "Cursed Renderer" that parses view output through an ANSI state machine, builds a 2D grid of cells (each with character + style attributes), and diffs individual cells against the previous frame. snaptui deliberately does not adopt this approach.

The reason is the ratio of computation cost to I/O savings. In Go, the ANSI parser is compiled machine code -- iterating 80x40 = 3,200 cells, comparing structs, and building output takes microseconds. The I/O savings (writing 200 bytes instead of 3,000) are worth more than the computation cost.

In Python, the same state-machine parser would iterate character-by-character through interpreted code, create objects for each cell, and do attribute comparisons through Python's object protocol. For 3,200 cells this easily costs 1-5ms, while the I/O savings of ~2,800 fewer bytes is maybe 0.1ms at TTY write speeds. You'd spend 5ms of CPU to save 0.1ms of I/O.

The line-diff renderer sidesteps this: string equality (old == new) is a single C-level comparison in CPython. No parsing, no cell objects, no per-character work. The cost is near-zero, and unchanged lines (the majority of the screen on any given frame) are skipped entirely.

What's ported from where

snaptui combines features from four Charm libraries into one package, porting from these exact versions:

Go module Version
charmbracelet/bubbletea v1.3.10
charmbracelet/lipgloss v1.1.0
charmbracelet/bubbles v1.0.0
charmbracelet/huh v0.8.0
charmbracelet/x/ansi v0.11.6

Here's what's implemented and what's not:

Core Framework

Feature Charm Source snaptui Status
Elm Architecture (init/update/view) bubbletea Model protocol, Program Done
Message types (Key, WindowSize, Quit) bubbletea KeyMsg, WindowSizeMsg, QuitMsg Done
Command system (async Cmd) bubbletea Cmd, batch(), quit_cmd Done
Raw terminal mode (termios) bubbletea terminal.make_raw() Done
Alternate screen buffer bubbletea Program(alt_screen=True) Done
Line-diff renderer bubbletea renderer.Renderer Done
Keyboard input parsing (70+ sequences) bubbletea keys.read_key() Done
SIGWINCH resize handling bubbletea terminal.listen_for_resize() Done
Mouse input events (SGR) bubbletea MouseMsg, Program(mouse=True) Done
Bracketed paste events bubbletea PasteMsg, Program(bracketed_paste=True) Done
Declarative View struct (cursor, title, alt screen) bubbletea v2 View dataclass Done
Key modifier bitfield bubbletea v2 Mod IntFlag on KeyMsg Done
Focus/blur events bubbletea v2 FocusMsg Done
Kitty keyboard protocol bubbletea v2 -- Not yet
Clipboard (OSC 52) bubbletea v2 ClipboardMsg, set_clipboard() Done
Subscriptions (long-running listeners) bubbletea Sub, Model.subscriptions() Done
Suspend/resume (Ctrl+Z) bubbletea SIGTSTP/SIGCONT handling in Program Done

Styling

Feature Charm Source snaptui Status
Chainable style builder lipgloss Style class Done
Text attributes (bold, dim, italic, underline, reverse, strikethrough) lipgloss Style.bold(), .dim(), etc. Done
Underline styles (curly, dotted, dashed, double) and color lipgloss v2 Style.underline_style(), .underline_color() Done
True color (24-bit RGB) lipgloss Style.fg(), .bg() Done
Padding (CSS shorthand) lipgloss Style.padding() Done
Margin (CSS shorthand) lipgloss Style.margin() Done
Width/height constraints lipgloss Style.width(), .height() Done
Max width/height lipgloss Style.max_width(), .max_height() Done
Auto word-wrap on .width() lipgloss Style._apply_wrap() Done
Borders (5 types, per-side, colored) lipgloss Style.border(), .border_fg() Done
Horizontal alignment lipgloss Style.align() Done
Immutable builder pattern lipgloss Style._copy() Done
ANSI 16/256 color fallback lipgloss -- Not yet
Adaptive colors (light/dark) lipgloss -- Not yet
Vertical alignment lipgloss -- Not yet
Tab width lipgloss -- Not yet
Table rendering lipgloss/table -- Not yet
List rendering (enumerators) lipgloss/list -- Not yet
Tree rendering lipgloss/tree -- Not yet

Layout

Feature Charm Source snaptui Status
Join blocks horizontally lipgloss join_horizontal() Done
Join blocks vertically lipgloss join_vertical() Done
Place content in canvas (2D) lipgloss place() Done
PlaceHorizontal / PlaceVertical lipgloss -- Not yet

String Utilities

Feature Charm Source snaptui Status
ANSI-aware visible width x/ansi strutil.visible_width() Done
ANSI stripping x/ansi strutil.strip_ansi() Done
ANSI-aware truncation x/ansi strutil.truncate() Done
ANSI-aware word wrap x/ansi strutil.word_wrap() Done
ANSI-aware padding x/ansi strutil.pad_right() Done
CJK wide character support x/ansi strutil._char_width() Done
Grapheme cluster segmentation x/ansi -- Not yet
Hard wrap (non-word-boundary) x/ansi -- Not yet (hard-break exists within word_wrap)
Truncate with tail ("...") x/ansi strutil.truncate(s, w, tail="...") Done

Components

Component Charm Source snaptui Status
Single-line text input bubbles/textinput TextInput Done
Multi-line text area bubbles/textarea TextArea Done
Scrollable viewport bubbles/viewport Viewport Done
Paginated list (delegate pattern) bubbles/list + bubbles/paginator List Done
Option picker (select) bubbles/list Select Done
Yes/No confirmation huh Confirm Done
Multi-field form huh Form Done
Form theme (focused/blurred styles) huh Theme, ThemeCharm Done
App theme (titles, borders, list items) snaptui-original AppTheme, AppThemeCharm Done
Table bubbles/table Table, Column Done
Spinner bubbles/spinner Spinner, SpinnerTickMsg Done
Progress bar bubbles/progress Progress Done
File picker bubbles/filepicker -- Not yet
Timer / Stopwatch bubbles/timer, bubbles/stopwatch -- Not yet
Help (auto-generated keybinds) bubbles/help Help, KeyBinding Done

Legend

  • Done -- Implemented and tested
  • Not yet -- Not implemented; listed here so future apps know what to add when they need it

Architecture

your_app.py
    |
    +-- snaptui (framework + styling + components in one package)
            |
            +-- model.py        Elm Architecture: Model protocol, Msg, Cmd
            +-- keys.py         Keyboard input: escape sequence parser
            +-- program.py      Event loop: raw mode, resize, render cycle
            +-- terminal.py     Low-level: termios, ioctl, ANSI sequences
            +-- renderer.py     Line-diff screen updates
            +-- style.py        Lip Gloss equivalent: chainable styling
            +-- strutil.py      ANSI-aware string ops: width, wrap, truncate
            +-- layout.py       join_horizontal, join_vertical, place
            +-- components/
                    +-- textinput.py   Single-line input
                    +-- textarea.py    Multi-line editor
                    +-- viewport.py    Scrollable content
                    +-- list.py        Paginated list with delegates
                    +-- select.py      Option picker
                    +-- confirm.py     Yes/No prompt
                    +-- form.py        Multi-field form
                    +-- table.py       Column-aligned data table
                    +-- spinner.py     Animated spinner
                    +-- progress.py    Progress bar
                    +-- help.py        Auto-generated keybind help

In Go, these are spread across four repos (bubbletea, lipgloss, bubbles, x/ansi). snaptui combines them because Python apps can just import what they need from one package.

File mapping

Exact Go source → snaptui file mapping for reference when porting new features or chasing bugs back to the Go implementation.

Go source repos:

  • bubbletea (v1.3.10) — event loop, renderer, key input
  • lipgloss (v1.1.0) — styling, layout, borders
  • bubbles (v1.0.0) — textinput, textarea, viewport, list
  • huh (v0.8.0) — form, select, confirm, theme
  • x/ansi (v0.11.6) — string width, wrap, truncate
snaptui file Go source(s)
model.py bubbletea/tea.go
program.py bubbletea/tea.go, bubbletea/signals_unix.go
renderer.py bubbletea/standard_renderer.go
keys.py bubbletea/key.go, bubbletea/key_sequences.go
terminal.py bubbletea/tty_unix.go, x/ansi/mode.go
style.py lipgloss/style.go, lipgloss/set.go, lipgloss/get.go, lipgloss/color.go, lipgloss/borders.go, lipgloss/whitespace.go, lipgloss/size.go
layout.py lipgloss/join.go, lipgloss/align.go, lipgloss/position.go
strutil.py x/ansi/width.go, x/ansi/wrap.go, x/ansi/truncate.go, x/ansi/style.go
theme.py huh/theme.go (Form theme); AppTheme/AppThemeCharm are snaptui-original
components/textinput.py bubbles/textinput/textinput.go, bubbles/cursor/cursor.go
components/textarea.py bubbles/textarea/textarea.go
components/viewport.py bubbles/viewport/viewport.go, bubbles/viewport/keymap.go
components/list.py bubbles/list/list.go, bubbles/list/defaultitem.go, bubbles/paginator/paginator.go
components/select.py huh/field_select.go
components/confirm.py huh/field_confirm.go
components/form.py huh/form.go, huh/group.go
components/table.py bubbles/table/table.go
components/spinner.py bubbles/spinner/spinner.go
components/progress.py bubbles/progress/progress.go
components/help.py bubbles/help/help.go, bubbles/key/key.go

Running tests

python3 -m pytest

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

snaptui-0.2.2.tar.gz (46.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

snaptui-0.2.2-py3-none-any.whl (44.6 kB view details)

Uploaded Python 3

File details

Details for the file snaptui-0.2.2.tar.gz.

File metadata

  • Download URL: snaptui-0.2.2.tar.gz
  • Upload date:
  • Size: 46.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.4

File hashes

Hashes for snaptui-0.2.2.tar.gz
Algorithm Hash digest
SHA256 c33567a46bdbd3a85f6cc4d4dfd55fed77b8dc14ff5ccec30b9ae8f3890cf68c
MD5 5837a33c622be19ef0efcce5e4f89c83
BLAKE2b-256 8f5b7afb94f0f13742483d7d2f1de125c01ecab37ab5b7551a3d97dc4bbaaf06

See more details on using hashes here.

File details

Details for the file snaptui-0.2.2-py3-none-any.whl.

File metadata

  • Download URL: snaptui-0.2.2-py3-none-any.whl
  • Upload date:
  • Size: 44.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.4

File hashes

Hashes for snaptui-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 25677b0719609fa2a8c4b00dde313400a3db564f1c66e61fee73b28d0745f30c
MD5 4f4e3a6ff935574747beccb1f0acf34c
BLAKE2b-256 8e119ad461c27b696b54be7ae82bccaa58dee8f5616685b8779e2cc3b0e0580b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page