# Rustlings Administrator and Developer Guide

This guide is the source-based reference for engineers who maintain
Rustlings, fork it for a language community, ship community exercise sets,
or integrate Rustlings into a curriculum or CI pipeline. It documents the
binary's architecture, the `info.toml` format, the watch-mode internals, the
`rustlings dev` toolchain, the workspace layout, and the contribution path.

## 1. Architecture Overview

Rustlings is a single Rust binary built from the `rustlings` package in a
two-crate workspace (`rustlings` and `rustlings-macros`). It is fully
self-contained — there is no daemon, no server, no shared library — and
delegates all heavy lifting (compile, test, lint) to **`cargo`**, **`rustc`**,
**`cargo-test`**, **`cargo-clippy`**, and **`rustfmt`**, which it invokes as
child processes.

When you `cargo install rustlings` and run `rustlings init`, the resulting
exercises directory is **the** Cargo project. Every `<name>.rs` exercise file
is registered as a binary target in `dev/Cargo.toml`'s `bin = […]` list, so
`cargo build --bin <name>` is the unit of work for one exercise.

```text
                         ┌──────────────────────────┐
   keyboard / file       │     rustlings binary     │
   change events  ─────► │                          │
                         │  ┌───────────────────┐   │
                         │  │  src/main.rs      │   │   parsed by clap
                         │  │  (CLI dispatch)   │   │
                         │  └─────┬─────────────┘   │
                         │        │                 │
                         │   ┌────▼─────────┐       │
                         │   │ AppState     │ ◄── reads .rustlings-state.txt
                         │   │ (state-file) │ ──► writes  .rustlings-state.txt
                         │   └────┬─────────┘       │
                         │        │                 │
                         │   ┌────┴────────────┐    │
                         │   │ exercises[]     │ ◄── EMBEDDED_FILES (rustlings-macros)
                         │   └────┬────────────┘    │
                         │        │                 │
                         │   ┌────┴────────────┐    │
                         │   │ Run / Watch /   │    │
                         │   │ List / Check-all│    │
                         │   └────┬────────────┘    │
                         │        │                 │
                         │   ┌────▼─────────┐       │
                         │   │ CmdRunner    │       │
                         │   │ (cargo / rustc) │    │
                         │   └────┬─────────┘       │
                         └────────┼─────────────────┘
                                  │
                            ┌─────▼──────┐  ┌────────────┐  ┌────────────┐
                            │ cargo build│  │ cargo test │  │ cargo clippy│
                            └────────────┘  └────────────┘  └────────────┘
```

> [!NOTE]
> The exercise files, solution files, the `info.toml` registry, and per-folder
> READMEs are all **embedded into the binary at compile time** by the
> `rustlings_macros::include_files!()` proc-macro. That's why the **end-user
> install flow does not need to clone the repository** — `rustlings init` can
> dump the entire `exercises/` and `solutions/` tree from the binary itself.
> See `rustlings-macros/src/lib.rs` and `src/embedded.rs`.

### 1.1 Component inventory

| Component | Path | Role |
|---|---|---|
| CLI dispatcher | `src/main.rs` | Parse args, route to `init`/`run`/`reset`/`hint`/`check-all`/`dev`/watch. |
| Argument schema | `src/cli.rs` | `clap` `Parser`/`Subcommand` definitions for the public CLI surface. |
| App state machine | `src/app_state.rs` | The `AppState` struct: holds exercises, current index, done count, state file handle, embedded `CmdRunner`, and editor handle. |
| Cargo invoker | `src/cmd.rs` | `CmdRunner` queries `cargo metadata` once at startup, then builds child processes for `cargo build/test/clippy`. |
| Embedded data | `src/embedded.rs` | Wraps `EMBEDDED_FILES` (the proc-macro output) and exposes `init_exercises_dir`, `write_exercise_to_disk`, `write_solution_to_disk`. |
| Exercise model | `src/exercise.rs` | `Exercise` struct, `RunnableExercise` trait. Encapsulates "build → test (if `test=true`) → clippy → run-bin". |
| Info file | `src/info_file.rs` | `ExerciseInfo`/`InfoFile` structs and `InfoFile::parse()`. Supports both embedded and on-disk `info.toml`. |
| Init command | `src/init.rs` | Logic for `rustlings init`. |
| Run command | `src/run.rs` | One-shot `run <name>`. |
| Watch mode | `src/watch.rs`, `src/watch/state.rs`, `src/watch/notify_event.rs`, `src/watch/terminal_event.rs` | The interactive default mode. |
| List view | `src/list.rs`, `src/list/state.rs`, `src/list/scroll_state.rs` | Full-screen scrolling list with search, filter, reset, set-current. |
| Editor | `src/editor.rs`, `src/editor/zellij.rs` | Editor opener (VS Code, custom, Zellij). |
| Terminal helpers | `src/term.rs` | ANSI helpers, progress bar, file-link emitter, length-bounded writer. |
| Cargo.toml updater | `src/cargo_toml.rs` | Parses and rewrites the `bin = […]` block in `dev/Cargo.toml` / `Cargo.toml` to match `info.toml`. |
| Dev toolchain | `src/dev.rs`, `src/dev/check.rs`, `src/dev/new.rs`, `src/dev/update.rs` | `rustlings dev new`, `rustlings dev check`, `rustlings dev update`. |
| Embed macro | `rustlings-macros/src/lib.rs` | `#[proc_macro] pub fn include_files`. Reads `info.toml` at build time, expands to a static `EmbeddedFiles` literal containing `include_bytes!()` calls for every exercise, solution, and README. |
| Build helper | `build.rs` | On Windows only, copies `dev/Cargo.toml` to `dev-Cargo.toml` because Windows can't include the symlink. |

### 1.2 Technology stack

| Layer | Crate / tool | Purpose |
|---|---|---|
| CLI parsing | `clap` 4 (`derive` feature) | Subcommands, args, version flag, doc-comment-derived help. |
| Error context | `anyhow` 1 | `Result`/`Error`/`bail!`/`Context` for human-readable error chains. |
| Terminal | `crossterm` 0.29 | ANSI styling, cursor motion, raw mode, key/mouse events. Default features off; only `windows` + `events` enabled. |
| File watching | `notify` 8 + `RecommendedWatcher` | OS-native file events (inotify/FSEvents/ReadDirectoryChangesW), 1 s poll interval as fallback, follow-symlinks disabled. |
| Embed | `rustlings-macros` (this workspace) | Proc-macro that bakes `info.toml` + every exercise/solution/README into the binary. |
| Serialization | `serde` 1 + `serde_json` 1 + `toml` 1 | Parse `cargo metadata`, parse `info.toml`. |
| Shell-style splitting | `shlex` 1 | Parse the `--edit-cmd "PROGRAM …"` string. |
| Termios (non-Windows) | `rustix` 1.0 (`std`+`stdio`+`termios`, `default-features = false`) | Disable canonical line buffering and echo for the input side of watch mode. |
| Test temp files | `tempfile` 3 (dev-only) | Used by unit tests in `app_state.rs`. |

The release profile sets `panic = "abort"` to keep the binary smaller. The
workspace-wide Clippy lints **forbid** `unsafe_code`, `unstable_features`, and
`empty_loop`, and **deny** `disallowed-types`/`disallowed-methods`,
`infinite_loop`, and `mem_forget`. Custom denials in `clippy.toml` redirect
contributors away from `crossterm::style::Stylize`, `crossterm::style::style`,
`std::thread::spawn`, `std::thread::Scope::spawn`, and `std::process::exit`.

### 1.3 Top-level data flow

The `main` function in `src/main.rs` runs the following sequence on every
invocation:

1. **Parse args** with `clap` (`Args::parse()` returning the `Args` struct).
2. **Block the legacy workflow.** In a release build, if a
   `dev/rustlings-repo.txt` marker file is present in the current directory,
   bail with the "old method before version 6" error.
3. **Priority subcommands.** `Init` and `Dev(...)` short-circuit through their
   handlers in `init::init()` and `DevCommand::run()` respectively, then
   exit `SUCCESS`.
4. **Verify location.** If `exercises/` doesn't exist, print the `PRE_INIT_MSG`
   ASCII banner with usage instructions and exit `FAILURE`.
5. **Parse `info.toml`.** `InfoFile::parse()` first tries to read `info.toml`
   from the current dir (community track), falling back to the embedded
   `info.toml` baked in by `rustlings_macros::include_files!()`.
6. **Reject newer `format_version`.** Bail with `FORMAT_VERSION_HIGHER_ERR` if
   `info_file.format_version > CURRENT_FORMAT_VERSION (1)`.
7. **Detect editor.** `Editor::new(args.edit_cmd, vs_code_term)`. Returns
   `Some(Editor::Cmd(...))` for VS Code or `--edit-cmd`, `Some(Editor::Zellij(_))`
   if `$ZELLIJ` is set and `zellij --version` succeeds, or `None`.
8. **Build `AppState`.** Reads `.rustlings-state.txt` (handles the
   "first run" case where it doesn't exist yet), populates the `Exercise`
   vector with leaked-static `path` and computed `canonical_path`, and
   builds the `CmdRunner` (which calls `cargo metadata` once).
9. **Show welcome message** if the state file did not exist before this run.
10. **Dispatch on subcommand:**
    - `None` → `watch::watch(...)`. Requires a TTY.
    - `Run { name }` → optional set-current-by-name, then `run::run(...)`.
    - `CheckAll` → `app_state.check_all_exercises(...)` and report.
    - `Reset { name }` → `set_current_exercise_by_name`, then
      `reset_current_exercise`.
    - `Hint { name }` → optional set-current, then print the hint.

## 2. System Requirements

| Layer | Minimum | Reason |
|---|---|---|
| Rust | 1.88 | `Cargo.toml` `rust-version = "1.88"`. The `release-hook.sh` also runs `cargo +1.88 dev check --require-solutions` so the MSRV is enforced before every release. |
| Cargo | bundled with Rust 1.88 | Subprocess invocations require modern `cargo metadata` and `cargo clippy`. |
| Clippy | required | Enforced by `init` (`cargo clippy --version`). The Clippy lint set in `[workspace.lints.clippy]` and the per-exercise `[lints.clippy]` in the generated `Cargo.toml` rely on it. |
| Rustfmt 2024 edition | required for solution-shipping community tracks | `dev::check` runs `rustfmt --check --edition 2024` against every solution. |
| `git` | optional, required for community-track resets | `init` runs `git init`; community-track resets run `git stash push -- <path>`. |
| Working terminal | TTY required for watch and list modes | Non-TTY stdout exits with "Unsupported or missing terminal/TTY". |

> [!IMPORTANT]
> The MSRV is enforced via `cargo +1.88 dev check` in the release hook. Any PR
> that pushes the MSRV upward must update `Cargo.toml`'s `rust-version` field
> and the release hook script in lockstep, or the next release will silently
> ship a binary that won't compile on the documented minimum toolchain.

## 3. Configuration Reference

Rustlings has two configuration surfaces: **CLI flags** (driving runtime
behavior) and **`info.toml`** (defining the exercise set itself).

### 3.1 CLI flags (parent `rustlings` command)

| Flag | Type | Default | Description |
|---|---|---|---|
| `--no-editor` | bool | `false` | Disable automatic editor opening; `--edit-cmd` is ignored when set. |
| `--edit-cmd <CMD>` | optional string | none | Command to launch your editor on the current exercise. Parsed with `shlex`. The exercise path is appended as the last argument. The command must not block — use `code`, `subl -n`, etc. — never `vim`. |
| `--manual-run` | bool | `false` | Disable file watching. In watch mode, requires pressing `r` to rerun. |
| `--version` | bool | n/a | Print the version (auto-derived from the workspace `Cargo.toml` via `#[command(version)]`). |
| `--help` | bool | n/a | Print top-level help. Per-subcommand `--help` exists too. |

### 3.2 CLI subcommands

| Subcommand | Args / flags | Description |
|---|---|---|
| `init` | none | Bootstrap a new `rustlings/` project in the current directory. |
| `run [NAME]` | `NAME`: optional exercise name | Run a single exercise once; if `NAME` omitted, the next pending exercise. |
| `check-all` | none | Build/test/clippy/run every exercise in parallel; mark each done or pending. |
| `reset NAME` | `NAME`: required exercise name | Restore a single exercise to its bundled original (or `git stash push` for community tracks). |
| `hint [NAME]` | `NAME`: optional exercise name | Print the curated hint and exit. |
| `dev new PATH [--no-git]` | `PATH`: required, `--no-git`: bool | Scaffold a community-exercises project. |
| `dev check [-r/--require-solutions]` | `--require-solutions`: bool | Validate `info.toml`, `Cargo.toml`, and (optionally) every solution. |
| `dev update` | none | Regenerate the `bin = […]` block from `info.toml`. |

### 3.3 The `info.toml` schema

Every exercise track (the official one and any community fork) is defined by
a single `info.toml` at the project root. Schema:

```toml
# Required. The format version. Currently only `1` is supported.
format_version = 1

# Optional. Shown to users on first run (when no .rustlings-state.txt exists).
welcome_message = """…multi-line text…"""

# Optional. Shown after all exercises are done (above the Ferris ASCII banner).
final_message = """…multi-line text…"""

# Repeat for every exercise.
[[exercises]]
name = "intro1"        # required, [a-zA-Z0-9_]+ , <= 32 chars, unique
dir = "00_intro"       # optional. If set, file is exercises/<dir>/<name>.rs
test = false           # optional, default true. If true, `cargo test` is run.
strict_clippy = false  # optional, default false. If true, clippy errors on any warning.
hint = """…multi-line hint shown via the `h` key or `rustlings hint`…"""
skip_check_unsolved = true  # optional, default false. See below.
```

| Field | Type | Default | Required? | Description |
|---|---|---|---|---|
| `format_version` | u8 | n/a | yes | Must equal `CURRENT_FORMAT_VERSION` (currently `1`). Higher = bail. Lower = bail (in `dev check`). |
| `welcome_message` | string | unset | no | Shown on first run only. |
| `final_message` | string | unset | no | Shown when all exercises are done. |
| `exercises` | array | n/a | yes | At least one entry, max 999. |
| `exercises[].name` | `&'static str` | n/a | yes | Unique. Pattern `[a-zA-Z0-9_]+`. Max 32 chars. Must not be empty. Becomes the Cargo bin name and the `<name>.rs` file stem. |
| `exercises[].dir` | optional string | none | no | If set, file path is `exercises/<dir>/<name>.rs`; otherwise `exercises/<name>.rs`. Same character rules as `name`, must not be empty if present. |
| `exercises[].test` | bool | `true` | no | If true, `cargo test --bin <name>` is run after build. Exercise file MUST contain `#[test]\n` if true; MUST NOT if false (validated by `dev check`). |
| `exercises[].strict_clippy` | bool | `false` | no | If true, `cargo clippy` is run with `-- -D warnings`. Use only for exercises whose entire point is fixing Clippy lints. |
| `exercises[].hint` | string | n/a | yes | Non-empty after trimming ASCII whitespace. Multi-line strings are common. |
| `exercises[].skip_check_unsolved` | bool | `false` | no | If true, `dev check` won't fail this exercise for being already-solved. Use for intro-style exercises that ship pre-solved (e.g. `intro1`). |

> [!IMPORTANT]
> The official `info.toml` lives at `rustlings-macros/info.toml` and is
> embedded at compile time. Editing it requires recompiling the `rustlings`
> binary — installed copies will not see your edits. For runtime-editable
> tracks, use `rustlings dev new` to scaffold a project with a local
> `info.toml`, which `InfoFile::parse()` prefers over the embedded copy.

### 3.4 The state file format

`.rustlings-state.txt` is a simple line-oriented text file maintained by
`AppState`:

```text
DON'T EDIT THIS FILE!

intro2

intro1
variables1
variables2
```

- Line 1: `STATE_FILE_HEADER` (`DON'T EDIT THIS FILE!`).
- Line 2: blank.
- Line 3: name of the **current** exercise (always present, even with no done
  exercises).
- Line 4: blank.
- Lines 5+: names of done exercises, one per line.

Writes are atomic from the program's perspective: `rewind()` → `set_len(0)` →
`write_all()` against the same opened `File` handle. Concurrent writers are
not supported (Rustlings assumes a single process per project).

### 3.5 The generated project layout (after `rustlings init`)

```text
<dir>/
  .gitignore                      ← excludes Cargo.lock, target/, .vscode/ (except extensions.json)
  .rustlings-state.txt            ← created on first run
  .vscode/extensions.json         ← {"recommendations":["rust-lang.rust-analyzer"]}
  Cargo.toml                      ← bin = [...] generated from info.toml; pinned edition 2024
  rust-analyzer.toml              ← check.command = "clippy"; check.extraArgs = ["--profile","test"]; cargo.targetDir = true
  exercises/
    README.md
    00_intro/
      README.md
      intro1.rs
      intro2.rs
    01_variables/
      …
    …
  solutions/
    README.md
    00_intro/intro1.rs            ← placeholder, filled when exercise passes
    01_variables/variables1.rs
    …
```

If `init` runs inside a Cargo workspace, the layout is the same except `Cargo.toml`
is added as a workspace member (`workspace.members += ["rustlings"]`) and
no `git init` is run.

### 3.6 The `Cargo.toml` `bin = […]` contract

The generated `Cargo.toml` (or `dev/Cargo.toml` in the Rustlings dev tree) has
one `[[bin]]` entry per exercise in `info.toml`, **in the same order**. In a
community track that ships solutions, additional `_sol` binaries also appear:

```toml
bin = [
  { name = "intro1", path = "exercises/00_intro/intro1.rs" },
  { name = "intro1_sol", path = "solutions/00_intro/intro1.rs" },
  { name = "intro2", path = "exercises/00_intro/intro2.rs" },
  ...
]
```

`src/cargo_toml.rs::bins_start_end_ind` finds the literal `bin = [` and the
matching `]`; `append_bins` regenerates the contents. `dev check` diffs the
existing block against the regenerated one and fails if they differ; `dev
update` rewrites the file in place. **Never hand-edit `bin = […]`** — use
`rustlings dev update`.

### 3.7 Build-time configuration

| File | Purpose |
|---|---|
| `Cargo.toml` (workspace root) | Workspace members (the `rustlings` package and `rustlings-macros`). Excludes `tests/test_exercises` and `dev` from the workspace. |
| `rustlings-macros/Cargo.toml` | `[lib] proc-macro = true`. Must include `info.toml` in `package.include` to embed it in the published crate. |
| `clippy.toml` | Workspace-wide Clippy denials for inefficient or footgun-y APIs (Stylize, `std::thread::spawn`, `std::process::exit`). |
| `.rumdl.toml` | Disables MD013 (line length) and MD057 (table separator). Per-file ignores for the website's markdown. |
| `.typos.toml` | Excludes `CHANGELOG.md`; whitelists `earch` (used in `<s>earch` HTML). |
| `release-hook.sh` | Pre-release gauntlet: `typos` → `cargo clippy --deny warnings` → `cargo fmt --check` → `cargo test --workspace` → `cargo dev check --require-solutions` → `cargo +1.88 dev check --require-solutions`. |

## 4. Deployment and Distribution

Rustlings is distributed as a single Cargo-installable binary; there is no
deployment in the server sense.

### 4.1 The published artifacts

| Artifact | Where | Purpose |
|---|---|---|
| `rustlings` crate | crates.io | `cargo install rustlings` source. Includes only `src/`, `exercises/`, `solutions/`, `dev-Cargo.toml`, `README.md`, `LICENSE`. |
| `rustlings-macros` crate | crates.io | The proc-macro that embeds files. Includes only `src/` and `info.toml`. Published as a hard dependency at the same workspace version (`= 6.5.0`). |
| `rustlings.rust-lang.org` website | GitHub Pages (Zola) | Static site built from `website/` (Tailwind via `@tailwindcss/cli`). Theme: dracula. Search index: disabled. |

### 4.2 Release procedure

1. Bump `version` in `[workspace.package]` (and consequently both crate
   manifests, since they use `version.workspace = true`).
2. Update `CHANGELOG.md`.
3. Run the release hook locally:
   ```bash
   ./release-hook.sh
   ```
   This runs `typos`, Clippy with warnings denied, `cargo fmt --check`,
   `cargo test --workspace`, `cargo dev check --require-solutions`, and the
   same `dev check` again under MSRV (`cargo +1.88`).
4. Tag the commit (`git tag vX.Y.Z`) and push.
5. Publish `rustlings-macros` first (`cargo publish -p rustlings-macros`),
   then `rustlings` — they share a workspace version so the dependency
   constraint `rustlings-macros = "=X.Y.Z"` resolves immediately.
6. Update `website/` and deploy.

> [!NOTE]
> Because the workspace pins `rustlings-macros = "=6.5.0"` (an `=` constraint),
> any version bump to `rustlings` requires a matching version bump and
> simultaneous publish of `rustlings-macros`. The release hook implicitly
> verifies this by running `cargo dev check` against the workspace.

### 4.3 Distribution-channel-specific notes

| Channel | Notes |
|---|---|
| `cargo install rustlings` | Recommended. Uses the latest published crate. |
| `cargo install rustlings --locked` | Pins transitive deps to the published `Cargo.lock`. Use in CI and reproducible scripts. |
| Distro packages | None of the major Linux distros currently ship Rustlings. |
| Source | `cargo install --path .` from a checkout. Only useful for contributors. |

## 5. Operations

There is no service to operate, but there are recurring maintenance tasks for
the project itself and for community-track maintainers.

### 5.1 Adding or changing an official exercise

1. Edit `rustlings-macros/info.toml` to add the `[[exercises]]` block.
   Honor the schema in section 3.3.
2. Add `exercises/<dir>/<name>.rs` with at least:
   - A `fn main()` declaration.
   - At least one `// TODO` comment.
   - If `test = true`, at least one `#[test]\n`-annotated function (`dev check`
     enforces this).
3. Add `solutions/<dir>/<name>.rs` and write the solution. `rustfmt`-format it
   (`rustfmt --edition 2024 solutions/<dir>/<name>.rs`).
4. Run `cargo dev update` to regenerate `dev/Cargo.toml`.
5. Run `cargo dev check --require-solutions` to validate everything.
6. Run `cargo run -- run <name>` to play through the failing state, fix it,
   confirm it passes.
7. Commit and open a PR.

> [!TIP]
> Don't try to author exercises that need network access, big external
> dependencies, or custom build scripts. The `[lints.rust]` block in the
> generated `Cargo.toml` forbids `unsafe_code` and `unstable_features` for
> exercise binaries — keep solutions stable-Rust and self-contained.

### 5.2 Authoring a community-exercise track

```bash
rustlings dev new ./my-rustlings
cd my-rustlings
git add .
```

`dev new` creates the standard layout, with a single
`[[exercises]]` placeholder named `???` you must replace. See section 5.1
points 1–6 for adding exercises (substituting `cargo run --` with `rustlings`
since you're using the installed binary).

To validate before publishing:

```bash
rustlings dev check --require-solutions
```

To distribute, push the repo to a Git hosting provider. Users clone it and run
`rustlings` inside the clone — Rustlings detects the local `info.toml` and
runs the watch loop against it.

### 5.3 Maintenance checklist

- [ ] Run `cargo test --workspace` weekly to catch toolchain regressions.
- [ ] Run `cargo dev check --require-solutions` whenever a solution or
      exercise file changes.
- [ ] Run `cargo dev update` and commit `dev/Cargo.toml` whenever exercises
      are added, removed, or renamed.
- [ ] Bump MSRV (`Cargo.toml`'s `rust-version` AND `release-hook.sh`'s
      `+1.88`) together with anything that requires a newer compiler.
- [ ] Keep `info.toml` hints up to date when chapters of *The Rust Programming
      Language* book are renumbered (most hints link to specific chapters).

## 6. Performance

### 6.1 Watch-mode latency

The hot loop is: file save → notify event → 200 ms debounce → cargo build →
(cargo test) → cargo clippy → cargo run → render. Latency is dominated by
Cargo, which Rustlings cannot meaningfully control. Rustlings' own overhead
(notify dispatch, ANSI render, hint render) is sub-millisecond.

The 200 ms debounce in `src/watch/notify_event.rs` deduplicates rapid saves
(many editors fire two `notify` events per save, one for the temp file and
one for the rename); it adds at most 200 ms to the perceived round-trip.

> [!NOTE]
> Cargo is fastest when the target directory is hot. The first exercise after
> a clean checkout takes 30–60 s while Cargo compiles the entire dependency
> tree of the exercise. Subsequent exercises in the same session are fast
> because most dependencies are cached.

### 6.2 `check-all` parallelism

`AppState::check_all_exercises` uses `thread::scope` to spawn up to
`thread::available_parallelism().map_or(8, |n| n.get())` worker threads, each
running one exercise to completion at a time. A single channel funnels the
results back to the main thread for live progress rendering. If any worker
fails (e.g. EMFILE under heavy parallelism), Rustlings re-runs that exercise
**sequentially** in a fallback pass.

The default of 8 if `available_parallelism` errors is a deliberate floor —
parallelism above ~16 saturates Cargo's own internal parallelism and yields
diminishing returns.

### 6.3 Embedded-file size

Every exercise, solution, and README is `include_bytes!()`-ed into the
`rustlings` binary. As of `b730882` that's:

- 96 exercise files (~336 KB of source across all of them).
- 96 solution files (similar).
- 24 per-folder READMEs.
- 1 `info.toml` (~38 KB).
- 1 `dev-Cargo.toml`.

The release binary is in the low single-digit MB range.

## 7. Security

### 7.1 Threat model

Rustlings is a developer tool that runs on the developer's own machine,
processes files the user authors, and shells out to `cargo`/`rustc`. The
threat model is therefore **the developer's untrusted exercise files** —
malicious code in an exercise file will execute with the user's privileges
under `cargo run`.

> [!WARNING]
> When you `git clone` a community Rustlings track and run `rustlings`, you
> are about to execute Rust code from the repository on your machine, exactly
> as if you ran `cargo run` on it. Treat community tracks like any other
> untrusted source code: review the exercises before running.

### 7.2 Hardening choices in source

| Mechanism | Where | Effect |
|---|---|---|
| `unsafe_code = "forbid"` | workspace `[workspace.lints.rust]` and exercise `[lints.rust]` | The `rustlings` binary, and every shipped exercise, refuses to compile any `unsafe` block. |
| `unstable_features = "forbid"` | same | No `#![feature(...)]` allowed; pinning to the documented MSRV. |
| `disallowed-types`/`disallowed-methods` | `clippy.toml` | Forbids known-inefficient `crossterm` styling and known-footgun `std::thread::spawn`, `std::process::exit`. |
| Subprocess stdin closed | `src/cmd.rs::run_cmd` | All `cargo` subprocesses run with `Stdio::null()` for stdin to prevent prompts hanging the watcher. |
| `--no-default-features` for crossterm | `Cargo.toml` | Only `windows` and `events` features are enabled — keeps the dep surface small. |
| TTY check before raw mode | `src/main.rs` | Watch mode aborts with a clean error if stdout isn't a TTY rather than silently corrupting redirected output. |
| Termios save/restore | `src/watch.rs::watch` (non-Windows) | The original `local_modes` are saved before disabling ICANON/ECHO and restored on exit, even if watch mode panics. |
| Editor command parsed with shlex | `src/editor.rs` | Avoids ad-hoc string splitting that could mis-handle quoted arguments. |

### 7.3 Subprocess invariants

Every `cargo` subprocess Rustlings spawns has:

- `stdin` set to `Stdio::null()`.
- `stdout` and `stderr` either both piped to a buffer (when capturing output)
  or both `Stdio::null()` (when discarding).
- `--no-deps` and `--format-version 1` for `cargo metadata` (deterministic
  output, no network resolution).

This avoids interactive prompts, prevents partial writes, and guarantees
deterministic JSON parsing.

### 7.4 Reset safety

`reset` is destructive. For official Rustlings, Rustlings overwrites the file
with the embedded original (no backup). For community tracks, Rustlings runs
`git stash push -- <path>` so your work is recoverable from the Git stash.

> [!CAUTION]
> If a user runs `rustlings reset <name>` outside any Git working tree on a
> community track, the underlying `git stash push` will fail and Rustlings
> aborts before touching the file. **In an official Rustlings track**, however,
> the reset proceeds without any backup, so unsaved work in the exercise file
> is gone permanently. Always commit or back up your edits before invoking
> reset on the official track.

## 8. Developer Internals

### 8.1 Repository structure

```text
rustlings/
├── Cargo.toml                         # workspace + rustlings package
├── Cargo.lock
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE                            # MIT
├── README.md
├── build.rs                           # Windows-only dev-Cargo.toml copy
├── clippy.toml                        # workspace-wide Clippy denials
├── dev-Cargo.toml                     # symlink/copy of dev/Cargo.toml (for crates.io packaging)
├── release-hook.sh
├── src/
│   ├── main.rs                        # CLI dispatcher
│   ├── cli.rs                         # clap definitions
│   ├── app_state.rs                   # AppState + ExercisesProgress + state-file logic
│   ├── exercise.rs                    # Exercise + RunnableExercise trait
│   ├── info_file.rs                   # ExerciseInfo + InfoFile + parse()
│   ├── embedded.rs                    # EMBEDDED_FILES wrapper
│   ├── cargo_toml.rs                  # `bin = […]` regen
│   ├── cmd.rs                         # CmdRunner subprocess wrapper
│   ├── editor.rs                      # Editor (Cmd, Zellij)
│   ├── editor/zellij.rs               # Zellij action edit / pane close helpers
│   ├── init.rs                        # `rustlings init`
│   ├── run.rs                         # `rustlings run`
│   ├── list.rs                        # list-mode entry + key handling
│   ├── list/scroll_state.rs           # scroll-window math
│   ├── list/state.rs                  # ListState (filter, search, draw)
│   ├── term.rs                        # ANSI helpers, progress bar, file-link emitter
│   ├── watch.rs                       # watch-mode entry, termios save/restore
│   ├── watch/state.rs                 # WatchState (run, render, hint, reset, check-all)
│   ├── watch/notify_event.rs          # NotifyEventHandler (200 ms debounce, exercise-name match)
│   ├── watch/terminal_event.rs        # terminal_event_handler (key→InputEvent)
│   ├── dev.rs                         # DevCommand subcommand dispatch
│   ├── dev/check.rs                   # `rustlings dev check`
│   ├── dev/new.rs                     # `rustlings dev new`
│   └── dev/update.rs                  # `rustlings dev update`
├── rustlings-macros/
│   ├── Cargo.toml
│   ├── info.toml                      # the official exercise registry (the IP)
│   └── src/lib.rs                     # `include_files!()` proc-macro
├── exercises/                         # 96 exercise files in 24 dirs + per-dir README
├── solutions/                         # 96 solutions matching exercises/ structure
├── tests/
│   ├── integration_tests.rs           # CLI-as-subprocess tests
│   └── test_exercises/                # synthetic mini-track for testing dev check
├── website/                           # Zola static site → rustlings.rust-lang.org
└── dev/                               # dev-only Cargo project (`cargo dev …`)
    └── Cargo.toml                     # generated bin = […] for in-tree dev runs
```

### 8.2 `AppState` lifecycle

`AppState` is the single owning struct for runtime state. It is constructed
in `main()` and passed `&mut` to every subcommand handler.

**Construction (`AppState::new`):**

1. Build a `CmdRunner` (calls `cargo metadata` once).
2. `OpenOptions::new().create(true).read(true).write(true).truncate(false)`
   on `.rustlings-state.txt`.
3. `term::canonicalize("exercises")` — the canonical absolute path used to
   build clickable terminal file links.
4. Build the `exercises: Vec<Exercise>` from `info_file.exercises`:
   - Compute each exercise's `path` (`exercises/<dir>/<name>.rs`) and
     `canonical_path` (canonicalized, with the Windows `\\?\` prefix stripped).
   - Leak the `path` String to `&'static str` so it can be reused in editor
     subprocess args without lifetime juggling.
5. Read the existing state file, parse current-exercise + done-set lines.
   If the file is empty or malformed, return `StateFileStatus::NotRead` so
   `main` knows to show the welcome message.

**Mutation methods:**

- `set_current_exercise_ind(ind)` / `set_current_exercise_by_name(name)` —
  bounds-check, set the index, write the state file.
- `set_status(ind, done)` — flip `Exercise.done` and increment/decrement
  `n_done` without writing.
- `set_pending(ind)` — convenience: `set_status(ind, false)` then write if
  changed.
- `reset_current_exercise()` / `reset_exercise_by_ind(ind)` — set pending,
  then dump the file (official: from `EMBEDDED_FILES`, community:
  `git stash push -- <path>`).
- `check_all_exercises(stdout)` — see section 6.2.
- `done_current_exercise::<CLEAR_BEFORE_FINAL_CHECK>(stdout)` — mark current
  done, advance to next pending, or run a final check-all when nothing pending
  remains. The const-generic flag controls whether to clear the screen before
  the final check (used by watch mode but not by `run`).

### 8.3 The `RunnableExercise` trait

```rust
pub trait RunnableExercise {
    fn name(&self) -> &str;
    fn dir(&self) -> Option<&str>;
    fn strict_clippy(&self) -> bool;
    fn test(&self) -> bool;

    fn run<const FORCE_STRICT_CLIPPY: bool>(
        &self,
        bin_name: &str,
        output: Option<&mut Vec<u8>>,
        cmd_runner: &CmdRunner,
    ) -> Result<bool> { ... }

    fn run_exercise(&self, output, cmd_runner) -> Result<bool> { ... }
    fn run_solution(&self, output, cmd_runner) -> Result<bool> { ... }
    fn sol_path(&self) -> String { ... }
}
```

`run_exercise` calls the generic `run::<false>(self.name(), …)`. `run_solution`
appends `_sol` to the bin name and calls `run::<true>(…)` so solutions are
always linted at the strict level.

The `run` method's pipeline:

1. `cargo build -q --bin <bin>` (with optional `--manifest-path dev/Cargo.toml`
   in debug builds for the in-tree development workflow).
2. If build failed, return `Ok(false)` early.
3. If `test` is true: `cargo test … --bin <bin>` (with `-- --color always
   --format pretty` if output is captured).
4. If test failed, run the bin to dump output, return `Ok(false)`.
5. `cargo clippy --profile test --bin <bin>` (with `-- -D warnings` if
   `strict_clippy` or the const-generic flag is set).
6. Run the bin: `cmd_runner.run_debug_bin(bin_name, …)` — directly executes
   `<target_dir>/debug/<bin>` rather than going through `cargo run`, to avoid
   re-entering Cargo.
7. Return `Ok(clippy_success && run_success)`.

> [!NOTE]
> Steps 1, 3, and 5 each clear the output buffer between phases so Rustlings
> can show only the **most relevant** stage's output to the user — there's no
> reason to show stale `cargo build` output once `cargo clippy` has spoken.

### 8.4 The `EMBEDDED_FILES` proc-macro expansion

`rustlings-macros/src/lib.rs::include_files!()` runs **at compile time**:

1. Reads `rustlings-macros/info.toml` via `include_bytes!()` (with `\r`
   stripped for Windows).
2. Parses with `toml::de::from_str::<InfoFile>` to get the exercise list.
3. Builds three vectors:
   - `exercise_files`: `format!("../exercises/{dir}/{name}.rs")`.
   - `solution_files`: `format!("../solutions/{dir}/{name}.rs")`.
   - `dirs`: deduplicated directory names, with each exercise's `dir_ind`
     pointing into this vector (avoids storing the directory string per
     exercise).
4. Builds `readmes` from each unique dir: `format!("../exercises/{dir}/README.md")`.
5. Emits a `quote!` macro expansion that builds an `EmbeddedFiles { … }`
   literal — every path becomes a compile-time `include_bytes!()` call, so
   the output is a static byte-array reference.

The expansion is the single static `EMBEDDED_FILES: EmbeddedFiles = …` in
`src/embedded.rs`.

### 8.5 The watch-mode state machine

`src/watch/state.rs::WatchState` holds `app_state`, `output`, `show_hint`,
`done_status`, `manual_run`, `term_width`, and a sender channel for
unpausing the terminal-event listener.

The main `run_watch` function in `src/watch.rs`:

1. Spawns a `notify` watcher rooted at `exercises/`, with
   `with_follow_symlinks(false)` and a 1 s poll interval as fallback.
2. Spawns the `NotifyEventHandler` worker with a 200 ms debounce timer.
3. Spawns the `terminal_event_handler` thread (reads keys/resizes via
   `crossterm::event::read`).
4. Builds a `WatchState` and immediately calls `run_current_exercise`.
5. Enters a `recv()` loop on the watch event channel and dispatches:
   - `Input::Next` → `next_exercise` (no-op if not done).
   - `Input::Run` → `run_current_exercise` (manual mode only).
   - `Input::Hint` → `show_hint`.
   - `Input::List` → returns `WatchExit::List` (the list/watch outer loop
     handles entering/leaving the list view cleanly).
   - `Input::CheckAll` → `check_all_exercises`.
   - `Input::Reset` → `reset_exercise` (interactive y/n prompt; pauses the
     terminal-event thread until the prompt is dismissed).
   - `Input::Quit` → break.
   - `FileChange { exercise_ind }` → ignored if it isn't the current
     exercise; otherwise re-run.
   - `TerminalResize { width }` → re-render with the new width.
   - errors → propagate.

Input handling is paused (`InputPauseGuard`/`EXERCISE_RUNNING` atomic) while
exercises are running, so keys pressed during the build don't get queued and
processed afterward.

### 8.6 List-view internals

The `ListState` in `src/list/state.rs` keeps a `ScrollState`, an optional
`Filter` (`None`/`Done`/`Pending`), and a `search_query: String`. Drawing
uses `crossterm` synchronized updates (`BeginSynchronizedUpdate`/
`EndSynchronizedUpdate`) to avoid flicker. The `ScrollState` (in
`scroll_state.rs`) handles selection-window math: it keeps a scroll padding
of `max_n_rows_to_display / 4` (capped at `max_scroll_padding`) so the
selection rarely sits at the very top or bottom of the visible window.

`apply_search_query` is called on every search-mode keystroke; it filters
the in-memory exercise list and rebuilds the visible rows.

### 8.7 The `dev check` rule set

`src/dev/check.rs::check` is the validator for community tracks. It runs in
this order:

1. `info_file.exercises.len() <= MAX_N_EXERCISES (999)` or bail.
2. `check_cargo_toml`: the `bin = […]` regenerated from `info.toml` byte-for-
   byte equals the existing `bin = […]`. If not, suggest `rustlings dev update`.
3. `check_exercises` (via `check_info_file_exercises`): per-exercise:
   - `name` non-empty and `<= 32` chars.
   - `name` matches `[a-zA-Z0-9_]`.
   - If `dir` is present: same character set, non-empty.
   - `hint` non-empty after `trim_ascii`.
   - `name` is unique (HashSet insertion).
   - File at `path()` opens for reading.
   - File contains `fn main()`.
   - File contains `// TODO`.
   - File contains `#[test]\n` if and only if `test == true`.
4. `check_unexpected_files("exercises", …)`: every file in `exercises/` is
   either a `README.md` or matches an `info.toml` entry. Only one level of
   directory nesting is allowed.
5. In parallel with 3+4, `check_exercises_unsolved` actually runs every
   exercise (skipping those with `skip_check_unsolved = true`) and bails if
   any exercise passes — the bundled exercise file shouldn't already be
   solved.
6. If `--require-solutions`:
   - `check_solutions` runs every solution, requires presence (errors if
     missing), runs `cargo build/test/clippy/run` on it, and must pass with
     strict Clippy.
   - `rustfmt --check --edition 2024` is run against every solution path.
   - `check_unexpected_files("solutions", …)` validates layout.

> [!IMPORTANT]
> The `dev check` rule "exercise must contain `// TODO`" is a strong UX
> contract: it guarantees that every exercise file has at least one obvious
> hand-hold for the learner. If you intentionally don't want a TODO comment
> (e.g. an intro file that's pre-solved), set `skip_check_unsolved = true`
> AND also keep at least one TODO comment in the file — `dev check` does not
> currently exempt TODO checking even when `skip_check_unsolved` is set.

### 8.8 Testing strategy

| Layer | Where | What it covers |
|---|---|---|
| Unit | inline `#[cfg(test)] mod tests` blocks in `app_state.rs`, `cargo_toml.rs`, `cmd.rs`, `embedded.rs` | The `next_pending_exercise_ind` algorithm, `bin = […]` parser/regenerator, `run_cmd` echoing through a pipe, the embedded-files round-trip vs. `info.toml`. |
| Integration | `tests/integration_tests.rs` plus the synthetic track in `tests/test_exercises/` | Run the binary as a subprocess against a known-good and known-bad exercise set, assert the right exit codes and outputs. |
| Format check | `cargo fmt --check` (release hook) | Style consistency. |
| Lint | `cargo clippy --deny warnings` (release hook) | Catches lint regressions. |
| MSRV | `cargo +1.88 dev check --require-solutions` (release hook) | Confirms the binary still builds and the whole exercise set still passes on the MSRV toolchain. |

There are 26 classes (structs/enums) and 86 named functions/methods across
the binary. Coverage is concentrated in `app_state.rs` (state-file invariants)
and `cargo_toml.rs` (the `bin = […]` regen); the watch-mode and editor logic
are exercised by integration tests rather than units, since they require a
real TTY and a real `notify` backend.

### 8.9 Code conventions

- **No `unsafe`, ever.** Workspace-wide forbid.
- **No unstable features.** Workspace-wide forbid.
- **`anyhow::bail!`/`Context` only**, never `panic!()` for user-facing
  errors. Panics are reserved for invariants the type system can't express.
- **Static error strings** are pulled out as `const _ERR: &str = "…"` at the
  bottom of the module they're raised in, mirroring the Rust standard
  library's style.
- **`leak()` is used judiciously** to convert `String` to `&'static str` when
  the data lives until program exit (exercise paths, parsed `info.toml`
  contents). The trade-off is intentional: avoiding lifetime parameters on
  `Editor::open` is worth a few KB of leaked allocations that the OS reclaims
  on exit.
- **`thread::Builder::spawn` not `thread::spawn`.** The Clippy `disallowed-
  methods` config forces every thread spawn to handle the spawn error
  explicitly, which catches OOM and resource-exhaustion at thread-creation
  time rather than after the fact.
- **`process::ExitCode` not `process::exit()`.** All paths return through
  `main -> Result<ExitCode>` so destructors run.

### 8.10 Potential improvements

The community has flagged several enhancements via GitHub Issues; the team
tracks them publicly:

| Issue | Topic | Notes |
|---|---|---|
| #1501 | Hints with offline documentation suggestions | Open. Would require a new `info.toml` field. |
| #1764 | More quizzes (quiz4+) | Open. Material decision pending. |
| #2275 | Offline mode | Open. The runtime is already offline once installed; the hints only contain external URLs as references. |
| #2272 | Clearer "how to start" docs in the README | Open. Resolved largely in CONTRIBUTING.md, but the README itself still leans on the website. |
| #2326 | `quiz1` wording | Open. Cosmetic. |
| #2340 | Reorder exercises to match The Book | Open. Would be a major UX/`info.toml` rewrite. |
| #2379 | Potential overflow in `functions5` exercise | Open suggestion. The exercise tests the operator-as-expression-vs-statement issue, not arithmetic correctness, so the type the user picks doesn't actually overflow under the test. |
| #887 | More `Option` exercises (`ok_or`, `and_then`) | Open. |
| #2230 | Confusing comment in `09_strings/strings2` | Open. |
| #2307 | `08_enums/enum2` Messages-array vs. variables | Open design discussion. |
| #2338 | More links in `09_strings/strings3` README | Open. |
| #2339 | Better test for `10_modules/modules1` | Open. |
| #2228 | `structs2` line 35 panic (closed) | Resolved upstream. |
| #2164 | Open current exercise in `$EDITOR` (closed) | Resolved by the `--edit-cmd` flag and VS Code/Zellij autodetection. |

### 8.11 Contributing

The full guide lives in `CONTRIBUTING.md`. Workflow summary:

1. Open an issue first for non-trivial changes — the team prefers design-
   level alignment before code review.
2. Branch from `main`, make changes.
3. Run `./release-hook.sh` locally to mirror CI.
4. PR. CI runs the same gauntlet on Linux/macOS/Windows.
5. Squash-merge after review.

> [!TIP]
> The `release-hook.sh` script is the canonical "did I break anything"
> check. If it passes locally, your PR will almost certainly pass CI. If
> you want to run only a subset, `cargo clippy -- --deny warnings` and
> `cargo dev check --require-solutions` together cover the bulk of the
> rules `dev check` enforces.

## 9. Integration Points

Rustlings is a leaf-node tool, but it does interface with several external
systems:

| System | Direction | How |
|---|---|---|
| Cargo | outbound | `cargo metadata` once at startup; per-exercise `cargo build/test/clippy`. |
| `rustfmt` | outbound | `dev check --require-solutions` formats every solution. |
| `git` | outbound | `init` runs `git init`. Community-track `reset` runs `git stash push -- <path>`. |
| File-system watcher | inbound | `notify::RecommendedWatcher` on `exercises/` (recursive). |
| Terminal | bidirectional | `crossterm` for input/output. `rustix::termios` (non-Windows) to disable canonical mode and echo. |
| VS Code | outbound | `code` (or `codium`) executable for the `Open editor` flow. Editor detection via `TERM_PROGRAM=vscode`. |
| Zellij | outbound | `zellij action edit <path>` to open exercises as panes. Pane-id parsing in `src/editor/zellij.rs`. |
| `cargo install` registry | inbound | crates.io publication. |

### 9.1 Embedding Rustlings into a curriculum

A common pattern: an organization wants a private exercise set that runs in
the student's normal Rustlings install. Two approaches:

1. **Community-track repo.** Use `rustlings dev new`, push the resulting
   project to a private Git repo, ask students to clone and run. Maintains
   one Rustlings install across all of them; you ship only the exercise
   files.
2. **Forked binary.** Fork `rust-lang/rustlings`, replace
   `rustlings-macros/info.toml` and the `exercises/`/`solutions/` trees,
   bump the binary name, and publish to a private cargo registry. Heavier
   maintenance burden, but you control the binary's behavior.

Approach 1 is strongly recommended unless you need to change the runtime
itself.

### 9.2 CI integration

A CI job for a community track typically runs:

```bash
cargo install rustlings --locked
rustlings dev check --require-solutions
```

This validates `info.toml`, the `Cargo.toml` `bin` block, every exercise
file's conventions, and every solution. A failing exit code from `dev check`
means at least one rule is violated; the error message identifies the file
and rule.

For curriculum-level CI (each student's progress), `rustlings check-all`
exits 1 if anything is pending, which is composable with standard CI gates.

---

_Generated April 2026 from rust-lang/rustlings at b730882. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
