aboutsummaryrefslogtreecommitdiff
path: root/tabify-nix.py
diff options
context:
space:
mode:
authorDylan <boss@tehbox.org>2026-10-17 22:02:09 +1300
committerDylan <boss@tehbox.org>2026-10-17 22:04:40 +1300
commit008b591a0a5942ae42f4c07a5abbee4dbd2d0743 (patch)
tree0ae88a36bc00ede3b43b31489adae633dc54e8cd /tabify-nix.py
parent280e1bbe5e733128a451260685cc8ecd10a042a3 (diff)
downloadnixos-configuration-008b591a0a5942ae42f4c07a5abbee4dbd2d0743.tar.gz
nixos-configuration-008b591a0a5942ae42f4c07a5abbee4dbd2d0743.zip
feat: Formatting changes
Diffstat (limited to 'tabify-nix.py')
-rwxr-xr-xtabify-nix.py128
1 files changed, 128 insertions, 0 deletions
diff --git a/tabify-nix.py b/tabify-nix.py
new file mode 100755
index 0000000..0220ab9
--- /dev/null
+++ b/tabify-nix.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+"""Convert nixfmt's 2-space indentation to tabs.
+
+Usage: ./tabify-nix.py [FILE...] (default: every git-tracked .nix file)
+
+Only lines that start in code are touched. Lines that start inside a string
+('' or ") are left exactly as they are, because Nix only strips leading
+*spaces* from '' strings, so tabs there would change the string's value.
+"""
+import subprocess
+import sys
+
+INDENT = 2
+
+
+def lex_line(line, stack):
+ """Advance the lexer state `stack` over one line (mutates it in place).
+
+ Stack entries: ["code", brace_depth], ["ind"] (''), ["dq"] ("), ["block"].
+ A "code" entry above a string entry is a ${ } interpolation.
+ """
+ i, n = 0, len(line)
+ while i < n:
+ mode = stack[-1][0]
+ rest = line[i:]
+ if mode == "code":
+ c = line[i]
+ if c == "#":
+ return
+ if rest.startswith("/*"):
+ stack.append(["block"])
+ i += 2
+ elif c == '"':
+ stack.append(["dq"])
+ i += 1
+ elif rest.startswith("''"):
+ stack.append(["ind"])
+ i += 2
+ elif c == "{":
+ stack[-1][1] += 1
+ i += 1
+ elif c == "}":
+ if stack[-1][1] == 0 and len(stack) > 1:
+ stack.pop() # end of ${ } interpolation
+ else:
+ stack[-1][1] -= 1
+ i += 1
+ else:
+ i += 1
+ elif mode == "block":
+ if rest.startswith("*/"):
+ stack.pop()
+ i += 2
+ else:
+ i += 1
+ elif mode == "dq":
+ if rest.startswith("\\"):
+ i += 2
+ elif rest.startswith("$${"):
+ i += 3
+ elif rest.startswith("${"):
+ stack.append(["code", 0])
+ i += 2
+ elif rest.startswith('"'):
+ stack.pop()
+ i += 1
+ else:
+ i += 1
+ elif mode == "ind":
+ if rest.startswith("'''") or rest.startswith("''$"):
+ i += 3
+ elif rest.startswith("''\\"):
+ i += 4
+ elif rest.startswith("''"):
+ stack.pop()
+ i += 2
+ elif rest.startswith("$${"):
+ i += 3
+ elif rest.startswith("${"):
+ stack.append(["code", 0])
+ i += 2
+ else:
+ i += 1
+
+
+def tabify(text):
+ stack = [["code", 0]]
+ out = []
+ for line in text.split("\n"):
+ if stack[-1][0] in ("code", "block"):
+ stripped = line.lstrip(" \t")
+ ws = line[: len(line) - len(stripped)]
+ if stripped:
+ tabs = ws.count("\t")
+ spaces = ws.count(" ")
+ line = "\t" * (tabs + spaces // INDENT) + " " * (spaces % INDENT) + stripped
+ else:
+ line = ""
+ out.append(line)
+ lex_line(line, stack)
+ if stack != [["code", 0]]:
+ raise ValueError(f"unbalanced lexer state at EOF: {stack}")
+ return "\n".join(out)
+
+
+def main():
+ files = sys.argv[1:] or subprocess.run(
+ ["git", "ls-files", "*.nix"], capture_output=True, text=True, check=True
+ ).stdout.split()
+ failed = False
+ for path in files:
+ with open(path) as f:
+ src = f.read()
+ try:
+ new = tabify(src)
+ except ValueError as e:
+ print(f"{path}: {e}, skipped", file=sys.stderr)
+ failed = True
+ continue
+ if new != src:
+ with open(path, "w") as f:
+ f.write(new)
+ print(f"tabified {path}")
+ sys.exit(1 if failed else 0)
+
+
+if __name__ == "__main__":
+ main()