audium 2.0.0

A terminal music app
# Contributing to audium

Issues and pull requests are welcome. Please open an issue before starting work
on a large change.

## Before you push

```sh
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo test
```

These are what CI runs, so a green local run is a green CI run.

audium tracks the latest stable Rust, so new language features can be used the
day they stabilise. `rust-toolchain.toml` pins the channel to `stable` and
rustup fetches it for you; keep yours current with `rustup update stable`
before blaming a build failure on the code.

`rust-version` in `Cargo.toml` is the stable of the day, not a support floor.
It is what makes `cargo install` fail with a readable message rather than a
syntax error on a feature the user's compiler lacks, and it **must be raised to
the new stable when the channel moves**: treat it as part of cutting a release.
Nothing enforces this, so a stale value quietly stops protecting anyone.

The lint set lives in `Cargo.toml` rather than in CI flags, so a plain
`cargo build` fails the same way the pipeline does. It is strict on purpose:
warnings denied, `unsafe_code` forbidden, and clippy with `pedantic`, `nursery`
and a hand-picked selection of `restriction`. Do not silence a lint to land a
change. An `#[allow]` needs a comment saying why the lint is wrong *here*, and
changing the lint set belongs in its own PR rather than inside a feature.

## Text

`src/` is ASCII, including comments. The one exception is the table of glyphs
audium draws on screen; every entry there has an ASCII counterpart for
terminals that cannot render it.

Documentation is ASCII too. `LICENSE` is the sole exception: it reproduces the
GPL-3.0-or-later text verbatim, typographic quotes included, and must not be
retyped.

Nowhere in the repository uses an en or em dash. Write `--`, or reach for a
colon or parentheses instead.

Comments explain *why*, not *what*: the reason a reader would otherwise have to
reconstruct, or the alternative that was tried and rejected. Every module opens
with a `//!` header and every public item carries a `///`. A comment that
restates the code is worse than none, because it is one edit away from lying.

## Rust

**Nothing panics.** `unwrap`, `expect`, `panic!`, `unreachable!`, `todo!` and
`unimplemented!` are denied: releases abort on panic, and a panic behind a TUI
leaves the terminal in raw mode with the message scrolled off screen. Fallible
work returns `anyhow::Result` with `.with_context(|| ...)` naming the operation
that failed. Everything else uses the total API (`unwrap_or`, `map_or`, `get`,
`saturating_*`, `min`).

**Bad input is a fallback, not an error.** Tags, config and hand-edited files
come from users and are often wrong. An unrecognised value falls back to a
default and the app keeps going.

**Never print.** The TUI owns the terminal, so `println!`, `eprintln!` and
`dbg!` corrupt the frame and are denied. What the user must see goes in a
dialog.

**Casts are audited in one place.** Lossy or truncating conversions live in a
single module, each documenting why it is sound for the inputs it is given.
Scattering `#[allow(clippy::cast_*)]` across call sites turns an audited
decision into an unreviewed one.

**Arithmetic on sizes saturates.** Terminal dimensions are user-controlled: a
window dragged down to three columns must render badly, not underflow.

**Take the narrow type.** Prefer `&str` and `&[T]` in parameters, return owned
values only where ownership really transfers, and derive traits rather than
hand-writing them. Clone when it is genuinely simpler, not to escape a borrow
that wanted restructuring.

**Match our own enums exhaustively.** A catch-all `_ =>` arm on a type defined
in this crate silently swallows the next variant someone adds; list them and
let the compiler point at every site that needs updating.

**Name the constant.** A literal that appears twice, or that a reader would
have to measure to understand, is a `const` with a doc comment.

**Keep logic out of I/O and rendering.** Functions that take data and return
data are the only part of a TUI that is testable at all, so push decisions into
them and keep the edges thin.

**One hasher project-wide.** Use `rustc_hash::FxHashMap` / `FxHashSet`, never
the `std::collections` equivalents. Every key here is small and locally
generated, so SipHash's DoS resistance buys nothing and costs speed, and a
single choice means nobody has to wonder why two exist.

## Data on disk

**The tags are the record; the index is a cache.** Metadata lives in the audio
files' own tags, so a library survives audium being deleted and opens in any
other player. The index only records which files exist and what the playlists
hold: never add a field to it that could have been a tag.

**Identity is the filename.** That is what makes the index hand-editable and
portable between machines. Nothing may key off an absolute path or an inode.

**Every write is atomic:** write a temporary file, then rename it over the
target. Truncate-then-write loses the file outright if the process dies
mid-write. Name scratch files so a leftover one cannot be picked up as real
content.

**Nothing is destroyed to make room for a new format.** Files audium writes
carry a `version`. One it cannot read is renamed aside and rebuilt from
scratch, never migrated in place and never deleted. Serde defaults on every
field let an older file load with the new keys filled in and the old ones
untouched.

## Layout

A module with children is `foo.rs` alongside a `foo/` directory; there are no
`mod.rs` files, and clippy enforces it. Shared behaviour gets a small module
rather than a copy: list navigation, numeric casts and the modal helpers are
each defined once, which is why the same keys work in every list and every
dialog has the same margins. A new list or dialog goes through them too.

## UI conventions

**Never hardcode a color or a glyph.** Colors come off the active theme, and
drawing characters off its glyph table, which yields the ASCII set on terminals
that cannot do better. A literal RGB value or box-drawing character looks fine
on the machine it was written on and breaks silently on a tty.

**Modal spacing is centralised, never hand-rolled.** One helper insets every
dialog by a fixed padding on all four sides. A renderer lays out *content
only*: no leading blank line, no trailing `Constraint::Min(0)` standing in for
a bottom margin, no `format!("  {label}")` gutters. This is what keeps the gap
between the border and the first line of text identical in every popup.

It follows that a dialog is sized from its content plus the shared chrome
constant, never from a hardcoded height, which drifts into a lopsided gap the
moment a row is added or removed. Blank rows *between* content groups are still
the renderer's business: the rule governs margins, not internal separators, and
a gap row must be guaranteed rather than left over. Use `Min(1)` when the
content above it is fixed-height and `Length(1)` when it is the flexible
region; `Min(0)` promises nothing and collapses to zero the moment the dialog
is sized correctly.

**Key hints go at the bottom, as `[key] action` pairs**, built and rendered
with the shared helpers rather than hand-written strings. The brackets are what
make the pairing legible: spacing alone cannot distinguish a gap *inside* a
pair from a gap *between* two, and color does not survive a monochrome tty. Ask
the helper for the footer's height *before* sizing the dialog, since it wraps
at whole pairs and can take more than one row.

Both rules cover *every* overlay, not just the ones in `modal.rs`.

**A new keybinding is documented in the commit that adds it.** The help dialog
is the only place bindings are advertised, so one missing from it does not
exist as far as users are concerned.

## Tests

There is deliberately no test suite, and a PR is not expected to add one. Most
of what a test would assert here is already enforced by the compiler and the
lint set, and a test that needs scaffolding is a second thing to maintain that
breaks for reasons of its own.

A test earns its place only by proving something genuinely worth proving, and
only if it is as static as the code it covers: a pure function in, a value out.
No fixtures, no temp directories, no terminal, no timing, no dev-dependency. If
a bug comes down to a small assertion over a pure function, put it in a
`#[cfg(test)]` module in the same file. Otherwise leave it out.

## Commits and pull requests

Keep commits focused: a formatting sweep, a lint change and a behaviour change
are three commits, not one. Say what a user would notice, and name the terminal
you tested in. If it touches rendering, check a bare Linux console too: that is
the path the ASCII glyphs and console themes exist for, and the one most easily
broken without noticing.