Status: This document is being developed as part of the documentation overhaul (Week 3).
This guide provides an overview of Heimdal's codebase structure and module organization to help contributors navigate and understand the project.
Heimdal consists of approximately 85 source files organized into 14 major modules, totaling around 24,000 lines of code.
src/
├── main.rs # CLI entry point (667 lines)
├── cli.rs # CLI argument parsing (146 lines)
├── commands/ # Command implementations
│ ├── packages/ # Package management commands
│ └── state/ # State management commands
├── config/ # Configuration system
├── git/ # Git operations
├── hooks/ # Hook system
├── import/ # Import from other dotfile managers
├── package/ # Package management
├── profile/ # Profile management
├── secrets/ # Secret management
├── state/ # State management
├── symlink/ # Symlink creation (GNU Stow)
├── sync/ # Git sync operations
├── templates/ # Template engine
├── utils/ # Shared utilities
└── wizard/ # Interactive setup wizard
Location: src/main.rs (667 lines)
Purpose: Application entry point, CLI routing, and command dispatch.
Key Functions:
main()- Entry point- Command routing based on CLI arguments
- Global error handling
- Logging setup
Dependencies:
- All command modules
climodule for argument parsing
Location: src/cli.rs (146 lines)
Purpose: Define CLI structure using clap crate.
Key Structs:
Cli- Root CLI structureCommands- Enum of all subcommands- Various subcommand structs
Dependencies:
clapfor argument parsing
Files:
mod.rs- Module exportssearch.rs- Package searchinstall.rs- Package installationinfo.rs- Package informationoutdated.rs- Outdated package detectionupgrade.rs- Package upgrades
Purpose: Implement all heimdal packages * commands.
Key Functions:
search_packages()- Fuzzy search with scoringinstall_package()- Install single packageget_outdated_packages()- Detect outdated packagesupgrade_packages()- Upgrade packages
Dependencies:
packagemoduleconfigmodulestatemodule
Files:
mod.rs- Module exportslock.rs- State lockingunlock.rs- State unlockingconflict.rs- Conflict resolution
Purpose: Implement all heimdal state * commands.
Key Functions:
lock_state()- Acquire state lockunlock_state()- Release state lockresolve_conflicts()- Resolve state conflicts
Dependencies:
statemoduleconfigmodule
Files:
mod.rs- Module exports and main loaderschema.rs- Configuration structsloader.rs- YAML loading and parsingprofile.rs- Profile resolutionconditions.rs- Conditional configuration
Purpose: Load, validate, and manage heimdal.yaml configuration.
Key Structs:
Config- Root configurationProfile- Profile configurationPackageSource- Package sourcesDotfilesConfig- Dotfile configuration
Key Functions:
load_config()- Load config from filevalidate_config()- Validate configurationresolve_profile()- Resolve active profile
Dependencies:
serde_yamlfor YAML parsinganyhowfor error handling
Files:
mod.rs- Module exportsoperations.rs- Git command wrapperssync.rs- Sync operations
Purpose: Wrap Git operations for internal use by heimdal commit and heimdal sync.
Note (v2.0.0): The Git module functions are for internal use only. CLI commands
push,pull,branch, andremotewere removed in v2.0.0. Users should use native Git commands for these operations. Heimdal focuses on dotfiles-specific workflows.
Key Functions (Internal):
git_commit()- Used byheimdal commitcommandgit_status()- Used byheimdal statuscommandsync_pull()- Used byheimdal synccommand- Other functions remain for potential future use or internal operations
Dependencies:
git2crate for Git operations- Or
std::process::Commandfor git CLI
Files:
mod.rs- Hook executionrunner.rs- Script runner
Purpose: Execute user-defined hooks (pre/post scripts).
Key Functions:
run_hook()- Execute a hook scriptfind_hooks()- Discover hook scripts
Dependencies:
std::process::Commandfor script execution
Files:
mod.rs- Module exports and detectionstow.rs- GNU Stow importdotbot.rs- dotbot importchezmoi.rs- chezmoi importyadm.rs- yadm importhomesick.rs- homesick import
Purpose: Import configurations from other dotfile managers.
Key Functions:
detect_dotfile_manager()- Auto-detect which managerimport_from_stow()- Convert Stow setupimport_from_dotbot()- Parseinstall.conf.yaml
Dependencies:
configmoduleserde_yamlfor parsing configs
Files:
mod.rs- Module exportsmanager.rs- Package manager abstractionmapper.rs- Cross-platform name mappinginstaller.rs- Package installationdatabase/- Package database subsystemcore.rs- Database interfaceloader.rs- Download and load databasecache.rs- Local cachingsearch.rs- Search functionality
profiles/- Package profiles
Purpose: Universal package management across all platforms.
Key Structs:
PackageManager- Abstraction over brew/apt/dnf/pacmanPackageDatabase- In-memory databasePackage- Package metadata
Key Functions:
detect_package_manager()- Detect platform package managerinstall_package()- Install via appropriate managersearch_database()- Search package databasemap_package_name()- Map canonical name to platform name
Dependencies:
std::process::Commandfor running package managersbincodefor database deserializationfuzzy-matcherfor search
Files:
mod.rs- Module exportsmanager.rs- Profile operationsinheritance.rs- Profile inheritance
Purpose: Manage multiple profiles (work, personal, server).
Key Functions:
switch_profile()- Change active profilelist_profiles()- List available profilesresolve_inheritance()- Handle profile inheritance
Dependencies:
configmodulestatemodule
Files:
mod.rs- Module exportsstore.rs- Keychain integration
Purpose: Secure secret storage using OS keychains.
Key Functions:
store_secret()- Save secret to keychainretrieve_secret()- Get secret from keychaindelete_secret()- Remove secret
Dependencies:
keyringcrate for OS keychain access
Files:
mod.rs- Module exports and core logiclock.rs- State locking (Terraform-inspired)conflict.rs- Conflict detection and resolutionhistory.rs- State history
Purpose: Track Heimdal state, prevent conflicts, handle locking.
Key Structs:
State- Current state (active profile, packages, etc.)StateLock- Lock informationConflictInfo- Conflict details
Key Functions:
acquire_lock()- Acquire state lockrelease_lock()- Release state lockdetect_conflicts()- Find conflicting changessave_state()- Persist state to disk
Dependencies:
serde_jsonfor state serialization
See Also: State Management Documentation
Files:
mod.rs- Module exportslinker.rs- Symlink creationstow.rs- GNU Stow compatibilityconflict.rs- Conflict detection
Purpose: Create and manage symlinks (GNU Stow compatible).
Key Functions:
create_symlinks()- Create symlinks for dotfilesdetect_conflicts()- Find existing filesresolve_conflicts()- Backup/overwrite/skipstow_directory()- Stow-style directory handling
Dependencies:
std::fsfor filesystem operations
Files:
mod.rs- Module exportsmanager.rs- Sync manager
Purpose: High-level Git sync operations.
Key Functions:
sync_pull()- Pull changes from remotesync_push()- Push changes to remoteauto_sync()- Automatic background sync
Dependencies:
gitmodulestatemodule
Files:
mod.rs- Module exportsengine.rs- Template processingvariables.rs- Variable resolution
Purpose: Process templates with variable substitution.
Key Functions:
render_template()- Process template filesubstitute_variables()- Replace {{variable}} placeholdersresolve_variable()- Get variable value from config/env
Dependencies:
regexfor pattern matching
Files:
mod.rs- Module exportslogger.rs- Logging helperserror.rs- Error typesos.rs- OS detectionpath.rs- Path utilities
Purpose: Shared utility functions across modules.
Key Functions:
detect_os()- Detect current OSexpand_tilde()- Expand~in pathssetup_logger()- Initialize logging- Error types and helpers
Dependencies:
logcrate for logginganyhowfor error handling
Files:
mod.rs- Module exports and orchestrationscanner.rs- Dotfile scanningprompts.rs- Interactive promptsgenerator.rs- Config generationpackage_detector.rs- Detect installed packages
Purpose: Interactive setup wizard for first-time users.
Key Functions:
run_wizard()- Main wizard flowscan_dotfiles()- Scan home directorydetect_packages()- Detect installed packagesgenerate_config()- Generateheimdal.yaml
Dependencies:
dialoguerfor interactive promptsindicatiffor progress barsconfigmodulepackagemodule
graph TD
Main[main.rs] --> CLI[cli.rs]
Main --> Commands
Main --> Wizard
Commands --> PackageCmd[commands/packages/*]
Commands --> StateCmd[commands/state/*]
PackageCmd --> Package[package/]
PackageCmd --> Config[config/]
PackageCmd --> State[state/]
StateCmd --> State
StateCmd --> Config
Wizard --> Config
Wizard --> Package
Wizard --> Profile[profile/]
Config --> Schema[config/schema.rs]
Config --> Loader[config/loader.rs]
Package --> Manager[package/manager.rs]
Package --> Database[package/database/]
Package --> Mapper[package/mapper.rs]
Database --> Core[database/core.rs]
Database --> Cache[database/cache.rs]
Database --> Search[database/search.rs]
Profile --> Config
Profile --> State
Symlink[symlink/] --> Stow[symlink/stow.rs]
Symlink --> Conflict[symlink/conflict.rs]
Sync[sync/] --> Git[git/]
Sync --> State
Templates[templates/] --> Secrets[secrets/]
Git --> Operations[git/operations.rs]
State --> Lock[state/lock.rs]
State --> ConflictMgr[state/conflict.rs]
Secrets --> Keychain[(OS Keychain)]
Utils[utils/] -.-> All[All Modules]
style Main fill:#e1f5ff
style Config fill:#fff4e1
style Package fill:#e8f5e9
style State fill:#ffcdd2
style Utils fill:#f0f0f0
flowchart LR
subgraph Input
User([User Command])
ConfigFile[heimdal.yaml]
end
subgraph Processing
CLI --> Config
Config --> Profile
Profile --> Package
Package --> State
State --> Output
end
subgraph External
PackageDB[(Package DB)]
GitRepo[(Git Repo)]
OSKeychain[(Keychain)]
end
User --> CLI
ConfigFile --> Config
Package <--> PackageDB
State <--> GitRepo
Config <--> OSKeychain
Output([Result])
style Input fill:#e1f5ff
style Processing fill:#fff4e1
style External fill:#f0f0f0
All modules use anyhow::Result<T> for error handling:
use anyhow::{Context, Result};
pub fn load_config(path: &Path) -> Result<Config> {
let content = fs::read_to_string(path)
.context("Failed to read config file")?;
let config: Config = serde_yaml::from_str(&content)
.context("Failed to parse YAML")?;
Ok(config)
}Real Example from src/config/loader.rs:
pub fn load_config(config_path: Option<&Path>) -> Result<Config> {
let path = config_path
.map(|p| p.to_path_buf())
.unwrap_or_else(|| {
dirs::home_dir()
.expect("Could not find home directory")
.join(".heimdal")
.join("heimdal.yaml")
});
if !path.exists() {
anyhow::bail!("Config file not found at {:?}", path);
}
let content = fs::read_to_string(&path)
.context(format!("Failed to read config file: {:?}", path))?;
let config: Config = serde_yaml::from_str(&content)
.context("Failed to parse YAML configuration")?;
validate_config(&config)?;
Ok(config)
}Use the log crate macros:
use log::{info, warn, error, debug};
info!("Loading configuration from {:?}", path);
warn!("Package not found in database: {}", name);
error!("Failed to create symlink: {}", err);
debug!("Resolved package name: {} -> {}", canonical, platform);Real Example from src/package/database/loader.rs:
pub async fn load_database() -> Result<PackageDatabase> {
debug!("Loading package database...");
let cache_path = get_cache_path()?;
if cache_path.exists() && !is_cache_stale(&cache_path)? {
info!("Loading package database from cache: {:?}", cache_path);
return load_from_cache(&cache_path);
}
info!("Cache stale or missing, downloading fresh database...");
download_and_cache_database().await
}All package managers implement a common trait:
Real Example from src/package/manager.rs:
/// Common interface for all package managers
pub trait PackageManager: Send + Sync {
/// Name of the package manager
fn name(&self) -> &str;
/// Check if this package manager is available on the system
fn is_available(&self) -> bool;
/// Check if a package is installed
fn is_installed(&self, package: &str) -> bool;
/// Install a package
fn install(&self, package: &str, dry_run: bool) -> Result<()>;
/// Install multiple packages at once (more efficient)
fn install_many(&self, packages: &[String], dry_run: bool)
-> Result<Vec<InstallResult>>;
/// Update package manager's package list
fn update(&self, dry_run: bool) -> Result<()>;
}
// Example implementation for Homebrew
pub struct Homebrew;
impl PackageManager for Homebrew {
fn name(&self) -> &str {
"homebrew"
}
fn is_available(&self) -> bool {
Command::new("brew")
.arg("--version")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn is_installed(&self, package: &str) -> bool {
Command::new("brew")
.args(["list", package])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn install(&self, package: &str, dry_run: bool) -> Result<()> {
if dry_run {
info!("DRY RUN: Would install package: {}", package);
return Ok(());
}
let status = Command::new("brew")
.args(["install", package])
.status()
.context("Failed to execute brew install")?;
if !status.success() {
anyhow::bail!("Failed to install package: {}", package);
}
Ok(())
}
// ... other methods
}Real Example from src/config/schema.rs:
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// Active profile name
pub profile: Option<String>,
/// All defined profiles
pub profiles: HashMap<String, Profile>,
/// Dotfiles configuration
pub dotfiles: Option<DotfilesConfig>,
/// Git sync configuration
pub sync: Option<SyncConfig>,
/// Global settings
pub settings: Option<Settings>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Profile {
/// Profile display name
pub name: String,
/// Inherit from another profile
pub inherits: Option<String>,
/// Packages to install
pub packages: Vec<String>,
/// Template variables
pub variables: Option<HashMap<String, String>>,
/// Platform-specific overrides
pub platforms: Option<HashMap<String, PlatformConfig>>,
}Real Example from src/state/mod.rs:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct State {
/// Active profile name
pub active_profile: String,
/// Installed packages with metadata
pub installed_packages: Vec<InstalledPackage>,
/// Applied dotfiles with checksums
pub applied_dotfiles: Vec<AppliedDotfile>,
/// Last sync timestamp
pub last_sync: Option<DateTime<Utc>>,
/// State lock information
pub lock: Option<StateLock>,
}
impl State {
/// Load state from disk
pub fn load() -> Result<Self> {
let state_path = Self::state_path()?;
if !state_path.exists() {
return Ok(Self::default());
}
let content = fs::read_to_string(&state_path)
.context("Failed to read state file")?;
let state: State = serde_json::from_str(&content)
.context("Failed to parse state JSON")?;
Ok(state)
}
/// Save state to disk
pub fn save(&self) -> Result<()> {
let state_path = Self::state_path()?;
let state_dir = state_path.parent()
.context("Invalid state path")?;
fs::create_dir_all(state_dir)
.context("Failed to create state directory")?;
let content = serde_json::to_string_pretty(self)
.context("Failed to serialize state")?;
fs::write(&state_path, content)
.context("Failed to write state file")?;
Ok(())
}
}Tests are colocated with source files using #[cfg(test)]:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_package_search() {
let db = PackageDatabase::new();
let results = db.search("nodejs");
assert!(!results.is_empty());
}
#[test]
fn test_config_validation() {
let config = Config {
profile: Some("work".to_string()),
profiles: HashMap::new(),
dotfiles: None,
sync: None,
settings: None,
};
let result = validate_config(&config);
assert!(result.is_err()); // Missing required profile
}
}Real Example from src/package/mapper.rs:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_common_package_mappings() {
let mapper = PackageMapper::new();
// Test Node.js mapping
assert_eq!(
mapper.map_package_name("nodejs", Platform::MacOS),
"node"
);
// Test Python mapping
assert_eq!(
mapper.map_package_name("python3", Platform::Ubuntu),
"python3"
);
// Test package manager specific names
assert_eq!(
mapper.map_package_name("httpie", Platform::MacOS),
"httpie"
);
}
#[test]
fn test_platform_detection() {
let platform = detect_platform();
assert!(platform.is_ok());
}
}| Want to... | Start here | Key files |
|---|---|---|
| Add a new CLI command | src/cli.rs |
src/commands/, src/main.rs |
| Support new package manager | src/package/manager.rs |
src/package/mapper.rs |
| Modify configuration schema | src/config/schema.rs |
src/config/loader.rs |
| Change state management | src/state/mod.rs |
src/state/lock.rs |
| Add template features | src/templates/engine.rs |
src/templates/variables.rs |
| Implement new import format | src/import/ |
Create new file in import/ |
| Modify symlink behavior | src/symlink/linker.rs |
src/symlink/stow.rs |
| Change Git operations | src/git/operations.rs |
src/sync/manager.rs |
| Add secret storage options | src/secrets/store.rs |
Update to support new backends |
| Extend wizard functionality | src/wizard/mod.rs |
src/wizard/prompts.rs |
- Define CLI arguments in
src/cli.rs - Create command handler in
src/commands/<category>/<command>.rs - Add routing in
src/main.rs - Write tests in the same file
- Update documentation in wiki
- Add platform detection in
src/package/manager.rs - Implement install/uninstall methods
- Add to package mapping in
src/package/mapper.rs - Write integration tests
- Update documentation
- Create importer in
src/import/<format>.rs - Add detection logic in
src/import/mod.rs - Implement conversion to Heimdal format
- Write tests with sample configs
- Update wizard to include new format
Related Documentation:
Getting Started:
- Read through
src/main.rsto understand the entry point - Explore
src/config/to see how configuration works - Check
src/package/database/for package database internals - Look at
src/wizard/for interactive CLI patterns