diff --git a/README.md b/README.md index e72179a..163f9f8 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,12 @@ GPL-3.0-or-later · npm create waybetter

String packs · wayBetter for X

+

npm +license: GPL-3.0-or-later +runtime dependencies: 0 +Tauri 2 +platforms: macOS, Windows, Linux

+ 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 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 -

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

- 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. @@ -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 ``` -

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

+![npm create waybetter app: 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](assets/readme/term/create-app.webp) 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 plugin: a Hide Trends plugin file and how to install it](assets/readme/term/create-plugin.webp) + 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 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); `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/ARCHITECTURE.md](docs/ARCHITECTURE.md): every module of the host, runtime and shell diff --git a/assets/readme/architecture.svg b/assets/readme/architecture.svg deleted file mode 100644 index 9d64aef..0000000 --- a/assets/readme/architecture.svg +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - — ARCHITECTURE - One window, three layers, one small adapter. - - - - - The website you already use - loaded live from its exact https origin, never copied: x.com, kick.com, … - - - - TAURI 2 WINDOW · SYSTEM WEBVIEW - - - - Shell - packages/shell · local, trusted - - Tabs, split panes, address bar - Command palette, accounts, decks - Settings panel, theme builder - Store and the install screen - Workspaces, backup, updates - - the only webview allowed to change anything - - - - Site tabs - one webview + profile per tab · packages/runtime - - Plugin API - Theme engine - String packs - DOM hub - Fetch / XHR hub - Link routing - Accounts - UI kit - Deck columns - - untrusted: may only report about itself - - - - - - - NATIVE HOST · RUST - waybetter-host - crates/waybetter-host - - Every command re-checks its caller (webview label, exact origin) - Profiles, settings, plugins, themes, fonts, workspaces - Signed Store (Ed25519), backup, encrypted sync, signed updates - - - - all commands - a short, reviewed list - - - - YOUR APP - The adapter - what only your site knows - - site.rs · SiteSpec - hosts, account rules - - site.ts · SiteAdapter - selectors, API marks, links - - capabilities/*.json - who may call what - - - - - NPM · waybetter - The tooling - - npm create waybetter - waybetter dev --watch - waybetter store build|sign|verify - waybetter strings check - waybetter release - waybetter build-env - - diff --git a/assets/readme/create-terminal.svg b/assets/readme/create-terminal.svg deleted file mode 100644 index 1878c88..0000000 --- a/assets/readme/create-terminal.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - npm create waybetter - - $ 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 - $ ▍ - - diff --git a/assets/readme/term/create-app.txt b/assets/readme/term/create-app.txt new file mode 100644 index 0000000..6468a2d --- /dev/null +++ b/assets/readme/term/create-app.txt @@ -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 diff --git a/assets/readme/term/create-app.webp b/assets/readme/term/create-app.webp new file mode 100644 index 0000000..1d5e274 Binary files /dev/null and b/assets/readme/term/create-app.webp differ diff --git a/assets/readme/term/create-plugin.txt b/assets/readme/term/create-plugin.txt new file mode 100644 index 0000000..8de0d1d --- /dev/null +++ b/assets/readme/term/create-plugin.txt @@ -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 diff --git a/assets/readme/term/create-plugin.webp b/assets/readme/term/create-plugin.webp new file mode 100644 index 0000000..bb5bacd Binary files /dev/null and b/assets/readme/term/create-plugin.webp differ diff --git a/tools/readme-term-shots.mjs b/tools/readme-term-shots.mjs new file mode 100644 index 0000000..6a4ee43 --- /dev/null +++ b/tools/readme-term-shots.mjs @@ -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/.txt the cleaned output (first line "$ ", no ANSI, temp paths made short) +// assets/readme/term/.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=) 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, '>'); +function lineHtml(l) { + if (l.startsWith('$ ')) return `$ ${esc(l.slice(2))}`; + let h = esc(l); + h = h.replace(/\b(error|ERROR|failed)\b/g, m => `${m}`); + h = h.replace(/^(\s*)(Created|Wrote)\b/, (m, a, b) => `${a}${b}`); + h = h.replace(/^(\s{2,})(cd |npm |git |then )/, (m, a, b) => `${a}${b}`); + return h; +} +const page = (title, lines) => `
${esc(title)}
${lines.map(lineHtml).join('\n')}
`; + +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 }); }