dnsglobe is a Rust TUI (ratatui + crossterm + hickory-resolver) that watches a DNS record propagate across public resolvers worldwide. This file describes the process for building a new feature; it applies to any coding agent or human contributor.
| File | Owns |
|---|---|
src/main.rs |
CLI (clap), event loop, key handling, --once mode |
src/app.rs |
App state, input editing, answer grouping, TTL verdicts, table column layout |
src/ui.rs |
All rendering: map, table, gauge, footer |
src/theme.rs |
UI color roles, Paint/Muted parsing |
src/globe.rs |
Orthographic globe projection and panel geometry |
src/world_data.rs |
Coastline vector data |
src/dns.rs |
Resolver queries and error mapping |
src/resolvers.rs |
Built-in resolver list |
src/config.rs |
TOML config file (resolvers, theme, ecs, view) |
src/sites.rs |
Anycast POP discovery (NSID, id.server fallback, IATA codes) |
Keep logic in app.rs (testable, no I/O) and rendering in ui.rs. Key
bindings live in handle_key in main.rs and should call one-line App
methods rather than mutating state inline.
-
Branch off
main. Direct pushes tomainare blocked by an org ruleset — all changes land via PR. -
Implement, with state and rendering separated. New behavior goes in
Appmethods;ui.rsonly reads state. Comments explain why (protocol quirks, terminal compatibility, DNS semantics), not what the code does — match the density and tone of the surrounding code. -
Unit-test the logic. Tests live in
#[cfg(test)] mod testsat the bottom of the same file. Anything inapp.rsshould be testable without a terminal or network. -
Run the quality gates locally — CI enforces exactly these on every PR:
cargo fmt --check cargo clippy --all-targets -- -D warnings cargo test -
Verify end-to-end, not just with tests.
- Query/output logic:
cargo run -- example.com --onceprints a plain table without needing a TTY. - TUI behavior (key bindings, rendering, watch mode): drive the real
binary through a PTY, e.g. with
expector tmux, and assert on the rendered output. Three PTY gotchas: anexpect-spawned PTY starts with zero size (fix withstty rows 30 columns 110 < $spawn_out(slave,name)after spawn); Tcl's\xescape eats all following hex digits — writeEscas\033, never\x1bfollowed by a hex-looking char; and Tcl'ssleepstops reading the PTY, so a continuously-redrawing app fills the ~16KB kernel buffer and blocks mid-write, freezing frames — wait withexpect -timeout N "ZZZ_never_matches"instead, which keeps draining output.
- Query/output logic:
-
Keep the UI discoverable. A new key binding must appear in the footer hint line in
draw_footer(src/ui.rs). Mind macOS/Windows/Linux differences: Option on macOS arrives as ALT (or asEsc b/Esc ffrom Terminal.app); Cmd (SUPER) only arrives under the kitty keyboard protocol, so always provide a universal fallback (Home/End, Ctrl+letter). -
Update
CHANGELOG.md. Add an entry under[Unreleased]in the matching Keep-a-Changelog section (Added/Changed/Fixed), written for users, ending with a link to the PR. Exception: fixing a bug that was itself introduced in[Unreleased]gets no entry — no released version ever had the bug, so there is nothing for users to note. If the fix changes behavior that an existing[Unreleased]entry describes, amend that entry instead. -
Refresh the demo if visuals changed. The README GIF is recorded with vhs from
demo/demo.tape. -
Open a PR against
main. Describe what changed, how it works, and how it was verified (including the end-to-end check from step 5). CI must pass before merge.
Releases are cut with a version-bump PR:
- Bump
versioninCargo.tomland runcargo checksoCargo.lockpicks up the new version. - In
CHANGELOG.md, rename[Unreleased]to the new version with today's date and add a fresh empty[Unreleased]heading above it. - Open a PR with just those changes and merge it.
On merge, tag-release.yml sees the new (untagged) version on main and
dispatches the dist-generated release.yml, which builds binaries, publishes
to crates.io (publish-crates.yml, needs the CARGO_REGISTRY_TOKEN secret)
and the Homebrew tap, and creates the v* tag and GitHub release. A release
can also be re-run or cut manually from the Actions tab by dispatching
release.yml with the tag.
Feature PRs never touch versioning — just leave the changelog entry under
[Unreleased].