From 289aa82dedc3336f3e716036836f3ab6cdf2450a Mon Sep 17 00:00:00 2001 From: Ray Sinurat Date: Fri, 6 Feb 2026 03:31:45 -0600 Subject: [PATCH] chore(docs): add documentations --- crates/doot-cli/src/commands/apply.rs | 1 + crates/doot-cli/src/commands/check.rs | 1 + crates/doot-cli/src/commands/decrypt.rs | 1 + crates/doot-cli/src/commands/diff.rs | 1 + crates/doot-cli/src/commands/edit.rs | 1 + crates/doot-cli/src/commands/encrypt.rs | 1 + crates/doot-cli/src/commands/fmt.rs | 194 ++++++++++++++++++++--- crates/doot-cli/src/commands/init.rs | 1 + crates/doot-cli/src/commands/lsp.rs | 1 + crates/doot-cli/src/commands/mod.rs | 5 + crates/doot-cli/src/commands/package.rs | 3 + crates/doot-cli/src/commands/rollback.rs | 1 + crates/doot-cli/src/commands/snapshot.rs | 1 + crates/doot-cli/src/commands/status.rs | 1 + crates/doot-cli/src/commands/tui.rs | 1 + crates/doot-cli/src/main.rs | 47 +++++- crates/doot-core/src/package/apt.rs | 1 + crates/doot-core/src/package/brew.rs | 1 + crates/doot-core/src/package/pacman.rs | 1 + crates/doot-core/src/package/yay.rs | 1 + 20 files changed, 245 insertions(+), 20 deletions(-) diff --git a/crates/doot-cli/src/commands/apply.rs b/crates/doot-cli/src/commands/apply.rs index ee310c8..20712d9 100644 --- a/crates/doot-cli/src/commands/apply.rs +++ b/crates/doot-cli/src/commands/apply.rs @@ -11,6 +11,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::time::Instant; +/// Applies the dotfile configuration, deploying files and installing packages. #[tracing::instrument(skip_all, fields(dry_run, prune))] pub fn run(config_path: Option, dry_run: bool, prune: bool) -> anyhow::Result<()> { let start = Instant::now(); diff --git a/crates/doot-cli/src/commands/check.rs b/crates/doot-cli/src/commands/check.rs index a7f1e11..fdcf2d1 100644 --- a/crates/doot-cli/src/commands/check.rs +++ b/crates/doot-cli/src/commands/check.rs @@ -1,6 +1,7 @@ use super::{find_config_file, parse_config, type_check}; use std::path::PathBuf; +/// Validates a config file (parse + type check). #[tracing::instrument(skip_all)] pub fn run(config_path: Option) -> anyhow::Result<()> { let path = find_config_file(config_path)?; diff --git a/crates/doot-cli/src/commands/decrypt.rs b/crates/doot-cli/src/commands/decrypt.rs index 0250701..092fc0b 100644 --- a/crates/doot-cli/src/commands/decrypt.rs +++ b/crates/doot-cli/src/commands/decrypt.rs @@ -1,6 +1,7 @@ use doot_core::{Config, encryption::AgeEncryption}; use std::path::PathBuf; +/// Decrypts an age-encrypted file. #[tracing::instrument(skip_all, fields(file = %file.display()))] pub fn run(file: PathBuf, identity: Option) -> anyhow::Result<()> { let config = Config::default(); diff --git a/crates/doot-cli/src/commands/diff.rs b/crates/doot-cli/src/commands/diff.rs index 6f0ecab..5b051e5 100644 --- a/crates/doot-cli/src/commands/diff.rs +++ b/crates/doot-cli/src/commands/diff.rs @@ -5,6 +5,7 @@ use doot_lang::Evaluator; use doot_lang::evaluator::PermissionRule; use std::path::{Path, PathBuf}; +/// Shows diffs between source and deployed dotfiles. #[tracing::instrument(skip_all, fields(all))] pub fn run(config_path: Option, all: bool) -> anyhow::Result<()> { let path = find_config_file(config_path)?; diff --git a/crates/doot-cli/src/commands/edit.rs b/crates/doot-cli/src/commands/edit.rs index 6227776..ef67c5b 100644 --- a/crates/doot-cli/src/commands/edit.rs +++ b/crates/doot-cli/src/commands/edit.rs @@ -9,6 +9,7 @@ use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::process::Command; +/// Opens a deployed file for editing and optionally re-applies. #[tracing::instrument(skip_all, fields(target = %target, auto_apply, skip_prompt))] pub fn run( config_path: Option, diff --git a/crates/doot-cli/src/commands/encrypt.rs b/crates/doot-cli/src/commands/encrypt.rs index 3c0e0f2..1dcd22c 100644 --- a/crates/doot-cli/src/commands/encrypt.rs +++ b/crates/doot-cli/src/commands/encrypt.rs @@ -1,6 +1,7 @@ use doot_core::{Config, encryption::AgeEncryption}; use std::path::PathBuf; +/// Encrypts a file using age encryption. #[tracing::instrument(skip_all, fields(file = %file.display()))] pub fn run(file: PathBuf, recipient: Option) -> anyhow::Result<()> { let config_dir = Config::default_config_dir(); diff --git a/crates/doot-cli/src/commands/fmt.rs b/crates/doot-cli/src/commands/fmt.rs index 9fc0dec..2169193 100644 --- a/crates/doot-cli/src/commands/fmt.rs +++ b/crates/doot-cli/src/commands/fmt.rs @@ -1,6 +1,8 @@ use super::find_config_file; +use doot_core::deploy::diff::DiffDisplay; use std::path::PathBuf; +/// Formats or checks formatting of a `.doot` config file. #[tracing::instrument(skip_all, fields(check))] pub fn run(config_path: Option, check: bool) -> anyhow::Result<()> { let path = find_config_file(config_path)?; @@ -10,7 +12,8 @@ pub fn run(config_path: Option, check: bool) -> anyhow::Result<()> { if check { if formatted != source { - eprintln!("{} would be reformatted", path.display()); + let diff = DiffDisplay::diff_strings(&source, &formatted); + eprintln!("{}\n{}", path.display(), diff); std::process::exit(1); } else { println!("{} is formatted correctly", path.display()); @@ -27,9 +30,12 @@ pub fn run(config_path: Option, check: bool) -> anyhow::Result<()> { #[tracing::instrument(level = "trace", skip_all)] fn format_source(source: &str) -> String { + // Use an indent stack to track nesting levels from raw whitespace. + // When indentation increases, push a new level; when it decreases, + // pop back. This handles files with inconsistent indent widths. let mut result = String::new(); - let mut indent_level = 0; let mut prev_was_blank = false; + let mut indent_stack: Vec = vec![0]; // raw whitespace widths for line in source.lines() { let trimmed = line.trim(); @@ -43,29 +49,25 @@ fn format_source(source: &str) -> String { } prev_was_blank = false; - if trimmed.starts_with('#') { - result.push_str(&" ".repeat(indent_level)); - result.push_str(trimmed); - result.push('\n'); - continue; + let leading = line.len() - line.trim_start().len(); + + if leading > *indent_stack.last().unwrap() { + // Deeper nesting + indent_stack.push(leading); + } else if leading < *indent_stack.last().unwrap() { + // Dedent: pop until we find a level <= current + while indent_stack.len() > 1 && *indent_stack.last().unwrap() > leading { + indent_stack.pop(); + } } - let dedent_keywords = ["else"]; - let should_dedent = dedent_keywords.iter().any(|k| trimmed.starts_with(k)); - - if should_dedent && indent_level > 0 { - indent_level -= 1; - } - - result.push_str(&" ".repeat(indent_level)); + let level = indent_stack.len() - 1; + result.push_str(&" ".repeat(level)); result.push_str(trimmed); result.push('\n'); - - if trimmed.ends_with(':') && !trimmed.starts_with('#') { - indent_level += 1; - } } + // Trim trailing blank lines while result.ends_with("\n\n") { result.pop(); } @@ -76,3 +78,157 @@ fn format_source(source: &str) -> String { result } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_preserves_top_level_blocks() { + let input = "\ +dotfile: + src: fish + dst: ~/.config/fish + +dotfile: + src: nvim + dst: ~/.config/nvim + +dotfile: + src: git + dst: ~/.config/git +"; + let result = format_source(input); + assert_eq!(result, input); + } + + #[test] + fn test_normalizes_4space_to_2space() { + let input = "\ +dotfile: + src: fish + dst: ~/.config/fish +"; + let expected = "\ +dotfile: + src: fish + dst: ~/.config/fish +"; + assert_eq!(format_source(input), expected); + } + + #[test] + fn test_nested_blocks() { + let input = "\ +dotfile: + src: fish + if os == \"linux\": + package: apt + else: + package: brew +"; + let result = format_source(input); + assert_eq!(result, input); + } + + #[test] + fn test_collapses_consecutive_blank_lines() { + let input = "\ +dotfile: + src: fish + + + dst: ~/.config/fish +"; + let expected = "\ +dotfile: + src: fish + + dst: ~/.config/fish +"; + assert_eq!(format_source(input), expected); + } + + #[test] + fn test_trailing_blank_lines_trimmed() { + let input = "\ +dotfile: + src: fish + + +"; + let expected = "\ +dotfile: + src: fish +"; + assert_eq!(format_source(input), expected); + } + + #[test] + fn test_comments_preserve_indent() { + let input = "\ +# top-level comment +dotfile: + # nested comment + src: fish +"; + let result = format_source(input); + assert_eq!(result, input); + } + + #[test] + fn test_no_indented_lines() { + let input = "\ +one +two +three +"; + let result = format_source(input); + assert_eq!(result, input); + } + + #[test] + fn test_mixed_indent_normalizes() { + let input = "\ +dotfile: + src: fish + if cond: + package: apt +"; + let expected = "\ +dotfile: + src: fish + if cond: + package: apt +"; + assert_eq!(format_source(input), expected); + } + + #[test] + fn test_inconsistent_indent_widths() { + // Mix of 6-space, 2-space, and 4-space indentation (GCD = 2) + let input = "\ +dotfile: + source = \"config/*\" + target = config_dir() + +if cond: + package: \"fish\" + +if other: + package: \"bat\" +"; + let expected = "\ +dotfile: + source = \"config/*\" + target = config_dir() + +if cond: + package: \"fish\" + +if other: + package: \"bat\" +"; + assert_eq!(format_source(input), expected); + } +} diff --git a/crates/doot-cli/src/commands/init.rs b/crates/doot-cli/src/commands/init.rs index 5fb22de..72a7309 100644 --- a/crates/doot-cli/src/commands/init.rs +++ b/crates/doot-cli/src/commands/init.rs @@ -1,6 +1,7 @@ use doot_core::Config; use std::path::{Path, PathBuf}; +/// Initializes a new doot project directory structure. #[tracing::instrument(skip_all)] pub fn run(path: Option) -> anyhow::Result<()> { let source_dir = path.unwrap_or_else(Config::default_source_dir); diff --git a/crates/doot-cli/src/commands/lsp.rs b/crates/doot-cli/src/commands/lsp.rs index 60fc89b..c97f0c6 100644 --- a/crates/doot-cli/src/commands/lsp.rs +++ b/crates/doot-cli/src/commands/lsp.rs @@ -1,3 +1,4 @@ +/// Starts the doot language server. #[tracing::instrument] pub fn run() -> anyhow::Result<()> { println!("doot language server"); diff --git a/crates/doot-cli/src/commands/mod.rs b/crates/doot-cli/src/commands/mod.rs index d3b1b55..1ee9ae0 100644 --- a/crates/doot-cli/src/commands/mod.rs +++ b/crates/doot-cli/src/commands/mod.rs @@ -1,3 +1,5 @@ +//! CLI command handlers. + pub mod apply; pub mod check; pub mod decrypt; @@ -17,6 +19,7 @@ use doot_core::Config; use doot_lang::{Lexer, Parser, TypeChecker}; use std::path::PathBuf; +/// Resolves the config file path, checking the given path or default locations. #[tracing::instrument(skip_all)] pub fn find_config_file(base: Option) -> anyhow::Result { if let Some(path) = base { @@ -48,6 +51,7 @@ fn byte_offset_to_line(source: &str, offset: usize) -> usize { + 1 } +/// Parses a `.doot` config file into a program AST. #[tracing::instrument(skip_all, fields(path = %path.display()))] pub fn parse_config(path: &PathBuf) -> anyhow::Result { let source = std::fs::read_to_string(path)?; @@ -78,6 +82,7 @@ pub fn parse_config(path: &PathBuf) -> anyhow::Result { Ok(program) } +/// Runs the type checker on a parsed program. #[tracing::instrument(skip_all)] pub fn type_check( program: &doot_lang::Program, diff --git a/crates/doot-cli/src/commands/package.rs b/crates/doot-cli/src/commands/package.rs index 5d0bef9..cadaaed 100644 --- a/crates/doot-cli/src/commands/package.rs +++ b/crates/doot-cli/src/commands/package.rs @@ -2,6 +2,7 @@ use super::{find_config_file, parse_config, type_check}; use doot_lang::Evaluator; use std::path::PathBuf; +/// Installs packages defined in the config. #[tracing::instrument(skip_all)] pub fn install(config_path: Option) -> anyhow::Result<()> { let path = find_config_file(config_path)?; @@ -51,6 +52,7 @@ pub fn install(config_path: Option) -> anyhow::Result<()> { Ok(()) } +/// Updates the system package index. #[tracing::instrument(skip_all)] pub fn update() -> anyhow::Result<()> { let manager = doot_core::package::detect_package_manager() @@ -64,6 +66,7 @@ pub fn update() -> anyhow::Result<()> { Ok(()) } +/// Lists configured packages and their install status. #[tracing::instrument(skip_all)] pub fn list(config_path: Option) -> anyhow::Result<()> { let path = find_config_file(config_path)?; diff --git a/crates/doot-cli/src/commands/rollback.rs b/crates/doot-cli/src/commands/rollback.rs index b438e66..074d923 100644 --- a/crates/doot-cli/src/commands/rollback.rs +++ b/crates/doot-cli/src/commands/rollback.rs @@ -4,6 +4,7 @@ use doot_core::{ }; use std::path::PathBuf; +/// Rolls back to a previous snapshot. #[tracing::instrument(skip_all)] pub fn run(_config_path: Option, snapshot_name: Option) -> anyhow::Result<()> { let config = Config::default(); diff --git a/crates/doot-cli/src/commands/snapshot.rs b/crates/doot-cli/src/commands/snapshot.rs index 4a0049c..7e5cf9f 100644 --- a/crates/doot-cli/src/commands/snapshot.rs +++ b/crates/doot-cli/src/commands/snapshot.rs @@ -4,6 +4,7 @@ use doot_core::{ }; use std::path::PathBuf; +/// Creates a named snapshot of the current deployment state. #[tracing::instrument(skip_all, fields(name = %name))] pub fn run(_config_path: Option, name: String) -> anyhow::Result<()> { let config = Config::default(); diff --git a/crates/doot-cli/src/commands/status.rs b/crates/doot-cli/src/commands/status.rs index e42d26a..5a8cc01 100644 --- a/crates/doot-cli/src/commands/status.rs +++ b/crates/doot-cli/src/commands/status.rs @@ -3,6 +3,7 @@ use doot_core::state::{StateStore, SyncStatus}; use doot_lang::Evaluator; use std::path::PathBuf; +/// Shows the deployment status of managed dotfiles. #[tracing::instrument(skip_all)] pub fn run(config_path: Option) -> anyhow::Result<()> { let path = find_config_file(config_path)?; diff --git a/crates/doot-cli/src/commands/tui.rs b/crates/doot-cli/src/commands/tui.rs index 3abed57..9b078c2 100644 --- a/crates/doot-cli/src/commands/tui.rs +++ b/crates/doot-cli/src/commands/tui.rs @@ -19,6 +19,7 @@ use ratatui::{ use std::io; use std::path::PathBuf; +/// Launches the interactive TUI for managing dotfiles. #[tracing::instrument(skip_all)] pub fn run(config_path: Option) -> anyhow::Result<()> { enable_raw_mode()?; diff --git a/crates/doot-cli/src/main.rs b/crates/doot-cli/src/main.rs index 58a26f3..404e1f6 100644 --- a/crates/doot-cli/src/main.rs +++ b/crates/doot-cli/src/main.rs @@ -1,9 +1,24 @@ +//! doot CLI — a modern dotfiles manager with a typed DSL. +//! +//! ```sh +//! doot init # scaffold a new project +//! doot check # validate config +//! doot apply # deploy dotfiles & install packages +//! doot apply -n # dry-run +//! doot status # show deployment state +//! doot diff # show pending changes +//! doot fmt # format config file +//! doot fmt --check # check formatting (CI) +//! doot package install # install configured packages +//! ``` + mod commands; use clap::{Parser, Subcommand, ValueEnum}; use std::path::PathBuf; use tracing_subscriber::EnvFilter; +/// Top-level CLI arguments. #[derive(Parser)] #[command(name = "doot", version)] #[command(about = "A modern dotfiles manager with a typed DSL", long_about = None)] @@ -31,6 +46,7 @@ struct Cli { log_format: LogFormatArg, } +/// Log verbosity level. #[derive(Clone, ValueEnum)] enum LogLevelArg { Trace, @@ -40,20 +56,25 @@ enum LogLevelArg { Error, } +/// Log output format. #[derive(Clone, ValueEnum)] enum LogFormatArg { Text, Json, } +/// Available subcommands. #[derive(Subcommand)] enum Commands { + /// Initialize a new doot project: `doot init [PATH]` Init { /// Source directory for dotfiles (default: ~/.config/doot) path: Option, }, + /// Deploy dotfiles and install packages: `doot apply [-n] [--prune]` Apply { + /// Preview changes without writing: `doot apply -n` #[arg(short = 'n', long)] dry_run: bool, @@ -62,52 +83,71 @@ enum Commands { prune: bool, }, + /// Show diffs between source and deployed files: `doot diff [-a]` Diff { + /// Include unchanged files #[arg(short, long)] all: bool, }, + /// Show deployment status of managed dotfiles: `doot status` Status, + /// Validate config (parse + type check): `doot check` Check, + /// Format config file: `doot fmt [--check]` Fmt { + /// Check formatting without modifying (exits 1 if unformatted) #[arg(short, long)] check: bool, }, + /// Roll back to a previous snapshot: `doot rollback [SNAPSHOT]` Rollback { + /// Snapshot name (defaults to latest) snapshot: Option, }, + /// Save a named snapshot of current state: `doot snapshot ` Snapshot { + /// Snapshot name name: String, }, + /// Encrypt a file with age: `doot encrypt [-r RECIPIENT]` Encrypt { + /// File to encrypt file: PathBuf, + /// Recipient public key #[arg(short, long)] recipient: Option, }, + /// Decrypt an age-encrypted file: `doot decrypt [-i IDENTITY]` Decrypt { + /// File to decrypt file: PathBuf, + /// Path to age identity file #[arg(short, long)] identity: Option, }, + /// Manage system packages: `doot package {install|update|list}` Package { #[command(subcommand)] action: PackageAction, }, + /// Start the doot language server: `doot lsp` Lsp, + /// Launch interactive TUI: `doot tui` Tui, - /// Open source file in editor for a deployed target + /// Open source file in editor for a deployed target: `doot edit [-a] [-y]` Edit { /// Target path or dotfile name (e.g., ~/.config/nvim or nvim) target: String, @@ -122,13 +162,18 @@ enum Commands { }, } +/// Package subcommands. #[derive(Subcommand)] enum PackageAction { + /// Install packages from config: `doot package install` Install, + /// Update system package index: `doot package update` Update, + /// List configured packages: `doot package list` List, } +/// Entry point — parses CLI args, sets up logging, and dispatches commands. fn main() -> anyhow::Result<()> { let cli = Cli::parse(); diff --git a/crates/doot-core/src/package/apt.rs b/crates/doot-core/src/package/apt.rs index 46a6a1d..a44d23d 100644 --- a/crates/doot-core/src/package/apt.rs +++ b/crates/doot-core/src/package/apt.rs @@ -2,6 +2,7 @@ use super::{PackageError, PackageManager}; use std::io::Write; use std::process::{Command, Stdio}; +/// APT package manager (Debian/Ubuntu). pub struct Apt { dry_run: bool, use_sudo: bool, diff --git a/crates/doot-core/src/package/brew.rs b/crates/doot-core/src/package/brew.rs index b310828..a764422 100644 --- a/crates/doot-core/src/package/brew.rs +++ b/crates/doot-core/src/package/brew.rs @@ -1,6 +1,7 @@ use super::{PackageError, PackageManager}; use std::process::Command; +/// Homebrew package manager (macOS/Linux). pub struct Brew { dry_run: bool, } diff --git a/crates/doot-core/src/package/pacman.rs b/crates/doot-core/src/package/pacman.rs index 9462620..9a3b035 100644 --- a/crates/doot-core/src/package/pacman.rs +++ b/crates/doot-core/src/package/pacman.rs @@ -2,6 +2,7 @@ use super::{PackageError, PackageManager}; use std::io::Write; use std::process::{Command, Stdio}; +/// Pacman package manager (Arch Linux). pub struct Pacman { dry_run: bool, use_sudo: bool, diff --git a/crates/doot-core/src/package/yay.rs b/crates/doot-core/src/package/yay.rs index 6abb257..2367a39 100644 --- a/crates/doot-core/src/package/yay.rs +++ b/crates/doot-core/src/package/yay.rs @@ -1,6 +1,7 @@ use super::{PackageError, PackageManager}; use std::process::Command; +/// Yay AUR helper (Arch Linux). pub struct Yay { dry_run: bool, }