Godot 4.4+ · GDExtension · MIT

Desktop portals for
Godot games on Linux.

Keep GameMode on, stop the screen blanking mid-cutscene, react to power-saver mode, open a URL, post a notification — from six lines of GDScript. Works inside a Flatpak sandbox, where the usual tricks quietly stop working.

No D-Bus in your game code. No shelling out. No broad sandbox permissions.

Why this exists

Everything a Linux game wants from the desktop sits behind D-Bus, and Godot cannot speak it. The usual workarounds break the moment your game ships as a Flatpak.

Without portals

  • OS.execute("gdbus", ...) — fails in a sandbox, and spawns a process per call.
  • Talking to gamemoded directly — needs a broad --talk-name that reviewers push back on.
  • Screen blanks during your cutscene because nothing inhibited the idle timer.
  • No way to know the laptop just entered power-saver mode.
  • Desktop-specific code paths for GNOME vs KDE vs wlroots.

With godot-xdg-portals

  • One autoload, DesktopServices, with a typed API and real enums.
  • GLib/GIO under the hood — no subprocess, ever.
  • Flatpak needs exactly --socket=session-bus and one --talk-name.
  • Capabilities discovered at runtime, so one build covers every desktop.
  • Windows, mobile and web load a null backend and keep running; macOS gets power-saver state.

Five portals, one small API

Each maps to an org.freedesktop.portal.* interface, discovered at startup and version-gated where the spec requires it.

GameMode

org.freedesktop.portal.GameMode

Ask the host to apply CPU governor and scheduling tweaks while your game is in the foreground.

DesktopServices.request_game_mode()

Inhibit

org.freedesktop.portal.Inhibit

Block idle, suspend, logout or user switching for as long as you hold the handle. Cutscenes stay on screen.

DesktopServices.inhibit(flags, reason)

PowerProfileMonitor

org.freedesktop.portal.PowerProfileMonitor

Know when the machine enters power-saver mode so you can drop the frame cap instead of draining the battery.

DesktopServices.is_power_saver_enabled()

OpenURI

org.freedesktop.portal.OpenURI

Open your manual, storefront or Discord in the user's browser. file: is refused, case-insensitively.

DesktopServices.open_uri(uri)

Notification

org.freedesktop.portal.Notification

Post a desktop notification with a priority, withdraw it later, and receive the action the player clicked.

DesktopServices.add_notification(id, title)

Runtime discovery

version properties & capabilities

Ask what the running desktop actually exports before you use it. Ship one build for every distro and compositor.

DesktopServices.has_capability(cap)

What it looks like

The whole integration, from a real project.

Keep the screen awake during a cutscene

var _handle := ""

func begin_cutscene() -> void:
    var flags := DesktopServices.InhibitFlags.IDLE | DesktopServices.InhibitFlags.SUSPEND
    _handle = DesktopServices.inhibit(flags, "Playing a cutscene")

func end_cutscene() -> void:
    if _handle != "":
        DesktopServices.close_request(_handle)
        _handle = ""

Turn GameMode on, and follow the battery

func _ready() -> void:
    if DesktopServices.has_capability(DesktopServices.Capability.GAME_MODE):
        DesktopServices.request_game_mode()

    DesktopServices.power_saver_changed.connect(_on_power_saver_changed)

func _on_power_saver_changed(enabled: bool) -> void:
    Engine.max_fps = 30 if enabled else 0

Unknown is a real answer, not a guess

# Returns true, false, or null — never a plausible-looking lie.
var saving: Variant = DesktopServices.is_power_saver_enabled()

if saving == null:
    pass  # The portal could not tell us. Do nothing.
elif saving:
    reduce_quality()

Honest by construction

A portal that is missing, an old desktop, a machine with no session bus — each has a defined answer, and none of them is a fake success.

SituationWhat you get
Portal interface not exportedhas_capability() is false; calls return false or ""
Power-saver state unreadablenull — never a guessed false
Portal too old for SchemeSupportednull, because the question genuinely cannot be answered
User dismisses the "open with" dialogResponse.CANCELLED, not a success
Windows, mobile, webNull backend; the addon loads and every call reports unavailable
macOS (experimental)Power-saver state only; the other four capabilities report unavailable
Headless Linux, no session busNull backend with the reason attached, no crash

Install it

Grab the release archive, or build the extension yourself.

From a release (v0.2.0, Linux x86-64 and arm64)

# godot-xdg-portals-0.2.0-linux-x86_64.zip (or -arm64), checked against its .sha256.
# Extract addons/xdg_portals into your project, then enable
# "XDG Portals" in Project Settings → Plugins.
# The plugin installs the DesktopServices autoload for you.

From source

git clone https://github.com/KevinTCoughlin/godot-xdg-portals.git
cd godot-xdg-portals
sudo apt-get install build-essential cmake pkg-config libglib2.0-dev
./scripts/build.sh --target template_release

Flatpak manifest — the whole permission cost

finish-args:
  - --socket=session-bus
  - --talk-name=org.freedesktop.portal.Desktop