Working notes for AI coding agents (Claude Code, Cursor, Pi, etc.) contributing to Ono. Read this before touching code.
A framework-agnostic library of beautiful terminal UI components. Think shadcn / Aceternity, but for TUIs.
Two usage modes, both first-class:
- Library (default):
cargo add ono,use ono::components::splash::Splash;. Themeable Ratatui widgets driven by typed builders. - Eject (power path):
ono add <name>copies the component's source into the user's tree. Use when the typed params aren't enough and you want to rewrite rendering. Ejected code has no runtime dependency onono.
Status: v0.1.0 is the first public release (2026-04-25). Repo is open source (MIT); plan/ stays gitignored cause it's for personal planning.
Internal planning — goals, roadmap, sprint tasks, aesthetic decisions, theming rules — lives under plan/. That directory is gitignored (public contributors don't see it, but agents working on this repo do). If anything below conflicts with a file in plan/, the file in plan/ wins.
When in doubt about what to build next, or why a decision was made, read plan/. Start with plan/ono-plan.md.
Two top-level concerns in the shipped crate (ono/src/):
- Components — what the user copies. Two kinds:
- Elements (
ono/src/elements/) — atomic:box,progress,spinner,percentage,sparkline,typewriter. Cannot compose other components. - Components (
ono/src/components/) — composite:splash,boot,dashboard,statusbar,map. Compose elements and/or other components.
- Elements (
- Themes (
ono/src/theme/) —Themeenum +Palette(9 semantic roles) +Knobs(animation/behavior). Components refer to palette roles by name, never hex.
Composition rule: components compose elements and other components. Elements are atomic. Prevents cycles.
Spec files (ono/specs/) are the source of truth for each component's params, defaults, composition, and class→role mapping. Consumed by the engine for preview and, later, by codegen. They live inside the ono crate so they ship in the published binary (embedded at compile time).
ono/
├── Cargo.toml workspace; rust-version = 1.85
├── ono/ the shipped crate (CLI + engine + component source)
│ ├── Cargo.toml features: theme-retro, theme-minimal, theme-cyber, all-themes
│ └── src/
│ ├── main.rs CLI entry (list, add, preview)
│ ├── theme/ Theme + Palette + Knobs; forest always-built, others feature-gated
│ ├── elements/ atomic components (Ratatui source)
│ ├── components/ composite components (Ratatui source)
│ ├── spec/ TOML parse + composition resolver
│ └── cli/ subcommand implementations
│ └── specs/ TOML source of truth (embedded at build time)
│ ├── elements/*.toml
│ └── components/*.toml
├── experiments/ prototype crate — scratchpad for new work before it graduates into ono/
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs original Theme primitives (graduate into ono/src/theme/)
│ └── bin/ one binary per prototype
├── examples/
│ └── ratatui-demo/ example project consuming several components
├── docs/ Astro Starlight site (narrative docs + showcase, deploys to ono.nullorder.dev)
├── plan/ internal planning (gitignored) — follow along there
├── .cargo/config.toml `cargo experiments <name>` alias
├── justfile dev recipes
├── README.md public
├── LICENSE MIT, NullOrder
└── AGENTS.md this file
The experiments/ crate stays in the repo as a scratchpad — new explorations land there before graduating into ono/. Don't delete it. Don't refactor it into permanent infrastructure either.
just experiment <name> # run a prototype in forest (default)
just theme <name> <theme> # run in forest|retro|minimal|cyber
just experiments # list available
just all-themes # build with every theme enabled
just check # cargo check --workspace --features all-themes
just format # cargo fmt --all
just rustdoc [PORT] # build + serve the ono crate's rustdoc (was `just docs`)
cargo experiments <name> # raw alias, same as `just experiment`Always just check (or build with --features all-themes) before declaring a change done — the default build only compiles forest, so theme-gated regressions can hide.
The narrative docs + showcase site lives at docs/ and deploys to ono.nullorder.dev. Use Node 24 and pnpm — both are pinned and required. Node version is set by docs/.nvmrc (24); the packageManager field in docs/package.json pins pnpm. Do not use npm or yarn against this directory — pnpm-lock.yaml is the only committed lockfile.
nvm use # picks up docs/.nvmrc → Node 24
just docs-install # pnpm install in docs/
just docs # dev server (hot reload)
just docs-build # production build → docs/dist/
just docs-preview # serve the production build
just docs-clean # rm -rf docs/dist docs/.astroBumping Astro / Starlight / sharp is allowed; bumping the pinned Node major or swapping package managers needs an explicit reason in the commit message.
These are non-negotiable. Reviewers will reject code that violates them.
- No comments unless the WHY is non-obvious. Don't explain WHAT the code does — names already do that. Don't reference the current task or fix.
- No emojis in code or files unless explicitly requested.
- No backwards-compat shims for things you're removing. Just delete.
- No new abstractions beyond what the task requires. Three similar lines beats a premature trait.
- No hardcoded hex colors in component code. Always go through
theme.palette().<role>. Hardcoded hex in theme palette definitions is fine (that's where palettes live); anywhere else is a bug. - No branching on theme identity for visual logic.
if theme == Theme::Forestis a code smell. Branch on knobs or palette roles:if theme.knobs().scanline { ... }. Branching on theme for non-visual structural choices (e.g., border type) is tolerable but prefer knobs when possible. - No
unwrap()on user-reachable code paths. OK inmainsetup and prototype glue; not OK in render loops. - Match Rust idioms. snake_case for fns/vars, PascalCase for types, SCREAMING for consts.
cargo fmtbefore committing. - Concrete types over generics when only one concrete type is used. Example:
Terminal<CrosstermBackend<io::Stdout>>notTerminal<B: Backend>. - Component source must work identically in library mode and eject mode. Use relative
super::super::theme::...imports so the same.rsfile compiles both in theonocrate (library users) and under a vendoredsrc/ono/tree (eject users). Do notuse ono::...insideono/src/components/orono/src/elements/— that would break the eject path. Library users get the components viause ono::components::...externally; that's fine.
Details in plan/theming.md. Quick reference:
- Ono ships multiple themes. Forest is the default and is always built. Retro, Minimal, and Cyber are feature-gated (
theme-retro,theme-minimal,theme-cyber, aggregateall-themes) so users only pay for what they use. All four are first-class. - Adding a theme requires: a new variant in
Theme, aPaletteconstant, aKnobsconstant, agradient()arm, aname()arm — all behind#[cfg(feature = "theme-<name>")]. - Every theme must fill all 9 Palette roles and all Knob fields. No
Option<Color>. No per-component fallbacks. - Components must not branch on theme identity for visual logic. Branch on knob fields or palette roles. A component that only looks right under one theme is leaking palette assumptions.
ono addemitstheme.rsinto the user's project with concrete hex for the chosen theme. First add writes it; subsequent adds reuse.
Details in plan/aesthetic-decision.md. Quick reference:
- Palette: only the 9 canonical roles. No new hues without updating
Palettefor every theme. - Animation: slow and breathing. No bounce/elastic easing. No scanline on forest.
- Two O's in "ono" = eyes. Keep them visually prominent in hero pieces. Subtle async pulse.
- Tagline "beautiful terminal UI components" pairs with the wordmark until the brand is established.
- Don't announce a release until it ships. Repo can be public earlier; marketing waits for working code.
- Ejected code must not import from
ono. Component source imports only the target framework and the user's owntheme.rs. Library users get the same files viause ono::components::...— that's the library path and it's fine. Don't conflate the two. - Don't let generated code look generated. When codegen lands, template output must read as idiomatic. Stop and fix templates if it doesn't.
- Don't force cross-target universality. Today every component targets Ratatui; if/when other targets land, some components will be target-specific. Document divergence honestly rather than faking a lowest-common-denominator abstraction.
A new experiment binary:
- Create
experiments/src/bin/<name>.rs. - Use
Theme::parse_from_args()to read--theme. - Pull colors from
theme.palette(), behavior fromtheme.knobs(). - Conform to forest for default appearance; verify retro + minimal + cyber don't crash with
just all-themes. - No new shared abstractions in
lib.rsunless every existing experiment needs them.
A new element or component in the real crate:
- Draft the spec:
ono/specs/elements/<name>.tomlorono/specs/components/<name>.toml. - Hand-write the Ratatui source under
ono/src/elements/orono/src/components/. - Register in the component catalog.
- Ensure it uses palette roles (not hex) and relative
super::super::themeimports (so the same file compiles both under theonocrate and under an ejectedsrc/ono/tree — nouse ono::...inside the component file). - Verify
ono listshows it;ono preview <name>renders it;ono add <name>copies a clean file.
A new theme (rare — see plan/theming.md first):
- Add
theme-<name>to the relevantCargo.toml[features]. - Add the
Themevariant +Paletteconst +Knobsconst + match arms, all#[cfg]-gated. - Add
SPINNER_<NAME>if needed. - Update
plan/theming.mdwith the rationale.
A new dev recipe: add to justfile. Keep names lowercase, hyphenated. Brief comment line above each.
just checkpasses (withall-themes).cargo build --workspace --releasepasses (default features).- For UI changes: actually run the binary in a real terminal. Type-check passes ≠ visual correctness. If you can't run it (no TTY), say so explicitly — don't claim success.
- For new themes: run every experiment/component under that theme. None should panic.
- Don't create
*.mdplanning docs outsideplan/without asking. - Don't add dependencies without checking if the existing ones cover the need.
- Don't introduce
clapto experiments (std::env::args()is enough). The real CLI inono/can use clap. - Don't add
tokioor async runtimes. Rendering is sync. - Don't run
cargo updatecasually — it churnsCargo.lock. Update deliberately, in its own commit. - Don't push to
mainofgithub.com/nullorder/onowithout explicit human ask. - Don't post anything publicly on the project's behalf. Release posts go through Ani.
Re-read plan/ono-plan.md. It's the brief. Most "should I…?" questions are answered there.