diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9b97ba5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig — https://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{yml,yaml,json,toml,md}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..467977c --- /dev/null +++ b/.gitignore @@ -0,0 +1,72 @@ +# --- Rust --- +/target/ +**/target/ +Cargo.lock.orig +*.rlib +*.rmeta + +# Rust keeps its downloaded-crate cache here. It grows to hundreds of MB and is +# pure cache: delete it any time, cargo re-fetches. Not in git. +/.cargo-cache/ + +# --- Android / Gradle --- +.gradle/ +**/gradle/ +local.properties +*.apk +*.aab +*.keystore +*.jks +keystore.properties +release-keys/ + +# --- Desktop / release artifacts --- +/dist/ +*.exe +*.msi +*.deb +*.rpm +*.AppImage + +# --- Secrets — never commit --- +.env +.env.* +api_key.txt +*.pem +*.key +*.p12 +*.pfx +secrets.toml +credentials.json +ssh_key* +id_ed25519* +id_rsa* + +# --- Android Studio / IntelliJ / VS Code --- +.idea/ +*.iml +.kotlin/ +.cxx/ +.vscode/* +!.vscode/extensions.json + +# --- Local databases (never commit runtime state) --- +*.db +*.db-journal +*.db-wal +*.db-shm +*.sqlite +*.sqlite3 + +# --- OS cruft --- +.DS_Store +Thumbs.db +*~ +*.swp + +# --- Local scratch --- +/tmp/ +*.log + +# Claude Code local scheduler state — machine-specific, not project config +.claude/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bbe6de9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,294 @@ +# CLAUDE.md — "Blocker": cross-platform app & website blocker + +Functional equivalent of AppBlock (appblock.app) for **Android, Windows, Linux**. +Goal: same behavior, near-zero resource usage. Own name, own branding, own assets. + +## 0. Rules for the agent (read first) + +1. **Functional parity, not visual copying.** Do not reuse AppBlock's name, logo, icons, copy, or screenshots. Replicate behavior only. +2. **Never fake a feature.** If a platform cannot enforce something (see §6), implement the closest honest approximation, document the gap in `docs/LIMITATIONS.md`, and tell me. Do not silently stub. +3. **Stop and ask** before: adding any dependency not listed in §3, changing the architecture in §2, or choosing between two non-obvious OS mechanisms. +4. **Work in milestones (§8).** Finish one, run its acceptance checks, then stop and report. No jumping ahead. +5. **Every OS call that can fail must be handled.** No `unwrap()` / `!!` on OS or I/O results outside tests. +6. **Performance is a feature** (§5). Measure it; don't assume it. +7. **Everything goes through git** on my Gitea (§10). Never work outside a branch; never commit to `main` directly. +8. **Turning blocking off must be deliberately painful (§1.5), but never impossible and never unsafe.** The friction is opt-in, chosen by the user at activation time, always finite, and never touches the safety exemptions in §1.5.4. + +## 1. Feature spec (target: 1:1 behavior) + +### 1.1 Schedules (the core object) +A schedule = +- name + icon (user-chosen) +- **targets**: apps, websites, keywords +- **one or more conditions** (see 1.2) +- **mode**: `Blocklist` (listed items blocked) or `Allowlist` (only listed apps allowed; apps only, not websites) +- enabled / paused state + +Rules: +- If multiple schedules touch the same target, **strictest wins**: blocked while ≥1 schedule wants it blocked. +- Schedules are editable at any time **unless Strict Mode is active**. +- Ship starter templates: Focus, Wind Down, Mindful Morning, Study Time, At Home, Digital Detox. + +### 1.2 Conditions +| Condition | Behavior | +|---|---| +| Time | days of week + start/end time (must support overnight ranges) | +| Usage limit | block after N minutes per day or per hour | +| Launch count | block after N opens per day or per hour | +| Location | block inside (or outside) a radius of a point — mobile only | +| Wi-Fi | block while connected to a chosen SSID | + +Combination rules (matches AppBlock; enforce in validation): +- Time + usage limit: **not allowed** +- Time + launch count: **not allowed** +- Usage limit + launch count: **not allowed** +- Usage limit + location, launch count + location: **not allowed** +- Use separate schedules to combine these (strictest-wins handles it). + +### 1.3 Blocking behavior +- **Apps:** when a blocked app comes to the foreground, show a block screen immediately and send the user away from it. +- **Websites:** block domains and keyword matches in browsers. +- **QuickBlock:** one-tap immediate block of a chosen set for a chosen duration. +- **Pause / break:** temporarily pause a schedule (disabled or restricted under Strict Mode). +- **Block screen:** shows which schedule blocked it and when it ends; no bypass button while Strict Mode is on. + +### 1.4 Strict Mode +Requires at least one schedule or QuickBlock to exist. When active, the user cannot: +- edit or delete **any** schedule (even if Strict Mode was tied to one — all are locked) +- pause schedules / QuickBlock +- uninstall the app (Android: Device Admin; desktop: see §6) +- open device settings pages that would let them disable enforcement (configurable) + +Configurable parts: +- which restrictions to lock (checklist) +- **unlock condition** chosen at activation time (timer end, cooldown, etc.) +- **cooldown**: 2–10 minute wait on the Strict Mode screen before deactivation +- optional "block uninstall" even when Strict Mode is off + +### 1.5 Friction: making it a pain to turn off + +Design goal: disabling blocking in a weak moment must cost enough time and effort that the urge passes, while a determined, calm decision still gets through eventually. This goes **beyond** AppBlock's Strict Mode and is the main differentiator of this project. + +**1.5.1 Lock levels** — chosen at activation, shown with a plain-language description of exactly what it will cost to undo: + +| Level | Disable flow | +|---|---| +| Normal | edit/pause freely | +| Strict | AppBlock-equivalent Strict Mode (§1.4) | +| Stubborn | Strict + the full friction ladder below | + +Rules for levels: +- A level can be **raised instantly** but **lowered only by going through that level's disable flow**. +- The user must see and confirm the exact disable cost before activating ("To undo this you will wait 24h and retype a 300-character code"). + +**1.5.2 The friction ladder** (each step is individually configurable; Stubborn enables all): + +1. **Cooldown:** configurable wait (default 10 min, up to 30) on the disable screen. Leaving the screen resets it. +2. **Time-lock request:** disabling doesn't disable; it files a *request*. Blocking stays fully active for the configured delay (default 12 h, max 24 h). When the delay ends, a short confirmation window opens (e.g. 10 min). Missing the window cancels the request and starts over. The user can cancel the request any time. +3. **Typing challenge:** retype a random string (default 300 chars, mixed case + digits). Paste disabled, no autofill/suggestions, and any typo resets it. Regenerate a new string on each attempt. +4. **Escalation:** each abandoned or failed attempt doubles the next cooldown (cap at 30 min per step; resets after 7 days with no attempts). +5. **Attempt log:** every disable attempt, with timestamp, is shown to the user on the block screen ("You tried to disable this 4 times today"). Local only. +6. **No shortcuts:** no notification action, quick tile, widget, or tray menu item that disables, pauses, or opens the disable flow. The flow lives only inside the app, behind a plain-looking entry. +7. **Optional keyholder:** at activation the app generates a one-time unlock code and shows it **once** so the user can hand it to someone they trust (or print it and give it away). Entering it skips steps 1–4. If the user declines this option, the ladder still applies. + +**1.5.3 Anti-tamper requirements (the engine must enforce these)** +- All delay timers use a **monotonic clock** (Android `elapsedRealtime`, `CLOCK_MONOTONIC`, `QueryPerformanceCounter`). Changing the system date/time must never shorten a delay. If a wall-clock jump is detected, log it and **extend** the active lock, never shorten it. +- Timer state survives reboot (persist elapsed-so-far with a boot ID; if reboot detected, resume conservatively). +- Force-stopping, killing, or disabling the enforcer is detected and repaired where the OS allows (§4, §6). Platforms that can't prevent it must still log it and re-apply blocks on next start. +- Schedule/Strict state is stored in the core DB, not in editable plaintext config. + +**1.5.4 Safety guardrails (non-negotiable, these protect the user)** +- **Never-block list**, hardcoded and not user-removable from the blocklist: phone/dialer, emergency calls/SOS, emergency alerts, and on desktop the OS's own shell and Settings basics needed to keep the machine usable. Surface them in docs. +- **Finite worst case:** the longest possible disable path is capped at 24 h of waiting. Never design a lock with no exit. +- **Factory reset, safe mode, and OS-level recovery are never interfered with.** Do not attempt to block them. +- **Opt-in only:** Stubborn/Strict are never enabled by default or by a schedule template; the user turns them on explicitly with the cost shown. +- The disable flow must be documented in-app (a plain "How to turn this off" page reachable *without* disabling anything), so the user is never stuck wondering. +- If the app itself malfunctions (service crash loop, corrupt DB), fail **open** after logging, never leave the device unusable. + +### 1.6 Statistics +Per-app and per-website usage tracking (daily/weekly), computed locally. + +### 1.7 Settings +Dark/light theme, behavior when location is unavailable, which settings stay accessible under Strict Mode, troubleshooting screen (shows which permissions/services are missing). + +### 1.8 Out of scope for v1 +iOS, cloud accounts, cross-device sync, billing/premium tiers (all features are free), browser extensions beyond the §6 website mechanism. Design the storage so sync could be added later; do not build it. + +## 2. Architecture + +One **shared core** holds all decision logic so behavior is identical on every OS. Platform code only (a) observes the OS and (b) enforces decisions. + +``` +┌────────────────────────── core (Rust, no_std-friendly where possible) ──────────────────────────┐ +│ model: Schedule, Condition, Target, StrictConfig │ +│ engine: evaluate(state, now, events) -> Decision{blocked set, reasons, next_wakeup} │ +│ storage: SQLite (rusqlite, bundled) — schedules, usage counters, settings │ +│ usage accounting: foreground-time & launch-count aggregation │ +└──────────────▲───────────────────────────────────────────────────▲───────────────────────────┘ + │ uniffi (Kotlin bindings) │ direct Rust crate + ┌──────┴───────┐ ┌─────────────┴──────────────┐ + │ android/ │ │ desktop/ (win + linux) │ + │ Kotlin shell │ │ daemon + small UI │ + └──────────────┘ └────────────────────────────┘ +``` + +Key principle: **the engine is a pure function** of `(schedules, usage state, wall clock, context like SSID/location)`. No OS calls inside it. This makes it unit-testable and guarantees parity. + +Engine must be **event/alarm driven, never polling.** It returns `next_wakeup` (the next time any decision could change); the platform layer sleeps until then or until an OS event arrives. + +Repo layout: +``` +/core Rust crate (engine, model, storage, usage) +/android Kotlin app (thin: services, overlay, UI) +/desktop Rust workspace: /daemon, /ui, /platform-windows, /platform-linux +/docs ARCHITECTURE.md, LIMITATIONS.md, PERMISSIONS.md, DECISIONS.md, PERF.md, TESTING.md +/tests cross-platform scenario tests (golden decision tables) +/scripts ci.sh (same checks as CI, runnable locally), size-check, release helpers +/.gitea workflows/, issue + PR templates +``` + +## 3. Language & dependency choices + +| Layer | Choice | Why | +|---|---|---| +| Core + desktop | **Rust** | no GC, tiny idle footprint, one codebase for logic | +| Android shell | **Kotlin** (no Compose, no DI framework, no Jetpack extras beyond what's required) | The blocking APIs (AccessibilityService, UsageStatsManager, DevicePolicyManager, overlays) are JVM-only; a non-JVM language can't register these services. The shell stays thin, so performance is dominated by the Rust core | +| Core ↔ Android | `uniffi` (or hand-written JNI if uniffi bloats size — ask) | | +| Storage | `rusqlite` with bundled SQLite | | +| Desktop UI | `slint` (preferred) or `egui` — decide in M3 after a size/RAM test, ask me | far lighter than Electron/Tauri webviews | +| Windows API | `windows` crate | | +| Linux | `x11rb` (X11), `zbus` (D-Bus/logind/NetworkManager), `nix` | | + +Rules: no Electron, no webview, no Flutter, no React Native, no JVM on desktop. Prefer std over crates. Every added crate must justify its binary-size cost. + +## 4. Platform enforcement design + +### 4.1 Android +- **Foreground detection:** `AccessibilityService` (`TYPE_WINDOW_STATE_CHANGED`) — event-driven, no polling. Use `UsageStatsManager` only for historical/usage stats and as a fallback. +- **Block action:** launch block screen activity / overlay (`SYSTEM_ALERT_WINDOW`) and send user Home. +- **Websites:** Accessibility node reading of the browser URL bar for known browsers (keep a per-browser resource-id table, make it updatable); match domains + keywords. +- **Strict Mode / uninstall protection:** `DeviceAdminReceiver` for uninstall block; Accessibility service intercepts Settings screens that would disable the service. +- **Location / Wi-Fi:** Geofencing API for location; `ConnectivityManager` network callback for SSID. +- **Reliability:** foreground service with persistent notification, battery-optimization exemption prompt, `BOOT_COMPLETED` receiver, OEM autostart guidance in the troubleshooting screen. +- **Permissions screen** guides the user through: Usage access, Accessibility, Display over other apps, Battery optimization, Location (optional), Autostart (OEM-specific). +- Play Store note: Accessibility and Device Admin use require policy justification. Also provide a sideload/F-Droid build flavor. + +### 4.2 Windows +- **Foreground detection:** `SetWinEventHook(EVENT_SYSTEM_FOREGROUND)` — event-driven. Map HWND → PID → exe path (and package identity for UWP apps). +- **Block action:** show a topmost block window and minimize/close the offending window; terminate the process only as an escalation, configurable. +- **Usage/launch counting:** from the same foreground events + `EVENT_OBJECT_CREATE`/process start via WMI or ETW (choose lightest, ask me). +- **Runs as:** per-user background daemon at login + a Windows Service watchdog that restarts it. +- **Websites:** see §6 (this is the weak point). +- **Wi-Fi:** WLAN API notification callback. + +### 4.3 Linux +- **X11:** `_NET_ACTIVE_WINDOW` property-change events via `x11rb` → `_NET_WM_PID` → `/proc//exe`. +- **Wayland:** there is **no portable global API** for the focused window. Implement compositor-specific backends behind a trait: wlroots `foreign-toplevel-management`, KDE (KWin script/D-Bus), GNOME (extension). Fallback: **process-based blocking** — watch process creation (netlink proc connector, needs privilege) and match by executable. +- **Block action:** overlay window (X11) / layer-shell (wlroots) / terminate process as fallback. +- **Runs as:** `systemd --user` service; optional system service for tamper resistance. +- **Wi-Fi:** NetworkManager via D-Bus. + +## 5. Performance budgets (must be measured and reported per milestone) + +| Metric | Android | Windows / Linux daemon | +|---|---|---| +| Idle CPU | ~0% (no wakeups without events) | ~0%, ≤ 1 wakeup/min when idle | +| Idle RAM | ≤ 30 MB PSS | ≤ 15 MB RSS (daemon), UI process closed when window closed | +| Block latency (foreground change → block shown) | ≤ 150 ms | ≤ 150 ms | +| Daemon binary | — | ≤ 5 MB stripped (`opt-level="z"`, LTO, `panic="abort"`, `strip`) | +| APK size | ≤ 10 MB (release, R8) | — | +| Battery | no wakelocks outside active enforcement | — | + +How to measure: `adb shell dumpsys meminfo`, Perfetto/battery stats (Android); `psutil`/`perf`/`heaptrack` (Linux); Process Explorer/ETW (Windows). Put results in `docs/PERF.md`. + +## 6. Known hard limits (tell me, don't hide them) + +1. **Desktop Strict Mode cannot be tamper-proof against an admin/root user.** Mitigations: watchdog service, file ACLs / immutable flag on config, restart-on-kill, tamper log. Document honestly in `LIMITATIONS.md`. Do not claim "unbypassable." +2. **Desktop website blocking has no single clean mechanism.** Evaluate and report options before building: (a) local DNS resolver / hosts-file management, (b) browser extension via native messaging (needed for keyword and path-level blocking, and HTTPS-aware), (c) both. DNS-only cannot do keyword blocking or defeat DoH. Ask me which to ship. +3. **Wayland** focused-window detection is compositor-dependent (§4.3). +4. **Location schedules** are mobile-only (no reliable desktop equivalent); hide that condition on desktop. +5. **Android OEMs** (Xiaomi, Huawei, Samsung, etc.) kill background services aggressively; troubleshooting screen must cover this. +6. **Android Accessibility URL reading** breaks when browsers change their UI; the resource-id table needs a maintenance path. +7. **"Pain to turn off" is friction, not a wall.** A user with ADB, root, safe mode, a factory reset, or admin rights on desktop can always remove the app. That's correct and must stay true (§1.5.4). The goal is to outlast an impulse, not to defeat a determined person. Do not try to block ADB, safe mode, or recovery. +8. **Play Store policy** is hostile to apps that resist uninstall. Keep the strongest friction (Stubborn level, Device Admin uninstall block) behind the explicit opt-in and plan on a sideload/F-Droid flavor as the primary distribution. + +## 7. Testing + +- **Engine:** property tests + golden decision tables in `/tests` — same table runs against the core and is the parity contract for all platforms. Cover: overnight time ranges, DST changes, timezone changes, clock tampering (use monotonic time for usage accounting), strictest-wins, allowlist mode, invalid condition combos, Strict Mode lock rules. +- **Platform layers:** manual test checklist per OS in `docs/TESTING.md` plus automated tests where feasible (Android instrumented tests, Linux Xvfb tests). +- A milestone isn't done until its acceptance checks pass **and** perf numbers are recorded. + +## 8. Milestones + +**M0 — Repo bootstrap (git first).** +Create the repo on my Gitea, set up layout, `.gitignore`, `scripts/ci.sh`, CI workflow skeleton, issue/PR templates, `docs/DECISIONS.md`. File every §6 limitation as an issue. *Accept:* clean clone builds an empty workspace; CI (or `scripts/ci.sh` if no runner) passes on `main`. + +**M1 — Core engine (no UI, no OS).** +Model, evaluator, storage, usage accounting, golden tests. *Accept:* all decision-table tests pass; `evaluate` is pure and has no platform imports. + +**M2 — Android MVP.** +Permissions flow, foreground detection, app blocking by Time condition, block screen, QuickBlock. *Accept:* block latency ≤ 150 ms, idle CPU ~0%, survives reboot. + +**M3 — Desktop daemon + UI (Linux/X11 first, then Windows).** +Daemon with app blocking by Time, block window, minimal UI. Pick Slint vs egui here with measured RAM/size. *Accept:* §5 budgets met. + +**M4 — All conditions.** +Usage limit, launch count, Wi-Fi, Location (mobile), validation of combination rules, templates, pause behavior. + +**M5 — Websites & keywords.** +Android via Accessibility; desktop via the mechanism I choose from §6.2. + +**M6 — Strict Mode + friction ladder (§1.4, §1.5).** +Lock rules, lock levels, cooldown, time-lock request, typing challenge, escalation, attempt log, keyholder code, monotonic-clock anti-tamper, never-block list, in-app "How to turn this off" page, uninstall protection (Android Device Admin), desktop watchdog. Write the honest limitations doc. *Accept:* engine tests prove that changing the system clock never shortens a lock, a reboot never resets one, the worst-case disable path is ≤ 24 h, and the never-block list can't be blocked. + +**M7 — Statistics, settings, troubleshooting screen, Wayland backends, release builds, perf report.** + +## 9. Definition of done (per feature) +- Behavior matches §1 on every platform where it's supported +- Golden tests updated +- No new idle wakeups introduced +- Perf budget table re-measured if the feature touches the hot path +- Limitations documented where the platform can't comply + +## 10. Git workflow (everything goes through git) + +**Remote:** my self-hosted Gitea at `https://git.valiq.vu` (Gitea 1.26, so Gitea Actions and the REST API at `/api/swagger` are available). + +**Before first push, ask me for:** the owner/org and repo name, whether to use SSH or an access token, and whether a Gitea Actions runner (`act_runner`) is registered. Never guess these. Never commit tokens, keys, or keystores; read credentials from the environment or my SSH agent. + +### 10.1 What lives in git +Everything that defines the project: source, golden test tables, CI config, scripts, docs, architecture decisions (`docs/DECISIONS.md`), perf measurements (`docs/PERF.md`), the permission/limitation docs, issue and PR templates. + +What does **not**: build outputs, APKs/binaries (attach those to Gitea Releases instead), Android keystores, `.env` files, local SQLite databases, IDE junk. Keep `.gitignore` strict, and run a secret scanner (e.g. `gitleaks`) in `scripts/ci.sh` and CI. + +### 10.2 Branching and commits +- `main` is always green and releasable. **No direct commits to `main`.** +- One short-lived branch per task, named `/`, e.g. `m2/accessibility-foreground`, `m6/time-lock`. +- Small, atomic, **conventional commits** (`feat:`, `fix:`, `perf:`, `refactor:`, `test:`, `docs:`, `ci:`). Each commit must build and pass tests on its own. +- Open a **pull request** on Gitea for each branch (via the API or `tea` CLI) with: what changed, which §1 requirement it covers, test evidence, and perf numbers if it touches a hot path. +- **I merge.** The agent never merges its own PR without my explicit go-ahead. Use squash or rebase merge, keep history linear. +- Tag each finished milestone (`m1`, `m2`, ...) and each release (`v0.1.0`, semver). Tags are annotated. + +### 10.3 Issues as the backlog +- File one Gitea issue per milestone (M0 to M7) with its acceptance checks as a checklist. +- File one issue per §6 hard limit, labeled `limitation`, so none get forgotten. +- Reference issues in commits and PRs (`refs #12`, `closes #12`). +- Any open question for me becomes an issue labeled `question` instead of being buried in chat. + +### 10.4 CI (Gitea Actions, `.gitea/workflows/`) +Every PR must pass: +1. `cargo fmt --check` and `cargo clippy -- -D warnings` +2. `cargo test` including the golden decision tables +3. **Size gate:** fail if the stripped desktop daemon exceeds 5 MB (§5) +4. Android: `lint` and `assembleRelease` (only if the runner has the SDK; ask me) +5. Secret scan + +`scripts/ci.sh` runs the same checks locally, so work is verifiable even if no runner exists. If I have no runner, write the workflows anyway and tell me what runner setup they need. + +### 10.5 Releases +Tagged release builds attach artifacts to a Gitea Release: Android APK (sideload flavor), Windows `.exe`/installer, Linux binary + `.service` unit, each with a SHA-256 checksum file. Signing keys are never in the repo. + +### 10.6 Agent hygiene +- Run `git status` and `git diff` before every commit; no `git add -A` blindly. +- Never rewrite or force-push shared history (`main`, tags). Force-push is allowed only on your own unmerged feature branch. +- If a milestone's checks fail, say so in the PR; don't weaken a test to make it pass. diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..5225afc --- /dev/null +++ b/clippy.toml @@ -0,0 +1,26 @@ +# Clippy is run with `-D warnings` in CI (CLAUDE.md §10.4), so every lint we +# enable here is a build failure. Only turn on lints we are willing to fix. + +[lints.rust] +# Silent truncation of a u64 millisecond count into a u32 is a real bug class in +# this codebase (usage counters, monotonic deltas). Make it impossible to miss. +integer_conversions = "deny" + +[lints.clippy] +# Engine code is mostly pure data transformation; `&self` everywhere reads better +# than `&mut self` and avoids accidental aliasing bugs across the FFI boundary. +needless_pass_by_ref_mut = "warn" + +# Large enum variants waste stack in the hot evaluate() path. +large_enum_variant = "warn" + +# Every OS call returns Result; we handle them all, but catch `.unwrap()` slips. +unwrap_used = "deny" +expect_used = "deny" + +# Panicking in release builds is a last resort — see CLAUDE.md §0.5. +panic = "deny" + +# Prefer clarity over micro-optimisation; perf budgets in §5 are measured with +# real benchmarks, not by guessing at clever rewrites. +too_many_arguments = "warn" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..1a40a18 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,12 @@ +edition = "2021" +max_width = 100 + +# Imports are grouped std / external / crate, and sorted within each group. +imports_granularity = "Crate" +group_imports = "StdExternalCrate" + +# We write a lot of match arms over enums and error plumbing. Keep them tight. +match_block_trailing_comma = false + +# Derive Debug on virtually everything — engine output is logged in the field. +derive_debug = false