README: real terminal shots (tools/readme-term-shots.mjs) instead of hand-drawn SVGs, a badge row
The architecture and create-terminal SVGs were drawn by hand and their text overflowed. The quick start now shows the real output of npm create waybetter, rendered the way storebound renders its terminal shots; the layers stay a table.
This commit is contained in:
parent
612322a45e
commit
8d45e3b7b3
8 changed files with 135 additions and 159 deletions
15
README.md
15
README.md
|
|
@ -13,6 +13,12 @@ GPL-3.0-or-later · <code>npm create waybetter</code></p>
|
||||||
<a href="docs/STRING-PACKS.md">String packs</a> ·
|
<a href="docs/STRING-PACKS.md">String packs</a> ·
|
||||||
<a href="https://git.crt.fyi/CRT/waybetter-for-x">wayBetter for X</a></p>
|
<a href="https://git.crt.fyi/CRT/waybetter-for-x">wayBetter for X</a></p>
|
||||||
|
|
||||||
|
<p align="center"><a href="https://www.npmjs.com/package/waybetter"><img src="https://img.shields.io/npm/v/waybetter?label=npm&color=cb3837" alt="npm"></a>
|
||||||
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-GPL--3.0--or--later-blue" alt="license: GPL-3.0-or-later"></a>
|
||||||
|
<a href="packages/waybetter/package.json"><img src="https://img.shields.io/badge/runtime%20dependencies-0-brightgreen" alt="runtime dependencies: 0"></a>
|
||||||
|
<img src="https://img.shields.io/badge/Tauri-2-24c8db" alt="Tauri 2">
|
||||||
|
<img src="https://img.shields.io/badge/platforms-macOS%20%7C%20Windows%20%7C%20Linux-informational" alt="platforms: macOS, Windows, Linux"></p>
|
||||||
|
|
||||||
You bring a site and a small adapter that says what the site is. wayBetter gives you the rest: a
|
You bring a site and a small adapter that says what the site is. wayBetter gives you the rest: a
|
||||||
Tauri 2 window with tabs, split panes and every account signed in side by side; a page runtime with
|
Tauri 2 window with tabs, split panes and every account signed in side by side; a page runtime with
|
||||||
a plugin API, a theme engine and string packs; a signed Store with a safe install screen; backups,
|
a plugin API, a theme engine and string packs; a signed Store with a safe install screen; backups,
|
||||||
|
|
@ -29,8 +35,6 @@ encrypted sync and signed updates; and the tooling to scaffold, sign, scan and r
|
||||||
|
|
||||||
## How it fits together
|
## How it fits together
|
||||||
|
|
||||||
<p align="center"><img src="assets/readme/architecture.svg" width="880" alt="Architecture: a Tauri 2 window with a trusted shell webview and untrusted site-tab webviews running the page runtime, the Rust host underneath that re-checks every command, the app's adapter on the side and the waybetter tooling"></p>
|
|
||||||
|
|
||||||
A wayBetter app is one Tauri 2 window with several webviews. The framework has one piece for each
|
A wayBetter app is one Tauri 2 window with several webviews. The framework has one piece for each
|
||||||
layer and does not need to know which site you wrap.
|
layer and does not need to know which site you wrap.
|
||||||
|
|
||||||
|
|
@ -48,7 +52,7 @@ Details of every module are in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
||||||
npm create waybetter app kickapp -- --site kick.com --name "wayBetter for Kick" --yes
|
npm create waybetter app kickapp -- --site kick.com --name "wayBetter for Kick" --yes
|
||||||
```
|
```
|
||||||
|
|
||||||
<p align="center"><img src="assets/readme/create-terminal.svg" width="820" alt="Terminal output: Created a wayBetter app in kickapp (32 files), then the next steps: cd kickapp, add the framework as a git submodule, npm install, npm run build, npm run tauri dev"></p>
|

|
||||||
|
|
||||||
The same tool starts a plugin or a theme:
|
The same tool starts a plugin or a theme:
|
||||||
|
|
||||||
|
|
@ -57,6 +61,8 @@ npm create waybetter plugin -- --name "Hide Trends" # add --ts for Ty
|
||||||
npm create waybetter theme -- --name "Midnight Mint" --accent "#3ddc97"
|
npm create waybetter theme -- --name "Midnight Mint" --accent "#3ddc97"
|
||||||
```
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
The app scaffold adds the framework as a git submodule at `waybetter/` by default (`--host-source
|
The app scaffold adds the framework as a git submodule at `waybetter/` by default (`--host-source
|
||||||
path` and `--host-source git` are the alternatives). After `npm run tauri dev`, replace the
|
path` and `--host-source git` are the alternatives). After `npm run tauri dev`, replace the
|
||||||
placeholder selectors in `src/site.ts` and `src-tauri/src/site.rs`. All flags:
|
placeholder selectors in `src/site.ts` and `src-tauri/src/site.rs`. All flags:
|
||||||
|
|
@ -241,6 +247,9 @@ bun run test:create # scaffolds each kind into temp folders and checks them a
|
||||||
`test:create` also runs `cargo check` on a generated app (about a minute cold);
|
`test:create` also runs `cargo check` on a generated app (about a minute cold);
|
||||||
`WAYBETTER_SKIP_CARGO_CHECK=1` skips that step. Requires Bun 1.4.2+, Node 24+ and Rust 1.88+.
|
`WAYBETTER_SKIP_CARGO_CHECK=1` skips that step. Requires Bun 1.4.2+, Node 24+ and Rust 1.88+.
|
||||||
|
|
||||||
|
The README's terminal images are real output: `node tools/readme-term-shots.mjs` runs the commands and
|
||||||
|
redraws `assets/readme/term/` (Google Chrome and cwebp).
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): every module of the host, runtime and shell
|
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): every module of the host, runtime and shell
|
||||||
|
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 760" width="1200" height="760" role="img" aria-label="wayBetter architecture: one Tauri 2 window with a trusted shell webview and one webview per site tab, a Rust host underneath, your adapter on the side, and the waybetter command for store, release and build tooling">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#0b1020"/>
|
|
||||||
<stop offset="1" stop-color="#06070c"/>
|
|
||||||
</linearGradient>
|
|
||||||
<radialGradient id="glow" cx="0.5" cy="0" r="0.7">
|
|
||||||
<stop offset="0" stop-color="#1d3a8a" stop-opacity="0.55"/>
|
|
||||||
<stop offset="1" stop-color="#1d3a8a" stop-opacity="0"/>
|
|
||||||
</radialGradient>
|
|
||||||
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
|
|
||||||
<stop offset="0" stop-color="#3b82f6"/>
|
|
||||||
<stop offset="0.55" stop-color="#22d3ee"/>
|
|
||||||
<stop offset="1" stop-color="#a855f7"/>
|
|
||||||
</linearGradient>
|
|
||||||
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
||||||
<path d="M0 0L10 5L0 10z" fill="#64748b"/>
|
|
||||||
</marker>
|
|
||||||
<style>
|
|
||||||
.t{font-family:ui-sans-serif,"Segoe UI",system-ui,-apple-system,Helvetica,Arial,sans-serif;fill:#e6e9f2}
|
|
||||||
.m{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace}
|
|
||||||
.h{font-size:22px;font-weight:700;letter-spacing:-0.01em}
|
|
||||||
.k{font-size:12px;font-weight:600;letter-spacing:0.14em;fill:#7dd3fc}
|
|
||||||
.s{font-size:14px;fill:#a3acc2}
|
|
||||||
.c{font-size:13px;fill:#cbd5e1}
|
|
||||||
.box{fill:#0f1626;stroke:#24304a;stroke-width:1.2}
|
|
||||||
.trust{fill:#101b33;stroke:#3b82f6;stroke-width:1.4}
|
|
||||||
.site{fill:#140f26;stroke:#7c3aed;stroke-width:1.4}
|
|
||||||
.chip{fill:#17213a;stroke:#2b3a5c}
|
|
||||||
.ln{stroke:#64748b;stroke-width:1.4;fill:none}
|
|
||||||
.dash{stroke-dasharray:5 5}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<rect width="1200" height="760" rx="18" fill="url(#bg)"/>
|
|
||||||
<rect width="1200" height="380" rx="18" fill="url(#glow)"/>
|
|
||||||
|
|
||||||
<!-- title -->
|
|
||||||
<text class="t k" x="48" y="54">— ARCHITECTURE</text>
|
|
||||||
<text class="t" x="48" y="92" font-size="32" font-weight="760" letter-spacing="-0.02em">One window, three layers, one small adapter.</text>
|
|
||||||
<rect x="48" y="104" width="560" height="3" rx="1.5" fill="url(#accent)"/>
|
|
||||||
|
|
||||||
<!-- the site -->
|
|
||||||
<rect x="48" y="138" width="760" height="64" rx="12" fill="none" stroke="#334155" stroke-dasharray="6 6"/>
|
|
||||||
<text class="t h" x="72" y="170" font-size="18">The website you already use</text>
|
|
||||||
<text class="t s" x="72" y="190">loaded live from its exact https origin, never copied: x.com, kick.com, …</text>
|
|
||||||
|
|
||||||
<!-- window -->
|
|
||||||
<rect x="48" y="226" width="760" height="300" rx="16" class="box"/>
|
|
||||||
<text class="t k" x="72" y="256">TAURI 2 WINDOW · SYSTEM WEBVIEW</text>
|
|
||||||
|
|
||||||
<!-- shell -->
|
|
||||||
<rect x="72" y="272" width="300" height="234" rx="12" class="trust"/>
|
|
||||||
<text class="t h" x="92" y="304">Shell</text>
|
|
||||||
<text class="t m s" x="92" y="326" font-size="12">packages/shell · local, trusted</text>
|
|
||||||
<g class="t c">
|
|
||||||
<text x="92" y="356">Tabs, split panes, address bar</text>
|
|
||||||
<text x="92" y="380">Command palette, accounts, decks</text>
|
|
||||||
<text x="92" y="404">Settings panel, theme builder</text>
|
|
||||||
<text x="92" y="428">Store and the install screen</text>
|
|
||||||
<text x="92" y="452">Workspaces, backup, updates</text>
|
|
||||||
</g>
|
|
||||||
<text class="t s" x="92" y="488" font-size="12">the only webview allowed to change anything</text>
|
|
||||||
|
|
||||||
<!-- tabs -->
|
|
||||||
<rect x="392" y="272" width="396" height="234" rx="12" class="site"/>
|
|
||||||
<text class="t h" x="412" y="304">Site tabs</text>
|
|
||||||
<text class="t m s" x="412" y="326" font-size="12">one webview + profile per tab · packages/runtime</text>
|
|
||||||
<g>
|
|
||||||
<rect x="412" y="342" width="110" height="30" rx="8" class="chip"/><text class="t c" x="425" y="362">Plugin API</text>
|
|
||||||
<rect x="530" y="342" width="106" height="30" rx="8" class="chip"/><text class="t c" x="543" y="362">Theme engine</text>
|
|
||||||
<rect x="644" y="342" width="124" height="30" rx="8" class="chip"/><text class="t c" x="657" y="362">String packs</text>
|
|
||||||
<rect x="412" y="380" width="110" height="30" rx="8" class="chip"/><text class="t c" x="425" y="400">DOM hub</text>
|
|
||||||
<rect x="530" y="380" width="106" height="30" rx="8" class="chip"/><text class="t c" x="543" y="400">Fetch / XHR hub</text>
|
|
||||||
<rect x="644" y="380" width="124" height="30" rx="8" class="chip"/><text class="t c" x="657" y="400">Link routing</text>
|
|
||||||
<rect x="412" y="418" width="110" height="30" rx="8" class="chip"/><text class="t c" x="425" y="438">Accounts</text>
|
|
||||||
<rect x="530" y="418" width="106" height="30" rx="8" class="chip"/><text class="t c" x="543" y="438">UI kit</text>
|
|
||||||
<rect x="644" y="418" width="124" height="30" rx="8" class="chip"/><text class="t c" x="657" y="438">Deck columns</text>
|
|
||||||
</g>
|
|
||||||
<text class="t s" x="412" y="488" font-size="12">untrusted: may only report about itself</text>
|
|
||||||
|
|
||||||
<!-- arrows site -> tabs -->
|
|
||||||
<path class="ln" d="M590 202 V268" marker-end="url(#arr)"/>
|
|
||||||
|
|
||||||
<!-- host -->
|
|
||||||
<rect x="48" y="550" width="760" height="166" rx="16" class="box"/>
|
|
||||||
<text class="t k" x="72" y="580">NATIVE HOST · RUST</text>
|
|
||||||
<text class="t h" x="72" y="612">waybetter-host</text>
|
|
||||||
<text class="t m s" x="258" y="612" font-size="12">crates/waybetter-host</text>
|
|
||||||
<g class="t c">
|
|
||||||
<text x="72" y="644">Every command re-checks its caller (webview label, exact origin)</text>
|
|
||||||
<text x="72" y="668">Profiles, settings, plugins, themes, fonts, workspaces</text>
|
|
||||||
<text x="72" y="692">Signed Store (Ed25519), backup, encrypted sync, signed updates</text>
|
|
||||||
</g>
|
|
||||||
<path class="ln" d="M222 506 V546" marker-end="url(#arr)" marker-start="url(#arr)"/>
|
|
||||||
<path class="ln dash" d="M590 506 V546" marker-end="url(#arr)"/>
|
|
||||||
<text class="t s" x="236" y="532" font-size="12">all commands</text>
|
|
||||||
<text class="t s" x="604" y="532" font-size="12">a short, reviewed list</text>
|
|
||||||
|
|
||||||
<!-- adapter -->
|
|
||||||
<rect x="836" y="138" width="316" height="300" rx="16" fill="#0f1a1a" stroke="#14b8a6" stroke-width="1.4"/>
|
|
||||||
<text class="t k" x="860" y="168" style="fill:#5eead4">YOUR APP</text>
|
|
||||||
<text class="t h" x="860" y="200">The adapter</text>
|
|
||||||
<text class="t s" x="860" y="222">what only your site knows</text>
|
|
||||||
<rect x="860" y="242" width="268" height="52" rx="10" class="chip"/>
|
|
||||||
<text class="t m c" x="876" y="264">site.rs · SiteSpec</text>
|
|
||||||
<text class="t s" x="876" y="283" font-size="12">hosts, account rules</text>
|
|
||||||
<rect x="860" y="304" width="268" height="52" rx="10" class="chip"/>
|
|
||||||
<text class="t m c" x="876" y="326">site.ts · SiteAdapter</text>
|
|
||||||
<text class="t s" x="876" y="345" font-size="12">selectors, API marks, links</text>
|
|
||||||
<rect x="860" y="366" width="268" height="52" rx="10" class="chip"/>
|
|
||||||
<text class="t m c" x="876" y="388">capabilities/*.json</text>
|
|
||||||
<text class="t s" x="876" y="407" font-size="12">who may call what</text>
|
|
||||||
<path class="ln dash" d="M836 290 H812" marker-end="url(#arr)"/>
|
|
||||||
|
|
||||||
<!-- tooling -->
|
|
||||||
<rect x="836" y="462" width="316" height="254" rx="16" fill="#151022" stroke="#a855f7" stroke-width="1.4"/>
|
|
||||||
<text class="t k" x="860" y="492" style="fill:#d8b4fe">NPM · waybetter</text>
|
|
||||||
<text class="t h" x="860" y="524">The tooling</text>
|
|
||||||
<g class="t m c">
|
|
||||||
<text x="860" y="558">npm create waybetter</text>
|
|
||||||
<text x="860" y="584">waybetter dev --watch</text>
|
|
||||||
<text x="860" y="610">waybetter store build|sign|verify</text>
|
|
||||||
<text x="860" y="636">waybetter strings check</text>
|
|
||||||
<text x="860" y="662">waybetter release</text>
|
|
||||||
<text x="860" y="688">waybetter build-env</text>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 7.4 KiB |
|
|
@ -1,29 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 400" width="1000" height="400" role="img" aria-label="Terminal: npm create waybetter app kickapp creates a wayBetter app in kickapp (32 files) and prints the next steps">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#0d1222"/>
|
|
||||||
<stop offset="1" stop-color="#080a12"/>
|
|
||||||
</linearGradient>
|
|
||||||
<style>
|
|
||||||
.m{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;font-size:15px;fill:#cbd5e1}
|
|
||||||
.p{fill:#22c55e}.d{fill:#7c8aa5}.o{fill:#e6e9f2;font-weight:600}.a{fill:#7dd3fc}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<rect width="1000" height="400" rx="14" fill="url(#bg)" stroke="#24304a"/>
|
|
||||||
<rect width="1000" height="36" rx="14" fill="#121a2e"/>
|
|
||||||
<rect y="22" width="1000" height="14" fill="#121a2e"/>
|
|
||||||
<circle cx="22" cy="18" r="6" fill="#ef4444" opacity=".8"/><circle cx="42" cy="18" r="6" fill="#f59e0b" opacity=".8"/><circle cx="62" cy="18" r="6" fill="#22c55e" opacity=".8"/>
|
|
||||||
<text class="m d" x="500" y="23" text-anchor="middle" font-size="13">npm create waybetter</text>
|
|
||||||
<g class="m">
|
|
||||||
<text x="28" y="76"><tspan class="p">$</tspan> npm create waybetter app kickapp -- --site kick.com --name "wayBetter for Kick" --yes</text>
|
|
||||||
<text x="28" y="120" class="o">Created a wayBetter app in kickapp (32 files).</text>
|
|
||||||
<text x="28" y="160">Next:</text>
|
|
||||||
<text x="52" y="186"><tspan class="a">cd kickapp</tspan></text>
|
|
||||||
<text x="52" y="210"><tspan class="a">git init && git submodule add https://git.crt.fyi/CRT/waybetter.git waybetter</tspan></text>
|
|
||||||
<text x="52" y="234"><tspan class="a">npm install</tspan></text>
|
|
||||||
<text x="52" y="258"><tspan class="a">npm run build</tspan></text>
|
|
||||||
<text x="52" y="282"><tspan class="a">npm run tauri dev</tspan></text>
|
|
||||||
<text x="52" y="306" class="d">then replace the PLACEHOLDER selectors in src/site.ts and src-tauri/src/site.rs</text>
|
|
||||||
<text x="28" y="356"><tspan class="p">$</tspan> <tspan class="d">▍</tspan></text>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2 KiB |
11
assets/readme/term/create-app.txt
Normal file
11
assets/readme/term/create-app.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
$ npm create waybetter app kickapp -- --site kick.com --name "wayBetter for Kick" --yes
|
||||||
|
|
||||||
|
Created a wayBetter app in kickapp (32 files).
|
||||||
|
|
||||||
|
Next:
|
||||||
|
cd kickapp
|
||||||
|
git init && git submodule add https://git.crt.fyi/CRT/waybetter.git waybetter
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
npm run tauri dev
|
||||||
|
then replace the PLACEHOLDER selectors in src/site.ts and src-tauri/src/site.rs
|
||||||
BIN
assets/readme/term/create-app.webp
Normal file
BIN
assets/readme/term/create-app.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
7
assets/readme/term/create-plugin.txt
Normal file
7
assets/readme/term/create-plugin.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
$ npm create waybetter plugin -- --name "Hide Trends"
|
||||||
|
|
||||||
|
Created a wayBetter plugin in hide-trends (2 files).
|
||||||
|
|
||||||
|
Next:
|
||||||
|
cd hide-trends
|
||||||
|
copy hide-trends.plugin.js into wayBetter → Plugins → Open folder, then Rescan
|
||||||
BIN
assets/readme/term/create-plugin.webp
Normal file
BIN
assets/readme/term/create-plugin.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
105
tools/readme-term-shots.mjs
Normal file
105
tools/readme-term-shots.mjs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// Terminal shots for the README: runs real `create-waybetter` commands in a throwaway folder and saves, per shot,
|
||||||
|
// assets/readme/term/<name>.txt the cleaned output (first line "$ <command>", no ANSI, temp paths made short)
|
||||||
|
// assets/readme/term/<name>.webp that text drawn as a terminal (dark), rendered by headless Google Chrome and
|
||||||
|
// encoded with cwebp (the smaller of lossy q80 and lossless)
|
||||||
|
// Fails when a command exits non-zero, its output lacks its expected marker, or Chrome / cwebp hang.
|
||||||
|
// node tools/readme-term-shots.mjs all shots
|
||||||
|
// node tools/readme-term-shots.mjs --text only the .txt files (no browser)
|
||||||
|
// node tools/readme-term-shots.mjs app only these shots
|
||||||
|
// Needs Google Chrome (or CHROME=<path>) and cwebp on PATH for the images.
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const CLI = join(ROOT, 'packages', 'create-waybetter', 'bin', 'create-waybetter.js');
|
||||||
|
const OUT = join(ROOT, 'assets', 'readme', 'term');
|
||||||
|
const CHROME = process.env.CHROME || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||||
|
const TIMEOUT_MS = 60_000;
|
||||||
|
const log = m => console.log(`${new Date().toISOString().slice(11, 19)} [term-shots] ${m}`);
|
||||||
|
|
||||||
|
const SHOTS = [
|
||||||
|
{ name: 'create-app', title: 'npm create waybetter', marker: 'Created a wayBetter app',
|
||||||
|
shown: 'npm create waybetter app kickapp -- --site kick.com --name "wayBetter for Kick" --yes',
|
||||||
|
argv: ['app', 'kickapp', '--site', 'kick.com', '--name', 'wayBetter for Kick', '--yes'] },
|
||||||
|
{ name: 'create-plugin', title: 'npm create waybetter', marker: 'hide-trends',
|
||||||
|
shown: 'npm create waybetter plugin -- --name "Hide Trends"',
|
||||||
|
argv: ['plugin', '--name', 'Hide Trends'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const esc = s => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
function lineHtml(l) {
|
||||||
|
if (l.startsWith('$ ')) return `<span class="p">$</span> <span class="cmd">${esc(l.slice(2))}</span>`;
|
||||||
|
let h = esc(l);
|
||||||
|
h = h.replace(/\b(error|ERROR|failed)\b/g, m => `<span class="bad">${m}</span>`);
|
||||||
|
h = h.replace(/^(\s*)(Created|Wrote)\b/, (m, a, b) => `${a}<span class="ok">${b}</span>`);
|
||||||
|
h = h.replace(/^(\s{2,})(cd |npm |git |then )/, (m, a, b) => `${a}<span class="cmd">${b}</span>`);
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
const page = (title, lines) => `<!doctype html><html><head><meta charset="utf-8"><style>
|
||||||
|
:root { --code-bg: #060a14; --code-ink: #e2e8f0; --code-muted: #64748b; --radius: 14px;
|
||||||
|
--shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 12px 32px -16px rgba(0, 0, 0, .7);
|
||||||
|
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; }
|
||||||
|
html, body { margin: 0; background: #0b1120; }
|
||||||
|
body { padding: 24px; width: 960px; box-sizing: content-box; }
|
||||||
|
.term { background: var(--code-bg); color: var(--code-ink); border-radius: var(--radius); box-shadow: var(--shadow); border: 1px solid color-mix(in srgb, var(--code-ink) 10%, transparent); overflow: hidden; }
|
||||||
|
.term-bar { display: flex; align-items: center; gap: 6px; padding: 10px 14px; border-bottom: 1px solid color-mix(in srgb, var(--code-ink) 10%, transparent); }
|
||||||
|
.term-bar span { width: 10px; height: 10px; border-radius: 50%; background: #334155; }
|
||||||
|
.term-bar em { margin-left: 8px; font: 12px var(--mono); color: var(--code-muted); font-style: normal; }
|
||||||
|
.term pre { margin: 0; padding: 16px 18px 20px; color: var(--code-ink); font: 13.5px/1.75 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||||
|
.p { color: #818cf8; } .cmd { color: #a5b4fc; } .c { color: var(--code-muted); }
|
||||||
|
.bad { color: #fca5a5; font-weight: 600; } .ok { color: #86efac; }
|
||||||
|
</style></head><body><div class="term"><div class="term-bar"><span></span><span></span><span></span><em>${esc(title)}</em></div><pre><code>${lines.map(lineHtml).join('\n')}</code></pre></div></body></html>`;
|
||||||
|
|
||||||
|
function run(cmd, args, opts = {}) {
|
||||||
|
const r = spawnSync(cmd, args, { encoding: 'utf8', timeout: TIMEOUT_MS, ...opts });
|
||||||
|
if (r.error) throw new Error(`${cmd}: ${r.error.message}`);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shoot(shot, work) {
|
||||||
|
const r = run(process.execPath, [CLI, ...shot.argv], { cwd: work, input: '', env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' } });
|
||||||
|
const out = `${r.stdout}${r.stderr}`.replace(/\x1b\[[0-9;]*m/g, '').replaceAll(work + '/', '').replaceAll(work, '.').trimEnd();
|
||||||
|
if (r.status !== 0) throw new Error(`${shot.name}: exit ${r.status}\n${out}`);
|
||||||
|
if (!out.includes(shot.marker)) throw new Error(`${shot.name}: output lacks "${shot.marker}"\n${out}`);
|
||||||
|
return [`$ ${shot.shown}`, ...out.split('\n')];
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(shot, lines) {
|
||||||
|
const tmp = mkdtempSync(join(tmpdir(), 'wb-term-'));
|
||||||
|
try {
|
||||||
|
const html = join(tmp, 'page.html'), png = join(tmp, 'shot.png');
|
||||||
|
writeFileSync(html, page(shot.title, lines));
|
||||||
|
// Height: bar + padding + lines; a little spare, the page background is the frame.
|
||||||
|
const height = Math.ceil(24 * 2 + 38 + 36 + lines.length * 13.5 * 1.75 + 8);
|
||||||
|
run(CHROME, ['--headless=new', '--disable-gpu', '--hide-scrollbars', '--force-device-scale-factor=2', `--window-size=1008,${height}`, `--screenshot=${png}`, `file://${html}`]);
|
||||||
|
if (!existsSync(png)) throw new Error(`${shot.name}: Chrome wrote no screenshot`);
|
||||||
|
const best = ['q80', 'lossless'].map(mode => {
|
||||||
|
const file = join(tmp, `${mode}.webp`);
|
||||||
|
const args = mode === 'q80' ? ['-q', '80', '-m', '6'] : ['-lossless', '-z', '9'];
|
||||||
|
const c = run('cwebp', ['-quiet', ...args, '-resize', '1600', '0', png, '-o', file]);
|
||||||
|
if (c.status !== 0) throw new Error(`cwebp ${mode}: ${c.stderr}`);
|
||||||
|
return { file, bytes: statSync(file).size };
|
||||||
|
}).reduce((a, b) => (b.bytes < a.bytes ? b : a));
|
||||||
|
writeFileSync(join(OUT, `${shot.name}.webp`), readFileSync(best.file));
|
||||||
|
return best.bytes;
|
||||||
|
} finally { rmSync(tmp, { recursive: true, force: true }); }
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const textOnly = args.includes('--text');
|
||||||
|
const only = args.filter(a => !a.startsWith('--'));
|
||||||
|
const shots = only.length ? SHOTS.filter(s => only.includes(s.name)) : SHOTS;
|
||||||
|
if (only.length && shots.length !== only.length) throw new Error(`unknown shot(s); known: ${SHOTS.map(s => s.name).join(', ')}`);
|
||||||
|
mkdirSync(OUT, { recursive: true });
|
||||||
|
const work = mkdtempSync(join(tmpdir(), 'wb-readme-'));
|
||||||
|
try {
|
||||||
|
for (const shot of shots) {
|
||||||
|
const lines = shoot(shot, work);
|
||||||
|
writeFileSync(join(OUT, `${shot.name}.txt`), lines.join('\n') + '\n');
|
||||||
|
log(`${shot.name}: ${lines.length} lines${textOnly ? '' : `, ${Math.round(render(shot, lines) / 1024)} KB webp`}`);
|
||||||
|
}
|
||||||
|
} finally { rmSync(work, { recursive: true, force: true }); }
|
||||||
Loading…
Add table
Add a link
Reference in a new issue