# Rust Development Mac Tahoe Setup
Adding Rust development on top of an Apple Silicon Mac already configured per [[General_Development_Mac_Tahoe_Setup]]. Most of the stack carries over — Ghostty, Starship, Git, GitHub CLI, VS Code, Claude Code, and the zsh setup all work identically. This guide only covers the Rust-specific additions.
> [!tip] AI assistance is optional
> The reference to Claude Code above assumes you completed Part 7 of the General guide. If you skipped it (which is fine — your dev environment works without any AI) or use a different AI tool (Copilot, Cursor, Continue, etc.), this guide's instructions still work — the language toolchain doesn't depend on any AI being present.
> [!info] Coexistence
> C, Python, Ruby, Go, and Rust can all live on the same system without interfering. Each language's toolchain installs to its own prefix: `uv` under `~/.local/share/uv/`, `rv` under `~/.rv/`, Go under `~/go/`, Rust under `~/.rustup/` and `~/.cargo/`, C/CMake under Homebrew. No conflicts.
---
## Install Rust via rustup
**Use rustup, not Homebrew's `rust` formula.** The Rust team maintains rustup as the canonical toolchain manager; Homebrew's build lags behind and doesn't handle toolchain switching or component management. Everyone serious about Rust uses rustup.
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
The installer is interactive. Accept the default ("Proceed with standard installation") unless you have a specific reason to customize. It installs:
- `rustc` — the compiler
- `cargo` — the package manager and build tool
- `rustup` — the toolchain manager (install multiple Rust versions, switch between stable/nightly)
- `rustfmt` — the formatter
- `clippy` — the linter
- Native arm64 toolchain (Apple Silicon build)
Everything lands in `~/.cargo/` and `~/.rustup/`.
### Add Cargo's bin directory to PATH
The installer typically prompts you and handles this, but verify by adding to `~/.zshrc`:
```bash
# Rust — make cargo-installed binaries callable
. "$HOME/.cargo/env"
```
Reload with `source ~/.zshrc`.
Verify:
```bash
rustc --version
cargo --version
rustup --version
```
### Updating Rust
```bash
rustup update
rustup update nightly
rustup show
```
### Installing additional toolchains
For occasional use of nightly features (e.g., benchmarks, specific unstable features):
```bash
rustup toolchain install nightly
rustup run nightly cargo build
rustup override set nightly
```
---
## Global Rust tooling
Install these once — they're used across all Rust projects:
```bash
# Fast replacement for cargo-audit, watches files, reruns tests/build
cargo install cargo-watch
# Dependency security audit (checks against RustSec advisories)
cargo install cargo-audit
# Faster incremental builds
cargo install cargo-nextest
# Cargo extension that shows a dependency tree
cargo install cargo-tree 2>/dev/null || true
# cargo-edit — adds `cargo add`, `cargo rm`, `cargo upgrade`
# NOTE: `cargo add` is built-in as of 1.62, but cargo-edit adds `upgrade` and more
cargo install cargo-edit
# cargo-outdated — shows outdated deps
cargo install cargo-outdated
```
All of these land in `~/.cargo/bin/`, which is on your PATH from the `. "$HOME/.cargo/env"` line.
---
## VS Code extensions for Rust
```bash
# The official rust-analyzer language server (formerly RLS, which is deprecated)
code --install-extension rust-lang.rust-analyzer
# CodeLLDB — debugger that works natively on Apple Silicon
code --install-extension vadimcn.vscode-lldb
# crates — shows latest versions of crates in Cargo.toml
code --install-extension serayuzgur.crates
# Even Better TOML (also useful for general dev — you may already have it)
code --install-extension tamasfe.even-better-toml
```
> [!info] rust-analyzer vs "Rust" extension
> The extension named just "Rust" (previously maintained by the Rust team via RLS) is deprecated. `rust-analyzer` replaced it years ago and is now the official choice — published by `rust-lang.rust-analyzer`. If you see tutorials recommending the "Rust" extension by rust-lang, they're outdated.
---
## VS Code settings
Append these language-specific settings to your existing `settings.json` at `~/Library/Application Support/Code/User/settings.json`:
```json
{
// ── Rust ──────────────────────────────────────────────────
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.formatOnSave": true,
"editor.tabSize": 4,
"editor.rulers": [100]
},
"rust-analyzer.check.command": "clippy",
"rust-analyzer.cargo.features": "all",
"rust-analyzer.inlayHints.chainingHints.enable": true,
"rust-analyzer.inlayHints.parameterHints.enable": true,
"rust-analyzer.inlayHints.typeHints.enable": true,
"rust-analyzer.inlayHints.closureReturnTypeHints.enable": "always",
"rust-analyzer.lens.enable": true,
"rust-analyzer.lens.run.enable": true,
"rust-analyzer.lens.debug.enable": true,
"rust-analyzer.hover.actions.enable": true
}
```
> [!info] `rust-analyzer.check.command: "clippy"`
> By default, `rust-analyzer` uses `cargo check` for on-the-fly diagnostics. Switching to `clippy` runs the linter continuously, so you get style and correctness warnings (like "this could be simplified", "unused import", "collapsible if") inline as you type.
---
## Full demo: A calculator program
This walks through building, testing, debugging, and publishing a four-function calculator from scratch. Parallel implementations exist in C, Python, Ruby, and Go.
### Create the project
```bash
cd ~/projects
cargo new --lib calc-rust
cd calc-rust
# Add a binary target for the CLI
mkdir -p src/bin
```
`cargo new --lib` creates a library crate. Structure:
```
calc-rust/
├── Cargo.toml
├── .gitignore
└── src/
├── lib.rs
└── bin/
```
### `Cargo.toml`
```toml
[package]
name = "calc"
version = "0.1.0"
edition = "2021"
[lib]
name = "calc"
path = "src/lib.rs"
[[bin]]
name = "calc"
path = "src/bin/calc.rs"
[[bin]]
name = "calc-gui"
path = "src/bin/calc_gui.rs"
[dependencies]
# (GUI dep added in the GUI section)
[dev-dependencies]
```
### `src/lib.rs` — the core library
```rust
//! Four-function calculator library.
use std::fmt;
#[derive(Debug, PartialEq)]
pub enum CalcError {
DivideByZero,
UnknownOperator(String),
ParseError(String),
}
impl fmt::Display for CalcError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CalcError::DivideByZero => write!(f, "division by zero"),
CalcError::UnknownOperator(op) => write!(f, "unknown operator '{op}'"),
CalcError::ParseError(s) => write!(f, "cannot parse '{s}' as a number"),
}
}
}
impl std::error::Error for CalcError {}
pub fn add(a: f64, b: f64) -> f64 { a + b }
pub fn sub(a: f64, b: f64) -> f64 { a - b }
pub fn mul(a: f64, b: f64) -> f64 { a * b }
pub fn div(a: f64, b: f64) -> Result<f64, CalcError> {
if b == 0.0 {
Err(CalcError::DivideByZero)
} else {
Ok(a / b)
}
}
/// Dispatch to the appropriate operation based on `op`.
/// Supported operators: "+", "-", "*", "x", "/".
pub fn calculate(a: f64, op: &str, b: f64) -> Result<f64, CalcError> {
match op {
"+" => Ok(add(a, b)),
"-" => Ok(sub(a, b)),
"*" | "x" => Ok(mul(a, b)),
"/" => div(a, b),
_ => Err(CalcError::UnknownOperator(op.to_string())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(calculate(6.0, "+", 7.0), Ok(13.0));
}
#[test]
fn test_sub() {
assert_eq!(calculate(10.0, "-", 3.0), Ok(7.0));
}
#[test]
fn test_mul_star() {
assert_eq!(calculate(4.0, "*", 5.0), Ok(20.0));
}
#[test]
fn test_mul_x() {
assert_eq!(calculate(4.0, "x", 5.0), Ok(20.0));
}
#[test]
fn test_div() {
assert_eq!(calculate(20.0, "/", 4.0), Ok(5.0));
}
#[test]
fn test_div_by_zero() {
assert_eq!(calculate(5.0, "/", 0.0), Err(CalcError::DivideByZero));
}
#[test]
fn test_unknown_operator() {
assert!(matches!(
calculate(1.0, "?", 2.0),
Err(CalcError::UnknownOperator(_))
));
}
// Parametrized-style: table-driven tests (a Rust idiom)
#[test]
fn test_operations_table() {
let cases: &[(f64, &str, f64, f64)] = &[
(1.0, "+", 1.0, 2.0),
(0.0, "+", 0.0, 0.0),
(-1.0, "+", 1.0, 0.0),
(1.5, "+", 2.5, 4.0),
(10.0, "-", 20.0, -10.0),
(3.0, "*", 4.0, 12.0),
];
for (a, op, b, expected) in cases {
assert_eq!(
calculate(*a, op, *b),
Ok(*expected),
"calculate({a}, {op:?}, {b}) should equal {expected}"
);
}
}
}
```
### `src/bin/calc.rs` — the CLI
```rust
//! Command-line entry point for the four-function calculator.
use std::env;
use std::process::ExitCode;
use calc::calculate;
fn usage(prog: &str) {
eprintln!("Usage: {prog} <number> <op> <number>");
eprintln!(" op: + - * /");
eprintln!("Example: {prog} 6 + 7");
}
fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
if args.len() != 4 {
usage(&args[0]);
return ExitCode::from(1);
}
let a: f64 = match args[1].parse() {
Ok(v) => v,
Err(_) => {
eprintln!("Error: cannot parse '{}' as a number", args[1]);
return ExitCode::from(1);
}
};
let b: f64 = match args[3].parse() {
Ok(v) => v,
Err(_) => {
eprintln!("Error: cannot parse '{}' as a number", args[3]);
return ExitCode::from(1);
}
};
match calculate(a, &args[2], b) {
Ok(result) => {
// Print as integer if it came out whole
if result == (result as i64) as f64 {
println!("{}", result as i64);
} else {
println!("{result}");
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("Error: {e}");
ExitCode::from(1)
}
}
}
```
### `.gitignore`
Cargo generates a minimal one. Append if needed:
```
target/
Cargo.lock
.DS_Store
```
> [!info] `Cargo.lock` — commit it?
> For **binary** crates (i.e., you distribute compiled executables), commit `Cargo.lock` for reproducible builds. For **library** crates (other projects depend on yours), don't commit it — downstream users need their own resolution. This project is a binary, so keep it in git.
### Build and run
```bash
# Run the CLI (debug build, fast to compile)
cargo run --bin calc -- 6 + 7
cargo run --bin calc -- 10 - 3
cargo run --bin calc -- 4 x 5
cargo run --bin calc -- 20 / 4
# Release build (optimized, slower to compile)
cargo build --release
./target/release/calc 6 + 7
# Install to ~/.cargo/bin/ so `calc` is callable anywhere
cargo install --path . --bin calc
# Run tests
cargo test
# Or with nextest for faster parallel runs (if you installed cargo-nextest)
cargo nextest run
# Lint
cargo clippy -- -D warnings
# Format all files in place
cargo fmt
# Check formatting without modifying files
cargo fmt --check
# Full quality gate (common in CI)
cargo fmt --check && cargo clippy -- -D warnings && cargo test
```
### Debug in VS Code
With `vadimcn.vscode-lldb` installed, CodeLens appears above each `fn main()` and each `#[test]` with "Run | Debug" buttons. Click **Debug** to step through.
For the CLI with arguments, create `.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug calc CLI",
"type": "lldb",
"request": "launch",
"cargo": {
"args": ["build", "--bin=calc"],
"filter": { "name": "calc", "kind": "bin" }
},
"args": ["6", "+", "7"],
"cwd": "${workspaceFolder}"
},
{
"name": "Debug unit tests",
"type": "lldb",
"request": "launch",
"cargo": {
"args": ["test", "--no-run", "--lib"],
"filter": { "name": "calc", "kind": "lib" }
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}
```
Set a breakpoint, hit **F5**, step through native arm64 code with full variable inspection.
### Publish to GitHub
Same workflow as any project (see [[General_Development_Mac_Tahoe_Setup#Publishing a repo to GitHub]]):
```bash
git init
git add .
git commit -m "Initial commit: four-function calculator"
gh repo create calc-rust --private --source=. --remote=origin --push
```
### `CLAUDE.md` for this project
```markdown
# Project: calc-rust
## Purpose
Four-function command-line calculator in Rust — pedagogical example.
## Conventions
- Rust 2021 edition, stable toolchain
- Library (src/lib.rs) + binaries (src/bin/)
- Tests colocated with code via #[cfg(test)] mod tests
- Format with rustfmt (default style)
- Lint with clippy — treat warnings as errors in CI
## Commands
- Run CLI: `cargo run --bin calc -- <a> <op> <b>`
- Run GUI: `cargo run --bin calc-gui`
- Build release: `cargo build --release`
- Install: `cargo install --path . --bin calc`
- Test: `cargo test` (or `cargo nextest run` for parallel)
- Lint: `cargo clippy -- -D warnings`
- Format: `cargo fmt`
- Quality gate: `cargo fmt --check && cargo clippy -- -D warnings && cargo test`
## Style
- 4-space indent, 100-column line length
- snake_case for functions and variables, CamelCase for types
- Return `Result<T, E>` with a concrete error enum for recoverable errors
- Use `#[derive(Debug, PartialEq)]` on public types where possible
- Doc comments (`///`) on every public item
```
---
## Full demo: A calculator GUI
Same calculator, wrapped in a graphical interface. This uses **iced**, a pure-Rust, Elm-inspired GUI toolkit — the most popular Rust GUI library in 2026.
### Why iced?
| Option | Pros | Cons |
|--------|------|------|
| **iced** | Pure Rust, Elm architecture (predictable state), native-feeling, actively developed | Still pre-1.0 (API shifts between versions) |
| **egui** | Immediate-mode, great for tools/debug UIs, very simple | Custom widget look — not native |
| **Slint** | Declarative DSL, compiles to native, excellent perf | Custom language (`.slint` files) — not pure Rust |
| **Tauri** | HTML/JS frontend + Rust backend | Bundles a webview |
| **GTK-rs** | Native GTK widgets | Dated on macOS, heavy dependencies |
For a pedagogical calculator in Rust, iced is the right default. The Elm architecture (Model + Update + View) makes the state machine obvious and unit-testable.
### Add iced as a dependency
Add to `Cargo.toml`:
```toml
[dependencies]
iced = "0.13"
```
Or use `cargo-edit`:
```bash
cargo add iced
```
### `src/bin/calc_gui.rs` — the GUI
```rust
//! Graphical four-function calculator using iced.
use iced::widget::{button, column, container, row, text, text_input};
use iced::{Element, Length, Task, Theme};
use calc::{calculate, CalcError};
pub fn main() -> iced::Result {
iced::application("Calculator", Calculator::update, Calculator::view)
.theme(|_| Theme::Dark)
.window_size((260.0, 360.0))
.run()
}
#[derive(Debug, Clone)]
pub enum Message {
Press(String),
Clear,
}
#[derive(Default)]
pub struct Calculator {
current: String,
stored: Option<f64>,
pending_op: Option<String>,
display: String,
}
impl Calculator {
pub fn new() -> Self {
Self {
display: "0".to_string(),
..Default::default()
}
}
pub fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Press(label) => self.handle_press(&label),
Message::Clear => self.clear(),
}
Task::none()
}
pub fn handle_press(&mut self, label: &str) {
match label {
d if d.chars().all(|c| c.is_ascii_digit() || c == '.') => {
self.current.push_str(d);
self.display = self.current.clone();
}
"+" | "-" | "*" | "/" => {
self.apply_pending();
self.pending_op = Some(label.to_string());
}
"=" => {
self.apply_pending();
self.pending_op = None;
}
_ => {}
}
}
pub fn clear(&mut self) {
self.current.clear();
self.stored = None;
self.pending_op = None;
self.display = "0".to_string();
}
fn apply_pending(&mut self) {
if self.current.is_empty() {
return;
}
let value: f64 = match self.current.parse() {
Ok(v) => v,
Err(_) => {
self.display = "Error".to_string();
self.reset_internal();
return;
}
};
let new_value = match (self.stored, self.pending_op.as_deref()) {
(None, _) | (_, None) => value,
(Some(s), Some(op)) => match calculate(s, op, value) {
Ok(v) => v,
Err(_) => {
self.display = "Error".to_string();
self.reset_internal();
return;
}
},
};
self.stored = Some(new_value);
// Format: integer if whole
self.display = if new_value == (new_value as i64) as f64 {
(new_value as i64).to_string()
} else {
format!("{new_value}")
};
self.current.clear();
}
fn reset_internal(&mut self) {
self.current.clear();
self.stored = None;
self.pending_op = None;
}
pub fn view(&self) -> Element<Message> {
let display = text_input("0", &self.display)
.size(28)
.align_x(iced::alignment::Horizontal::Right);
let btn = |label: &str| -> Element<Message> {
button(text(label).size(18).center())
.width(Length::Fill)
.padding(12)
.on_press(Message::Press(label.to_string()))
.into()
};
let grid = column![
row![btn("7"), btn("8"), btn("9"), btn("/")].spacing(5),
row![btn("4"), btn("5"), btn("6"), btn("*")].spacing(5),
row![btn("1"), btn("2"), btn("3"), btn("-")].spacing(5),
row![btn("0"), btn("."), btn("="), btn("+")].spacing(5),
]
.spacing(5);
let clear = button(text("Clear").center())
.width(Length::Fill)
.padding(10)
.on_press(Message::Clear);
container(
column![display, grid, clear]
.spacing(10)
.padding(10),
)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn press_sequence(presses: &[&str]) -> Calculator {
let mut c = Calculator::new();
for p in presses {
c.handle_press(p);
}
c
}
#[test]
fn initial_display() {
let c = Calculator::new();
assert_eq!(c.display, "0");
}
#[test]
fn single_digit() {
let c = press_sequence(&["5"]);
assert_eq!(c.display, "5");
}
#[test]
fn multi_digit() {
let c = press_sequence(&["1", "2", "3"]);
assert_eq!(c.display, "123");
}
#[test]
fn simple_addition() {
let c = press_sequence(&["6", "+", "7", "="]);
assert_eq!(c.display, "13");
}
#[test]
fn chained_operations() {
// 2 + 3 * 4 → (2+3)*4 = 20 (left-to-right)
let c = press_sequence(&["2", "+", "3", "*", "4", "="]);
assert_eq!(c.display, "20");
}
#[test]
fn division() {
let c = press_sequence(&["2", "0", "/", "4", "="]);
assert_eq!(c.display, "5");
}
#[test]
fn division_by_zero() {
let c = press_sequence(&["5", "/", "0", "="]);
assert_eq!(c.display, "Error");
}
#[test]
fn clear_resets_state() {
let mut c = press_sequence(&["5", "+", "3"]);
c.clear();
assert_eq!(c.display, "0");
assert!(c.current.is_empty());
assert!(c.stored.is_none());
assert!(c.pending_op.is_none());
}
#[test]
fn decimal_input() {
let c = press_sequence(&["1", ".", "5", "+", "2", ".", "5", "="]);
assert_eq!(c.display, "4");
}
}
```
### Run everything
```bash
# CLI
cargo run --bin calc -- 6 + 7
# GUI (may take a while to compile the first time — iced pulls in winit, wgpu, etc.)
cargo run --bin calc-gui
# All tests (library + GUI state machine)
cargo test
# Release builds
cargo build --release
./target/release/calc-gui
```
The first iced build is slow (several minutes) because it compiles the whole graphics stack — winit for windowing, wgpu for rendering. Subsequent builds are cached and fast.
> [!info] Shipping iced apps
> For a distributable macOS `.app` bundle with icon and code signing, use `cargo bundle`:
> ```bash
> cargo install cargo-bundle
> cargo bundle --release
> ```
> This produces `target/release/bundle/osx/Calculator.app` ready for `codesign` and `notarytool`.
---
## Starship prompt — Rust auto-detection
The general Starship config you set up already includes the `[rust]` module. When you `cd` into this project, the prompt shows:
```
~/projects/calc-rust main 1.83.0 ❯
```
Reading left-to-right: directory, git branch, Rust version.
---
## Installing CLI tools from other Rust projects
Cargo's `cargo install` is the equivalent of `uv tool install` or `go install` — it installs binaries globally (to `~/.cargo/bin/`) from any Rust project on crates.io or from a git URL:
```bash
# Install ripgrep from crates.io (you may already have it via brew)
cargo install ripgrep
# Install bat (cat clone with syntax highlighting)
cargo install bat
# Install tokei (fast code counter)
cargo install tokei
# Install from a git repo
cargo install --git https://github.com/you/your-tool.git
```
These land in `~/.cargo/bin/` and are callable by name anywhere.
> [!info] Cargo vs Homebrew for Rust tools
> Many popular Rust CLIs (ripgrep, bat, fd, eza) are available via both `brew` and `cargo install`. `brew` gives you precompiled binaries and faster installation; `cargo install` compiles from source each time but gives you the latest version with your local CPU's optimizations. For daily use, prefer `brew`. Use `cargo install` when Homebrew doesn't carry the tool, or when you want a specific version.
---
## Troubleshooting
> [!warning] `rustc: command not found` after installing
> The installer adds `. "$HOME/.cargo/env"` to your shell config. Verify with `grep cargo/env ~/.zshrc`. If missing, add it and open a fresh terminal. Don't `source` it manually in a running shell that was open before the install — just restart the terminal.
> [!warning] Very slow first build of a GUI project
> iced (and egui, and Slint) pull in substantial graphics dependencies (winit, wgpu, glyphon). The first `cargo build` for a GUI project typically takes 3–8 minutes. Subsequent builds are incremental and take seconds. This is normal — the whole graphics stack is compiled from source once and cached under `target/`.
> [!warning] `rust-analyzer` seems unresponsive in VS Code
> Open a terminal and run `cargo check` first. rust-analyzer uses cargo under the hood — if that's stuck indexing or building, VS Code can appear frozen. Also check the "Rust Analyzer Language Server" output panel in VS Code for errors.
> [!warning] `cargo build` error: "linker `cc` not found"
> You need the Xcode Command Line Tools (provides `cc`/`clang`). Run `xcode-select --install`. This is a prerequisite from the general setup and should already be installed.
> [!warning] `cargo test` failing with "error: could not compile"
> Read the first error carefully — Rust's compiler messages are famously good. Common causes: forgotten `&` on borrows, mismatched types, missing `use` statements, and lifetime issues. rust-analyzer in VS Code catches most of these before you hit `cargo test`.
---
## Summary — the one-shot Rust addition
> [!warning] This is a checklist, not a script
> The block below is **not a shell script you can save and run.** It's a sequence of commands to **copy and paste into your terminal one block at a time**, in order. The first step (`curl ... | sh`) **runs the interactive rustup installer** — you have to read its prompts and respond before the rest will work. The later `cargo install` commands compile from source and can take 1–3 minutes each. After running these, you also need to **append the recommended Rust settings block** from earlier in this guide to `~/Library/Application Support/Code/User/settings.json`.
```bash
# Rust toolchain (interactive installer — accept defaults)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
. "$HOME/.cargo/env"
# Global Cargo tooling
cargo install cargo-watch cargo-audit cargo-nextest cargo-edit cargo-outdated
# VS Code extensions
code --install-extension rust-lang.rust-analyzer
code --install-extension vadimcn.vscode-lldb
code --install-extension serayuzgur.crates
# Paste the Rust block into ~/Library/Application Support/Code/User/settings.json
# Verify
rustc --version && cargo --version && rustup --version
```
About five minutes of install time on top of the general setup (the `cargo install` calls compile from source).
---
## Related notes
- [[General_Development_Mac_Tahoe_Setup]]
- [[Python_Development_Mac_Tahoe_Setup]]
- [[C_Development_Mac_Tahoe_Setup]]
- [[Ruby_Development_Mac_Tahoe_Setup]]
- [[Go_Development_Mac_Tahoe_Setup]]
- [Cargo Cheat Sheet](https://doc.rust-lang.org/cargo/)
- [iced Architecture Notes](https://book.iced.rs/architecture.html)