Introduction
What is Guisu?
Guisu is a Rust-based dotfile manager. It helps you keep your configuration files (dotfiles) under version control and apply them across multiple machines, with support for templates, encryption, secret managers, hooks, and a three-way merge that detects when files have been edited locally.
Note
Early development Guisu is pre-1.0. APIs and behaviour may change between minor versions. Pin to a specific version if you depend on it.
Why a new dotfile manager?
Guisu exists because the existing tools are good but each leaves room for improvement in one dimension:
| Dimension | Guisu | Chezmoi |
|---|---|---|
| Implementation | Rust, single static binary | Go, with cgo for some features |
| Encryption | Built-in (age crate) | External age binary, or built-in |
| Git | Built-in (git2 crate) | External git binary, or built-in |
| Database | redb (pure Rust) | BoltDB (cgo) |
| Template engine | minijinja (Jinja2-compatible) | Go text/template |
| Type safety | Compile-time path types (AbsPath, RelPath) | Runtime string checks |
| Typical binary size | 3-5 MB | ~20 MB |
The goal is chezmoi parity with a smaller, faster, more self-contained binary and a friendlier API for embedding Guisu as a library.
The three-state model
Guisu manages every dotfile through three states:
Source state Target state Destination state
(repository) (after processing) (actual files)
↓ ↓ ↓
.bashrc.j2 → .bashrc (rendered) → ~/.bashrc
key.txt.age → key.txt (decrypted) → ~/key.txt
A persistent redb database tracks the content hash of the target state that was last successfully applied. On the next run, Guisu compares the new target against the on-disk destination against the database and produces a status for each file: Synced, Added, Modified, Removed, or Conflict. The interactive --interactive mode lets you resolve conflicts in a TUI.
Inspiration
Guisu is heavily inspired by chezmoi and shares its directory layout, file-attribute conventions, and overall feel.
What to read next
- Installation — get a working
guisubinary. - Getting Started — init —
guisu init. - User Guide — File Attributes — prefixes and suffixes.
- Reference — Commands — every subcommand.
- Developer Guide — Architecture — crates, state model, data flow, contributing.
Installation
From source
Requires the Rust toolchain (edition 2024, stable):
git clone https://github.com/YvanY0/guisu.git
cd guisu
cargo install --path crates/cli
The resulting binary is at ~/.cargo/bin/guisu. Add ~/.cargo/bin to your $PATH if it is not already.
Binary releases
Pre-built binaries for macOS, Linux, and Windows are linked from the releases page once they ship. Each release archives a static binary plus a SHA-256 checksum.
Verifying the install
guisu --version
guisu info
The info command prints the resolved source directory, working tree, and destination directory so you can confirm Guisu sees your repository correctly. Add --all for build info, version, public keys, and configuration; add --json for machine-readable output.
Tip
Check the source dir
guisu infowill print the resolved source dir, which is.if you run it inside a source repo, or~/.local/share/guisuif you do not. If the resolved value is not what you expect, fix it withguisu initor by setting[general] src_dirin your.guisu.toml.
Next step
Getting Started — init
guisu init is the entry point: it obtains a source repository (by cloning from GitHub or by initialising one locally) and, by default, runs an apply to materialise your dotfiles.
Clone an existing GitHub repository
guisu init username # shorthand for github.com/username/dotfiles
guisu init https://github.com/username/dotfiles.git
guisu init --ssh username # use SSH instead of HTTPS
guisu init --depth 1 username # shallow clone (faster, no history)
guisu init --no-apply username # clone but do not apply yet
guisu init clones into the source directory (default ~/.local/share/guisu) and then runs guisu apply. On the first run, apply uses interactive mode by default so you can review the changes before they land in your home directory.
Initialise a brand-new local repository
mkdir ~/dotfiles && cd ~/dotfiles
git init
cd ..
guisu init
This creates a new directory, initialises git inside it, and writes a starter .guisu.toml. The next guisu add knows where to put files.
Note
Where does the source go? By default, Guisu uses
~/.local/share/guisuas the source directory — the same convention as chezmoi. Override per-call with--sourceor globally in.guisu.tomlunder[general] src_dir.
Where things go
| Path | Default | Override |
|---|---|---|
| Source directory | ~/.local/share/guisu | --source / [general] src_dir |
| Destination directory | $HOME | --dest / [general] dst_dir |
| Persistent state DB | <source>/.guisu-state.db | (not configurable) |
Editor (for guisu edit) | $EDITOR or [general] editor | env var or config |
What init does not do
init does not commit any uncommitted changes. After cloning, the source repo is exactly as the remote left it; if you later guisu add a file, that change is staged in git but not committed.
Next step
Getting Started — add
guisu add copies a file or directory from your destination into the source repository. It strips the $HOME prefix and preserves the rest of the path verbatim.
Basic usage
guisu add ~/.bashrc # copy ~/.bashrc → source as .bashrc
guisu add ~/.config/nvim # copy a directory recursively
guisu add --encrypt ~/.ssh/id_rsa # add as age-encrypted .age file
guisu add --template ~/.gitconfig # add as a template (.j2)
guisu add ~/.config/nvim/init.vim # add a single file inside a directory
guisu add --no-git ~/.zshrc # add but do not run `git add`
add runs git add on the resulting source path by default. Pass --no-git to skip that step.
Re-adding an existing source file
guisu add --force ~/.bashrc
--force overwrites the source file with the destination’s current content. The --encrypt and --template flags compose with --force to re-add with the same attributes.
Warning
–force overwrites silently
--forcedoes not prompt. If you have local changes in the source repo (template edits, secret rotations), they are lost. Useguisu difffirst to see what would change.
Common flags
| Flag | Effect |
|---|---|
--encrypt | Store the file as <name>.age in the source. |
--template | Store the file as <name>.j2 for Jinja rendering. |
--private | Force mode 0600 on apply. |
--executable | Force mode 0755 on apply. |
--exact | Preserve the source filename as-is (no attribute inference). |
--force | Overwrite the existing source entry. |
--no-git | Skip the git add step. |
Transformations you can expect
| Input | Source file | On apply |
|---|---|---|
~/.bashrc | .bashrc | ~/.bashrc (verbatim) |
~/.ssh/config | .ssh/config | ~/.ssh/config (verbatim) |
~/.ssh/id_rsa + --encrypt | .ssh/id_rsa.age | ~/.ssh/id_rsa (decrypted, mode 0600) |
~/.config/nvim/init.vim + --template | .config/nvim/init.vim.j2 | ~/.config/nvim/init.vim (rendered) |
Next step
Getting Started — apply
guisu apply materialises the source state into the destination directory, applying templates, decrypting, and respecting conflict rules. It is the command you will run most often — usually via guisu update which is git pull + apply.
Basic usage
guisu apply # apply everything
guisu apply --dry-run # show what would change, do not write
guisu apply --interactive # prompt for conflicts in a TUI
guisu apply --force # overwrite destination without asking
guisu apply path1 path2 # apply only specific paths
guisu apply --include 'dot_*' --exclude '*.tmp'
How status is determined
For each file, Guisu compares three sources of truth:
- Target — the rendered, decrypted content the source state wants.
- Destination — the file currently on disk.
- Database — the content hash stored the last time
applysucceeded (in<source>/.guisu-state.db).
The result is one of Synced, Added, Modified, Removed, or Conflict.
| Target | Destination | Database | Status | Default action |
|---|---|---|---|---|
| A | A | A | Synced | Skip |
| A | B | A | Modified (by you) | Overwrite |
| A | A | B | Modified (in source) | Apply |
| A | B | C | Conflict | Prompt (--interactive) or overwrite |
| A | — | — | Added | Create |
| — | B | B | Removed | Delete |
| — | B | A | Modified + Removed | Conflict |
Interactive mode
--interactive opens a TUI for every conflict, showing a side-by-side diff and four actions: Overwrite, Skip, View Diff, Quit.
guisu apply --interactive
Tip
Pipe-friendly output
guisu apply --dry-runprints a list of planned changes. Combine withguisu infofor a complete preview before a real apply.
Exit codes
| Code | Meaning |
|---|---|
0 | All changes applied (or nothing to do). |
1 | An error occurred. |
Non-zero with --dry-run | A planned change would have been made. |
Hooks
apply runs pre- and post-hooks from .guisu/hooks/{pre,post}/{always,once,onchange}/ around the apply. See Hooks.
Next step
Read the User Guide for the file-attribute conventions, or jump to Templates if your dotfiles need environment-specific rendering.
File Attributes
Guisu derives per-file behaviour from three sources, in this order of precedence:
- The source file’s actual Unix mode bits (read from disk via
metadata().mode()). These are the source of truth for permissions and are propagated verbatim to the destination byapply. - The file extension on the source filename (
.j2,.age,.j2.age). The extension is stripped from the target name. - The
[remove] pathsarray in.guisu/state.toml, which declares dest paths to remove onapply.
Extensions (transformation markers)
| Suffix | Meaning | Result on apply |
|---|---|---|
.j2 | Jinja2 template | File is rendered with the template engine before write |
.age | age-encrypted | File is decrypted before any other processing |
.j2.age | Encrypted template | Decrypt first, then render |
Extensions stack in the order .j2.age (decrypt, then render). The
reverse order .age.j2 is not valid — the renderer would run on
the encrypted bytes.
File permissions
The destination file’s mode is the source file’s mode. If
~/.local/share/guisu/home/.config/zsh/zshrc is 0o644 on disk, the
rendered ~/.config/zsh/zshrc will be 0o644 after guisu apply.
guisu diff reports any mismatch between source and dest mode bits.
guisu apply chmods the destination to match the source on every run.
This is a deliberate departure from chezmoi: guisu does not encode
permissions in the filename. There is no private_ or executable_
prefix — if you want a private file, make the source file private
(chmod 600 path/to/source) and apply will propagate it. Keeping
the filename free of permission markers means a dotfiles repo can be
shared across machines with different UIDs without the filename lying
about the resulting mode.
Removing a file from the destination
To remove a file or directory from the destination on apply, list
it under [remove] paths in .guisu/state.toml:
[remove]
paths = [
"~/.cache/foo",
"~/.config/old-app",
]
- Paths are dest-relative under the destination root.
~is expanded to the user’s home; absolute paths and..traversal are rejected as a safety check. - Missing paths are silently ignored (idempotent directive).
--dry-runreports the removal without touching the filesystem.
The legacy remove_xxx filename convention is not supported — the
only way to declare a removal is via state.toml. There is currently
no CLI command for editing the remove list; edit state.toml by
hand.
Directories
The same rules apply. A directory’s mode is read from the source and
propagated to the destination. A directory with no source counterpart
(an “extra” directory in the dest) is left alone unless it is listed
under [remove].
See also
- Templates — how
.j2files are rendered. - Encryption — how
.agefiles are decrypted. - Hooks — pre/post scripts that run around
apply. - Config — how
.guisu/state.tomlis loaded.
Templates
Guisu renders .j2 files with minijinja, which implements the Jinja2 template language. Templates are evaluated after any .age decryption, so an encrypted template (.j2.age) is decrypted first and then rendered.
Variables available in templates
The template context is composed from four sources:
| Source | Keys |
|---|---|
| System | os, arch, hostname, username, home_dir |
| Environment | (read with the env function: env("HOME")) |
| Guisu | source_dir, working_tree, dest_dir, config |
| User | every key from .guisu.toml [variables] and from .guisu/variables/*.toml and platform-specific subdirectories |
Example
# ~/.bashrc — rendered on {{ os }} ({{ arch }})
{% if os == "darwin" %}
export HOMEBREW_PREFIX="/opt/homebrew"
{% elif os == "linux" %}
export HOMEBREW_PREFIX="/home/linuxbrew/.linuxbrew"
{% endif %}
export EDITOR="{{ editor }}"
export EMAIL="{{ email }}"
Built-in template functions
The full list is in Reference — Template Functions. The most-used ones:
| Function | Purpose |
|---|---|
env("NAME") | Read an environment variable (empty string if unset). |
lookPath("cmd") | Absolute path of cmd on $PATH, or empty. |
bitwarden("Item") | Fetch a Bitwarden item as an object. |
bitwardenFields("Item", "Field") | Fetch a specific field. |
include("name") | Include the raw content of another template file. |
includeTemplate("name") | Include and render a template file. |
decrypt(value) (filter) | Decrypt an inline age:base64,... string. Used as `{{ ‘value’ |
encrypt(value) (filter) | Encrypt a value for the configured recipients. |
joinPath(parts...) | Join path components portably. |
Tip
Cache expensive lookups Vault calls (
bitwarden,pass, etc.) shell out to an external process. The vault layer caches the response for the duration of a singleapplyso repeated lookups in the same template are cheap. Cross-applycaching is not implemented — everyapplyre-fetches.
Platform-specific variables
Variables can be split across multiple files under .guisu/variables/:
.guisu/variables/
├── user.toml # global
├── visual.toml # global
├── darwin/
│ ├── git.toml
│ └── terminal.toml
└── linux/
├── git.toml
└── terminal.toml
Loading order:
- Global files (any
*.tomldirectly in.guisu/variables/). - Platform-specific files from
<os>/.
The merge is per section: a platform-specific [git] table is merged into the global [git] table, key by key, rather than overwriting the whole table. This means you can override one key in darwin/git.toml without re-listing the others.
Templated configuration
A .guisu.toml.j2 is itself rendered with the same context before being parsed. This lets you vary configuration per machine without committing a per-machine file.
See also
- Reference — Template Functions
- Configuration for the full
.guisu.tomlschema. - Vault for password-manager integration.
Encryption
Guisu uses age for symmetric and asymmetric encryption. Files marked with the .age suffix are decrypted on apply.
Warning
Do not commit your age key The age identity file is the only thing that can decrypt your secrets. Never commit it to the dotfiles repo; keep it in
~/.config/guisu/(which is outside the source) or in a hardware token. If you lose the identity, the encrypted files are gone.
Generate an identity
guisu age generate -o ~/.config/guisu/key.txt
This writes a native age key. To use an existing SSH key instead, point .guisu.toml at it:
[age]
identity = "~/.ssh/id_ed25519"
derive = true # derive the recipient from the public key for encryption
When derive = true, Guisu uses the SSH public key as an age recipient. You can encrypt to the SSH public key, and the SSH private key acts as the age identity for decryption.
Add an encrypted file
guisu add --encrypt ~/.ssh/id_rsa
The source file is id_rsa.age (ASCII-armored). On apply, it is decrypted to ~/.ssh/id_rsa with mode 0600, because Guisa infers the private_ prefix from the destination path .ssh/....
Edit an encrypted file
guisu edit ~/.ssh/id_rsa
Guisu decrypts to a temp file (mode 0600), opens your $EDITOR, then re-encrypts and replaces the source on save. The temp file is securely deleted when the editor exits.
Warning
Editor backups and swap files Your editor may leave backup files (
~/.ssh/id_rsa~,.swp, etc.) on disk. Configure your editor to disable backups (set nobackup nowritebackup noswapfilein vim) or runguisu editfrom a tmpfs-backed directory.
Inline encryption in templates
For small secrets (API tokens, etc.) you can encrypt inline and embed in a template:
export GITHUB_TOKEN="{{ 'age:base64,YWdl...' | decrypt }}"
Generate an inline value with:
guisu age encrypt --inline 'ghp_xxxxxxxxxxxx'
The output is safe to commit. Decryption happens at render time and the plaintext only ever exists in memory.
Multiple recipients
[age]
recipients = [
"age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p",
"age1j0p6m6j3xcfua6jn8u6vnn7qk8h0qg5k7z2q3w..."
]
identity = "~/.config/guisu/key.txt"
Guisu encrypts to every recipient; any one identity can decrypt. Use this to give multiple machines access to the same secrets without sharing a private key.
See also
- Vault for fetching secrets from a password manager instead of committing them.
- Reference — Configuration for the
[age]section.
Vault (Secret Providers)
Guisu can pull secrets from a password manager CLI and expose them in templates. The built-in integrations are Bitwarden (bw), Roboform-bitwarden (rbw), and Bitwarden Secrets (bws).
Warning
Authentication is your responsibility Guisu does not log you in to the password manager. You must run
rbw login,bw login, orbws login(or set the appropriate env var) once per session beforeguisu apply. If the vault CLI is unauthenticated, the call will fail with a non-zero exit code.
Configure a provider
[bitwarden]
provider = "rbw" # or "bw", "bws"
The provider binary must be on $PATH and authenticated. There is no per-provider feature flag — all three providers compile in by default.
Use in a template
export GITHUB_TOKEN="{{ bitwarden('GitHub').login.password }}"
export API_KEY="{{ bitwardenFields('GitHub', 'APIKey') }}"
The first call returns the full item as a structured object; the second returns a specific custom field. Both are JSON-typed — Jinja2’s dot syntax and [] indexing both work.
Caching
The vault layer caches the response for the duration of a single apply. If your template calls bitwarden("GitHub") three times, the underlying CLI is invoked once. The cache is per-apply and is dropped at the end of the run.
Tip
Reference items by stable name Password manager item names are user-defined and can change. If you rename an item in Bitwarden, the template will fail at the next
applybecause the lookup returnsnull. Treat item names as part of your template contract.
Adding a new provider
See the add-vault-provider skill in the repository. New providers implement the SecretProvider trait in guisu-vault:
#![allow(unused)]
fn main() {
pub trait SecretProvider: Send + Sync {
fn name(&self) -> &str;
fn is_available(&self) -> bool;
fn execute(&self, args: &[&str]) -> Result<serde_json::Value>;
fn help(&self) -> &str;
}
}
The CLI for the provider must return JSON; Guisu parses it as serde_json::Value and makes it available to the template.
See also
- Templates for the broader template engine.
- Encryption for committing encrypted secrets instead of fetching them.
Hooks
Hooks are scripts that run before, after, or on change of an apply. They live under .guisu/hooks/ in the source repository, and they are version-controlled alongside your dotfiles.
Layout
.guisu/hooks/
├── pre/
│ ├── always/
│ ├── once/
│ └── onchange/
└── post/
├── always/
├── once/
└── onchange/
pre/runs before the apply begins.post/runs after the apply completes successfully.always/runs every time.once/runs at most once per script (recorded in the persistent state).onchange/runs when the script’s blake3 hash has changed since the last run.
Filename ordering
pre/always/10-install-packages.sh runs before pre/always/20-configure-shell.sh. Number prefixes control order; ties break by filename (lexicographic).
Script interpreter
The script interpreter is auto-detected from the shebang line. #!/bin/bash, #!/usr/bin/env python3, #!/usr/bin/env nix-shell, etc. all work. If there is no shebang and the file is not executable, the script fails.
Template rendering
Hook commands are rendered as templates before execution. {{ os }}, {{ guisu.source_dir }}, {{ guisu.working_tree }}, etc. are available. This is how you can branch on platform inside a single hook file:
#!/bin/bash
{% if os == "darwin" %}
brew update
{% elif os == "linux" %}
sudo apt-get update
{% endif %}
Platform filtering
You can place hooks under a platform subdirectory to scope them:
.guisu/hooks/pre/always/darwin/10-install-brew.sh
.guisu/hooks/pre/always/linux/10-install-apt.sh
Only the directory matching the current platform is executed. The hook in the unfiltered pre/always/ directory runs on every platform.
Environment variables
When a hook is invoked, the following variables are set:
| Variable | Value |
|---|---|
GUISU_SOURCE_DIR | Absolute path to the source repo |
GUISU_DEST_DIR | Absolute path to the destination root |
GUISU_WORKING_TREE | Path of the dotfiles working tree |
GUISU_PHASE | pre or post |
GUISU_MODE | always, once, or onchange |
GUISU_SCRIPT | Absolute path to the script being run |
GUISU_TARGET | (modify_ files only) The destination path the script should modify |
See also
- Reference — Configuration for the hooks section.
Configuration
Guisu reads .guisu.toml from the source directory. The file can itself be a template (.guisu.toml.j2), which lets you vary config per machine without committing per-machine files.
Quick start
[general]
color = true
progress = true
editor = "nvim"
[age]
identity = "~/.config/guisu/key.txt"
derive = true
[bitwarden]
provider = "rbw"
[variables]
email = "user@example.com"
editor = "nvim"
[ignore]
global = [".git", ".DS_Store"]
darwin = ["Thumbs.db"]
linux = ["*~"]
Sections
| Section | Purpose |
|---|---|
[general] | Top-level behaviour: paths, output, editor. |
[age] | Encryption identity and recipients. |
[bitwarden] | Password-manager provider selection. |
[ui] | Pager and diff tool overrides. |
[ignore] | Files to skip, with per-platform variants. |
[variables] | Free-form key/value map exposed as template variables. |
[hooks] | (Optional) Hook execution defaults. |
The full schema with type, default, and notes per key is in Reference — Configuration.
Templated configuration
A .guisu.toml.j2 is rendered with the same context as a regular template. Use this for per-machine values:
[general]
editor = "{{ env("EDITOR") | default(value="nvim") }}"
[variables]
hostname = "{{ hostname }}"
The file is rendered before being parsed, so the resulting TOML is what the rest of the system sees.
Note
Config file is rendered with the same context as templates
hostname,os,arch, env vars, and user variables are all available in.guisu.toml.j2. This means a config file can be both a config and a small template at the same time.
Validation
guisu info prints the resolved configuration. Use it to debug:
guisu info --all
guisu info prints the resolved configuration, the version, public keys, and a validation summary. Use --json for machine-readable output. There is no separate guisu config get / guisu config set subcommand — edit .guisu.toml directly.
See also
- Reference — Configuration — every key with type and default.
- Templates — variables and templated config.
- Encryption — the
[age]section.
Commands
This page enumerates every guisu subcommand. Run guisu <subcommand> --help for the authoritative flag list; this page is the curated overview.
Note
How the page is organised Top-level commands first, then sub-commands grouped under their parents (
age,ignored,templates,hooks). Flags shown are the curated highlights; the full set comes from--help.
Top-level commands
| Command | Purpose |
|---|---|
guisu init [TARGET] | Clone a dotfiles repo or initialise a new one locally. |
guisu add PATH... | Copy files from the destination into the source repo. |
guisu apply [PATH]... | Render the source state and write to the destination. |
guisu diff | Show the differences between target and destination. |
guisu status | Per-file status (Synced / Added / Modified / Removed / Conflict). |
guisu cat PATH | Print the rendered (target) content of a file. |
guisu edit PATH | Open the source file in $EDITOR with transparent decrypt/encrypt. |
guisu update | Fetch, merge, and apply. |
guisu info | Print resolved source / destination / version / configuration. |
guisu variables | Print all template variables available in the current context. |
guisu age | See age subcommand group. |
guisu ignored | See ignored subcommand group. |
guisu templates | See templates subcommand group. |
guisu hooks | See hooks subcommand group. |
guisu init
guisu init [PATH_OR_REPO] [FLAGS]
| Flag | Effect |
|---|---|
--apply / -a | Apply changes after initialisation (default behaviour for cloned repos). |
--depth N | Shallow clone with the given commit depth. |
--branch NAME | Clone the specified branch. |
--ssh | Use SSH instead of HTTPS when guessing the GitHub URL. |
--recurse-submodules | Clone submodules recursively. |
PATH_OR_REPO accepts a local path, a GitHub username (shorthand for github.com/username/dotfiles), or a full URL. Defaults to ~/.local/share/guisu.
guisu add
guisu add PATH... [FLAGS]
| Flag | Effect |
|---|---|
--encrypt | Store the file as <name>.age in the source. |
--template | Store the file as <name>.j2 for Jinja rendering. |
--private | Force mode 0600 (or 0700 for directories). |
--executable | Force mode 0755. |
--exact | Preserve the source filename as-is; do not infer attributes. |
--force | Overwrite an existing source entry. |
--no-git | Skip the git add step. |
guisu apply
guisu apply [PATH]... [FLAGS]
| Flag | Effect |
|---|---|
--dry-run | Show planned changes without writing. |
--force | Overwrite destination without prompting. |
--interactive | Open the TUI for every conflict. |
--include PATTERN | Only apply files matching the pattern. |
--exclude PATTERN | Skip files matching the pattern. |
--no-apply | (Used by init.) Skip the apply step. |
guisu diff
guisu diff [PATH]... [FLAGS]
Shows a unified diff between target and destination. Honours --include / --exclude and the same source/destination overrides as apply.
guisu status
guisu status [PATH]... [FLAGS]
Per-file status output, one line per entry. --json emits machine-readable output.
guisu cat
guisu cat PATH
Print the rendered (decrypted + templated) target content to stdout. Useful for debugging templates without applying.
guisu edit
guisu edit PATH
Decrypt (if .age), open in $EDITOR, re-encrypt on save. Temp file is 0600 and securely deleted on editor exit.
guisu update
guisu update [FLAGS]
Fetch from the source repo’s remote, merge (or rebase with --rebase), and run apply. --no-apply skips the apply step.
guisu info
guisu info [FLAGS]
Print the resolved source directory, working tree, destination directory, and version. With --all, also print build info, public keys, and configuration. With --json, machine-readable.
Tip
Use info for first-time setup debugging If
guisu applycannot find your source dir, runguisu info --allto see what Guisu resolved. Most “why is nothing happening?” questions answer themselves in this output.
guisu variables
guisu variables [FLAGS]
Print every variable available in the current template context: system, guisu, user-defined, and platform-specific. --builtin shows only system + guisu; --user shows only user-defined; --json emits machine-readable.
Subcommand groups
guisu age
| Subcommand | Purpose |
|---|---|
guisu age generate -o FILE | Create a new age identity. |
guisu age encrypt FILE | Encrypt a file with the configured recipients. |
guisu age decrypt FILE | Decrypt a file with the configured identities. |
guisu age encrypt --inline VALUE | Emit an inline-encrypted string for use in templates. |
guisu age decrypt --inline VALUE | Decrypt an inline-encrypted string. |
guisu age recipients | List the configured recipients. |
guisu ignored
| Subcommand | Purpose |
|---|---|
guisu ignored list | List currently ignored files and the patterns that matched. |
guisu ignored add PATTERN | Add a pattern to .guisu.toml [ignore]. |
guisu ignored remove PATTERN | Remove a pattern. |
guisu templates
| Subcommand | Purpose |
|---|---|
guisu templates execute PATH | Render a template with the current context (no apply). |
guisu hooks
| Subcommand | Purpose |
|---|---|
guisu hooks run PHASE | Manually execute pre or post hooks for the given phase. |
guisu hooks list | List all discovered hooks with their resolved mode. |
guisu hooks show HOOK | Show the script path and the environment variables it would receive. |
Global flags
Most top-level commands accept:
| Flag | Effect |
|---|---|
--source DIR | Override the source directory. Also reads GUISU_SOURCE_DIR. |
--dest DIR | Override the destination directory. Also reads GUISU_DEST_DIR. |
--config FILE | Override the config file path. Also reads GUISU_CONFIG. |
--log-file FILE | Mirror logs to this file. Also reads GUISU_LOG_FILE. |
--color WHEN | auto / always / never. |
--progress WHEN | auto / always / never. |
Template Functions
The full list of functions and filters registered with the minijinja environment, grouped by category. Run guisu variables to inspect what is available in a given context.
Note
Functions vs filters A function is called with parentheses:
fn(arg). A filter is applied with the pipe operator:value | filter. The same underlying operation may exist as both — see the encryption row below.
System
| Function | Returns | Notes |
|---|---|---|
os() | string | "darwin", "linux", or "windows". |
arch() | string | "x86_64", "aarch64", "arm". |
hostname() | string | Machine hostname. |
username() | string | Current OS user. |
home_dir() | string | $HOME. |
Environment
| Function | Returns | Notes |
|---|---|---|
env("NAME") | string | Empty string if unset. |
lookPath("cmd") | string | Absolute path of cmd on $PATH, or empty if not found. |
Paths
| Function | Returns | Notes |
|---|---|---|
joinPath("a", "b", "c") | string | Join path components portably. |
Vault (Bitwarden)
All four functions are available when the [bitwarden] provider is configured. Caching is per-apply.
| Function | Returns | Notes |
|---|---|---|
bitwarden("Item") | object | The full Bitwarden item as a JSON object. |
bitwardenFields("Item", "Field") | value | A specific custom field on the item. |
bitwardenAttachment("Item", "filename") | string | An attachment’s contents as a string. |
bitwardenSecrets("Item", "Field") | string | A secret field (Bitwarden Secrets only). |
Templates
| Function | Returns | Notes |
|---|---|---|
include("name") | string | The raw content of another template file. |
includeTemplate("name") | string | Include and render a template file with the current context. |
Encryption
These are filters (not functions) — they go on the right side of a |.
| Filter | Returns | Notes |
|---|---|---|
value | decrypt | string | Decrypt an inline age:base64,... string. |
value | encrypt | string | Encrypt a string for the configured recipients. |
export TOKEN="{{ 'age:base64,YWdl...' | decrypt }}"
Strings
| Function | Returns | Notes |
|---|---|---|
regexMatch(pattern, string) | bool | True if the regex matches anywhere. |
regexReplaceAll(pattern, replacement, string) | string | Replace all matches. |
split(separator, string) | list | Split a string into a list. |
join(separator, list) | string | Join a list into a string. |
String filters
| Filter | Returns | Notes |
|---|---|---|
s | quote | string | Surround with double quotes; escape inner quotes. |
s | trim | string | Strip leading and trailing whitespace. |
s | trimStart | string | Strip leading whitespace. |
s | trimEnd | string | Strip trailing whitespace. |
s | custom | string | Uppercase the string. (Built-in alias; useful for templates.) |
Data formats
| Filter | Returns | Notes |
|---|---|---|
value | toJson | string | Serialise to JSON. |
s | fromJson | value | Parse JSON. |
value | toToml | string | Serialise to TOML. |
s | fromToml | value | Parse TOML. |
Hashing
| Filter | Returns | Notes |
|---|---|---|
s | blake3sum | string | Hex-encoded BLAKE3-256 of the input. |
Tip
Generated inline values Run
guisu age encrypt --inline "my secret"to print an inline-encrypted value. The output can be committed safely and embedded in templates as{{ 'value' | decrypt }}.
See also
- User Guide — Templates for the broader template engine and platform-specific variables.
- User Guide — Encryption for the
.agesuffix and identity management. - User Guide — Vault for the Bitwarden integration.
Configuration Reference
The full schema of .guisu.toml. Every key is optional; Guisu applies a default if a key is missing. The file is rendered as a Jinja template if its name is .guisu.toml.j2.
Note
Derived from source This page is derived from
crates/config/src/config.rsandcrates/config/src/ignores.rs. The authoritative list is the source — if a key is added or removed, the source wins.
[general]
| Key | Type | Default | Notes |
|---|---|---|---|
src_dir | path | ~/.local/share/guisu | Source repository location. |
dst_dir | path | ~ | Destination root. |
root_entry | path | (resolved at runtime) | Every target path is relative to this subpath. |
color | bool | true | ANSI colour in output. |
progress | bool | true | Progress bars. |
editor | string | $EDITOR | Editor used by guisu edit. |
editor_args | list of string | [] | Extra args passed to the editor. |
use_builtin_age | enum | auto | auto / true / false. Use the in-process age implementation. |
use_builtin_git | enum | auto | auto / true / false. Use the in-process git implementation. |
[age]
| Key | Type | Default | Notes |
|---|---|---|---|
identity | path | — | Single age or SSH identity file. |
identities | list of path | [] | Multiple identity files; any one can decrypt. |
recipient | string | — | Single age recipient. |
recipients | list of string | [] | Multiple recipients; encryption writes to all. |
derive | bool | false | Derive a recipient from the SSH identity for encryption. |
fail_on_decrypt_error | bool | false | Fail apply if a .age file cannot be decrypted. The default is to log a warning and skip. |
[bitwarden]
| Key | Type | Default | Notes |
|---|---|---|---|
provider | string | "rbw" | One of bw, rbw, bws. |
[ui]
| Key | Type | Default | Notes |
|---|---|---|---|
icons | enum | auto | auto / text / symbols. How status and diff mark file types. |
diff_format | string | "unified" | One of unified, side-by-side. Used by the conflict TUI. |
context_lines | integer | 3 | Lines of context around a change in diff output. |
preview_lines | integer | 10 | Lines of preview in the conflict TUI. |
[edit]
| Key | Type | Default | Notes |
|---|---|---|---|
apply | bool | true | Whether guisu edit should apply after a successful save. Set to false for read-only editing of templates. |
[ignore]
| Key | Type | Default | Notes |
|---|---|---|---|
global | list of string | [] | Always ignored, regardless of platform. |
darwin | list of string | [] | macOS-only ignores. |
linux | list of string | [] | Linux-only ignores. |
windows | list of string | [] | Windows-only ignores. |
Patterns are gitignore-style: glob with *, ?, **, negation with !, and trailing / for directory-only.
[variables]
Free-form key/value map. Every key is exposed as a top-level variable in the template context. See User Guide — Templates.
[variables]
email = "user@example.com"
editor = "nvim"
work = true
servers = ["srv1", "srv2", "srv3"]
Resolving the effective config
- Defaults (compiled in).
.guisu.tomlin the source directory (rendered as template if.guisu.toml.j2).- Per-platform files in
.guisu/ignores/{darwin,linux,windows}.toml(only for[ignore]). - Environment variables (
GUISU_SOURCE_DIR,GUISU_DEST_DIR,GUISU_CONFIG,GUISU_LOG_FILE). - Command-line flags.
Run guisu info --all to see the merged result.
See also
- User Guide — Configuration for a curated overview and a templated-config example.
Architecture
Guisu is a Cargo workspace with 7 crates organised in strict layers. Higher layers depend on lower layers, never the reverse. There are no circular dependencies; cargo tree --workspace --no-dedupe confirms it.
Dependency table
The arrows below are read as “depends on”. The table lists every cross-crate dependency; an absent row means the crate is fully self-contained.
| ↓ depends on → | core | crypto | vault | template | config | engine | cli |
|---|---|---|---|---|---|---|---|
core (Layer 0) | — | ||||||
crypto (Layer 1) | ✓ | — | |||||
vault (Layer 1) | ✓ | — | |||||
template (Layer 2) | ✓ | ✓ | ✓ | — | |||
config (Layer 2) | ✓ | ✓ | ✓ | — | |||
engine (Layer 3) | ✓ | ✓ | ✓ | ✓ | ✓ | — | |
cli (Layer 4) | ✓ | ✓ | ✓ | ✓ | ✓ | — |
Read row-by-row:
corehas no dependencies (onlystd).cryptoandvaultdepend only oncore.templatedepends oncore,crypto,vault(so it can call encryption and vault lookups during rendering).configdepends oncore,crypto,template(so.guisu.toml.j2can render with the same context as regular templates).enginedepends on all ofcore,crypto,vault,template,config(it orchestrates the apply pipeline).clidepends on every other crate (it is the user-facing binary).
Layer responsibilities (one-line summary)
| Crate | Layer | Role |
|---|---|---|
guisu-core | 0 | Newtype path types, platform detection, error types. Only depends on std. |
guisu-crypto | 1 | age identity loading, encryption, decryption, file + inline. |
guisu-vault | 1 | SecretProvider trait; built-in bw and rbw in bw.rs, bws in bws.rs. |
guisu-template | 2 | minijinja env, ~30 functions and filters, platform-aware context. |
guisu-config | 2 | .guisu.toml loading, per-platform variables, path resolution. |
guisu-engine | 3 | Three-state model, persistent redb state, parallel processing. |
guisu-cli | 4 | clap argument parsing, command implementations, conflict TUI. |
Module map
guisu-engine is the largest crate. Its src/ layout is:
| File | What it contains |
|---|---|
state.rs | SourceState, TargetState, DestinationState, PersistentState trait, redb-backed implementation. |
entry.rs | SourceEntry, TargetEntry, DestEntry, EntryKind enums. |
attr.rs | FileAttributes (bitflags: DOT, PRIVATE, READONLY, EXECUTABLE, TEMPLATE, ENCRYPTED). |
content.rs | The raw byte pipeline: read, decrypt, render. |
processor.rs | ContentProcessor<D, R> — generic decrypt + render pipeline. |
database.rs | redb table definitions. |
hash.rs | BLAKE3 helpers. |
modify.rs | In-place modification of modify_* files. |
system.rs | Platform-specific destination state reads. |
validator.rs | Cross-state validation (e.g. is the source well-formed?). |
git.rs | In-process git operations (init, fetch, merge) using git2. |
hooks/ | Pre/post/once/onchange hook discovery and execution. |
adapters/ | Adapters to alternative implementations (e.g. an HTTP-based SourceState reader for tests). |
Why strict layers?
The strict layering buys two things:
- Bounded compile time. A change to
guisu-corerebuilds the whole workspace; a change toguisu-clirebuilds onlyguisu-cliand the libraries it transitively depends on (which is everything in practice, but the layer check still catches accidental layering violations). - Substitutability. Library users can pick a subset: a CI tool that just renders templates and never touches the engine can depend on
guisu-templatealone.
See also
- Three-State Model — what the engine does.
- Data Flow — how a request moves through the layers.
- Crates — per-crate public API and responsibility.
Three-State Model
The engine keeps three views of every file under management, plus a persistent record of what was last applied. Comparing the four is what makes conflict detection and safe interactive resolution possible.
The four stores
The engine keeps four views of every file under management. The arrows below describe how data flows from one store to the next during an apply.
SOURCE TARGET DESTINATION PERSISTENT
(filesystem) (in memory) (filesystem) (redb)
.bashrc.j2 ───► .bashrc (rendered) ──► ~/.bashrc ◄──► blake3(.bashrc)
key.txt.age ───► key.txt (decrypted)──► ~/key.txt ◄──► blake3(key.txt)
decrypt + render write + chmod record
Source → Target : decrypt .age, then render .j2
Target → Destination: write to disk, apply mode
Target ↔ Persistent : hash target, store in db
Target ↔ Destination: three-way compare to detect Synced/Modified/Conflict
The bold arrows show the writes the apply loop performs. The plain arrows are reads that drive the comparison.
| Store | Where it lives | Mutable? | Notes |
|---|---|---|---|
| Source | Files in the source repository (filesystem) | Read-only during apply | Filenames encode extensions (.j2, .age, .j2.age); file mode bits come from metadata().mode(). |
| Target | Rendered, decrypted content (in memory) | Recomputed on demand | Always the desired post-apply state for a given source. |
| Destination | The actual files on the user’s machine (filesystem) | Read + Write | Where the user’s dotfiles actually live. |
| Persistent | redb database at <source>/.guisu-state.db | Written after a successful apply | Content hash + mode of the last applied target. |
Status types
For each file under management, the engine computes a status by comparing target, destination, and database:
| Target | Destination | Database | Status | Default action |
|---|---|---|---|---|
| A | A | A | Synced | Skip |
| A | B | A | Modified (by you) | Overwrite |
| A | A | B | Modified (in source) | Apply |
| A | B | C | Conflict | Prompt (--interactive) or overwrite |
| A | — | — | Added | Create |
| — | B | B | Removed | Delete |
| — | B | A | Modified + Removed | Conflict |
Entry types
crates/engine/src/entry.rs defines three enums and a struct:
#![allow(unused)]
fn main() {
pub enum SourceEntry {
File { source_path, target_path, attributes },
Directory { source_path, target_path, attributes },
Symlink { source_path, target_path, link_target },
}
pub enum TargetEntry {
File { path, content: Vec<u8>, mode: Option<u32> },
Directory { path, mode: Option<u32> },
Symlink { path, target: PathBuf },
Remove { path },
}
pub struct DestEntry {
pub path: RelPath,
pub kind: EntryKind,
pub content: Option<Vec<u8>>,
pub mode: Option<u32>,
pub link_target: Option<PathBuf>,
}
pub enum EntryKind {
File,
Directory,
Symlink,
Missing,
}
}
SourceEntry and TargetEntry differ slightly because the source carries parsed attributes while the target carries concrete content.
Destinations may also receive a remove directive from
Metadata::remove (declared in .guisu/state.toml); see
User Guide — File Attributes.
The apply step processes removes in a separate pre-pass before
applying targets.
File attributes
FileAttributes in crates/engine/src/attr.rs is a plain struct:
#![allow(unused)]
fn main() {
pub struct FileAttributes {
pub is_template: bool, // .j2 extension
pub is_encrypted: bool, // .age extension
pub mode: Option<u32>, // source file's metadata().mode()
}
}
is_template and is_encrypted are decoded from the filename
extension at read time. mode is read from the source file’s
metadata().mode() and propagated to the destination by apply. The
permission-related bitflags (private_ / readonly_ / executable_
/ dot_ / exact_) and the entry-type prefixes (modify_ /
remove_ / symlink_) are no longer recognized. See
User Guide — File Attributes.
See also
- Data Flow — apply / init / add / update flows.
- Crates — guisu-engine — the public API for state types.
Data Flow
This page traces the major commands through the layers. Each flow is the same shape: parse args → load config → load identities → read source → build target → compare with destination and database → resolve conflicts → write → update database.
guisu apply
The core command. Materialises source into destination. The flow is:
- Parse CLI args —
--interactive,--dry-run,--include,--exclude, source/destination overrides. - Load
.guisu.toml+ any platform-specific variable files; merge them. - Load age identities from the configured identity files (age keys or SSH keys).
- Build the template engine and a context populated with system info, guisu metadata, and user variables.
- Read
SourceState— walk the source directory in parallel via rayon; parse file attributes from each filename; buildSourceEntryobjects. - Build
TargetState— for each source entry, decrypt.agefiles and render.j2templates, again in parallel. - Open the redb database at
<source>/.guisu-state.db. - For each entry in the target state (sequential, so writes are deterministic):
- Read the corresponding
DestinationStateentry (the actual file on disk). - Load the database entry (the last applied hash + mode).
- Three-way compare the three to compute a
FileStatus. - Resolve the status:
Synced— skip.Added/Modified— write the target content with the target mode.Conflict— if--interactive, open the TUI; otherwise overwrite.- User can also
Quitfrom the TUI, which aborts the entire apply.
- Update the database with the new hash and mode.
- Read the corresponding
- Show stats — counts of added / modified / skipped / errored entries.
Steps 5 and 6 use rayon for parallel processing. Steps 8 and 9 are sequential so writes happen in a deterministic order and a mid-apply crash leaves the destination in a recoverable state.
guisu init
- Parse the target — a local path, a GitHub username, or
owner/repo. - Determine the source directory (default
~/.local/share/guisu). - If the source directory already exists, error out.
- Clone the repository (in-process via the
git2crate). - Run
guisu applyin interactive mode so the user can review the changes before they land.
guisu add
- Resolve the path: expand
~, make it absolute, verify it exists. - Compute the target path: strip the
$HOMEprefix. - If
--encryptwas passed, encrypt the content and append.ageas the suffix.--templateinstead appends.j2. Otherwise the file is used as-is. - Copy the (possibly transformed) file to the source directory, preserving metadata.
- Run
git addon the resulting source path.
--private and --executable do not change the file’s location, only the attributes Guisa applies on the next apply.
guisu update
- Open the source repo via
git2. git fetch origin.- If
--rebase, rungit rebase; otherwisegit merge. - If there are conflicts, error out (the user must resolve them manually).
- Run
guisu apply.
guisu edit
- Map the destination path back to the source path (look for
.j2/.agesuffixes). - If the source is an
.agefile, decrypt to a temp file (mode0600). - Open the source (or temp) file in
$EDITORand wait for it to exit. - If the content’s hash changed, and the source is
.age, re-encrypt and replace the source. Otherwise just replace the source. - Delete the temp file.
The temp file lives on the same filesystem as the source, with mode 0600, and is unlink()-ed when the editor exits. Secure erasure (overwrite before unlink) is on the roadmap but not implemented.
Parallel processing
The engine uses rayon for two passes:
| Pass | Operation | What parallelises |
|---|---|---|
| Read source | SourceState::read(root) | One task per file system entry: read bytes, parse attributes, build SourceEntry. |
| Build target | TargetState::from_source(source, processor, context) | One task per source entry: decrypt (if .age), render template (if .j2), build TargetEntry. |
Sequential steps (write, update db, compare) stay single-threaded to keep the on-disk state coherent and the diff output stable.
See also
- Three-State Model — the four stores the flows above touch.
- Crates — guisu-engine — the types in the flow.
Crates
Detailed per-crate reference. The seven crates form a strict DAG; see Architecture for the diagram. Module paths below are crates/<name>/src/....
guisu-core
Foundation types shared by every other crate. Zero non-std dependencies.
| Module | Types |
|---|---|
path.rs | AbsPath, RelPath — newtype wrappers around PathBuf that cannot be mixed at compile time. |
platform.rs | Platform { os, arch } and the CURRENT_PLATFORM constant. |
traits.rs | Shared traits: AsAbsPath, AsRelPath. |
error.rs | Error enum with the variants every crate needs. |
#![allow(unused)]
fn main() {
let p = AbsPath::new("/home/user/.config/guisu")?;
let r = p.join(RelPath::new("dotfiles")?);
}
guisu-crypto
age encryption, file and inline. Built on the age crate.
| Module | Public API |
|---|---|
age.rs | decrypt_file_content, encrypt_file_content, decrypt_inline, encrypt_inline. |
identity.rs | load_identities(path, is_ssh) — read age or SSH identity files. |
recipient.rs | parse_recipient(string) and derive_recipients(identities) — used during encryption. |
Encryption writes to all configured recipients; decryption tries each identity in order until one works.
guisu-vault
Secret provider abstraction over password-manager CLIs.
#![allow(unused)]
fn main() {
pub trait SecretProvider: Send + Sync {
fn name(&self) -> &str;
fn is_available(&self) -> bool;
fn execute(&self, args: &[&str]) -> Result<serde_json::Value>;
fn help(&self) -> &str;
}
}
| File | Provider |
|---|---|
bw.rs | BwCli (Bitwarden CLI) and RbwCli (unofficial Rust Bitwarden CLI). |
bws.rs | BwsCli (Bitwarden Secrets). |
The cache lives in guisu-template (per-apply), not here.
guisu-template
minijinja environment with a curated function library.
| Module | What it contains |
|---|---|
engine.rs | TemplateEngine::new() and the two with_* constructors. add_function / add_filter calls register every function. |
context.rs | TemplateContext — the typed bag of variables injected into every render. |
functions/ | One file per category: system.rs, vault.rs, strings.rs, data.rs, crypto.rs, files.rs. |
info.rs | Helper for guisu info to summarise the template engine state. |
#![allow(unused)]
fn main() {
let engine = TemplateEngine::with_identities_and_template_dir(
identities,
Some(template_dir),
);
let rendered = engine.render_str(template, &context)?;
}
guisu-config
Loads and merges .guisu.toml plus platform-specific variable files.
| Module | What it contains |
|---|---|
config.rs | The Config struct and sub-configs (GeneralConfig, AgeConfig, BitwardenConfig, UiConfig, IgnoreConfig, EditConfig). |
dirs.rs | resolve_dirs — applies [general] src_dir / dst_dir to the runtime context. |
ignores.rs | IgnoresConfig and the loader for per-platform ignore files. |
patterns.rs | IgnoreMatcher — gitignore-style pattern compilation. |
variables.rs | Per-platform variable loading (.guisu/variables/{darwin,linux,windows}/*.toml). |
#![allow(unused)]
fn main() {
let config = Config::load_from_source(source_dir)?;
let patterns = config.platform_ignore_patterns();
}
guisu-engine
The three-state model and the apply loop. The largest crate by line count.
| Module | What it contains |
|---|---|
state.rs | SourceState, TargetState, DestinationState, PersistentState trait + RedbPersistentState, EntryState, HookState. |
entry.rs | SourceEntry, TargetEntry, DestEntry, EntryKind. |
attr.rs | FileAttributes (plain struct: is_template, is_encrypted, mode). |
content.rs | The raw byte pipeline. |
processor.rs | ContentProcessor<D, R> — generic decrypt + render pipeline. |
database.rs | redb table definitions. |
hash.rs | BLAKE3 helpers. |
system.rs | Platform-specific destination state reads. |
validator.rs | Cross-state validation. |
git.rs | In-process git operations (init, fetch, merge) using git2. |
hooks/ | Pre/post/once/onchange hook discovery and execution. |
adapters/ | Alternative implementations (e.g. for tests). |
guisu-cli
Binary + library. The binary entry point is src/main.rs, which delegates to guisu::run(cli).
| Module | What it contains |
|---|---|
lib.rs | Cli (clap derive) and Commands enum. |
cmd/ | One file per subcommand: add.rs, age.rs, apply.rs, cat.rs, diff.rs, edit.rs, hooks.rs, ignored.rs, info.rs, init.rs, status.rs, templates.rs, update.rs, variables.rs. |
command.rs | The Command trait implemented by every subcommand. |
common.rs | RuntimeContext — shared state (config, paths, etc.) passed to every command. |
conflict.rs | The interactive conflict TUI. |
ui/ | Reusable TUI widgets. |
stats.rs | ApplyStats — counted output of an apply. |
#![allow(unused)]
fn main() {
impl Command for ApplyCommand {
type Output = ApplyStats;
fn execute(&self, ctx: &RuntimeContext) -> Result<ApplyStats> { ... }
}
}
Public surface stability
The guisu-cli binary’s command-line interface is stable. The library crates (guisu-core, guisu-crypto, guisu-vault, guisu-template, guisu-config, guisu-engine) are not yet API-stable; expect breaking changes before v1.0. See the Roadmap for the stabilisation timeline.
See also
- Architecture — layer diagram.
- Three-State Model — the engine’s core types.
Error Handling
Guisu uses a two-tier error strategy, mirrored by AGENTS.md “Rules”:
- Libraries (
guisu-core,guisu-crypto,guisu-engine, …) — typed errors viathiserror. The error enum ispuband variants are matchable. Errors are converted toanyhow::Errorat the boundary. - CLI (
guisu-cli) —anyhow::Resultat the boundary. Library errors are converted via?and.context(...)is added at the call site.
Library error example
#![allow(unused)]
fn main() {
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("failed to read source file {path}: {source}")]
ReadSource {
path: String,
#[source]
source: std::io::Error,
},
#[error("template error at {location}: {message}")]
Render { location: String, message: String },
#[error("decrypt failed for {path}: {source}")]
Decrypt {
path: String,
#[source]
source: guisu_crypto::Error,
},
}
}
Key conventions:
- One error enum per crate, named simply
Errorand re-exported ascrate::Error. #[source]chains the underlying cause so it shows up in theDisplayoutput andstd::error::Error::source()chain.#[from]is used sparingly — most conversions go through explicitmap_errcalls so the user-visible error stays short.
CLI usage
#![allow(unused)]
fn main() {
use anyhow::Context;
fn run() -> anyhow::Result<()> {
let state = engine::read_source(&path)
.context("failed to read source state")?;
let target = engine::build_target(&state, &processor, &context)
.context("failed to build target state")?;
engine::apply(&target, &dest, &apply_opts)
.context("apply failed")?;
Ok(())
}
}
The .context(...) calls add a one-line breadcrumb that miette renders as part of the error chain. crates/cli/src/main.rs configures miette to print the full chain in a coloured, code-aware format.
Logging
tracing is used for structured logging. Levels: ERROR, WARN, INFO, DEBUG, TRACE. RUST_LOG=info is the default for the CLI; raise it with RUST_LOG=guisu_engine=debug for a single crate.
#![allow(unused)]
fn main() {
use tracing::{info, warn, error, debug, instrument};
#[instrument(skip(content))]
fn process_file(path: &Path, content: &[u8]) -> Result<()> {
debug!(path = %path.display(), size = content.len(), "processing file");
// ...
info!(path = %path.display(), "file processed successfully");
Ok(())
}
}
When to use unwrap
Almost never in library code. The two acceptable places are:
- In tests (
#[cfg(test)] mod tests) where a panic is a clear test failure. - In
build.rsor other build-time code where a panic is acceptable because the build itself is the only thing that can fail.
Application code (guisu-cli) uses anyhow::Context and lets miette display the error. Library code uses ? and typed errors.
See also
- Contributing — the rules for adding new errors.
- AGENTS.md — the project-level rule “No bare
unwrap()— use?with anyhow”.
Contributing
Thanks for your interest in contributing. Guisu is pre-1.0; expect breaking API changes in the library crates.
Documentation
Warning
Update docs in the same PR Every PR that adds or changes a user-facing command, flag, config key, file-attribute convention, template function, or hook mode must update the relevant page under
docs/src/{user-guide,reference}/in the same PR. The PR template (below) has a matching checklist.
Checklist before opening a PR:
- If the PR adds a new subcommand, update Reference — Commands and add a “Common usage” example to the relevant user-guide page.
- If the PR adds a template function or filter, update Reference — Template Functions with the function’s name, signature, and a one-line description. Mark the row as a function or filter explicitly.
- If the PR adds a config key, update Reference — Configuration with type, default, and notes.
- If the PR changes file-attribute behaviour, update User Guide — File Attributes.
- If the PR changes hook semantics, update User Guide — Hooks.
- If the PR changes the CLI global flags (
--source,--dest,--config,--log-file,--color,--progress), update Reference — Commands. - If the PR adds a new shell or installer integration (e.g. extending
guisu completion), update README — Shell completion with install instructions for the new shell.
The site is English-only. A Chinese translation is not in scope; if one is added later, it is a separate effort and tracked elsewhere.
Development setup
git clone https://github.com/YvanY0/guisu.git
cd guisu
cargo build
cargo test --workspace
cargo clippy --workspace -- -D warnings
cargo fmt -- --check
Recommended tools:
mise— Rust toolchain + project tasks (seerust-toolchain.toml).pre-commit— see.pre-commit-config.yaml. Install once withpre-commit install.cargo-nextest— faster test runner (cargo nextest run).
Pull request process
- Branch from
main. Use a short descriptive prefix:feat/...,fix/...,docs/...,refactor/.... - Commit with
-sand-S(DCO + GPG/SSH signature). The repo enforces both. SeeAGENTS.md“Commit rules”. - Run the build & verify commands above. They must all pass.
- Open a PR with a clear title and a short description of the change. Reference any related issues.
- Address review feedback. Squash-merge is preferred for a linear history.
PR template
## Description
Brief description of changes.
## Motivation
Why is this change needed?
## Changes
- List of changes
- Another change
## Documentation
- [ ] Updated the relevant docs page (which one: ...)
- [ ] No docs update needed (reason: ...)
## Testing
How was this tested?
## Checklist
- [ ] Tests added/updated
- [ ] `cargo fmt -- --check` passes
- [ ] `cargo clippy --workspace -- -D warnings` passes
- [ ] `cargo test --workspace` passes
- [ ] Commit signed (`-s -S`)
Architecture guidelines
- Follow the existing layered architecture — see Architecture. Lower layers never depend on higher layers.
- Use newtype paths (
AbsPath,RelPath), not rawPathBuf. - Use
?with.context(...)in the CLI, not bareunwrap(). Library errors usethiserror; see Error Handling. - For new template functions, follow the existing per-category layout in
crates/template/src/functions/. One file per category; one function perpub fnwith a doctest. - For new vault providers, implement
SecretProviderincrates/vault/src/. See theadd-vault-providerskill for the full checklist.
Adding a new subcommand
- Create
crates/cli/src/cmd/<name>.rswith a#[derive(Args)]struct and aCommandimpl. - Add a variant to
Commandsincrates/cli/src/lib.rswith a docstring. - Add a row to Reference — Commands.
- Add tests under
crates/cli/src/cmd/<name>_test.rs(or inline with#[cfg(test)]).
See also
- Architecture
- Three-State Model
- Crates
- Error Handling
- AGENTS.md — the project rules every agent follows.