14 days of Rust
I wanted to get hands-on and finally spend time with Rust. Figured two weeks was a solid enough time to spend daily reading and coding. I asked Claude to give me a plan and here it is.
9 years 4 months 6 days since last time I wrote a post, a lot has happened in between but right now I'm writing Rust so that will be the focus for a while.
I told Claude I was coming from zero and wanted to be able to write CLI, TUI and a simple Markdown → HTML formatter by the end. I like to use Exercism to give me hands on experience, and I'm using the Rust-Book from Brown after I watched this Will Crichton: Rust for Everyone!. Specifically seeing the Aquascope Playground explain borrow checking was cool.
The following is Claude's plan for 14 days of Rust, you can tell right away it's Claude. I wonder in a few years if it will sound different. Some formatting had to be fixed to render correctly here, but nothing about the content was changed.
14 Days of Rust — from zero to a working SSG
- Start: Tue 11 Aug 2026
- End: Mon 24 Aug 2026 (14 consecutive days)
- Budget: ~2 hours/day
- Tools: RustRover, Exercism Rust track, The Rust Book
The deal
One project, three faces. You build a static site generator as a single crate:
| Piece | What it is | Days |
|---|---|---|
lib.rs | Markdown → HTML parser + site model | 8–10 |
src/main.rs | ssg build / ssg new CLI | 7, 13 |
src/bin/tui.rs | Browse pages, live-preview the render | 11–12 |
Everything before Day 7 is loading the magazine. Days 7–14 are the project.
Daily shape (~2h)
- Week 1 — ~40 min read + type along · ~40 min Exercism · ~40 min consolidating.
- Week 2 — ~30 min reading · ~90 min project. Exercism is optional and marked as such.
Two rules that don't change:
- Type the code, don't paste it. Typing is where the borrow checker teaches you.
- After each Exercism exercise, read 2–3 community solutions for it. Single highest-leverage habit on the list.
Rust Book chapter numbers shifted in the 2024 edition. Titles are stable — go by title if a number looks off.
Day 0 — before you start (15 min, today)
-
rustup update && rustup component add clippy rustfmt rust-analyzer -
rustup doc --book— offline copy of the Book -
Install the Exercism CLI,
exercism configure --token=..., join the Rust track - In RustRover: enable Clippy as the external linter (Settings → Rust → External Linters → Clippy, tick "Run on save")
- Turn on Rustfmt on save (Settings → Rust → Rustfmt)
-
git initis already done — commit at the end of every single day, even broken code
Week 1 — the language
Day 1 (Tue 11 Aug) — Cargo, syntax, the shape of a program
Read: Book ch. 1–3 (Getting Started, Guessing Game, Common Programming Concepts)
- Type out the guessing game by hand, end to end. Run it.
-
Know cold:
cargo new,run,build,test,check,clippy,fmt,add -
cargo checkis your fast loop — bind it to a shortcut in RustRover -
Variables vs. mutability, shadowing, scalar/compound types,
if/loop/while/for, functions & expression-vs-statement
Exercism: hello-world, two-fer, reverse-string, gigasecond
-
Read the community solutions for
reverse-string— note the.chars().rev()vs. bytes distinction. That's a UTF-8 lesson you'll need on Day 9.
Done when: you can explain why let x = 5; x = 6; fails and let x = 5; let x = 6; doesn't.
Day 2 (Wed 12 Aug) — Ownership, borrowing, slices
Read: Book ch. 4 (Understanding Ownership) — slowly. This is the chapter.
- Move vs. copy vs. clone
-
&Tvs.&mut T; the "one mutable XOR many immutable" rule -
Stringvs.&str, and why&stris the right function parameter - Deliberately write 3 programs that fail to compile, and read each error out loud
Exercism: raindrops, acronym, pangram, scrabble-score
-
Force yourself to take
&strand returnStringwhere appropriate
Done when: you can say what fn longest(a: &str, b: &str) -> &str is missing without looking it up. (You'll fix it on Day 6.)
Day 3 (Thu 13 Aug) — Structs, enums, pattern matching
Read: Book ch. 5–6 (Structs, Enums and Pattern Matching)
-
implblocks, associated functions vs. methods,Self -
#[derive(Debug, Clone, PartialEq)]and when each matters -
Option<T>,matchexhaustiveness,if let/let else -
Sketch (don't build) an enum for a markdown block:
Heading,Paragraph,CodeBlock,Rule
Exercism: space-age, high-scores, allergies
Nothing here needs a hand-written trait impl yet — traits are Day 6, and
clockwaits until then.
Done when: match on an enum feels better than a chain of ifs. Keep that enum sketch; it becomes Block on Day 8.
Day 4 (Fri 14 Aug) — Collections + module system
Read: Book ch. 7 (Managing Growing Projects) and ch. 8 (Common Collections)
-
Vec<T>,String,HashMap<K, V>— and the entry API -
mod,pub,use,crate::vs.super::, file-per-module layout -
Split a toy project into
src/lib.rs+src/main.rsand call the lib from the bin — you'll do exactly this for real on Day 7
Exercism: word-count, nucleotide-count, etl
-
word-countis a dry run for your tokenizer. Take it seriously.
Done when: you know why src/main.rs calls mycrate::foo() and not crate::foo().
Day 5 (Sat 15 Aug) — Error handling
Read: Book ch. 9 (Error Handling)
-
panic!vs.Result<T, E>; when unwrap is honestly fine (tests, prototypes, provable invariants) -
The
?operator and what it desugars to (From::fromon the error) -
Write one custom error enum by hand implementing
Display+std::error::Error -
Then do it in 4 lines with
thiserrorand feel the difference
Crates: cargo add anyhow thiserror in a scratch project
-
Rule of thumb you'll use all fortnight:
thiserrorin the library,anyhowin the binaries
Exercism: luhn, matching-brackets, roman-numerals
Done when: you can convert a function from unwrap()-everywhere to ?-everywhere without thinking.
Day 6 (Sun 16 Aug) — Generics, traits, lifetimes, tests
Read: Book ch. 10 (Generic Types, Traits, Lifetimes) and ch. 11 (Writing Automated Tests)
-
Trait definitions, default methods,
impl Traitin argument and return position -
Trait bounds and
whereclauses -
Lifetime annotations — go back and fix Day 2's
longest -
#[cfg(test)] mod tests,assert_eq!,#[should_panic],-> Result<(), E>tests -
Table-driven tests: a
&[(input, expected)]array in a loop. You'll use this for every parser rule.
Exercism: clock (this is your impl Display + operator-trait exercise), sublist (generics + bounds), triangle
Done when: you've written a generic function with a trait bound that compiles on first try.
Week 2 — the build
Week 2 is project-dominant: ~30 min reading, ~90 min building. Exercism entries below are optional — do them only if the project work finished early.
Day 7 (Mon 17 Aug) — CLI skeleton + pin the scope
Read: Book ch. 12 (An I/O Project: Building a Command Line Program) — build minigrep fully. Best 90 minutes in the Book. If time is tight, read it and build the argument-parsing half only.
Project — mdssg is born:
-
cargo new mdssgin this repo; addsrc/lib.rsalongsidesrc/main.rs -
cargo add clap --features derive·cargo add anyhow thiserror walkdir -
Define the CLI with clap's derive API:
ssg build --input <dir> --output <dir> # default ./content -> ./public ssg new <title> # scaffold a content/*.md file -
--verboseflag,-houtput that reads well -
buildcurrently: walk--inputfor*.md, print each path, exit 0 -
mainreturnsanyhow::Result<()>; every error path carries.context("...") -
RustRover: create a run configuration for
ssg buildwith args pre-filled
Write SPEC.md — this is the important half of today. Fifteen lines, no more, stating exactly what your markdown dialect supports and what it doesn't. Draft:
Supported: ATX headings (# .. ######), paragraphs, fenced code (``` with optional lang),
thematic breaks (---), unordered lists (- *), ordered lists (1.),
blockquotes (>), inline: *em* **strong** `code` [link](href) , \escapes
Not supported: setext headings, reference links, HTML passthrough, tables,
nested lists, lazy continuation, autolinks, footnotes
-
Create
tests/fixtures/<feature>.md+<feature>.htmlpairs for every "supported" line - Write the test harness that walks the fixture directory and asserts each pair — it fails everything today, and that's the point. Days 8–10 are "make the fixtures pass."
Done when: cargo run -- build --input content lists your markdown files, and SPEC.md has a line you can point at whenever you're tempted to add a feature.
Day 8 (Tue 18 Aug) — Iterators + block parser, part 1
Read: Book ch. 13 (Iterators and Closures)
-
map/filter/filter_map/fold/collect/take_while/peekable -
Peekable<Lines>is the workhorse of a line-based block parser — get comfortable with it -
Closures:
Fn/FnMut/FnOnce, and whymoveexists
Project — the flat blocks only (src/parser/block.rs):
-
Define the AST, with list items deliberately flat for now:
pub enum Block { Heading { level: u8, inlines: Vec<Inline> }, Paragraph(Vec<Inline>), CodeBlock { lang: Option<String>, code: String }, Rule, // List and Quote land on Day 10 } -
pub fn parse_blocks(src: &str) -> Vec<Block>driven by aPeekableline iterator -
Support four things only: ATX headings, fenced code,
---rules, paragraphs as fallback -
Inlines stay raw for now —
Vec<Inline>holds a singleInline::Textplaceholder -
Table-driven tests including the ugly cases: empty file, no trailing newline, unterminated fence,
#-with-no-space -
Use the RustRover debugger. Breakpoint inside the parse loop, step through a 10-line document, watch the
Peekablestate. Twenty minutes here beats an hour ofprintln!.
Exercism (optional): accumulate
Done when: those four block types parse correctly and their fixtures are the first ones passing.
Day 9 (Wed 19 Aug) — Inline parser + HTML renderer
Read: Rust By Example §"Strings", plus the std::str and char docs. Skim pulldown-cmark's docs to see how the pros model events — don't add it as a dependency; you're writing this one.
-
chars()vs.bytes()vs.char_indices(); why&s[0..1]can panic -
String::push_str, andwrite!into aStringviastd::fmt::Write
Project — inlines and the first real HTML:
-
Define the inline enum:
pub enum Inline { Text(String), Emph(Vec<Inline>), Strong(Vec<Inline>), Code(String) } // Link and Image land on Day 10 -
parse_inlines(&str) -> Vec<Inline>handling\escapes,`code`,**strong**,*em* -
Do escapes and code spans first — they're unambiguous. Emphasis is the fiddly one; get the simple non-nested case working and move on. Nested
**a *b* c**is a stretch goal, not a today goal. - Wire it into Day 8's blocks (replace the placeholder)
-
render_html(&[Block]) -> String— recursive, writing into oneStringbuffer -
Escape
& < > "in text and attributes. Write the test that proves<script>in a paragraph comes out inert.
Exercism (optional): anagram
Done when: md_to_html("# Hi\n\nsome **bold** text") produces exactly the HTML you'd hand-write.
Day 10 (Thu 20 Aug) — Recursive blocks + catch-up day
Read: Book ch. 15 (Smart Pointers)
-
Box<T>for recursion — the reasonBlock::Quote(Vec<Block>)compiles at all -
Rc<T>/RefCell<T>and interior mutability; know when not to reach for them -
DerefandDrop— theDrophalf is what makes Day 11's terminal guard work
This is the slack day. If Days 8–9 overran, spend the whole session finishing them and skip everything below except the last two boxes. Nothing downstream depends on lists or links.
Project — the recursive cases:
-
Block::Quote(Vec<Block>)— strip the>prefix, recurse intoparse_blocks -
Block::List { ordered: bool, items: Vec<Vec<Inline>> }— flat items, one paragraph each. Nested lists are explicitly out of scope perSPEC.md. -
Inline::Link { href, text }andInline::Image { src, alt }, with attribute escaping -
MdErrorenum viathiserror; the lib returnsResult<_, MdError>, the bin wraps it withanyhow -
cargo clippy -- -W clippy::pedantic— fix what you understand, note the rest
Done when: every fixture from Day 7's SPEC.md passes, or you've knowingly moved a line from "supported" to "not supported" and deleted its fixture.
Day 11 (Fri 21 Aug) — TUI: the event loop
Read: the ratatui book — Getting Started + the "Hello World" and "Counter" tutorials
-
cargo add ratatui crossterm - Install the panic hook first. Before anything else: a hook that restores the terminal on panic. Skip this and one crash leaves you in raw mode with no echo, typing blind.
-
Raw mode, alternate screen, and RAII teardown — a guard struct with a
Dropimpl (Day 10 pays off) - The immediate-mode idea: you redraw the whole frame every tick; no widget state persists
Project — src/bin/tui.rs:
-
Appstruct holding state: file list, selected index, scroll offset, mode -
Event loop: poll crossterm events,
q/Escquits,j/k+ arrows move -
Two-pane
Layout: left = list ofcontent/*.md, right = placeholder - Status bar showing the selected filename and a keybinding hint
Done when: you can navigate the file list and quit cleanly, with your terminal intact.
Day 12 (Sat 22 Aug) — TUI preview, then earn the abstraction
Read: the trait-objects / OOP chapter (ch. 17 or 18 depending on edition) — dyn Trait vs. generics, static vs. dynamic dispatch
Project — build it concrete, then refactor:
-
Right pane renders the selected file's parsed content — walk
Vec<Block>and map to ratatuiLine/Spanwith styles (headings bold, code dimmed, links underlined). Write this as a plain function, duplicating the HTML renderer's traversal shape. -
Scrolling in the preview pane (
PageUp/PageDown,g/G);Tabswitches focus, focused pane gets a highlighted border;rreloads from disk - Handle the sad paths: empty directory, unreadable file, file deleted while selected
-
Now look at the two renderers side by side. They walk the same AST with the same match arms and differ only in what they emit. Now extract a
Renderertrait withHtmlRendererandTuiRendererimpls — with two real implementors in front of you, you'll get the method signatures right. - Note what the trait can't cleanly express. That gap is the actual lesson about abstraction.
Done when: one parser feeds two renderers through one trait, and you can browse a folder of markdown rendered in your terminal.
Day 13 (Sun 23 Aug) — The full build pipeline
Read: Book ch. 14 (More About Cargo and Crates.io) — profiles, workspaces, features
-
cargo add serde --features derive·cargo add toml
Project — turn a parser into an SSG:
-
Frontmatter: strip a leading
---\n...\n---block, parse as TOML into#[derive(Deserialize)] struct FrontMatter { title, date, tags, draft } -
Templating: a
templates/page.htmlwith${title}/${content}placeholders. Write the 20-line substituter yourself — no template crate. -
Build:
content/**/*.md→public/**/*.html, preserving directory structure; copystatic/verbatim -
Index: generate
public/index.htmllisting every non-draft page, newest first - Error messages that name the offending file and line
ssg servewith file-watching is not in the 14 days — it's a whole evening ofnotifyplus a socket server. It's first on the after-Day-14 list.
Done when: ssg build turns a folder of markdown into a site you can open in a browser.
Day 14 (Mon 24 Aug) — Ship it
Project:
-
Doc comments (
///) on every public item;cargo test --docpasses;cargo doc --openlooks like a library you'd publish -
cargo fmt --all·cargo clippy --all-targets -- -D warnings— zero warnings -
cargo testgreen, including the fixture harness; add an integration test intests/that runs the real binary -
README.md: what it is, install, usage, and linkSPEC.mdfor the supported subset -
cargo build --releaseand time a build of 100 files -
Dogfood: write the retro below as a markdown file in
content/and build it with your own tool -
Tag it:
git tag v0.1.0
Retro — write real answers:
- Which compiler error did you hit most, and do you now understand it?
-
Where did you reach for
.clone()because you couldn't see the borrow-checker-friendly design? - What's the ugliest function in the codebase, and what would fix it?
Done when: it's tagged, documented, and you'd show it to someone.
After Day 14 — where to go next
-
ssg serve—notifyfor file watching plus a smallstd::net::TcpListenerserver, then live reload. Cut from the fortnight on purpose; it's the natural next evening. - Parser depth: nested emphasis, nested lists, setext headings, reference links, autolinks
- Spec compliance: run the CommonMark test suite against your parser and see how far off you are. Humbling and instructive.
-
Concurrency: Book ch. 16 → parallelise the build with
rayon(one page per core), then Exercism'sparallel-letter-frequencyproperly -
Async: the async chapter →
tokio+axumfor a real dev server -
Correctness: fuzz the parser with
cargo-fuzz; property tests withproptest -
Exercism hard mode:
forth,poker,dominoes,xorcism
Reference
Crates you'll install
| Crate | For | Day |
|---|---|---|
clap (derive) | CLI args | 7 |
anyhow | error handling in binaries | 5, 7 |
thiserror | error types in the library | 5, 10 |
walkdir | recursive file discovery | 7 |
ratatui + crossterm | TUI | 11 |
serde + toml | frontmatter | 13 |
Deliberately not used: pulldown-cmark, comrak, any template engine. Writing those is the point.
The loop that matters
cargo check → fix → cargo test → cargo clippy → commit. Every day.
When you're stuck
- Read the full error. Rust's errors are essays; the fix is usually in the
help:line. rustc --explain E0502(or whatever the code is).- Draw the ownership on paper — who owns it, who borrowed it, when does each drop?
- Ask "what would this look like with no references at all?" Get it compiling with
.clone(), then remove clones one at a time.
Triage — when you fall behind
Days 8–10 are the parser and they will be the tight ones. In priority order:
- Never cut Days 8–9. Headings, paragraphs, code blocks, and basic inlines are the whole spine.
- Day 10 is the buffer. Spend all of it on unfinished Day 8–9 work if needed. Lists, quotes and links are optional — move them to "not supported" in
SPEC.mdand delete their fixtures. - Then cut Day 12's polish (scrolling, focus switching, reload) before cutting the trait extraction — the refactor is the lesson.
- Last resort: ship the CLI and parser without the TUI. Two of three faces is a real result.
Cut the Exercism block before the project block, always.
Rules for this fortnight
- Type the code, don't paste it.
- Commit daily, even when it's broken.
- Read other people's Exercism solutions after each exercise.
- When tempted by a markdown feature, check
SPEC.md. If it's not there, it's not today's problem.
Exercism note
Exercise names occasionally move or get retired on the Rust track. If one isn't there, pick the nearest equivalent from the same concept group — the concept is what matters, not the specific exercise.