惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
月光博客
月光博客
雷峰网
雷峰网
WordPress大学
WordPress大学
博客园 - 司徒正美
Last Week in AI
Last Week in AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
Visual Studio Blog
H
Help Net Security
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
S
Security @ Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
爱范儿
爱范儿
W
WeLiveSecurity
J
Java Code Geeks
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
T
Threatpost
The Cloudflare Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
N
Netflix TechBlog - Medium
Latest news
Latest news
V2EX - 技术
V2EX - 技术
小众软件
小众软件
T
The Blog of Author Tim Ferriss
A
Arctic Wolf
B
Blog RSS Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
I
InfoQ
C
Check Point Blog
N
News | PayPal Newsroom
Cyberwarzone
Cyberwarzone
V
V2EX
TaoSecurity Blog
TaoSecurity Blog
P
Privacy & Cybersecurity Law Blog
Microsoft Security Blog
Microsoft Security Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
D
DataBreaches.Net
F
Fortinet All Blogs
阮一峰的网络日志
阮一峰的网络日志
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
IT之家
IT之家
K
Kaspersky official blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google DeepMind News
Google DeepMind News
C
CXSECURITY Database RSS Feed - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com

Pierce Freeman

A browser for agents | Pierce Freeman The grey market of podcast appearances The way I travel | Pierce Freeman Fixing slow AWS uploads | Pierce Freeman Local tools should still use vaults We solved scratch content first Starting a podcast in 2025 Being late but still being early Automating our home video imports Adding my parents to tailscale A deep dive on agent sandboxes Language servers for AI | Pierce Freeman My simple home podcast studio We need centralized infrastructure | Pierce Freeman Coercing agents to follow conventions using AST validation My unified theory of social selling My personal backup strategy | Pierce Freeman July updates to the homelab How the KV Cache works httpx is the right way to do web requests in Python Reputation is becoming everything | Pierce Freeman Building a (kind of) invisible mac app Updated knowledge in language models Making an ascii animation | Pierce Freeman How speculative decoding works | Pierce Freeman Under the hood of Claude Code Doing things because they're easy, not hard Speeding up sideeffects with JIT in mountaineer Firehot for hot reloading in Python Misadventures in Python hot reloading How text diffusion works | Pierce Freeman The tenacity of modern LLMs The ergonomics of rails | Pierce Freeman How language servers work | Pierce Freeman Just add eggs | Pierce Freeman Unfortunately SEO still matters | Pierce Freeman The futility of human-only web requirements Setting up Input Leap | Pierce Freeman Checking in on Waymo | Pierce Freeman The react revolution | Pierce Freeman Speeding up many small transfers to a unifi nas Quick notes on swift libraries AI engineering is a different animal San Francisco | Pierce Freeman Debugging a mountaineer rendering segfault Local network config on macOS Building our home network | Pierce Freeman Introducing Envelope.dev | Pierce Freeman Legacy code and AI copilots Typehinting from day-zero | Pierce Freeman Generating database migrations with acyclic graphs Lofoten | Pierce Freeman Mountaineer v0.1: Webapps in Python and React Constraining LLM Outputs | Pierce Freeman Passthrough above all | Pierce Freeman Accuracy in kudos | Pierce Freeman How quick we are to adapt The curious case of LM repetition Costa Rica | Pierce Freeman Debugging chrome extensions with system-level logging Speeding up runpod | Pierce Freeman Inline footnotes with html templates Parsing Common Crawl in a day for $60 All or nothing with remote work The Next 10 Years | Pierce Freeman Adding wheels to flash-attention | Pierce Freeman LLMs as interdisciplinary agents | Pierce Freeman New Zealand | Pierce Freeman Representations in autoregressive models | Pierce Freeman Let's talk about Siri | Pierce Freeman Minimum viable public infrastructure | Pierce Freeman Reasoning vs. Memorization in LLMs Automatically migrate enums in alembic Greater sequence lengths will set us free On learning to ski | Pierce Freeman Dolomites | Pierce Freeman Using grpc with node and typescript Opportunity years | Pierce Freeman Buzzword peaks and valleys | Pierce Freeman Buenos Aires | Pierce Freeman Network routing interaction on MacOS Independent work: November recap | Pierce Freeman Debugging slow pytorch training performance The provenance of copy and paste Debugging tips for neural network training Patagonia | Pierce Freeman Santiago | Pierce Freeman My 2022 digital travel kit AWS vs GCP - GPU Availability V2 Independent work: October recap | Pierce Freeman Planning Patagonia | Pierce Freeman Relationship modeling | Pierce Freeman The power of status updates A new chapter | Pierce Freeman Give my library a coffee shop AWS vs GCP - GPU Availability V1 Switzerland | Pierce Freeman Headfull browsers beat headless | Pierce Freeman Webcrawling tradeoffs | Pierce Freeman Copenhagen | Pierce Freeman
An era of rich CLI
2023-11-03 · via Pierce Freeman

There's something interesting happening in the CLI landscape right now. I'm seeing a surge in projects looking to build interactive interfaces in the terminal. Not just prettier output or better color schemes, but interactive applications that feel more like desktop software than traditional command-line tools.

Textual: Full, CSS-inspired UIs and interaction

Rich: Rendering of tables, progress bars, syntax highlighting

Gum: Drop-in interactive widgets (prompts, pickers, spinners)

Ratatui: Complex dashboards and interactive apps with widgets, layouts, and real-time updates

Bubble Tea: Interactive applications with state management and animations

tview: Pre-built widgets like tables, forms, and modal dialogs

Why now? Why are we suddenly seeing this renaissance in terminal interfaces after decades of pretty basic text output and more complex desktop UIs?

The old way

Almost every language bakes in CLI support, since that's the main way we've always spun up applications that don't have an interface layer. Here's basically how you'd make one in Python:

import sys
import time

def progress_bar(current, total, width=50):
    """Manual progress bar implementation"""
    percent = current / total
    filled = int(width * percent)
    bar = '=' * filled + '-' * (width - filled)
    sys.stdout.write(f'\r[{bar}] {percent:.1%}')
    sys.stdout.flush()

def process_files(files):
    for i, file in enumerate(files):
        # Do some work
        time.sleep(0.1)
        progress_bar(i + 1, len(files))
    print()  # New line when done

# Usage
files = ['file1.txt', 'file2.txt', 'file3.txt']
process_files(files)

This was the state of the art for "interactive" CLI tools around 2010. Manual cursor positioning with \r to overwrite previous rows, careful flush() calls to ensure output appears immediately, and ASCII art for any visual elements. If you wanted colors, you'd hardcode ANSI escape sequences:

# Colors the hard way
RED = '\033[31m'
GREEN = '\033[32m'
RESET = '\033[0m'

print(f"{RED}Error:{RESET} File not found")
print(f"{GREEN}Success:{RESET} File processed")

Building anything more complex meant diving deep into terminal control sequences. Want to create a menu? You'd need to manually handle key input, clear screen regions, and track cursor positions. Most developers just gave up and built web interfaces instead.

The tooling that did exist was powerful but low-level. Libraries like curses could create full-screen terminal applications but the learning curve was steep:

import curses

def main(stdscr):
    # Initialize colors
    curses.start_color()
    curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
    
    # Clear screen
    stdscr.clear()
    
    # Add some text
    stdscr.addstr(0, 0, "Hello World!", curses.color_pair(1))
    stdscr.refresh()
    
    # Wait for input
    stdscr.getch()

curses.wrapper(main)

Even this simple "Hello World" requires understanding color pairs, screen management, manual layout, and the curses wrapper pattern. Have fun with mvaddch() and nodelay().

The new way

Textual sits at one end of the extreme where it's trying to create full user interfaces - you can click around, scroll, change "windows" right within one terminal session. The component model looks more like React than building up a curses application.

We start with the styling with actual CSS. Textual supports flexbox-style layouts, themed colors, and responsive design. The CSS is compiled and reformatted in a way that's compatible with the terminal's rendering layer:

from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.widgets import Button, Header, Footer, Static, DataTable, ProgressBar
from textual.binding import Binding

class DashboardApp(App):
    """A dashboard application with multiple interactive widgets."""

    CSS = """
    .box {
        height: 1fr;
        border: solid $primary;
        padding: 1;
        margin: 1;
    }
    
    #sidebar {
        width: 30;
        background: $surface;
    }
    
    .stats {
        height: 3;
        background: $boost;
        color: $text;
        text-align: center;
        padding: 1;
    }
    """

Notice the $primary, $surface, $boost variables? These reference Textual's built-in theme system, automatically adapting to light/dark terminal themes.

We can define global keyboard shortcuts that work anywhere in the app:

    BINDINGS = [
        Binding("q", "quit", "Quit"),
        Binding("r", "refresh", "Refresh Data"),
    ]

The compose method defines our UI layout using a component tree. This feels very much like React's JSX, but with Python context managers:

    def compose(self) -> ComposeResult:
        yield Header()
        
        with Horizontal():
            # Sidebar with controls
            with Vertical(id="sidebar"):
                yield Static("Dashboard Controls", classes="box")
                yield Button("Load Data", id="load", variant="primary")
                yield Button("Export", id="export", variant="success")
                yield Button("Settings", id="settings")
                
                # Progress indicators
                yield Static("System Status", classes="box")
                yield ProgressBar(total=100, show_eta=True, id="cpu")
                yield ProgressBar(total=100, show_eta=True, id="memory")
            
            # Main content area
            with Vertical():
                # Stats row
                with Horizontal():
                    yield Static("CPU: 67%", classes="stats")
                    yield Static("Memory: 45%", classes="stats")
                    yield Static("Disk: 23%", classes="stats")
                
                # Data table
                yield DataTable(id="data-table", classes="box")
        
        yield Footer()

The Horizontal and Vertical containers work like CSS flexbox - automatically handling layout and spacing. Widgets get IDs and CSS classes just like HTML elements.

When the app starts up, we populate our interactive widgets with real data:

    def on_mount(self) -> None:
        """Initialize the data table when app starts."""
        table = self.query_one("#data-table", DataTable)
        table.add_columns("Service", "Status", "CPU", "Memory")
        table.add_rows([
            ("web-server", "✓ Running", "12%", "256MB"),
            ("database", "✓ Running", "45%", "1.2GB"),
            ("cache", "⚠ Warning", "8%", "128MB"),
            ("worker", "✗ Error", "0%", "0MB"),
        ])
        
        # Update progress bars with current system status
        self.query_one("#cpu", ProgressBar).advance(67)
        self.query_one("#memory", ProgressBar).advance(45)

The query_one method works like document.querySelector in web development - find the widget by ID and manipulate it.

Finally, we handle user interactions with event callbacks:

    def on_button_pressed(self, event: Button.Pressed) -> None:
        """Handle button clicks."""
        if event.button.id == "load":
            self.notify("Loading fresh data...")
        elif event.button.id == "export":
            self.notify("Exporting to CSV...")

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

The infrastructure shift

The revolution in rich CLI interfaces isn't happening because developers suddenly got more creative. It's happening because the underlying infrastructure finally caught up to enable it.

Terminal Protocol: Modern terminals support vastly more sophisticated control sequences than their ancestors. The original VT100 from 1978 supported basic cursor movement and colors. Today's terminals support:

  • 24-bit true color (16.7 million colors vs the original 8)
  • Mouse input and click handling
  • Complex text rendering with ligatures and emoji
  • Hyperlinks that you can click in the terminal
  • Image rendering protocols (sixel, kitty graphics)

Unicode Standardization: Emoji, box-drawing characters, and international text all render consistently across platforms now. You can build interfaces using actual graphic characters instead of ASCII approximations:

┌─ Processing Files ─────────────────────────┐
│ ✓ config.json                   [DONE]     │
│ ⏳ large_dataset.csv            [50%] ████ │
│ ⏸ backup.zip                    [WAIT]     │
└────────────────────────────────────────────┘

Why now?

Developer Experience Expectations: Developers who grew up with modern web frameworks expect better tooling. After using React dev tools or Chrome DevTools, going back to printf debugging feels primitive. The bar has been raised for what constitutes acceptable developer experience.

Terminal Renaissance: With the rise of remote deployment, Docker containers, and GPU accelerated environments, developers are spending more time in terminal environments.1 SSH into a production server and you don't have a GUI - but you still need sophisticated debugging and monitoring tools.

CLI Portability: There's something that feels very self contained about working with a terminal interface. A management interface driven by a CLI usually just requires you to install Python (already probably pre-installed) & the given requirements.txt. There's less of a cognitive headache versus having to set up some web console.

Framework Maturity: The underlying libraries finally caught up to the vision. Building terminal applications used to require expertise in low-level terminal control. Now it requires knowledge of layout systems and event handling - skills that transfer directly from web development.

Even though the technical foundations were possible for some time, they weren't accessible. Modern CLI frameworks abstract away the hard parts (terminal compatibility, input handling, efficient rendering) and expose the interesting parts (application logic, user experience design).

The downsides

At the end of the day, these terminal GUIs are building on a base stack that never really expected to natively support these user interactions. Terminal interfaces still bump up against limitations that traditional GUIs solved decades ago.

Accessibility: Rich terminal UIs are essentially inaccessible to screen readers and assistive technologies. When you build a terminal dashboard with buttons and tables, screen readers see nothing but raw terminal output - a stream of ANSI escape sequences and positioned text characters. There are no semantic objects, no tab order, no ARIA labels.

Traditional CLI tools actually handle accessibility better because they're just text input and output. A screen reader can easily announce "git status" and read back the textual results. But a rich TUI that renders a visual table? The screen reader has no concept of rows, columns, or interactive elements.

Web browsers and native GUI frameworks solved this with accessibility APIs - structured object models that assistive technologies can navigate. Terminal applications have no equivalent infrastructure (yet) for TUIs to plug into.

Input method limitations: Rich terminal UIs are constrained by what keyboards and terminal protocols can express. You can't right-click for context menus. Drag-and-drop is impossible. Multi-touch gestures don't exist. Complex text input (emoji pickers, IME support for international keyboards) ranges from limited to broken.

Even basic interactions that users expect from desktop apps - like Ctrl+clicking to select multiple items or using modifier keys for shortcuts - may not work consistently across different terminal configurations.

Debugging complexity: When your rich CLI app breaks, debugging becomes significantly harder than traditional command-line tools. Instead of simple stdout/stderr streams, you're dealing with:

  • Complex rendering pipelines that manage screen buffers
  • Event loops handling keyboard/mouse input
  • Layout engines calculating widget positioning
  • State management across multiple UI components

A simple "button doesn't work" bug might require tracing through event dispatch systems, CSS parsing, and widget lifecycle management. The debugging tools are not yet as mature as browser dev tools or native GUI debuggers.

Performance walls: Terminal rendering has performance limitations. Every screen update requires sending character data over what's essentially a text protocol. Complex interfaces with many simultaneous updates - real-time dashboards with dozens of metrics, data tables with thousands of rows - can overwhelm the terminal's ability to render smoothly. Even with diff-based rendering, high-frequency full-grid updates (e.g. heat-maps) can saturate the PTY or emulator.

Modern web browsers can leverage GPU acceleration, efficient DOM diffing, and sophisticated caching. Terminal applications are stuck with character-by-character updates that become sluggish as complexity increases.

Discovery and onboarding: Traditional GUIs provide visual affordances - buttons look clickable, text fields have cursor indicators, menus are clearly navigable. Rich terminal UIs often look impressive but provide few hints about how to interact with them.

Users who encounter your terminal application for the first time have no obvious way to discover functionality. There's no equivalent to hovering over UI elements to see tooltips, no visual hierarchy that guides the eye, no familiar interaction patterns that transfer from other applications they use daily.

The result is that even sophisticated terminal applications often require more upfront learning than their GUI equivalents, limiting their appeal to technical users who are comfortable with trial-and-error exploration.

Conclusion

The pendulum swings. For decades, we moved from command-line interfaces to graphical ones because graphics enabled richer interaction. Now perhaps we're seeing a move back toward text-based interfaces because they enable simpler deployment, better automation, and more universal access.

I suspect the best applications of the next decade will be the ones that embrace this constraint and find creative ways to build rich experiences within text or API paradigms. After all, some of the most enduring software tools - Git, Vim, SSH - are still text-based after decades of GUI evolution.

Decades later, the command line is still going strong.

  1. Obviously not as much time as you'd spend in 1990, but it's a notable bounce back from the IDE-rules-everything vibes of the early and mid 2000s. ↩