Files
Nomarchy/tools/check-menu-back.py
Bernardo Magri e6be5a5770
Some checks failed
Check / eval (push) Has been cancelled
fix: disk picker fd0 (#112), menu Back guard (#106), offline theme (#113)
- #112: list_installable_disks drops fd/loop/sr/zram/<8GiB, largest-first
  so OVMF no longer offers /dev/fd0 as the default wipe target
- #106: tools/check-menu-back.py + checks.menu-back (proved to fail)
- #113: offline theme-switch contract documented (default/pinned only);
  theme-sync run_switch adds offline-oriented hint when no network

Verified: V0 flake check; installer-safety; menu-back; py_compile.
2026-07-15 10:02:19 +01:00

95 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""Guard the nomarchy-menu Back/Left contract (BACKLOG #106).
Every *internal* list picker built with `rofi_menu` must emit the shared
`↩ Back` row (via `back`, `printf … "$BACK"`, or a literal). External
modi/tools and free-text prompts are listed as exceptions — Esc is their
back path.
Usage: check-menu-back.py <modules/home/rofi.nix>
Pure — no rofi runtime. Fails on a list menu that can strand the user.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
if len(sys.argv) != 2:
sys.exit(f"usage: {sys.argv[0]} <rofi.nix>")
text = Path(sys.argv[1]).read_text(encoding="utf-8")
# Strip Nix multi-line comments lightly? Keep simple — work on the whole file.
# Find `| rofi_menu` call sites (possibly with line breaks before `|`).
# Look at a window of lines before each call for BACK emission.
lines = text.splitlines()
# Exception markers: if the window mentions these, allow no BACK row.
EXCEPTION_MARKERS = (
"External tool:",
"external modi",
"Esc closes",
"Esc is their back",
"free-text",
"Free-text",
"no list to",
"rofi_menu -p search", # web: empty stdin free-text
"rofi_menu -p ask",
# calc/emoji/network are exec-only without rofi_menu list construction
)
def window_has_back(chunk: str) -> bool:
# Shared helpers emit ↩ Back: bare `back`, printf of $BACK, or a literal.
if re.search(r"(?m)^\s*back\s*$", chunk):
return True
if re.search(r"\bback\b", chunk) and "back()" not in chunk.split("rofi_menu")[0][-20:]:
# `back` as a command (not only the word in comments). Prefer line-start.
if re.search(r"(?m)^\s*back\b", chunk):
return True
if "$BACK" in chunk or r"\$BACK" in chunk or "↩ Back" in chunk:
return True
if re.search(r'printf[^\n]*BACK', chunk):
return True
return False
def window_is_exception(chunk: str) -> bool:
return any(m in chunk for m in EXCEPTION_MARKERS)
# Match lines that invoke rofi_menu as a picker (not the function definition).
invoke_re = re.compile(r"\brofi_menu\b")
bad: list[str] = []
for i, line in enumerate(lines):
if "rofi_menu()" in line or "rofi_menu ()" in line:
continue
if not invoke_re.search(line):
continue
# Skip comments and the definition of the helper's docs.
stripped = line.lstrip()
if stripped.startswith("#"):
continue
# Window: up to 40 lines before this call (menu construction).
start = max(0, i - 40)
chunk = "\n".join(lines[start : i + 1])
if window_is_exception(chunk):
continue
# Free-text: empty stdin piped in
if re.search(r"rofi_menu[^\n]*<\s*/dev/null", chunk) or re.search(
r"rofi_menu[^\n]*\n[^\n]*<\s*/dev/null", chunk
):
continue
if not window_has_back(chunk):
bad.append(f"line {i + 1}: rofi_menu without ↩ Back in preceding construction:\n {line.strip()}")
if bad:
print("check-menu-back: FAIL — internal list menus must end with ↩ Back:\n", file=sys.stderr)
for b in bad:
print(b, file=sys.stderr)
sys.exit(1)
print(f"check-menu-back: ok — {sum(1 for l in lines if invoke_re.search(l) and 'rofi_menu()' not in l)} rofi_menu sites scanned")