Some checks failed
Check / eval (push) Has been cancelled
- #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.
95 lines
3.2 KiB
Python
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")
|