diff --git a/.gitignore b/.gitignore index db4c6d9..b186605 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ dist -node_modules \ No newline at end of file +node_modules +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e90152..98eaed6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +- [2026-07-17] [the readme is the landing page: real captures only](https://github.com/RubricLab/tokenmaxx/commit/0db404df5a3d7ac4fa7fadcc8c4d7d1a74139ae4) +- [2026-07-17] [drop the asset factory: fixture renders, remotion, brand generator](https://github.com/RubricLab/tokenmaxx/commit/3de71c1a13402ff8abb24bb111cdca42a25d5267) - [2026-07-17] [routing becomes a plain tokenmaxx on/off in settings The accounts title is quiet when on (just the auto policy) and shows '✗ off' diff --git a/README.md b/README.md index d290b4c..73ee4d4 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,16 @@
- - - - tokenmaxx - +TOKENMAXX -

+
-**Juggle rate limits across all your Codex and Claude Code accounts — and see every token you burn.** +**Juggle rate limits across all your Codex and Claude Code accounts. See every token you burn.** -macOS · [Bun](https://bun.sh) · [tokenmaxx.sh](https://tokenmaxx.sh) · a [Rubric Labs](https://rubriclabs.com) project +macOS · [Bun](https://bun.sh) · a [Rubric Labs](https://rubriclabs.com) project · not affiliated with OpenAI or Anthropic -
+

- - - - tokenmaxx beside live Claude Code and Codex sessions - +a day of juggling: meters fill, accounts max out, the active dot hops to fresh ones
@@ -29,95 +21,103 @@ bun add -g tokenmaxx ``` ```bash -tokenmaxx login codex # sign in (isolated; existing sessions untouched) +tokenmaxx login codex # sign in; your existing sessions stay untouched tokenmaxx login claude -tokenmaxx install # route native codex & claude through tokenmaxx +tokenmaxx install # route native codex & claude through tokenmaxx -codex # use the clients as before — tokenmaxx injects the account -claude +codex # use your clients as before +claude # tokenmaxx injects the account, per request ``` ## What it does -You have more than one Codex and Claude subscription, and you keep hitting the five-hour or -weekly limit on whichever you're using. tokenmaxx runs a loopback proxy on `127.0.0.1:8459` -that reads the **active account per request** and injects its credential — so switching takes -effect on the very next request, even mid-turn, with the clients running unmodified. -Credentials stay in the macOS Keychain; nothing but opaque references touches disk. - -Both providers report live rate-limit state on every response, and the proxy reads it as -traffic streams by — so tokenmaxx always knows exactly how full the active account is, with -zero extra requests. Anything speaking the Anthropic or OpenAI Responses API can point at -`http://127.0.0.1:8459/anthropic` or `/openai` and get the same juggling — the proxy doesn't -care which harness is on the other end. - -## Dashboard - -Run `tokenmaxx` for a live dashboard. **Accounts** shows every account and its rate-limit -windows, colored by pressure; press space to expand one. **Analytics** is a combined -token-throughput view (tokens/time across every account and both providers) with total -tokens, the **≈ API-list-price value** you're extracting from your flat subscriptions, a -per-model breakdown, and the input / output / cache-read / cache-write split. Tokens are -metered by the proxy as responses stream by — every number is cross-checkable against the -clients' own session logs — and it never buffers or delays them. **Settings** tunes -auto-rotation, the switch threshold, and dwell per provider, applied live. +You run a fleet of coding agents, and they burn through the five-hour or +weekly limit on whichever subscription you're using:
- - - - token throughput, API value, and the per-model split - +a desktop full of parallel agent sessions burning tokens +
+ +tokenmaxx runs a loopback proxy that reads the active account on every request +and injects its credential, so a switch lands on the very next request, even +mid-turn, with your clients running unmodified. Credentials stay in the macOS +Keychain; nothing but opaque references touches disk. + +## The dashboard + +Run `tokenmaxx`. **Accounts** shows every account and its live rate-limit +windows, colored by pressure, with plan tier and reset countdowns inline. Rows +sort by pressure; the ● marks where traffic is going right now. + +
+every account and its live rate-limit windows, colored by pressure +
+ +**Analytics** is combined token throughput across all accounts and both +providers, with the ≈ API-list-price value you're pulling from your flat +subscriptions. Tokens are metered as responses stream by, never buffered, so +every number is cross-checkable against your clients' own session logs. Press +`m` for the full pricing breakdown per model. + +
+combined token throughput and API-list-price value across all accounts +
+ +**Settings** holds the master on/off per provider, then auto-rotation, the +switch threshold, and cooldown, applied live. + +
+routing, auto-rotation, threshold, and cooldown, tuned per provider
## Auto-rotation -Turn it on and tokenmaxx moves off an account the moment its fullest window crosses your -threshold, onto the one with the most headroom — mid-turn, on the next request. Pressure is -read off every response, so a burst from a fleet of parallel agents can't outrun the switch; -if an account still hits its hard limit mid-flight, the proxy rotates and retries that request -on the next account before your client ever sees the 429. It's off by default; enabling it is +Turn it on and tokenmaxx steps off an account the moment its fullest window +crosses your threshold, onto the one with the most headroom. If an account +still hits its hard limit mid-flight, the proxy rotates and retries that +request before your client ever sees the 429. Off by default; enabling it is your confirmation that your provider permits this use. ```bash tokenmaxx auto both on --threshold 90 # or: codex | claude … off ``` -
- - - - an at-limit account rotating to a fresh one - -
+## How it works + +A single loopback proxy on `127.0.0.1:8459`, and the clients you already use. + +- **Account per request.** The proxy reads which account is active for each + request and injects its credential. Switching lands on the next request. +- **Pressure read for free.** Both providers report rate-limit state on every + response; the proxy reads it as traffic streams by, so it always knows how + full the active account is, with zero extra requests. +- **Harness-agnostic.** Anything speaking the Anthropic or OpenAI Responses API + can point at `/anthropic` or `/openai` and get the same juggling. ## Commands -| Command | | -|---|---| -| `tokenmaxx` | Live dashboard | -| `tokenmaxx login ` | Sign in (idempotent) | -| `tokenmaxx install` · `uninstall` | Route native clients · restore config | -| `tokenmaxx switch ` | Make an account active | -| `tokenmaxx auto [--threshold N]` | Auto-rotation | -| `tokenmaxx list` · `status` · `refresh` · `doctor` | Accounts · JSON · re-probe · checks | +```text +tokenmaxx live dashboard +tokenmaxx login sign in; isolated, idempotent +tokenmaxx install route native codex & claude +tokenmaxx uninstall restore native config +tokenmaxx switch make an account active +tokenmaxx auto [--threshold N] +tokenmaxx list | status | refresh | doctor +``` Env: `TOKENMAXX_HOME`, `TOKENMAXX_PROXY_PORT`, `TOKENMAXX_THEME`. ## Not affiliated -tokenmaxx is an independent [Rubric Labs](https://rubriclabs.com) project. It is **not** an -official product of, affiliated with, or endorsed by OpenAI or Anthropic. Use only accounts -you own, and only where the relevant terms permit account automation. - -## Credits - -Inspired by [`codex-account-switcher`](https://github.com/Sls0n/codex-account-switcher) by -[Sls0n](https://github.com/Sls0n) — thanks for the spark. +An independent [Rubric Labs](https://rubriclabs.com) project, not an official +product of, affiliated with, or endorsed by OpenAI or Anthropic. Use only +accounts you own, and only where the relevant terms permit account automation. +Inspired by [codex-account-switcher](https://github.com/Sls0n/codex-account-switcher).

-Rubric Labs +Rubric Labs
Built by Rubric Labs · go nuts
diff --git a/assets/.gitignore b/assets/.gitignore deleted file mode 100644 index 6b86746..0000000 --- a/assets/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# Intermediates — regenerated from assets.config.ts + the fixture seam. -.tmp/ -tapes/ - -# generated/ IS committed: the README embeds those images, so GitHub needs them -# in the repo. Regenerate any time with `bun run assets`. diff --git a/assets/README.md b/assets/README.md deleted file mode 100644 index 97a3404..0000000 --- a/assets/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# tokenmaxx assets - -Every image and video in the docs is generated from source — no hand-editing. -The real tokenmaxx TUI is rendered against **synthetic fixtures** with a **pinned -clock**, so frames are deterministic and need no daemon, network, or real -accounts. - -## How it works - -``` -src/tui/fixtures.ts fabricates schema-valid AnalyticsSnapshots (scenarios) - │ - ▼ -tokenmaxx --fixture the real TUI renders that snapshot (hidden seam, - │ pinned by TOKENMAXX_NOW / TOKENMAXX_THEME / TZ) - ▼ -assets/assets.config.ts the shot list: scenario × view × theme × size - │ - ▼ -assets/render.ts drives VHS to frame each shot (macOS window chrome) - ▼ -assets/generated/*.png committed build products the README embeds -``` - -## Regenerate - -```bash -bun run assets # all screenshots, dark + light -bun run assets:screens accounts analytics # only matching shots -``` - -Requirements (macOS): - -```bash -brew install vhs gifski -brew install --cask font-jetbrains-mono-nerd-font # braille + block + symbol glyphs -``` - -## Iterating - -- **Change what a shot shows** → edit the scenario in `../src/tui/fixtures.ts` - (or add a new one) and/or the shot's `keys`/`height` in `assets.config.ts`. -- **Add a shot** → append to `SHOTS` in `assets.config.ts`, re-run. -- **Preview one live** without rendering: - - ```bash - TOKENMAXX_FIXTURE=oneHot TOKENMAXX_THEME=dark TZ=America/Los_Angeles \ - bun run src/index.ts # press ←/→, space, 1–5 to explore - ``` - -## Layout - -| Path | Role | -|---|---| -| `assets.config.ts` | shot list + render constants (source of truth) | -| `render.ts` | VHS driver (screenshots) | -| `generated/` | committed PNGs the README embeds + `manifest.json` | -| `tapes/`, `.tmp/` | regenerated intermediates (gitignored) | - -Scenarios live in `../src/tui/fixtures.ts`: `cruising`, `oneHot`, `rotated`, -`onboarding`. diff --git a/assets/assets.config.ts b/assets/assets.config.ts deleted file mode 100644 index 20add58..0000000 --- a/assets/assets.config.ts +++ /dev/null @@ -1,93 +0,0 @@ -// The single source of truth for every generated screenshot. Each Shot renders -// the real tokenmaxx TUI through the hidden fixture seam (src/tui/fixtures.ts) at a -// pinned clock, then frames it with VHS (macOS window chrome + shadowless dark -// backdrop). Edit this list — never the images — and re-run `bun run -// assets:screens`. - -export type ThemeName = 'dark' | 'light' -export const THEMES: readonly ThemeName[] = ['dark', 'light'] - -// Rendering constants shared by every frame. JetBrainsMono Nerd Font is required -// (brew install --cask font-jetbrains-mono-nerd-font) — it carries the braille, -// block, and symbol glyphs the TUI draws. LetterSpacing 0 keeps text tight. -export const FONT = 'JetBrainsMono Nerd Font' -export const FONT_SIZE = 15 -export const WIDTH = 1200 - -// The desktop the framed window sits on: near-black for dark, soft grey for -// light, so the window edge always reads. -export const MARGIN_FILL: Record = { - dark: '#0b0d10', - light: '#e6ebf2' -} - -// Pins the header clock. FIXTURE_NOW (see src/tui/fixtures.ts) reads as a clean -// 9:42 AM in Pacific, so every frame shows the same time. -export const FIXTURE_TZ = 'America/Los_Angeles' - -export interface Shot { - // Output basename: generated/-.png - name: string - // Fixture scenario from src/tui/fixtures.ts. - scenario: string - // Canvas height in pixels (width is fixed). Tune per view so the content fills - // the frame without a tall empty gap. - height: number - // Keys sent after first paint to reach a view (e.g. ["Right"] → Analytics, - // ["Space"] → expand the selected account). Each key waits for a repaint. - keys?: string[] - // false shows the "native routing is off" banner (onboarding). - installed?: boolean - // One-line caption for the docs manifest; not rendered into the frame. - caption: string -} - -export const SHOTS: readonly Shot[] = [ - { - caption: 'Every account and its live rate-limit windows, at a glance.', - height: 560, - name: 'accounts', - scenario: 'cruising' - }, - { - caption: 'The active Codex account is nearly out — auto-rotate is about to fire.', - height: 560, - name: 'hot', - scenario: 'oneHot' - }, - { - caption: 'Press space: plan tier, per-window reset countdowns, account id.', - height: 720, - keys: ['Space'], - name: 'expanded', - scenario: 'cruising' - }, - { - caption: 'Combined token throughput across all accounts and both providers.', - height: 720, - keys: ['Tab'], - name: 'analytics', - scenario: 'cruising' - }, - { - caption: 'Total tokens and ≈ API value over the timeframe.', - height: 720, - keys: ['Tab', 'Right'], - name: 'analytics-week', - scenario: 'oneHot' - }, - { - caption: 'Auto-rotation, threshold, and dwell — tuned per provider, applied live.', - height: 560, - keys: ['Tab', 'Tab'], - name: 'settings', - scenario: 'cruising' - }, - { - caption: 'A fresh install: sign in, then route native codex & claude.', - height: 470, - installed: false, - name: 'onboarding', - scenario: 'onboarding' - } -] diff --git a/assets/brand/logo-dark.svg b/assets/brand/logo-dark.svg deleted file mode 100644 index 26a41b4..0000000 --- a/assets/brand/logo-dark.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - tokenmaxx - rate-limit control plane · Codex + Claude Code - - - - - - diff --git a/assets/brand/logo-light.svg b/assets/brand/logo-light.svg deleted file mode 100644 index f51d724..0000000 --- a/assets/brand/logo-light.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - tokenmaxx - rate-limit control plane · Codex + Claude Code - - - - - - diff --git a/assets/brand/rubric-mark.svg b/assets/brand/rubric-mark.svg deleted file mode 100644 index 5023d0b..0000000 --- a/assets/brand/rubric-mark.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/flagship.ts b/assets/flagship.ts deleted file mode 100644 index 0525e74..0000000 --- a/assets/flagship.ts +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env bun -// Renders the flagship composite (dark + light): the real tokenmaxx dashboard beside -// mock Claude Code and Codex sessions, arranged in a tmux layout and framed by -// VHS. The layout is built by flagship/build.sh; the sessions are the static -// mock-claude.sh / mock-codex.sh (modelled on the real TUIs). Deterministic. -// -// bun run assets/flagship.ts - -import { spawnSync } from 'node:child_process' -import { existsSync, mkdirSync, writeFileSync } from 'node:fs' -import { join } from 'node:path' -import { FIXTURE_NOW } from '../src/tui/fixtures.ts' -import { FONT, FONT_SIZE, MARGIN_FILL, THEMES, type ThemeName } from './assets.config.ts' - -const assetsDir = import.meta.dir -const outDir = join(assetsDir, 'generated') -const tapeDir = join(assetsDir, 'tapes') -const tmpDir = join(assetsDir, '.tmp') -for (const dir of [outDir, tapeDir, tmpDir]) { - mkdirSync(dir, { recursive: true }) -} - -const vhs = existsSync('/opt/homebrew/bin/vhs') ? '/opt/homebrew/bin/vhs' : 'vhs' -const buildScript = join(assetsDir, 'flagship', 'build.sh') - -// The whole terminal background must match the tokenmaxx theme, otherwise the mock -// panes (which don't paint their own background) show VHS's default and their -// theme-coloured text vanishes. Single-pane shots don't need this — tokenmaxx fills -// the frame — but the flagship's mock panes do. -const TERM_BG: Record = { dark: '#0b0d10', light: '#fbfcfe' } -const TERM_FG: Record = { dark: '#e6e6e6', light: '#1c2430' } - -function tape(theme: ThemeName, png: string, gif: string): string { - return [ - `Output "${gif}"`, - `Set Theme '{ "background": "${TERM_BG[theme]}", "foreground": "${TERM_FG[theme]}" }'`, - `Set FontFamily "${FONT}"`, - `Set FontSize ${FONT_SIZE - 1}`, - 'Set LetterSpacing 0', - 'Set Width 1560', - 'Set Height 860', - 'Set Padding 22', - 'Set Margin 46', - `Set MarginFill "${MARGIN_FILL[theme]}"`, - 'Set WindowBar Colorful', - 'Set WindowBarSize 40', - 'Set BorderRadius 12', - 'Set TypingSpeed 1ms', - `Env TOKENMAXX_NOW "${FIXTURE_NOW}"`, - `Type "THEME=${theme} bash ${buildScript}"`, - 'Enter', - 'Sleep 9s', - `Screenshot "${png}"`, - '' - ].join('\n') -} - -let ok = 0 -const failed: string[] = [] -for (const theme of THEMES) { - const base = `flagship-${theme}` - const png = join(outDir, `${base}.png`) - const gif = join(tmpDir, `${base}.gif`) - const tapePath = join(tapeDir, `${base}.tape`) - writeFileSync(tapePath, tape(theme, png, gif)) - spawnSync('rm', ['-f', png]) - let landed = false - for (let attempt = 1; attempt <= 4 && !landed; attempt += 1) { - spawnSync(vhs, [tapePath], { stdio: 'ignore' }) - // build.sh runs tmux inside VHS's own shell, but kill any stray session too. - spawnSync('tmux', ['kill-session', '-t', 'fs'], { stdio: 'ignore' }) - landed = existsSync(png) - if (!landed) { - Bun.sleepSync(1500) - } - } - if (landed) { - ok += 1 - console.log(` ok ${base}.png`) - } else { - failed.push(base) - console.log(` FAIL ${base}.png`) - } -} - -console.log( - `\n${ok} rendered, ${failed.length} failed${failed.length ? `: ${failed.join(', ')}` : ''}` -) -process.exitCode = failed.length > 0 ? 1 : 0 diff --git a/assets/flagship/build.sh b/assets/flagship/build.sh deleted file mode 100755 index 67f9946..0000000 --- a/assets/flagship/build.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# Composes the flagship layout inside the current terminal and attaches: -# left = the real tokenmaxx dashboard (fixture, no daemon) -# right-top = a mock Claude Code session -# right-bottom= a mock Codex session -# All three panes are created before their commands run, so each renders at its -# final size (the tokenmaxx fixture does not repaint on resize). -set -u -theme="${THEME:-dark}" -now="${TOKENMAXX_NOW:-1784133720000}" -dir="$(cd "$(dirname "$0")" && pwd)" -repo="$(cd "$dir/../.." && pwd)" -bun="$(command -v bun)" -cols=$(tput cols) -rows=$(tput lines) -[ "$theme" = "light" ] && border="fg=#c7cedb" || border="fg=#2a3038" - -tmux kill-session -t fs 2>/dev/null || true -tmux new-session -d -s fs -x "$cols" -y "$rows" -tmux set -t fs status off \; set -t fs pane-border-style "$border" \; \ - set -t fs pane-active-border-style "$border" - -# Right column at 46% (tokenmaxx keeps the wider left), then split it in half. -tmux split-window -h -t fs:0.0 -l 46% -tmux split-window -v -t fs:0.1 -l 52% - -tmux send-keys -t fs:0.0 \ - "clear; TOKENMAXX_FIXTURE=oneHot TOKENMAXX_THEME=$theme TOKENMAXX_NOW=$now TZ=America/Los_Angeles $bun run $repo/src/index.ts" Enter -tmux send-keys -t fs:0.1 "clear; THEME=$theme bash $dir/mock-claude.sh" Enter -tmux send-keys -t fs:0.2 "clear; THEME=$theme bash $dir/mock-codex.sh" Enter -tmux select-pane -t fs:0.0 -sleep 6 -tmux attach -t fs diff --git a/assets/flagship/mock-claude.sh b/assets/flagship/mock-claude.sh deleted file mode 100755 index 4e628f6..0000000 --- a/assets/flagship/mock-claude.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -# A believable, static Claude Code session for the flagship composite — modelled -# on the real v2.1.210 TUI (rounded header, ❯ prompt, ● assistant marker, diff -# summary, status line). Synthetic content; no real session. THEME=dark|light. -set -u -theme="${THEME:-dark}" -if [ "$theme" = "light" ]; then - fg=$'\e[38;2;28;36;48m'; dim=$'\e[38;2;90;100;114m'; faint=$'\e[38;2;170;178;189m' -else - fg=$'\e[38;2;230;230;230m'; dim=$'\e[38;2;139;147;161m'; faint=$'\e[38;2;75;81;92m' -fi -clay=$'\e[38;2;209;119;87m'; grn=$'\e[38;2;58;208;122m'; red=$'\e[38;2;235;90;104m' -b=$'\e[1m'; r=$'\e[0m' -printf '\e[2J\e[H' -printf '\n' -printf ' %s%sClaude Code%s %s~/code/rubrot · Opus 4.8 · Max 20×%s\n' "$b" "$clay" "$r" "$dim" "$r" -printf '\n' -printf ' %s❯%s %swire the inbox webhook to the bot runner%s\n' "$clay" "$r" "$fg" "$r" -printf '\n' -printf ' %s●%s %sAdded the webhook route and a replay guard.%s\n' "$clay" "$r" "$fg" "$r" -printf ' %s⎿%s %ssrc/inbox/webhook.ts%s %s+34%s %s-2%s\n' "$faint" "$r" "$dim" "$r" "$grn" "$r" "$red" "$r" -printf ' %ssrc/inbox/webhook.test.ts%s %s+51%s\n' "$dim" "$r" "$grn" "$r" -printf '\n' -printf ' %s●%s %sAll 128 tests pass. Deploy to metal?%s\n' "$clay" "$r" "$fg" "$r" -printf '\n' -printf ' %s──────────────────────────────────────────────%s\n' "$faint" "$r" -printf ' %s❯%s %sdo it%s%s▏%s\n' "$clay" "$r" "$fg" "$r" "$dim" "$r" -printf ' %s──────────────────────────────────────────────%s\n' "$faint" "$r" -printf ' %s⏵⏵ accept edits · 2 files · esc to interrupt%s\n' "$dim" "$r" -tput civis 2>/dev/null || true -sleep 3600 diff --git a/assets/flagship/mock-codex.sh b/assets/flagship/mock-codex.sh deleted file mode 100755 index 0d07c32..0000000 --- a/assets/flagship/mock-codex.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# A believable, static Codex session for the flagship composite — modelled on the -# real v0.144.1 TUI (>_ OpenAI Codex banner with model/directory/permissions, • -# notices, › prompt, status line). Synthetic content. THEME=dark|light. -set -u -theme="${THEME:-dark}" -if [ "$theme" = "light" ]; then - fg=$'\e[38;2;28;36;48m'; dim=$'\e[38;2;90;100;114m'; faint=$'\e[38;2;170;178;189m' -else - fg=$'\e[38;2;230;230;230m'; dim=$'\e[38;2;139;147;161m'; faint=$'\e[38;2;75;81;92m' -fi -cyan=$'\e[38;2;120;196;196m'; grn=$'\e[38;2;58;208;122m' -b=$'\e[1m'; r=$'\e[0m' -printf '\e[2J\e[H' -printf '\n' -printf ' %s╭────────────────────────────────────────────╮%s\n' "$faint" "$r" -printf ' %s│%s %s%s>_ OpenAI Codex%s %s(v0.144.1)%s %s│%s\n' "$faint" "$r" "$b" "$cyan" "$r" "$dim" "$r" "$faint" "$r" -printf ' %s│%s %smodel:%s gpt-5.6-sol %s/model%s %s│%s\n' "$faint" "$r" "$dim" "$r" "$dim" "$r" "$faint" "$r" -printf ' %s│%s %sdirectory:%s ~/code/rubrot %s│%s\n' "$faint" "$r" "$dim" "$r" "$faint" "$r" -printf ' %s╰────────────────────────────────────────────╯%s\n' "$faint" "$r" -printf '\n' -printf ' %s› %sextract the selector into a pure function%s\n' "$cyan" "$fg" "$r" -printf '\n' -printf ' %s•%s %sExtracted selectRotation; added 7 cases.%s\n' "$cyan" "$r" "$fg" "$r" -printf ' %s└%s %ssrc/selection.ts%s %s· 74 passing%s\n' "$faint" "$r" "$dim" "$r" "$grn" "$r" -printf '\n' -printf ' %s•%s %sThreshold check is now side-effect free.%s\n' "$cyan" "$r" "$fg" "$r" -printf '\n' -printf ' %s› %s%srun /review on my changes%s\n' "$cyan" "$r" "$dim" "$r" -printf '\n' -printf ' %sgpt-5.6-sol default · ~/code/rubrot%s\n' "$faint" "$r" -tput civis 2>/dev/null || true -sleep 3600 diff --git a/assets/generated/accounts-dark.png b/assets/generated/accounts-dark.png deleted file mode 100644 index 8b0043c..0000000 Binary files a/assets/generated/accounts-dark.png and /dev/null differ diff --git a/assets/generated/accounts-light.png b/assets/generated/accounts-light.png deleted file mode 100644 index 6560f39..0000000 Binary files a/assets/generated/accounts-light.png and /dev/null differ diff --git a/assets/generated/analytics-dark.png b/assets/generated/analytics-dark.png deleted file mode 100644 index 4802011..0000000 Binary files a/assets/generated/analytics-dark.png and /dev/null differ diff --git a/assets/generated/analytics-light.png b/assets/generated/analytics-light.png deleted file mode 100644 index 39f5d99..0000000 Binary files a/assets/generated/analytics-light.png and /dev/null differ diff --git a/assets/generated/analytics-week-dark.png b/assets/generated/analytics-week-dark.png deleted file mode 100644 index a01a4f1..0000000 Binary files a/assets/generated/analytics-week-dark.png and /dev/null differ diff --git a/assets/generated/analytics-week-light.png b/assets/generated/analytics-week-light.png deleted file mode 100644 index 000fcaf..0000000 Binary files a/assets/generated/analytics-week-light.png and /dev/null differ diff --git a/assets/generated/expanded-dark.png b/assets/generated/expanded-dark.png deleted file mode 100644 index 3cafe90..0000000 Binary files a/assets/generated/expanded-dark.png and /dev/null differ diff --git a/assets/generated/expanded-light.png b/assets/generated/expanded-light.png deleted file mode 100644 index 8485191..0000000 Binary files a/assets/generated/expanded-light.png and /dev/null differ diff --git a/assets/generated/flagship-dark.png b/assets/generated/flagship-dark.png deleted file mode 100644 index 4bdde3f..0000000 Binary files a/assets/generated/flagship-dark.png and /dev/null differ diff --git a/assets/generated/flagship-light.png b/assets/generated/flagship-light.png deleted file mode 100644 index 202a430..0000000 Binary files a/assets/generated/flagship-light.png and /dev/null differ diff --git a/assets/generated/hot-dark.png b/assets/generated/hot-dark.png deleted file mode 100644 index 37c2206..0000000 Binary files a/assets/generated/hot-dark.png and /dev/null differ diff --git a/assets/generated/hot-light.png b/assets/generated/hot-light.png deleted file mode 100644 index 48c2f7f..0000000 Binary files a/assets/generated/hot-light.png and /dev/null differ diff --git a/assets/generated/manifest.json b/assets/generated/manifest.json deleted file mode 100644 index 2a0482d..0000000 --- a/assets/generated/manifest.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "caption": "Every account and its live rate-limit windows, at a glance.", - "name": "accounts" - }, - { - "caption": "The active Codex account is nearly out — auto-rotate is about to fire.", - "name": "hot" - }, - { - "caption": "Press space: plan tier, per-window reset countdowns, account id.", - "name": "expanded" - }, - { - "caption": "Combined token throughput across all accounts and both providers.", - "name": "analytics" - }, - { - "caption": "Total tokens and ≈ API value over the timeframe.", - "name": "analytics-week" - }, - { - "caption": "Auto-rotation, threshold, and dwell — tuned per provider, applied live.", - "name": "settings" - }, - { - "caption": "A fresh install: sign in, then route native codex & claude.", - "name": "onboarding" - } -] diff --git a/assets/generated/onboarding-dark.png b/assets/generated/onboarding-dark.png deleted file mode 100644 index 5aa4fd6..0000000 Binary files a/assets/generated/onboarding-dark.png and /dev/null differ diff --git a/assets/generated/onboarding-light.png b/assets/generated/onboarding-light.png deleted file mode 100644 index 6dab7ab..0000000 Binary files a/assets/generated/onboarding-light.png and /dev/null differ diff --git a/assets/generated/settings-dark.png b/assets/generated/settings-dark.png deleted file mode 100644 index 26bf913..0000000 Binary files a/assets/generated/settings-dark.png and /dev/null differ diff --git a/assets/generated/settings-light.png b/assets/generated/settings-light.png deleted file mode 100644 index 267bde4..0000000 Binary files a/assets/generated/settings-light.png and /dev/null differ diff --git a/assets/generated/switch-dark.gif b/assets/generated/switch-dark.gif deleted file mode 100644 index 770b9cb..0000000 Binary files a/assets/generated/switch-dark.gif and /dev/null differ diff --git a/assets/generated/switch-light.gif b/assets/generated/switch-light.gif deleted file mode 100644 index 225f5c0..0000000 Binary files a/assets/generated/switch-light.gif and /dev/null differ diff --git a/assets/generated/timelapse-dark.gif b/assets/generated/timelapse-dark.gif deleted file mode 100644 index 7f27da0..0000000 Binary files a/assets/generated/timelapse-dark.gif and /dev/null differ diff --git a/assets/generated/timelapse-light.gif b/assets/generated/timelapse-light.gif deleted file mode 100644 index 86cbdd6..0000000 Binary files a/assets/generated/timelapse-light.gif and /dev/null differ diff --git a/assets/render.ts b/assets/render.ts deleted file mode 100644 index 5861de5..0000000 --- a/assets/render.ts +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env bun -// Renders every Shot in assets.config.ts to a framed PNG (dark + light) by -// driving the real tokenmaxx TUI through the fixture seam with VHS. Deterministic: -// no daemon, no network, a pinned clock. Re-run any time; images are build -// products under generated/. -// -// bun run assets/render.ts # all shots, both themes -// bun run assets/render.ts accounts # only shots whose name matches an arg - -import { spawnSync } from 'node:child_process' -import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' -import { join } from 'node:path' -import { FIXTURE_NOW } from '../src/tui/fixtures.ts' -import { - FIXTURE_TZ, - FONT, - FONT_SIZE, - MARGIN_FILL, - SHOTS, - type Shot, - THEMES, - type ThemeName, - WIDTH -} from './assets.config.ts' - -const assetsDir = import.meta.dir -const repo = join(assetsDir, '..') -const outDir = join(assetsDir, 'generated') -const tapeDir = join(assetsDir, 'tapes') -const tmpDir = join(assetsDir, '.tmp') -for (const dir of [outDir, tapeDir, tmpDir]) { - mkdirSync(dir, { recursive: true }) -} - -const bun = process.execPath -const vhs = existsSync('/opt/homebrew/bin/vhs') ? '/opt/homebrew/bin/vhs' : 'vhs' -const entry = join(repo, 'src/index.ts') - -function tape(shot: Shot, theme: ThemeName, pngPath: string, gifPath: string): string { - const installed = shot.installed === false ? 'false' : 'true' - const keyLines = (shot.keys ?? []).flatMap(key => [key, 'Sleep 900ms']) - return [ - `Output "${gifPath}"`, - `Set FontFamily "${FONT}"`, - `Set FontSize ${FONT_SIZE}`, - 'Set LetterSpacing 0', - `Set Width ${WIDTH}`, - `Set Height ${shot.height}`, - 'Set Padding 24', - 'Set Margin 50', - `Set MarginFill "${MARGIN_FILL[theme]}"`, - 'Set WindowBar Colorful', - 'Set WindowBarSize 40', - 'Set BorderRadius 12', - 'Set TypingSpeed 1ms', - `Env TOKENMAXX_FIXTURE "${shot.scenario}"`, - `Env TOKENMAXX_THEME "${theme}"`, - `Env TOKENMAXX_NOW "${FIXTURE_NOW}"`, - `Env TOKENMAXX_INSTALLED "${installed}"`, - `Env TZ "${FIXTURE_TZ}"`, - `Type "${bun} run ${entry}"`, - 'Enter', - 'Sleep 6s', - ...keyLines, - `Screenshot "${pngPath}"`, - '' - ].join('\n') -} - -const filter = process.argv.slice(2) -const wanted = (shot: Shot) => filter.length === 0 || filter.some(f => shot.name.includes(f)) - -let ok = 0 -const failed: string[] = [] -for (const theme of THEMES) { - for (const shot of SHOTS) { - if (!wanted(shot)) { - continue - } - const base = `${shot.name}-${theme}` - const png = join(outDir, `${base}.png`) - const gif = join(tmpDir, `${base}.gif`) - const tapePath = join(tapeDir, `${base}.tape`) - writeFileSync(tapePath, tape(shot, theme, png, gif)) - rmSync(png, { force: true }) - // VHS is occasionally flaky when spawned in quick succession (its browser + - // ttyd backend races); verify the frame landed and retry a few times, with a - // short settle between attempts, before giving up. - let landed = false - for (let attempt = 1; attempt <= 4 && !landed; attempt += 1) { - spawnSync(vhs, [tapePath], { stdio: 'ignore' }) - landed = existsSync(png) - if (!landed) { - Bun.sleepSync(1500) - } - } - if (landed) { - ok += 1 - console.log(` ok ${base}.png`) - } else { - failed.push(base) - console.log(` FAIL ${base}.png`) - } - } -} - -writeFileSync( - join(outDir, 'manifest.json'), - `${JSON.stringify( - SHOTS.map(shot => ({ caption: shot.caption, name: shot.name })), - null, - 2 - )}\n` -) - -console.log( - `\n${ok} rendered, ${failed.length} failed${failed.length ? `: ${failed.join(', ')}` : ''}` -) -process.exitCode = failed.length > 0 ? 1 : 0 diff --git a/media/accounts.png b/media/accounts.png new file mode 100644 index 0000000..9340b52 Binary files /dev/null and b/media/accounts.png differ diff --git a/media/analytics.png b/media/analytics.png new file mode 100644 index 0000000..3e9ee6c Binary files /dev/null and b/media/analytics.png differ diff --git a/media/fleet.gif b/media/fleet.gif new file mode 100644 index 0000000..c39e899 Binary files /dev/null and b/media/fleet.gif differ diff --git a/media/relay.gif b/media/relay.gif new file mode 100644 index 0000000..99961c6 Binary files /dev/null and b/media/relay.gif differ diff --git a/media/rubric-mark.svg b/media/rubric-mark.svg new file mode 100644 index 0000000..0eb721f --- /dev/null +++ b/media/rubric-mark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/media/settings.png b/media/settings.png new file mode 100644 index 0000000..37d9777 Binary files /dev/null and b/media/settings.png differ diff --git a/media/wordmark.png b/media/wordmark.png new file mode 100644 index 0000000..176c04a Binary files /dev/null and b/media/wordmark.png differ diff --git a/package.json b/package.json index 0897cd8..041d843 100644 --- a/package.json +++ b/package.json @@ -46,10 +46,6 @@ "url": "https://github.com/RubricLab/tokenmaxx.git" }, "scripts": { - "assets": "bun run assets:screens && bun run assets:flagship && bun run assets:site", - "assets:flagship": "bun run assets/flagship.ts", - "assets:screens": "bun run assets/render.ts", - "assets:site": "mkdir -p site/img && cp assets/brand/wordmark-dark.png assets/brand/rubric-mark.svg assets/generated/flagship-dark.png assets/generated/accounts-dark.png assets/generated/analytics-dark.png assets/generated/metrics-dark.png assets/generated/settings-dark.png assets/generated/switch-dark.gif site/img/", "bleed": "bun x npm-check-updates -u", "build": "bun run clean && bun build ./src/index.ts --target bun --outdir dist --external '@opentui/*' && cp bin/launcher.cjs dist/launcher.cjs && chmod 755 dist/index.js dist/launcher.cjs", "check": "bun run typecheck && bun x biome check . && bun test", @@ -63,5 +59,5 @@ "post-commit": "bun x @rubriclab/package post-commit" }, "type": "module", - "version": "0.0.32" + "version": "0.0.34" } diff --git a/remotion/.gitignore b/remotion/.gitignore deleted file mode 100644 index f9e6631..0000000 --- a/remotion/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules/ -out/ -package-lock.json diff --git a/remotion/package.json b/remotion/package.json deleted file mode 100644 index 5dfe51e..0000000 --- a/remotion/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "tokenmaxx-remotion", - "private": true, - "version": "0.0.0", - "description": "Data-driven timelapse + switch-interaction videos for tokenmaxx docs", - "scripts": { - "render": "bun run render.ts", - "studio": "remotion studio src/index.ts" - }, - "dependencies": { - "@remotion/cli": "4.0.286", - "remotion": "4.0.286", - "react": "19.0.0", - "react-dom": "19.0.0" - }, - "devDependencies": { - "@types/react": "19.0.0" - } -} diff --git a/remotion/render.ts b/remotion/render.ts deleted file mode 100644 index c370063..0000000 --- a/remotion/render.ts +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env node -// Renders each composition in both themes to an mp4, then converts to a -// high-quality gif (ffmpeg two-pass palette) into ../assets/generated/. -// -// cd remotion && node render.ts # (or: npm run render) - -const { spawnSync } = require("node:child_process"); -const { mkdirSync } = require("node:fs"); -const { join } = require("node:path"); - -const here = __dirname; -const outDir = join(here, "out"); -const gifDir = join(here, "..", "assets", "generated"); -mkdirSync(outDir, { recursive: true }); -mkdirSync(gifDir, { recursive: true }); - -const ffmpeg = "/opt/homebrew/bin/ffmpeg"; -const GIF_WIDTH = 960; -const GIF_FPS = 24; - -// [composition id, output gif basename] -const comps = [ - ["UsageTimelapse", "timelapse"], - ["SwitchRotation", "switch"], -]; -const themes = ["dark", "light"]; - -function run(cmd, args, cwd) { - const r = spawnSync(cmd, args, { cwd: cwd || here, stdio: "inherit" }); - if (r.status !== 0) { - throw new Error(`${cmd} exited ${r.status}`); - } -} - -for (const [id, base] of comps) { - for (const theme of themes) { - const mp4 = join(outDir, `${base}-${theme}.mp4`); - console.log(`\n▶ ${id} (${theme}) → ${mp4}`); - run("npx", [ - "remotion", - "render", - "src/index.ts", - id, - mp4, - `--props={"themeName":"${theme}"}`, - "--log=error", - "--overwrite", - ]); - - const gif = join(gifDir, `${base}-${theme}.gif`); - const palette = join(outDir, `${base}-${theme}-palette.png`); - const filters = `fps=${GIF_FPS},scale=${GIF_WIDTH}:-1:flags=lanczos`; - console.log(` → gif ${gif}`); - run(ffmpeg, ["-y", "-i", mp4, "-vf", `${filters},palettegen=stats_mode=diff`, palette]); - run(ffmpeg, [ - "-y", - "-i", - mp4, - "-i", - palette, - "-lavfi", - `${filters}[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=3`, - gif, - ]); - } -} - -console.log("\n✓ videos + gifs written to assets/generated/"); diff --git a/remotion/src/Frame.tsx b/remotion/src/Frame.tsx deleted file mode 100644 index 45c586b..0000000 --- a/remotion/src/Frame.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import type React from "react"; -import { AbsoluteFill } from "remotion"; -import { MONO, type Theme } from "./theme"; - -const Dot: React.FC<{ color: string }> = ({ color }) => ( -
-); - -// A macOS-style window on a desktop surface, matching the screenshots' chrome so -// the videos read as the same product. -export const Frame: React.FC<{ - theme: Theme; - title?: string; - children: React.ReactNode; -}> = ({ theme, title, children }) => ( - -
-
- - - - {title ? ( - {title} - ) : null} -
-
{children}
-
-
-); - -// The tokenmaxx wordmark, used as a small brand mark in the corner of each video. -export const Wordmark: React.FC<{ theme: Theme; size?: number }> = ({ theme, size = 20 }) => ( - - tokenmaxx - -); diff --git a/remotion/src/Root.tsx b/remotion/src/Root.tsx deleted file mode 100644 index 43883ba..0000000 --- a/remotion/src/Root.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Composition } from "remotion"; -import { SwitchRotation } from "./SwitchRotation"; -import { UsageTimelapse } from "./UsageTimelapse"; - -// Both comps take a themeName prop so dark + light render from one definition -// (see render.ts, which passes --props). -export const RemotionRoot: React.FC = () => ( - <> - - - -); diff --git a/remotion/src/SwitchRotation.tsx b/remotion/src/SwitchRotation.tsx deleted file mode 100644 index 05add1d..0000000 --- a/remotion/src/SwitchRotation.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; -import { Frame, Wordmark } from "./Frame"; -import { MONO, pressure, type Theme, type ThemeName, themes } from "./theme"; - -const ROW_H = 68; -const SWITCH = 102; // frame the rotation commits - -const Meter: React.FC<{ theme: Theme; percent: number; width: number }> = ({ - theme, - percent, - width, -}) => { - const color = pressure(theme, percent); - return ( -
-
-
-
- {Math.round(percent)}% -
- ); -}; - -const Row: React.FC<{ - theme: Theme; - email: string; - active: boolean; - percent: number; -}> = ({ theme, email, active, percent }) => ( -
- - {active ? "●" : "○"} - - - {email} - - 5h - -
-); - -export const SwitchRotation: React.FC<{ themeName: ThemeName }> = ({ themeName }) => { - const theme = themes[themeName]; - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - // dexter climbs to the limit, then rotation hands off to the cool standby. - const dexterPct = interpolate(frame, [0, SWITCH], [72, 97], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const shipPct = 21; - const switched = frame >= SWITCH; - - // The active highlight slides from row 0 to row 1 on the switch. - const slide = spring({ frame: frame - SWITCH, fps, config: { damping: 16, mass: 0.7 } }); - const highlightTop = interpolate(slide, [0, 1], [0, ROW_H]); - - const rotating = interpolate(frame, [SWITCH - 8, SWITCH, SWITCH + 26, SWITCH + 40], [0, 1, 1, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const done = interpolate(frame, [SWITCH + 26, SWITCH + 44], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const generation = switched ? 8 : 7; - - return ( - -
-
- OpenAI · Codex - - ⟳ auto-rotate 95% · gen {generation} - -
- -
- -
-
-
- - -
-
- -
-
- ⟳ dexter@ hit 96% on its 5-hour window — rotating… -
-
0 ? -28 : 0 }}> - ✓ switched to ship@rubriclabs.com · the next request uses it, mid-turn · gen 8 -
-
-
- - ); -}; diff --git a/remotion/src/UsageTimelapse.tsx b/remotion/src/UsageTimelapse.tsx deleted file mode 100644 index 39198c8..0000000 --- a/remotion/src/UsageTimelapse.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { interpolate, useCurrentFrame, useVideoConfig } from "remotion"; -import { Frame, Wordmark } from "./Frame"; -import { MONO, pressure, type ThemeName, themes } from "./theme"; - -// A synthetic 5-hour-sawtooth-over-24h usage series — the same shape the TUI -// charts, generated deterministically here so the video needs no runtime data. -function series(n: number): number[] { - const out: number[] = []; - for (let i = 0; i < n; i += 1) { - const t = i / (n - 1); - const cyclePos = (t * 4.8) % 1; // ~4.8 five-hour cycles across the day - const ramp = Math.min(1, cyclePos / 0.9); - const peak = 62 + 30 * t; // the day trends busier - const wobble = 3 * Math.sin(i * 1.7); - out.push(Math.max(0, Math.min(100, peak * ramp + wobble))); - } - return out; -} - -const N = 220; -const DATA = series(N); - -const CHART = { w: 1040, h: 300, x: 0, y: 0 }; - -function path(values: number[], upto: number): string { - const pts = values.slice(0, Math.max(2, upto)); - return pts - .map((v, i) => { - const x = (i / (N - 1)) * CHART.w; - const y = CHART.h - (v / 100) * CHART.h; - return `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`; - }) - .join(" "); -} - -const Pill: React.FC<{ label: string; active: boolean; theme: (typeof themes)[ThemeName] }> = ({ - label, - active, - theme, -}) => ( - - {label} - -); - -export const UsageTimelapse: React.FC<{ themeName: ThemeName }> = ({ themeName }) => { - const theme = themes[themeName]; - const frame = useCurrentFrame(); - const { durationInFrames } = useVideoConfig(); - - // Ease the reveal, hold full at the end. - const reveal = interpolate(frame, [8, durationInFrames - 20], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const frontier = Math.max(1, Math.floor(reveal * (N - 1))); - const now = DATA[frontier] ?? 0; - const shown = DATA.slice(0, frontier + 1); - const peak = Math.max(...shown); - const avg = shown.reduce((s, v) => s + v, 0) / shown.length; - const color = pressure(theme, now); - const frontierX = (frontier / (N - 1)) * CHART.w; - const frontierY = CHART.h - (now / 100) * CHART.h; - - return ( - -
-
- - Codex · dexter@rubriclabs.com ·{" "} - Pro - -
- {["1h", "5h", "24h", "7d", "31d"].map((l) => ( - - ))} -
- -
- - 100 - - - 0 - - - usage over 24 hours - {[0.25, 0.5, 0.75].map((g) => ( - - ))} - - - - - - - {shown.length > 2 ? ( - <> - - - - - - ) : null} - -
- -
-
- now - {Math.round(now)}% - · peak {Math.round(peak)}% · avg {Math.round(avg)}% -
- -
-
- - ); -}; diff --git a/remotion/src/index.ts b/remotion/src/index.ts deleted file mode 100644 index f31c790..0000000 --- a/remotion/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { registerRoot } from "remotion"; -import { RemotionRoot } from "./Root"; - -registerRoot(RemotionRoot); diff --git a/remotion/src/theme.ts b/remotion/src/theme.ts deleted file mode 100644 index cc590e1..0000000 --- a/remotion/src/theme.ts +++ /dev/null @@ -1,63 +0,0 @@ -// The tokenmaxx palette, mirrored from src/tui/format.ts so the videos read as the -// same product as the screenshots. - -export interface Theme { - fg: string; - dim: string; - faint: string; - accent: string; - bg: string; - panel: string; - selected: string; - border: string; - good: string; - warn: string; - bad: string; - desktop: string; // the surface the window sits on -} - -export const dark: Theme = { - fg: "#e6e6e6", - dim: "#8b93a1", - faint: "#4b515c", - accent: "#5ab0ff", - bg: "#0b0d10", - panel: "#0f1216", - selected: "#1b2330", - border: "#2a3038", - good: "#3ad07a", - warn: "#f0a83a", - bad: "#ff5f6e", - desktop: "#05070a", -}; - -export const light: Theme = { - fg: "#1c2430", - dim: "#5a6472", - faint: "#aab2bd", - accent: "#0b62d6", - bg: "#fbfcfe", - panel: "#f2f4f8", - selected: "#e3e9f2", - border: "#c7cedb", - good: "#1f9d57", - warn: "#b9770f", - bad: "#d23b48", - desktop: "#dfe4ec", -}; - -export const themes = { dark, light } as const; -export type ThemeName = keyof typeof themes; - -// Usage-pressure colour, matching pressureColor() in the TUI. -export function pressure(theme: Theme, percent: number): string { - if (percent >= 85) { - return theme.bad; - } - if (percent >= 60) { - return theme.warn; - } - return theme.good; -} - -export const MONO = "ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, monospace";