extends Plugin ## Adds a "Switch to Steam" button to the power menu on ChimeraOS and SteamOS. ## ## OpenGamepadUI's built-in ChimeraOS platform support adds a "Switch to ## Desktop" button but has no way to return to Steam game mode. This plugin ## adds that missing button so users can round-trip between an exclusive ## OpenGamepadUI session and Steam. const SESSION_SELECT_PATH := "/usr/lib/os-session-select" const POWER_MENU_GROUP := "power_menu" const BUTTON_NAME := "SwitchToSteamButton" const MAX_ATTEMPTS := 600 # ChimeraOS records the session that was active before OpenGamepadUI took over # in this file when the OpenGamepadUI session is entered. We return to that # session instead of hardcoding a name so the autologin config is not mangled. const RETURN_SESSION_FILE := "/.config/chimeraos-session-return" const DEFAULT_RETURN_SESSION := "gamescope-session-steam-plus" # Sessions os-session-select can be told to return to (matching the values it # writes to the return-session file). Returning to the OpenGamepadUI session is # deliberately excluded: this plugin only runs inside OpenGamepadUI, so a return # file pointing at it is a stale self-reference (the last os-session-select run # recorded whatever session autologin was set to, which is OpenGamepadUI itself # once an exclusive OGPU session is active) and would just bounce back here. const RETURN_SESSIONS := [ "gamescope-session-steam-plus", "gamescope-session-steam", ] func _ready() -> void: logger = Log.get_logger("SessionSwitch", Log.LEVEL.INFO) if not _has_session_switcher(): logger.info("No session switcher script detected: " + SESSION_SELECT_PATH) return _add_button_when_ready() func _add_button_when_ready() -> void: for i in MAX_ATTEMPTS: if _try_add_button(): return await get_tree().process_frame logger.info("Power menu never became available. Skipping 'Switch to Steam' button.") func _try_add_button() -> bool: var power_menu := get_tree().get_first_node_in_group(POWER_MENU_GROUP) if not power_menu: return false var exit_button = power_menu.get("exit_button") if not exit_button: return false var container = exit_button.get_parent() if not container: return false if container.has_node(BUTTON_NAME): return true var button_scene := load("res://core/ui/components/card_button.tscn") as PackedScene if not button_scene: logger.error("Unable to load card_button scene.") return true var button := button_scene.instantiate() button.name = BUTTON_NAME button.set("click_focuses", false) button.set("text", "Switch to Steam") button.pressed.connect(_switch_session) container.add_child(button) container.move_child(button, exit_button.get_index()) var focus_group = power_menu.get("focus_group") if focus_group and focus_group.has_method("recalculate_focus"): focus_group.recalculate_focus() logger.info("Added 'Switch to Steam' button to the power menu.") return true ## Returns true if the session switcher script is present. func _has_session_switcher() -> bool: return FileAccess.file_exists(SESSION_SELECT_PATH) ## Switches back to the session that was active before OpenGamepadUI took ## over, using the OS session switcher. func _switch_session() -> void: _restart_inputplumber() var target := _get_return_session() var out: Array = [] var code := OS.execute(SESSION_SELECT_PATH, [target], out) if code != OK: logger.error("Unable to switch sessions: " + _result_text(out)) ## Restarts InputPlumber before switching back so the exclusive OpenGamepadUI ## session does not leave the gamepad intercepted (blocked) for Steam. ## ChimeraOS ships passwordless sudo for exactly this command in its default ## sudoers, so the switch itself remains non-interactive. func _restart_inputplumber() -> void: var out: Array = [] var code := OS.execute("sudo", ["-n", "systemctl", "restart", "inputplumber"], out) if code != OK: logger.warning("Unable to restart InputPlumber: " + _result_text(out)) ## Returns the session recorded by os-session-select before OpenGamepadUI was ## entered, falling back to the ChimeraOS default Steam session. func _get_return_session() -> String: var home := OS.get_environment("HOME") if home.is_empty(): return DEFAULT_RETURN_SESSION var file := FileAccess.open(home + RETURN_SESSION_FILE, FileAccess.READ) if file: var session := file.get_as_text().strip_edges() if session in RETURN_SESSIONS: return session return DEFAULT_RETURN_SESSION func _result_text(out: Array) -> String: return out[0] if not out.is_empty() else ""