Backstory
Some of us really have a craze for configs and ricing. Though, looking at the dotfiles of others, I have realized that although they have really efficient scripts, they are not always easy to maintain and/or understand for others. I, personally, sometimes used to have a hard time navigating others' configurations.
And while, I might not be able to solve it for everyone, I have been using macOS for the past 4 years. Using it has given me a pretty good grip on the ecosystem and how preferences are handled.
Why?
While using macOS, I have come to realize that most solutions like Nix are not always easy to maintain either. So, I wanted to create a tool that abstracts away all the complexity into simple, already-known terms for most power users.
And that's how cutler
came to be.
What this book does
This book, by name, wants to introduce you with cutler
, a software made to declaratively manage your macOS preferences and essentials. It can do a number of things. This book will help you navigate all of them, including new ones which are currently in development.
It's a solo project, but I hope it will give you the proper impression for shrinking dotfiles by an order of magnitude.
To get started with your cutlery, move to the next page.
Installation
You can install cutler by directly running this command in the terminal:
$ /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/hitblast/cutler/main/install.sh)"
Other installation methods are:
- Using 🍺
homebrew
:
$ brew install hitblast/tap/cutler
- Using
cargo
:
$ cargo install cutler
- Using
mise
:
# NOTE: This will compile the binary manually for your system.
$ mise use -g cargo:cutler
Once installed, you can also enable shell completions for your shell instance if needed. Installing via Homebrew doesn't require doing this step.
Manual Installation
Get the latest prebuilt compressed binaries if you would like to manually install the project.
Note than on devices running macOS, you'll have to remove the quarantine attribute from the binary:
$ xattr -d com.apple.quarantine bin/cutler # inside extracted zip
Shell Integrations
Completions
cutler supports built-in shell completion for your ease of access for a variety of system shells, including
bash
, zsh
, powershell
etc. Below you will find instructions for each of them.
If you have installed cutler using Homebrew, the shell completion will automatically be installed. Just restart your shell after initial installation.
Bash completions setup
- Make a directory to store Bash-specific completions:
$ mkdir ~/.bash-completion.d/
- Generate the completion script using the following command and pipe the output to a new file:
$ cutler completion bash > cutler.bash
$ mv cutler.bash ~/.bash-completion.d/
- Finally, source the completion script. The best way would be to simply add it to your
.bashrc
file:
$ source ~/.bash_completion.d/cutler.bash > ~/.bashrc
Zsh completions setup
- Make sure you have a directory for custom completions:
$ mkdir -p ~/.zfunc
- Then, generate the completion script and move it over:
$ cutler completion zsh > _cutler
$ mv _cutler ~/.zfunc/
- Then, add to your
~/.zshrc
:
$ fpath=(~/.zfunc $fpath)
$ autoload -U compinit && compinit
- Restart your shell or run:
$ source ~/.zshrc
For other shells
# Fish
$ cutler completion fish
# Elvish
$ cutler completion elvish
# PowerShell
$ cutler completion powershell
Quickstart
To easily get started, simply type the following command to generate a sample configuration:
Most commands support a set of global flags that affect output and behavior. See Global Flags for details.
$ cutler init
By default, cutler stores your configuration in ~/.config/cutler/config.toml
.
But, it can also have other values depending on your setup:
$XDG_CONFIG_HOME/cutler/config.toml
~/.config/cutler/config.toml
~/.config/cutler.toml
cutler.toml
in the current directory
It respects your $XDG_CONFIG_HOME
, so you don't have to worry about path issues.
The next section will describe how you can use this one configuration file to orchestrate between or use all of cutler's capabilities.
System Preferences
cutler can do a number of things if you use it right. Here's a basic example of a TOML configuration for cutler:
[set.dock]
tilesize = 46
[set.menuextra.clock]
FlashDateSeparators = true
macOS heavily relies on preference files (in .plist
format) stored in certain ways to save the state of your Mac's apps and settings. cutler takes advantage of this mechanism to automatically put your desired system settings in place by following the config file you wrote. It's a "declarative" way to set your settings without even touching the app itself.
Ideally, the block above would look something like this if you were to manually call the defaults
CLI tool which is used to modify these values on macOS:
$ defaults write com.apple.dock "tilesize" -int "46"
$ defaults write com.apple.menuextra.clock "FlashDateSeparators" -bool true
You can also configure global preferences like this:
[set.NSGlobalDomain]
InitialKeyRepeat = 15
ApplePressAndHoldEnabled = true
"com.apple.mouse.linear" = true
# or, for the third entry, alternate structure:
#
# [set.NSGlobalDomain.com.apple.mouse]
# linear = true
Again, if you were to use defaults
, it would look something like this:
$ defaults write NSGlobalDomain "ApplePressAndHoldEnabled" -bool true
$ defaults write NSGlobalDomain com.apple.mouse.linear -bool true
Once you're ready, run this command to apply everything:
$ cutler apply
The apply
command has multiple functionalities which happen alongside of applying the preferences. Please read the sections below in depth to learn more.
cutler also takes the changes into account and tracks them. To see your status, run:
$ cutler status
Unapplying everything is also as easy. Run the command below and cutler will restore your preferences to the exact previous state:
$ cutler unapply
Action Hints
The fun part about using cutler is, it will mostly tell you to take certain actions based on what command you are using, without you having to think about it. This is due to cutler's immense synchronization between commands.
Say, for example, if my Dock should be at the right
based on cutler's configuration, and bottom
is its actual value right now, cutler will show this when running cutler status
:
$ cutler status
[WARNING] com.apple.dock | orientation: should be right (currently bottom)
[WARNING] Run `cutler apply` to apply these changes from your config.
🍎 All Homebrew formulae/casks match config.
As you can see, it suggests me to run cutler apply
. Running the suggested command will only affect the changed portion of the preferences, and cutler will skip the rest.
Risky Operations
If you would like to write non-existent domains (create them) using cutler, you can run the apply
command with a particular flag:
$ cutler apply --disable-checks
This will disable the "Domain does not exist" error which happens when cutler's backend does not recognize a domain.
Homebrew Management
If you're a person who struggles to keep tabs on all the installed formulae or apps using Homebrew, then cutler could be a great choice for you! Make sure your Homebrew installation is accessible from the $PATH
variable, and then you can back up the necessary formula/cask names into the config file you previously wrote, using this command:
$ cutler brew backup
# or, only backup the ones which are not a dependency:
$ cutler brew backup --no-deps
This eliminates the usage of the notorious brew bundle
command which creates a separate Bundlefile
for you to track. Why do so much when all you need is just one, central file?
Now, when you want to install from the file, simply run:
$ cutler brew install
You can also invoke the command's functionalty from within cutler apply
:
$ cutler apply --with-brew
This will install every formula/cask alongside applying preferences and running external commands.
The structure of the brew
table inside cutler's configuration is like such:
[brew]
taps = ["hitblast/tap"]
casks = ["zed", "zulu@21", "android-studio"]
formulae = ["rust", "python3"]
# Ensure dependencies aren't accounted for.
# This is auto-set if --no-deps is used in `brew backup`.
no-deps = true
While running this command, cutler will also notify you about any extra software which is untracked by it. Then, you can run cutler brew backup
again to sync.
Backend Prerequisites
Obviously, running Homebrew on a Mac requires the Xcode Command-Line Tools to be installed, let it be through Xcode itself or through the preincluded utility in macOS. By default, cutler will try to ensure that it is there, before executing any of the subprocesses.
If you want to manually install it, you can do so by running:
$ xcode-select --install
External Commands
Running commands to spring up your environment is essential for any workflow. Luckily, cutler is made with most scenarios in mind, given that most people usually set their dotfiles up with shell scripts which require manual execution and intervention.
You can define external commands with simple syntax like this:
[commands.greet]
run = "echo Hello World"
# This runs:
# $ echo Hello World
The ideal structure for writing external commands should be like this:
[vars]
hostname = "darkstar"
[commands.hostname]
run = """
#!/usr/bin/env bash
scutil --set LocalHostName $hostname
scutil --set HostName $hostname
scutil --set ComputerName $hostname
"""
sudo = true # a more "annotated" sudo
Some people would like to run their commands "before" other commands. But, cutler runs all commands in parallel, which might not be what you want. In that case, you can use the ensure-first
key to run then in your desired serial. You can apply this to multiple commands.
[commands.dotfiles]
run = "git clone repo && cd repo && stow . -t ~"
ensure-first = true
External commands are run whenever you run cutler apply
by default. However, if you'd like to only run the commands and not apply defaults, run:
$ cutler exec
You can also run a specific external command by attaching a name parameter:
$ cutler exec hostname # this runs the hostname command
Configuration Management
cutler's configuration can be tiny or versatile depending on your needs. But, there are some nifty features built into the software for your convenience.
Config-locking
When you run cutler init, the configuration file will usually contain this key-value pair at the very top:
lock = true
Unless you remove it, this will happen:
$ cutler apply
[ERROR] The config file is locked. Run `cutler config unlock` to unlock.
$
You can use this feature to mark configurations as potentially unsafe to apply. cutler uses it to generate new configuration files for you so that you don't accidentally apply the sample.
There are two commands to manage the lock status:
$ cutler config lock
# or
$ cutler config unlock
View or delete
To view your cutler configuration without the use of cat
or any other fancy tools, use:
$ cutler config show
You can also delete it using:
$ cutler config delete
Global Flags
cutler supports several global flags that can be used with any command:
-v
,--verbose
: Increase output verbosity.--quiet
: Suppress all output except errors and warnings. This is useful for scripting or when you only want to see problems.--dry-run
: Print what would be done, but do not execute any changes.-y
,--accept-interactive
: Accept all interactive prompts automatically.-n
,--no-restart-services
: Do not restart system services after command execution.
Example usage:
cutler apply --quiet
This will apply your configuration, but only errors and warnings will be "hushed".
Resources
Finding the ideal set of macOS defaults can be challenging. Visit this website to have a look at some useful ones fast:
Sample configuration files are preincluded with this repository for you to have a look at and get hold of the tool quickly: see examples
Backend for Preferences
In cutler's earlier versions, it used to wrap around the defaults
CLI using asynchronous command invocations. However, this had a counterpart of being slow and extensive in terms of I/O and shell command execution.
cutler's latest versions include defaults-rs, a frontend for macOS preferences which has been directly written for interop with cutler. Instead of command invocation, it parses the preferences and can also modify them in batches, all the while minimizing the amount of I/O per call.
It has a neat little Rust API which is also asynchronous.
This gives some visible benefits:
-
Instead of lag between commands, now you get instantaneous modifications to your settings (use
cutler apply -v
to feel the speed). -
Granular control over what gets modified and what doesn't.
-
The community gets to modify and upgrade the backend.
Project Links
Contribution Guidelines
This is the standard contribution/development guidelines for the project. You may follow these to get a hold of the project quickly.
Table of Contents
Getting Started
The commonplace of contributing is to first clone the repository and install the dependencies.
The prerequisites are as follows:
- Rust (
cutler
is configured to use the 2024 edition of the language) - A Mac (preferably with Apple Silicon) for rapid development
Cloning the repository
Once you have ensured the prerequisites, fork the repository from here and clone it using the following command:
# https
$ git clone https://github.com/<your_username>/cutler.git
# ssh
$ git clone git@github.com:<your_username>/cutler.git
Replace <your_username>
with your GitHub username.
Preparing the environment
Working on this project will require a few Rust components beforehand:
Production Release Workflow
This chain of commands can be used to fully test and build the final product.
Testing
# raw command
$ cargo fmt --all -- --check && cargo test --verbose && cargo clippy && cargo build
If you want to manually run this command before pushing, I recommend using hookman. I've built it to automate setting up git hooks. Just run this command inside the source directory to set things up:
$ hookman build
The unit tests in the CI workflow are done using an Apple Silicon M1 (3-core) runner provided by GitHub Actions. See this page in GitHub's documentation for more information on all the runners. If the runners used in this project get outdated and don't get a bump, you may suggest one through GitHub Issues.
Build Reproduction
You can easily create a release build for cutler using the following command:
$ cargo build --release --verbose --locked
The major part of the release automation is currently done with GitHub Actions via the following workflow so, you can have a look at it to view the entire pipeline.
The unit testing is done via this workflow.
Code Formatting
cutler
uses basic Rust formatting for code reliability and maintainability. This ensures that the codebase remains clean, readable, and consistent across different contributors.
Simply run the following command to format the code:
$ cargo fmt --all
Pull Request Guidelines
Before submitting a pull request, please ensure the following:
- Your code is well-documented and follows the established coding standards.
- The repository is correctly forked and your working branch is up-to-date with the latest changes from the main branch.
- All tests pass locally, and you have verified that your changes do not introduce regressions.
- If your pull request fixes an issue, mention the issue number in your PR description (e.g., Fixes #123).
- For larger changes, consider discussing your approach by opening an issue first.
Pull requests and issues must have the following pattern:
(<type>) <title>
Possible types include:
- feat: New feature or enhancement
- fix: Bug fix
- docs: Documentation update
- style: Code style or formatting change
- refactor: Code refactoring without changing functionality
- test: Test-related changes
- chore: Maintenance or administrative tasks
License
This project is licensed under the MIT License - see the LICENSE file for details.