Files
Tui-pages-website/index.html
2026-06-02 22:35:13 +02:00

749 lines
42 KiB
HTML

<!doctype html>
<html lang="en" data-theme="night" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#09090b" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)">
<title>tui-pages — a framework for building TUIs in Rust</title>
<meta name="description" content="A complete UX framework for building TUIs in Rust. Stop rewriting the same architecture for every project.">
<meta name="keywords" content="rust, tui, ratatui, terminal, framework, ui, cargo, crate">
<meta name="author" content="Filip Riečický">
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
<link rel="canonical" href="https://tui-pages.dev/">
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:title" content="tui-pages — a framework for building TUIs in Rust">
<meta property="og:description" content="A complete framework for building TUIs in Rust on top of ratatui. Pre-programmed focus manager, input pipeline, keymaps, and page navigation.">
<meta property="og:url" content="https://tui-pages.dev/">
<meta property="og:image" content="/static/img/og-image.svg">
<meta property="og:site_name" content="tui-pages">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="tui-pages — a framework for building TUIs in Rust">
<meta name="twitter:description" content="UX for TUIs in Rust. Stop rewriting the same architecture for every project.">
<meta name="twitter:image" content="/static/img/og-image.svg">
<!-- Preconnect -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://unpkg.com" crossorigin>
<!-- Fonts: Inter (UI) + JetBrains Mono (code) -->
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
<!-- Tailwind v3 Play CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'Segoe UI', 'sans-serif'],
mono: ['"JetBrains Mono"', 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'monospace'],
},
colors: {
accent: { DEFAULT: '#b7410e', fg: '#fff5f0' },
},
maxWidth: { '8xl': '88rem' },
},
},
};
</script>
<!-- DaisyUI 4 (prebuilt CSS, loaded before custom CSS) -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.10/dist/full.min.css" rel="stylesheet" type="text/css" />
<!-- highlight.js for code blocks -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark-dimmed.min.css">
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/rust.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/toml.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/bash.min.js" defer></script>
<!-- HTMX 2 -->
<script src="https://unpkg.com/htmx.org@2.0.4" defer></script>
<!-- Alpine.js 3 (theme toggle, mobile menu, tabs, copy buttons) -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
<!-- Our custom layer (after Tailwind + DaisyUI compile) -->
<link rel="stylesheet" href="/static/css/site.css">
<!-- Animated ASCII art layer (chafa-rendered) -->
<link rel="stylesheet" href="/static/css/ascii.css">
</head>
<body
x-data="{ theme: localStorage.getItem('tp-theme') || 'night' }"
x-init="
$watch('theme', t => {
localStorage.setItem('tp-theme', t);
document.documentElement.setAttribute('data-theme', t);
document.documentElement.classList.toggle('dark', t === 'night');
});
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.classList.toggle('dark', theme === 'night');
"
class="min-h-screen bg-base-100 text-base-content antialiased"
>
<!-- ============================ ASCII BACKGROUND ============================ -->
<!-- Full-bleed 3D mountain flyover. tools/mountain.py renders 60 PNG
frames; chafa converts each to 80x24 ASCII; tools/build_mountain_js.py
bundles them into static/js/mountain.js. The background <pre> is
mounted by static/js/mountain-bg.js, which cycles the 60 frames at
12 fps. Style lives in static/css/ascii.css. -->
<pre id="tp-mountain-bg" class="tp-mountain-bg" aria-hidden="true"></pre>
<a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:px-3 focus:py-2 focus:bg-accent focus:text-accent-fg focus:rounded">Skip to content</a>
<!-- 3D mountain flyover: pre-baked ASCII frames cycled at 12 fps -->
<script src="/static/js/mountain.js" defer></script>
<script src="/static/js/mountain-bg.js" defer></script>
<!-- ============================ NAV ============================ -->
<header class="tp-nav sticky top-0 z-40">
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center gap-4">
<a href="/" class="flex items-center gap-2.5 group" aria-label="tui-pages home">
<span class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-accent/10 border border-accent/30 text-accent">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="7 7 13 12 7 17"/>
<rect x="15" y="7" width="2.5" height="10" fill="currentColor" stroke="none"/>
</svg>
</span>
<span class="font-mono font-bold text-base tracking-tight text-zinc-100 group-hover:text-white transition-colors">
tui-pages
</span>
</a>
<ul class="hidden md:flex items-center gap-1 ml-4 text-sm">
<li><a href="/examples.html" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">Examples</a></li>
<li><a href="https://tui-pages.farmeris.sk" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">Book</a></li>
<li><a href="https://docs.rs/tui-pages" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">API</a></li>
<li><a href="https://gitlab.com/filipriec/tui-pages" class="px-3 py-1.5 text-zinc-300 hover:text-white rounded-md hover:bg-zinc-800/50 transition-colors">GitLab</a></li>
</ul>
<div class="ml-auto flex items-center gap-2">
<button
type="button"
@click="theme = (theme === 'night' ? 'winter' : 'night')"
:aria-label="theme === 'night' ? 'Switch to light theme' : 'Switch to dark theme'"
class="w-9 h-9 inline-flex items-center justify-center rounded-md border border-zinc-800 bg-zinc-900/50 text-zinc-300 hover:text-white hover:border-zinc-700 transition-colors"
>
<svg x-show="theme === 'night'" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
<svg x-show="theme !== 'night'" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" x-cloak>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</button>
<a href="https://gitlab.com/filipriec/tui-pages" class="hidden sm:inline-flex items-center gap-1.5 h-9 px-3.5 rounded-md bg-accent text-accent-fg text-sm font-medium hover:bg-accent/90 transition-colors">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 21.5l3.7-11.4H8.3L12 21.5zM2.5 10.1L1 14.4c-.2.6 0 1.3.5 1.6L12 21.5 2.5 10.1zm19 0L21.5 14.4c.2.6 0 1.3-.5 1.6L12 21.5l10-11.4zM15.7 8.6L17.4 2.3c.1-.4-.4-.7-.7-.4L13 4.6c-.4.3-.9.3-1.3.3s-.9 0-1.3-.3L7 1.9c-.3-.3-.8 0-.7.4l1.7 6.3h7.7z"/></svg>
View source
</a>
<button
type="button"
x-data
@click="$dispatch('open-mobile')"
class="md:hidden w-9 h-9 inline-flex items-center justify-center rounded-md border border-zinc-800 text-zinc-300 hover:text-white"
aria-label="Open menu"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
</div>
</nav>
<!-- Mobile menu -->
<div
x-data="{ open: false }"
@open-mobile.window="open = !open"
x-show="open"
x-transition.opacity
x-cloak
class="md:hidden border-t border-zinc-800 bg-zinc-950/95 backdrop-blur"
>
<ul class="px-4 py-3 space-y-1 text-sm">
<li><a href="/examples.html" class="block px-3 py-2 rounded-md text-zinc-300 hover:bg-zinc-800/60">Examples</a></li>
<li><a href="https://tui-pages.farmeris.sk" class="block px-3 py-2 rounded-md text-zinc-300 hover:bg-zinc-800/60">Book</a></li>
<li><a href="https://docs.rs/tui-pages" class="block px-3 py-2 rounded-md text-zinc-300 hover:bg-zinc-800/60">API</a></li>
<li><a href="https://gitlab.com/filipriec/tui-pages" class="block px-3 py-2 rounded-md text-zinc-300 hover:bg-zinc-800/60">GitLab</a></li>
</ul>
</div>
</header>
<main id="main">
<!-- ============================ HERO ============================ -->
<section class="tp-hero relative overflow-hidden">
<div class="tp-hero-glow"></div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-24 lg:pt-28 lg:pb-32">
<div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
<div>
<a href="https://crates.io/crates/tui-pages" class="inline-flex items-center gap-2 rounded-full border border-zinc-800 bg-zinc-900/60 px-3 py-1 text-xs text-zinc-300 hover:border-accent/60 hover:text-white transition-colors">
<span class="inline-block w-1.5 h-1.5 rounded-full bg-accent"></span>
<span class="font-mono">v0.7.2</span>
<span class="text-zinc-500">·</span>
<span>MIT</span>
<span class="text-zinc-500">·</span>
<span class="text-zinc-400">on crates.io</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</a>
<h1 class="mt-6 text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight text-zinc-50 leading-[1.05]">
A framework for<br>
building <span class="text-accent">TUIs</span> in Rust
</h1>
<p class="mt-6 text-lg text-zinc-300 max-w-xl leading-relaxed">
<code class="font-mono text-accent">tui-pages</code> gives you full UX you would ever need. Stop rewriting the same architecture for every project.
</p>
<div class="mt-8 flex flex-wrap items-center gap-3">
<a href="https://docs.rs/tui-pages" class="inline-flex items-center gap-2 h-11 px-5 rounded-lg bg-accent text-accent-fg font-medium hover:bg-accent/90 transition-colors">
Get started
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</a>
<a href="https://tui-pages.farmeris.sk" class="inline-flex items-center gap-2 h-11 px-5 rounded-lg border border-zinc-700 bg-zinc-900/40 text-zinc-100 font-medium hover:border-zinc-500 hover:bg-zinc-900 transition-colors">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
Read the book
</a>
</div>
<!-- Install command with copy button -->
<div
x-data="{ copied: false }"
class="mt-8 inline-flex items-stretch max-w-full rounded-lg border border-zinc-800 bg-zinc-950/80 overflow-hidden font-mono text-sm"
>
<span class="flex items-center gap-2 pl-3 pr-2 text-zinc-500 select-none border-r border-zinc-800">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
</span>
<code class="px-3 py-2.5 text-zinc-100 whitespace-nowrap">cargo add tui-pages</code>
<button
type="button"
@click="navigator.clipboard.writeText('cargo add tui-pages').then(() => { copied = true; setTimeout(() => copied = false, 1400) })"
class="px-3 border-l border-zinc-800 text-zinc-400 hover:text-white hover:bg-zinc-900 transition-colors flex items-center gap-1.5"
:aria-label="copied ? 'Copied' : 'Copy command'"
>
<svg x-show="!copied" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<svg x-show="copied" x-cloak width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
<span x-text="copied ? 'Copied' : 'Copy'" class="text-xs"></span>
</button>
</div>
</div>
<!-- Hero terminal mockup -->
<div class="relative">
<div class="absolute -inset-4 -z-10 bg-gradient-to-br from-accent/15 via-transparent to-emerald-500/5 rounded-3xl blur-2xl"></div>
<div class="tp-float">
<div class="tp-terminal">
<img
src="/static/img/terminal-default.svg"
:src="theme === 'night' ? '/static/img/terminal-default.svg' : '/static/img/terminal-default-light.svg'"
alt="Screenshot of the examples/default TUI app built with tui-pages"
class="block w-full h-auto"
loading="eager" width="640" height="400">
</div>
<p class="mt-3 text-center text-xs text-zinc-500 font-mono">examples/default · cargo run</p>
</div>
</div>
</div>
</div>
</section>
<!-- ============================ TRUST STRIP ============================ -->
<section class="border-y border-zinc-800/60 bg-zinc-950/40">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<dl class="grid grid-cols-2 sm:grid-cols-4 gap-6 sm:gap-8 text-center">
<div>
<dt class="text-xs uppercase tracking-widest text-zinc-500 font-medium">Version</dt>
<dd class="mt-1 text-2xl font-mono font-semibold text-zinc-100">0.7.2</dd>
</div>
<div>
<dt class="text-xs uppercase tracking-widest text-zinc-500 font-medium">License</dt>
<dd class="mt-1 text-2xl font-mono font-semibold text-zinc-100">MIT</dd>
</div>
<div>
<dt class="text-xs uppercase tracking-widest text-zinc-500 font-medium">Features</dt>
<dd class="mt-1 text-2xl font-mono font-semibold text-zinc-100">3 opt-in</dd>
</div>
<div>
<dt class="text-xs uppercase tracking-widest text-zinc-500 font-medium">Renderers</dt>
<dd class="mt-1 text-2xl font-mono font-semibold text-zinc-100">agnostic</dd>
</div>
</dl>
</div>
</section>
<!-- ============================ FEATURES ============================ -->
<section id="features" class="py-24 sm:py-32">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-2xl">
<p class="text-sm font-medium text-accent uppercase tracking-widest">What's inside</p>
<h2 class="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-zinc-50">
Everything a multi-page TUI needs,<br class="hidden sm:block"> already wired up.
</h2>
<p class="mt-4 text-lg text-zinc-400 leading-relaxed">
The crate separates the framework from your pages. You assign element IDs to a list, the framework handles the rest: focus, movement, keymaps, modal dialogs, and page navigation.
</p>
</div>
<div class="mt-14 grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Card: Focus Manager -->
<div class="tp-card">
<div class="tp-card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M3 7V5a2 2 0 0 1 2-2h2M21 7V5a2 2 0 0 0-2-2h-2M3 17v2a2 2 0 0 0 2 2h2M21 17v2a2 2 0 0 1-2 2h-2"/></svg>
</div>
<h3 class="text-lg font-semibold text-zinc-100">Focus Manager</h3>
<p class="mt-2 text-sm text-zinc-400 leading-relaxed">
One global focus model. Be dumb — tell the manager what to focus, don't do it yourself. The system inside it handles library-vs-page focus conflicts.
</p>
</div>
<!-- Card: Input Pipeline -->
<div class="tp-card">
<div class="tp-card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
</div>
<h3 class="text-lg font-semibold text-zinc-100">Input Pipeline</h3>
<p class="mt-2 text-sm text-zinc-400 leading-relaxed">
Map type-safe <code class="font-mono text-zinc-300">Command::Save</code> to a key chord. KeyEvents are flushed into the pipeline; you never write key matching by hand.
</p>
</div>
<!-- Card: Page Navigation -->
<div class="tp-card">
<div class="tp-card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
</div>
<h3 class="text-lg font-semibold text-zinc-100">Page Navigation</h3>
<p class="mt-2 text-sm text-zinc-400 leading-relaxed">
Pages are first-class. Move between them, swap state, render any way you want. The runtime stays out of your way.
</p>
</div>
<!-- Card: Keymaps -->
<div class="tp-card">
<div class="tp-card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/></svg>
</div>
<h3 class="text-lg font-semibold text-zinc-100">Default Keymaps</h3>
<p class="mt-2 text-sm text-zinc-400 leading-relaxed">
Vim-style <span class="tp-kbd">j</span> <span class="tp-kbd">k</span> <span class="tp-kbd">gg</span> movement and VS Code-style navigation come pre-wired. Override or extend per page.
</p>
</div>
<!-- Card: Canvas -->
<div class="tp-card">
<div class="tp-card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>
</div>
<h3 class="text-lg font-semibold text-zinc-100">Canvas integration</h3>
<p class="mt-2 text-sm text-zinc-400 leading-relaxed">
One feature flag. Enables GUI renderers, suggestions, cursor style, validation, computed fields, textareas, text inputs, and a canvas-owned keymap.
</p>
</div>
<!-- Card: Modal dialog -->
<div class="tp-card">
<div class="tp-card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<h3 class="text-lg font-semibold text-zinc-100">Modal Dialog</h3>
<p class="mt-2 text-sm text-zinc-400 leading-relaxed">
Built-in dialog with content and result types plus a ratatui renderer. Enable it, write the content, you're done.
</p>
</div>
</div>
</div>
</section>
<!-- ============================ HOW IT WORKS ============================ -->
<section class="py-24 sm:py-32 border-t border-zinc-800/40 bg-zinc-950/30">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-2xl">
<p class="text-sm font-medium text-accent uppercase tracking-widest">How it works</p>
<h2 class="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-zinc-50">
A primitive list, a pipeline,<br class="hidden sm:block"> and an executor.
</h2>
<p class="mt-4 text-lg text-zinc-400 leading-relaxed">
Imagine <code class="font-mono text-zinc-300">0..n</code> element IDs. You're at ID 1 and want to move next, so you go to 2. That's the whole abstraction. Everything else is built on top of it.
</p>
</div>
<ol class="mt-14 grid md:grid-cols-2 lg:grid-cols-4 gap-4">
<li class="tp-card">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-accent text-accent-fg text-xs font-mono font-bold">01</span>
<h3 class="text-base font-semibold text-zinc-100">KeyEvent</h3>
</div>
<p class="mt-3 text-sm text-zinc-400 leading-relaxed">A key binding like <code class="font-mono text-zinc-300">"ctrl+s"</code> or a plain letter is flushed into the input pipeline.</p>
</li>
<li class="tp-card">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-accent text-accent-fg text-xs font-mono font-bold">02</span>
<h3 class="text-base font-semibold text-zinc-100">InputPipeline</h3>
</div>
<p class="mt-3 text-sm text-zinc-400 leading-relaxed">Maps a key chord to a type-safe <code class="font-mono text-zinc-300">Command::Save</code>.</p>
</li>
<li class="tp-card">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-accent text-accent-fg text-xs font-mono font-bold">03</span>
<h3 class="text-base font-semibold text-zinc-100">Orchestrator</h3>
</div>
<p class="mt-3 text-sm text-zinc-400 leading-relaxed">Decides where the request goes. <code class="font-mono text-zinc-300">j</code><code class="font-mono text-zinc-300">Movement::Down</code> → FocusManager.</p>
</li>
<li class="tp-card">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-accent text-accent-fg text-xs font-mono font-bold">04</span>
<h3 class="text-base font-semibold text-zinc-100">Executor</h3>
</div>
<p class="mt-3 text-sm text-zinc-400 leading-relaxed">Calls the page's function. Login page logs you in. The page owns its own logic.</p>
</li>
</ol>
</div>
</section>
<!-- ============================ CODE EXAMPLE ============================ -->
<section class="py-24 sm:py-32">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-2xl">
<p class="text-sm font-medium text-accent uppercase tracking-widest">Show me the code</p>
<h2 class="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-zinc-50">
Three lines of focus, three lines of pages.
</h2>
<p class="mt-4 text-lg text-zinc-400 leading-relaxed">
Define a page with an element list, register keymaps, run the loop. The framework handles focus, movement, and routing.
</p>
</div>
<div
x-data="{ tab: 'main' }"
class="mt-10 rounded-2xl border border-zinc-800 bg-zinc-950/60 overflow-hidden"
>
<!-- Tab bar -->
<div class="flex items-center gap-1 border-b border-zinc-800 bg-zinc-900/40 px-2 py-1.5 overflow-x-auto">
<button
type="button"
@click="tab = 'main'"
:class="tab === 'main' ? 'bg-zinc-800 text-white' : 'text-zinc-400 hover:text-zinc-200'"
class="px-3 py-1.5 text-xs font-mono rounded-md transition-colors whitespace-nowrap"
>src/main.rs</button>
<button
type="button"
@click="tab = 'cargo'"
:class="tab === 'cargo' ? 'bg-zinc-800 text-white' : 'text-zinc-400 hover:text-zinc-200'"
class="px-3 py-1.5 text-xs font-mono rounded-md transition-colors whitespace-nowrap"
>Cargo.toml</button>
<button
type="button"
@click="tab = 'page'"
:class="tab === 'page' ? 'bg-zinc-800 text-white' : 'text-zinc-400 hover:text-zinc-200'"
class="px-3 py-1.5 text-xs font-mono rounded-md transition-colors whitespace-nowrap"
>pages/login.rs</button>
<button
type="button"
@click="tab = 'keys'"
:class="tab === 'keys' ? 'bg-zinc-800 text-white' : 'text-zinc-400 hover:text-zinc-200'"
class="px-3 py-1.5 text-xs font-mono rounded-md transition-colors whitespace-nowrap"
>keymaps.rs</button>
</div>
<!-- Tab panels (use htmx to lazy-load from static partials) -->
<div class="tp-code relative">
<button
type="button"
x-data
@click="navigator.clipboard.writeText($refs.codeblock.textContent).then(() => { $el.querySelector('[data-label]').textContent = 'Copied'; setTimeout(() => $el.querySelector('[data-label]').textContent = 'Copy', 1200) })"
class="absolute top-3 right-3 z-10 inline-flex items-center gap-1.5 h-7 px-2.5 rounded-md border border-zinc-800 bg-zinc-900/80 text-zinc-400 hover:text-white text-xs transition-colors"
aria-label="Copy code"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<span data-label>Copy</span>
</button>
<div x-show="tab === 'main'" x-ref="codeblock">
<pre><code class="language-rust">use tui_pages::prelude::*;
fn main() -&gt; Result&lt;(), Box&lt;dyn std::error::Error&gt;&gt; {
// 1. Build the runtime with default keymaps and a terminal.
let mut rt = Runtime::new(Terminal::default())?;
// 2. Register pages. Each page owns its own focus list and logic.
rt.register_page("login", pages::login::build());
rt.register_page("home", pages::home::build());
rt.register_page("settings",pages::settings::build());
// 3. Run the main loop. Framework handles focus, movement, and routing.
rt.run()
}
</code></pre>
</div>
<div x-show="tab === 'cargo'" x-cloak x-ref="codeblock">
<pre><code class="language-toml">[package]
name = "my-tui"
version = "0.1.0"
edition = "2021"
[dependencies]
tui-pages = "0.7"
ratatui = "0.29" # only if you enable the `canvas` or `dialog` feature
crossterm = "0.28"
# Pick what you need:
# tui-pages = { version = "0.7", features = ["canvas"] } # +ratatui, GUI surfaces
# tui-pages = { version = "0.7", features = ["dialog"] } # +ratatui, built-in dialog
# tui-pages = { version = "0.7", features = ["serde"] } # +serde on input/mode types
</code></pre>
</div>
<div x-show="tab === 'page'" x-cloak x-ref="codeblock">
<pre><code class="language-rust">use tui_pages::prelude::*;
/// A page is just a focus list + a render fn + an executor.
pub fn build() -&gt; Page {
Page::new("login", render, executor)
// Elements are 0..n. The framework moves through them for you.
.with_elements(|s| s
.input("username")
.input("password")
.button("submit"))
.with_focus(0)
}
fn render(f: &amp;mut Frame, area: Rect, focus: &amp;Focus) {
// Your ratatui render. The focus index is handed to you.
let focused = focus.current();
// ... draw inputs and the button, highlight `focused`
}
fn executor(cmd: Command, focus: &amp;mut Focus, state: &amp;mut State) -&gt; Outcome {
match cmd {
Command::Submit =&gt; state.navigate("home"),
_ =&gt; Outcome::Next,
}
}
</code></pre>
</div>
<div x-show="tab === 'keys'" x-cloak x-ref="codeblock">
<pre><code class="language-rust">use tui_pages::prelude::*;
/// Map key chords to type-safe commands. Override defaults or add your own.
pub fn keymaps() -&gt; KeyMap {
KeyMap::new()
.bind("j", Command::Move(Movement::Down))
.bind("k", Command::Move(Movement::Up))
.bind("gg", Command::Move(Movement::Top))
.bind("G", Command::Move(Movement::Bottom))
.bind("enter", Command::Submit)
.bind("ctrl+s", Command::Save)
.bind("?", Command::OpenDialog(Dialog::Keybindings))
.bind("q", Command::Quit)
}
</code></pre>
</div>
</div>
</div>
</div>
</section>
<!-- ============================ EXAMPLES GALLERY ============================ -->
<section class="py-24 sm:py-32 border-t border-zinc-800/40 bg-zinc-950/30">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-wrap items-end justify-between gap-4">
<div class="max-w-2xl">
<p class="text-sm font-medium text-accent uppercase tracking-widest">Examples</p>
<h2 class="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-zinc-50">Rich examples.</h2>
<p class="mt-4 text-lg text-zinc-400 leading-relaxed">Each one is a simple <code class="font-mono text-zinc-300">cargo run</code> away. Read the source, run it, copy it.</p>
</div>
<a href="/examples.html" class="inline-flex items-center gap-1.5 text-sm text-zinc-300 hover:text-white">
All examples
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</a>
</div>
<div class="mt-12 grid md:grid-cols-3 gap-6">
<a href="/examples.html#default" class="group block rounded-2xl border border-zinc-800 bg-zinc-950/50 overflow-hidden hover:border-accent/60 transition-colors">
<div class="tp-terminal m-3">
<img
src="/static/img/terminal-default.svg"
:src="theme === 'night' ? '/static/img/terminal-default.svg' : '/static/img/terminal-default-light.svg'"
alt="default example screenshot"
class="block w-full h-auto"
loading="lazy" width="640" height="400">
</div>
<div class="p-5 pt-2">
<div class="flex items-center gap-2">
<h3 class="text-base font-semibold text-zinc-100 group-hover:text-accent transition-colors">examples/default</h3>
<span class="tp-kbd">basic</span>
</div>
<p class="mt-2 text-sm text-zinc-400">A multi-page app with a focusable list, page navigation, and quit. The minimal end-to-end example.</p>
</div>
</a>
<a href="/examples.html#canvas" class="group block rounded-2xl border border-zinc-800 bg-zinc-950/50 overflow-hidden hover:border-accent/60 transition-colors">
<div class="tp-terminal m-3">
<img
src="/static/img/terminal-canvas.svg"
:src="theme === 'night' ? '/static/img/terminal-canvas.svg' : '/static/img/terminal-canvas-light.svg'"
alt="canvas example screenshot"
class="block w-full h-auto"
loading="lazy" width="640" height="400">
</div>
<div class="p-5 pt-2">
<div class="flex items-center gap-2">
<h3 class="text-base font-semibold text-zinc-100 group-hover:text-accent transition-colors">examples/canvas</h3>
<span class="tp-kbd">canvas</span>
</div>
<p class="mt-2 text-sm text-zinc-400">Login form with validated inputs, a submit button, and a canvas-owned keymap. Enable the <code class="font-mono text-zinc-300">canvas</code> feature.</p>
</div>
</a>
<a href="/examples.html#keybindings" class="group block rounded-2xl border border-zinc-800 bg-zinc-950/50 overflow-hidden hover:border-accent/60 transition-colors">
<div class="tp-terminal m-3">
<img
src="/static/img/terminal-keybindings.svg"
:src="theme === 'night' ? '/static/img/terminal-keybindings.svg' : '/static/img/terminal-keybindings-light.svg'"
alt="keybindings example screenshot"
class="block w-full h-auto"
loading="lazy" width="640" height="400">
</div>
<div class="p-5 pt-2">
<div class="flex items-center gap-2">
<h3 class="text-base font-semibold text-zinc-100 group-hover:text-accent transition-colors">examples/keybindings</h3>
<span class="tp-kbd">dialog</span>
</div>
<p class="mt-2 text-sm text-zinc-400">A modal showing all keybindings, opened with <span class="tp-kbd">?</span>. Demonstrates the built-in dialog feature.</p>
</div>
</a>
</div>
</div>
</section>
<!-- ============================ WHY ============================ -->
<section class="py-24 sm:py-32">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-2xl">
<p class="text-sm font-medium text-accent uppercase tracking-widest">Why</p>
<h2 class="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-zinc-50">
The architecture you've already written, generalized.
</h2>
</div>
<div class="mt-10 grid md:grid-cols-2 gap-6">
<div class="tp-card">
<p class="text-sm font-mono text-zinc-500 mb-3">// what most TUI code looks like</p>
<pre class="tp-code"><code class="language-rust text-zinc-300">let mut app = App::new();
loop {
app.handle_event(event);
app.focus.update();
app.ui.draw(&amp;mut app);
// shared &amp;mut references
// everywhere
// god object strikes again
}</code></pre>
</div>
<div class="tp-card !border-accent/40">
<p class="text-sm font-mono text-accent mb-3">// what tui-pages looks like</p>
<pre class="tp-code"><code class="language-rust text-zinc-300">let mut rt = Runtime::new(terminal)?;
rt.register_page("home", pages::home::build());
rt.register_page("login", pages::login::build());
rt.register_page("settings",pages::settings::build());
rt.run() // framework handles the rest</code></pre>
</div>
</div>
</div>
</section>
<!-- ============================ CTA ============================ -->
<section class="py-24 sm:py-32 border-t border-zinc-800/40 bg-zinc-950/30">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 class="text-3xl sm:text-5xl font-bold tracking-tight text-zinc-50">
Build your first TUI page<br class="hidden sm:block"> in the next five minutes.
</h2>
<p class="mt-5 text-lg text-zinc-400 max-w-2xl mx-auto">
The book walks you from <code class="font-mono text-zinc-300">cargo new</code> to a working multi-page app. Or read the API reference and dive in.
</p>
<div class="mt-10 flex flex-wrap items-center justify-center gap-3">
<a href="https://tui-pages.farmeris.sk" class="inline-flex items-center gap-2 h-12 px-6 rounded-lg bg-accent text-accent-fg font-medium hover:bg-accent/90 transition-colors">
Start the tutorial
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</a>
<a href="https://docs.rs/tui-pages" class="inline-flex items-center gap-2 h-12 px-6 rounded-lg border border-zinc-700 bg-zinc-900/40 text-zinc-100 font-medium hover:border-zinc-500 hover:bg-zinc-900 transition-colors">
Read the API
</a>
</div>
</div>
</section>
</main>
<!-- ============================ FOOTER ============================ -->
<footer class="border-t border-zinc-800/60 py-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid sm:grid-cols-2 md:grid-cols-4 gap-8">
<div class="sm:col-span-2">
<a href="/" class="inline-flex items-center gap-2.5">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-accent/10 border border-accent/30 text-accent">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="7 7 13 12 7 17"/><rect x="15" y="7" width="2.5" height="10" fill="currentColor" stroke="none"/></svg>
</span>
<span class="font-mono font-bold text-zinc-100">tui-pages</span>
</a>
<p class="mt-3 text-sm text-zinc-500 max-w-md">
A framework for building TUIs in Rust on top of ratatui. MIT licensed. Originally extracted from a production TUI accounting system.
</p>
</div>
<div>
<h4 class="text-xs font-semibold uppercase tracking-widest text-zinc-400">Resources</h4>
<ul class="mt-3 space-y-2 text-sm">
<li><a href="https://docs.rs/tui-pages" class="text-zinc-300 hover:text-white">API reference</a></li>
<li><a href="https://tui-pages.farmeris.sk" class="text-zinc-300 hover:text-white">The Book</a></li>
<li><a href="/examples.html" class="text-zinc-300 hover:text-white">Examples</a></li>
<li><a href="https://crates.io/crates/tui-pages" class="text-zinc-300 hover:text-white">crates.io</a></li>
</ul>
</div>
<div>
<h4 class="text-xs font-semibold uppercase tracking-widest text-zinc-400">Project</h4>
<ul class="mt-3 space-y-2 text-sm">
<li><a href="https://gitlab.com/filipriec/tui-pages" class="text-zinc-300 hover:text-white">GitLab repo</a></li>
<li><a href="https://gitlab.com/filipriec/tui-pages/-/issues" class="text-zinc-300 hover:text-white">Issues</a></li>
<li><a href="https://gitlab.com/filipriec/tui-pages/-/blob/main/LICENSE" class="text-zinc-300 hover:text-white">MIT License</a></li>
</ul>
</div>
</div>
<div class="mt-10 pt-6 border-t border-zinc-800/60 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 text-xs text-zinc-500">
<p>© 2026 Filip Riečický. Released under the MIT License.</p>
<p class="font-mono">crafted with <span class="text-accent">htmx</span> + <span class="text-accent">alpine</span> + <span class="text-accent">tailwind</span></p>
</div>
</div>
</footer>
<!-- Initialise highlight.js after the deferred scripts load -->
<script>
document.addEventListener('DOMContentLoaded', () => {
if (window.hljs) {
document.querySelectorAll('pre code').forEach(el => {
if (!el.classList.contains('hljs')) hljs.highlightElement(el);
});
}
});
</script>
</body>
</html>