Supersedes the old flat .config/ layout (last published 2026-06-28) with the private repo's structure: one shared base plus per-host overlays. - packages: common/ gui/ lw/ fl/ wm/ plus install.sh and bin/ tooling (dotsync, reconcile-hyde.sh) - new README covering the layout, deploy order and the HyDE dependency - current HyDE waybar rig (layouts/, cava), pi agent extensions, claude/ config, tmux, presenterm, aichat roles - fish: kp (keepassxc-cli + fzf picker, db path from $KP_DB) and bind_M_n_history (alt+1..9 recalls the nth history entry) - drops cruft that should never have been tracked: the duplicate top-level .pi/ copy, btop.log, zellij config.kdl.bak, fish_variables - .pi/agent/auth.json is gitignored; auth.json.example ships instead Host-specific work sessions and the personal backlog stay in the private tree. Endpoint locators in the llamacpp/whisper guides are placeholders ($SERVER, <own-domain>) — the guides themselves stay, since they are the useful part.
150 lines
4.4 KiB
Python
Executable File
150 lines
4.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""PostToolUse hook: format an edited file with the right formatter.
|
|
|
|
Reads the hook JSON from stdin, extracts the edited file path, and runs a
|
|
language-appropriate formatter -- but, to avoid imposing a style on a project
|
|
that never asked for it, opinionated formatters (stylua, ruff, prettier, taplo)
|
|
only run when the project opts in via a config file discoverable by walking up
|
|
from the edited file. Canonical single-style toolchains run on sight: gofmt and
|
|
rustfmt on their go.mod / Cargo.toml marker; fish_indent and shfmt whenever the
|
|
tool itself is installed (shell and fish have one de-facto style, no config).
|
|
|
|
Best-effort: always exits 0 so a format hiccup never blocks an edit. jq is not
|
|
guaranteed on the host, hence python for the stdin JSON parse.
|
|
"""
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
def find_up(start, names):
|
|
"""Nearest ancestor dir (incl. the file's own) containing any of `names`."""
|
|
d = os.path.dirname(os.path.abspath(start))
|
|
while True:
|
|
if any(os.path.exists(os.path.join(d, n)) for n in names):
|
|
return d
|
|
parent = os.path.dirname(d)
|
|
if parent == d:
|
|
return None
|
|
d = parent
|
|
|
|
|
|
def has_toml_section(start, filename, section):
|
|
"""True if the nearest ancestor `filename` contains `section`."""
|
|
d = find_up(start, [filename])
|
|
if not d:
|
|
return False
|
|
try:
|
|
with open(os.path.join(d, filename), errors="ignore") as fh:
|
|
return section in fh.read()
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def stylua_cmd(path):
|
|
exe = os.path.expanduser("~/.local/share/nvim/mason/bin/stylua")
|
|
if not os.access(exe, os.X_OK):
|
|
exe = shutil.which("stylua")
|
|
if exe and find_up(path, [".stylua.toml", "stylua.toml"]):
|
|
return [exe, path]
|
|
return None
|
|
|
|
|
|
def ruff_cmd(path):
|
|
exe = shutil.which("ruff")
|
|
if exe and (find_up(path, ["ruff.toml", ".ruff.toml"])
|
|
or has_toml_section(path, "pyproject.toml", "[tool.ruff")):
|
|
return [exe, "format", path]
|
|
return None
|
|
|
|
|
|
_PRETTIER_CFGS = [
|
|
".prettierrc", ".prettierrc.json", ".prettierrc.yaml", ".prettierrc.yml",
|
|
".prettierrc.json5", ".prettierrc.js", ".prettierrc.cjs", ".prettierrc.mjs",
|
|
".prettierrc.toml", "prettier.config.js", "prettier.config.cjs",
|
|
"prettier.config.mjs",
|
|
]
|
|
|
|
|
|
def prettier_cmd(path):
|
|
exe = shutil.which("prettier")
|
|
if exe and find_up(path, _PRETTIER_CFGS):
|
|
return [exe, "--write", path]
|
|
return None
|
|
|
|
|
|
def gofmt_cmd(path):
|
|
exe = shutil.which("gofmt")
|
|
if exe and find_up(path, ["go.mod"]): # one canonical style
|
|
return [exe, "-w", path]
|
|
return None
|
|
|
|
|
|
def rustfmt_cmd(path):
|
|
exe = shutil.which("rustfmt")
|
|
if exe and find_up(path, ["Cargo.toml", "rustfmt.toml", ".rustfmt.toml"]):
|
|
return [exe, path]
|
|
return None
|
|
|
|
|
|
def fish_cmd(path):
|
|
exe = shutil.which("fish_indent") # ships with fish, one canonical style
|
|
return [exe, "-w", path] if exe else None
|
|
|
|
|
|
def shfmt_cmd(path):
|
|
exe = shutil.which("shfmt") # de-facto shell formatter, sane defaults
|
|
return [exe, "-w", path] if exe else None
|
|
|
|
|
|
def taplo_cmd(path):
|
|
exe = shutil.which("taplo")
|
|
# opt-in: TOML reflow/align is intrusive, so only where the project asked.
|
|
if exe and find_up(path, ["taplo.toml", ".taplo.toml"]):
|
|
return [exe, "format", path]
|
|
return None
|
|
|
|
|
|
HANDLERS = {
|
|
".lua": stylua_cmd,
|
|
".py": ruff_cmd,
|
|
".js": prettier_cmd, ".jsx": prettier_cmd, ".mjs": prettier_cmd,
|
|
".cjs": prettier_cmd, ".ts": prettier_cmd, ".tsx": prettier_cmd,
|
|
".css": prettier_cmd, ".scss": prettier_cmd, ".less": prettier_cmd,
|
|
".html": prettier_cmd, ".vue": prettier_cmd, ".json": prettier_cmd,
|
|
".md": prettier_cmd, ".yaml": prettier_cmd, ".yml": prettier_cmd,
|
|
".go": gofmt_cmd,
|
|
".rs": rustfmt_cmd,
|
|
".fish": fish_cmd,
|
|
".sh": shfmt_cmd, ".bash": shfmt_cmd,
|
|
".toml": taplo_cmd,
|
|
}
|
|
|
|
|
|
def main():
|
|
try:
|
|
data = json.load(sys.stdin)
|
|
except Exception:
|
|
return
|
|
ti = data.get("tool_input") or {}
|
|
tr = data.get("tool_response") or {}
|
|
path = ti.get("file_path") or tr.get("filePath") or ""
|
|
if not path or not os.path.isfile(path):
|
|
return
|
|
handler = HANDLERS.get(os.path.splitext(path)[1].lower())
|
|
if not handler:
|
|
return
|
|
cmd = handler(path)
|
|
if not cmd:
|
|
return
|
|
try:
|
|
subprocess.run(cmd, timeout=15, capture_output=True)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|