diff --git a/.claude/tasks/0030-desktop-portals.md b/.claude/tasks/0030-desktop-portals.md index f18d3a5..62eb7b2 100644 --- a/.claude/tasks/0030-desktop-portals.md +++ b/.claude/tasks/0030-desktop-portals.md @@ -12,7 +12,24 @@ In-app screen sharing depends on these regardless of whether the recorder is pre ## Acceptance criteria -- [ ] A portals module exists in the desktop group and is enabled by the aggregator. -- [ ] The Hyprland desktop portal (screencast, screenshot, global shortcuts) and the GTK portal (file dialogs, appearance) are both configured. -- [ ] In-app screen sharing is available independent of the screen recorder. -- [ ] neogaia builds green under `nix flake check`. +- [x] A portals module exists in the desktop group and is enabled by the aggregator. +- [x] The Hyprland desktop portal (screencast, screenshot, global shortcuts) and the GTK portal (file dialogs, appearance) are both configured. +- [x] In-app screen sharing is available independent of the screen recorder. +- [x] neogaia builds green under `nix flake check`. + +## Implementation Notes + +- **The module owns routing, not the backend packages.** + The Hyprland compositor integration (`programs.hyprland`) already forces both portal backends into `xdg.portal.extraPortals` — `xdg-desktop-portal-hyprland` through its `portalPackage`, and `xdg-desktop-portal-gtk` through nixpkgs' `wayland-session.nix` (`enableGtkPortal` defaults on) — and turns `xdg.portal.enable` on. + A portal backend only answers while its compositor runs, so those packages belong with the compositor and cannot be removed there; re-declaring them here would only duplicate them. + The genuinely-missing, first-class piece was the routing: `xdg.portal.config` was empty, and which backend answered each request rode on a config file the Hyprland package happens to ship (`hyprland-portals.conf`, `default=hyprland;gtk`). + This module makes that routing explicit and declarative. + +- **Per-interface routing, not a preference list.** + Rather than `default = [ "hyprland" "gtk" ]` (which tries Hyprland first for every interface and falls through to GTK), the three interfaces the Hyprland portal actually implements — `ScreenCast`, `Screenshot`, `GlobalShortcuts`, confirmed from its `hyprland.portal` file — are routed to Hyprland explicitly, and GTK is the default for everything else. + This directly encodes the spec's split (Hyprland for the screen-facing requests, GTK for file dialogs and appearance) and keeps appearance on GTK even if a future Hyprland portal starts implementing `org.freedesktop.impl.portal.Settings`. + +- **Already functional, now first-class.** + Because the compositor integration already supplied both backends and a working shipped route, in-app screen sharing was effectively working before this task as an implicit side-effect. + The deliverable is the explicit, aggregator-enabled `modules.desktop.portals` module, so the desktop's checklist reads completely and screen sharing no longer depends on a package's incidental default. + Verified: the built config emits `/etc/xdg/xdg-desktop-portal/portals.conf` with `default=gtk` plus the three Hyprland routes, and neogaia's toplevel builds green. diff --git a/modules/desktop/desktop.nix b/modules/desktop/desktop.nix index b5ffa70..4e201e7 100644 --- a/modules/desktop/desktop.nix +++ b/modules/desktop/desktop.nix @@ -15,6 +15,7 @@ in modules.desktop.hypridle.enable = lib.mkDefault true; modules.desktop.login.enable = lib.mkDefault true; modules.desktop.mako.enable = lib.mkDefault true; + modules.desktop.portals.enable = lib.mkDefault true; modules.desktop.recording.enable = lib.mkDefault true; modules.desktop.rofi.enable = lib.mkDefault true; modules.desktop.screenshot.enable = lib.mkDefault true; diff --git a/modules/desktop/portals.nix b/modules/desktop/portals.nix new file mode 100644 index 0000000..c7466f4 --- /dev/null +++ b/modules/desktop/portals.nix @@ -0,0 +1,22 @@ +{ config, lib, ... }: +# XDG desktop portal routing for in-app screen sharing and file dialogs. +let + cfg = config.modules.desktop.portals; +in +{ + options.modules.desktop.portals.enable = + lib.mkEnableOption "XDG desktop portals for in-app screen sharing and file dialogs"; + + config = lib.mkIf cfg.enable { + # The backend packages arrive with the compositor, so only the routing is set + # here. + # GTK is the default backend, and the three compositor-native requests go to + # Hyprland. + xdg.portal.config.common = { + default = [ "gtk" ]; + "org.freedesktop.impl.portal.ScreenCast" = [ "hyprland" ]; + "org.freedesktop.impl.portal.Screenshot" = [ "hyprland" ]; + "org.freedesktop.impl.portal.GlobalShortcuts" = [ "hyprland" ]; + }; + }; +}