Integrations that make statemate disappear into your daily workflow. Most of these come from a real dotfiles repository managed with statemate; follow the links for the complete files.
Every deployed file is overwritten by the next mate apply, so an edit made to
~/.zshrc directly is either lost or turns into a conflict. mate edit takes the
target path and opens the source for you:
mate edit ~/.zshrc # opens zsh/.zshrc in the repository
mate edit ~/.ssh/config # decrypts, opens, re-encrypts on saveIf you edited the target anyway, mate apply stops at the conflict and offers
[i]mport, which copies your edit back into the source.
mate completion prints a completion script for bash, zsh, fish, or PowerShell:
# ~/.zshrc
source <(mate completion zsh)
# ~/.bashrc
source <(mate completion bash)
# fish
mate completion fish | sourceCompletion is context-aware: mate apply, diff and status complete the
files with pending changes and the directories holding them, mate clean
completes orphans, mate scripts run and mate hooks run complete script and
hook names, and --source / --profile complete from your configuration.
A hook can signal running shells when their configuration changes, so open terminals pick up a new alias without being restarted:
# mate.yaml
hooks:
shell-reload:
match:
- ".config/zsh/**"
- ".zshenv"
do:
- run: pkill -USR2 -u "$USER" zsh || true# .zshrc — re-source the configuration when SIGUSR2 arrives
TRAPUSR2() { source "${ZDOTDIR:-$HOME}/.zshrc" }The || true keeps the hook from failing when no zsh is running.
Three problems come up when editing a statemate repository in Neovim, and an
autocommand plugin solves each of them. The complete version is
plugin/statemate.lua.
Neovim reads settings.json#profile:work#enc as a file with the extension
json#profile:work#enc, so highlighting and LSP stop working. Strip the
attributes and detect again:
vim.api.nvim_create_autocmd({ "BufReadPost", "BufNewFile" }, {
pattern = "*#*",
callback = function(args)
local name = vim.fs.basename(args.file):gsub("#.*$", "")
local ft = vim.filetype.match({ filename = name, buf = args.buf })
if ft then
vim.bo[args.buf].filetype = ft
end
end,
})Only the basename is stripped, so attributes on a parent directory
(etc#owner-r:root/) are left alone.
Opening ~/.ssh/config is almost always a mistake when the file is managed.
mate managed <path> resolves a target to its source, so a BufReadPost
autocommand can ask which one you meant:
statemate: managed file, edit which?
1: source: config#enc
2: target: ~/.ssh/config
The plugin runs mate managed on the opened path and switches the buffer to the
source if you pick it. It skips files whose ACTIVE column is empty, since their
source belongs to a profile that is not in use.
BufReadCmd and BufWriteCmd autocommands on *#enc* can decrypt with
age --decrypt --identity <key> on read and re-encrypt with
age --armor --recipient <key>… on write, reading the recipients from
mate.yaml. Disable the swap file, undo file, and backups for those buffers so
the plaintext never reaches the disk.
Use --armor: statemate writes armored ciphertext, and matching it keeps
git diff readable whichever tool last wrote the file.
Encrypted sources are ciphertext, so git diff and git log -p show nothing
useful. A textconv filter decrypts them for display only; the files stay
encrypted on disk and in the repository:
# .gitattributes in the repository
*#enc* diff=age# ~/.gitconfig
[diff "age"]
textconv = age --decrypt --identity ~/.config/statemate/key.txtThe pattern ends in * because #enc need not be the last attribute
(settings.json#enc#import). Tools that use git's diff machinery, such as
lazygit and tig, pick this up too.
diff_tool in mate.yaml replaces the built-in diff -u:
diff_tool: difftstatemate runs the tool as <tool> <old> <new> through sh, so the setting may
include options (diff_tool: delta --side-by-side). The output is captured
rather than shown on the terminal, so the tool cannot detect the terminal's
width. A wrapper that reads the width from the terminal itself:
#!/bin/sh
# ~/.local/bin/mate-diff
cols=$( (stty size < /dev/tty) 2>/dev/null | cut -d' ' -f2 )
exec delta --side-by-side --width="${cols:-160}" "$1" "$2"diff_tool: mate-diffThe two files are temporary copies named after the target (mate-123-config.toml),
so a tool that picks a syntax from the extension still can. The [d]iff answer to
a conflict during mate apply uses the tool too, and mate diff --tool <name>
overrides the setting for one run.
mate status --short prints one compact line meant for status bars, and nothing
at all when everything is up to date:
~2 !1 *1
| Token | Meaning |
|---|---|
+N |
new files |
~N |
modified files |
!N |
conflicts |
<N |
pending imports |
?N |
orphans |
*N |
pending scripts |
sN |
secrets needing a fetch |
It exits non-zero when it cannot run at all, such as when the configuration does not load, so a status bar can tell "clean" from "broken". Every run scans your sources, so poll it every few seconds rather than continuously.
A status-line segment, colouring each token:
#!/usr/bin/env bash
# ~/.local/bin/tmux-statemate
status=$(mate status --short 2>/dev/null) || exit 0
[[ -z "$status" ]] && exit 0
for token in $status; do
case "$token" in
!*) printf '#[fg=red]%s ' "$token" ;;
~*) printf '#[fg=blue]%s ' "$token" ;;
*) printf '#[fg=default]%s ' "$token" ;;
esac
doneset -g status-interval 10
set -ag status-right '#(tmux-statemate)'And a key binding that runs mate apply in a popup, after confirming:
bind . confirm-before -p "Run mate apply?" \
"display-popup -E 'mate apply --list; read -n 1 -s -r -p \"Press any key\"'"The full versions are
__statemate_status
and
50-statemate.conf.
The same script can emit JSON for a Waybar custom module, with the full
mate status as the tooltip:
"custom/statemate": {
"exec": "__statemate_status waybar",
"return-type": "json",
"interval": 10,
"on-click": "mate apply"
}# the waybar branch of the script
if [[ -z "$status" ]]; then
jq -cn '{text: "", class: "clean", tooltip: "statemate: up to date"}'
else
jq -cn --arg t "$status" --arg tip "$(mate status 2>/dev/null)" \
'{text: $t, class: "dirty", tooltip: $tip}'
fiVariables can be nested, and an include file can hold
nothing but variables. Together they make a single theme file that feeds every
application's colours:
# .mate/theme.yaml
variables:
color:
bg:
default: 1a1e24
dark: 15181e
fg:
default: d7d7d7# mate.yaml
include:
- .mate/theme.yaml# kitty/.config/kitty/theme.conf#tmpl
background #{{ .Vars.color.bg.default }}
foreground #{{ .Vars.color.fg.default }}
Changing a colour in one place redeploys every template that uses it on the next
mate apply. Renaming or removing a key breaks every template that refers to it.
Run grep -r 'color.bg.dark' first, and mate eval the templates you touched.