Skip to content

✍️ 필사 모드: TUIルネサンス 2026 — Ratatui・Bubble Tea・Textual・Inkで再び美しくなったターミナル(深掘りガイド)

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

プロローグ — 黒い画面のうえのルネサンス

2010年代半ば、誰かに「ターミナルUIを作っている」と言うと2種類の反応が返ってきた。1つめは「なんで?」、2つめは「ncurses?」。そして会話はそこで終わった。

2026年の風景はまったく違う。

  • lazygitを初めて見た人は「これElectronアプリですか?」と聞く。違う。Goで書かれたTUIだ。
  • k9sはKubernetes運用者の標準ツールになった。Webダッシュボードよりも速い。
  • atuinはシェル履歴をSQLiteに保存し、デバイス間で同期し、TUIで検索する。一度使うと素のCtrl+Rには戻れない。
  • gumはシェルスクリプトに角丸の入力フォームを被せる。gum choosegum confirmだけでinstall.shが綺麗になる。
  • postingはPostmanをターミナルに持ち込んだ。JSONツリー、Markdownレスポンス、変数環境まで。
  • slumberはHTTPクライアントをTOML設定+キーバインドで書き直した。

これが可能になったのは、4つのtoolkitがほぼ同じ時期に成熟したからだ。

  • Ratatui(Rust、immediate-mode) — tui-rsからフォークされ2023年に正式発足、2026年でv0.31。lazygit的ツールのRust陣営をほぼ独占。
  • Bubble Tea(Go、Elm Architecture) — Charm社製、The Elm Architecture(TEA)のGo移植。lazygit、gh、gum、soft-serveの土台。
  • Textual(Python、宣言+CSSライク) — Will McGugan(Richの作者)によるPython向けtoolkit。2026年でv5.x、postingの土台。
  • Ink(Node.js、JSX/React) — Vadim DemedesによるReact for CLI。Claude Code、GitHub Copilot CLI、Gatsby CLIの土台。

これにC++のFTXUI、Rustのcursive、Cのnotcurses、Nodeのblessed/blessed-contribを加えれば、「ターミナルは死んだプラットフォームではなく、復活したプラットフォームだ」と断言できる。

この記事はそのルネサンスを整理する。なぜまたターミナルか(1章)、どんなデモアプリが流れを変えたか(2章)、4つのtoolkitの哲学の違い(3〜6章)、同じカウンターアプリを4言語で書く比較(7章)、30分で最初のTUIを作る道筋(8章)、正直なトレードオフ(9章)。


第1章 · なぜTUIが再び来たのか — 6つの理由

最初に正直に言う。TUIは「エモい」「レトロ」のためではない。その幻想ではlazygitがGitHub Desktopを置き換えることはない。本当の理由は別にある。

1. SSH・コンテナ・CI環境が日常になった。 クラウド・Kubernetes・リモート開発が普通になり、私たちはよく「GUIのない場所」で働く。SSH1行でk9sを立ち上げられるのは、X11 forwardingでGUIアプリを動かすのとは比べ物にならない。

2. キーボードファーストのワークフローが再発見された。 Vim・Neovim・Emacsユーザーにとってマウスへの往復は認知コストだ。lazygitのCtrl+eでfetch、sでstage、cでcommitという流れはSourceTreeより速い。測定可能な意味で。

3. ターミナルエミュレータが強くなった。 Alacritty・WezTerm・Kitty・GhosttyはGPUアクセラレーション・トゥルーカラー・画像プロトコル・undercurl・リガチャ・マウスイベントまでサポートする。1990年代のncurses時代の256色の限界は過去の話だ。

4. Unicodeとフォントが追いついた。 Nerd Fonts(アイコン)、Powerlineグリフ、罫線素片、角丸(╭─╮)が普通になり、「ターミナルは文字しか描けない」という制約は実質的になくなった。

5. AIコーディングツールのデフォルトUIがTUIになった。 Claude Code、Codex CLI、GitHub Copilot CLI、Aider — コーディングエージェントの主役たちはみなターミナル内に住んでいる。チャットUIがWebでない場所にあることは、もはや奇妙ではなく普通だ。

6. Charm・Ratatuiという『マーケティングの上手いライブラリ』が生まれた。 Charm社はlipgloss・glow・gum・soft-serve・VHSのようなツールで「TUIも綺麗になれる」とデモで証明した。RatatuiはManning社の本(2024年)・チュートリアル・公式サイトratatui.rsでRustのTUIシーンを引っ張った。ツールは常にあった。マーケティングがなかっただけだ。

覚えておく一行: 「TUIはGUIに劣る形式ではない。違う制約のもとで違う最適解を見つける、別の形式だ。」


第2章 · TUIを再びクールにしたデモアプリたち

ツールの成功は、ツール自体ではなく、それで作られた成果物が決める。2020年代のTUI復興を牽引した代表選手を見る。

lazygit(Go、Bubble Tea以前世代のgocui)

Gitの日常作業を全部キー1〜2回で終わらせる。パネル切り替えは1・2・3・4、stageはs、commitはc、pushはP。Git GUIの事実上の代替。2026年5月時点でGitHub星約53k。

gh(Go、Bubble Tea部分使用)

GitHubの公式CLI。gh pr creategh pr viewgh repo cloneのようなコマンドはお馴染みだが、gh pr checkoutやインタラクティブモードではBubble Teaのパターンが見える。

gum(Go、Bubble Tea + Lipgloss)

Charm社の「シェルスクリプト用ウィジェットライブラリ」。gum inputgum confirmgum choosegum spinのようなコマンドをbashから直接呼ぶ。インストールスクリプト・dotfilesブートストラップの事実上の標準。

k9s(Go、tcell + tview)

KubernetesクラスタをTUIで運用。ネームスペース切り替え、pod logs、port-forward、edit deploymentまでキーボードだけで。運用チームの標準ツール。

atuin(Rust、Ratatui)

魔法のようなシェル履歴。SQLiteにコマンド履歴を保存し、デバイス間で同期し、Ctrl+Rでfuzzy検索する。一度使うとデフォルトのCtrl+Rには戻れない。

posting(Python、Textual)

「ターミナル版Postman」。HTTPリクエスト・環境変数・コレクション・JSONツリー表示・Markdownレスポンスのレンダリングまで。Darren BurnsがTextualの限界を見せるために作ったデモアプリが正式なツールに成長した。

slumber(Rust、Ratatui)

YAML設定+キーバインドで書かれたHTTPクライアント。Postmanコレクションをテキストで扱いたい人のための道具。

gitui(Rust、Ratatui)

lazygitのRust版。より速く、メモリも少ないというのがセールスポイント。2026年でv0.27。

btop(C++、独自レンダリング)

top/htopの後継者。角丸ボックス、グラフ、マウス対応。C++で書かれているがUIデザインがTUIルネサンスの視覚的な基準を作った。

glow(Go、Bubble Tea)

Markdownビューア。glow README.mdでターミナルでMarkdownを綺麗に見る。Charmエコシステムの出発点の一つ。

vhs(Go、Bubble Tea + Headlessターミナル)

.tapeスクリプトでターミナルのGIF/MP4を自動生成。READMEのデモGIFが一貫して綺麗に見える秘密。

nushell、tabiew、yazi、television、他

CSV/Parquetビューアtabiew(Rust/Ratatui)、ファイルマネージャyazi(Rust/Ratatui)、検索ツールtelevision(Rust/Ratatui)など、Ratatuiベースのツールが爆発的に増えている。

ここである1つのパターンが見える。Go = Bubble Tea、Rust = Ratatui、Python = Textual、Node = Ink。 各言語の事実上の標準toolkitが定まった。


第3章 · Ratatui — Rustのimmediate-mode TUI

出発点

Florian Dehauが2016年に始めたtui-rsが2023年にメンテナ不在で止まり、コミュニティがratatuiとしてフォークすることで正式に発足した。2026年5月時点でv0.31.x、GitHub星約14k、依存するcrate数は1,000を超えている。Rustエコシステムでatuin・gitui・yazi・bottom・televisionなど事実上すべての主要TUIがRatatui上に乗っている。

中核哲学 — Immediate-mode

ほとんどのGUIフレームワークはretained-modeだ。ウィジェットツリーを作り、状態が変われば更新し、フレームワークが再描画する。Ratatuiはimmediate-modeだ。毎フレーム、開発者が「今フレームのUIはこれだ」を直接描く。

// 毎フレーム呼ばれるdrawクロージャ
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);
})?;

何が良いのか?状態同期のバグがない。ツリーに古いノードが残ることもない。ゲームエンジン(bevy_eguiegui)が好むパターンがそのままターミナルに来た。

代わりに代償もある。毎フレーム全部描く責任は開発者にある。60fpsで回せばdrawクロージャは60回呼ばれる。高価な計算は自分でキャッシュしなければならない。(実際にはinputイベントがあるときだけ再描画するパターンが一般的。)

ウィジェットとレイアウト

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]);

基本ウィジェット: Block、Paragraph、List、Table、Tabs、Gauge、Sparkline、BarChart、Chart、Canvas。Canvasでは点・線・世界地図まで描ける。

イベント — crossterm

Ratatui自体は描画のみで、イベントはcrossterm(または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,
            _ => {}
        }
    }
}

誰が使っているか

  • atuin — シェル履歴
  • gitui — Git TUI
  • yazi — ファイルマネージャ
  • bottom — システムモニタ(htop代替)
  • television — fuzzy finder、fzfの後継候補
  • tabiew — CSV/Parquetビューア
  • slumber — HTTPクライアント

強みと弱み

  • 強み — 性能(コンパイル済みRust)、メモリ効率、安定性、豊富なウィジェット、活発なコミュニティ、Manning社の本(2024年)、ratatui.rs公式サイト。
  • 弱み — immediate-modeの学習曲線、ボイラープレート(crossterm初期化・alternate screen・raw mode)、デバッグが難しい(ターミナル自体がstdoutなのでprintlnデバッグ不可)。

第4章 · Bubble Tea — GoのThe Elm Architecture

出発点

Charm社(Toby Padilla・Christian Rocha)が2020年にリリース。The Elm Architecture(TEA)をGoに直接移植した。2026年5月時点でv1.3、GitHub星約32k。lazygitが基盤を移行中、gh CLIの一部、gum全体、glow全体、soft-serve全体がBubble Tea上に乗っている。

中核哲学 — Elm Architecture(MVU)

ModelUpdateView。状態はModelにある。メッセージ(イベント)が入ってきたらUpdate(msg, model) -> (newModel, cmd)View(model) -> stringが画面を描く。副作用はCmd(高階関数)で隔離する。

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)
}

この関数型パターンの利点はimmediate-modeとは違う。状態変化の流れが一箇所にある。Updateを見るだけで何が可能な変化かわかる。テストしやすい(純粋関数)。

Lipgloss — スタイリング

Bubble Tea単独では色・ボックス・配置のみを描く。その上にLipglossがCSSライクなスタイルAPIを乗せる。

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"))

色・余白・ボックス・配置・配置・切り詰め・折り返しすべてメソッドチェーンで。角丸ボックスも1行。

Bubbles — ウィジェットライブラリ

Bubble Teaコアは意図的に小さい。ウィジェットは別ライブラリのbubblesに。textinput、textarea、list、table、viewport、spinner、progress、paginator、key。

Wish & Soft Serve — TUIをSSHで

Charmの別の作品。wishはSSHサーバライブラリ。Bubble TeaアプリをSSHで公開すると、ユーザーはssh tui.example.comだけで接続できる。インストール不要。soft-serve(CharmのセルフホストGitサーバ)がこのパターンの代表例。

誰が使っているか

  • gum — シェルスクリプトウィジェット
  • glow — Markdownビューア
  • vhs — ターミナル録画
  • soft-serve — Git サーバTUI
  • bubbletea-app-templateベースの新興アプリ多数
  • lazygitの一部 — 段階的移行

強みと弱み

  • 強み — 綺麗なアーキテクチャ、関数型なのでテストしやすい、Lipglossで視覚的ポリッシュが高い、Charmエコシステムの巨大なツール群。
  • 弱み — Goインターフェースのキャスティングのボイラープレート(tea.Msg型スイッチ)、非常に大きなモデルでのimmutable updateのコスト、アドホックな非同期はtea.Cmdで包む必要。

第5章 · Textual — Pythonの宣言型TUI

出発点

Will McGugan(Richライブラリの作者)が2021年に開始。Richが「綺麗なprint」ならTextualは「綺麗なアプリ」。2026年5月時点でv5.x、GitHub星約28k。postingの成功が決定打になった。

中核哲学 — Webっぽい宣言

CSSライクなスタイリング(.tcss)、ウィジェットツリー、イベントハンドラ、async-first。一言で言えば「ターミナルで動くReactっぽいもの」 — ただし明示的な仮想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()

ここで見えるもの — composeでツリーを宣言、BINDINGSで単機キーをマッピング、action_*メソッドが自動で呼ばれる、query_oneでCSSライクなセレクタでウィジェットを取得。

CSSライクなスタイル — TCSS

別の.tcssファイルにスタイルを分離。

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

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

色はデザインシステム変数(primaryprimary、boost、$successなど)をそのまま使える。

豊富なウィジェット

Static、Label、Button、Input、TextArea、Select、Switch、Checkbox、RadioSet、ListView、DataTable、Tree、Tabs、ContentSwitcher、ProgressBar、Sparkline、LoadingIndicator、Pretty、Markdown、RichLog、Log、Header、Footer、Placeholder。本当に多い。

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のasyncioを一級で受け入れているので、HTTP・DB・ファイルIOが自然だ。postingがこれを活用した代表例。

Textual Web — 同じコード、Webで

textual serveで同じアプリをブラウザで立ち上げられる。SSH・ローカル・Webの3つのデプロイ経路。2026年現在ベータだが動く。

誰が使っているか

  • posting — ターミナルHTTPクライアント(Postman代替)
  • harlequin — DuckDB・PostgreSQL SQL IDE
  • frogmouth — Markdownビューア
  • memray — メモリプロファイラ(部分)
  • 社内データツール多数

強みと弱み

  • 強み — Python親和的、async自然、CSS分離でデザイナと開発者の分業しやすい、Textual WebでWebデプロイまで、豊富なウィジェット、Richとのシナジー(Markdown・コードハイライト)。
  • 弱み — Python起動時間(インタプリタ + import)、メモリ使用量(Rust/Goより重い)、GILの限界、async学習曲線。

第6章 · Ink — Node.js + JSX/React for CLI

出発点

Vadim Demedesが2017年に開始。「Reactコンポーネントでcliを書きたい」という単純なアイデアから出発。2026年5月時点でv5.x、GitHub星約28k。Claude Code、GitHub Copilot CLI、Gatsby CLI、Prisma CLI、AWS Amplify CLIすべてInkで書かれている。

中核哲学 — ReactコンポーネントがテキストをRender

ブラウザでReactがDOMをレンダリングするように、InkはReactをターミナルテキストにレンダリングする。<div>/<span>の代わりに<Box>/<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がそのまま動く。useStateuseEffectuseReducer、カスタムフックまで。ルーティングが必要ならink-router、フォームが必要ならink-form、入力はink-text-input、マルチセレクトはink-multi-select

Flexboxレイアウト

ブラウザのようにFlexboxで配置。

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

flexDirectionflexGrowjustifyContentalignItemspaddingmarginwidthheightすべて馴染みのCSS名前のまま。

Suspense・streaming・async

React 18+パターン(Suspense、transitions)がInk 5で動く。AIコーディングツールがトークンストリーミングレスポンスを表示するのに適している理由。

誰が使っているか

  • Claude Code — Anthropicのコーディングエージェント
  • GitHub Copilot CLIgh copilot
  • Gatsby CLI
  • Prisma CLI
  • AWS Amplify CLI
  • Twilio CLI

強みと弱み

  • 強み — React知識がそのまま転移、npmエコシステム、JSXの表現力、AI/streaming UIの自然さ、デザイナ親和的。
  • 弱み — Node.js起動コスト(node + V8 + 依存ロード)、メモリ(最も重い)、Textの中にBoxを入れられないなどのテキストノードツリーの制約、大きなツリーでの再レンダリングコスト。

第7章 · 同じカウンターアプリ、4言語で — 直接比較

最も単純なアプリ:「↑/↓でカウント増減、qで終了。」4つのtoolkitがどう違うか、一画面で見る。

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 />);

コード行数の比較

  • Ratatui: 約35行(ターミナルraw modeセットアップ含む)。最も長いが最も明示的。
  • Bubble Tea: 約30行。MVUパターンは綺麗だが型スイッチがボイラープレート。
  • Textual: 約20行。最も短く最も読みやすい。
  • Ink: 約15行。JSX親和的なら最も短い。

行数即優劣ではない。最も短いコード(Ink)は最も重いランタイム(Node + V8)を敷いており、最も長いコード(Ratatui)は最も軽い単一静的バイナリを作る。


第8章 · 最初のTUI 30分 — 実戦経路

Path A: Ratatuiで始める(性能・デプロイ優先)

# 1. 新しいプロジェクト
cargo new my_tui && cd my_tui

# 2. 依存を追加
cargo add ratatui crossterm

# 3. src/main.rsに上のカウンター例を貼り付け
cargo run

デプロイはcargo build --release1行で単一静的バイナリ。依存zero。

Path B: Bubble Teaで始める(Goエコシステム親和)

mkdir my_tui && cd my_tui
go mod init my_tui
go get github.com/charmbracelet/bubbletea
# main.goに上の例を貼り付け
go run main.go

クロスプラットフォームビルドはGOOS=linux GOARCH=amd64 go buildのように簡単。

Path C: Textualで始める(Python親和)

pip install textual
python counter.py

textual run --dev counter.pyでhot reload + dev consoleまでオンになる。

Path D: Inkで始める(React親和)

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

スキャフォールドがテキスト入力・テスト・ビルドまで全部やってくれる。

その次にやること — 共通チェックリスト

  • alt screen モードの出入りを正確に処理(Ratatuiは明示的、他は自動)。
  • raw mode進入時はCtrl+C処理も自前で(またはtoolkitが処理)。
  • resizeイベントを受けたら再描画。
  • マウスイベントはオプトイン。
  • truecolorサポートを$COLORTERMで確認。
  • Unicode幅の処理(絵文字・CJKは2セル)。unicode-widthクレートまたはwcwidthを使う。
  • ログ分離 — printlnでデバッグできない。別ログファイルに書くかdev consoleで。

第9章 · 正直なトレードオフ — TUIが合わないケース

TUIはすべてを解決しない。次の場合は別の道具のほうが良い。

1. チャート・グラフ重めのダッシュボード

罫線素片でsparklineは描けるが、散布図・ヒートマップ・3Dチャートは限界が明確。データ可視化が中心ならWeb/Streamlit・Grafanaが良い。

2. 画像・動画

iTerm2・Kitty・WezTermのインライン画像プロトコルがあるが、ユーザー環境依存が大きい。tmuxの中で壊れることが多い。

3. マウス中心のワークフロー

複雑なドラッグ&ドロップ、視覚編集(ダイアグラム・画像・キャンバス)。可能ではあるが、人々は好まない。

4. 非開発者向け

TUIはユーザーにキー単機やモード概念を要求する。一般ユーザーには学習曲線が急すぎる。

5. アクセシビリティがcritical

スクリーンリーダーはGUI/Webに最適化されており、TUIへのサポートは部分的だ。視覚障害ユーザーの比率が大きいツールはGUI/Webが良い。

6. 非常に大きなデータを一画面に

ターミナルは1行あたりの文字数、画面あたりの行数に制限がある。仮想化スクロール・ページネーションが必要だが、Webのほうが自然だ。

覚えておく一行: 「TUIはキーボードファースト・リモートアクセス・軽さが価値となる場所で輝く。可視化中心・一般ユーザー・アクセシビリティcriticalな場所ではGUI/Webが良い。」


第10章 · ツール選択マトリクス

状況別にどのtoolkitを選ぶか、一表で。

状況推奨理由
システムツール(ファイル/Git/モニタリング)Ratatui性能・デプロイ・単一バイナリ
シェルスクリプトウィジェットBubble Tea(gum)即使用可、標準ツール
社内データツール(SQL・HTTP・分析)Textualasync・Pythonライブラリ・豊富なウィジェット
AIコーディングツール・CLIウィザードInkstreaming UI・React親和
新規Goサービスの管理コンソールBubble Tea同じGoコードベースの中で
Rustサーバの管理者TUIRatatui同じバイナリの中で
高速プロトタイプTextual最少のボイラープレート
SSHで公開するマルチユーザーTUIBubble Tea + wishCharmのSSHライブラリ
クラスタ運用(k8sなど)Bubble Tea or Ratatuiキー単機・即応

エピローグ — ターミナルは死なず、復活した

長い記事だった。整理する。

一行要約

TUIルネサンスの本質は『レトロエモ』ではなく『キーボードファースト・リモート親和・軽量デプロイ』という3つの価値の再発見であり、Ratatui・Bubble Tea・Textual・Inkがそれぞれ異なる言語エコシステムでその価値を実装する。

30秒チェックリスト

  1. 新しいTUIを始める前に、本当にTUIが正しい形式か、9章のアンチケースと照合したか?
  2. 対象ユーザーはキーボード中心のワークフローに慣れているか?
  3. 言語エコシステム・チーム力に合うtoolkitを選んだか(Rust→Ratatui、Go→Bubble Tea、Python→Textual、Node→Ink)?
  4. 単一バイナリデプロイが重要か?Rust/Goが正解。
  5. asyncが重要か?Textual・Inkが自然。
  6. raw mode・alt screen・resize・Unicode幅を処理する準備はできているか?
  7. ターミナルデバッグの難しさ(printlnできない)のための別ログファイル/dev consoleを準備したか?
  8. マウスオプションをオンにするなら、どんなユーザーが拒否するか考えたか?
  9. truecolorを仮定して良いか?$COLORTERMを確認するか?
  10. GIF/スクリーンショット用のvhsのようなツールでデモを作りマーケティングまでフォローしたか?

アンチパターン10個

  1. キー入力ごとに重い計算を同期で回してUIが止まる。
  2. immediate-mode(Ratatui)で毎フレーム高価なIOを繰り返す。
  3. InkのTextの中にBoxを入れてランタイムエラー。
  4. Bubble TeaのUpdateで副作用をCmdに隔離せず直接実行。
  5. Textualのcomposeで同期IOを長く塞いで起動時間が爆発。
  6. raw modeを有効にしたままpanicで終了し、ターミナル状態が壊れる。
  7. ターミナルresizeイベントを無視してレイアウトが壊れる。
  8. CJK・絵文字の幅処理をせずウィジェットの境界がずれる。
  9. println/fmt.Println/print()をデバッグに使い画面を壊す。
  10. ユーザーが多様なターミナルを使っていることを忘れ$TERM/$COLORTERM未チェック。

次回予告

次回候補: Charmエコシステム征服記 — Bubble Tea・Lipgloss・Bubbles・Wish・Glow・VHSを一息でRatatuiでGit TUIを作る — lazygitのRustクローンを1週間でTextual + SQLiteで社内データツール — Pythonの強みをTUIに

「GUIはマウスの時代を定義し、TUIはキーボードの時代を再びもたらした。両者は敵ではなく、同じ作業への異なる答えだ。」

— TUIルネサンス 2026、終わり。


参考 / References

현재 단락 (1/427)

2010年代半ば、誰かに「ターミナルUIを作っている」と言うと2種類の反応が返ってきた。1つめは「なんで?」、2つめは「ncurses?」。そして会話はそこで終わった。

작성 글자: 0원문 글자: 19,169작성 단락: 0/427