Skip to content

Latest commit

 

History

History
277 lines (214 loc) · 10.8 KB

hooks.md

File metadata and controls

277 lines (214 loc) · 10.8 KB

Hooks

A hook runs commands or scripts after files matching a pattern are deployed or removed — reloading systemd after a unit file changes, re-sourcing tmux after its config changes.

hooks:
  systemd-reload:
    match: ["*.service", "*.timer"]
    profile: arch
    description: Reload systemd after unit changes
    do:
      - run: sudo systemctl daemon-reload

  tmux:
    match: .config/tmux/**/*.conf
    do:
      - run: tmux source-file ~/.config/tmux/tmux.conf

  session-env:
    match: .config/environment.d/*.conf
    profile: linux
    do:
      - run: dbus-update-activation-environment --systemd --all

  keyd:
    match: /etc/keyd/*.conf
    do:
      - run: sudo systemctl daemon-reload
      - script: keyd-restart.sh
      - run: notify-send "keyd reloaded ({{ len .Files }} files)"
  • systemd-reload reloads systemd when a unit or timer changes. profile: arch keeps it to machines using the arch profile.
  • tmux makes the running tmux server re-read its config.
  • session-env passes the environment to systemd user services and D-Bus activated applications after an environment.d file changes. Both are Linux only, hence profile: linux.
  • keyd combines step types: a command, a script, and a command rendered as a template, where .Files holds the files that changed.
Field Type Description
match string or list Target path patterns. Required
do list Steps run in order, each either run: or script:. At least one
profile string Only active under this profile, or one inheriting from it
description string Shown in the prompt, mate hooks list, and mate status
enabled bool Default true. false switches the hook off
confirm bool Default true. false runs the hook without asking — see Confirmation

In TOML the same structure is [hooks.keyd] with [[hooks.keyd.do]] steps.

Where hooks are declared

File Matches
mate.yaml Files from every source
Local config (~/.config/statemate/config.yaml) Files from every source
A source's .mate.yaml Only that source's files, and only while the source is active

Hooks from mate.yaml and the local config share one namespace. A local hook with the same name replaces the repository one entirely; fields are not merged. To switch a repository hook off on one machine:

# ~/.config/statemate/config.yaml
hooks:
  systemd-reload: { enabled: false }

A disabled hook needs neither match nor do.

Hooks in a source's .mate.yaml are named after the source, arch/keyd, so they never collide with other hooks. The local config cannot override them.

Patterns

Patterns match the target path, meaning where the file is deployed:

Pattern Matches
*.service Any file named *.service, at any depth, under any target root
.config/tmux/**/*.conf Relative to ~
~/.config/environment.d/*.conf Relative to ~, spelled out
/etc/keyd/*.conf Absolute
  • * matches within one path segment, and ** matches any number of directories.
  • A pattern naming a directory matches everything under it. A trailing / limits it to files inside the directory.
  • There is no ! negation, unlike ignore. A hook that should skip some files needs narrower patterns.
  • Attributes such as #tmpl are not part of the target path, so a pattern never needs them. Recognized source attributes in a pattern are ignored, on file and directory components alike: .config#perm-r:700/tmux/*.conf#tmpl matches the same files as .config/tmux/*.conf. Attributes do not filter matches by profile or file type.
  • Unknown # suffixes stay literal. To match a literal #tmpl in a target path, write [#]template or \#tmpl. In YAML, a pattern containing a backslash must be in single quotes.
  • target_base and targets: mappings make no difference: a unit deployed to /etc/systemd/system matches both *.service and /etc/systemd/system/*.service.

When hooks run

A hook triggers when at least one matching file was:

Event Command
Written, new or with changed content mate apply
Removed as an orphan mate clean
Removed along with its source mate delete (not with --keep-target)
Renamed mate rename — the old path and the new path are both matched

Only what actually reached the disk counts. These do not trigger hooks:

  • Permission-only fixes
  • #import files, which are copied back into the source rather than deployed
  • Conflicts answered with [s]kip

A write that fails aborts the whole apply, so no hooks or #after scripts run after it.

Scoped runs (mate apply <path>, mate apply --source <name>) trigger hooks for the files they wrote.

In mate apply, hooks run after files and packages and before #after scripts:

#before scripts → files → packages → hooks → #after scripts

Each triggered hook runs once, however many of its files changed. Triggered hooks run in name order, so a numeric prefix (10-systemd, 20-keyd) forces an order. Source hooks sort by their full <source>/<name>. Steps run in the order listed.

Steps

run:

A command run with sh -c. It is rendered as a template first, with .Files set to the triggering target paths, sorted.

In a source's .mate.yaml, the whole file is rendered once when it is loaded, before any hook runs, so {{ .Files }} there is always empty. Use the $STATEMATE_HOOK_FILES environment variable instead, or escape the expression so it survives the first rendering: {{`{{ .Files }}`}}.

Commands run in the directory of the config file that declared the hook: the repository root for mate.yaml, the source directory for .mate.yaml, and ~/.config/statemate for the local config.

mate does not elevate hook commands; write sudo yourself. After mate apply wrote system files, the sudo session it opened usually means no second password prompt.

script:

A script by the name mate scripts list shows in its NAME column, such as keyd-restart.sh. The name is resolved in this order:

  1. For a hook in a source's .mate.yaml, a script in that source.
  2. A script in the repository-root .matescripts/.
  3. A script in any other source. If more than one matches, it is a config error.

The script runs as it would with mate scripts run: its attributes, environment, and working directory are unchanged, and the run is recorded (except for #always and manual scripts, which never are). If the script has a #profile: that is not active, the step is skipped with a warning.

A script a hook ran is not run again as an #after script in the same apply.

Environment

Every step receives, on top of the usual environment:

Variable Value
STATEMATE_HOOK_NAME The hook name, arch/keyd for a source hook
STATEMATE_HOOK_FILES The triggering target paths, newline-separated, sorted
STATEMATE_SOURCE_DIR For run: steps, the declaring source of a hook in a source's .mate.yaml. For script: steps, the script's own source

STATEMATE_HOOK_FILES is empty when the hook is run with mate hooks run. script: steps also receive the usual script variables.

Confirmation

Hooks are confirmed like scripts, once per hook:

Run hook keyd (2 files)?
  Reload keyd after config changes
  - sudo systemctl daemon-reload
  - script keyd-restart.sh
  - notify-send "keyd reloaded (2 files)"
[Y]es / [n]o / [a]ll / [q]uit:
  • Enter answers [Y]es.
  • [a]ll confirms the remaining hooks and the #after scripts that follow.
  • [q]uit stops the remaining hooks and #after scripts. The files are already written.
  • --force confirms without asking. --no-scripts skips hooks too.
  • Without a terminal, hooks are skipped with a warning.
  • A hook with confirm: false runs without asking, even without a terminal. mate hooks list shows it as noconfirm. --no-scripts still skips it.

Pending hooks

Once the files are written they are unchanged, so a hook would not trigger again on its own. mate apply therefore records a triggered hook as pending as soon as its files are written, and keeps it pending until it runs. A pending hook:

  • is offered again on the next mate apply, even when no file changed, with the files from both runs in .Files
  • is listed under "Pending hooks" by mate status
  • is settled by running it with mate hooks run <name>

A hook stays pending when the apply was stopped before it ran ([q]uit, Ctrl-C, a package prompt quit), when it was skipped for want of a terminal or by --no-scripts, and when it failed. Answering [n]o settles it: declining is not remembered, so run it later with mate hooks run <name>.

Only mate apply records pending hooks. mate delete and mate clean confirm hooks unless --force (or --noconfirm) is given, and mate rename always asks.

Failure

A failing step stops its hook; the remaining steps are skipped. Other hooks and the #after scripts still run, and the command exits non-zero. Under mate apply, a failed hook stays pending and is offered again on the next apply.

Checking

A hook is validated when it is loaded, so mate apply refuses to start and mate check fails when:

  • match is missing or a pattern does not parse
  • do is empty, or a step has both or neither of run and script
  • a script: step matches no script, or more than one
  • a run: template does not parse

mate doctor warns about an active hook whose patterns match no managed file, which usually means a typo.

Commands

Command Purpose
mate hooks list Every hook: NAME, SCOPE, MATCH, PROFILE, STATUS, DESCRIPTION
mate hooks run <name> Run a hook now, without a prompt. --dry-run shows the steps instead
mate apply --dry-run Lists the hooks that would run; -l adds the files and steps
mate status Lists the hooks that pending changes would trigger

In mate hooks list, SCOPE is repo, local, or the source name. STATUS is disabled for a hook with enabled: false and n/a when its profile is not active. mate hooks run refuses a disabled hook or one whose profile is not active.

Hooks or #onchange scripts

An onchange script runs when anything in its source changes, or what its # Onchange: comment names, and needs a file of its own. Use a hook when the follow-up needs the changed files (.Files), runs after mate clean or mate delete, or is a single command. Use an onchange script for longer logic, for one that should also run when the script itself is edited, or call that script from a hook with script:.