1# Linux window capture
2
3Wayland only. Each capture picks one backend in `LinuxSnapShot.ts` (Hyprland helper, Niri IPC, KDE
4helper, Screenshot portal with active-window target, GNOME extension, then the Electron picker) and
5never falls back to another on failure, cancellation, or denial. A stale `NIRI_SOCKET` or a reachable
6GNOME extension must not select another desktop's backend; session detection comes from
7`XDG_CURRENT_DESKTOP`, in `linuxCaptureSession.ts`.
8
9## Module boundaries
10
11`dbus-next` must only be reached through dynamic imports. `linuxCaptureSession.ts` holds the pure
12helpers (session detection, Niri binding text, portal key mapping, PNG reading) so the main process
13can answer "which desktop is this" without loading a D-Bus client on macOS or Windows.
14
15`patches/dbus-next@0.10.2.patch` removes `usocket` (its optional native Unix-FD transport) and
16connects to both `unix:path=` and `unix:abstract=` buses through Node's `net`. We never pass Unix
17descriptors, and upstream had no `net` fallback for abstract sockets, which `dbus-launch` produces
18outside systemd. Keep the patch when bumping `dbus-next`.
19
20## Accessibility identity
21
22The Screenshot portal returns a PNG URI and no window identity, so portal and picker captures carry
23no accessibility data. Do not infer it from the subsequently focused window or a guessed title.
24
25Native backends supply PID, title, and frame. AT-SPI matching is by PID plus a unique title and size
26match. AT-SPI reports `(0, 0)` for a window's screen position on Wayland even when the compositor
27knows the real one, so only width and height are compared, and descendant coordinates are trusted
28only when the AT-SPI root agrees with the compositor frame and at least one descendant reports a
29distinct position. Otherwise bounds are `null` rather than zero-origin rectangles.
30
31Title matching ignores a single leading Braille CLI spinner frame, since terminals change it between
32capture and lookup. KDE supplies both `frameGeometry` and `clientGeometry`; accept either verified
33size, never a decoration-height tolerance.
34
35On GNOME, browser accessibility bridges need `org.gnome.desktop.interface toolkit-accessibility`
36before the browser starts. Do not toggle it during capture.
37
38## KDE
39
40KWin authorizes `org.kde.KWin.ScreenShot2` by resolving the calling PID's executable against its
41application registry, so the helper is installed at a stable path under the XDG data home with a
42hidden desktop entry, not run from the AppImage mount. Two traps:
43
44- `X-KDE-DBUS-Restricted-Interfaces` uses KConfig's comma-separated list syntax. A trailing
45 semicolon becomes part of the interface name and KWin rejects it.
46- KService's cache key includes search paths and locale. Refresh with `kbuildsycoca6` both directly
47 and through `systemd-run --user`; refreshing only inside the AppImage environment can leave KWin
48 reading stale permissions.
49
50Readiness is checked by calling `CaptureWindow` with an invalid ID and expecting `InvalidWindow`,
51which runs after KWin's authorization check. Files on disk alone never mean ready.
52
53Plasma consumes Shift for shifted digits and produces a punctuation keysym, so a successfully bound
54`Ctrl+Shift+2` may never fire on some layouts. Letter chords work around it. A general fix needs
55layout-aware encoding.
56
57## Hyprland
58
59The helper maps a foreign-toplevel handle to the full 64-bit window address with
60`hyprland-toplevel-mapping-v1` before exporting through `hyprland-toplevel-export-v1`. Never
61truncate the address or pick a window by title.
62
63Hyprland 0.56.2 renders an access-denied texture instead of failing the export frame when consent
64is rejected or a `no_screen_share` rule applies. `ready` is not a permission grant, and pixels must
65not be inspected to guess one. See
66[ScreenshareFrame](https://github.com/hyprwm/Hyprland/blob/v0.56.2/src/managers/screenshare/ScreenshareFrame.cpp).
67
68Hyprland's GlobalShortcuts portal registers actions, not key chords. `shortcutActionRegistered` is
69distinct from `shortcutRegistered`; nothing in the UI may claim the keys are reserved.
70
71The bundled protocol XML ships with the helper because its BSD license requires the notice.
72
73## Niri
74
75Niri does not implement the global-shortcut portal. While capture is enabled the app owns
76`<app-id>.SnapShot` on the session bus and exports `com.t3tools.SnapShot.Capture`; the config
77binding spawns `gdbus` to call it. Development and packaged app IDs use separate names so a dev
78build does not steal the user's binding.
79
80Niri supplies no global screenshot origin, so AT-SPI matching uses logical size and title only.
81
82## Config edits
83
84`CaptureShortcutConfig` edits user dotfiles. The invariants: opening setup never reads the file;
85**Review changes** is the read consent; the renderer only ever sees before/after text over trusted
86desktop IPC; **Save** sends a proposal ID and the desktop re-verifies bytes, inode, mode, and
87includes against the snapshot it previewed before writing. Writes go through a staged temp file and
88atomic rename with a backup beside the original. Niri validates the staged file first.
89
90Modifier serialization writes Linux `Ctrl` explicitly, never the cross-platform `mod` alias.
91
92## GNOME extension
93
94Source in `apps/desktop/gnome-extension`, UUID `snap-shot@t3.codes`. GNOME only discovers a newly
95installed extension at login, so setup distinguishes "installed, needs logout" from "discovered but
96disabled" and compares loaded and installed versions.
97
98The extension trusts callers that own `com.t3tools.T3Code.SnapShot` (or the `.Development`
99variant) on the same connection. This is GNOME's trusted-session-client pattern, not authentication
100against a hostile process on the user's bus.
101
102Electron does not position overlay windows on Wayland, so the flash and flight run as Shell actors
103inside the extension with coordinates relative to T3's content area. Electron 44's restored-session
104path can skip rebinding and leave callbacks behind on unregister, which is why
105`PortalCaptureShortcut` owns its own portal session instead of using Electron's global-shortcut API.
106
107GNOME 50 removed `Meta.is_wayland_compositor`. Shell internals change across majors; verify each
108version before adding it to `metadata.json`.