← All solutions
Caelestia Shell · Game Mode

Game Mode: Integrating Shell
and Hyprland Settings

Caelestia Shell features a native GameMode.qml service designed to declaratively switch off compositor performance hogs (shadows, blur, gaps, animations) in Hyprland. I wanted to extend this service to also control internal shell-level transitions and panel opacities, ensuring both the shell and compositor visual systems operate in sync.

Arch Linux Hyprland + Caelestia Quickshell / QML
The Challenge

While Caelestia's native GameMode.qml service successfully disabled compositor-level features via Hypr.extras.applyOptions, the shell itself was left untouched: its UI animations kept rendering in the background, and panels remained translucent without any blur behind them.

Extending the service to disable internal shell animations and panel translucency introduced visual artifacts: application windows remained transparent (without blur, which made the desktop background leak through games) and repeated toggles broke. Additionally, if the shell restarted or crashed while Game Mode was active, all shell-level animations remained permanently locked at speed 0 on subsequent boots.

Root Cause Analysis

I analyzed Caelestia Shell's singleton services and variables configuration files to isolate three integration issues:

  • Window Rules Stack Overflow: The service attempted to enforce opacity by dispatching opaque true rules dynamically via socket. But Hyprland already had windowrule = opacity $windowOpacity override in rules.conf. Appending dynamic rules via IPC messages polluted the stack and created race conditions on rapid toggles.
  • Volatile Animation State Caching: When Game Mode runs, the service updates GlobalConfig.appearance.anim.durations.scale = 0. This is immediately saved into shell.json on disk. When the shell crashed or restarted, it loaded with scale 0, and since the minimum UI scale configurable is 0.1, the user was stuck without animations forever.
  • Static Palette Binding: The colors service compiled the translucent palette statically from Tokens.transparency.enabled instead of monitoring the reactive configuration service, causing the panel's transparency to stay active during Game Mode.
Integration Details

I integrated Caelestia Shell's native Game Mode service with internal visual switches and resolved these visual conflicts using the following QML and configuration patches:

1. Bridging Game Mode through Sourced Config Files

Rather than injecting window rules through socket commands, I leveraged Hyprland's native file-based sourcing system. Hyprland reads hypr-vars.conf before any rules. I modified the toggle to write directly to this file:

qml (GameMode.qml)
onEnabledChanged: {
    if (enabled) {
        // Save old states
        oldAnimScale = GlobalConfig.appearance.anim.durations.scale;
        oldTransparency = GlobalConfig.appearance.transparency.enabled;

        // Force shell elements to be fast and opaque
        GlobalConfig.appearance.anim.durations.scale = 0;
        GlobalConfig.appearance.transparency.enabled = false;

        setDynamicConfs(); // apply options: animations 0, shadows 0, blur 0...
        
        // Write the $windowOpacity override to the sourced vars config and reload
        Quickshell.execDetached(["bash", "-c", "echo '$windowOpacity = 1.0' > /home/franz/.config/caelestia/hypr-vars.conf && hyprctl reload"]);
    } else {
        // Restore shell variables
        GlobalConfig.appearance.anim.durations.scale = oldAnimScale;
        GlobalConfig.appearance.transparency.enabled = oldTransparency;

        // Clear overrides and reload compositor configuration
        Quickshell.execDetached(["bash", "-c", "true > /home/franz/.config/caelestia/hypr-vars.conf && hyprctl reload"]);
    }
}

2. Preventing Persistent Animation Lockouts on Startup

To prevent a crash or sudden restart from locking scale at 0, I added an auto-recovery verification block in the component completion handler. If the scale factor loaded is 0 while Game Mode is off, it auto-resets back to 1:

qml
Component.onCompleted: {
    if (!enabled && GlobalConfig.appearance.anim.durations.scale === 0) {
        GlobalConfig.appearance.anim.durations.scale = 1;
        GlobalConfig.appearance.transparency.enabled = true;
    }
}

3. Securing Shell Transparency Responsiveness

In Colours.qml, I rewrote the translucent palette mapping and the Transparency component definitions to rely on the reactive global configuration property instead of the static token property. This guarantees QML immediately updates panel opacities whenever the toggle changes:

qml (Colours.qml)
readonly property var tPalette: transparency.enabled ? translucentPalette : palette

component Transparency: QtObject {
    readonly property bool enabled: GlobalConfig.appearance.transparency.enabled
}
By mapping variables natively and binding the shell's components to the global configuration properties, I turned Game Mode from a simple Hyprland toggle script into a deep, resilient shell integration.