labwc + Noctalia Shell (Testbranch, fedora-bootc-Basis)
This commit is contained in:
Executable
+171
@@ -0,0 +1,171 @@
|
||||
#!/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))
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# seed-labwc-skel: kopiert fehlende labwc/kanshi-Defaults aus /etc/skel.
|
||||
# Nuetzlich nach Rebase von Kinoite auf den labwc-Zweig, weil /home
|
||||
# erhalten bleibt und /etc/skel nur bei NEU angelegten Benutzern greift.
|
||||
# Bestehende Dateien werden NIEMALS ueberschrieben (cp -n).
|
||||
set -euo pipefail
|
||||
|
||||
SKEL="/etc/skel"
|
||||
TARGETS=(
|
||||
".config/labwc/environment"
|
||||
".config/labwc/autostart"
|
||||
".config/labwc/rc.xml"
|
||||
".config/labwc/menu.xml"
|
||||
".config/kanshi/config.template"
|
||||
)
|
||||
|
||||
for rel in "${TARGETS[@]}"; do
|
||||
src="${SKEL}/${rel}"
|
||||
dst="${HOME}/${rel}"
|
||||
if [[ ! -e "${dst}" ]]; then
|
||||
mkdir -p "$(dirname "${dst}")"
|
||||
cp -n "${src}" "${dst}"
|
||||
echo "angelegt: ${dst}"
|
||||
else
|
||||
echo "bleibt: ${dst}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ! -e "${HOME}/.config/kanshi/config" ]]; then
|
||||
echo ""
|
||||
echo "Keine kanshi-Config gefunden. Erzeugen mit:"
|
||||
echo " kanshi-autoconfig --apply (schreibt ~/.config/kanshi/config)"
|
||||
fi
|
||||
Reference in New Issue
Block a user