chore(docs): add documentations
This commit is contained in:
parent
19b82e6313
commit
289aa82ded
20 changed files with 245 additions and 20 deletions
|
|
@ -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<PathBuf>, dry_run: bool, prune: bool) -> anyhow::Result<()> {
|
||||
let start = Instant::now();
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>) -> anyhow::Result<()> {
|
||||
let path = find_config_file(config_path)?;
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>) -> anyhow::Result<()> {
|
||||
let config = Config::default();
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>, all: bool) -> anyhow::Result<()> {
|
||||
let path = find_config_file(config_path)?;
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>,
|
||||
|
|
|
|||
|
|
@ -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<String>) -> anyhow::Result<()> {
|
||||
let config_dir = Config::default_config_dir();
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>, check: bool) -> anyhow::Result<()> {
|
||||
let path = find_config_file(config_path)?;
|
||||
|
|
@ -10,7 +12,8 @@ pub fn run(config_path: Option<PathBuf>, 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<PathBuf>, 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<usize> = 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>) -> anyhow::Result<()> {
|
||||
let source_dir = path.unwrap_or_else(Config::default_source_dir);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/// Starts the doot language server.
|
||||
#[tracing::instrument]
|
||||
pub fn run() -> anyhow::Result<()> {
|
||||
println!("doot language server");
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>) -> anyhow::Result<PathBuf> {
|
||||
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<doot_lang::Program> {
|
||||
let source = std::fs::read_to_string(path)?;
|
||||
|
|
@ -78,6 +82,7 @@ pub fn parse_config(path: &PathBuf) -> anyhow::Result<doot_lang::Program> {
|
|||
Ok(program)
|
||||
}
|
||||
|
||||
/// Runs the type checker on a parsed program.
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn type_check(
|
||||
program: &doot_lang::Program,
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>) -> anyhow::Result<()> {
|
||||
let path = find_config_file(config_path)?;
|
||||
|
|
@ -51,6 +52,7 @@ pub fn install(config_path: Option<PathBuf>) -> 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<PathBuf>) -> anyhow::Result<()> {
|
||||
let path = find_config_file(config_path)?;
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>, snapshot_name: Option<String>) -> anyhow::Result<()> {
|
||||
let config = Config::default();
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>, name: String) -> anyhow::Result<()> {
|
||||
let config = Config::default();
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>) -> anyhow::Result<()> {
|
||||
let path = find_config_file(config_path)?;
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>) -> anyhow::Result<()> {
|
||||
enable_raw_mode()?;
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>,
|
||||
},
|
||||
|
||||
/// 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<String>,
|
||||
},
|
||||
|
||||
/// Save a named snapshot of current state: `doot snapshot <NAME>`
|
||||
Snapshot {
|
||||
/// Snapshot name
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Encrypt a file with age: `doot encrypt <FILE> [-r RECIPIENT]`
|
||||
Encrypt {
|
||||
/// File to encrypt
|
||||
file: PathBuf,
|
||||
|
||||
/// Recipient public key
|
||||
#[arg(short, long)]
|
||||
recipient: Option<String>,
|
||||
},
|
||||
|
||||
/// Decrypt an age-encrypted file: `doot decrypt <FILE> [-i IDENTITY]`
|
||||
Decrypt {
|
||||
/// File to decrypt
|
||||
file: PathBuf,
|
||||
|
||||
/// Path to age identity file
|
||||
#[arg(short, long)]
|
||||
identity: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// 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 <TARGET> [-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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use super::{PackageError, PackageManager};
|
||||
use std::process::Command;
|
||||
|
||||
/// Homebrew package manager (macOS/Linux).
|
||||
pub struct Brew {
|
||||
dry_run: bool,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use super::{PackageError, PackageManager};
|
||||
use std::process::Command;
|
||||
|
||||
/// Yay AUR helper (Arch Linux).
|
||||
pub struct Yay {
|
||||
dry_run: bool,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue