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::process::Command;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// Applies the dotfile configuration, deploying files and installing packages.
|
||||||
#[tracing::instrument(skip_all, fields(dry_run, prune))]
|
#[tracing::instrument(skip_all, fields(dry_run, prune))]
|
||||||
pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::Result<()> {
|
pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::Result<()> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use super::{find_config_file, parse_config, type_check};
|
use super::{find_config_file, parse_config, type_check};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Validates a config file (parse + type check).
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
let path = find_config_file(config_path)?;
|
let path = find_config_file(config_path)?;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use doot_core::{Config, encryption::AgeEncryption};
|
use doot_core::{Config, encryption::AgeEncryption};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Decrypts an age-encrypted file.
|
||||||
#[tracing::instrument(skip_all, fields(file = %file.display()))]
|
#[tracing::instrument(skip_all, fields(file = %file.display()))]
|
||||||
pub fn run(file: PathBuf, identity: Option<PathBuf>) -> anyhow::Result<()> {
|
pub fn run(file: PathBuf, identity: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
let config = Config::default();
|
let config = Config::default();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use doot_lang::Evaluator;
|
||||||
use doot_lang::evaluator::PermissionRule;
|
use doot_lang::evaluator::PermissionRule;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Shows diffs between source and deployed dotfiles.
|
||||||
#[tracing::instrument(skip_all, fields(all))]
|
#[tracing::instrument(skip_all, fields(all))]
|
||||||
pub fn run(config_path: Option<PathBuf>, all: bool) -> anyhow::Result<()> {
|
pub fn run(config_path: Option<PathBuf>, all: bool) -> anyhow::Result<()> {
|
||||||
let path = find_config_file(config_path)?;
|
let path = find_config_file(config_path)?;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use std::io::{self, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
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))]
|
#[tracing::instrument(skip_all, fields(target = %target, auto_apply, skip_prompt))]
|
||||||
pub fn run(
|
pub fn run(
|
||||||
config_path: Option<PathBuf>,
|
config_path: Option<PathBuf>,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use doot_core::{Config, encryption::AgeEncryption};
|
use doot_core::{Config, encryption::AgeEncryption};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Encrypts a file using age encryption.
|
||||||
#[tracing::instrument(skip_all, fields(file = %file.display()))]
|
#[tracing::instrument(skip_all, fields(file = %file.display()))]
|
||||||
pub fn run(file: PathBuf, recipient: Option<String>) -> anyhow::Result<()> {
|
pub fn run(file: PathBuf, recipient: Option<String>) -> anyhow::Result<()> {
|
||||||
let config_dir = Config::default_config_dir();
|
let config_dir = Config::default_config_dir();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use super::find_config_file;
|
use super::find_config_file;
|
||||||
|
use doot_core::deploy::diff::DiffDisplay;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Formats or checks formatting of a `.doot` config file.
|
||||||
#[tracing::instrument(skip_all, fields(check))]
|
#[tracing::instrument(skip_all, fields(check))]
|
||||||
pub fn run(config_path: Option<PathBuf>, check: bool) -> anyhow::Result<()> {
|
pub fn run(config_path: Option<PathBuf>, check: bool) -> anyhow::Result<()> {
|
||||||
let path = find_config_file(config_path)?;
|
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 check {
|
||||||
if formatted != source {
|
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);
|
std::process::exit(1);
|
||||||
} else {
|
} else {
|
||||||
println!("{} is formatted correctly", path.display());
|
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)]
|
#[tracing::instrument(level = "trace", skip_all)]
|
||||||
fn format_source(source: &str) -> String {
|
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 result = String::new();
|
||||||
let mut indent_level = 0;
|
|
||||||
let mut prev_was_blank = false;
|
let mut prev_was_blank = false;
|
||||||
|
let mut indent_stack: Vec<usize> = vec![0]; // raw whitespace widths
|
||||||
|
|
||||||
for line in source.lines() {
|
for line in source.lines() {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
|
|
@ -43,29 +49,25 @@ fn format_source(source: &str) -> String {
|
||||||
}
|
}
|
||||||
prev_was_blank = false;
|
prev_was_blank = false;
|
||||||
|
|
||||||
if trimmed.starts_with('#') {
|
let leading = line.len() - line.trim_start().len();
|
||||||
result.push_str(&" ".repeat(indent_level));
|
|
||||||
result.push_str(trimmed);
|
if leading > *indent_stack.last().unwrap() {
|
||||||
result.push('\n');
|
// Deeper nesting
|
||||||
continue;
|
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 level = indent_stack.len() - 1;
|
||||||
let should_dedent = dedent_keywords.iter().any(|k| trimmed.starts_with(k));
|
result.push_str(&" ".repeat(level));
|
||||||
|
|
||||||
if should_dedent && indent_level > 0 {
|
|
||||||
indent_level -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push_str(&" ".repeat(indent_level));
|
|
||||||
result.push_str(trimmed);
|
result.push_str(trimmed);
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
|
|
||||||
if trimmed.ends_with(':') && !trimmed.starts_with('#') {
|
|
||||||
indent_level += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trim trailing blank lines
|
||||||
while result.ends_with("\n\n") {
|
while result.ends_with("\n\n") {
|
||||||
result.pop();
|
result.pop();
|
||||||
}
|
}
|
||||||
|
|
@ -76,3 +78,157 @@ fn format_source(source: &str) -> String {
|
||||||
|
|
||||||
result
|
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 doot_core::Config;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Initializes a new doot project directory structure.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn run(path: Option<PathBuf>) -> anyhow::Result<()> {
|
pub fn run(path: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
let source_dir = path.unwrap_or_else(Config::default_source_dir);
|
let source_dir = path.unwrap_or_else(Config::default_source_dir);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
/// Starts the doot language server.
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub fn run() -> anyhow::Result<()> {
|
pub fn run() -> anyhow::Result<()> {
|
||||||
println!("doot language server");
|
println!("doot language server");
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! CLI command handlers.
|
||||||
|
|
||||||
pub mod apply;
|
pub mod apply;
|
||||||
pub mod check;
|
pub mod check;
|
||||||
pub mod decrypt;
|
pub mod decrypt;
|
||||||
|
|
@ -17,6 +19,7 @@ use doot_core::Config;
|
||||||
use doot_lang::{Lexer, Parser, TypeChecker};
|
use doot_lang::{Lexer, Parser, TypeChecker};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Resolves the config file path, checking the given path or default locations.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn find_config_file(base: Option<PathBuf>) -> anyhow::Result<PathBuf> {
|
pub fn find_config_file(base: Option<PathBuf>) -> anyhow::Result<PathBuf> {
|
||||||
if let Some(path) = base {
|
if let Some(path) = base {
|
||||||
|
|
@ -48,6 +51,7 @@ fn byte_offset_to_line(source: &str, offset: usize) -> usize {
|
||||||
+ 1
|
+ 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a `.doot` config file into a program AST.
|
||||||
#[tracing::instrument(skip_all, fields(path = %path.display()))]
|
#[tracing::instrument(skip_all, fields(path = %path.display()))]
|
||||||
pub fn parse_config(path: &PathBuf) -> anyhow::Result<doot_lang::Program> {
|
pub fn parse_config(path: &PathBuf) -> anyhow::Result<doot_lang::Program> {
|
||||||
let source = std::fs::read_to_string(path)?;
|
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)
|
Ok(program)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs the type checker on a parsed program.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn type_check(
|
pub fn type_check(
|
||||||
program: &doot_lang::Program,
|
program: &doot_lang::Program,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use super::{find_config_file, parse_config, type_check};
|
||||||
use doot_lang::Evaluator;
|
use doot_lang::Evaluator;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Installs packages defined in the config.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn install(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
pub fn install(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
let path = find_config_file(config_path)?;
|
let path = find_config_file(config_path)?;
|
||||||
|
|
@ -51,6 +52,7 @@ pub fn install(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the system package index.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn update() -> anyhow::Result<()> {
|
pub fn update() -> anyhow::Result<()> {
|
||||||
let manager = doot_core::package::detect_package_manager()
|
let manager = doot_core::package::detect_package_manager()
|
||||||
|
|
@ -64,6 +66,7 @@ pub fn update() -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lists configured packages and their install status.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn list(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
pub fn list(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
let path = find_config_file(config_path)?;
|
let path = find_config_file(config_path)?;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use doot_core::{
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Rolls back to a previous snapshot.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn run(_config_path: Option<PathBuf>, snapshot_name: Option<String>) -> anyhow::Result<()> {
|
pub fn run(_config_path: Option<PathBuf>, snapshot_name: Option<String>) -> anyhow::Result<()> {
|
||||||
let config = Config::default();
|
let config = Config::default();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use doot_core::{
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Creates a named snapshot of the current deployment state.
|
||||||
#[tracing::instrument(skip_all, fields(name = %name))]
|
#[tracing::instrument(skip_all, fields(name = %name))]
|
||||||
pub fn run(_config_path: Option<PathBuf>, name: String) -> anyhow::Result<()> {
|
pub fn run(_config_path: Option<PathBuf>, name: String) -> anyhow::Result<()> {
|
||||||
let config = Config::default();
|
let config = Config::default();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use doot_core::state::{StateStore, SyncStatus};
|
||||||
use doot_lang::Evaluator;
|
use doot_lang::Evaluator;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Shows the deployment status of managed dotfiles.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
let path = find_config_file(config_path)?;
|
let path = find_config_file(config_path)?;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use ratatui::{
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Launches the interactive TUI for managing dotfiles.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
enable_raw_mode()?;
|
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;
|
mod commands;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand, ValueEnum};
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
/// Top-level CLI arguments.
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "doot", version)]
|
#[command(name = "doot", version)]
|
||||||
#[command(about = "A modern dotfiles manager with a typed DSL", long_about = None)]
|
#[command(about = "A modern dotfiles manager with a typed DSL", long_about = None)]
|
||||||
|
|
@ -31,6 +46,7 @@ struct Cli {
|
||||||
log_format: LogFormatArg,
|
log_format: LogFormatArg,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Log verbosity level.
|
||||||
#[derive(Clone, ValueEnum)]
|
#[derive(Clone, ValueEnum)]
|
||||||
enum LogLevelArg {
|
enum LogLevelArg {
|
||||||
Trace,
|
Trace,
|
||||||
|
|
@ -40,20 +56,25 @@ enum LogLevelArg {
|
||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Log output format.
|
||||||
#[derive(Clone, ValueEnum)]
|
#[derive(Clone, ValueEnum)]
|
||||||
enum LogFormatArg {
|
enum LogFormatArg {
|
||||||
Text,
|
Text,
|
||||||
Json,
|
Json,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Available subcommands.
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
|
/// Initialize a new doot project: `doot init [PATH]`
|
||||||
Init {
|
Init {
|
||||||
/// Source directory for dotfiles (default: ~/.config/doot)
|
/// Source directory for dotfiles (default: ~/.config/doot)
|
||||||
path: Option<PathBuf>,
|
path: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Deploy dotfiles and install packages: `doot apply [-n] [--prune]`
|
||||||
Apply {
|
Apply {
|
||||||
|
/// Preview changes without writing: `doot apply -n`
|
||||||
#[arg(short = 'n', long)]
|
#[arg(short = 'n', long)]
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
|
|
||||||
|
|
@ -62,52 +83,71 @@ enum Commands {
|
||||||
prune: bool,
|
prune: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Show diffs between source and deployed files: `doot diff [-a]`
|
||||||
Diff {
|
Diff {
|
||||||
|
/// Include unchanged files
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
all: bool,
|
all: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Show deployment status of managed dotfiles: `doot status`
|
||||||
Status,
|
Status,
|
||||||
|
|
||||||
|
/// Validate config (parse + type check): `doot check`
|
||||||
Check,
|
Check,
|
||||||
|
|
||||||
|
/// Format config file: `doot fmt [--check]`
|
||||||
Fmt {
|
Fmt {
|
||||||
|
/// Check formatting without modifying (exits 1 if unformatted)
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
check: bool,
|
check: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Roll back to a previous snapshot: `doot rollback [SNAPSHOT]`
|
||||||
Rollback {
|
Rollback {
|
||||||
|
/// Snapshot name (defaults to latest)
|
||||||
snapshot: Option<String>,
|
snapshot: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Save a named snapshot of current state: `doot snapshot <NAME>`
|
||||||
Snapshot {
|
Snapshot {
|
||||||
|
/// Snapshot name
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Encrypt a file with age: `doot encrypt <FILE> [-r RECIPIENT]`
|
||||||
Encrypt {
|
Encrypt {
|
||||||
|
/// File to encrypt
|
||||||
file: PathBuf,
|
file: PathBuf,
|
||||||
|
|
||||||
|
/// Recipient public key
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
recipient: Option<String>,
|
recipient: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Decrypt an age-encrypted file: `doot decrypt <FILE> [-i IDENTITY]`
|
||||||
Decrypt {
|
Decrypt {
|
||||||
|
/// File to decrypt
|
||||||
file: PathBuf,
|
file: PathBuf,
|
||||||
|
|
||||||
|
/// Path to age identity file
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
identity: Option<PathBuf>,
|
identity: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Manage system packages: `doot package {install|update|list}`
|
||||||
Package {
|
Package {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
action: PackageAction,
|
action: PackageAction,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Start the doot language server: `doot lsp`
|
||||||
Lsp,
|
Lsp,
|
||||||
|
|
||||||
|
/// Launch interactive TUI: `doot tui`
|
||||||
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 {
|
Edit {
|
||||||
/// Target path or dotfile name (e.g., ~/.config/nvim or nvim)
|
/// Target path or dotfile name (e.g., ~/.config/nvim or nvim)
|
||||||
target: String,
|
target: String,
|
||||||
|
|
@ -122,13 +162,18 @@ enum Commands {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Package subcommands.
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum PackageAction {
|
enum PackageAction {
|
||||||
|
/// Install packages from config: `doot package install`
|
||||||
Install,
|
Install,
|
||||||
|
/// Update system package index: `doot package update`
|
||||||
Update,
|
Update,
|
||||||
|
/// List configured packages: `doot package list`
|
||||||
List,
|
List,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Entry point — parses CLI args, sets up logging, and dispatches commands.
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use super::{PackageError, PackageManager};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
/// APT package manager (Debian/Ubuntu).
|
||||||
pub struct Apt {
|
pub struct Apt {
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
use_sudo: bool,
|
use_sudo: bool,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use super::{PackageError, PackageManager};
|
use super::{PackageError, PackageManager};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Homebrew package manager (macOS/Linux).
|
||||||
pub struct Brew {
|
pub struct Brew {
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use super::{PackageError, PackageManager};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
/// Pacman package manager (Arch Linux).
|
||||||
pub struct Pacman {
|
pub struct Pacman {
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
use_sudo: bool,
|
use_sudo: bool,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use super::{PackageError, PackageManager};
|
use super::{PackageError, PackageManager};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Yay AUR helper (Arch Linux).
|
||||||
pub struct Yay {
|
pub struct Yay {
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue