edamame 0.1.5

A fast TUI Markdown editor and viewer
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Command-line entry points

> Part of the edamame contributor deep-dives. Index and project-wide conventions: [`AGENTS.md`](../../AGENTS.md). Sibling docs live in [`docs/dev/`](.).


- **`main` is a dispatcher over `cli::Invocation`, and the parser is hand-rolled on purpose.** `clap` is deliberately absent from the graph (hence `mermaid-rs-renderer` declared `default-features = false` — its `cli` feature would drag clap in), so a new flag is a match arm in `cli::args`, not a new dependency. Arguments are taken as `OsString`: `std::env::args()` *panics* on a non-UTF-8 argument, which is a legal Linux file name, so flags are matched only after a successful `to_str()` and anything else falls through to the positional arm intact.
- **`--doctor` never re-derives capability text.** The five capability rows come from `ui::cap_summary::CapSummary::from_caps`, the same builder the welcome modal and capabilities notice use; a second phrasing would drift. `cli::doctor` adds only the *system* half (OS, terminal, `TERM`, `COLORTERM`, locale, tmux) and the `Status::Unknown` state. Every row describes the *machine*, never the person — the report is written to be pasted into a public issue tracker, which is why the config directory is absent (a username in the common case, carrying only "this path is or isn't the default"). A new row owes the same test.
- **That third state exists because the probe needs a tty.** `Capabilities::detect` writes escape sequences and reads replies off the terminal, so `edamame --doctor > report.txt` would pollute the file and report "no image support" for a terminal that has it. `doctor::run` checks `IsTerminal` on stdout *and* stdin, falls back to `Capabilities::env_only`, and marks exactly the two probe-derived rows — Images and Keyboard — unknown.
- **System facts are file reads, never subprocesses.** `/etc/os-release` on Linux, `SystemVersion.plist` on macOS (a string scan), `$TERM_PROGRAM_VERSION` for the terminal version; Windows reports the bare OS name. Spawning `sw_vers` / `lsb_release` would add a process spawn to an area [security-invariants.md](security-invariants.md) hardens; every lookup degrades to a coarser answer instead of an error.
- **`--no-config` is enforced at every read and write site, through one gate.** Skipping the load is the easy half; the trap is a mid-session *write* overwriting the user's real config with compiled defaults. `config::persistence` owns one process-global `AtomicBool` that `main` clears via `disable_config_dir()` before any config file is touched, exposed as `config_writes_allowed()` and `config_reads_allowed()` — two names for one fact, so a reader isn't guarded by a function with "writes" in its name.
  - **Every write site asks.** The four mid-session ones: `Config::save` (returns `Ok(())` without writing — nothing went wrong); the keybinds overlay's `try_persist` (whose `save_to` *truncates* `keybindings.toml` from in-memory overrides a `--no-config` session never read, so one rebind would wipe the real file); the export-theme modal (refused outright — a custom theme *is* its file, so a suppressed write would leave `set_theme` naming an unresolvable theme); and `App::open_config_in_editor` (refused: it both seeds `config.toml` and reloads from it). `Config::load`'s own writes (the theme-fallback persist and the legacy-key migration) and `State::save` ([machine-state.md](machine-state.md)) gate too. `ensure_default_files` is the sole exception, and only because `main` never calls it under the flag.
  - **The read half needs the same treatment**, because the config dir is read again mid-session by surfaces enumerating what the user dropped in. Ungated, `--no-config` would still list — and on selection load — `themes/*.toml` in the theme picker, the settings overlay's theme cycle, and the export-theme source list. Three sites ask `config_reads_allowed()`: `theme::list_theme_names` (which all three theme surfaces build from), `readers::list_export_stylesheets`, and `readers::read_theme_named` as defense in depth — it substitutes the capability-appropriate built-in and returns `None` for the fallback name, since there is no rename to persist and nothing is *missing* (a `MissingTheme` modal every launch would be noise). A new reader or writer owes the matching check.
  - **The gate is a global because a `Config` field was not enough.** As a `#[serde(skip)]` field it silently reverted to its default when `App::open_config_in_editor` reloaded and assigned config wholesale. There is deliberately no way to re-enable it. Messages reporting a settings change append `config::unpersisted_suffix()` (`NOT_PERSISTED_NOTE`, one const so all three flashes phrase it identically) so "Configuration updated" never claims a write that didn't happen.