Skip to content

✍️ 필사 모드: The TUI Renaissance 2026 — Ratatui, Bubble Tea, Textual, Ink and the Beautifully Reborn Terminal (Deep Dive)

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

Prologue — A Renaissance on a Black Screen

Mid-2010s. Tell someone you were building a terminal UI and you got two reactions. First, "why?" Second, "ncurses?" That ended the conversation.

The 2026 landscape is entirely different.

  • Someone seeing lazygit for the first time asks "is this an Electron app?" No. It is a TUI written in Go.
  • k9s has become the standard tool for Kubernetes operators. Faster than any web dashboard.
  • atuin stores shell history in SQLite, syncs it across devices, and searches it through a TUI. Once you try it, you cannot go back to plain Ctrl+R.
  • gum puts rounded-corner input forms on top of shell scripts. gum choose, gum confirm and your install.sh becomes pretty.
  • posting brings Postman into the terminal. JSON tree, markdown response, variable environments, the works.
  • slumber rebuilt the HTTP client around TOML config plus keybindings.

This became possible because four toolkits matured at roughly the same time.

  • Ratatui (Rust, immediate-mode) — forked from tui-rs and officially launched in 2023, now v0.31 in 2026. Practically owns the lazygit-ish tools on the Rust side.
  • Bubble Tea (Go, Elm Architecture) — Charm's Go port of The Elm Architecture (TEA). The foundation under lazygit, gh, gum, and soft-serve.
  • Textual (Python, declarative with CSS-like styling) — built by Will McGugan (author of Rich). Now at 5.x in 2026 and the base for posting.
  • Ink (Node.js, JSX/React) — Vadim Demedes's React for CLIs. Powers Claude Code, GitHub Copilot CLI, and the Gatsby CLI.

Add C++'s FTXUI, Rust's cursive, C's notcurses, Node's blessed/blessed-contrib, and one thing is clear: the terminal is not a dead platform, it is a resurrected one.

This piece organises that renaissance. Why the terminal, again (chapter 1). Which demo apps changed the wind (chapter 2). The four toolkits and their philosophies (chapters 3–6). Same counter app in four languages, side by side (chapter 7). A 30-minute first-TUI path (chapter 8). Honest trade-offs (chapter 9).


1. Why TUIs Came Back — Six Reasons

Honest first. TUIs are not about "vibes" or "retro." That illusion does not let lazygit replace GitHub Desktop. The real reasons are elsewhere.

1. SSH, containers, and CI became daily life. With cloud, Kubernetes, and remote development everywhere, we work "without a GUI" often. Being able to launch k9s with one ssh line beats X11 forwarding a GUI app, by a wide margin.

2. Keyboard-first workflows were rediscovered. For Vim, Neovim, and Emacs users every round trip to the mouse is a cognitive tax. lazygit's Ctrl+e to fetch, s to stage, c to commit flow is faster than SourceTree. Measurably.

3. Terminal emulators got powerful. Alacritty, WezTerm, Kitty, Ghostty support GPU acceleration, truecolor, image protocols, undercurl, ligatures, and mouse events. The 256-color ceiling of 1990s ncurses is gone.

4. Unicode and fonts caught up. Nerd Fonts (icons), Powerline glyphs, box-drawing characters, rounded corners (╭─╮) became default. The old claim that "terminals can only draw text" is, practically, untrue.

5. The default UI of AI coding tools went TUI. Claude Code, Codex CLI, GitHub Copilot CLI, Aider — every flagship coding agent lives inside the terminal. A chat UI outside the browser is no longer weird, it is normal.

6. Marketing-savvy libraries appeared. Charm shipped lipgloss, glow, gum, soft-serve, VHS and proved by demo that TUIs can be pretty. Ratatui drove the Rust TUI scene with a book (Manning, 2024), tutorials, and a polished site at ratatui.rs. The tools were always there. The marketing was missing.

Remember one line: "A TUI is not 'worse than a GUI.' It is a different form that finds a different local optimum under different constraints."


2. The Demo Apps That Made TUIs Cool Again

Tools succeed because of what is built with them, not because of the tools themselves. Here are the apps that drove the 2020s TUI revival.

lazygit (Go, on gocui before the Bubble Tea era)

Every common Git task in one or two keystrokes. Panel switching is 1/2/3/4, stage is s, commit is c, push is P. A de facto replacement for Git GUIs. About 53k GitHub stars as of May 2026.

gh (Go, partly Bubble Tea)

GitHub's official CLI. The familiar gh pr create, gh pr view, gh repo clone commands, with Bubble Tea patterns visible inside gh pr checkout and interactive flows.

gum (Go, Bubble Tea + Lipgloss)

Charm's "widget library for shell scripts." Call gum input, gum confirm, gum choose, gum spin directly from bash. Effectively the standard for install scripts and dotfile bootstraps.

k9s (Go, tcell + tview)

Operate Kubernetes clusters from a TUI. Switch namespaces, tail pod logs, port-forward, edit deployments — keyboard only. The ops team's standard tool.

atuin (Rust, Ratatui)

Magical shell history. Records commands in SQLite, syncs across devices, fuzzy-searches them on Ctrl+R. Once you adopt it, the plain shell history feels broken.

posting (Python, Textual)

"Postman in the terminal." HTTP requests, environment variables, collections, JSON tree view, markdown response rendering. Darren Burns built it to show Textual's limits, and it grew into a real product.

slumber (Rust, Ratatui)

A TOML-and-keybindings HTTP client. For people who want to keep their Postman collections in plain text under version control.

gitui (Rust, Ratatui)

The Rust take on lazygit. The selling point is faster, leaner memory. v0.27 in 2026.

btop (C++, custom rendering)

The successor to top/htop. Rounded boxes, graphs, mouse support. Written in C++ but its UI design set the visual bar for the renaissance.

glow (Go, Bubble Tea)

A markdown viewer. glow README.md and the README looks great in the terminal. One of the entry points to Charm's ecosystem.

vhs (Go, Bubble Tea + headless terminal)

Generates terminal GIFs and MP4s from .tape scripts. The reason README demo GIFs across the Charm world look consistent.

nushell, tabiew, yazi, television, and more

CSV/Parquet viewer tabiew (Rust/Ratatui), file manager yazi (Rust/Ratatui), fuzzy finder television (Rust/Ratatui), and dozens of others. Ratatui-based tools have exploded in number.

One pattern jumps out. Go = Bubble Tea, Rust = Ratatui, Python = Textual, Node = Ink. Each language picked a de facto standard toolkit.


3. Ratatui — Rust's Immediate-Mode TUI

Origins

Florian Dehau's tui-rs started in 2016 and stalled in 2023 when the maintainer stepped back. The community forked it as ratatui and made it official. As of May 2026 it is at v0.31.x with about 14k GitHub stars and over 1,000 downstream crates. Practically every major Rust TUI — atuin, gitui, yazi, bottom, television — sits on top of Ratatui.

Core Philosophy — Immediate Mode

Most GUI frameworks are retained-mode: build a widget tree, mutate it on state changes, let the framework redraw. Ratatui is immediate-mode: you draw "the UI for this frame" yourself every frame.

// The draw closure is invoked every frame
terminal.draw(|frame| {
    let area = frame.area();
    let block = Block::default().borders(Borders::ALL).title("Counter");
    let text = Paragraph::new(format!("count: {}", count)).block(block);
    frame.render_widget(text, area);
})?;

Why is that nice? No state-sync bugs. No stale nodes hanging around in a tree. The pattern game engines (egui, bevy_egui) love, ported to the terminal.

The cost is on you. Drawing every frame is your responsibility. At 60fps that means 60 draw calls. Expensive computations need explicit caching. In practice most apps only redraw on input events, which is fine.

Widgets and Layout

use ratatui::{
    layout::{Constraint, Direction, Layout},
    widgets::{Block, Borders, List, ListItem, Paragraph},
};

let chunks = Layout::default()
    .direction(Direction::Horizontal)
    .constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
    .split(frame.area());

frame.render_widget(sidebar, chunks[0]);
frame.render_widget(main_area, chunks[1]);

Built-in widgets: Block, Paragraph, List, Table, Tabs, Gauge, Sparkline, BarChart, Chart, Canvas. With Canvas you can draw points, lines, even a world map.

Events — crossterm

Ratatui itself only draws. Events come from crossterm (or termion).

use crossterm::event::{self, Event, KeyCode};

if event::poll(std::time::Duration::from_millis(100))? {
    if let Event::Key(key) = event::read()? {
        match key.code {
            KeyCode::Char('q') => break,
            KeyCode::Up => count += 1,
            KeyCode::Down => count -= 1,
            _ => {}
        }
    }
}

Who Uses It

  • atuin — shell history
  • gitui — Git TUI
  • yazi — file manager
  • bottom — system monitor, an htop alternative
  • television — fuzzy finder, a candidate successor to fzf
  • tabiew — CSV/Parquet viewer
  • slumber — HTTP client

Strengths and Weaknesses

  • Strengths — performance (compiled Rust), memory efficiency, stability, rich widgets, active community, a Manning book in 2024, the polished ratatui.rs site.
  • Weaknesses — immediate-mode learning curve, boilerplate (crossterm init, alternate screen, raw mode), hard to debug (stdout is the screen, so no println debugging).

4. Bubble Tea — The Elm Architecture in Go

Origins

Charm (Toby Padilla and Christian Rocha) released it in 2020. A direct port of The Elm Architecture (TEA) into Go. As of May 2026 it is at v1.3 with about 32k GitHub stars. lazygit is migrating, parts of gh CLI use it, all of gum, all of glow, and all of soft-serve sit on top of it.

Core Philosophy — Elm Architecture (MVU)

Model, Update, View. State lives in Model. Messages (events) flow through Update(msg, model) -> (newModel, cmd). View(model) -> string paints the screen. Side effects are isolated into Cmd (higher-order functions).

type Model struct {
    count int
}

func (m Model) Init() tea.Cmd { return nil }

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "q", "ctrl+c":
            return m, tea.Quit
        case "up":
            m.count++
        case "down":
            m.count--
        }
    }
    return m, nil
}

func (m Model) View() string {
    return fmt.Sprintf("count: %d\n\nq to quit", m.count)
}

The functional pattern buys different benefits than immediate mode. The flow of state change is in one place. Read Update and you know every possible transition. Easy to test (pure functions).

Lipgloss — Styling

Bubble Tea on its own draws colors, boxes, and alignment. Lipgloss layers a CSS-like style API on top.

import "github.com/charmbracelet/lipgloss"

var titleStyle = lipgloss.NewStyle().
    Bold(true).
    Foreground(lipgloss.Color("#FAFAFA")).
    Background(lipgloss.Color("#7D56F4")).
    PaddingTop(1).
    PaddingBottom(1).
    PaddingLeft(2).
    PaddingRight(2)

fmt.Println(titleStyle.Render("Hello, TUI"))

Colors, padding, borders, alignment, truncation, wrapping — all chained methods. Rounded corners in one line.

Bubbles — Widget Library

The Bubble Tea core stays intentionally small. Widgets live in the separate bubbles library: textinput, textarea, list, table, viewport, spinner, progress, paginator, key.

Wish and Soft Serve — TUIs Over SSH

Another Charm piece. wish is an SSH server library. Expose a Bubble Tea app over SSH and users connect with ssh tui.example.com — no install needed. soft-serve, Charm's self-hosted Git server, is the canonical example.

Who Uses It

  • gum — shell script widgets
  • glow — markdown viewer
  • vhs — terminal recording
  • soft-serve — Git server TUI
  • Many newer apps started from the bubbletea-app-template
  • lazygit is gradually migrating

Strengths and Weaknesses

  • Strengths — clean architecture, easy to test thanks to the functional shape, strong visual polish via Lipgloss, the huge Charm ecosystem.
  • Weaknesses — Go's interface type switches give you boilerplate (tea.Msg type matching), immutable update cost in very large models, ad hoc async needs tea.Cmd wrappers.

5. Textual — Declarative TUI for Python

Origins

Will McGugan (author of Rich) started it in 2021. If Rich is "pretty print," Textual is "pretty app." As of May 2026 it is at v5.x with about 28k GitHub stars. posting's success was the deciding moment.

Core Philosophy — Web-Like Declarations

CSS-like styling (.tcss), a widget tree, event handlers, async-first. In one phrase: "something like React, in the terminal" — minus the explicit virtual DOM.

from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static

class CounterApp(App):
    BINDINGS = [
        ("up", "increment", "Increment"),
        ("down", "decrement", "Decrement"),
        ("q", "quit", "Quit"),
    ]

    def __init__(self):
        super().__init__()
        self.count = 0

    def compose(self) -> ComposeResult:
        yield Header()
        yield Static(f"count: {self.count}", id="counter")
        yield Footer()

    def action_increment(self):
        self.count += 1
        self.query_one("#counter", Static).update(f"count: {self.count}")

    def action_decrement(self):
        self.count -= 1
        self.query_one("#counter", Static).update(f"count: {self.count}")

if __name__ == "__main__":
    CounterApp().run()

Three things to notice: compose declares the tree, BINDINGS maps shortcuts, action_* methods are dispatched automatically, and query_one finds widgets by CSS-like selectors.

CSS-Like Styling — TCSS

Styles go in a separate .tcss file.

Static {
    background: $boost;
    padding: 1 2;
    border: round $primary;
    text-align: center;
}

#counter {
    width: 30;
    height: 5;
    margin: 1;
}

Colors can reference the design-system variables (primary,primary, boost, $success, etc.).

Rich Widget Library

Static, Label, Button, Input, TextArea, Select, Switch, Checkbox, RadioSet, ListView, DataTable, Tree, Tabs, ContentSwitcher, ProgressBar, Sparkline, LoadingIndicator, Pretty, Markdown, RichLog, Log, Header, Footer, Placeholder. A lot.

Async-First

async def on_button_pressed(self, event: Button.Pressed) -> None:
    async with httpx.AsyncClient() as client:
        result = await client.get("https://api.example.com")
        self.query_one("#result", Static).update(result.text)

Python's asyncio is first-class, which makes HTTP, DB, and file IO feel natural. posting is the canonical example.

Textual Web — Same Code, in the Browser

textual serve hosts the same app in a browser. Three deployment paths: SSH, local, web. Still beta in 2026 but it works.

Who Uses It

  • posting — terminal HTTP client (Postman alternative)
  • harlequin — DuckDB and Postgres SQL IDE
  • frogmouth — markdown viewer
  • memray — memory profiler (partial)
  • Many internal data tools

Strengths and Weaknesses

  • Strengths — Python-friendly, async feels natural, separating CSS lets designers and engineers split work, Textual Web for browser deployment, rich widget library, great synergy with Rich (markdown, code highlighting).
  • Weaknesses — Python startup time (interpreter plus imports), memory usage (heavier than Rust/Go), the GIL ceiling, async learning curve.

6. Ink — JSX/React for CLIs in Node.js

Origins

Vadim Demedes started it in 2017 from one simple idea: "I want to write CLIs with React components." As of May 2026 it is at v5.x with about 28k GitHub stars. Claude Code, GitHub Copilot CLI, Gatsby CLI, Prisma CLI, AWS Amplify CLI all use Ink.

Core Philosophy — React Renders Text

Just as React renders DOM in the browser, Ink renders React to terminal text. Instead of div and span, you use Box and Text.

import React, { useState } from 'react';
import { render, Box, Text, useInput } from 'ink';

const Counter = () => {
  const [count, setCount] = useState(0);

  useInput((input, key) => {
    if (key.upArrow) setCount(c => c + 1);
    if (key.downArrow) setCount(c => c - 1);
    if (input === 'q') process.exit(0);
  });

  return (
    <Box borderStyle="round" padding={1}>
      <Text>count: {count}</Text>
    </Box>
  );
};

render(<Counter />);

React Hooks work as-is. useState, useEffect, useReducer, custom hooks. Need routing? ink-router. Forms? ink-form. Text input? ink-text-input. Multi-select? ink-multi-select.

Flexbox Layout

Layout uses Flexbox, like the browser.

<Box flexDirection="column" width="100%">
  <Box height={3} borderStyle="single">
    <Text>Header</Text>
  </Box>
  <Box flexGrow={1}>
    <Text>Body</Text>
  </Box>
</Box>

flexDirection, flexGrow, justifyContent, alignItems, padding, margin, width, height — familiar CSS names, identical semantics.

Suspense, Streaming, Async

React 18+ patterns (Suspense, transitions) work under Ink 5. Why it fits AI coding tools that stream tokens: rendering partial responses is exactly what Suspense was designed for.

Who Uses It

  • Claude Code — Anthropic's coding agent
  • GitHub Copilot CLIgh copilot
  • Gatsby CLI
  • Prisma CLI
  • AWS Amplify CLI
  • Twilio CLI

Strengths and Weaknesses

  • Strengths — React knowledge transfers directly, npm ecosystem, JSX expressiveness, natural fit for AI streaming UIs, designer-friendly.
  • Weaknesses — Node startup cost (node, V8, deps), heaviest memory footprint, constraints in the text-node tree (you cannot nest Box inside Text), re-render cost in large trees.

7. The Same Counter App, in Four Languages — A Direct Comparison

The simplest possible app: up/down keys increment/decrement a counter, q quits. Here is how the four toolkits express it.

Ratatui (Rust)

use crossterm::{
    event::{self, Event, KeyCode},
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
    ExecutableCommand,
};
use ratatui::{
    backend::CrosstermBackend,
    widgets::{Block, Borders, Paragraph},
    Terminal,
};
use std::io;

fn main() -> io::Result<()> {
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    stdout.execute(EnterAlternateScreen)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;
    let mut count: i32 = 0;

    loop {
        terminal.draw(|f| {
            let block = Block::default().borders(Borders::ALL).title("Counter");
            let para = Paragraph::new(format!("count: {}", count)).block(block);
            f.render_widget(para, f.area());
        })?;

        if event::poll(std::time::Duration::from_millis(100))? {
            if let Event::Key(key) = event::read()? {
                match key.code {
                    KeyCode::Char('q') => break,
                    KeyCode::Up => count += 1,
                    KeyCode::Down => count -= 1,
                    _ => {}
                }
            }
        }
    }

    disable_raw_mode()?;
    terminal.backend_mut().execute(LeaveAlternateScreen)?;
    Ok(())
}

Bubble Tea (Go)

package main

import (
    "fmt"
    "os"
    tea "github.com/charmbracelet/bubbletea"
)

type model struct{ count int }

func (m model) Init() tea.Cmd { return nil }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    if k, ok := msg.(tea.KeyMsg); ok {
        switch k.String() {
        case "q", "ctrl+c":
            return m, tea.Quit
        case "up":
            m.count++
        case "down":
            m.count--
        }
    }
    return m, nil
}

func (m model) View() string {
    return fmt.Sprintf("count: %d\n\n(q to quit)\n", m.count)
}

func main() {
    if _, err := tea.NewProgram(model{}).Run(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

Textual (Python)

from textual.app import App, ComposeResult
from textual.widgets import Static

class CounterApp(App):
    BINDINGS = [
        ("up", "inc", "Up"),
        ("down", "dec", "Down"),
        ("q", "quit", "Quit"),
    ]

    count = 0

    def compose(self) -> ComposeResult:
        yield Static(f"count: {self.count}", id="c")

    def render_count(self):
        self.query_one("#c", Static).update(f"count: {self.count}")

    def action_inc(self):
        self.count += 1
        self.render_count()

    def action_dec(self):
        self.count -= 1
        self.render_count()

if __name__ == "__main__":
    CounterApp().run()

Ink (TSX)

import React, { useState } from 'react';
import { render, Box, Text, useInput } from 'ink';

const Counter = () => {
  const [count, setCount] = useState(0);

  useInput((input, key) => {
    if (key.upArrow) setCount(c => c + 1);
    if (key.downArrow) setCount(c => c - 1);
    if (input === 'q') process.exit(0);
  });

  return (
    <Box borderStyle="round" padding={1}>
      <Text>count: {count}</Text>
    </Box>
  );
};

render(<Counter />);

Line Counts

  • Ratatui: ~35 lines, including terminal raw-mode setup. Longest but most explicit.
  • Bubble Tea: ~30 lines. The MVU pattern is clean but the type switches add boilerplate.
  • Textual: ~20 lines. Shortest and easiest to read.
  • Ink: ~15 lines. If you already think in JSX, the shortest.

Line count is not quality. The shortest code (Ink) sits on the heaviest runtime (Node and V8). The longest (Ratatui) compiles to the smallest single static binary.


8. Your First TUI in 30 Minutes — Real Paths

Path A: Start with Ratatui (performance and deployment first)

# 1. New project
cargo new my_tui && cd my_tui

# 2. Add deps
cargo add ratatui crossterm

# 3. Paste the counter example into src/main.rs
cargo run

For shipping: cargo build --release gives you a single static binary. Zero dependencies.

Path B: Start with Bubble Tea (Go-friendly stack)

mkdir my_tui && cd my_tui
go mod init my_tui
go get github.com/charmbracelet/bubbletea
# Paste the example into main.go
go run main.go

Cross-compilation is one env var: GOOS=linux GOARCH=amd64 go build.

Path C: Start with Textual (Python-friendly stack)

pip install textual
python counter.py

textual run --dev counter.py adds hot reload and a dev console.

Path D: Start with Ink (React-friendly stack)

npx create-ink-app my-cli --typescript
cd my-cli
npm run dev

The scaffold takes care of testing and builds for you.

After That — A Common Checklist

  • Get alternate screen enter/exit right (explicit in Ratatui, automatic in the rest).
  • Handle raw mode carefully, including Ctrl+C trapping.
  • React to resize events and redraw.
  • Opt into mouse events deliberately.
  • Verify truecolor support with COLORTERM.
  • Handle Unicode width correctly — emoji and CJK characters take two cells. Use unicode-width or wcwidth.
  • Split logging from the screen. You cannot debug with println; write to a log file or to a dev console.

9. Honest Trade-Offs — Where TUIs Are the Wrong Form

TUIs do not solve everything. In these cases a different tool is better.

1. Heavy chart/graph dashboards

Box-drawing sparklines are fine, but scatter plots, heatmaps, and 3D charts hit hard limits. If data visualisation is the centre, the web (Streamlit, Grafana) wins.

2. Images and video

iTerm2, Kitty, and WezTerm have inline image protocols, but the environment dependency is significant. Things break inside tmux often.

3. Mouse-first workflows

Complex drag-and-drop, visual editing (diagrams, images, canvases). Technically possible, but users do not like it.

4. Non-developer audiences

A TUI asks users to learn modes and shortcuts. The curve is too steep for the general public.

5. Critical accessibility needs

Screen readers target GUIs and the web first. TUI support is partial. Tools serving large populations of visually impaired users should go GUI/web.

6. Very large data on one screen

A terminal has hard limits on cells per line and lines per screen. Virtualisation and pagination are needed anyway, and the browser does that better.

Remember one line: "A TUI shines when keyboard-first, remote access, and lightweight deployment matter. It loses where heavy visualisation, general audiences, or accessibility dominate."


10. Tool Selection Matrix

A quick lookup by situation.

SituationPickWhy
System tools (file, Git, monitoring)Ratatuiperformance, deploy, single binary
Shell script widgetsBubble Tea (gum)ready to use, the standard
Internal data tools (SQL, HTTP, analysis)Textualasync, Python libs, rich widgets
AI coding tools, CLI wizardsInkstreaming UI, React skills
Admin console inside a new Go serviceBubble Teasame Go codebase
Operator TUI for a Rust serverRatatuisame binary
Fast prototypeTextualleast boilerplate
Multi-user TUI over SSHBubble Tea + wishCharm's SSH lib
Cluster operations (k8s and similar)Bubble Tea or Ratatuikeyboard shortcuts, instant feedback

Epilogue — The Terminal Is Not Dead, It Was Reborn

That was long. Recap.

One-Liner

The TUI renaissance is not retro nostalgia. It is the rediscovery of three values — keyboard-first interaction, remote-friendliness, and lightweight deployment. Ratatui, Bubble Tea, Textual, and Ink instantiate those values in their respective language ecosystems.

Thirty-Second Checklist

  1. Before starting a new TUI, did you check Chapter 9 anti-cases to confirm TUI really is the right form?
  2. Is your audience comfortable with keyboard-first workflows?
  3. Did you pick a toolkit that matches your language ecosystem and team — Rust to Ratatui, Go to Bubble Tea, Python to Textual, Node to Ink?
  4. Does single-binary deployment matter? Rust and Go are the answer.
  5. Does async matter? Textual and Ink feel natural.
  6. Are you ready for raw mode, alternate screen, resize, and Unicode-width handling?
  7. Have you planned a log file or dev console because println debugging is impossible?
  8. If you turn on mouse mode, did you think about who will refuse it?
  9. Can you assume truecolor, or do you need to check COLORTERM?
  10. Did you build a demo with vhs or similar so marketing keeps up with the code?

Ten Anti-Patterns

  1. Doing heavy work synchronously on every keystroke and freezing the UI.
  2. Repeating expensive IO on every frame in immediate-mode (Ratatui).
  3. Nesting Box inside Text in Ink — runtime error.
  4. Running side effects directly in Bubble Tea's Update instead of isolating them into Cmd.
  5. Blocking sync IO in Textual's compose, causing slow startup.
  6. Crashing while raw mode is on and leaving the terminal in a broken state.
  7. Ignoring resize events and breaking the layout.
  8. Skipping CJK/emoji width handling and breaking widget borders.
  9. Using println / fmt.Println / print() for debug and corrupting the screen.
  10. Assuming everyone uses the same terminal and skipping TERM/COLORTERM checks.

What's Next

Candidates: Conquering the Charm Ecosystem — Bubble Tea, Lipgloss, Bubbles, Wish, Glow, VHS in One Pass, Build a Git TUI with Ratatui — Cloning lazygit in a Week, Textual plus SQLite for Internal Data Tools — Python's Strengths in a TUI.

"The GUI defined the mouse era. The TUI brought the keyboard era back. They are not enemies — they are different answers to the same question."

— TUI Renaissance 2026, end.


References

현재 단락 (1/427)

Mid-2010s. Tell someone you were building a terminal UI and you got two reactions. First, "why?" Sec...

작성 글자: 0원문 글자: 24,483작성 단락: 0/427