From 46c03df06d67a0f4112b75a27472109816905988 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Tue, 1 Sep 2026 10:19:30 +0100 Subject: [PATCH] feat(sway): 4K scale 2, natural mouse scroll, idle inhibit for emulators Three unrelated desktop-side fixes, all in home/sway.nix. Sway cannot key an output rule on resolution, so scale 2 on 4K displays is applied by a user service that runs once at session start and again on every output event, which is what makes it survive a display being switched off and on. The scale guard in the jq filter stops the subscription feeding itself. kanshi still owns mode and position; no profile here sets a scale, so the two do not fight. natural_scroll on type:pointer gives mice the same scroll direction as the touchpads already had. PCSX2 and Dolphin default to borderless-windowed rather than compositor fullscreen, so the existing inhibit_idle fullscreen rules never matched and swaylock engaged mid-game. Inhibit on focus for the emulators instead. The Steam session is unaffected either way: swayidle is wanted by graphical-session.target and conditional on WAYLAND_DISPLAY, and the gamescope session provides neither. Co-Authored-By: Claude Opus 5 (1M context) --- docs/hosts/console.md | 19 +++++++++++++++ docs/keybindings.md | 4 ++-- home/sway.nix | 54 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/docs/hosts/console.md b/docs/hosts/console.md index a487d9e..e7483b5 100644 --- a/docs/hosts/console.md +++ b/docs/hosts/console.md @@ -124,6 +124,25 @@ The greeter is **Dvorak**, like every other host here (`modules/sway.nix` forces `XKB_DEFAULT_VARIANT=dvorak` on cage). Only relevant if someone else has to type a password. +### The Sway session behind it + +Two fleet-wide rules in `home/sway.nix` matter more on a television than +anywhere else: + +- Any output running at 4K is forced to **scale 2** (1920x1080 logical). Sway's + config cannot key an output rule on resolution, so a small user service + applies it at session start and again on every output event — which is what + makes it survive the TV being switched off and on. +- The emulators inhibit idle **while focused**, not only while fullscreen. + PCSX2 and Dolphin default to borderless-windowed rather than compositor + fullscreen, so the generic `inhibit_idle fullscreen` rules never fired and + swaylock cut in mid-game. + +Neither applies in the Steam session: `swayidle` is a home-manager user service +wanted by `graphical-session.target` and conditional on `WAYLAND_DISPLAY`, and +the gamescope session starts neither, so nothing locks the screen in Big +Picture. + ### Adding non-Steam titles to Big Picture RetroArch, PCSX2, Dolphin and Clone Hero are installed system-wide but are not diff --git a/docs/keybindings.md b/docs/keybindings.md index 84831fb..8d5d6ae 100644 --- a/docs/keybindings.md +++ b/docs/keybindings.md @@ -18,8 +18,8 @@ rebuild, never the generated dotfiles. - Letter keys are **keysyms** (the character produced), not physical positions. The keyboard is **Dvorak** (`us`/`dvorak`), so e.g. "Super+s" is whatever key types `s` in Dvorak. -- Shortcuts apply to every Sway host (MBP, T400, Mac Pro); brightness keys are - laptop-only, as noted. +- Shortcuts apply to every Sway host (MBP, T400, Mac Pro, Console); brightness + keys are laptop-only, as noted. --- diff --git a/home/sway.nix b/home/sway.nix index 6374608..1aafa14 100644 --- a/home/sway.nix +++ b/home/sway.nix @@ -30,6 +30,28 @@ let - | ${pkgs.swappy}/bin/swappy -f - ''; + # Sway's config cannot express an output rule keyed on resolution, so scale is + # applied from outside it: once at session start, then on every output event, + # which is what makes it survive a display being unplugged or power-cycled. + # The `.scale != 2` guard stops the subscription feeding itself, since setting + # a scale is itself an output event. + scaleHidpi = pkgs.writeShellScript "sway-scale-hidpi" '' + apply() { + ${pkgs.sway}/bin/swaymsg -r -t get_outputs \ + | ${pkgs.jq}/bin/jq -r '.[] + | select(.active and .current_mode.width >= 3840 and .scale != 2) + | .name' \ + | while read -r name; do + ${pkgs.sway}/bin/swaymsg output "$name" scale 2 + done + } + + apply + ${pkgs.sway}/bin/swaymsg -t subscribe -m '["output"]' | while read -r _; do + apply + done + ''; + # Binding-mode names. The string is both the `modes` attr key and what the # bar's mode indicator shows, so the keys are spelled out in the label. layoutMode = "layout: [s]tacking [w]tabbed [e]split"; @@ -60,6 +82,8 @@ in tap = "enabled"; natural_scroll = "enabled"; }; + # Mice (the MX Master 3 among them) scroll the same way as the touchpads. + input."type:pointer".natural_scroll = "enabled"; # Solid Catppuccin Mocha base as the wallpaper (no image dependency). output."*".bg = "#${ctp.base} solid_color"; @@ -111,6 +135,18 @@ in criteria.class = ".*"; command = "inhibit_idle fullscreen"; } + # Emulators default to borderless-windowed rather than compositor + # fullscreen, so the rules above never fire and swaylock cuts in + # mid-game. Inhibit on focus instead, which does not depend on how the + # application asked to fill the screen. + { + criteria.app_id = "(?i)^(pcsx2|dolphin-emu|retroarch|clone.?hero)"; + command = "inhibit_idle focus"; + } + { + criteria.class = "(?i)^(pcsx2|dolphin-emu|retroarch|clone.?hero)"; + command = "inhibit_idle focus"; + } ]; # Binding modes (submenus). Entered from keybindings below; each action @@ -307,6 +343,24 @@ in Install.WantedBy = [ "sway-session.target" ]; }; + # Every 4K output runs at scale 2 (1920x1080 logical), on every host and + # whatever the display is called. kanshi below owns mode and position; this + # owns scale for the one case that is a fleet-wide rule rather than a + # per-display fact, and the two do not fight because no kanshi profile here + # sets a scale. + systemd.user.services.sway-scale-hidpi = { + Unit = { + Description = "Force scale 2 on 4K Sway outputs"; + PartOf = [ "graphical-session.target" ]; + After = [ "graphical-session.target" ]; + }; + Service = { + ExecStart = "${scaleHidpi}"; + Restart = "on-failure"; + }; + Install.WantedBy = [ "sway-session.target" ]; + }; + # Output/display management. Reacts to hotplug and applies per-display # mode/scale/position. Profiles are hardware-specific: the safe default below # just enables the internal laptop panel; add docked/desktop profiles with the