727 lines
41 KiB
HTML
727 lines
41 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 framework for building TUIs in Rust on top of ratatui. Pre-programmed focus manager, input pipeline, keymaps, and page navigation. 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="Pre-programmed focus, keymaps, and page navigation. Stop rewriting the same architecture for every TUI 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 a pre-programmed focus manager, input pipeline, keymaps, and page navigation on top of <a href="https://ratatui.rs" class="text-zinc-100 underline decoration-zinc-700 underline-offset-4 hover:decoration-accent">ratatui</a>. 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" 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() -> Result<(), Box<dyn std::error::Error>> {
|
|
// 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() -> 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: &mut Frame, area: Rect, focus: &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: &mut Focus, state: &mut State) -> Outcome {
|
|
match cmd {
|
|
Command::Submit => state.navigate("home"),
|
|
_ => 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() -> 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">Three apps in the box.</h2>
|
|
<p class="mt-4 text-lg text-zinc-400 leading-relaxed">Each one is a full <code class="font-mono text-zinc-300">cargo run</code> away. Read the source, run it, fork 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" 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" 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" 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(&mut app);
|
|
// shared &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>
|