2026-06-01 23:40:17 +02:00
2026-06-01 23:40:17 +02:00
2026-06-01 23:40:17 +02:00
2026-06-01 23:40:17 +02:00
2026-06-01 20:01:15 +02:00
2026-06-01 20:01:15 +02:00
2026-06-01 22:47:24 +02:00
2026-06-01 23:40:17 +02:00
2026-06-01 23:40:17 +02:00
2026-06-01 23:40:17 +02:00
2026-06-01 23:40:17 +02:00
2026-06-01 20:01:15 +02:00
2026-06-01 20:01:15 +02:00

tui-pages website

Static marketing site for the tui-pages Rust crate. No build step required — open index.html in a browser and you're done.

Stack

Layer Tech Source
HTML Hand-written semantic *.html
CSS framework Tailwind CSS v3 (Play CDN) runtime
Components DaisyUI v4 (prebuilt CSS) runtime
Interactivity HTMX 2 runtime
Light JS Alpine.js 3 runtime
Code highlight highlight.js 11 (Rust, TOML, Bash) runtime
Fonts Inter + JetBrains Mono (Google Fonts) runtime
Icons Lucide (inline SVG) hand-written
Page transitions View Transitions API (native) n/a

Everything is loaded from public CDNs. The only thing served from this repo is the HTML, our small static/css/site.css layer, and the SVG assets in static/img/.

Local development

# Just open the file
xdg-open index.html     # Linux
open index.html         # macOS

# Or serve it (recommended — gives you a stable URL for htmx)
python3 -m http.server 8000
# then visit http://localhost:8000

The Makefile wraps the Python server and a few convenience commands:

make serve       # python3 -m http.server 8000  (serves the WORKING TREE — use this while iterating)
make size        # report file sizes
make validate    # quick HTML syntax check (search for unclosed tags)
make tidy        # run html-tidy on every .html (warnings only)
make video NAME=foo   # regenerate the ASCII background from video/foo.mp4

Note: make serve reads from the working tree (so it picks up the latest static/js/mountain.js after a make video). For a production build, use nix run .#serve — but note that the Nix build only includes git-tracked files, so you must git add your new mountain.js (and any other new files) before nix run .#serve will pick them up.

Animated ASCII video background

The page background is an animated chafa ASCII playback of a video, running at 12 fps behind the entire page. ffmpeg extracts frames from a video you provide; chafa converts each frame to an 80 × 24 ASCII grid; a bundler (tools/build_mountain_js.py) concatenates all grids into a single JS array (static/js/mountain.js). A tiny cycler (static/js/mountain-bg.js) swaps textContent on a single <pre> every ~83 ms.

Swap the video

  1. Drop a .mp4 in video/:

    cp ~/Videos/my-clip.mp4 video/myclip.mp4
    
  2. Build it into the site (run inside the nix dev shell for ffmpeg + chafa):

    nix develop -c make video NAME=myclip
    

    This runs three steps:

    • make video-frames NAME=myclipffmpeg extracts PNGs at 12 fps into build/myclip/, then chafa -s 80x24 --symbols ascii produces ASCII for each
    • make video-bundle NAME=myclip — bundles all frame-*.txt into static/js/mountain.js
    • the wrapper make video does both
  3. Serve and view:

    make serve
    # → http://localhost:8000
    

Helper targets

make video-list              # show available videos in video/
make video-frames NAME=foo   # only extract + chafa, skip the bundle
make video-bundle NAME=foo   # only re-bundle from an existing build/foo/

Tuning

  • Frame rate — edit Makefile fps=12 in the video-frames target. If you change it, also update TP_MOUNTAIN_FPS at the bottom of static/js/mountain.js (regenerated automatically each make video).
  • ASCII resolution-s 80x24 in the chafa call. Wider/higher = larger bundle, more detail.
  • Bundle size — the resulting static/js/mountain.js is ~1.7 kB × N_FRAMES (333 kB for a 15 s clip at 12 fps). For long videos, drop fps or downscale the source video first.
       ↓ chafa -s 80x24
build/mountain/frame-*.txt   intermediate ASCII grids
       ↓
static/js/mountain.js        bundled 60-frame array (≈120 kB)
       ↓ requestAnimationFrame
DOM <pre.tp-mountain-frame>  what you actually see, 12 fps

To re-render (you need chafa and python3 on $PATH, or use nix develop):

make mountain          # frames + bundle, end-to-end
make mountain-frames   # just the PNG + ASCII frames
make mountain-bundle   # just rebundle the JS array from existing frames

How it's wired

  • static/js/mountain.js exposes window.TP_MOUNTAIN_FRAMES (an array of 60 template strings) plus TP_MOUNTAIN_N_FRAMES = 60 and TP_MOUNTAIN_FPS = 12.
  • static/js/mountain-bg.js mounts a <pre class="tp-mountain-frame" aria-hidden="true"> to document.body, sets the first frame as textContent, then cycles with requestAnimationFrame and a setInterval-style accumulator so frame drops don't desync.
  • The script pauses when document.hidden is true (no wasted CPU on backgrounded tabs) and respects prefers-reduced-motion: reduce by showing a single mid-loop frame and not animating.
  • Style lives in static/css/ascii.css:
    • position: fixed; inset: 0; z-index: 0 — full-bleed, behind page content
    • color: #f4a26b (rust-orange) with a 0.45 opacity and a soft text-shadow glow — picks up the brand colour
    • font-family: 'JetBrains Mono' for the in-browser renderer
    • A ::after pseudo-element overlays a 2 px CRT scanline pattern that ticks downward at 8 s/cycle
    • Light theme (data-theme="winter") drops the glow, dims the opacity to 0.20, and switches the scanlines to white-on-transparent
    • The scanline animation is killed inside @media (prefers-reduced-motion: reduce)

The hero (#hero) is not part of the mountain background — it keeps the original static static/img/terminal-default.svg mockup, exactly as designed. The terminal sits on top of the mountain as a discrete card.

Tuning the animation

The renderer is parameterized at the top of tools/mountain.py:

Constant Default What it does
W, H 320 × 200 Output PNG resolution
N_FRAMES 60 How long the loop is
HORIZON_FRAC 0.45 Sky height as a fraction of the image
SEED deterministic Heightmap random seed
STEPS 80 Forward steps per frame in camera space

Bump W/H for sharper terrain, or --symbols ascii+block+half in the chafa call inside tools/build_mountain_js.py for denser glyphs. Both changes require a make mountain rebuild and a higher page weight (mountain.js is currently ≈120 kB).

A flake.nix is included that ships a reproducible dev shell, a buildable package, a serve app, and an html-tidy check. You do not need Nix to use this project — it's a convenience.

# Enter the dev shell (gives you: gnumake, python3, asciinema, html-tidy, …)
nix develop

# Build the whole site → ./result/  (16 files, ~150 kB, ready to upload)
nix build
ls result/

# Build + serve on http://localhost:8000  (or $PORT)
nix run .#serve

# Validate the HTML, format the flake
nix flake check
nix fmt

What the flake provides

Output Command What it does
packages.<sys>.default nix build Assembles the static site into a single derivation
apps.<sys>.serve nix run .#serve Builds the package, then python3 -m http.server it
devShells.<sys>.default nix develop Shell with make, python3, asciinema, tidy, curl
checks.<sys>.tidy nix flake check Runs html-tidy over every .html in the built site
formatter.<sys> nix fmt nixpkgs-fmt for the flake itself

Why no Rust toolchain?

The crate lives in ../komp_ac/tui-pages/. This repo only contains the static website, so the flake intentionally omits rust-overlay and a rustPlatform — they would add hundreds of MB to the shell closure for nothing. When the site eventually gains a Rust backend, those inputs come back.

Why the per-system pattern?

Mirrored from the upstream Codex CLI flake so future contributors see a shape they already recognise. forAllSystems is a one-liner that guarantees linux/darwin × x86_64/aarch64 parity without sprinkling if branches across the file.

Going to production

The CDN approach is fine for marketing pages. For better performance (smaller CSS, no runtime Tailwind compile), swap the Play CDN for the Tailwind standalone CLI:

# 1. Download the standalone CLI
#    https://tailwindcss.com/blog/standalone-cli

# 2. Put the binary in ./bin/tailwindcss

# 3. Create src/site.css that imports Tailwind and DaisyUI:
#        @import "tailwindcss";
#        @plugin "daisyui";

# 4. Build
./bin/tailwindcss -i src/site.css -o static/css/site.css --minify

Then in index.html remove the Tailwind Play CDN <script> and the DaisyUI <link>, and ensure /static/css/site.css is loaded last in <head>.

File layout

tui-pages-web/
├── index.html              # landing page
├── examples.html           # examples gallery
├── 404.html                # not found
├── robots.txt              # search engine hints
├── sitemap.xml             # sitemap
├── tools/
│   ├── mountain.py             # pure-Python 3D-mountain PNG renderer
│   └── build_mountain_js.py    # bundles 60 ASCII frames into mountain.js
├── static/
│   ├── css/
│   │   ├── site.css        # our custom layer (small)
│   │   └── ascii.css       # animation rules for the mountain background
│   ├── img/
│   │   ├── favicon.svg
│   │   ├── logo.svg
│   │   ├── og-image.svg        # 1200x630 social share card
│   │   ├── terminal-default.svg
│   │   ├── terminal-canvas.svg
│   │   └── terminal-keybindings.svg
│   ├── js/
│   │   ├── mountain.js         # 60-frame ASCII array (≈120 kB)
│   │   └── mountain-bg.js      # requestAnimationFrame cycler
│   └── demos/              # (empty — drop asciinema .cast files here)
├── content/                # (empty — markdown for blog/changelog later)
├── flake.nix               # Nix dev shell, package, app, checks
├── flake.lock              # (generated — pinned nixpkgs)
├── Makefile
├── .gitignore
└── README.md

Adding an asciinema demo to the hero

The hero currently shows the static SVG mockup static/img/terminal-default.svg. To replace it with a real asciinema recording (much more impressive, but requires a recorded cast):

  1. Record one of the examples:

    cd ../komp_ac/tui-pages/examples/default
    asciinema rec ../../tui-pages-web/static/demos/intro.cast
    # ... run the app for a few seconds, hit ctrl-d to stop
    
  2. In index.html, replace the <!-- Hero terminal mockup --> block with:

    <div class="tp-terminal">
      <asciinema-player src="/static/demos/intro.cast" autoplay loop></asciinema-player>
    </div>
    
  3. Add the asciinema player to the <head>:

    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/asciinema-player@3.7.0/dist/bundle/asciinema-player.css">
    <script src="https://cdn.jsdelivr.net/npm/asciinema-player@3.7.0/dist/bundle/asciinema-player.js" defer></script>
    

The 3D-mountain ASCII background is independent of the hero and stays as-is.

License

The website source is MIT-licensed. The terminal mockup SVGs in static/img/ are hand-drawn and original. The crate itself is at https://gitlab.com/filipriec/tui-pages.

Description
No description provided
Readme MIT 5.4 MiB
Languages
JavaScript 99%
HTML 0.7%
CSS 0.2%