mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-15 09:30:59 +01:00
The hook entry was an absolute /home/kuhy/utils path, copied from a config written for ~/.claude where it only ever ran locally. On an Actions runner that is exit code 127 — "No such file or directory" — so gatelock-precommit and crdt-sync-precommit failed on every commit of this refactor while the hook silently checked nothing. Now repo-relative, matching the sibling hooks (`entry: scripts/check_subproject_scope.sh`): pre-commit always runs from the git root, so it resolves the same in both places. Fixing that made the CI configs runnable locally, and `--all-files` then found what per-file runs had missed — ruff pruning imports that exist *for* their re-export. `import crdt_sync` was broken three separate ways (FileSyncStateStore, encode_key, FileCredentialStore). Each affected module now states an explicit __all__ so the autofixer cannot take them again, and the two files that pushed past the cap once __all__ was added are split further: _firebase.py 260 -> 247 + _fberrors 25 _sync.py 257 -> 188 + _syncargs 91 Test patch targets followed the code they patch: gatelock._outputs.shutil -> _xrandr.shutil, gatelock._arbiter.Path -> _claims.Path. gatelock and crdt-sync both pass `pre-commit run --all-files` in full; 389 and 259 tests at 100% branch coverage; the root gate exits 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
112 lines
3.5 KiB
Python
112 lines
3.5 KiB
Python
"""Reading outputs by shelling out to `xrandr --query`.
|
|
|
|
Split from :mod:`gatelock._outputs`, which keeps the value types and the
|
|
enumerator that picks a source. This is the fallback path: slower than the
|
|
RandR backend in :mod:`gatelock._randr` and it cannot deliver change events,
|
|
but it needs no optional dependency, so it is what runs on a bare install.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
|
|
from gatelock._output_types import Output, OutputRect
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
_XRANDR_TIMEOUT_S = 5.0
|
|
|
|
# Output lines start at column 0; mode lines are indented four spaces. That
|
|
# anchor is the whole robustness story -- without it, the mode line
|
|
# " 2560x1440 59.95*+" parses as a geometry and every modeless output
|
|
# silently looks live.
|
|
_OUTPUT_LINE = re.compile(
|
|
r"^(?P<name>\S+)\s+(?P<state>connected|disconnected)\b(?P<rest>.*)$",
|
|
re.MULTILINE,
|
|
)
|
|
_GEOMETRY = re.compile(r"(?P<w>\d+)x(?P<h>\d+)\+(?P<x>-?\d+)\+(?P<y>-?\d+)")
|
|
|
|
_GEOMETRY = re.compile(r"(?P<w>\d+)x(?P<h>\d+)\+(?P<x>-?\d+)\+(?P<y>-?\d+)")
|
|
|
|
|
|
def _parse_rect(head: str) -> OutputRect | None:
|
|
"""Parse a geometry token out of an xrandr output line's head segment."""
|
|
match = _GEOMETRY.search(head)
|
|
if match is None:
|
|
return None
|
|
width = int(match.group("w"))
|
|
height = int(match.group("h"))
|
|
if width <= 0 or height <= 0:
|
|
return None
|
|
return OutputRect(
|
|
x=int(match.group("x")),
|
|
y=int(match.group("y")),
|
|
width=width,
|
|
height=height,
|
|
)
|
|
|
|
|
|
def parse_xrandr_query(text: str) -> tuple[Output, ...]:
|
|
"""Parse ``xrandr --query`` output into outputs.
|
|
|
|
Args:
|
|
text: Raw stdout from ``xrandr --query``.
|
|
|
|
Returns:
|
|
One :class:`Output` per output line, in the order xrandr listed them.
|
|
"""
|
|
outputs: list[Output] = []
|
|
for match in _OUTPUT_LINE.finditer(text):
|
|
# Everything after the first "(" is rotation/reflection vocabulary and
|
|
# panning noise -- never geometry. Slicing it off keeps the search
|
|
# honest.
|
|
head = match.group("rest").split("(", 1)[0]
|
|
outputs.append(
|
|
Output(
|
|
name=match.group("name"),
|
|
connected=match.group("state") == "connected",
|
|
rect=_parse_rect(head),
|
|
primary="primary" in head.split(),
|
|
)
|
|
)
|
|
return tuple(outputs)
|
|
|
|
|
|
def scan_xrandr() -> tuple[Output, ...] | None:
|
|
"""Enumerate outputs by shelling out to ``xrandr --query``.
|
|
|
|
Returns:
|
|
The parsed outputs, or None if xrandr is missing, failed, timed out or
|
|
produced nothing parseable.
|
|
"""
|
|
binary = shutil.which("xrandr")
|
|
if binary is None:
|
|
_logger.warning("xrandr is not on PATH; cannot enumerate outputs")
|
|
return None
|
|
try:
|
|
completed = subprocess.run(
|
|
[binary, "--query"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=_XRANDR_TIMEOUT_S,
|
|
check=False,
|
|
)
|
|
except (subprocess.TimeoutExpired, OSError) as exc:
|
|
_logger.warning("xrandr --query failed to run: %s", exc)
|
|
return None
|
|
if completed.returncode != 0:
|
|
_logger.warning(
|
|
"xrandr --query exited %d: %s",
|
|
completed.returncode,
|
|
completed.stderr.strip(),
|
|
)
|
|
return None
|
|
outputs = parse_xrandr_query(completed.stdout)
|
|
if not outputs:
|
|
_logger.warning("xrandr --query produced no parseable output lines")
|
|
return None
|
|
return outputs
|