#!/usr/bin/env python3 """kanshi-autoconfig - generiert kanshi-Profile aus live erkannten Outputs. Liest die angeschlossenen Monitore via `wlr-randr --json` aus (EDID-Namen, bevorzugte Modi, aktuelle Skalierung) und schreibt daraus eine funktionsfaehige ~/.config/kanshi/config mit den Profilen docked / undocked / fallback. Verhalten: --print Vorschlag auf stdout, schreibt nichts. --apply Schreibt (mit Backup *.bak), fragt nicht. --apply-if-managed Schreibt nur, wenn die Config fehlt oder den Marker '# kanshi-autoconfig: managed' enthaelt. -> Hand-Edits bleiben unangetastet. Wird vom labwc-Autostart bei jedem Login aufgerufen. Faellt die Erkennung fehl (kein Wayland, kein wlr-randr), wird eine reine Fallback-Config geschrieben, damit nie ein schwarzer Bildschirm ohne Layout dasteht. """ import json import shutil import subprocess import sys from datetime import datetime from pathlib import Path MARKER = "# kanshi-autoconfig: managed" CONFIG_PATH = Path.home() / ".config" / "kanshi" / "config" def detect_outputs(): """Liste von Dicts: name, internal, width, height, rate, scale.""" try: proc = subprocess.run( ["wlr-randr", "--json"], capture_output=True, text=True, timeout=10 ) except (FileNotFoundError, subprocess.TimeoutExpired): return [] if proc.returncode != 0: return [] try: data = json.loads(proc.stdout) except json.JSONDecodeError: return [] outputs = [] for out in data: name = out.get("name", "?") # Nur verbundene Outputs betrachten (kanshi matcht nur angesteckte) if out.get("connected") is False: continue modes = out.get("modes", []) or [] preferred = next((m for m in modes if m.get("preferred")), None) current = out.get("current_mode") or out.get("mode") chosen = preferred or current or (modes[0] if modes else None) if not chosen: continue width = chosen.get("width") height = chosen.get("height") rate = chosen.get("refresh") or chosen.get("rate") if isinstance(rate, (int, float)) and rate > 1000: rate = rate / 1000 # mHz -> Hz outputs.append( { "name": name, "internal": name.startswith(("eDP-", "LVDS-", "DSI-")), "width": width, "height": height, "rate": rate, "scale": out.get("scale", 1) or 1, } ) return outputs def mode_str(o): s = f"{o['width']}x{o['height']}" if o["rate"]: s += f"@{o['rate']:.2f}" if isinstance(o["rate"], float) else f"@{o['rate']}" return s def render(outputs): lines = [ MARKER, f"# generiert am {datetime.now().isoformat(timespec='seconds')}", "# Quelle: wlr-randr (live). Feintuning: wdisplays oder Hand-Edit", "# (Hand-Edit: Marker-Zeile loeschen, dann fasst --apply-if-managed", "# diese Datei nicht mehr an.)", "", ] externals = [o for o in outputs if not o["internal"]] internals = [o for o in outputs if o["internal"]] if externals: lines.append("profile docked {") for i in internals: lines.append(f" output {i['name']} disable") x = 0 for o in externals: lines.append( f" output {o['name']} mode {mode_str(o)} position {x},0 scale {o['scale']}" ) x += o["width"] or 0 lines.append("}") lines.append("") if internals: lines.append("profile undocked {") for o in internals: lines.append( f" output {o['name']} mode {mode_str(o)} position 0,0 scale {o['scale']}" ) lines.append("}") lines.append("") elif not externals: # Gar nichts erkannt -> reiner Fallback, nie leer lassen lines.append("# Keine Outputs erkannt (ausserhalb einer Wayland-Session") lines.append("# aufgerufen?) - nur Fallback geschrieben.") lines.append("") lines.append("profile fallback {") lines.append(" output * enable") lines.append("}") lines.append("") return "\n".join(lines) def is_managed(path: Path): if not path.exists(): return True try: return MARKER in path.read_text() except OSError: return False def apply(text): CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) if CONFIG_PATH.exists(): backup = CONFIG_PATH.with_suffix(".bak") shutil.copy2(CONFIG_PATH, backup) print(f"Backup: {backup}") CONFIG_PATH.write_text(text) print(f"Geschrieben: {CONFIG_PATH}") def main(argv): outputs = detect_outputs() if not outputs: print("WARN: keine Outputs erkannt - schreibe Fallback-Config", file=sys.stderr) text = render(outputs) # Default ohne Schreib-Flag: nur anzeigen, nichts schreiben (sicher). if "--print" in argv or ("--apply" not in argv and "--apply-if-managed" not in argv): print(text) if outputs: print( f"# Erkannt: {', '.join(o['name'] for o in outputs)}", file=sys.stderr, ) return 0 if "--apply-if-managed" in argv and not is_managed(CONFIG_PATH): print(f"{CONFIG_PATH} ist hand-gepflegt - lasse sie unangetastet.") return 0 apply(text) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))