プロローグ — 黒い画面のうえのルネサンス
2010年代半ば、誰かに「ターミナルUIを作っている」と言うと2種類の反応が返ってきた。1つめは「なんで?」、2つめは「ncurses?」。そして会話はそこで終わった。
2026年の風景はまったく違う。
lazygitを初めて見た人は「これElectronアプリですか?」と聞く。違う。Goで書かれたTUIだ。k9sはKubernetes運用者の標準ツールになった。Webダッシュボードよりも速い。atuinはシェル履歴をSQLiteに保存し、デバイス間で同期し、TUIで検索する。一度使うと素のCtrl+Rには戻れない。gumはシェルスクリプトに角丸の入力フォームを被せる。gum choose、gum 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 create、gh pr view、gh repo cloneのようなコマンドはお馴染みだが、gh pr checkoutやインタラクティブモードではBubble Teaのパターンが見える。
gum(Go、Bubble Tea + Lipgloss)
Charm社の「シェルスクリプト用ウィジェットライブラリ」。gum input、gum confirm、gum choose、gum 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_egui、egui)が好むパターンがそのままターミナルに来た。
代わりに代償もある。毎フレーム全部描く責任は開発者にある。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)
Model → Update → View。状態は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;
}
色はデザインシステム変数(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がそのまま動く。useState、useEffect、useReducer、カスタムフックまで。ルーティングが必要なら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>
flexDirection、flexGrow、justifyContent、alignItems、padding、margin、width、heightすべて馴染みのCSS名前のまま。
Suspense・streaming・async
React 18+パターン(Suspense、transitions)がInk 5で動く。AIコーディングツールがトークンストリーミングレスポンスを表示するのに適している理由。
誰が使っているか
- Claude Code — Anthropicのコーディングエージェント
- GitHub Copilot CLI —
gh 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・分析) | Textual | async・Pythonライブラリ・豊富なウィジェット |
| AIコーディングツール・CLIウィザード | Ink | streaming UI・React親和 |
| 新規Goサービスの管理コンソール | Bubble Tea | 同じGoコードベースの中で |
| Rustサーバの管理者TUI | Ratatui | 同じバイナリの中で |
| 高速プロトタイプ | Textual | 最少のボイラープレート |
| SSHで公開するマルチユーザーTUI | Bubble Tea + wish | CharmのSSHライブラリ |
| クラスタ運用(k8sなど) | Bubble Tea or Ratatui | キー単機・即応 |
エピローグ — ターミナルは死なず、復活した
長い記事だった。整理する。
一行要約
TUIルネサンスの本質は『レトロエモ』ではなく『キーボードファースト・リモート親和・軽量デプロイ』という3つの価値の再発見であり、Ratatui・Bubble Tea・Textual・Inkがそれぞれ異なる言語エコシステムでその価値を実装する。
30秒チェックリスト
- 新しいTUIを始める前に、本当にTUIが正しい形式か、9章のアンチケースと照合したか?
- 対象ユーザーはキーボード中心のワークフローに慣れているか?
- 言語エコシステム・チーム力に合うtoolkitを選んだか(Rust→Ratatui、Go→Bubble Tea、Python→Textual、Node→Ink)?
- 単一バイナリデプロイが重要か?Rust/Goが正解。
- asyncが重要か?Textual・Inkが自然。
- raw mode・alt screen・resize・Unicode幅を処理する準備はできているか?
- ターミナルデバッグの難しさ(printlnできない)のための別ログファイル/dev consoleを準備したか?
- マウスオプションをオンにするなら、どんなユーザーが拒否するか考えたか?
- truecolorを仮定して良いか?
$COLORTERMを確認するか? - GIF/スクリーンショット用のvhsのようなツールでデモを作りマーケティングまでフォローしたか?
アンチパターン10個
- キー入力ごとに重い計算を同期で回してUIが止まる。
- immediate-mode(Ratatui)で毎フレーム高価なIOを繰り返す。
- Inkの
Textの中にBoxを入れてランタイムエラー。 - Bubble Teaの
Updateで副作用をCmdに隔離せず直接実行。 - Textualの
composeで同期IOを長く塞いで起動時間が爆発。 - raw modeを有効にしたままpanicで終了し、ターミナル状態が壊れる。
- ターミナルresizeイベントを無視してレイアウトが壊れる。
- CJK・絵文字の幅処理をせずウィジェットの境界がずれる。
println/fmt.Println/print()をデバッグに使い画面を壊す。- ユーザーが多様なターミナルを使っていることを忘れ
$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
- Ratatui公式サイト
- Ratatui GitHub
- Ratatui Book(Manning)
- Bubble Tea GitHub
- Charm公式サイト
- Lipgloss GitHub
- Bubblesウィジェットライブラリ
- Wish — SSHサーバライブラリ
- Textual公式ドキュメント
- Textual GitHub
- Richライブラリ
- Ink公式GitHub
- Ink UIコンポーネント
- lazygit GitHub
- gh CLI
- gum — シェルスクリプトウィジェット
- k9s — Kubernetes TUI
- atuin — シェル履歴
- posting — HTTPクライアント(Textual)
- slumber — HTTPクライアント(Ratatui)
- gitui — Git TUI(Ratatui)
- yazi — ファイルマネージャ(Ratatui)
- bottom — システムモニタ(Ratatui)
- btop — システムモニタ(C++)
- glow — Markdownビューア
- vhs — ターミナル録画
- crossterm — RustターミナルIO
- FTXUI — C++ TUI
- Notcurses — C TUI
- blessed-contrib — Node
- harlequin — SQL IDE(Textual)
- television — fuzzy finder(Ratatui)
- tabiew — CSV/Parquetビューア(Ratatui)
- Alacritty
- WezTerm
- Kittyターミナル
- Ghostty
- The Elm Architecture
현재 단락 (1/427)
2010年代半ば、誰かに「ターミナルUIを作っている」と言うと2種類の反応が返ってきた。1つめは「なんで?」、2つめは「ncurses?」。そして会話はそこで終わった。