No description
  • Python 83.8%
  • C++ 12.1%
  • Nix 3.5%
  • CMake 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-13 12:44:30 +02:00
nix Fix native tray restoration and offer a combined control icon 2026-09-13 12:44:30 +02:00
patches Fix native tray restoration and offer a combined control icon 2026-09-13 12:44:30 +02:00
src Fix native tray restoration and offer a combined control icon 2026-09-13 12:44:30 +02:00
tests Fix native tray restoration and offer a combined control icon 2026-09-13 12:44:30 +02:00
.gitattributes feat: introduce standalone Gamescope tray helper 2026-09-13 11:46:38 +02:00
.gitignore feat: introduce standalone Gamescope tray helper 2026-09-13 11:46:38 +02:00
CMakeLists.txt Fix native tray restoration and offer a combined control icon 2026-09-13 12:44:30 +02:00
default.nix Fix native tray restoration and offer a combined control icon 2026-09-13 12:44:30 +02:00
flake.lock feat: introduce standalone Gamescope tray helper 2026-09-13 11:46:38 +02:00
flake.nix feat: introduce standalone Gamescope tray helper 2026-09-13 11:46:38 +02:00
LICENSE feat: introduce standalone Gamescope tray helper 2026-09-13 11:46:38 +02:00
pyproject.toml feat: introduce standalone Gamescope tray helper 2026-09-13 11:46:38 +02:00
README.md Fix native tray restoration and offer a combined control icon 2026-09-13 12:44:30 +02:00

Gamescope tray

Run an application inside a private Gamescope display with its own tray icon, hide/restore shortcut, resolution and FPS controls, and playback volume/muting. Applications can use the helper's tray icon or forward their own native XEmbed icons. Each instance controls only its own display and tagged audio streams.

Requires Linux, an X11-capable desktop session and a StatusNotifier tray host. The package uses Gamescope's SDL X11 backend; a Wayland desktop needs Xwayland. Automatic startup dimensions and refresh detection currently require niri.

Build, run and install

With Nix and flakes enabled, build directly from the public repository:

nix build 'git+https://git.karpfen.dev/karpfen/gamescope-tray?ref=main#gamescope-tray'
./result/bin/gamescope-tray -- your-application

Run without installing into a profile:

nix run 'git+https://git.karpfen.dev/karpfen/gamescope-tray?ref=main#gamescope-tray' -- -- your-application

Install into your user profile:

nix profile add 'git+https://git.karpfen.dev/karpfen/gamescope-tray?ref=main#gamescope-tray'

Upgrade to the latest commit on main:

nix profile upgrade --refresh gamescope-tray

Restart the application after upgrading to use the updated helper. From a repository checkout, use .#gamescope-tray in place of the remote reference for nix build or nix run.

Launch options

For Steam launch options:

gamescope-tray -- %command%

Outside niri, specify numeric dimensions and a visible FPS cap (0 is unlimited):

gamescope-tray -w 1920 -h 1080 -r 0 -- your-application

In helper mode, click the tray icon to hide or restore the application. Alt+X also hides a focused helper-mode window. The shortcut waits for key release and is local to that window. --hide-shortcut none passes it through to the application.

Use native mode for applications that already have an XEmbed tray:

gamescope-tray --tray-mode native -w 2560 -h 1440 -W auto -H auto -- your-application

Native mode follows the application's own tray state and passes Alt+X through to it. Choose how the native tray and our settings appear:

  • --native-tray combined (default): one persistent icon using the game-window image. Left-click forwards activation to the application's native tray, so it can resume rendering. Right-click opens resolution, FPS and audio settings. Middle-click and scrolling also pass through when there is one native icon. The Application tray submenu offers native activation and context-menu requests. If the application has multiple native icons, left-click opens the menu so you can select one. If none exists, it opens settings.
  • --native-tray separate: forwards each native icon with its original mouse actions, plus a settings-only icon with a sliders badge over the game-window image. This is useful for applications with their own tray menus. The settings icon has no Hide/Restore action; use the native icon to resume the application.

Both modes keep the XEmbed bridge alive independently of the settings UI. Restarting the settings UI retains live native activation endpoints and does not reveal an application that remains in its own tray mode. Removed native icons are forgotten and newly created ones are discovered. A destroyed icon cannot be activated until the application supplies another one.

Launchers or dialogs that keep normal windows mapped can keep the outer window visible. Native context-menu display still depends on the application and desktop.

Titles and icons follow the largest suitable inner application window and are retained while hidden. Before detection, the title uses the command basename; the icon uses an available Steam application icon or a controller symbol. --title NAME overrides automatic title selection.

Display and frame rate

Game dimensions and visible FPS default to auto: niri's current physical output mode is queried once at startup, independently of desktop scaling. --startup-output NAME selects an output instead of using the focused one. Explicit rendering dimensions remain independent of the outer window size. Missing or disabled outputs produce an error; numeric values need no query.

The tray offers resolution presets and custom sizes from 320×200 to 7680×4320, fullscreen, visible FPS (0–1000, with 0 meaning unlimited) and hidden FPS (1–240). Changing the virtual display size does not guarantee the application adjusts its render resolution; some applications require a settings change or restart. Physical output changes and outer window resizing do not change the selected render resolution. These controls never change the physical monitor's mode.

Helper mode defaults to --limiter mangohud, using a private configuration and no HUD overlay. Its game-side cap defaults to the output's refresh rate while visible and 5 FPS while hidden. --limiter-timing early|late selects timing; the default is early. Late timing may work better for NVIDIA OpenGL workloads. The private file lives under $XDG_CACHE_HOME/gamescope-tray and is removed on normal exit. Global MangoHud configuration is not modified or merged.

Native mode defaults to --limiter gamescope. This controls only the private Gamescope FPS property and restores its previous value when limiting ends. Use --limiter mangohud explicitly for game-side throttling in native mode. Only one limiter runs at a time. Gamescope-only limiting does not reliably reduce the application's rendering workload on every renderer.

MangoHud cap changes currently reload its configuration. Concurrent reload stability remains an unresolved limitation; this extraction does not fix it. Low FPS can affect application simulation or networking, and VRAM stays allocated. Measure behavior and GPU consumption with the actual application.

Audio and idle behavior

The Audio menu controls volume, manual mute and Mute while hidden. Automatic muting is enabled by default; disable it in the menu or use --no-mute-while-hidden. Restore removes only the automatic mute, preserving existing or explicitly selected manual mutes. Volume preserves channel balance, and newly opened streams inherit the instance's selected settings.

Playback streams are matched through a unique per-launch PulseAudio property. This includes pipewire-pulse and Wine/Proton clients that preserve the tag. Device volumes, microphone streams and unrelated playback are not controlled. The worker handles audio-server disconnects without blocking the tray.

The packaged Gamescope carries two scoped patches. One centralizes idle inhibition around window visibility, including external X11 unmaps and hidden title updates. Restoring the application reacquires inhibition; setting SDL_VIDEO_ALLOW_SCREENSAVER=1 disables inhibition throughout. The other handles the helper's shortcut inside SDL and hides the window after key release. The supervisor observes ordinary window visibility, without a custom X11 request property. Keeping keyboard handling inside SDL preserves the shortcut when Gamescope grabs the keyboard. If the tray UI fails, the supervisor restarts the control icon and, in helper mode, restores the window.

Options

Option Purpose
--tray-mode helper|native Owned tray or native tray forwarding
--native-tray combined|separate One combined icon (default) or native icons plus badged settings
--title NAME Fixed control icon title
--hide-shortcut alt-x|none Helper-mode hide shortcut
--[no-]mute-while-hidden Automatic playback mute
-w, -h Initial inner dimensions, default auto
-W, -H Outer dimensions, default inner dimensions; accepts auto
-r, --fps-limit, --refresh Visible cap, default auto; 0 is unlimited
--idle-fps Hidden cap, default 5
--startup-output NAME niri output for automatic startup values
--limiter mangohud|gamescope Game-side or compositor-side limiter
--limiter-timing early|late MangoHud limiter timing
-F linear|nearest|fsr|nis|pixel Scaling filter
-f, --windowed Fullscreen (default) or windowed startup
--help Full option listing (-h means height)

Selections last for the current instance. Launch options supply defaults for the next launch. Do not nest the helper inside another Gamescope instance or add Gamescope's Steam mode (-e). Restart the application after package updates.

Development and tests

The launcher and support modules live in src/, package expressions in default.nix and nix/, scoped Gamescope patches in patches/, and regression tests in tests/. default.nix accepts the caller's package set via pkgs.callPackage, allowing consumers to retain their configuration and overlays. The KDE-derived bridge retains its upstream LGPL licensing; helper code is MIT.

The commands below assume Nix, Ruff and Python with python-xlib, dbus-python and pulsectl are available. The included development shell provides these tools, or you can supply them through your existing environment.

nix flake check
python -m unittest discover -s tests -p 'test_*.py' -v
ruff check src tests
ruff format --check src tests

Unit tests cover destroyed-window races, automatic startup mode selection, fixed rendering dimensions, malformed outputs and CLI wiring. They need no desktop. nix flake check also builds the package.

Desktop tests also require niri, Xwayland, a StatusNotifier tray host and busctl. They launch uniquely named synthetic windows:

nix build .#gamescope-tray
python tests/check_helper.py ./result/bin/gamescope-tray
python tests/check.py ./result/bin/gamescope-tray
python tests/check_title.py ./result/bin/gamescope-tray
python tests/check_icon.py ./result/bin/gamescope-tray
python tests/check_shortcut.py ./result/bin/gamescope-tray
python tests/check_idle_override.py ./result/bin/gamescope-tray
python tests/check_native_controls.py ./result/bin/gamescope-tray
python tests/check_native_combined.py ./result/bin/gamescope-tray

These cover owned/native trays, isolated instances, live resolutions and FPS, idle inhibition, titles/icons, shortcut forwarding and UI recovery. Run them sequentially in an available desktop session. They can move focus and briefly enter fullscreen. Diagnostics are written under /tmp/gamescope-tray-*.

check_audio.py LAUNCHER PULSEAUDIO_PACKAGE uses a private server and silent streams to test volume/mute behavior and reconnects. check_render.py LAUNCHER SDL2_LIBRARY measures an OpenGL producer before, during and after hiding. For Vulkan, build .#vulkan-fixture and pass its bin/vkcube with --vulkan. These are optional desktop checks, not part of nix flake check.

Gamescope 3.16.23 has a known SDL teardown abort after child exit; desktop tests report it separately. Native XEmbed context-menu forwarding and rendering-layer compatibility still depend on the application and desktop environment.