# Capture window classes for BACKLOG #41 float candidates (hyprpolkitagent, # pinentry-qt, optional portal). Headless softGL Hyprland; dumps hyprctl # clients while dialogs are open into $out/classes.json + classes.txt. # # nix build --impure -f tools/capture-float-classes.nix --no-link --print-out-paths # # Maintainer harness — not a checks.* gate (needs interactive dialogs; # softGL may drop some Qt windows). let flake = builtins.getFlake ("git+file://" + toString ../.); inherit (flake.inputs) nixpkgs home-manager; pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ flake.overlays.default ]; config.allowUnfree = true; }; in pkgs.testers.runNixOSTest { name = "capture-float-classes"; node.pkgsReadOnly = false; node.specialArgs = { username = "nomarchy"; }; nodes.machine = { pkgs, lib, ... }: { imports = [ flake.nixosModules.nomarchy home-manager.nixosModules.home-manager ]; virtualisation.memorySize = 4096; virtualisation.cores = 2; hardware.graphics.enable = true; environment.variables.LIBGL_ALWAYS_SOFTWARE = "1"; boot.initrd.availableKernelModules = [ "virtio_gpu" ]; virtualisation.qemu.options = [ "-vga none" "-device virtio-gpu-pci,xres=1280,yres=800" ]; users.users.nomarchy = { isNormalUser = true; password = "test"; extraGroups = [ "wheel" "video" ]; }; security.sudo.wheelNeedsPassword = false; # PolicyKit test action so pkexec can raise the agent. security.polkit.enable = true; nomarchy.system.stateFile = flake + "/theme-state.json"; systemd.tmpfiles.rules = [ "d /home/nomarchy/.nomarchy 0755 nomarchy users -" "C /home/nomarchy/.nomarchy/theme-state.json 0644 nomarchy users - ${flake + "/theme-state.json"}" ]; services.greetd.settings.initial_session = { command = "start-hyprland"; user = "nomarchy"; }; home-manager.useGlobalPkgs = true; home-manager.users.nomarchy = { imports = [ flake.homeModules.nomarchy ]; nomarchy.stateFile = flake + "/theme-state.json"; nomarchy.idle.enable = false; # Ensure pinentry-qt is the active pinentry (keys.nix default). }; }; testScript = '' machine.wait_for_unit("multi-user.target") machine.wait_for_file("/run/user/1000/hypr", 180) machine.sleep(15) hy_env = "XDG_RUNTIME_DIR=/run/user/1000 HYPRLAND_INSTANCE_SIGNATURE=$(ls /run/user/1000/hypr | head -1)" hyctl = f"su - nomarchy -c '{hy_env} hyprctl" def clients(): return machine.succeed(f"{hyctl} clients -j'").strip() dumps = [] # ── pinentry-qt (keys.nix default) ─────────────────────────────── machine.execute( f"su - nomarchy -c '{hy_env} timeout 8 pinentry-qt' >/tmp/pinentry.log 2>&1 &" ) machine.sleep(3) dumps.append(("pinentry", clients())) machine.execute("pkill -x pinentry-qt >/dev/null 2>&1 || true") machine.sleep(1) # ── hyprpolkitagent via pkexec (passwordless sudo wheel — may still # open agent depending on polkit rules; best-effort) ──────────── machine.execute( f"su - nomarchy -c '{hy_env} timeout 8 pkexec true' >/tmp/pkexec.log 2>&1 &" ) machine.sleep(3) dumps.append(("pkexec", clients())) machine.execute("pkill -x hyprpolkitagent >/dev/null 2>&1 || true") machine.execute("pkill -f pkexec >/dev/null 2>&1 || true") machine.sleep(1) # ── Always dump current clients for residual ───────────────────── dumps.append(("idle", clients())) import json out_path = machine.succeed("mktemp -d").strip() summary = [] for label, raw in dumps: path = f"{out_path}/{label}.json" machine.succeed(f"cat > {path} <<'EOF'\n{raw}\nEOF") try: data = json.loads(raw) if raw else [] except Exception as e: data = [] summary.append(f"{label}: parse error {e}") classes = sorted({ c.get("class") or c.get("initialClass") or "" for c in (data if isinstance(data, list) else []) } - {""}) titles = sorted({ c.get("title") or c.get("initialTitle") or "" for c in (data if isinstance(data, list) else []) } - {""}) summary.append(f"{label}: classes={classes} titles={titles}") machine.succeed( "cat > /tmp/float-class-summary.txt <<'EOF'\n" + "\n".join(summary) + "\nEOF" ) print("\n".join(summary)) # Copy into the test result via machine.copy_from_vm if available — # at minimum assert pinentry produced *some* client dump JSON. machine.succeed("test -s /tmp/float-class-summary.txt") ''; }