Overview
Singularity Dotfiles Manager is a robust, extendable CLI tool designed to automate the installation and management of your system configuration and application ecosystem. Built with Rust, and powered by the Rhai scripting engine for ultimate flexibility.
Key Features
- Release-Based Configuration: Deploy entire dotfile environments from a single JSON release config.
- Programmable Hooks: Use the built-in Rhai Hook API to run logic at specific points during the installation process.
- App Suites: Install from curated lists of applications tailored to your specific dotfiles.
- Developer Friendly: Full API documentation.
- Cross-Distribution Ready: Intelligent path resolution (including
~) and distribution detection.
Installation
The Singularity Dotfiles Manager is written in Rust. You will need the Rust toolchain installed to build the application from source. Use the following command to install the dotfiles manager directly from git:
cargo install --git https://github.com/JTreske/singularity-dotfiles-manager.git
Usage
Find help using the singularity-dotfiles-manager by running:
singularity-dotfiles-manager --help
Recommended Dotfiles Structure
.
├── 📂 apps/
│ ├── 📄 arch.json # App suite for Arch and Arch-like distros
│ └── 📄 ... # App suites for other systems
├── 📂 dependencies/ # Package dependencies for the dotfiles repo separated by newline
│ ├── 📄 arch # Packages list for Arch and Arch-like distros
│ └── 📄 ... # Packages lists for other distros
├── 📂 dotfiles/ # The part of the repository that is symlinked to the home directory
│ ├── 📂 .config/
│ │ └── ...
│ └── ...
├── 📂 hooks/
│ ├── 📜 arch.rhai # Hooks script for Arch and Arch-like distros
│ └── 📜 ... # Scripts for other distributions
├── 📄 dotfiles-rolling.json # Release config for rolling release on main
├── 📄 dotfiles-stable.json # Release config for stable release
└── 📄 install.sh # Optional bootstrap script
Install Dotfiles
The core command of Singularity. It pulls a dotfiles repository based on a configuration file.
singularity-dotfiles-manager install https://example.com/release-config.json
The dotfiles release config is a detailed description of your dotfiles repository. Find the JSON schema here.
Extend the installation workflow with Rhai hook scripts as described in Hooks API.
App Suite
Let the user install from a suite of applications defined in your profile. The app suite is defined in JSON. Find the JSON schema here.
singularity-dotfiles-manager apps
Hooks API
The application supports custom scripting via Rhai, a lightweight embedded scripting language for Rust. Hooks allow you to extend the application’s behavior at specific points in the dotfiles installation workflow, using a simple, powerful syntax.
Requirements
The application looks for a script file, named <os-name>.rhai (e.g., arch.rhai), within the configured hooks directory.
Script Structure
Your script is processed in two stages:
- Initialization: The script is compiled and executed from top to bottom. This is where you should define global variables (stored within the
Scope). - Hook Execution: The application searches for specific functions within your script. These functions must take no parameters. If a function is defined, it is executed; if it is missing, the application simply continues.
Hook Functions
The following functions can be implemented in your script:
pre_backup()- Executed before the backup of the active profile is taken.post_backup_pre_deps()- Executed after the backup is created and before the dependencies of the new dotfiles repository are installed.post_deps_pre_install()- Executed after the dependencies are installed and before the new dotfiles are installed into a profile.post_install_pre_restore()- Executed after the installation of the dotfiles into the profile and before the selected restore items are copied from the backup to the new active profile (only in case of an update).post_restore_pre_link()- Executed after the restore is complete and before the files from defined dotfiles directory in the active profile are linked to the users home directory.finalize()- Executed after the linking is complete.
What the Application Provides
When your script runs, the application injects several constants and modules into the Global Scope. These are available everywhere in your script.
Constants
| Name | Type | Description |
|---|---|---|
info | HookInformation | Contains context about the current run (paths, flags). |
runner | Runner | A handle to the internal system runner for package management. |
HookInformation
update: bool - whether this is an update or a new installation of the dotfiles
backup: bool - whether backup is performed or not (backups are always performed if profile exists)
install_dir: ImmutableString - the directory where the profile is located (defaults to ~/.mydotfiles)
profile_dir: ImmutableString - the profiles directory (<install_dir>/<dotfiles-id>)
dotfiles_dir: ImmutableString - the dotfiles directory defined within the profile_dir
The api Module
The core of your scripting power comes from the api module. It provides three main namespaces:
api::log: Functions for printing output to the terminal without disrupting the UI.api::runner: High-level system operations like installing packages or cloning repositories.api::utility: Helper functions for file I/O, JSON parsing, and user prompts.
Note
View the full API reference for a detailed list of every available function.
Tip
Find the definitions JSON file here.
Quickstart & Best Practices
Communicating Between Hooks
Because the script’s Scope is preserved for the entire lifetime of the application, you can use global variables to share state between different hook functions.
#![allow(unused)]
fn main() {
// This runs during initialization
let keyboard_layout = "";
fn pre_backup() {
keyboard_layout = api::utility::select("Choose your keyboard layout", "de", [["us", "us", ""], ["de", "de", ""]]);
}
fn post_restore_pre_link() {
api::log::info(`You chose keyboard layout: ${keyboard_layout}`);
}
}
Accessing Context
You can use the info object to make decisions based on the current environment:
#![allow(unused)]
fn main() {
fn post_deps_pre_install() {
api::log::info(`Installing to: ${info.install_dir}`);
if info.update {
api::log::remark("Update flag detected, skipping backup...");
} else if info.backup {
api::log::step("Performing pre-install backup...");
}
}
}
log
Namespace: global/api/log
fn debug
fn debug(msg: String)
fn step
fn step(msg: String)
fn info
fn info(msg: String)
fn success
fn success(msg: String)
fn remark
fn remark(msg: String)
fn warn
fn warn(msg: String)
fn error
fn error(msg: String)
runner
Namespace: global/api/runner
fn install_package
fn install_package(runner: HookRunner, package: String)
fn os_short_name
fn os_short_name(runner: HookRunner) -> String
fn run_command
fn run_command(runner: HookRunner, program: String, args: Array, interactive: bool, request_password_injection: bool, env: Map)
fn run_command(runner: HookRunner, program: String, args: Array, interactive: bool, request_password_injection: bool, env: Map, cwd: String)
If root privileges are needed, set program to sudo or interactive to true.
Note: interactive mode disrupts the clean UI.
fn clone_git_repository
fn clone_git_repository(runner: HookRunner, url: String, target: String)
fn clone_git_repository(runner: HookRunner, url: String, target: String, tag: String)
utility
Namespace: global/api/utility
fn note
fn note(prompt: String, msg: String)
fn confirm
fn confirm(msg: String, initial_value: bool) -> bool
fn select
fn select(msg: String, initial_value: String, items: Array) -> String
fn multi_select
fn multi_select(msg: String, initial_values: Array, items: Array) -> Array
fn resolve_home
fn resolve_home(path: String) -> String
fn read_file
fn read_file(path: String) -> String
fn write_file
fn write_file(path: String, content: String, append: bool)
fn regex_replace
fn regex_replace(input: String, pattern: String, replace: String) -> String
fn create_tmp_dir
fn create_tmp_dir() -> TmpDir
fn get_tmp_path
fn get_tmp_path(tmp_dir: TmpDir) -> String
fn remove_file
fn remove_file(path: String)
fn copy_recursive
fn copy_recursive(src: String, dst: String)
fn parse_json
fn parse_json(json: String) -> ?
fn path_exists
fn path_exists(path: String) -> bool
fn ensure_dir
fn ensure_dir(path: String)