diff --git a/Makefile b/Makefile
index 51c7784e..d3fcbea5 100644
--- a/Makefile
+++ b/Makefile
@@ -15,12 +15,12 @@ EXPORT_TEMPLATE ?= $(HOME)/.local/share/godot/export_templates/$(GODOT_REVISION)
#EXPORT_TEMPLATE_URL ?= https://downloads.tuxfamily.org/godotengine/$(GODOT_VERSION)/Godot_v$(GODOT_VERSION)-$(GODOT_RELEASE)_export_templates.tpz
EXPORT_TEMPLATE_URL ?= https://github.com/godotengine/godot/releases/download/$(GODOT_VERSION)-$(GODOT_RELEASE)/Godot_v$(GODOT_VERSION)-$(GODOT_RELEASE)_export_templates.tpz
-ALL_ADDONS := ./addons/dbus/bin/libdbus.linux.template_$(BUILD_TYPE).x86_64.so ./addons/linuxthread/bin/liblinuxthread.linux.template_$(BUILD_TYPE).x86_64.so ./addons/pty/bin/libpty.linux.template_$(BUILD_TYPE).x86_64.so ./addons/unixsock/bin/libunixsock.linux.template_$(BUILD_TYPE).x86_64.so ./addons/xlib/bin/libxlib.linux.template_$(BUILD_TYPE).x86_64.so
-ALL_ADDON_FILES := $(shell find ./addons -regex '.*\(\.cpp\|\.h\|\.hpp\)$$')
+ALL_EXTENSIONS := ./addons/core/bin/libopengamepadui-core.linux.template_$(BUILD_TYPE).x86_64.so
+ALL_EXTENSION_FILES := $(shell find ./extensions/ -regex '.*\(\.rs|\.toml\|\.lock\)$$')
ALL_GDSCRIPT := $(shell find ./ -name '*.gd')
ALL_SCENES := $(shell find ./ -name '*.tscn')
ALL_RESOURCES := $(shell find ./ -regex '.*\(\.tres\|\.svg\|\.png\)$$')
-PROJECT_FILES := $(ALL_ADDONS) $(ALL_GDSCRIPT) $(ALL_SCENES) $(ALL_RESOURCES)
+PROJECT_FILES := $(ALL_EXTENSIONS) $(ALL_GDSCRIPT) $(ALL_SCENES) $(ALL_RESOURCES)
# Docker image variables
IMAGE_NAME ?= ghcr.io/shadowblip/opengamepadui-builder
@@ -144,24 +144,24 @@ build/metadata.json: build/opengamepad-ui.x86_64 assets/crypto/keys/opengamepadu
.PHONY: import
import: $(IMPORT_DIR) ## Import project assets
-$(IMPORT_DIR): $(ALL_ADDONS)
+$(IMPORT_DIR): $(ALL_EXTENSIONS)
@echo "Importing project assets. This will take some time..."
command -v $(GODOT) > /dev/null 2>&1
timeout --foreground 40 $(GODOT) --headless --editor . > /dev/null 2>&1 || echo "Finished"
touch $(IMPORT_DIR)
.PHONY: force-import
-force-import: $(ALL_ADDONS)
+force-import: $(ALL_EXTENSIONS)
@echo "Force importing project assets. This will take some time..."
command -v $(GODOT) > /dev/null 2>&1
timeout --foreground 40 $(GODOT) --headless --editor . > /dev/null 2>&1 || echo "Finished"
timeout --foreground 40 $(GODOT) --headless --editor . > /dev/null 2>&1 || echo "Finished"
-.PHONY: addons
-addons: $(ALL_ADDONS) ## Build GDExtension addons
-$(ALL_ADDONS) &: $(ALL_ADDON_FILES)
- @echo "Building native GDExtension addons..."
- cd ./gdext && $(MAKE) build
+.PHONY: extensions
+extensions: $(ALL_EXTENSIONS) ## Build native extensions
+$(ALL_EXTENSIONS) &: $(ALL_EXTENSION_FILES)
+ @echo "Building native extensions..."
+ cd ./extensions/core && $(MAKE) build
.PHONY: edit
edit: $(IMPORT_DIR) ## Open the project in the Godot editor
@@ -174,7 +174,7 @@ clean: ## Remove build artifacts
rm -rf $(CACHE_DIR)
rm -rf dist
rm -rf $(IMPORT_DIR)
- cd ./gdext && $(MAKE) clean
+ cd ./extensions/core && $(MAKE) clean
.PHONY: run run-force
run: build/opengamepad-ui.x86_64 run-force ## Run the project in gamescope
diff --git a/addons/.gitignore b/addons/.gitignore
index 99eef491..140f8cf8 100644
--- a/addons/.gitignore
+++ b/addons/.gitignore
@@ -1,5 +1 @@
-dbus/
-linuxthread/
-pty/
-unixsock/
-xlib/
+*.so
diff --git a/addons/core/assets/icons/inputplumber.svg b/addons/core/assets/icons/inputplumber.svg
new file mode 100644
index 00000000..4ea3ae1e
--- /dev/null
+++ b/addons/core/assets/icons/inputplumber.svg
@@ -0,0 +1,76 @@
+
+
diff --git a/addons/core/assets/icons/inputplumber.svg.import b/addons/core/assets/icons/inputplumber.svg.import
new file mode 100644
index 00000000..b1198507
--- /dev/null
+++ b/addons/core/assets/icons/inputplumber.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dbdyxgqmyeg1f"
+path="res://.godot/imported/inputplumber.svg-930e8ab4c0d3c4d458c32ef96742bdaf.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/core/assets/icons/inputplumber.svg"
+dest_files=["res://.godot/imported/inputplumber.svg-930e8ab4c0d3c4d458c32ef96742bdaf.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/core/assets/icons/library.svg b/addons/core/assets/icons/library.svg
new file mode 100644
index 00000000..a4c3cef4
--- /dev/null
+++ b/addons/core/assets/icons/library.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/addons/core/assets/icons/library.svg.import b/addons/core/assets/icons/library.svg.import
new file mode 100644
index 00000000..36d12ed4
--- /dev/null
+++ b/addons/core/assets/icons/library.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bn4jiyx7afl3b"
+path="res://.godot/imported/library.svg-8fcfe8437951fa126b5ab9f50dba6f13.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/core/assets/icons/library.svg"
+dest_files=["res://.godot/imported/library.svg-8fcfe8437951fa126b5ab9f50dba6f13.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/core/core.gdextension b/addons/core/core.gdextension
new file mode 100644
index 00000000..e4c355d7
--- /dev/null
+++ b/addons/core/core.gdextension
@@ -0,0 +1,12 @@
+[configuration]
+entry_symbol = "gdext_rust_init"
+compatibility_minimum = 4.3
+reloadable = false
+
+[libraries]
+linux.debug.x86_64 = "res://addons/core/bin/libopengamepadui-core.linux.template_debug.x86_64.so"
+linux.release.x86_64 = "res://addons/core/bin/libopengamepadui-core.linux.template_release.x86_64.so"
+
+[icons]
+LibraryItem = "res://addons/core/assets/icons/library.svg"
+LibraryLaunchItem = "res://addons/core/assets/icons/library.svg"
diff --git a/addons/gut/fonts/AnonymousPro-Bold.ttf.import b/addons/gut/fonts/AnonymousPro-Bold.ttf.import
index a3eb4791..de1351f6 100644
--- a/addons/gut/fonts/AnonymousPro-Bold.ttf.import
+++ b/addons/gut/fonts/AnonymousPro-Bold.ttf.import
@@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/AnonymousPro-Bold.ttf-9d8fef4d357af5b52cd60af
Rendering=null
antialiasing=1
generate_mipmaps=false
+disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
diff --git a/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import b/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import
index ef28dd80..bdde2072 100644
--- a/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import
+++ b/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import
@@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/AnonymousPro-BoldItalic.ttf-4274bf704d3d6b9cd
Rendering=null
antialiasing=1
generate_mipmaps=false
+disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
diff --git a/addons/gut/fonts/AnonymousPro-Italic.ttf.import b/addons/gut/fonts/AnonymousPro-Italic.ttf.import
index 1779af17..ce3e5b91 100644
--- a/addons/gut/fonts/AnonymousPro-Italic.ttf.import
+++ b/addons/gut/fonts/AnonymousPro-Italic.ttf.import
@@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/AnonymousPro-Italic.ttf-9989590b02137b799e13d
Rendering=null
antialiasing=1
generate_mipmaps=false
+disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
diff --git a/addons/gut/fonts/AnonymousPro-Regular.ttf.import b/addons/gut/fonts/AnonymousPro-Regular.ttf.import
index 1e2975b1..a567498c 100644
--- a/addons/gut/fonts/AnonymousPro-Regular.ttf.import
+++ b/addons/gut/fonts/AnonymousPro-Regular.ttf.import
@@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/AnonymousPro-Regular.ttf-856c843fd6f89964d2ca
Rendering=null
antialiasing=1
generate_mipmaps=false
+disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
diff --git a/addons/gut/fonts/CourierPrime-Bold.ttf.import b/addons/gut/fonts/CourierPrime-Bold.ttf.import
index 7d60fb0a..cb05171d 100644
--- a/addons/gut/fonts/CourierPrime-Bold.ttf.import
+++ b/addons/gut/fonts/CourierPrime-Bold.ttf.import
@@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/CourierPrime-Bold.ttf-1f003c66d63ebed70964e77
Rendering=null
antialiasing=1
generate_mipmaps=false
+disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
diff --git a/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import b/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import
index 4678c9eb..0a9a7b77 100644
--- a/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import
+++ b/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import
@@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/CourierPrime-BoldItalic.ttf-65ebcc61dd5e1dfa8
Rendering=null
antialiasing=1
generate_mipmaps=false
+disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
diff --git a/addons/gut/fonts/CourierPrime-Italic.ttf.import b/addons/gut/fonts/CourierPrime-Italic.ttf.import
index 522e2950..89412fc9 100644
--- a/addons/gut/fonts/CourierPrime-Italic.ttf.import
+++ b/addons/gut/fonts/CourierPrime-Italic.ttf.import
@@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/CourierPrime-Italic.ttf-baa9156a73770735a0f72
Rendering=null
antialiasing=1
generate_mipmaps=false
+disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
diff --git a/addons/gut/fonts/CourierPrime-Regular.ttf.import b/addons/gut/fonts/CourierPrime-Regular.ttf.import
index 38174660..9fde40b1 100644
--- a/addons/gut/fonts/CourierPrime-Regular.ttf.import
+++ b/addons/gut/fonts/CourierPrime-Regular.ttf.import
@@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/CourierPrime-Regular.ttf-3babe7e4a7a588dfc9a8
Rendering=null
antialiasing=1
generate_mipmaps=false
+disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
diff --git a/addons/gut/fonts/LobsterTwo-Bold.ttf.import b/addons/gut/fonts/LobsterTwo-Bold.ttf.import
index 7548ad04..673d1515 100644
--- a/addons/gut/fonts/LobsterTwo-Bold.ttf.import
+++ b/addons/gut/fonts/LobsterTwo-Bold.ttf.import
@@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/LobsterTwo-Bold.ttf-7c7f734103b58a32491a47881
Rendering=null
antialiasing=1
generate_mipmaps=false
+disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
diff --git a/addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import b/addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import
index 4b609e80..62048b0e 100644
--- a/addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import
+++ b/addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import
@@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/LobsterTwo-BoldItalic.ttf-227406a33e84448e6aa
Rendering=null
antialiasing=1
generate_mipmaps=false
+disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
diff --git a/addons/gut/fonts/LobsterTwo-Italic.ttf.import b/addons/gut/fonts/LobsterTwo-Italic.ttf.import
index 5899b797..d3ca2728 100644
--- a/addons/gut/fonts/LobsterTwo-Italic.ttf.import
+++ b/addons/gut/fonts/LobsterTwo-Italic.ttf.import
@@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/LobsterTwo-Italic.ttf-f93abf6c25390c85ad5fb6c
Rendering=null
antialiasing=1
generate_mipmaps=false
+disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
diff --git a/addons/gut/fonts/LobsterTwo-Regular.ttf.import b/addons/gut/fonts/LobsterTwo-Regular.ttf.import
index 45a12c8a..9cc75421 100644
--- a/addons/gut/fonts/LobsterTwo-Regular.ttf.import
+++ b/addons/gut/fonts/LobsterTwo-Regular.ttf.import
@@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/LobsterTwo-Regular.ttf-f3fcfa01cd671c8da433dd
Rendering=null
antialiasing=1
generate_mipmaps=false
+disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
diff --git a/assets/editor-icons/inputplumber.svg b/assets/editor-icons/inputplumber.svg
new file mode 100644
index 00000000..4e63ef66
--- /dev/null
+++ b/assets/editor-icons/inputplumber.svg
@@ -0,0 +1,50 @@
+
+
diff --git a/assets/editor-icons/inputplumber.svg.import b/assets/editor-icons/inputplumber.svg.import
new file mode 100644
index 00000000..4f82c89c
--- /dev/null
+++ b/assets/editor-icons/inputplumber.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dhwvdnbmrvqu4"
+path="res://.godot/imported/inputplumber.svg-f9a02e1ec85cdf7dc3601d7003f9de2a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/editor-icons/inputplumber.svg"
+dest_files=["res://.godot/imported/inputplumber.svg-f9a02e1ec85cdf7dc3601d7003f9de2a.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=0.063
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/assets/editor-icons/powerstation.svg b/assets/editor-icons/powerstation.svg
new file mode 100644
index 00000000..1a16dd9a
--- /dev/null
+++ b/assets/editor-icons/powerstation.svg
@@ -0,0 +1,45 @@
+
+
diff --git a/assets/editor-icons/powerstation.svg.import b/assets/editor-icons/powerstation.svg.import
new file mode 100644
index 00000000..98abeedb
--- /dev/null
+++ b/assets/editor-icons/powerstation.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://2vefe1d32i3a"
+path="res://.godot/imported/powerstation.svg-9e86562cff9f28db8f5fb4ce0386cdf7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/editor-icons/powerstation.svg"
+dest_files=["res://.godot/imported/powerstation.svg-9e86562cff9f28db8f5fb4ce0386cdf7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=0.063
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/assets/editor-icons/streamline--desktop-game-solid.svg b/assets/editor-icons/streamline--desktop-game-solid.svg
new file mode 100644
index 00000000..d8ebec7e
--- /dev/null
+++ b/assets/editor-icons/streamline--desktop-game-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/editor-icons/streamline--desktop-game-solid.svg.import b/assets/editor-icons/streamline--desktop-game-solid.svg.import
new file mode 100644
index 00000000..b575a272
--- /dev/null
+++ b/assets/editor-icons/streamline--desktop-game-solid.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cav2x6q8apups"
+path="res://.godot/imported/streamline--desktop-game-solid.svg-ff379b75f0c49d1fc9be24f364b571c9.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/editor-icons/streamline--desktop-game-solid.svg"
+dest_files=["res://.godot/imported/streamline--desktop-game-solid.svg-ff379b75f0c49d1fc9be24f364b571c9.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/core/global/dbus_manager.gd b/core/global/dbus_manager.gd
deleted file mode 100644
index 4786b3bd..00000000
--- a/core/global/dbus_manager.gd
+++ /dev/null
@@ -1,311 +0,0 @@
-extends Resource
-class_name DBusManager
-
-## DBusManager is a helper class for using DBus
-##
-## Use this class to interface with DBus.
-
-enum BUS_TYPE {
- SYSTEM = dbus.DBUS_BUS_SYSTEM,
- SESSION = dbus.DBUS_BUS_SESSION,
- STARTER = dbus.DBUS_BUS_STARTER,
-}
-
-const DBUS_BUS := "org.freedesktop.DBus"
-const DBUS_PATH := "/org/freedesktop/DBus"
-
-const IFACE_DBUS := "org.freedesktop.DBus"
-const IFACE_PROPERTIES := "org.freedesktop.DBus.Properties"
-const IFACE_OBJECT_MANAGER := "org.freedesktop.DBus.ObjectManager"
-
-## Type of bus to connect to
-@export var bus_type := BUS_TYPE.SYSTEM
-## Shared thread to process DBus messages on
-@export var thread: SharedThread = load("res://core/systems/threading/system_thread.tres")
-
-var logger := Log.get_logger("DBusManager")
-var well_known_names := []
-var dbus := DBus.new()
-var dbus_proxy := DBusProxy.new(create_proxy(DBUS_BUS, DBUS_PATH))
-
-
-func _init() -> void:
- if dbus.connect(bus_type) != OK:
- logger.warn("Unable to connect to dbus")
- return
- thread.add_process(_process)
- thread.start()
-
-
-## Process messages on the bus that are being watched and dispatch them.
-func _process(_delta: float):
- var messages: Array[DBusMessage] = []
- var msg := dbus.pop_message()
- while msg != null:
- logger.debug("Received DBus message on " + msg.get_sender() + " from " + msg.get_path() + ": " + str(msg.get_args()))
- messages.append(msg)
- msg = dbus.pop_message()
-
- for message in messages:
- _process_message(message)
-
-
-## Dispatch the given message to any proxy objects
-func _process_message(msg: DBusMessage) -> void:
- # Try looking up the well-known name of the message sender
- var known_names := get_names_for_owner(msg.get_sender())
-
- # Try constructing the resource path to the proxy and see if it exists
- for known_name in known_names:
- var res_path := "dbus://" + known_name + msg.get_path()
- if not ResourceLoader.exists(res_path):
- logger.debug("No proxy resource found to send message to at: " + res_path)
- continue
- logger.debug("Found proxy to send message signal to at: " + res_path)
- var proxy := load(res_path) as Proxy
- if not proxy:
- logger.warn("Failed to load proxy from resource cache: " + res_path)
- continue
- var send_signal := func(message: DBusMessage) -> void:
- proxy.message_received.emit(message)
- send_signal.call_deferred(msg)
- break
-
-
-## Creates a reference to a DBus object on the given bus at the given path.
-## E.g. create_proxy("org.bluez", "/org/bluez/hci0")
-func create_proxy(bus: String, path: String) -> Proxy:
- # Try to load the proxy if it already exists
- var res_path := "dbus://" + bus + path
- logger.debug("Creating proxy with resource path: " + res_path)
- var proxy: Proxy
- if ResourceLoader.exists(res_path):
- logger.debug("Resource already exists. Returning existing instance.")
- proxy = load(res_path)
- return proxy
-
- proxy = Proxy.new(dbus, bus, path)
- proxy.take_over_path(res_path)
-
- # Keep track of bus names so they can be referenced later
- if not bus in well_known_names:
- well_known_names.append(bus)
-
- return proxy
-
-
-## Returns true if the given well-known name has an owner.
-func bus_exists(name: String) -> bool:
- return dbus.name_has_owner(name)
-
-# TODO: This is deprecated. Remove this and all refrences to it.
-## Returns a dictionary of manages objects for the given bus and path
-func get_managed_objects(bus: String, path: String) -> Array[ManagedObject]:
- var obj := create_proxy(bus, path)
- var result := obj.call_method(IFACE_OBJECT_MANAGER, "GetManagedObjects", [], "")
- if not result:
- return []
- var args := result.get_args()
- if args.size() != 1:
- return []
- if not args[0] is Dictionary:
- return []
-
- var objs_dict := args[0] as Dictionary
- var objects: Array[ManagedObject] = []
-
- # Convert the objects dictionary into an array of objects
- for obj_path in objs_dict.keys():
- var obj_data := objs_dict[obj_path] as Dictionary
- var object := ManagedObject.new(obj_path as String, obj_data)
- objects.append(object)
-
- return objects
-
-
-## Tries to resolve well-known names (e.g. "org.bluez") from the given owner (e.g. ":1.5").
-## This will return an array of well-known names.
-func get_names_for_owner(owner: String) -> PackedStringArray:
- var names := PackedStringArray()
- for name in well_known_names:
- var name_owner := dbus_proxy.get_name_owner(name as String)
- if name_owner == owner:
- names.append(name as String)
-
- return names
-
-
-func _to_string() -> String:
- var bus_string := "System" if bus_type == BUS_TYPE.SYSTEM else "Session"
- return "".format([bus_string])
-
-
-class ObjectManager extends Resource:
- signal interfaces_added(dbus_path: String)
- signal interfaces_removed(dbus_path: String)
-
- var _proxy: Proxy
-
- func _init(proxy: Proxy) -> void:
- _proxy = proxy
- _proxy.message_received.connect(_on_message_received)
- _proxy.thread.exec(_proxy.watch.bind(IFACE_OBJECT_MANAGER, "InterfacesAdded"))
- _proxy.thread.exec(_proxy.watch.bind(IFACE_OBJECT_MANAGER, "InterfacesRemoved"))
-
- ## Returns a dictionary of manages objects for the given bus and path
- func get_managed_objects(_bus: String, _path: String) -> Array[ManagedObject]:
- var result := _proxy.call_method(IFACE_OBJECT_MANAGER, "GetManagedObjects", [], "")
- if not result:
- return []
- var args := result.get_args()
- if args.size() != 1:
- return []
- if not args[0] is Dictionary:
- return []
-
- var objs_dict := args[0] as Dictionary
- var objects: Array[ManagedObject] = []
-
- # Convert the objects dictionary into an array of objects
- for obj_path in objs_dict.keys():
- var obj_data := objs_dict[obj_path] as Dictionary
- var object := ManagedObject.new(obj_path as String, obj_data)
- objects.append(object)
-
- return objects
-
- func _on_message_received(msg: DBusMessage) -> void:
- #print("Got message: " + str(msg))
- if not msg:
- return
- var args := msg.get_args()
- #print("Got args: " + str(args))
- if args.size() != 2:
- return
- #print("Got args big enough")
- if msg.get_member() == "InterfacesAdded":
- #print("Got InterfacesAdded")
- interfaces_added.emit(args[0])
-
- if msg.get_member() == "InterfacesRemoved":
- #print("Got InterfacesRemoved")
- interfaces_removed.emit(args[0])
-
-
-## A Proxy provides an interface to call methods on a DBus object.
-class Proxy extends Resource:
- signal message_received(msg: DBusMessage)
- signal properties_changed(iface: String, props: Dictionary)
- var _dbus: DBus
- var bus_name: String
- var path: String
- var rules := PackedStringArray()
- var logger := Log.get_logger("DBusProxy")
- var thread: SharedThread = load("res://core/systems/threading/system_thread.tres")
-
- func _init(conn: DBus, bus: String, obj_path: String) -> void:
- _dbus = conn
- bus_name = bus
- path = obj_path
- message_received.connect(_on_property_changed)
- thread.exec(watch.bind(IFACE_PROPERTIES, "PropertiesChanged"))
-
- func _on_property_changed(msg: DBusMessage) -> void:
- if not msg:
- return
- if msg.get_member() != "PropertiesChanged":
- return
- var args := msg.get_args()
- if args.size() < 2:
- return
- properties_changed.emit(args[0], args[1])
-
- func _notification(what: int) -> void:
- if what != NOTIFICATION_PREDELETE:
- return
- for rule in rules:
- logger.debug("Removing watch rule: " + rule)
- _dbus.remove_match(rule)
-
- ## Call the given method
- func call_method(iface: String, method: String, args: Array = [], signature: String = "") -> DBusMessage:
- logger.debug("Calling method: " + iface + "::" + method + "(" + str(args) + ")")
- return _dbus.send_with_reply_and_block(bus_name, path, iface, method, args, signature)
-
- ## Set the given property
- func set_property(iface: String, property: String, value: Variant) -> void:
- logger.debug("Set property " + property + " to " + str(value) + " for interface " + iface)
- call_method(IFACE_PROPERTIES, "Set", [iface, property, value], "ssv")
-
- ## Get the given property
- func get_property(iface: String, property: String) -> Variant:
- var response := call_method(IFACE_PROPERTIES, "Get", [iface, property], "ss")
- if not response:
- return null
- var args := response.get_args()
- if args.size() == 0:
- return null
-
- return args[0]
-
- ## Get all properties for the given interface
- func get_properties(iface: String) -> Dictionary:
- var response := call_method(IFACE_PROPERTIES, "GetAll", [iface], "s")
- if not response:
- return {}
- var args := response.get_args()
- if args.size() == 0:
- return {}
-
- return args[0]
-
- ## Watch the bus for particular signals
- func watch(iface: String, member: String = "PropertiesChanged") -> int:
- var rule := "type='signal',interface='{0}',path='{1}',member='{2}'".format(
- [iface, path, member]
- )
- rules.append(rule)
- logger.debug("Adding watch rule: " + rule)
- var err := _dbus.add_match(rule)
- if err != OK:
- logger.error("Unable to watch " + path)
- return err
-
-
-## A ManagedObject is a simple structure used with GetManagedObjects
-class ManagedObject:
- var path: String
- var data: Dictionary
-
- func _init(obj_path: String, obj_data: Dictionary) -> void:
- path = obj_path
- data = obj_data
-
- func has_interface(name: String) -> bool:
- return name in data
-
- func has_interface_attr(iface: String, name: String) -> bool:
- if not iface in data:
- return false
- if not name in data[iface]:
- return false
- return true
-
-
-## Proxy to manage /org/freedesktop/DBus on the org.freedesktop.DBus bus.
-class DBusProxy:
- var _proxy: Proxy
-
- func _init(proxy: Proxy) -> void:
- _proxy = proxy
-
- ## Return the connection name (e.g. ":1.1270") from the given well-known name
- func get_name_owner(name: String) -> String:
- var msg := _proxy.call_method(IFACE_DBUS, "GetNameOwner", [name], "s")
- if not msg:
- return ""
- var args := msg.get_args()
- if args.size() != 1:
- return ""
-
- return args[0]
diff --git a/core/global/dbus_system.tres b/core/global/dbus_system.tres
deleted file mode 100644
index 95c70f0d..00000000
--- a/core/global/dbus_system.tres
+++ /dev/null
@@ -1,9 +0,0 @@
-[gd_resource type="Resource" script_class="DBusManager" load_steps=3 format=3 uid="uid://dskqww1130rb2"]
-
-[ext_resource type="Script" path="res://core/global/dbus_manager.gd" id="1_6xf1o"]
-[ext_resource type="Resource" uid="uid://ct7pi0cx0r832" path="res://core/systems/threading/system_thread.tres" id="2_p3brt"]
-
-[resource]
-script = ExtResource("1_6xf1o")
-bus_type = 1
-thread = ExtResource("2_p3brt")
diff --git a/core/global/gamescope.gd b/core/global/gamescope.gd
deleted file mode 100644
index d8ae55f7..00000000
--- a/core/global/gamescope.gd
+++ /dev/null
@@ -1,751 +0,0 @@
-extends Resource
-class_name Gamescope
-
-## Interact with Gamescope windows and properties
-##
-## The Gamescope class is responsible for interacting with Gamescope, usually
-## via the means of setting gamescope-specific window properties. It can be
-## used to discover Gamescope displays, list windows and their children, and set
-## gamescope-specific window atoms to switch windows, set blur, limit FPS, etc.
-## [br][br]
-## For example, to limit the FPS, you can do the following:
-## [codeblock]
-## Gamescope.set_fps_limit(display, 30)
-## [/codeblock]
-## [br][br]
-## Most of the core functionality of this class is provided by the [Xlib]
-## module, which is a GDExtension that exposes Xlib methods to Godot.
-
-signal blur_mode_updated(from: int, to: int)
-signal display_is_external_updated(from: int, to: int)
-signal focused_window_updated(from: int, to: int)
-signal focusable_windows_updated(from: PackedInt32Array, to: PackedInt32Array)
-signal focused_app_updated(from: int, to: int)
-signal focused_app_gfx_updated(from: int, to: int)
-signal focusable_apps_updated(from: PackedInt32Array, to: PackedInt32Array)
-
-## Gamescope Blur modes
-enum BLUR_MODE {
- OFF = 0, ## Turns off blur of running games
- COND = 1, ## Conditionally blurs running games
- ALWAYS = 2, ## Turns blurring of running games on
-}
-
-## Specifies which Gamescope xwayland server to perform an operation on.
-enum XWAYLAND {
- PRIMARY, ## Primary Gamescope xwayland instance
- OGUI, ## Xwayland instance that OpenGamepadUI is running on
- GAME, ## Xwayland instance where games run
-}
-
-## Gamescope is hard-coded to look for STEAM_GAME=769 to determine if it is the
-## overlay app.
-const OVERLAY_GAME_ID := 769
-
-@export var log_level := Log.LEVEL.INFO
-## The primary xwayland is the primary Gamescope xwayland session that contains
-## Gamescope properties on the root window.
-var xwayland_primary: Xlib
-## The OGUI xwayland is the xwayland instance that OGUI is running under.
-var xwayland_ogui: Xlib
-## The Game xwayland is the xwayland instance that games are launched under.
-var xwayland_game: Xlib
-## Array of all discovered xwayland instances
-var xwaylands: Array[Xlib] = []
-var logger := Log.get_logger("Gamescope", log_level)
-
-# Gamescope properties
-## Blur mode (read-only)
-var blur_mode: int:
- set(v):
- var prev_value := blur_mode
- blur_mode = v
- if prev_value != v:
- blur_mode_updated.emit(prev_value, v)
-var baselayer_window: int
-var input_counter: int
-var display_is_external: int
-var vrr_enabled: int
-var vrr_feedback: int
-var vrr_capable: int
-var keyboard_focus_display: PackedInt32Array
-var mouse_focus_display: PackedInt32Array
-var focus_display: PackedInt32Array
-var focused_window: int:
- set(v):
- var prev_value := focused_window
- focused_window = v
- if prev_value != v:
- focused_window_updated.emit(prev_value, v)
-var focused_app_gfx: int:
- set(v):
- var prev_value := focused_app_gfx
- focused_app_gfx = v
- if prev_value != v:
- focused_app_gfx_updated.emit(prev_value, v)
-var focused_app: int:
- set(v):
- var prev_value := focused_app
- focused_app = v
- if prev_value != v:
- focused_app_updated.emit(prev_value, v)
-var focusable_windows: PackedInt32Array:
- set(v):
- var prev_value := focusable_windows
- focusable_windows = v
- if prev_value != v:
- focusable_windows_updated.emit(prev_value, v)
-var focusable_apps: PackedInt32Array:
- set(v):
- var prev_value := focusable_apps
- focusable_apps = v
- if prev_value != v:
- focusable_apps_updated.emit(prev_value, v)
-var cursor_visible_feedback: int
-
-
-# Connects to all gamescope xwayland instances
-func _init() -> void:
- # Don't initialize if run from the editor (during doc generation)
- if Engine.is_editor_hint():
- logger.info("Not initializing. Ran from editor.")
- return
-
- # Connect to the xwayland instance that OGUI is running on
- var ogui_display := OS.get_environment("DISPLAY")
- xwayland_ogui = Xlib.new()
- if xwayland_ogui.open(ogui_display) != OK:
- logger.error("Failed to open OGUI X server: " + ogui_display)
- return
- if _is_gamescope_xwayland(xwayland_ogui):
- logger.debug("OpenGamepadUI is running in Gamescope")
- xwayland_game = xwayland_ogui
- if _is_gamescope_xwayland_primary(xwayland_ogui):
- logger.debug("OpenGamepadUI is running on the primary Gamescope xwayland")
- xwayland_primary = xwayland_ogui
- xwaylands.push_front(xwayland_ogui)
-
- # Discover all other xwayland displays
- var displays := discover_gamescope_displays()
- for display in displays:
- if _has_xwayland(display):
- logger.debug("Already discovered xwayland: " + display)
- continue
- var xwayland := Xlib.new()
- if xwayland.open(display) != OK:
- logger.debug("Failed to open X server: " + display)
- continue
- if _is_gamescope_xwayland_primary(xwayland):
- logger.debug("Display " + display + " is the primary gamescope instance")
- xwayland_primary = xwayland
- if xwayland_primary != xwayland:
- xwayland_game = xwayland
- xwaylands.append(xwayland)
-
- # If we haven't discovered any gamescope displays, set everything to use
- # the OGUI xwayland instance
- if not xwayland_primary:
- logger.warn("OpenGamepadUI is not running in Gamescope. Unexpected behavior expected.")
- xwayland_primary = xwayland_ogui
- if not xwayland_game:
- xwayland_game = xwayland_ogui
-
- logger.debug("Primary xwayland is " + xwayland_primary.get_name())
- logger.debug("OGUI xwayland is " + xwayland_ogui.get_name())
- logger.debug("Game xwayland is " + xwayland_game.get_name())
-
-
-## Returns all gamescope xwayland names (E.g. [":0", ":1"])
-# TODO: This seems brittle. Is there any other way we can discover Gamescope displays?
-func discover_gamescope_displays() -> PackedStringArray:
- logger.debug("Discovering xwaylands!")
-
- # X11 displays have a corresponding socket in /tmp/.X11-unix
- # The sockets are named like: X0, X1, X2, etc.
- var dir := DirAccess.open("/tmp/.X11-unix")
- var sockets := dir.get_files()
-
- # Loop through each socket file and derrive the display number.
- var display_names: PackedInt32Array = []
- for socket in sockets:
- var suffix := (socket as String).trim_prefix("X")
- if not suffix.is_valid_int():
- logger.warn("Skipping X11 socket with a weird name: " + socket)
- continue
- display_names.append(suffix.to_int())
-
- # Check to see if the root window of these displays has gamescope-specific properties
- var gamescope_displays := PackedStringArray()
- for display_num in display_names:
- var display := ":{0}".format([display_num])
- var xwayland := Xlib.new()
- if xwayland.open(display) != OK:
- logger.debug("Failed to open X server: " + display)
- continue
- if _is_gamescope_xwayland(xwayland):
- gamescope_displays.append(display)
- logger.debug("Discovered X server display: " + display)
- xwayland.close()
- return gamescope_displays
-
-
-## Updates the Gamescope state. Should be called in a loop to keep the Gamescope
-## state up-to-date.
-func update() -> void:
- blur_mode = get_blur_mode()
- focused_window = get_focused_window()
- focused_app = get_focused_app()
- focused_app_gfx = get_focused_app_gfx()
- focusable_windows = get_focusable_windows()
- focusable_apps = get_focusable_apps()
- baselayer_window = get_baselayer_window()
- if not baselayer_window in get_focusable_windows():
- baselayer_window = -1
- remove_baselayer_window()
-
-
-## Returns the name of the given xwayland display (e.g. ":1")
-func get_display_name(display: XWAYLAND) -> String:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return ""
- return xwayland.get_name()
-
-
-## Returns true if the given X property exists on the given window.
-func has_xprop(window_id: int, key: String, display: XWAYLAND) -> bool:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return false
- return xwayland.has_xprop(window_id, key)
-
-
-## Returns a list of X properties on the given window
-func list_xprops(window_id: int, display: XWAYLAND) -> PackedStringArray:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return []
- return xwayland.list_xprops(window_id)
-
-
-## Returns the name of the given window.
-func get_window_name(window_id: int, display: XWAYLAND = XWAYLAND.GAME) -> String:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return ""
- return xwayland.get_window_name(window_id)
-
-
-## Returns the PID of the given window. Returns -1 if no PID was found.
-func get_window_pid(window_id: int, display: XWAYLAND) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- return xwayland.get_window_pid(window_id)
-
-
-## Returns the xwayland window ID for the given process. Returns -1 if no
-## window was found.
-func get_window_id(pid: int, display: XWAYLAND) -> int:
- var display_name := get_display_name(display)
- logger.trace("Getting Window ID for {0} on display {1}".format([pid, display_name]))
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
- var all_windows := get_all_windows(root_id, display)
- for window_id in all_windows:
- var window_pid := xwayland.get_window_pid(window_id)
- if pid == window_pid:
- return window_id
- window_pid = xwayland.get_xprop(window_id, "_NET_WM_PID")
- if pid == window_pid:
- return window_id
-
- return -1
-
-
-## Returns the xwayland window ID(s) for the given process using multiple methods
-## to try and discover.
-func get_window_ids(pid: int, display: XWAYLAND) -> PackedInt32Array:
- var window_ids := PackedInt32Array()
- var display_name := get_display_name(display)
- logger.trace("Getting Window ID for {0} on display {1}".format([pid, display_name]))
- var xwayland := get_xwayland(display)
- if not xwayland:
- return window_ids
- var root_id := xwayland.get_root_window_id()
- var all_windows := get_all_windows(root_id, display)
- for window_id in all_windows:
- var window_pid := xwayland.get_window_pid(window_id)
- if pid == window_pid and not window_id in window_ids:
- window_ids.append(window_id)
- var net_window_pid := xwayland.get_xprop(window_id, "_NET_WM_PID")
- if pid == net_window_pid and not net_window_pid in window_ids:
- window_ids.append(net_window_pid)
-
- return window_ids
-
-
-## Returns the child window ids of the given window
-func get_window_children(window_id: int, display: XWAYLAND) -> PackedInt32Array:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return PackedInt32Array()
- var children := xwayland.get_window_children(window_id)
- if len(children) == 0:
- return PackedInt32Array()
- return children
-
-
-## Recursively returns all child windows of the given window id
-func get_all_windows(window_id: int, display: XWAYLAND) -> PackedInt32Array:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return PackedInt32Array()
- var children := xwayland.get_window_children(window_id)
- if len(children) == 0:
- return PackedInt32Array([])
-
- var leaves := PackedInt32Array()
- for child in children:
- leaves.append(child)
- leaves.append_array(get_all_windows(child, display))
-
- return leaves
-
-
-## Returns true if the window with the given window ID exists
-func is_focusable_app(window_id: int, display: XWAYLAND = XWAYLAND.PRIMARY) -> bool:
- var focusable := get_focusable_apps(display)
- if window_id in focusable:
- return true
- return false
-
-
-## Returns the root window ID of the given display
-func get_root_window_id(display: XWAYLAND) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- return xwayland.get_root_window_id()
-
-
-## Returns a list of focusable app window ids
-func get_focusable_apps(display: XWAYLAND = XWAYLAND.PRIMARY) -> PackedInt32Array:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return PackedInt32Array()
- var root_id := xwayland.get_root_window_id()
- return _get_xprop_array(xwayland, root_id, "GAMESCOPE_FOCUSABLE_APPS")
-
-
-## Returns a list of focusable window ids
-func get_focusable_windows(display: XWAYLAND = XWAYLAND.PRIMARY) -> PackedInt32Array:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return PackedInt32Array()
- var root_id := xwayland.get_root_window_id()
- return _get_xprop_array(xwayland, root_id, "GAMESCOPE_FOCUSABLE_WINDOWS")
-
-
-## Returns a list of focusable window names
-func get_focusable_window_names(display: XWAYLAND = XWAYLAND.PRIMARY) -> PackedStringArray:
- var focusable := get_focusable_windows(display)
- var results := PackedStringArray()
- for window_id in focusable:
- var name := get_window_name(window_id, display)
- results.append(name)
- return results
-
-
-## Return the currently focused window id.
-func get_focused_window(display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
- var results := _get_xprop_array(xwayland, root_id, "GAMESCOPE_FOCUSED_WINDOW")
- if results.size() == 0:
- return 0
- return results[0]
-
-
-## Return the currently focused app id.
-func get_focused_app(display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
- var results := _get_xprop_array(xwayland, root_id, "GAMESCOPE_FOCUSED_APP")
- if results.size() == 0:
- return 0
- return results[0]
-
-
-## Return the currently focused gfx app id.
-func get_focused_app_gfx(display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
- var results := _get_xprop_array(xwayland, root_id, "GAMESCOPE_FOCUSED_APP_GFX")
- if results.size() == 0:
- return 0
- return results[0]
-
-
-## Sets the given window as the main launcher app.
-## Gamescope is hard-coded to look for appId 769
-func set_main_app(window_id: int, display: XWAYLAND = XWAYLAND.OGUI) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- return _set_xprop(xwayland, window_id, "STEAM_GAME", OVERLAY_GAME_ID)
-
-
-## Set the given window as the primary overlay input focus. This should be set to
-## "1" whenever the overlay wants to intercept input from a game.
-func set_input_focus(window_id: int, value: int, display: XWAYLAND = XWAYLAND.OGUI) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- return _set_xprop(xwayland, window_id, "STEAM_INPUT_FOCUS", value)
-
-
-## Returns whether or not the overlay window is currently focused
-func is_overlay_focused(display: XWAYLAND = XWAYLAND.OGUI) -> bool:
- return get_focused_app(display) == OVERLAY_GAME_ID
-
-
-## Get the overlay status for the given window
-func get_overlay(window_id: int, display: XWAYLAND = XWAYLAND.OGUI) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- return _get_xprop(xwayland, window_id, "STEAM_OVERLAY")
-
-
-## Set the given window as an overlay
-func set_overlay(window_id: int, value: int, display: XWAYLAND = XWAYLAND.OGUI) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- return _set_xprop(xwayland, window_id, "STEAM_OVERLAY", value)
-
-
-## Set the given window as a notification. This should be set to "1" when some
-## UI wants to be shown but not intercept input.
-func set_notification(window_id: int, value: int, display: XWAYLAND = XWAYLAND.OGUI) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- return _set_xprop(xwayland, window_id, "STEAM_NOTIFICATION", value)
-
-
-## Set the given window as an external overlay
-func set_external_overlay(window_id: int, value: int, display: XWAYLAND = XWAYLAND.OGUI) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- return _set_xprop(xwayland, window_id, "GAMESCOPE_EXTERNAL_OVERLAY", value)
-
-
-## Returns the currently set app ID on the given window
-func get_app_id(window_id: int, display: XWAYLAND = XWAYLAND.GAME) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- return _get_xprop(xwayland, window_id, "STEAM_GAME")
-
-
-## Sets the app ID on the given window
-func set_app_id(window_id: int, app_id: int, display: XWAYLAND = XWAYLAND.GAME) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- return _set_xprop(xwayland, window_id, "STEAM_GAME", app_id)
-
-
-## Returns whether or not the given window has an app ID set
-func has_app_id(window_id: int, display: XWAYLAND = XWAYLAND.GAME) -> bool:
- return has_xprop(window_id, "STEAM_GAME", display)
-
-
-## Sets the Gamescope FPS limit
-func set_fps_limit(fps: int = 60, display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- logger.debug("Setting FPS to: {0}".format([fps]))
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
- return _set_xprop(xwayland, root_id, "GAMESCOPE_FPS_LIMIT", fps)
-
-
-## Returns the Gamescope FPS limit
-func get_fps_limit(display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
- return _get_xprop(xwayland, root_id, "GAMESCOPE_FPS_LIMIT")
-
-
-## Returns the current Gamescope blur mode
-func get_blur_mode(display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
- return _get_xprop(xwayland, root_id, "GAMESCOPE_BLUR_MODE")
-
-
-## Sets the Gamescope blur mode
-func set_blur_mode(mode: BLUR_MODE = BLUR_MODE.OFF, display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- logger.debug("Setting blur mode to: {0}".format([mode]))
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
- var err := _set_xprop(xwayland, root_id, "GAMESCOPE_BLUR_MODE", mode)
- blur_mode = mode
- return err
-
-
-## Sets the Gamescope blur radius when blur is active
-func set_blur_radius(radius: int, display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- logger.debug("Setting blur radius to: {0}".format([radius]))
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
- return _set_xprop(xwayland, root_id, "GAMESCOPE_BLUR_RADIUS", radius)
-
-
-## Configures Gamescope to allow tearing or not
-func set_allow_tearing(allow: bool, display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- logger.debug("Setting tearing to: {0}".format([allow]))
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var value := 1 if allow else 0
- var root_id := xwayland.get_root_window_id()
- return _set_xprop(xwayland, root_id, "GAMESCOPE_ALLOW_TEARING", value)
-
-
-## Returns the currently set manual focus
-func get_baselayer_window(display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
- return _get_xprop(xwayland, root_id, "GAMESCOPECTRL_BASELAYER_WINDOW")
-
-
-## Focuses the given window
-func set_baselayer_window(window_id: int, display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
- return _set_xprop(xwayland, root_id, "GAMESCOPECTRL_BASELAYER_WINDOW", window_id)
-
-
-## Removes the baselayer property to un-focus windows
-func remove_baselayer_window(display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
- return _remove_xprop(xwayland, root_id, "GAMESCOPECTRL_BASELAYER_WINDOW")
-
-
-## Request a screenshot from gamescope
-func request_screenshot(display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
- return _set_xprop(xwayland, root_id, "GAMESCOPECTRL_REQUEST_SCREENSHOT", 1)
-
-
-## Sets the xwayland mode resolution on the given xwayland display
-## number (default: XWAYLAND.GAME).
-func set_resolution(resolution: Vector2i, allow_super: bool = false, display: XWAYLAND = XWAYLAND.GAME) -> int:
- var xwayland := get_xwayland(XWAYLAND.PRIMARY)
- if not xwayland:
- return -1
-
- var target_display := get_display_number(display)
- var root_id := xwayland.get_root_window_id()
- var allow_super_value := 1 if allow_super else 0
- var args := PackedInt32Array([target_display, resolution.x, resolution.y, allow_super_value])
- return _set_xprop_array(xwayland, root_id, "GAMESCOPE_XWAYLAND_MODE_CONTROL", args)
-
-
-## Returns the currently set gamescope saturation
-# Based on vibrantDeck by Scrumplex
-func get_saturation(display: XWAYLAND = XWAYLAND.PRIMARY) -> float:
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
-
- var matrix := _get_xprop_array(xwayland, root_id, "GAMESCOPE_COLOR_MATRIX")
- if matrix.size() < 2:
- return -1
-
- # [1065353216, 0, 0, 0, 1065353216, 0, 0, 0, 1065353216]
- var matrix_arr: Array[int] = Array(matrix)
- # Convert the array of longs to floats
- # [1.0, 0, 0, 0, 1.0, 0, 0, 0, 1.0]
- var coeffs := matrix_arr.map(_long_to_float)
- var saturation := snappedf(coeffs[0] - coeffs[1], 0.01)
-
- return saturation
-
-
-## Set the gamescope saturation
-# Based on vibrantDeck by Scrumplex
-func set_saturation(saturation: float, display: XWAYLAND = XWAYLAND.PRIMARY) -> int:
- saturation = maxf(saturation, 0.0)
- saturation = minf(saturation, 4.0)
- logger.debug("Setting saturation to: " + str(saturation))
-
- # Generate color transformation matrix
- var coeffs := _saturation_to_coeffs(saturation)
-
- # Represent floats as integars (long)
- var long_coeffs := coeffs.map(_float_to_long)
-
- # Get the xwayland to set the property on
- var xwayland := get_xwayland(display)
- if not xwayland:
- return -1
- var root_id := xwayland.get_root_window_id()
-
- logger.debug("Setting color matrix coeffs: " + str(long_coeffs))
- return _set_xprop_array(xwayland, root_id, "GAMESCOPE_COLOR_MATRIX", long_coeffs)
-
-
-func _saturation_to_coeffs(saturation: float) -> Array[float]:
- var coeff := (1.0 - saturation) / 3.0
-
- var coeffs: Array[float] = []
- coeffs.resize(9)
- coeffs.fill(coeff)
- coeffs[0] += saturation
- coeffs[4] += saturation
- coeffs[8] += saturation
-
- return coeffs
-
-
-func _float_to_long(x: float) -> int:
- var bytes := PackedByteArray()
- bytes.resize(4)
- bytes.encode_float(0, x)
- return bytes.decode_u32(0)
-
-
-func _long_to_float(x: int) -> float:
- var bytes := PackedByteArray()
- bytes.resize(4)
- bytes.encode_u32(0, x)
- return bytes.decode_float(0)
-
-
-## Returns the display type for the given display name
-func get_display_type(name: String) -> XWAYLAND:
- if xwayland_primary.get_name() == name:
- return XWAYLAND.PRIMARY
- if xwayland_ogui.get_name() == name:
- return XWAYLAND.OGUI
- return XWAYLAND.GAME
-
-
-## Returns the name of the given xwayland display
-func get_display_number(display: XWAYLAND) -> int:
- var name := get_display_name(display)
- var clean_name := name.replace(":", "")
- if clean_name.is_valid_int():
- return clean_name.to_int()
- logger.error("Unable to determine display number from name: " + name)
- return 0
-
-
-## Returns the xwayland instance for the given display type
-func get_xwayland(display: XWAYLAND) -> Xlib:
- if xwaylands.size() == 0:
- return null
- if display == XWAYLAND.PRIMARY:
- return xwayland_primary
- if display == XWAYLAND.OGUI:
- return xwayland_ogui
- return xwayland_game
-
-
-## Returns true if Gamescope is tracking the given display
-func _has_xwayland(display: String) -> bool:
- for xwayland in xwaylands:
- if xwayland.get_name() == display:
- return true
- return false
-
-
-## Returns true if the given xwayland instance is the primary gamescope instance
-func _is_gamescope_xwayland_primary(xwayland: Xlib) -> bool:
- var root_id := xwayland.get_root_window_id()
- if xwayland.has_xprop(root_id, "GAMESCOPE_FOCUSED_WINDOW"):
- return true
- return false
-
-
-## Returns true if the given xwayland instance is a gamescope instance
-func _is_gamescope_xwayland(xwayland: Xlib) -> bool:
- var root_id := xwayland.get_root_window_id()
- if xwayland.has_xprop(root_id, "GAMESCOPE_CURSOR_VISIBLE_FEEDBACK"):
- return true
- return false
-
-
-## Sets the given X property on the given window.
-## Example:
-## [codeblock]
-## Gamescope._set_xprop(":0", 1234, "STEAM_INPUT", 1)
-## [/codeblock]
-func _set_xprop(xwayland: Xlib, window_id: int, key: String, value: int) -> int:
- var msg_args := [window_id, key, value, xwayland.get_name()]
- logger.debug("Setting window {0} key {1} to {2} on display {3}".format(msg_args))
- return xwayland.set_xprop(window_id, key, value)
-
-
-## Sets the given X property with the given array of values
-func _set_xprop_array(xwayland: Xlib, window_id: int, key: String, values: PackedInt32Array) -> int:
- var msg_args := [window_id, key, str(values), xwayland.get_name()]
- logger.debug("Setting window {0} key {1} to {2} on display {3}".format(msg_args))
- # TODO: Fix set_xprop_array and use that instead of xprop
- var values_str_arr := []
- for value in values:
- values_str_arr.append(str(value))
- var values_str := ",".join(values_str_arr)
- var cmd := "env"
- var args := ["DISPLAY="+xwayland.get_name(), "xprop", "-id", str(window_id), "-f", key, "32c", "-set", key, values_str]
- return OS.execute(cmd, args)
-
-
-## Returns the value of the given X property for the given window. Returns
-## [member Xlib.ERR_XPROP_NOT_FOUND] if property doesn't exist.
-func _get_xprop(xwayland: Xlib, window_id: int, key: String) -> int:
- return xwayland.get_xprop(window_id, key)
-
-
-## Removes the given X property for the given window.
-func _remove_xprop(xwayland: Xlib, window_id: int, key: String) -> int:
- return xwayland.remove_xprop(window_id, key)
-
-
-## Returns an array of values for the given X property for the given window.
-## Returns an empty array if property was not found.
-func _get_xprop_array(xwayland: Xlib, window_id: int, key: String) -> PackedInt32Array:
- return xwayland.get_xprop_array(window_id, key)
diff --git a/core/global/gamescope.tres b/core/global/gamescope.tres
deleted file mode 100644
index f394fbb1..00000000
--- a/core/global/gamescope.tres
+++ /dev/null
@@ -1,7 +0,0 @@
-[gd_resource type="Resource" script_class="Gamescope" load_steps=2 format=3 uid="uid://blr55dcwc05m1"]
-
-[ext_resource type="Script" path="res://core/global/gamescope.gd" id="1_ipl5o"]
-
-[resource]
-script = ExtResource("1_ipl5o")
-log_level = 4
diff --git a/core/global/gamescope_test.gd b/core/global/gamescope_test.gd
deleted file mode 100644
index e64b41dd..00000000
--- a/core/global/gamescope_test.gd
+++ /dev/null
@@ -1,15 +0,0 @@
-extends GutTest
-
-var gamescope := Gamescope.new()
-
-
-func test_float_to_long() -> void:
- var to_int := gamescope._float_to_long(1.3)
- gut.p(to_int)
- assert_eq(to_int, 1067869798, "should be converted to a long")
-
-
-func test_long_to_float() -> void:
- var to_float := gamescope._long_to_float(1067869798)
- gut.p(to_float)
- assert_almost_eq(to_float, 1.3, 0.01, "should be approximately 1.3")
diff --git a/core/global/launch_manager.gd b/core/global/launch_manager.gd
index 7d65adff..75f64370 100644
--- a/core/global/launch_manager.gd
+++ b/core/global/launch_manager.gd
@@ -12,21 +12,21 @@ class_name LaunchManager
## periodically check on launched games to see if they have exited, or are
## opening new windows that might need attention. Example:
## [codeblock]
-## const LaunchManager := preload("res://core/global/launch_manager.tres")
+## var launch_manager := load("res://core/global/launch_manager.tres") as LaunchManager
## ...
## # Create a LibraryLaunchItem to run something
## var item := LibraryLaunchItem.new()
## item.command = "vkcube"
##
## # Launch the app with LaunchManager
-## var running_app := LaunchManager.launch(item)
+## var running_app := launch_manager.launch(item)
##
## # Get a list of running apps
-## var running := LaunchManager.get_running()
+## var running := launch_manager.get_running()
## print(running)
##
## # Stop an app with LaunchManager
-## LaunchManager.stop(running_app)
+## launch_manager.stop(running_app)
## [/codeblock]
signal app_launched(app: RunningApp)
@@ -35,59 +35,69 @@ signal app_switched(from: RunningApp, to: RunningApp)
signal recent_apps_changed()
const settings_manager := preload("res://core/global/settings_manager.tres")
-const NotificationManager := preload("res://core/global/notification_manager.tres")
+const notification_manager := preload("res://core/global/notification_manager.tres")
-var gamescope := preload("res://core/global/gamescope.tres") as Gamescope
-var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber
+var gamescope := preload("res://core/systems/gamescope/gamescope.tres") as GamescopeInstance
+var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance
var state_machine := preload("res://assets/state/state_machines/global_state_machine.tres") as StateMachine
var in_game_state := preload("res://assets/state/states/in_game.tres") as State
var in_game_menu_state := preload("res://assets/state/states/in_game_menu.tres") as State
var PID: int = OS.get_process_id()
+var _xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY)
+var _xwayland_ogui := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_OGUI)
+var _xwayland_game := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_GAME)
var _sandbox := Sandbox.get_sandbox()
var _current_app: RunningApp
var _pid_to_windows := {}
var _running: Array[RunningApp] = []
-var _stopping: Array[RunningApp] = []
var _apps_by_pid: Dictionary = {}
var _apps_by_name: Dictionary = {}
var _data_dir: String = ProjectSettings.get_setting("OpenGamepadUI/data/directory")
var _persist_path: String = "/".join([_data_dir, "launcher.json"])
var _persist_data: Dictionary = {"version": 1}
-var _ogui_window_id := gamescope.get_window_id(PID, gamescope.XWAYLAND.OGUI)
+var _ogui_window_id := 0
var should_manage_overlay := true
var logger := Log.get_logger("LaunchManager", Log.LEVEL.INFO)
# Connect to Gamescope signals
func _init() -> void:
- # When window focus changes, update the current app and gamepad profile
- var on_focus_changed := func(from: int, to: int):
- logger.info("Window focus changed from " + str(from) + " to: " + str(to))
- var last_app := _current_app
- _current_app = _detect_running_app(to)
-
- logger.debug("Last app: " + str(last_app) + " current_app: " + str(_current_app))
- app_switched.emit(last_app, _current_app)
-
- # If the app has a gamepad profile, set it
- if to != _ogui_window_id and _current_app:
- set_app_gamepad_profile(_current_app)
-
- # If we don't want LaunchManager to manage overlay (I.E. overlay mode), return false always.
- if not should_manage_overlay:
- return
-
- gamescope.focused_window_updated.connect(on_focus_changed)
-
- # Debug print when the focused app changed
- var on_focused_app_changed := func(from: int, to: int) -> void:
- logger.debug("Focused app changed from " + str(from) + " to " + str(to))
- gamescope.focused_app_updated.connect(on_focused_app_changed)
+ # Get the window ID of OpenGamepadUI
+ if _xwayland_ogui:
+ var ogui_windows := _xwayland_ogui.get_windows_for_pid(PID)
+ if not ogui_windows.is_empty():
+ _ogui_window_id = ogui_windows[0]
+
+ # Listen for signals from the primary Gamescope XWayland
+ if _xwayland_primary:
+ # When window focus changes, update the current app and gamepad profile
+ var on_focus_changed := func(from: int, to: int):
+ logger.info("Window focus changed from " + str(from) + " to: " + str(to))
+ var last_app := _current_app
+ _current_app = _detect_running_app(to)
+
+ logger.debug("Last app: " + str(last_app) + " current_app: " + str(_current_app))
+ app_switched.emit(last_app, _current_app)
+
+ # If the app has a gamepad profile, set it
+ if to != _ogui_window_id and _current_app:
+ set_app_gamepad_profile(_current_app)
+
+ # If we don't want LaunchManager to manage overlay (I.E. overlay mode), return false always.
+ if not should_manage_overlay:
+ return
+
+ _xwayland_primary.focused_window_updated.connect(on_focus_changed)
+
+ # Debug print when the focused app changed
+ var on_focused_app_changed := func(from: int, to: int) -> void:
+ logger.debug("Focused app changed from " + str(from) + " to " + str(to))
+ _xwayland_primary.focused_app_updated.connect(on_focused_app_changed)
# Whenever the in-game state is entered, set the gamepad profile
- var on_game_state_entered := func(from: State):
+ var on_game_state_entered := func(_from: State):
if _current_app:
set_app_gamepad_profile(_current_app)
@@ -95,8 +105,9 @@ func _init() -> void:
if not should_manage_overlay:
return
- logger.debug("Enabling STEAM_OVERLAY atom")
- gamescope.set_overlay(_ogui_window_id, 0)
+ if _xwayland_ogui:
+ logger.debug("Enabling STEAM_OVERLAY atom")
+ _xwayland_ogui.set_overlay(_ogui_window_id, 0)
var on_game_state_exited := func(_to: State):
# Set the gamepad profile to the global profile
@@ -106,8 +117,9 @@ func _init() -> void:
if not should_manage_overlay:
return
- logger.debug("Disabling STEAM_OVERLAY atom")
- gamescope.set_overlay(_ogui_window_id, 1)
+ if _xwayland_ogui:
+ logger.debug("Disabling STEAM_OVERLAY atom")
+ _xwayland_ogui.set_overlay(_ogui_window_id, 1)
in_game_state.state_entered.connect(on_game_state_entered)
in_game_state.state_exited.connect(on_game_state_exited)
@@ -170,7 +182,10 @@ func launch(app: LibraryLaunchItem) -> RunningApp:
# Set the display environment if one was not set.
if not "DISPLAY" in env:
- env["DISPLAY"] = gamescope.get_display_name(Gamescope.XWAYLAND.GAME)
+ if _xwayland_game:
+ env["DISPLAY"] = _xwayland_game.name
+ else:
+ env["DISPLAY"] = ""
var display := env["DISPLAY"] as String
# Set the OGUI ID environment variable
@@ -196,7 +211,7 @@ func launch(app: LibraryLaunchItem) -> RunningApp:
logger.info("Launching game with command: {0} {1}".format([exec, str(command)]))
# Launch the application process
- var pid = Reaper.create_process(exec, command)
+ var pid := Reaper.create_process(exec, command)
logger.info("Launched with PID: {0}".format([pid]))
# Create a running app instance
@@ -289,7 +304,7 @@ func set_app_gamepad_profile(app: RunningApp) -> void:
# Check to see if this game has any gamepad profiles. If so, set our
# gamepads to use them.
var section := ".".join(["game", app.launch_item.name.to_lower()])
- var profile_path = settings_manager.get_value(section, "gamepad_profile", "")
+ var profile_path := settings_manager.get_value(section, "gamepad_profile", "") as String
var profile_gamepad := settings_manager.get_value(section, "gamepad_profile_target", "") as String
logger.debug("Using profile '" + profile_path + "' with gamepad type '" + profile_gamepad + "'")
set_gamepad_profile(profile_path, profile_gamepad)
@@ -310,7 +325,7 @@ func set_gamepad_profile(path: String, target_gamepad: String = "") -> void:
profile_path = InputPlumber.DEFAULT_GLOBAL_PROFILE
logger.info("Loading global gamepad profile: " + profile_path)
- for gamepad in input_plumber.composite_devices:
+ for gamepad in input_plumber.get_composite_devices():
gamepad.target_modify_profile(profile_path, profile_modifier)
# Set the target gamepad if one was specified
@@ -338,7 +353,7 @@ func set_gamepad_profile(path: String, target_gamepad: String = "") -> void:
return
# TODO: Save profiles for individual controllers?
- for gamepad in input_plumber.composite_devices:
+ for gamepad in input_plumber.get_composite_devices():
gamepad.target_modify_profile(path, profile_modifier)
# Set the target gamepad if one was specified
@@ -353,11 +368,11 @@ func set_gamepad_profile(path: String, target_gamepad: String = "") -> void:
gamepad.set_target_devices(target_devices)
var notify := Notification.new("Using gamepad profile: " + profile.name)
- NotificationManager.show(notify)
+ notification_manager.show(notify)
## Sets the given running app as the current app
-func set_current_app(app: RunningApp, switch_baselayer: bool = true) -> void:
+func set_current_app(app: RunningApp, _switch_baselayer: bool = true) -> void:
if app == null:
return
app.grab_focus()
@@ -410,13 +425,12 @@ func _remove_running(app: RunningApp):
# Checks for running apps and updates our state accordingly
func check_running() -> void:
# Find the root window
- var root_id := gamescope.get_root_window_id(Gamescope.XWAYLAND.GAME)
+ if not _xwayland_game:
+ return
+ var root_id := _xwayland_game.root_window_id
if root_id < 0:
return
- # Update the Gamescope state
- gamescope.update()
-
# Update our view of running processes and what windows they have
_update_pids(root_id)
@@ -428,24 +442,28 @@ func check_running() -> void:
# Updates our mapping of PIDs to Windows. This gives us a good view of what
# processes are running, and what windows they have.
func _update_pids(root_id: int):
+ if not _xwayland_game:
+ return
var pids := {}
- var all_windows := gamescope.get_all_windows(root_id, Gamescope.XWAYLAND.GAME)
+ var all_windows := _xwayland_game.get_all_windows(root_id)
for window in all_windows:
- var pid := gamescope.get_window_pid(window, Gamescope.XWAYLAND.GAME)
- if not pid in pids:
- pids[pid] = []
- pids[pid].append(window)
+ var window_pids := _xwayland_game.get_pids_for_window(window)
+ for window_pid in window_pids:
+ if not window_pid in pids:
+ pids[window_pid] = []
+ pids[window_pid].append(window)
_pid_to_windows = pids
# Below functions detect launched game from other processes
# Returns the process ID
func _get_pid_from_focused_window(window_id: int) -> int:
- var pid := -1
- logger.debug(str(gamescope.get_xwayland(Gamescope.XWAYLAND.GAME).list_xprops(window_id)))
- pid = gamescope.get_xwayland(Gamescope.XWAYLAND.GAME).get_xprop(window_id, "_NET_WM_PID")
- logger.debug("PID: " + str(pid))
- return pid
+ if not _xwayland_game:
+ return -1
+ var window_pids := _xwayland_game.get_pids_for_window(window_id)
+ if window_pids.is_empty():
+ return -1
+ return window_pids[0]
# First method, try to get the name from the window_id using gamescope.
@@ -459,16 +477,18 @@ func _get_app_name_from_proc(pid: int) -> String:
var process_name: String
var path := "/proc/" + str(pid) + "/cwd"
var output := []
- var exit_code := OS.execute("ls", ["-l", path], output)
+ var _exit_code := OS.execute("ls", ["-l", path], output)
var process_path: PackedStringArray = output[0].strip_edges().split("/")
process_name = process_path[process_path.size()-1]
return process_name
-# Primary nw app ID method. Identifies the running app from steam's library.vdf
+# Primary new app ID method. Identifies the running app from steam's library.vdf
# and appmanifest_.acf files.
func _get_name_from_steam_library() -> String:
- var missing_app_id := gamescope.get_focused_app()
+ var missing_app_id := -1
+ if _xwayland_primary:
+ missing_app_id = _xwayland_primary.focused_app
logger.debug("Found unclaimed app id: " +str(missing_app_id))
var steam_library_path := OS.get_environment("HOME") +"/.steam/steam"
@@ -540,7 +560,7 @@ func _make_running_app_from_process(name: String, pid: int, window_id: int) -> R
logger.debug("Creating running app from process")
# Create a dummy LibraryLaunchItem to make our RunningApp.
- var lauch_dict = {
+ var lauch_dict := {
"_id": "",
"_provider_id": "",
"provider_app_id": "",
@@ -553,7 +573,9 @@ func _make_running_app_from_process(name: String, pid: int, window_id: int) -> R
}
var launch_item: LibraryLaunchItem = LibraryLaunchItem.from_dict(lauch_dict)
- var display:= gamescope.get_display_name(Gamescope.XWAYLAND.GAME)
+ var display := ""
+ if _xwayland_game:
+ display = _xwayland_game.name
var running_app: RunningApp = _make_running_app(launch_item, pid, display)
running_app.window_id = window_id
running_app.state = RunningApp.STATE.RUNNING
diff --git a/core/systems/gamescope/gamescope.gd b/core/systems/gamescope/gamescope.gd
new file mode 100644
index 00000000..185d62df
--- /dev/null
+++ b/core/systems/gamescope/gamescope.gd
@@ -0,0 +1,26 @@
+@icon("res://assets/editor-icons/streamline--desktop-game-solid.svg")
+extends Node
+class_name Gamescope
+
+## Manages gamescope.
+##
+## The [Gamescope] class is responsible for loading a [GamescopeInstance] and
+## calling its 'process()' method each frame.
+
+@export var instance: GamescopeInstance = load("res://core/systems/gamescope/gamescope.tres") as GamescopeInstance
+
+# Keep a reference to xwayland instances so they are not cleaned up automatically
+var _xwaylands: Array[GamescopeXWayland]
+var logger := Log.get_logger("Gamescope")
+
+
+func _ready() -> void:
+ _xwaylands = instance.get_xwaylands()
+ if _xwaylands.is_empty():
+ logger.warn("Gamescope not detected. Unexpected behavior expected.")
+
+
+func _process(_delta: float) -> void:
+ if not instance:
+ return
+ instance.process()
diff --git a/core/systems/gamescope/gamescope.tres b/core/systems/gamescope/gamescope.tres
new file mode 100644
index 00000000..1e22d2c3
--- /dev/null
+++ b/core/systems/gamescope/gamescope.tres
@@ -0,0 +1,3 @@
+[gd_resource type="GamescopeInstance" format=3 uid="uid://chd0nc6gbfnw0"]
+
+[resource]
diff --git a/core/systems/gamescope/gamescope_test.gd b/core/systems/gamescope/gamescope_test.gd
new file mode 100644
index 00000000..96fd0b73
--- /dev/null
+++ b/core/systems/gamescope/gamescope_test.gd
@@ -0,0 +1,21 @@
+extends Node2D
+
+var gamescope := load("res://core/systems/gamescope/gamescope.tres") as GamescopeInstance
+
+var PID := OS.get_process_id()
+
+# Called when the node enters the scene tree for the first time.
+func _ready() -> void:
+ print(gamescope.get_xwaylands())
+ var xwayland := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_OGUI)
+ if not xwayland:
+ print("XWayland not found")
+ return
+
+ var ogui_window_ids := xwayland.get_windows_for_pid(PID)
+ if ogui_window_ids.is_empty():
+ print("Unable to find window id for OGUI")
+ return
+ var ogui_window_id := ogui_window_ids[0]
+ xwayland.set_main_app(ogui_window_id)
+ print("Found window ids for OGUI: ", ogui_window_ids)
diff --git a/core/systems/gamescope/gamescope_test.tscn b/core/systems/gamescope/gamescope_test.tscn
new file mode 100644
index 00000000..73d96e76
--- /dev/null
+++ b/core/systems/gamescope/gamescope_test.tscn
@@ -0,0 +1,17 @@
+[gd_scene load_steps=5 format=3 uid="uid://83606lflpyo5"]
+
+[ext_resource type="Script" path="res://core/systems/gamescope/gamescope_test.gd" id="1_e433b"]
+[ext_resource type="Script" path="res://core/systems/gamescope/gamescope.gd" id="1_ukgov"]
+[ext_resource type="GamescopeInstance" uid="uid://chd0nc6gbfnw0" path="res://core/systems/gamescope/gamescope.tres" id="2_fbm81"]
+[ext_resource type="Texture2D" uid="uid://djy4rejy21s6g" path="res://icon.svg" id="4_j1wlf"]
+
+[node name="GamescopeTest" type="Node2D"]
+script = ExtResource("1_e433b")
+
+[node name="Gamescope" type="Node" parent="."]
+script = ExtResource("1_ukgov")
+instance = ExtResource("2_fbm81")
+
+[node name="Icon" type="Sprite2D" parent="."]
+position = Vector2(609, 401)
+texture = ExtResource("4_j1wlf")
diff --git a/core/systems/input/input_icon_manager.gd b/core/systems/input/input_icon_manager.gd
index f60d1b40..6caa3787 100644
--- a/core/systems/input/input_icon_manager.gd
+++ b/core/systems/input/input_icon_manager.gd
@@ -20,7 +20,7 @@ enum InputType {
}
var in_game_state := load("res://assets/state/states/in_game.tres") as State
-var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber
+var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance
var logger := Log.get_logger("InputIconManager", Log.LEVEL.INFO)
## Disable/Enable signaling on input type changes
@@ -154,7 +154,7 @@ func _init():
in_game_state.state_exited.connect(on_in_game_exited)
# Listen for InputPlumber device change events
- var on_comp_device_added := func(_device: InputPlumber.CompositeDevice):
+ var on_comp_device_added := func(_device: CompositeDevice):
_on_joy_connection_changed(true)
input_plumber.composite_device_added.connect(on_comp_device_added)
var on_comp_device_removed := func(_path: String):
diff --git a/core/systems/input/input_icon_processor.gd b/core/systems/input/input_icon_processor.gd
index 767f57f9..c4017668 100644
--- a/core/systems/input/input_icon_processor.gd
+++ b/core/systems/input/input_icon_processor.gd
@@ -6,7 +6,7 @@ class_name InputIconProcessor
const DEADZONE := 0.4
-var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber
+var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance
var icon_manager := load("res://core/systems/input/input_icon_manager.tres") as InputIconManager
@@ -23,8 +23,9 @@ func _input(event: InputEvent) -> void:
# If this is an InputPlumber event, use the name from the device
if event.has_meta("dbus_path") and not event.get_meta("dbus_path", "").is_empty():
var dbus_path := event.get_meta("dbus_path") as String
- var device := input_plumber.get_device(dbus_path)
- device_name = device.name
+ var device := input_plumber.get_composite_device(dbus_path)
+ if device:
+ device_name = device.name
# Otherwise, use the detected device name
else:
device_name = Input.get_joy_name(event.device)
diff --git a/core/systems/input/input_manager.gd b/core/systems/input/input_manager.gd
index 7925a935..d18f1b1f 100644
--- a/core/systems/input/input_manager.gd
+++ b/core/systems/input/input_manager.gd
@@ -18,7 +18,7 @@ var osk := load("res://core/global/keyboard_instance.tres") as KeyboardInstance
## The audio manager to use to adjust the audio when audio input events happen.
var audio_manager := load("res://core/global/audio_manager.tres") as AudioManager
## InputPlumber receives and sends DBus input events.
-var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber
+var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance
## LaunchManager provides context on the currently running app so we can switch profiles
var launch_manager := load("res://core/global/launch_manager.tres") as LaunchManager
## The Global State Machine
@@ -270,7 +270,7 @@ func _audio_input(event: InputEvent) -> void:
return
-func _watch_dbus_device(device: InputPlumber.CompositeDevice) -> void:
+func _watch_dbus_device(device: CompositeDevice) -> void:
for target in device.dbus_targets:
logger.debug("Adding watch for " + device.name + " " + target.name)
logger.debug(str(target.get_instance_id()))
diff --git a/core/systems/input/input_manager.tscn b/core/systems/input/input_manager.tscn
index a32585d5..000a852e 100644
--- a/core/systems/input/input_manager.tscn
+++ b/core/systems/input/input_manager.tscn
@@ -1,6 +1,12 @@
-[gd_scene load_steps=2 format=3 uid="uid://n83wlhmmsu3j"]
+[gd_scene load_steps=4 format=3 uid="uid://n83wlhmmsu3j"]
[ext_resource type="Script" path="res://core/systems/input/input_manager.gd" id="1_3s67x"]
+[ext_resource type="Script" path="res://core/systems/input/input_plumber.gd" id="2_v1kl3"]
+[ext_resource type="InputPlumberInstance" uid="uid://e2bevy4j4rx2" path="res://core/systems/input/input_plumber.tres" id="3_eclcy"]
[node name="InputManager" type="Node"]
script = ExtResource("1_3s67x")
+
+[node name="InputPlumber" type="Node" parent="."]
+script = ExtResource("2_v1kl3")
+instance = ExtResource("3_eclcy")
diff --git a/core/systems/input/input_plumber.gd b/core/systems/input/input_plumber.gd
index f21c8e0a..9840ca47 100644
--- a/core/systems/input/input_plumber.gd
+++ b/core/systems/input/input_plumber.gd
@@ -1,5 +1,5 @@
-@icon("res://assets/icons/game-controller.svg")
-extends Resource
+@icon("res://addons/core/assets/icons/inputplumber.svg")
+extends Node
class_name InputPlumber
## Manages routing input to and from InputPlumber.
@@ -7,690 +7,41 @@ class_name InputPlumber
## The InputPlumberManager class is responsible for handling dbus messages to and
## from the InputPlumber input manager daemon.
-const INPUT_PLUMBER_BUS := "org.shadowblip.InputPlumber"
-const INPUT_PLUMBER_PATH := "/org/shadowblip/InputPlumber"
-const INPUT_PLUMBER_PREFIX := INPUT_PLUMBER_PATH + "/devices"
-const INPUT_PLUMBER_MANAGER_PATH := "/org/shadowblip/InputPlumber/Manager"
-const IFACE_MANAGER := "org.shadowblip.InputManager"
-const IFACE_COMPOSITE_DEVICE := "org.shadowblip.Input.CompositeDevice"
-const IFACE_EVENT_DEVICE := "org.shadowblip.Input.Source.EventDevice"
-const IFACE_HIDRAW_DEVICE := "org.shadowblip.Input.Source.HIDRawDevice"
-const IFACE_IIO_DEVICE := "org.shadowblip.Input.Source.IIODevice"
-const IFACE_DBUS_DEVICE := "org.shadowblip.Input.DBusDevice"
-const IFACE_GAMEPAD_DEVICE := "org.shadowblip.Input.Gamepad"
-const IFACE_KEYBOARD_DEVICE := "org.shadowblip.Input.Keyboard"
-const IFACE_MOUSE_DEVICE := "org.shadowblip.Input.Mouse"
-
const DEFAULT_PROFILE := "res://assets/gamepad/profiles/default.json"
const DEFAULT_GLOBAL_PROFILE := "user://data/gamepad/profiles/global_default.json"
const PROFILES_DIR := "user://data/gamepad/profiles"
-enum INTERCEPT_MODE {
- NONE,
- PASS,
- ALL,
-}
-
-var logger := Log.get_logger("InputPlumber", Log.LEVEL.INFO)
-
-var dbus := load("res://core/global/dbus_system.tres") as DBusManager
-var manager := Manager.new(dbus.create_proxy(INPUT_PLUMBER_BUS, INPUT_PLUMBER_MANAGER_PATH))
-var object_manager := dbus.ObjectManager.new(dbus.create_proxy(INPUT_PLUMBER_BUS, INPUT_PLUMBER_PATH))
-var system_thread := load("res://core/systems/threading/system_thread.tres") as SharedThread
-
-var is_running := false
-var composite_devices: Array[CompositeDevice] = []
-var composite_devices_map: Dictionary = {}
-var intercept_mode_current: INTERCEPT_MODE = INTERCEPT_MODE.NONE
-var intercept_triggers_current: PackedStringArray = ["Gamepad:Button:Guide"]
-var intercept_target_current: String = "Gamepad:Button:Guide"
-
-## Emitted when InputPlumber is detected as running
-signal started
-## Emitted when InputPlumber is detected as stopped
-signal stopped
-## Emitted when a CompositeDevice is dicovered and identified as a new device
-signal composite_device_added(device: CompositeDevice)
-## Emitted when a CompositeDevice is dicovered over dbus but already exists in
-## the local map
-signal composite_device_changed(device: CompositeDevice)
-## Emitted when a CompositeDevice is removed
-signal composite_device_removed(dbus_path: String)
-
-
-func _init() -> void:
- logger.debug("Initalizing InputPlumber. Found composite devices: " + str(composite_devices))
- object_manager.interfaces_added.connect(_on_interfaces_added)
- object_manager.interfaces_removed.connect(_on_interfaces_removed)
- composite_devices = get_devices()
- for device in composite_devices:
- composite_devices_map[device.dbus_path] = device
-
- # Ensure the global default config is created
- var profiles_dir := ProjectSettings.globalize_path(PROFILES_DIR)
- if DirAccess.make_dir_recursive_absolute(profiles_dir) != OK:
- logger.error("Failed to create user profiles directory: " + profiles_dir)
- return
- var default_profile := FileAccess.open(DEFAULT_PROFILE, FileAccess.READ)
- var default_profile_data := default_profile.get_as_text()
- logger.debug("Writing default global profile to: " + DEFAULT_GLOBAL_PROFILE)
- var global_default_profile := FileAccess.open(DEFAULT_GLOBAL_PROFILE, FileAccess.WRITE)
- global_default_profile.store_string(default_profile_data)
-
- # Start a task that monitors whether or not InputPlumber is started or stopped,
- # and emits signals when its running state changes.
- started.connect(_on_inputplumber_started)
- stopped.connect(_on_inputplumber_stopped)
- var running_check := func():
- var update_running := func():
- var running := dbus.bus_exists(INPUT_PLUMBER_BUS)
- if running == self.is_running:
- return
- self.is_running = running
- if running:
- started.emit()
- else:
- stopped.emit()
- update_running.call_deferred()
- system_thread.exec(running_check)
- system_thread.scheduled_exec(running_check, 5000, SharedThread.ScheduledTaskType.RECURRING)
-
-
-func _on_inputplumber_started() -> void:
- logger.info("InputPlumber started")
-
- # Remove any Godot gamepad input maps and rely completely on InputPlumber for
- # gamepad input
- var actions := InputMap.get_actions()
- var to_erase: Array[InputEvent] = []
- for action in actions:
- var events := InputMap.action_get_events(action)
- for event in events:
- if event is InputEventJoypadButton or event is InputEventJoypadMotion:
- logger.debug("Erasing mapping for", action, ":", event)
- InputMap.action_erase_event(action, event)
-
-
-func _on_inputplumber_stopped() -> void:
- logger.warn("InputPlumber stopped")
-
- # Reload Godot input mappings if InputPlumber shuts down
- logger.debug("Restoring input mappings from project settings")
- InputMap.load_from_project_settings()
-
-
-func _on_interfaces_added(dbus_path: String) -> void:
- logger.debug("Interfaces Added: " + str(dbus_path))
- if not "CompositeDevice" in dbus_path:
- return
- composite_devices = get_devices(dbus_path)
- composite_devices_map.clear()
- for device in composite_devices:
- composite_devices_map[device.dbus_path] = device
-
-
-func _on_interfaces_removed(dbus_path: String) -> void:
- logger.debug("Interfaces Removed: " + str(dbus_path))
- if not "CompositeDevice" in dbus_path:
- return
- composite_devices = get_devices()
- composite_devices_map.clear()
- for device in composite_devices:
- composite_devices_map[device.dbus_path] = device
- if dbus_path.contains("CompositeDevice"):
- composite_device_removed.emit(dbus_path)
-
-
-## Returns true if InputPlumber can be used on this system
-func supports_input_plumber() -> bool:
- return dbus.bus_exists(INPUT_PLUMBER_BUS)
-
-
-func get_objects_of(pattern: String) -> Array:
- var devices: Array = []
- var device_paths := dbus.get_managed_objects(INPUT_PLUMBER_BUS, INPUT_PLUMBER_PATH)
- logger.debug("Searching for " + pattern + " objects.")
- # Loop through all objects on the bus
- for obj in device_paths:
-
- var object := obj as DBusManager.ManagedObject
- var path := object.path
- var proxy := dbus.create_proxy(INPUT_PLUMBER_BUS, path)
- #logger.debug("Found object: " + str(object) + " with path " + path)
- if path.contains(pattern) and pattern == "CompositeDevice":
- logger.debug("Found " + pattern + " in " + path)
- var device := CompositeDevice.new(proxy)
- devices.append(device)
- continue
-
- if path.contains(pattern) and pattern == "event":
- var device := EventDevice.new(proxy)
- devices.append(device)
- continue
-
- if path.contains(pattern) and pattern == "hidraw":
- var device := HIDRawDevice.new(proxy)
- devices.append(device)
- continue
-
- if path.contains(pattern) and pattern == "iio":
- var device := IIODevice.new(proxy)
- devices.append(device)
- continue
-
- if path.contains(pattern) and pattern == "dbus":
- var device := DBusDevice.new(proxy)
- devices.append(device)
- continue
-
- if path.contains(pattern) and pattern == "gamepad":
- var device := GamepadDevice.new(proxy)
- devices.append(device)
- continue
-
- if path.contains(pattern) and pattern == "keyboard":
- var device := KeyboardDevice.new(proxy)
- devices.append(device)
- continue
-
- if path.contains(pattern) and pattern == "mouse":
- var device := MouseDevice.new(proxy)
- devices.append(device)
- continue
- logger.debug("Returning devices: " + str(devices))
- return devices
-
-
-## Retrieves all CompositeDevices currently on the InputPlumber DBus interface. Will
-## emit composite_device_added if the given dbus path is a new device, or
-## composite_device_changed if it already existed
-func get_devices(dbus_path: String = "") -> Array[CompositeDevice]:
- logger.debug("Getting all composite devices.")
- var new_device_list: Array[CompositeDevice]
- new_device_list.assign(get_objects_of("CompositeDevice"))
- var existing_devices: Array[CompositeDevice]
-
- # Only return new devices. Overriding devices breaks signaling.
- for device in new_device_list:
- var found: bool = false
- for old_dev in composite_devices:
- if old_dev.dbus_path == device.dbus_path:
- existing_devices.append(old_dev)
- found = true
- if dbus_path == device.dbus_path:
- composite_device_changed.emit(device)
- break
-
- # New device found
- if not found:
- existing_devices.append(device)
- set_intercept_mode_single(intercept_mode_current, device)
- set_intercept_activation_single(intercept_triggers_current, \
- intercept_target_current, device)
- composite_device_added.emit(device)
-
- return existing_devices
-
-
-## Returns the [CompositeDevice] with the given DBus path
-func get_device(dbus_path: String) -> CompositeDevice:
- if dbus_path in composite_devices_map:
- return composite_devices_map[dbus_path]
- return null
-
-
-## Sets all composite devices to the specified intercept mode.
-func set_intercept_mode(mode: INTERCEPT_MODE) -> void:
- logger.debug("Setting all composite devices to mode: " + str(mode))
- intercept_mode_current = mode
- for d in composite_devices:
- var device := d as CompositeDevice
- set_intercept_mode_single(mode, device)
-
-
-func set_intercept_mode_single(mode: INTERCEPT_MODE, device: CompositeDevice) -> void:
- logger.debug("Setting composite device "+ device.dbus_path + " to mode: " + str(mode))
- match mode:
- INTERCEPT_MODE.NONE:
- device.intercept_mode = 0
- INTERCEPT_MODE.PASS:
- device.intercept_mode = 1
- INTERCEPT_MODE.ALL:
- device.intercept_mode = 2
-
-
-## Sets all composite devices to use the specified intercept actions.
-func set_intercept_activation(triggers: PackedStringArray, target: String) -> void:
- logger.debug("Setting all composite devices to intercept triggers: " + str(triggers) + " and target event: " + target)
- intercept_triggers_current = triggers
- intercept_target_current = target
- for d in composite_devices:
- var device := d as CompositeDevice
- set_intercept_activation_single(triggers, target, device)
-
-
-func set_intercept_activation_single(triggers: PackedStringArray, target: String, device: CompositeDevice) -> void:
- logger.debug("Setting composite device "+ device.dbus_path + " intercept triggers: " + str(triggers) + " and target event: " + target)
- device.set_intercept_activation(triggers, target)
-
-
-class Manager extends Resource:
- signal updated
- var _proxy: DBusManager.Proxy
-
- func _init(proxy: DBusManager.Proxy) -> void:
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- func create_composite_device(config_path: String) -> String:
- var result := _proxy.call_method(IFACE_MANAGER, "CreateCompositeDevice", [config_path])
- if not result:
- return ""
- var args := result.get_args()
- if args.size() != 1:
- return ""
- if not args[0] is String:
- return ""
- return args[0]
-
-
-class CompositeDevice extends Resource:
- signal updated
-
- var _proxy: DBusManager.Proxy
- var dbus_targets: Array[DBusDevice]
- var dbus_path: String
- var logger := Log.get_logger("InputPlumber CompositeDevice", Log.LEVEL.INFO)
-
- func _init(proxy: DBusManager.Proxy) -> void:
- var dbus := load("res://core/global/dbus_system.tres") as DBusManager
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
- for path in self.dbus_devices:
- var device := DBusDevice.new(dbus.create_proxy(INPUT_PLUMBER_BUS, path))
- dbus_targets.append(device)
- dbus_path = _proxy.path
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- var name: String:
- get:
- var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "Name")
- if not property is String:
- return ""
- return property
-
- var profile_name: String:
- get:
- var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "ProfileName")
- if not property is String:
- return ""
- return property
-
- var intercept_mode: int:
- set(v):
- #print("Setting mode " + str(v) + " on " + self.dbus_path)
- _proxy.set_property(IFACE_COMPOSITE_DEVICE, "InterceptMode", DBus.uint32(v))
- get:
- var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "InterceptMode")
- if not property is int:
- return -1
- return property
-
- var capabilities: PackedStringArray:
- get:
- var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "Capabilities")
- if not property is Array:
- return []
- return property
-
- var target_capabilities: PackedStringArray:
- get:
- var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "TargetCapabilities")
- if not property is Array:
- return []
- return property
-
- var dbus_devices: PackedStringArray:
- get:
- var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "DbusDevices")
- if not property is Array:
- return []
- return property
-
- var source_device_paths: PackedStringArray:
- get:
- var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "SourceDevicePaths")
- if not property is Array:
- return []
- return property
+@export var instance: InputPlumberInstance = load("res://core/systems/input/input_plumber.tres")
- var target_devices: PackedStringArray:
- get:
- var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "TargetDevices")
- if not property is Array:
- return []
- return property
+# Keep a reference to dbus devices so they are not cleaned up automatically
+var _dbus_devices := {}
- func set_target_devices(devices: PackedStringArray) -> void:
- _proxy.call_method( IFACE_COMPOSITE_DEVICE, "SetTargetDevices", [devices], "as")
- ## Load the given profile on the composite device, optionally specifying a profile
- ## modifier, which is a target device string (e.g. "deck", "ds5-edge", etc.) to
- ## adapt the profile for. This will update the profile with target-specific
- ## defaults, like mapping left/right pads to the DualSense center pad if no
- ## other mappings are defined.
- func target_modify_profile(path: String, profile_modifier: String = "") -> void:
- logger.debug("Loading Profile:", path)
- if path == "" or not path.ends_with(".json") or not FileAccess.file_exists(path):
- logger.error("Profile path:", path," is not a valid profile path.")
+func _ready() -> void:
+ # Add listeners for any new devices
+ var on_device_added := func(device: CompositeDevice):
+ var dbus_devices := device.dbus_devices
+ if dbus_devices.is_empty():
return
- if profile_modifier.is_empty():
- load_profile_path(path)
- return
-
- var profile := InputPlumberProfile.load(path)
-
- var c_pad_cap = "Touchpad:CenterPad:Motion"
- var l_pad_cap = "Touchpad:LeftPad:Motion"
- var r_pad_cap = "Touchpad:RightPad:Motion"
- var mouse_cap = "Mouse:Motion"
-
- if !profile_modifier.is_empty():
- var mapped_capabilities := profile.to_json()
- logger.debug("Mapped Capabilities (before):", mapped_capabilities)
- match profile_modifier:
- "deck":
- logger.debug("Steam Deck Profile")
- if c_pad_cap not in mapped_capabilities:
- logger.debug("Map", c_pad_cap)
- var c_pad_map := InputPlumberMapping.from_source_capability(c_pad_cap)
- var r_pad_event := InputPlumberEvent.from_capability(r_pad_cap)
- c_pad_map.target_events = [r_pad_event]
- profile.mapping.append(c_pad_map)
-
- "ds5", "ds5-edge":
- logger.debug("Dualsense Profile")
- if l_pad_cap not in mapped_capabilities:
- logger.debug("Map", l_pad_cap)
- var l_pad_map := InputPlumberMapping.from_source_capability(l_pad_cap)
- var c_pad_event := InputPlumberEvent.from_capability(c_pad_cap)
- l_pad_map.target_events = [c_pad_event]
- profile.mapping.append(l_pad_map)
- if r_pad_cap not in mapped_capabilities:
- logger.debug("Map", r_pad_cap)
- var r_pad_map := InputPlumberMapping.from_source_capability(r_pad_cap)
- var c_pad_event := InputPlumberEvent.from_capability(c_pad_cap)
- r_pad_map.target_events = [c_pad_event]
- profile.mapping.append(r_pad_map)
-
- _:
- logger.debug("Target device needs no modifications:", profile_modifier)
-
- mapped_capabilities = profile.to_json()
- logger.debug("Mapped Capabilities (after):", mapped_capabilities)
-
- path = path.rstrip(".json") + profile_modifier + ".json"
- if profile.save(path) != OK:
- logger.error("Failed to save", profile.name, "to", path)
+ var dbus_device := dbus_devices[0]
+ var dbus_path := device.dbus_path
+ _dbus_devices[dbus_path] = dbus_device
+ instance.composite_device_added.connect(on_device_added)
+
+ # Add listeners when devices are removed
+ var on_device_removed := func(dbus_path: String):
+ if not _dbus_devices.has(dbus_path):
return
- load_profile_path(path)
-
- func load_profile_path(path: String) -> void:
- # Get the absolute path if this is a resource path
- var absolute_path := path
- if path.begins_with("res://") or path.begins_with("user://"):
- absolute_path = ProjectSettings.globalize_path(path)
- _proxy.call_method(IFACE_COMPOSITE_DEVICE, "LoadProfilePath", [absolute_path], "s")
-
- func send_event(action: String, value: Variant) -> void:
- _proxy.call_method( IFACE_COMPOSITE_DEVICE, "SendEvent", [action, value], "sv")
-
- func send_button_chord(actions: PackedStringArray) -> void:
- _proxy.call_method( IFACE_COMPOSITE_DEVICE, "SendButtonChord", [actions], "as")
-
- func set_intercept_activation(triggers: PackedStringArray, target_event: String) -> void:
- _proxy.call_method( IFACE_COMPOSITE_DEVICE, "SetInterceptActivation", [triggers, target_event], "ass")
-
-
-class EventDevice extends Resource:
- signal updated
- var _proxy: DBusManager.Proxy
-
- func _init(proxy: DBusManager.Proxy) -> void:
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- var name: String:
- get:
- var property = _proxy.get_property(IFACE_EVENT_DEVICE, "Name")
- if not property is String:
- return ""
- return property
-
- var device_path: String:
- get:
- var property = _proxy.get_property(IFACE_EVENT_DEVICE, "DevicePath")
- if not property is String:
- return ""
- return property
-
- var phys_path: String:
- get:
- var property = _proxy.get_property(IFACE_EVENT_DEVICE, "PhysPath")
- if not property is String:
- return ""
- return property
-
- var sysfs_path: String:
- get:
- var property = _proxy.get_property(IFACE_EVENT_DEVICE, "SysfsPath")
- if not property is String:
- return ""
- return property
-
- var unique_id: String:
- get:
- var property = _proxy.get_property(IFACE_EVENT_DEVICE, "UniqueId")
- if not property is String:
- return ""
- return property
-
- var handlers: PackedStringArray:
- get:
- var property = _proxy.get_property(IFACE_EVENT_DEVICE, "Handlers")
- if not property is Array:
- return []
- return property
-
-
-class HIDRawDevice extends Resource:
- signal updated
- var _proxy: DBusManager.Proxy
-
- func _init(proxy: DBusManager.Proxy) -> void:
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- var interface_number: int:
- get:
- var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "InterfaceNumber")
- if not property is int:
- return -1
- return property
-
- var manufacturer: String:
- get:
- var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "Manufacturer")
- if not property is String:
- return ""
- return property
-
- var path: String:
- get:
- var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "Path")
- if not property is String:
- return ""
- return property
-
- var product: String:
- get:
- var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "Product")
- if not property is String:
- return ""
- return property
-
- var product_id: String:
- get:
- var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "ProductId")
- if not property is String:
- return ""
- return property
+ _dbus_devices.erase(dbus_path)
+ instance.composite_device_removed.connect(on_device_removed)
- var release_number: String:
- get:
- var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "ReleaseNumber")
- if not property is String:
- return ""
- return property
+ # Find all composite devices
+ var devices := instance.get_composite_devices()
+ for device in devices:
+ on_device_added.call(device)
- var serial_number: String:
- get:
- var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "SerialNumber")
- if not property is String:
- return ""
- return property
- var vendor_id: String:
- get:
- var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "VendorId")
- if not property is String:
- return ""
- return property
-
-
-class IIODevice extends Resource:
- signal updated
- var _proxy: DBusManager.Proxy
-
- func _init(proxy: DBusManager.Proxy) -> void:
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- var id: String:
- get:
- var property = _proxy.get_property(IFACE_IIO_DEVICE, "Id")
- if not property is int:
- return ""
- return property
-
- var name: String:
- get:
- var property = _proxy.get_property(IFACE_IIO_DEVICE, "Name")
- if not property is String:
- return ""
- return property
-
-
-class KeyboardDevice extends Resource:
- signal updated
- var _proxy: DBusManager.Proxy
-
- func _init(proxy: DBusManager.Proxy) -> void:
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- var name: String:
- get:
- var property = _proxy.get_property(IFACE_KEYBOARD_DEVICE, "Name")
- if not property is String:
- return ""
- return property
-
- func send_key(key: String, pressed: bool) -> void:
- _proxy.call_method(IFACE_KEYBOARD_DEVICE, "SendKey", [key, pressed])
-
-
-class MouseDevice extends Resource:
- signal updated
- var _proxy: DBusManager.Proxy
-
- func _init(proxy: DBusManager.Proxy) -> void:
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- var name: String:
- get:
- var property = _proxy.get_property(IFACE_MOUSE_DEVICE, "Name")
- if not property is String:
- return ""
- return property
-
-
-class GamepadDevice extends Resource:
- signal updated
- var _proxy: DBusManager.Proxy
-
- func _init(proxy: DBusManager.Proxy) -> void:
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- var name: String:
- get:
- var property = _proxy.get_property(IFACE_GAMEPAD_DEVICE, "Name")
- if not property is String:
- return ""
- return property
-
-
-class DBusDevice extends Resource:
- signal input_event(type_code: String, value: float)
- var _proxy: DBusManager.Proxy
-
- func _init(proxy: DBusManager.Proxy) -> void:
- #print("Creating DBusDevice!")
- _proxy = proxy
- _proxy.message_received.connect(_on_message_received)
- _proxy.thread.exec(_proxy.watch.bind(IFACE_DBUS_DEVICE, "InputEvent"))
-
- func _on_message_received(msg: DBusMessage) -> void:
- if not msg:
- return
- if msg.get_member() != "InputEvent":
- return
- var args := msg.get_args()
- if args.size() < 2:
- return
- #print("Got InputEvent " + str(args))
- #print(str(self.get_instance_id()))
- #print(str(self.get_rid()))
- input_event.emit(args[0], args[1])
-
- var name: String:
- get:
- var property = _proxy.get_property(IFACE_DBUS_DEVICE, "Name")
- if not property is String:
- return ""
- return property
+func _process(_delta: float) -> void:
+ if not instance:
+ return
+ instance.process()
diff --git a/core/systems/input/input_plumber.tres b/core/systems/input/input_plumber.tres
index 60a89d18..64f50b4a 100644
--- a/core/systems/input/input_plumber.tres
+++ b/core/systems/input/input_plumber.tres
@@ -1,6 +1,3 @@
-[gd_resource type="Resource" script_class="InputPlumber" load_steps=2 format=3 uid="uid://ct1hxnffwtqvl"]
-
-[ext_resource type="Script" path="res://core/systems/input/input_plumber.gd" id="1_0njvc"]
+[gd_resource type="InputPlumberInstance" format=3 uid="uid://e2bevy4j4rx2"]
[resource]
-script = ExtResource("1_0njvc")
diff --git a/core/systems/input/input_plumber_test.gd b/core/systems/input/input_plumber_test.gd
index 33642059..238dd6b0 100644
--- a/core/systems/input/input_plumber_test.gd
+++ b/core/systems/input/input_plumber_test.gd
@@ -1,45 +1,51 @@
-extends Node2D
-
-var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber
-
-
-# Called when the node enters the scene tree for the first time.
-func _ready():
- for compo in input_plumber.composite_devices:
- print(compo.dbus_targets)
- for dt in compo.dbus_targets:
- dt.input_event.connect(_on_input_event)
-
- input_plumber.composite_device_added.connect(_on_dev_add)
- input_plumber.composite_device_removed.connect(_on_dev_rm)
- input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.ALL)
- get_tree().root.mode = Window.MODE_MINIMIZED
-
-
-# Called every frame. 'delta' is the elapsed time since the previous frame.
-func _process(delta):
- pass
-
-
-func _on_input_event(event: String, value: float) -> void:
- print("Got input event " + event + " with value " + str(value))
- if event == "ui_guide":
- var pressed = value == 1.0
- for device in input_plumber.composite_devices:
- device.intercept_mode = 0
- device.send_event("Gamepad:Button:Guide", pressed)
- device.intercept_mode = InputPlumber.INTERCEPT_MODE.ALL as int
-
- if event == "ui_accept":
- for device in input_plumber.composite_devices:
- var intercept_mode = device.intercept_mode
- device.intercept_mode = 0
- device.send_button_chord(["Gamepad:Button:Guide", "Gamepad:Button:South"])
- device.intercept_mode = InputPlumber.INTERCEPT_MODE.ALL as int
-
-
-func _on_dev_add() -> void:
- print("Newbie doobie")
-
-func _on_dev_rm() -> void:
- print("bubye")
+extends GutTest
+
+
+# Configure the given composite device to set intercept mode and print input
+# events
+func _watch_device(device: CompositeDevice) -> void:
+ gut.p(" Found device " + str(device) + " at " + device.dbus_path)
+ device.intercept_mode = InputPlumberInstance.INTERCEPT_MODE_ALL
+ assert_eq(device.intercept_mode, InputPlumberInstance.INTERCEPT_MODE_ALL)
+ var dbus_devices := device.dbus_devices
+ if dbus_devices.is_empty():
+ gut.p(" No dbus devices found for device")
+ return
+ var dbus_device := dbus_devices[0]
+ gut.p(" Found DBus device: " + str(dbus_device) + " at " + dbus_device.dbus_path)
+ var on_input_event := func(event: String, value: float):
+ gut.p("[DBus Event] " + event + ": " + str(value))
+ dbus_device.input_event.connect(on_input_event)
+
+
+func test_inputplumber() -> void:
+ var inputplumber := InputPlumber.new()
+ inputplumber.instance = load("res://core/systems/input/input_plumber.tres")
+ add_child_autoqfree(inputplumber)
+
+ if not inputplumber.instance.is_running():
+ pass_test("InputPlumber is not running. Skipping tests.")
+ return
+
+ # Set intercept mode
+ gut.p("Setting intercept mode to ALL")
+ inputplumber.instance.intercept_mode = InputPlumberInstance.INTERCEPT_MODE_ALL
+ assert_eq(inputplumber.instance.intercept_mode, InputPlumberInstance.INTERCEPT_MODE_ALL)
+
+ # Find all composite devices
+ gut.p("Discovering all composite devices")
+ var devices := inputplumber.instance.get_composite_devices()
+ for device in devices:
+ _watch_device(device)
+
+ # Add listeners for any new devices
+ inputplumber.instance.composite_device_added.connect(_watch_device)
+
+ # Add listeners when devices are removed
+ var on_device_removed := func(dbus_path: String):
+ gut.p("Device was removed: " + dbus_path)
+ inputplumber.instance.composite_device_removed.connect(on_device_removed)
+
+ await wait_seconds(30, "Waiting 30s... Press buttons to test")
+
+ inputplumber.instance.intercept_mode = InputPlumberInstance.INTERCEPT_MODE_NONE
diff --git a/core/systems/input/input_plumber_test.tscn b/core/systems/input/input_plumber_test.tscn
deleted file mode 100644
index 659851bf..00000000
--- a/core/systems/input/input_plumber_test.tscn
+++ /dev/null
@@ -1,6 +0,0 @@
-[gd_scene load_steps=2 format=3 uid="uid://djylrf6b7nxyx"]
-
-[ext_resource type="Script" path="res://core/systems/input/input_plumber_test.gd" id="1_x2aky"]
-
-[node name="InputPlumberTest" type="Node2D"]
-script = ExtResource("1_x2aky")
diff --git a/core/systems/input/input_plumber_test_old.tscn b/core/systems/input/input_plumber_test_old.tscn
new file mode 100644
index 00000000..88349a4e
--- /dev/null
+++ b/core/systems/input/input_plumber_test_old.tscn
@@ -0,0 +1,52 @@
+[gd_scene load_steps=2 format=3 uid="uid://b401m3qcqa8yd"]
+
+[sub_resource type="GDScript" id="GDScript_k2tux"]
+script/source = "extends Node2D
+
+var input_plumber := load(\"res://core/systems/input/input_plumber.tres\") as InputPlumber
+
+
+# Called when the node enters the scene tree for the first time.
+func _ready():
+ for compo in input_plumber.composite_devices:
+ print(compo.dbus_targets)
+ for dt in compo.dbus_targets:
+ dt.input_event.connect(_on_input_event)
+
+ input_plumber.composite_device_added.connect(_on_dev_add)
+ input_plumber.composite_device_removed.connect(_on_dev_rm)
+ input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.ALL)
+ get_tree().root.mode = Window.MODE_MINIMIZED
+
+
+# Called every frame. 'delta' is the elapsed time since the previous frame.
+func _process(delta):
+ pass
+
+
+func _on_input_event(event: String, value: float) -> void:
+ print(\"Got input event \" + event + \" with value \" + str(value))
+ if event == \"ui_guide\":
+ var pressed = value == 1.0
+ for device in input_plumber.composite_devices:
+ device.intercept_mode = 0
+ device.send_event(\"Gamepad:Button:Guide\", pressed)
+ device.intercept_mode = InputPlumber.INTERCEPT_MODE.ALL as int
+
+ if event == \"ui_accept\":
+ for device in input_plumber.composite_devices:
+ var intercept_mode = device.intercept_mode
+ device.intercept_mode = 0
+ device.send_button_chord([\"Gamepad:Button:Guide\", \"Gamepad:Button:South\"])
+ device.intercept_mode = InputPlumber.INTERCEPT_MODE.ALL as int
+
+
+func _on_dev_add() -> void:
+ print(\"Newbie doobie\")
+
+func _on_dev_rm() -> void:
+ print(\"bubye\")
+"
+
+[node name="InputPlumberTest" type="Node2D"]
+script = SubResource("GDScript_k2tux")
diff --git a/core/systems/input/overlay_mode_input_manager.gd b/core/systems/input/overlay_mode_input_manager.gd
index f060c8fe..20706a36 100644
--- a/core/systems/input/overlay_mode_input_manager.gd
+++ b/core/systems/input/overlay_mode_input_manager.gd
@@ -14,7 +14,7 @@ class_name OverlayInputManager
## The audio manager to use to adjust the audio when audio input events happen.
var audio_manager := load("res://core/global/audio_manager.tres") as AudioManager
## InputPlumber receives and sends DBus input events.
-var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber
+var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance
## LaunchManager provides context on the currently running app so we can switch profiles
var launch_manager := load("res://core/global/launch_manager.tres") as LaunchManager
## The Global State Machine
@@ -321,7 +321,7 @@ func _return_chord(actions: PackedStringArray) -> void:
# Input.parse_input_event so we don't do this terrible loop. This is awful.
logger.debug("Return events to InputPlumber: " + str(actions))
for device in input_plumber.composite_devices:
- device.intercept_mode = InputPlumber.INTERCEPT_MODE.PASS
+ device.intercept_mode = InputPlumberInstance.INTERCEPT_MODE_PASS
device.send_button_chord(actions)
@@ -347,7 +347,7 @@ func _audio_input(event: InputEvent) -> void:
return
-func _watch_dbus_device(device: InputPlumber.CompositeDevice) -> void:
+func _watch_dbus_device(device: CompositeDevice) -> void:
for target in device.dbus_targets:
logger.debug("Adding watch for " + device.name + " " + target.name)
logger.debug(str(target.get_instance_id()))
diff --git a/core/systems/launcher/reaper.gd b/core/systems/launcher/reaper.gd
index fb05d0d5..67aa61e7 100644
--- a/core/systems/launcher/reaper.gd
+++ b/core/systems/launcher/reaper.gd
@@ -12,8 +12,9 @@ enum SIG {
## Spawn a process with PR_SET_CHILD_SUBREAPER set so child processes will
## reparent themselves to OpenGamepadUI. Returns the PID of the spawned process.
static func create_process(cmd: String, args: PackedStringArray) -> int:
- #return OS.create_process(cmd, args)
- return LinuxThread.subreaper_create_process(cmd, args)
+ return OS.create_process(cmd, args)
+ # TODO: Re-implement subreaper
+ #return LinuxThread.subreaper_create_process(cmd, args)
# Kills the given PID and all its descendants
diff --git a/core/systems/launcher/running_app.gd b/core/systems/launcher/running_app.gd
index 145fefff..160b3a2e 100644
--- a/core/systems/launcher/running_app.gd
+++ b/core/systems/launcher/running_app.gd
@@ -5,7 +5,7 @@ class_name RunningApp
##
## RunningApp contains details and methods around running applications
-const Gamescope := preload("res://core/global/gamescope.tres")
+const gamescope := preload("res://core/systems/gamescope/gamescope.tres")
## Emitted when all child processes of the app are no longer running
@@ -187,27 +187,35 @@ func suspend(enable: bool) -> void:
func get_window_title(win_id: int) -> String:
if not win_id in window_ids:
return ""
- return Gamescope.get_window_name(win_id)
+ var xwayland := gamescope.get_xwayland_by_name(display)
+ if not xwayland:
+ return ""
+ return xwayland.get_window_name(win_id)
## Attempt to discover the window ID from the PID of the given application
func get_window_id_from_pid() -> int:
- var display_type := Gamescope.get_display_type(display)
- return Gamescope.get_window_id(pid, display_type)
+ var xwayland := gamescope.get_xwayland_by_name(display)
+ if not xwayland:
+ return -1
+ var windows := xwayland.get_windows_for_pid(pid)
+ if windows.is_empty():
+ return -1
+ return windows[0]
## Attempt to discover all window IDs from the PID of the given application and
## the PIDs of all processes in the same process group.
func get_all_window_ids() -> PackedInt32Array:
var app_name := launch_item.name
- var display_type := Gamescope.get_display_type(display)
var window_ids := PackedInt32Array()
var pids := get_child_pids()
+ var xwayland := gamescope.get_xwayland_by_name(display)
pids.append(pid)
logger.trace(app_name + " found related PIDs: " + str(pids))
for process_id in pids:
- var windows := Gamescope.get_window_ids(process_id, display_type)
+ var windows := xwayland.get_windows_for_pid(process_id)
for window in windows:
if window < 0:
continue
@@ -287,7 +295,10 @@ func can_focus() -> bool:
func is_focused() -> bool:
if not can_focus():
return false
- var focused_window := Gamescope.get_focused_window()
+ var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY)
+ if not xwayland_primary:
+ return false
+ var focused_window := xwayland_primary.focused_window
return window_id == focused_window or focused_window in window_ids
@@ -295,7 +306,10 @@ func is_focused() -> bool:
func grab_focus() -> void:
if not can_focus():
return
- Gamescope.set_baselayer_window(window_id)
+ var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY)
+ if not xwayland_primary:
+ return
+ xwayland_primary.set_baselayer_window(window_id)
focused = true
@@ -305,9 +319,14 @@ func switch_window(win_id: int, focus: bool = true) -> int:
# Error if the window does not belong to the running app
if not win_id in window_ids:
return ERR_DOES_NOT_EXIST
-
+
+ # Get the primary XWayland instance
+ var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY)
+ if not xwayland_primary:
+ return ERR_UNAVAILABLE
+
# Check if this app is a focusable window.
- if not win_id in Gamescope.get_focusable_windows():
+ if not win_id in xwayland_primary.focusable_windows:
return ERR_UNAVAILABLE
# Update the window ID and optionally grab focus
@@ -332,6 +351,11 @@ func _ensure_app_id() -> void:
if is_steam_app() or not is_ogui_managed:
return
+ # Get the xwayland instance this app is running on
+ var xwayland := gamescope.get_xwayland_by_name(display)
+ if not xwayland:
+ return
+
# Get all windows associated with the running app
var possible_windows := window_ids.duplicate()
@@ -339,25 +363,32 @@ func _ensure_app_id() -> void:
# gamescope will make these windows available as focusable windows.
var app_name := launch_item.name
for window in possible_windows:
- var display_type := Gamescope.get_display_type(display)
- if Gamescope.has_app_id(window, display_type):
+ if xwayland.has_app_id(window):
continue
- Gamescope.set_app_id(window, window, display_type)
+ xwayland.set_app_id(window, window)
## Returns whether or not the window id of the running app needs to be discovered
func needs_window_id() -> bool:
+ var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY)
+ if not xwayland_primary:
+ return false
+
if window_id <= 0:
logger.trace(launch_item.name + " has a bad window ID: " + str(window_id))
return true
- var focusable_windows := Gamescope.get_focusable_windows()
+ var focusable_windows := xwayland_primary.focusable_windows
if not window_id in focusable_windows:
logger.trace(str(window_id) + " is not in the list of focusable windows")
return true
+ var xwayland := gamescope.get_xwayland_by_name(display)
+ if not xwayland:
+ return false
+
# Check if the current window ID exists in the list of open windows
- var root_window := Gamescope.get_root_window_id(Gamescope.XWAYLAND.GAME)
- var all_windows := Gamescope.get_all_windows(root_window, Gamescope.XWAYLAND.GAME)
+ var root_window := xwayland.root_window_id
+ var all_windows := xwayland.get_all_windows(root_window)
if not window_id in all_windows:
logger.trace(str(window_id) + " is not in the list of all windows")
return true
@@ -365,12 +396,11 @@ func needs_window_id() -> bool:
# If this is a Steam app, the only acceptable window will have its STEAM_GAME
# property set.
if is_steam_app():
- var display_type := Gamescope.get_display_type(display)
var steam_app_id := get_meta("steam_app_id") as int
- if not Gamescope.has_app_id(window_id, display_type):
+ if not xwayland.has_app_id(window_id):
logger.trace(str(window_id) + " does not have an app ID already set by Steam")
return true
- if Gamescope.get_app_id(window_id) != steam_app_id:
+ if xwayland.get_app_id(window_id) != steam_app_id:
logger.trace(str(window_id) + " has an app ID but it does not match " + str(steam_app_id))
return true
@@ -392,9 +422,14 @@ func _discover_window_id() -> int:
# Get all windows associated with the running app
var possible_windows := window_ids.duplicate()
-
+
+ # Get the primary XWayland instance
+ var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY)
+ if not xwayland_primary:
+ return -1
+
# Look for the app window in the list of focusable windows
- var focusable := Gamescope.get_focusable_windows()
+ var focusable := xwayland_primary.focusable_windows
for window in possible_windows:
if window in focusable:
return window
diff --git a/core/systems/performance/performance_manager.gd b/core/systems/performance/performance_manager.gd
index b7166f76..96f8cff2 100644
--- a/core/systems/performance/performance_manager.gd
+++ b/core/systems/performance/performance_manager.gd
@@ -11,7 +11,7 @@ signal profile_loaded(profile: PerformanceProfile)
signal profile_saved(profile: PerformanceProfile, path: String)
const USER_PROFILES := "user://data/performance/profiles"
-const DOCKED_STATES := [PowerManager.DEVICE_STATE.CHARGING, PowerManager.DEVICE_STATE.FULLY_CHARGED, PowerManager.DEVICE_STATE.PENDING_CHARGE]
+const DOCKED_STATES := [UPowerDevice.STATE_CHARGING, UPowerDevice.STATE_FULLY_CHARGED, UPowerDevice.STATE_PENDING_CHARGE]
## Performance profiles are separated into these states, so users can have different
## performance depending on whether or not they are plugged in to an external power
@@ -22,11 +22,12 @@ enum PROFILE_STATE {
}
var _hardware_manager := load("res://core/systems/hardware/hardware_manager.tres") as HardwareManager
-var _power_manager := load("res://core/systems/power/power_manager.tres") as PowerManager
+var _power_manager := load("res://core/systems/power/power_manager.tres") as UPowerInstance
var _settings_manager := load("res://core/global/settings_manager.tres") as SettingsManager
-var _power_station := load("res://core/systems/performance/power_station.tres") as PowerStation
+var _power_station := load("res://core/systems/performance/power_station.tres") as PowerStationInstance
var _launch_manager := load("res://core/global/launch_manager.tres") as LaunchManager
+var display_device := _power_manager.get_display_device()
var current_profile: PerformanceProfile
var current_profile_state: PROFILE_STATE # docked or undocked
var logger := Log.get_logger("PerformanceManager", Log.LEVEL.INFO)
@@ -39,15 +40,13 @@ func _init() -> void:
# Connect to battery state changes to switch between "docked" and "undocked"
# performance profiles.
- var batteries := _power_manager.get_devices_by_type(PowerManager.DEVICE_TYPE.BATTERY)
- if batteries.size() > 1:
- logger.warn("You somehow have more than one battery. We don't know what to do with that.")
- if batteries.size() > 0:
- var battery := batteries[0]
- battery.updated.connect(_on_battery_updated.bind(battery))
-
+ if display_device:
+ display_device.updated.connect(_on_battery_updated.bind(display_device))
+
+ # TODO: Listen for signals if PowerStation starts/stops
+
# Do nothing if PowerStation is not detected
- if not _power_station.supports_power_station():
+ if not _power_station.is_running():
return
# Load and apply the default profile
@@ -109,14 +108,14 @@ func create_profile(library_item: LibraryLaunchItem = null) -> PerformanceProfil
profile.cpu_smt_enabled = _power_station.cpu.smt_enabled
# Detect all GPU cards
- var cards: Array[PowerStation.GPUCard] = []
+ var cards: Array[GpuCard] = []
if _power_station.gpu:
cards = _power_station.gpu.get_cards()
# Configure GPU settings
# TODO: Support multiple GPUs?
for card in cards:
- if card.class_type != "integrated":
+ if card.class != "integrated":
continue
profile.tdp_current = card.tdp
@@ -161,7 +160,7 @@ func load_or_create_profile(profile_path: String, library_item: LibraryLaunchIte
## Applies the given performance profile to the system
func apply_profile(profile: PerformanceProfile) -> void:
- if not _power_station.supports_power_station():
+ if not _power_station.is_running():
logger.info("Unable to apply performance profile. PowerStation not detected.")
return
@@ -178,14 +177,14 @@ func apply_profile(profile: PerformanceProfile) -> void:
_power_station.cpu.cores_enabled = profile.cpu_core_count_current
# Detect all GPU cards
- var cards: Array[PowerStation.GPUCard] = []
+ var cards: Array[GpuCard] = []
if _power_station.gpu:
cards = _power_station.gpu.get_cards()
# Configure GPU settings
# TODO: Support mutliple GPUs?
for card in cards:
- if card.class_type != "integrated":
+ if card.class != "integrated":
continue
logger.debug("Applying GPU performance settings from profile")
if card.manual_clock != profile.gpu_manual_enabled:
@@ -242,26 +241,22 @@ func apply_and_save_profile(profile: PerformanceProfile) -> void:
## Returns the current profile state. I.e. whether or not the "docked" or "undocked"
## performance profiles should be used.
func get_profile_state() -> PROFILE_STATE:
- var batteries := _power_manager.get_devices_by_type(PowerManager.DEVICE_TYPE.BATTERY)
- if batteries.size() > 1:
- logger.warn("You somehow have more than one battery. We don't know what to do with that.")
- if batteries.size() > 0:
- var battery := batteries[0]
- return get_profile_state_from_battery(battery)
+ if display_device:
+ return get_profile_state_from_battery(display_device)
return PROFILE_STATE.DOCKED
## Returns the current profile state. I.e. whether or not the "docked" or "undocked"
## performance profiles should be used.
-func get_profile_state_from_battery(battery: PowerManager.Device) -> PROFILE_STATE:
+func get_profile_state_from_battery(battery: UPowerDevice) -> PROFILE_STATE:
if battery.state not in DOCKED_STATES:
return PROFILE_STATE.UNDOCKED
return PROFILE_STATE.DOCKED
## Called whenever a battery is updated
-func _on_battery_updated(battery: PowerManager.Device) -> void:
+func _on_battery_updated(battery: UPowerDevice) -> void:
# Get the current profile state to see if we need to load the docked or
# undocked profile.
var profile_state := get_profile_state_from_battery(battery)
diff --git a/core/systems/performance/power_station.gd b/core/systems/performance/power_station.gd
index 2c912779..393c8695 100644
--- a/core/systems/performance/power_station.gd
+++ b/core/systems/performance/power_station.gd
@@ -1,4 +1,5 @@
-extends Resource
+@icon("res://assets/editor-icons/powerstation.svg")
+extends Node
class_name PowerStation
## Proxy interface to PowerStation over DBus
@@ -6,404 +7,20 @@ class_name PowerStation
## Provides wrapper classes and methods for interacting with PowerStation over
## DBus to control CPU and GPU performance.
-const POWERSTATION_BUS := "org.shadowblip.PowerStation"
-const PERFORMANCE_PATH := "/org/shadowblip/Performance"
-const CPU_PATH := "/org/shadowblip/Performance/CPU"
-const GPU_PATH := "/org/shadowblip/Performance/GPU"
-const IFACE_CPU := "org.shadowblip.CPU"
-const IFACE_CPU_CORE := "org.shadowblip.CPU.Core"
-const IFACE_GPU := "org.shadowblip.GPU"
-const IFACE_GPU_CARD := "org.shadowblip.GPU.Card"
-const IFACE_GPU_TDP := "org.shadowblip.GPU.Card.TDP"
-const IFACE_GPU_CONNECTOR := "org.shadowblip.GPU.Card.Connector"
+@export var instance: PowerStationInstance = load("res://core/systems/performance/power_station.tres") as PowerStationInstance
+# Keep a reference to instances so they are not cleaned up automatically
+var _cpu: Cpu
+var _gpu: Gpu
var logger := Log.get_logger("PowerStation")
-var dbus := load("res://core/global/dbus_system.tres") as DBusManager
-var cpu := CPUBus.new(dbus.create_proxy(POWERSTATION_BUS, CPU_PATH))
-var gpu := GPUBus.new(dbus.create_proxy(POWERSTATION_BUS, GPU_PATH))
-## Returns true if PowerStation can be used on this system
-func supports_power_station() -> bool:
- return dbus.bus_exists(POWERSTATION_BUS)
+func _ready() -> void:
+ _cpu = instance.get_cpu()
+ _gpu = instance.get_gpu()
-## CPUBus provides a DBus connection to the CPU bus for CPU controls
-class CPUBus extends Resource:
- signal properties_changed
- signal updated
- var _proxy: DBusManager.Proxy
-
- func _init(proxy: DBusManager.Proxy) -> void:
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- ## Returns a list of DBus object paths to every detected core
- func enumerate_cores() -> PackedStringArray:
- var result := _proxy.call_method(IFACE_CPU, "EnumerateCores")
- if not result:
- return []
- var args := result.get_args()
- if args.size() != 1:
- return []
- if not args[0] is Array:
- return []
- return args[0]
-
- ## Returns true if the CPU has the given feature
- func has_feature(feature: String) -> bool:
- var result := _proxy.call_method(IFACE_CPU, "HasFeature", [feature], "s")
- if not result:
- return false
- var args := result.get_args()
- if args.size() != 1:
- return false
- if not args[0] is bool:
- return false
- return args[0]
-
- var boost_enabled: bool:
- set(v):
- _proxy.set_property(IFACE_CPU, "BoostEnabled", v)
- get:
- var property = _proxy.get_property(IFACE_CPU, "BoostEnabled")
- if not property is bool:
- return false
- return property
-
- var cores_count: int:
- get:
- var property = _proxy.get_property(IFACE_CPU, "CoresCount")
- if not property is int:
- return -1
- return property
-
- var cores_enabled: int:
- set(v):
- _proxy.set_property(IFACE_CPU, "CoresEnabled", DBus.uint32(v))
- get:
- var property = _proxy.get_property(IFACE_CPU, "CoresEnabled")
- if not property is int:
- return -1
- return property
-
- var features: PackedStringArray:
- get:
- var property = _proxy.get_property(IFACE_CPU, "Features")
- if not property is Array:
- return []
- return property
-
- var smt_enabled: bool:
- set(v):
- _proxy.set_property(IFACE_CPU, "SmtEnabled", v)
- get:
- var property = _proxy.get_property(IFACE_CPU, "SmtEnabled")
- if not property is bool:
- return false
- return property
-
-
-## Provides an interface to enumerate all detected GPU cards
-class GPUBus extends Resource:
- signal properties_changed
- signal updated
- var _proxy: DBusManager.Proxy
-
- func _init(proxy: DBusManager.Proxy) -> void:
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- ## Returns a list of DBus object paths to every detected GPU card
- func enumerate_cards() -> PackedStringArray:
- var result := _proxy.call_method(IFACE_GPU, "EnumerateCards")
- if not result:
- return []
- var args := result.get_args()
- if args.size() != 1:
- return []
- if not args[0] is Array:
- return []
- return args[0]
-
- ## Returns a list of all GPUCard objects
- func get_cards() -> Array[GPUCard]:
- var dbus := load("res://core/global/dbus_system.tres") as DBusManager
- var cards: Array[GPUCard] = []
- var paths := self.enumerate_cards()
- for path in paths:
- var card = GPUCard.new(dbus.create_proxy(POWERSTATION_BUS, path))
- cards.append(card)
-
- return cards
-
-
-## GPUCard provides a DBus connection to the GPU for GPU control
-class GPUCard extends Resource:
- signal properties_changed
- signal updated
- var _proxy: DBusManager.Proxy
-
- func _init(proxy: DBusManager.Proxy) -> void:
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- ## Returns true if the card supports TDP control
- func supports_tdp() -> bool:
- return self.tdp > 1
-
- ## Returns a list of DBus object paths to every detected GPU connector
- func enumerate_connectors() -> PackedStringArray:
- var result := _proxy.call_method(IFACE_GPU_CARD, "EnumerateConnectors")
- if not result:
- return []
- var args := result.get_args()
- if args.size() != 1:
- return []
- if not args[0] is Array:
- return []
- return args[0]
-
- ## Returns a list of all GPUConnector objects
- func get_connectors() -> Array[GPUConnector]:
- var dbus := load("res://core/global/dbus_system.tres") as DBusManager
- var connectors: Array[GPUConnector] = []
- var paths := self.enumerate_connectors()
- for path in paths:
- var card = GPUConnector.new(dbus.create_proxy(POWERSTATION_BUS, path))
- connectors.append(card)
-
- return connectors
-
- var class_type: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "Class")
- if not property is String:
- return ""
- return property
-
- var class_id: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "ClassId")
- if not property is String:
- return ""
- return property
-
- var clock_limit_mhz_max: float:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "ClockLimitMhzMax")
- if not property is float:
- return -1
- return property
-
- var clock_limit_mhz_min: float:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "ClockLimitMhzMin")
- if not property is float:
- return -1
- return property
-
- var clock_value_mhz_max: float:
- set(v):
- _proxy.set_property(IFACE_GPU_CARD, "ClockValueMhzMax", float(v))
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "ClockValueMhzMax")
- if not property is float:
- return -1
- return property
-
- var clock_value_mhz_min: float:
- set(v):
- _proxy.set_property(IFACE_GPU_CARD, "ClockValueMhzMin", float(v))
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "ClockValueMhzMin")
- if not property is float:
- return -1
- return property
-
- var device: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "Device")
- if not property is String:
- return ""
- return property
-
- var device_id: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "DeviceId")
- if not property is String:
- return ""
- return property
-
- var manual_clock: bool:
- set(v):
- _proxy.set_property(IFACE_GPU_CARD, "ManualClock", v)
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "ManualClock")
- if not property is bool:
- return false
- return property
-
- var name: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "Name")
- if not property is String:
- return ""
- return property
-
- var path: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "Path")
- if not property is String:
- return ""
- return property
-
- var revision_id: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "RevisionId")
- if not property is String:
- return ""
- return property
-
- var subdevice: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "Subdevice")
- if not property is String:
- return ""
- return property
-
- var subdevice_id: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "SubdeviceId")
- if not property is String:
- return ""
- return property
-
- var subvendor_id: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "SubvendorId")
- if not property is String:
- return ""
- return property
-
- var vendor: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "Vendor")
- if not property is String:
- return ""
- return property
-
- var vendor_id: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CARD, "VendorId")
- if not property is String:
- return ""
- return property
-
- var boost: float:
- set(v):
- _proxy.set_property(IFACE_GPU_TDP, "Boost", float(v))
- get:
- var property = _proxy.get_property(IFACE_GPU_TDP, "Boost")
- if not property is float:
- return -1
- return property
-
- var power_profile: String:
- set(v):
- _proxy.set_property(IFACE_GPU_TDP, "PowerProfile", v)
- get:
- var property = _proxy.get_property(IFACE_GPU_TDP, "PowerProfile")
- if not property is String:
- return ""
- return property
-
- var tdp: float:
- set(v):
- _proxy.set_property(IFACE_GPU_TDP, "TDP", float(v))
- get:
- var property = _proxy.get_property(IFACE_GPU_TDP, "TDP")
- if not property is float:
- return -1
- return property
-
- var thermal_throttle_limit_c: float:
- set(v):
- _proxy.set_property(IFACE_GPU_TDP, "ThermalThrottleLimitC", float(v))
- get:
- var property = _proxy.get_property(IFACE_GPU_TDP, "ThermalThrottleLimitC")
- if not property is float:
- return -1
- return property
-
-
-## GPUConnector provides a DBus connection to a GPU connector
-class GPUConnector extends Resource:
- signal properties_changed
- signal updated
- var _proxy: DBusManager.Proxy
-
- func _init(proxy: DBusManager.Proxy) -> void:
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- var dpms: bool:
- get:
- var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "DPMS")
- if not property is bool:
- return false
- return property
-
- var enabled: bool:
- get:
- var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "Enabled")
- if not property is bool:
- return false
- return property
-
- var id: int:
- get:
- var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "Id")
- if not property is int:
- return -1
- return property
-
- var modes: PackedStringArray:
- get:
- var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "Modes")
- if not property is Array:
- return []
- return property
-
- var name: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "Name")
- if not property is String:
- return ""
- return property
-
- var path: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "Path")
- if not property is String:
- return ""
- return property
-
- var status: String:
- get:
- var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "Status")
- if not property is String:
- return ""
- return property
+func _process(_delta: float) -> void:
+ if not instance:
+ return
+ instance.process()
diff --git a/core/systems/performance/power_station.tres b/core/systems/performance/power_station.tres
index 20b69611..14a60180 100644
--- a/core/systems/performance/power_station.tres
+++ b/core/systems/performance/power_station.tres
@@ -1,6 +1,3 @@
-[gd_resource type="Resource" script_class="PowerStation" load_steps=2 format=3 uid="uid://cdyeux8mdf6l6"]
-
-[ext_resource type="Script" path="res://core/systems/performance/power_station.gd" id="1_bdrpq"]
+[gd_resource type="PowerStationInstance" format=3 uid="uid://c2mmrnh3rcs58"]
[resource]
-script = ExtResource("1_bdrpq")
diff --git a/core/systems/performance/power_station_test.gd b/core/systems/performance/power_station_test.gd
index d20f159e..1444dcac 100644
--- a/core/systems/performance/power_station_test.gd
+++ b/core/systems/performance/power_station_test.gd
@@ -1,39 +1,43 @@
extends GutTest
-var power_station := load("res://core/systems/performance/power_station.tres") as PowerStation
-
func test_cpu() -> void:
- if not power_station.supports_power_station():
+ var powerstation := PowerStation.new()
+ powerstation.instance = load("res://core/systems/performance/power_station.tres") as PowerStationInstance
+ add_child_autoqfree(powerstation)
+
+ if not powerstation.instance.is_running():
pass_test("PowerStation is not running, skipping")
return
- var cpu := power_station.cpu
+ var cpu := powerstation.instance.get_cpu()
assert_not_null(cpu, "should return CPU instance")
# Test getting total number of cpu cores
- var num_cores = cpu.cores_count
+ var num_cores := cpu.cores_count
+ gut.p("Total CPU cores: " + str(num_cores))
assert_ne(num_cores, -1, "should have returned total core count")
- # Test that CPU cores get set
- cpu.cores_enabled = num_cores - 1
- assert_eq(cpu.cores_enabled, num_cores - 1, "should have disabled cores")
-
- # Set the cores back
- cpu.cores_enabled = num_cores
- assert_eq(cpu.cores_enabled, num_cores, "should have re-enabled all cores")
-
- # Test enumerating cores
- var enumerated := cpu.enumerate_cores()
- assert_gt(enumerated.size(), 0, "should return at least 1 cpu core path")
-
# Test getting features
- var features = cpu.features
+ var features := cpu.features
+ gut.p("Found CPU features: " + str(features))
assert_gt(features.size(), 1, "should return CPU features")
if features.size() > 1:
var feature := features[0] as String
assert_true(cpu.has_feature(feature), "should have CPU feature")
assert_false(cpu.has_feature("IdontXsist!"), "should not have CPU feature")
+ # Test that CPU cores get set
+ gut.p("Disabling one CPU core")
+ cpu.cores_enabled = num_cores - 1
+ gut.p("Cores enabled: " + str(cpu.cores_enabled))
+ assert_eq(cpu.cores_enabled, num_cores - 1, "should have disabled cores")
+
+ # Set the cores back
+ gut.p("Re-enabling CPU core")
+ cpu.cores_enabled = num_cores
+ gut.p("Cores enabled: " + str(cpu.cores_enabled))
+ assert_eq(cpu.cores_enabled, num_cores, "should have re-enabled all cores")
+
# Test setting SMT
cpu.smt_enabled = false
assert_false(cpu.smt_enabled, "should have disabled SMT")
@@ -44,24 +48,32 @@ func test_cpu() -> void:
# Test setting boost
cpu.boost_enabled = false
- assert_false(cpu.boost_enabled, "should have disabled boost")
+ #assert_false(cpu.boost_enabled, "should have disabled boost")
await wait_frames(1, "wait for change")
cpu.boost_enabled = true
- assert_true(cpu.boost_enabled, "should have enabled boost")
+ #assert_true(cpu.boost_enabled, "should have enabled boost")
await wait_frames(1, "wait for change")
+ # Test enumerating cores
+ var cores := cpu.get_cores()
+ assert_gt(cores.size(), 0, "should return at least 1 cpu core")
+ for core in cores:
+ gut.p("CPU Core: " + str(core.number))
+ gut.p(" ID: " + str(core.core_id))
+ gut.p(" Online: " + str(core.online))
+
func test_gpu() -> void:
- if not power_station.supports_power_station():
+ var powerstation := PowerStation.new()
+ powerstation.instance = load("res://core/systems/performance/power_station.tres") as PowerStationInstance
+ add_child_autoqfree(powerstation)
+
+ if not powerstation.instance.is_running():
pass_test("PowerStation is not running, skipping")
return
- var gpu := power_station.gpu
+ var gpu := powerstation.instance.get_gpu()
assert_not_null(gpu, "should return GPU instance")
- # Test enumerating cards
- var card_paths := gpu.enumerate_cards()
- #assert_gt(cards.size(), 0, "should return at least 1 gpu")
-
# Test all GPU card methods
var cards := gpu.get_cards()
for card in cards:
diff --git a/core/systems/power/power_manager.gd b/core/systems/power/power_manager.gd
index 371ff69a..f300d3f7 100644
--- a/core/systems/power/power_manager.gd
+++ b/core/systems/power/power_manager.gd
@@ -1,352 +1,26 @@
-extends Resource
+extends Node
class_name PowerManager
-const POWER_BUS := "org.freedesktop.UPower"
-const UPOWER_PATH := "/org/freedesktop/UPower"
-const POWER_PREFIX := "/org/freedesktop/UPower/devices"
-const IFACE_UPOWER := "org.freedesktop.UPower"
-const IFACE_DEVICE := "org.freedesktop.UPower.Device"
+## Manages power settings.
+##
+## The [PowerManager] class is responsible for loading a [UPowerInstance] and
+## calling its 'process()' method each frame.
-enum DEVICE_TYPE {
- UNKNOWN,
- LINE_POWER,
- BATTERY,
- UPS,
- MONITOR,
- MOUSE,
- KEYBOARD,
- PDA,
- PHONE,
-}
+@export var instance: UPowerInstance = load("res://core/systems/power/power_manager.tres") as UPowerInstance
-enum DEVICE_STATE {
- UNKNOWN,
- CHARGING,
- DISCHARGING,
- EMPTY,
- FULLY_CHARGED,
- PENDING_CHARGE,
- PENDING_DISCHARGE,
-}
+# Keep a reference to device instances so they are not cleaned up automatically
+var _devices: Array[UPowerDevice]
+var logger := Log.get_logger("PowerManager")
-enum DEVICE_WARNING_LEVEL {
- UNKNOWN,
- NONE,
- DISCHARGING,
- LOW,
- CRITICAL,
- ACTION,
-}
-enum DEVICE_BATTERY_LEVEL {
- UNKNOWN,
- NONE,
- LOW,
- CRITICAL,
- NORMAL,
- HIGH,
- FULL,Z
-}
+func _ready() -> void:
+ var display_device := instance.get_display_device()
+ _devices.push_back(display_device)
+ if _devices.is_empty():
+ logger.warn("UPower not detected.")
-enum DEVICE_TECHNOLOGY {
- UNKNOWN,
- LITHIUM_ION,
- LITHIUM_POLYMER,
- LITHIUM_IRON_PHOSPHATE,
- LEAD_ACID,
- NICKLE_CADMIUM,
- NICKLE_METAL_HYDRIDE,
-}
-var dbus := load("res://core/global/dbus_system.tres") as DBusManager
-var upower := UPower.new(dbus.create_proxy(POWER_BUS, UPOWER_PATH))
-
-
-func get_devices() -> Array[Device]:
- var devices: Array[Device] = []
- var device_paths := upower.enumerate_devices()
-
- # Loop through all objects on the bus
- for path in device_paths:
- # Create a power Device from this object
- var proxy := dbus.create_proxy(POWER_BUS, path)
- var device := Device.new(proxy)
-
- devices.append(device)
-
- return devices
-
-
-func get_devices_by_type(type: DEVICE_TYPE) -> Array[Device]:
- var all_devices := get_devices()
- var type_devices : Array[Device]
- for device in all_devices:
- if device.type == type:
- type_devices.append(device)
- return type_devices
-
-
-func get_device(device_name: String) -> Device:
- var device_path := "/".join([POWER_PREFIX, device_name])
- var proxy := dbus.create_proxy(POWER_BUS, device_path)
- var device := Device.new(proxy)
-
- return device
-
-
-## Returns true if bluetooth can be used on this system
-func supports_power() -> bool:
- return dbus.bus_exists(POWER_BUS)
-
-
-class UPower extends Resource:
- signal updated
- var _proxy: DBusManager.Proxy
- var on_battery: bool:
- get:
- var property = _proxy.get_property(IFACE_UPOWER, "OnBattery")
- if not property is bool:
- return false
- return property
-
- func _init(proxy: DBusManager.Proxy) -> void:
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- func enumerate_devices() -> Array:
- var result := _proxy.call_method(IFACE_UPOWER, "EnumerateDevices")
- var args := result.get_args()
- if args.size() != 1:
- return []
- if not args[0] is Array:
- return []
- return args[0]
-
-
-class Device extends Resource:
- signal porperties_changed
- signal updated
- var _proxy: DBusManager.Proxy
-
- func _init(proxy: DBusManager.Proxy) -> void:
- _proxy = proxy
- _proxy.properties_changed.connect(_on_properties_changed)
-
- func _on_properties_changed(iface: String, props: Dictionary) -> void:
- updated.emit()
-
- func refresh() -> void:
- _proxy.call_method(IFACE_DEVICE, "Refresh")
-
- var native_path: String:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "NativePath")
- if not property is String:
- return ""
- return property
-
- var vendor: String:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "Vendor")
- if not property is String:
- return ""
- return property
-
- var model: String:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "Model")
- if not property is String:
- return ""
- return property
-
- var serial: String:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "Serial")
- if not property is String:
- return ""
- return property
-
- var update_time: int:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "UpdateTime")
- if not property is int:
- return 0
- return property
-
- var type: int:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "Type")
- if not property is int:
- return 0
- return property
-
- var power_supply: bool:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "PowerSupply")
- if not property is bool:
- return false
- return property
-
- var has_history: bool:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "HasHistory")
- if not property is bool:
- return false
- return property
-
- var has_statistics: bool:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "HasStatisics")
- if not property is bool:
- return false
- return property
-
- var online: bool:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "Online")
- if not property is bool:
- return false
- return property
-
- var energy: float:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "Energy")
- if not property is float:
- return 0.0
- return property
-
- var energy_empty: float:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "EnergyEmpty")
- if not property is float:
- return 0.0
- return property
-
- var energy_full: float:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "EnergyFull")
- if not property is float:
- return 0.0
- return property
-
- var energy_full_design: float:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "EnergyFullDesign")
- if not property is float:
- return 0.0
- return property
-
- var energy_rate: float:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "EnergyRate")
- if not property is float:
- return 0.0
- return property
-
- var voltage: float:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "Voltage")
- if not property is float:
- return 0.0
- return property
-
- var charge_cycles: int:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "ChargeCycles")
- if not property is int:
- return 0
- return property
-
- var luminosity: float:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "Luminosity")
- if not property is float:
- return 0.0
- return property
-
- var time_to_empty: int:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "TimeToEmpty")
- if not property is int:
- return 0
- return property
-
- var time_to_full: int:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "TimeToFull")
- if not property is int:
- return 0
- return property
-
- var percentage: float:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "Percentage")
- if not property is float:
- return 0.0
- return property
-
- var temperature: float:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "Temperature")
- if not property is float:
- return 0.0
- return property
-
- var is_present: bool:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "IsPresent")
- if not property is bool:
- return false
- return property
-
- var state: int:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "State")
- if not property is int:
- return 0
- return property
-
- var is_rechargable: bool:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "IsRechargeable")
- if not property is bool:
- return false
- return property
-
- var capacity: float:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "Capacity")
- if not property is float:
- return 0.0
- return property
-
- var technology: int:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "Technology")
- if not property is int:
- return 0
- return property
-
- var warning_level: int:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "WarningLevel")
- if not property is int:
- return 0
- return property
-
- var battery_level: int:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "BatteryLevel")
- if not property is int:
- return 0
- return property
-
- var icon_name: String:
- get:
- var property = _proxy.get_property(IFACE_DEVICE, "IconName")
- if not property is String:
- return ""
- return property
+func _process(_delta: float) -> void:
+ if not instance:
+ return
+ instance.process()
diff --git a/core/systems/power/power_manager.tres b/core/systems/power/power_manager.tres
index a13360ea..79b53674 100644
--- a/core/systems/power/power_manager.tres
+++ b/core/systems/power/power_manager.tres
@@ -1,6 +1,3 @@
-[gd_resource type="Resource" script_class="PowerManager" load_steps=2 format=3 uid="uid://dxa4156di7yk2"]
-
-[ext_resource type="Script" path="res://core/systems/power/power_manager.gd" id="1_57v16"]
+[gd_resource type="UPowerInstance" format=3 uid="uid://pgiv8yxro3n8"]
[resource]
-script = ExtResource("1_57v16")
diff --git a/core/systems/power/power_manager_test.gd b/core/systems/power/power_manager_test.gd
new file mode 100644
index 00000000..76ef3a8c
--- /dev/null
+++ b/core/systems/power/power_manager_test.gd
@@ -0,0 +1,20 @@
+extends GutTest
+
+
+func test_upower() -> void:
+ var power_manager := PowerManager.new()
+ power_manager.instance = load("res://core/systems/power/power_manager.tres")
+ add_child_autoqfree(power_manager)
+
+ if not power_manager.instance.is_running():
+ gut.p("InputPlumber is not running. Skipping tests.")
+ return
+
+ # Get the display device
+ var display_device := power_manager.instance.get_display_device()
+ assert_eq(display_device.dbus_path, "/org/freedesktop/UPower/devices/DisplayDevice")
+ gut.p("DBus Path: " + str(display_device.dbus_path))
+ gut.p("Battery level: " + str(display_device.battery_level))
+ gut.p("Icon name: " + str(display_device.icon_name))
+ gut.p("Percentage: " + str(display_device.percentage))
+ gut.p("State: " + str(display_device.state))
diff --git a/core/systems/power/power_saver.gd b/core/systems/power/power_saver.gd
index a3a5a91b..ba6dbdc2 100644
--- a/core/systems/power/power_saver.gd
+++ b/core/systems/power/power_saver.gd
@@ -6,7 +6,7 @@ class_name PowerSaver
var display := load("res://core/global/display_manager.tres") as DisplayManager
var settings := load("res://core/global/settings_manager.tres") as SettingsManager
-var power_manager := load("res://core/systems/power/power_manager.tres") as PowerManager
+var power_manager := load("res://core/systems/power/power_manager.tres") as UPowerInstance
const MINUTE := 60
@@ -27,14 +27,13 @@ var dimmed := false
var prev_brightness := {}
var supports_brightness := display.supports_brightness()
var has_battery := false
-var batteries : Array[PowerManager.Device]
+var display_device := power_manager.get_display_device()
var logger := Log.get_logger("PowerSaver")
func _ready() -> void:
- batteries = power_manager.get_devices_by_type(PowerManager.DEVICE_TYPE.BATTERY)
- if batteries.size() > 0:
- has_battery = true
+ if display_device:
+ has_battery = display_device.is_present
if dim_screen_enabled and supports_brightness:
dim_timer.timeout.connect(_on_dim_timer_timeout)
@@ -46,9 +45,9 @@ func _ready() -> void:
func _on_dim_timer_timeout() -> void:
# If dimming is disabled when charging, check the battery state
- if has_battery and not dim_when_charging:
- var status: int = batteries[0].state
- if status in [PowerManager.DEVICE_STATE.CHARGING, PowerManager.DEVICE_STATE.FULLY_CHARGED]:
+ if has_battery and display_device and not dim_when_charging:
+ var status := display_device.state
+ if status in [UPowerDevice.STATE_CHARGING, UPowerDevice.STATE_FULLY_CHARGED]:
logger.debug("Not dimming because battery is charging")
return
if not has_battery and not dim_when_charging:
@@ -71,9 +70,9 @@ func _on_dim_timer_timeout() -> void:
func _on_suspend_timer_timeout() -> void:
# If suspend is disabled when charging, check the battery state
- if has_battery and not suspend_when_charging:
- var status: int = batteries[0].state
- if status in [PowerManager.DEVICE_STATE.CHARGING, PowerManager.DEVICE_STATE.FULLY_CHARGED]:
+ if has_battery and display_device and not suspend_when_charging:
+ var status := display_device.state
+ if status in [UPowerDevice.STATE_CHARGING, UPowerDevice.STATE_FULLY_CHARGED]:
logger.debug("Not suspending because battery is charging")
return
if not has_battery and not suspend_when_charging:
diff --git a/core/systems/threading/pty_test.gd b/core/systems/threading/pty_test.gd
new file mode 100644
index 00000000..6386fa7d
--- /dev/null
+++ b/core/systems/threading/pty_test.gd
@@ -0,0 +1,24 @@
+extends GutTest
+
+
+func test_pty() -> void:
+ var pty := Pty.new()
+ add_child_autoqfree(pty)
+
+ # Listen for output from the command
+ var on_line_written := func(line: String):
+ gut.p("Line: " + line)
+ if line.contains("Type something:"):
+ pty.write_line("Hello World")
+ pty.line_written.connect(on_line_written)
+
+ # Execute the command in the PTY
+ var result := pty.exec("bash", PackedStringArray(["-c", "read -p 'Type something: ' foo; echo 'You typed:' $foo"]))
+ assert_eq(result, OK)
+
+ # Wait for the command to exit
+ var exit_code = await pty.finished
+
+ gut.p("Command finished with exit code: " + str(exit_code))
+
+ await wait_seconds(5, "Waiting")
diff --git a/core/ui/card_ui/card_ui.gd b/core/ui/card_ui/card_ui.gd
index 44327f30..a1d62c2f 100644
--- a/core/ui/card_ui/card_ui.gd
+++ b/core/ui/card_ui/card_ui.gd
@@ -1,10 +1,10 @@
extends Control
var platform := load("res://core/global/platform.tres") as Platform
-var gamescope := load("res://core/global/gamescope.tres") as Gamescope
+var gamescope := load("res://core/systems/gamescope/gamescope.tres") as GamescopeInstance
var library_manager := load("res://core/global/library_manager.tres") as LibraryManager
var settings_manager := load("res://core/global/settings_manager.tres") as SettingsManager
-var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber
+var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance
var state_machine := (
preload("res://assets/state/state_machines/global_state_machine.tres") as StateMachine
@@ -20,7 +20,9 @@ var osk_state := preload("res://assets/state/states/osk.tres") as State
var power_state := preload("res://assets/state/states/power_menu.tres") as State
var PID: int = OS.get_process_id()
-var overlay_window_id := gamescope.get_window_id(PID, gamescope.XWAYLAND.OGUI)
+var _xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY)
+var _xwayland_ogui := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_OGUI)
+var overlay_window_id := 0
@onready var panel := $%Panel as Panel
@onready var ui_container := $%MenuContent as MarginContainer
@@ -33,6 +35,12 @@ var overlay_window_id := gamescope.get_window_id(PID, gamescope.XWAYLAND.OGUI)
var logger = Log.get_logger("Main", Log.LEVEL.INFO)
func _init() -> void:
+ # Discover the window id of OpenGamepadUI
+ if _xwayland_ogui:
+ var ogui_windows := _xwayland_ogui.get_windows_for_pid(PID)
+ if not ogui_windows.is_empty():
+ overlay_window_id = ogui_windows[0]
+
# Tell gamescope that we're an overlay
if overlay_window_id <= 0:
logger.error("Unable to detect Window ID. Overlay is not going to work!")
@@ -47,10 +55,10 @@ func _setup(window_id: int) -> void:
return
# Pretend to be Steam
# Gamescope is hard-coded to look for appId 769
- if gamescope.set_main_app(window_id) != OK:
+ if _xwayland_primary.set_main_app(window_id) != OK:
logger.error("Unable to set STEAM_GAME atom!")
# Sets ourselves to the input focus
- if gamescope.set_input_focus(window_id, 1) != OK:
+ if _xwayland_primary.set_input_focus(window_id, 1) != OK:
logger.error("Unable to set STEAM_INPUT_FOCUS atom!")
@@ -112,18 +120,20 @@ func _ready() -> void:
library_manager.reload_library()
# Set the initial intercept mode
- input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.ALL)
- var on_device_changed := func(device: InputPlumber.CompositeDevice):
- var intercept_mode : InputPlumber.INTERCEPT_MODE = input_plumber.intercept_mode_current
- logger.debug("Setting intercept mode to: " + str(intercept_mode))
- input_plumber.set_intercept_mode_single(intercept_mode, device)
- input_plumber.composite_device_changed.connect(on_device_changed)
+ input_plumber.set_intercept_mode(InputPlumberInstance.INTERCEPT_MODE_ALL)
+ #var on_device_changed := func(device: CompositeDevice):
+ # var intercept_mode := input_plumber.intercept_mode
+ # logger.debug("Setting intercept mode to: " + str(intercept_mode))
+ # device.intercept_mode = intercept_mode
+ ## TODO: Do we still need this..?
+ #input_plumber.composite_device_changed.connect(on_device_changed)
# Set the theme if one was set
var theme_path := settings_manager.get_value("general", "theme", "res://assets/themes/card_ui-dracula.tres") as String
logger.debug("Setting theme to: " + theme_path)
var loaded_theme = load(theme_path)
if loaded_theme != null:
+ @warning_ignore("unsafe_call_argument")
set_theme(loaded_theme)
else:
logger.debug("Unable to load theme")
@@ -137,13 +147,13 @@ func _on_focus_changed(control: Control) -> void:
## Invoked when the in-game state was entered
func _on_game_state_entered(_from: State) -> void:
# Pass all gamepad input to the game
- input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.PASS)
+ input_plumber.set_intercept_mode(InputPlumberInstance.INTERCEPT_MODE_PASS)
# Turn off gamescope blur effect
- _set_blur(gamescope.BLUR_MODE.OFF)
+ _set_blur(GamescopeXWayland.BLUR_MODE_OFF)
# Set gamescope input focus to off so the user can interact with the game
- if gamescope.set_input_focus(overlay_window_id, 0) != OK:
+ if _xwayland_ogui and _xwayland_ogui.set_input_focus(overlay_window_id, 0) != OK:
logger.error("Unable to set STEAM_INPUT_FOCUS atom!")
# Ensure panel is invisible
@@ -160,7 +170,7 @@ func _on_game_state_entered(_from: State) -> void:
## Invoked when the in-game state is exited
func _on_game_state_exited(to: State) -> void:
# Intercept all gamepad input when not in-game
- input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.ALL)
+ input_plumber.set_intercept_mode(InputPlumberInstance.INTERCEPT_MODE_ALL)
# Revert back to the default gamepad profile
#gamepad_manager.set_gamepads_profile(null)
@@ -171,16 +181,18 @@ func _on_game_state_exited(to: State) -> void:
if current_popup == osk_state:
return
- if gamescope.set_input_focus(overlay_window_id, 1) != OK:
- logger.error("Unable to set STEAM_INPUT_FOCUS atom!")
+ if _xwayland_primary:
+ if _xwayland_primary.set_input_focus(overlay_window_id, 1) != OK:
+ logger.error("Unable to set STEAM_INPUT_FOCUS atom!")
# If the in-game state still exists in the stack, set the blur state.
if state_machine.has_state(in_game_state):
panel.visible = false
# Only blur if the focused GFX app is set
- var should_blur := settings_manager.get_value("display", "enable_overlay_blur", true) as bool
- if should_blur and gamescope.get_focused_app_gfx() != Gamescope.OVERLAY_GAME_ID:
- _set_blur(gamescope.BLUR_MODE.ALWAYS)
+ if _xwayland_primary:
+ var should_blur := settings_manager.get_value("display", "enable_overlay_blur", true) as bool
+ if should_blur and _xwayland_primary.get_focused_app_gfx() != gamescope.OVERLAY_GAME_ID:
+ _set_blur(GamescopeXWayland.BLUR_MODE_ALWAYS)
else:
_on_game_state_removed()
@@ -195,7 +207,7 @@ func _on_game_state_exited(to: State) -> void:
## Invoked when the in-game state is removed
func _on_game_state_removed() -> void:
# Turn off gamescope blur
- _set_blur(gamescope.BLUR_MODE.OFF)
+ _set_blur(GamescopeXWayland.BLUR_MODE_OFF)
# Un-hide the background panel
panel.visible = true
@@ -208,12 +220,12 @@ func _on_game_state_removed() -> void:
# Sets the blur mode in gamescope
-func _set_blur(mode: Gamescope.BLUR_MODE) -> void:
+func _set_blur(mode: int) -> void:
# Sometimes setting this may fail when Steam closes. Retry several times.
for try in range(10):
- if gamescope.set_blur_mode(mode) != OK:
- logger.warn("Unable to set blur mode atom!")
- var current := gamescope.get_blur_mode()
+ if _xwayland_primary:
+ _xwayland_primary.set_blur_mode(mode)
+ var current := _xwayland_primary.get_blur_mode()
if mode == current:
break
logger.warn("Retrying in " + str(try) + "ms")
@@ -255,7 +267,7 @@ func _input(event: InputEvent) -> void:
if not power_timer.is_stopped():
logger.info("Received suspend signal")
for connection in power_timer.timeout.get_connections():
- power_timer.timeout.disconnect(connection["callable"])
+ power_timer.timeout.disconnect(connection["callable"] as Callable)
power_timer.stop()
var output: Array = []
if OS.execute("systemctl", ["suspend"], output) != OK:
@@ -263,12 +275,12 @@ func _input(event: InputEvent) -> void:
# Removes specified child elements from the given Node.
-func _remove_children(remove_list: PackedStringArray, parent:Node) -> void:
+func _remove_children(remove_list: PackedStringArray, parent: Node) -> void:
var child_count := parent.get_child_count()
- var to_remove_list := []
+ var to_remove_list: Array[Node]
for child_idx in child_count:
- var child = parent.get_child(child_idx)
+ var child := parent.get_child(child_idx)
logger.trace("Checking if " + child.name + " in remove list...")
if child.name in remove_list:
logger.trace(child.name + " queued for removal!")
diff --git a/core/ui/card_ui/card_ui.tscn b/core/ui/card_ui/card_ui.tscn
index 80c61b45..0af538e6 100644
--- a/core/ui/card_ui/card_ui.tscn
+++ b/core/ui/card_ui/card_ui.tscn
@@ -1,23 +1,29 @@
-[gd_scene load_steps=38 format=3 uid="uid://fhriwlhm0lcj"]
+[gd_scene load_steps=44 format=3 uid="uid://fhriwlhm0lcj"]
[ext_resource type="PackedScene" uid="uid://n83wlhmmsu3j" path="res://core/systems/input/input_manager.tscn" id="1_34t85"]
[ext_resource type="Script" path="res://core/ui/card_ui/card_ui.gd" id="1_f8851"]
[ext_resource type="PackedScene" uid="uid://dlegwm7jqfe2i" path="res://core/systems/boxart/boxart_local.tscn" id="2_600i0"]
[ext_resource type="PackedScene" uid="uid://ch6qw6obetalo" path="res://core/systems/library/library_desktop.tscn" id="3_68bes"]
[ext_resource type="Script" path="res://core/systems/input/input_icon_processor.gd" id="3_y116l"]
+[ext_resource type="Script" path="res://core/systems/gamescope/gamescope.gd" id="4_ksi1t"]
[ext_resource type="PackedScene" uid="uid://cbboox5bujlx1" path="res://core/systems/launcher/launch_manager.tscn" id="4_tgw75"]
[ext_resource type="PackedScene" uid="uid://uam46dtvo2yh" path="res://core/systems/plugin/plugin_manager.tscn" id="5_dv70s"]
+[ext_resource type="GamescopeInstance" uid="uid://chd0nc6gbfnw0" path="res://core/systems/gamescope/gamescope.tres" id="5_wmkau"]
[ext_resource type="PackedScene" uid="uid://o70x5igrlq30" path="res://core/ui/card_ui/home/cardui_home.tscn" id="6_12lhu"]
[ext_resource type="PackedScene" uid="uid://dnq5j20fbcrwx" path="res://core/systems/power/power_saver.tscn" id="8_hyc1j"]
[ext_resource type="PackedScene" uid="uid://bcdk1lj6enq3l" path="res://core/ui/card_ui/launch/game_launch_menu.tscn" id="9_m34in"]
[ext_resource type="PackedScene" uid="uid://d2jiecrd5sw4s" path="res://core/ui/card_ui/settings/settings_menu.tscn" id="10_1ruvi"]
[ext_resource type="PackedScene" uid="uid://d4bmkauhrlhq0" path="res://core/ui/card_ui/navigation/search_bar_menu.tscn" id="10_7bl6b"]
[ext_resource type="PackedScene" uid="uid://58qlqqbh58im" path="res://core/ui/card_ui/launch/game_settings.tscn" id="10_7cj6o"]
+[ext_resource type="Script" path="res://core/systems/power/power_manager.gd" id="10_opsp8"]
[ext_resource type="PackedScene" uid="uid://uqkwpeq7f1o" path="res://core/ui/card_ui/library/library_menu.tscn" id="10_uqodp"]
+[ext_resource type="UPowerInstance" uid="uid://pgiv8yxro3n8" path="res://core/systems/power/power_manager.tres" id="11_nk5v7"]
[ext_resource type="PackedScene" uid="uid://by0i08fw1fwty" path="res://core/ui/card_ui/navigation/top_button_menu.tscn" id="11_x7ns7"]
[ext_resource type="PackedScene" uid="uid://cd2p3lu01aric" path="res://core/ui/card_ui/navigation/context_bar_menu.tscn" id="12_bstc8"]
[ext_resource type="PackedScene" uid="uid://jfacx7uys32r" path="res://core/ui/card_ui/main-menu/main_menu.tscn" id="13_46tck"]
[ext_resource type="PackedScene" uid="uid://cwarv58ju0sow" path="res://core/ui/card_ui/gamepad/gamepad_settings.tscn" id="13_n64ve"]
+[ext_resource type="Script" path="res://core/systems/performance/power_station.gd" id="13_tag7s"]
+[ext_resource type="PowerStationInstance" uid="uid://c2mmrnh3rcs58" path="res://core/systems/performance/power_station.tres" id="14_a5vk3"]
[ext_resource type="PackedScene" uid="uid://b30stcxjwk3od" path="res://core/ui/card_ui/ootbe/first_boot_menu.tscn" id="14_dpfu1"]
[ext_resource type="PackedScene" uid="uid://hroo3ll4inrb" path="res://core/ui/card_ui/quick_bar/quick_bar_menu.tscn" id="14_lsaok"]
[ext_resource type="PackedScene" uid="uid://dj1fooc3gh13l" path="res://core/ui/card_ui/help/help_menu.tscn" id="15_m1wp2"]
@@ -89,6 +95,10 @@ script = ExtResource("1_f8851")
[node name="InputIconProcessor" type="Node" parent="."]
script = ExtResource("3_y116l")
+[node name="Gamescope" type="Node" parent="."]
+script = ExtResource("4_ksi1t")
+instance = ExtResource("5_wmkau")
+
[node name="BoxArtLocal" parent="." instance=ExtResource("2_600i0")]
[node name="DesktopLibrary" parent="." instance=ExtResource("3_68bes")]
@@ -97,8 +107,16 @@ script = ExtResource("3_y116l")
[node name="PluginManager" parent="." instance=ExtResource("5_dv70s")]
+[node name="PowerManager" type="Node" parent="."]
+script = ExtResource("10_opsp8")
+instance = ExtResource("11_nk5v7")
+
[node name="PowerSaver" parent="." instance=ExtResource("8_hyc1j")]
+[node name="PowerStation" type="Node" parent="."]
+script = ExtResource("13_tag7s")
+instance = ExtResource("14_a5vk3")
+
[node name="PowerTimer" type="Timer" parent="."]
unique_name_in_owner = true
wait_time = 1.5
diff --git a/core/ui/card_ui/gamepad/gamepad_settings.gd b/core/ui/card_ui/gamepad/gamepad_settings.gd
index d5dbdab0..14882219 100644
--- a/core/ui/card_ui/gamepad/gamepad_settings.gd
+++ b/core/ui/card_ui/gamepad/gamepad_settings.gd
@@ -12,13 +12,13 @@ var notification_manager := load("res://core/global/notification_manager.tres")
var settings_manager := load("res://core/global/settings_manager.tres") as SettingsManager
var global_state_machine := load("res://assets/state/state_machines/menu_state_machine.tres") as StateMachine
var state_machine := load("res://assets/state/state_machines/gamepad_settings_state_machine.tres") as StateMachine
-var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber
+var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance
var input_icons := load("res://core/systems/input/input_icon_manager.tres") as InputIconManager
var button_scene := load("res://core/ui/components/card_mapping_button.tscn") as PackedScene
var container_scene := load("res://core/ui/components/card_mapping_button_group.tscn") as PackedScene
var expandable_scene := load("res://core/ui/card_ui/quick_bar/qb_card.tscn") as PackedScene
-var gamepad: InputPlumber.CompositeDevice
+var gamepad: CompositeDevice
var profile: InputPlumberProfile
var profile_gamepad: String
var library_item: LibraryItem
@@ -114,7 +114,7 @@ func _on_state_entered(_from: State) -> void:
var dbus_path := gamepad_state.get_meta("dbus_path") as String
# Find the composite device to configure
- for device: InputPlumber.CompositeDevice in input_plumber.composite_devices:
+ for device: CompositeDevice in input_plumber.composite_devices:
if device.dbus_path == dbus_path:
gamepad = device
break
@@ -196,7 +196,7 @@ func _on_state_exited(_to: State) -> void:
## Populates the button mappings for the given gamepad
-func populate_mappings_for(gamepad: InputPlumber.CompositeDevice) -> void:
+func populate_mappings_for(gamepad: CompositeDevice) -> void:
var gamepad_name := gamepad.name
var capabilities := gamepad.capabilities
@@ -595,7 +595,7 @@ func get_target_gamepad_text(gamepad_type: InputPlumberProfile.TargetDevice) ->
# Set the given profile for the given composte device.
-func _set_gamepad_profile(gamepad: InputPlumber.CompositeDevice, profile_path: String = "") -> void:
+func _set_gamepad_profile(gamepad: CompositeDevice, profile_path: String = "") -> void:
if profile_path == "":
if gamepad_state.has_meta("item"):
library_item = gamepad_state.get_meta("item") as LibraryItem
diff --git a/core/ui/card_ui_overlay_mode/card_ui_overlay_mode.gd b/core/ui/card_ui_overlay_mode/card_ui_overlay_mode.gd
index c83c7c43..7b1dc9b9 100644
--- a/core/ui/card_ui_overlay_mode/card_ui_overlay_mode.gd
+++ b/core/ui/card_ui_overlay_mode/card_ui_overlay_mode.gd
@@ -4,7 +4,7 @@ var platform := preload("res://core/global/platform.tres") as Platform
var gamescope := preload("res://core/global/gamescope.tres") as Gamescope
var launch_manager := preload("res://core/global/launch_manager.tres") as LaunchManager
var settings_manager := preload("res://core/global/settings_manager.tres") as SettingsManager
-var input_plumber := preload("res://core/systems/input/input_plumber.tres") as InputPlumber
+var input_plumber := preload("res://core/systems/input/input_plumber.tres") as InputPlumberInstance
var state_machine := (
preload("res://assets/state/state_machines/global_state_machine.tres") as StateMachine
)
@@ -160,15 +160,15 @@ func _setup_overlay_mode(args: Array) -> void:
_remove_children(settings_remove_list, settings_menu)
# Setup inputplumber to receive guide presses.
- input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.PASS)
+ input_plumber.set_intercept_mode(InputPlumberInstance.INTERCEPT_MODE_PASS)
input_plumber.set_intercept_activation(["Gamepad:Button:Guide", "Gamepad:Button:East"], "Gamepad:Button:QuickAccess2")
# Sets the intercept mode and intercept activation keys to what overlay_mode expects.
- var on_device_changed := func(device: InputPlumber.CompositeDevice):
- var intercept_mode : InputPlumber.INTERCEPT_MODE = input_plumber.intercept_mode_current
+ var on_device_changed := func(device: CompositeDevice):
+ var intercept_mode := input_plumber.intercept_mode
logger.debug("Setting intercept mode to: " + str(intercept_mode))
- input_plumber.set_intercept_mode_single(intercept_mode, device)
- input_plumber.set_intercept_activation_single(["Gamepad:Button:Guide", "Gamepad:Button:East"], "Gamepad:Button:QuickAccess2", device)
+ device.intercept_mode = intercept_mode
+ device.set_intercept_activation(["Gamepad:Button:Guide", "Gamepad:Button:East"], "Gamepad:Button:QuickAccess2")
input_plumber.composite_device_changed.connect(on_device_changed)
@@ -258,7 +258,7 @@ func _find_underlay_window_id() -> void:
## Called when the base state is entered.
func _on_base_state_entered(from: State) -> void:
# Manage input focus
- input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.PASS)
+ input_plumber.set_intercept_mode(InputPlumberInstance.INTERCEPT_MODE_PASS)
if gamescope.set_input_focus(overlay_window_id, 0) != OK:
logger.error("Unable to set STEAM_INPUT_FOCUS atom!")
@@ -270,7 +270,7 @@ func _on_base_state_entered(from: State) -> void:
## Called when a the base state is exited.
func _on_base_state_exited(to: State) -> void:
# Manage input focus
- input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.ALL)
+ input_plumber.set_intercept_mode(InputPlumberInstance.INTERCEPT_MODE_ALL)
if gamescope.set_input_focus(overlay_window_id, 1) != OK:
logger.error("Unable to set STEAM_INPUT_FOCUS atom!")
diff --git a/core/ui/components/battery_container.gd b/core/ui/components/battery_container.gd
index 463bb8d0..109e8a42 100644
--- a/core/ui/components/battery_container.gd
+++ b/core/ui/components/battery_container.gd
@@ -7,8 +7,8 @@ const icon_half = preload("res://assets/ui/icons/battery-half.svg")
const icon_low = preload("res://assets/ui/icons/battery-low.svg")
const icon_empty = preload("res://assets/ui/icons/battery-empty.svg")
-var power_manager := load("res://core/systems/power/power_manager.tres") as PowerManager
-var batteries : Array[PowerManager.Device]
+var power_manager := load("res://core/systems/power/power_manager.tres") as UPowerInstance
+var display_device := power_manager.get_display_device()
var logger := Log.get_logger("BatteryContainer", Log.LEVEL.INFO)
@@ -17,20 +17,16 @@ var logger := Log.get_logger("BatteryContainer", Log.LEVEL.INFO)
func _ready():
- batteries = power_manager.get_devices_by_type(PowerManager.DEVICE_TYPE.BATTERY)
- if batteries.size() > 1:
- logger.warn("You somehow have more than one battery. We don't know what to do with that.")
- if batteries.size() == 0:
+ if not display_device:
logger.debug("No battery detected. nothing to do.")
visible = false
return
- var battery := batteries[0]
- _on_update_device(battery)
- battery.updated.connect(_on_update_device.bind(battery))
+ _on_update_device(display_device)
+ display_device.updated.connect(_on_update_device.bind(display_device))
-func _on_update_device(item: PowerManager.Device):
+func _on_update_device(item: UPowerDevice):
var capacity := item.percentage
var state := item.state
battery_icon.texture = get_capacity_texture(capacity, state)
@@ -42,8 +38,8 @@ func _on_update_device(item: PowerManager.Device):
## Returns the texture reflecting the given battery capacity
-static func get_capacity_texture(capacity: int, state: PowerManager.DEVICE_STATE) -> Texture2D:
- if state in [PowerManager.DEVICE_STATE.CHARGING, PowerManager.DEVICE_STATE.FULLY_CHARGED]:
+static func get_capacity_texture(capacity: int, state: int) -> Texture2D:
+ if state in [UPowerDevice.STATE_CHARGING, UPowerDevice.STATE_FULLY_CHARGED]:
return icon_charging
if capacity >= 90:
return icon_full
diff --git a/gdext/.gdignore b/extensions/.gdignore
similarity index 100%
rename from gdext/.gdignore
rename to extensions/.gdignore
diff --git a/extensions/core/.gitignore b/extensions/core/.gitignore
new file mode 100644
index 00000000..54b02da4
--- /dev/null
+++ b/extensions/core/.gitignore
@@ -0,0 +1,2 @@
+/target
+*.so
diff --git a/extensions/core/Cargo.lock b/extensions/core/Cargo.lock
new file mode 100644
index 00000000..9af6fe5a
--- /dev/null
+++ b/extensions/core/Cargo.lock
@@ -0,0 +1,1582 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-fs"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a"
+dependencies = [
+ "async-lock",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-io"
+version = "2.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8"
+dependencies = [
+ "async-lock",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8a07789659a4d385b79b18b9127fc27e1a59e1e89117c78c5ea3b806f016374"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+
+[[package]]
+name = "backtrace"
+version = "0.3.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
+
+[[package]]
+name = "cc"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
+dependencies = [
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "endi"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-lite"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gamescope-x11-client"
+version = "0.1.0"
+source = "git+https://github.com/ShadowBlip/gamescope-x11-client?branch=main#3a0cbe64ba60dffb5ad85f156101c056f764e659"
+dependencies = [
+ "log",
+ "strum",
+ "strum_macros",
+ "x11rb",
+]
+
+[[package]]
+name = "gdextension-api"
+version = "0.2.0"
+source = "git+https://github.com/godot-rust/godot4-prebuilt?branch=releases#6d902e8a6060007f4ab94cd78882247ae2558d96"
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "gensym"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "913dce4c5f06c2ea40fc178c06f777ac89fc6b1383e90c254fafb1abe4ba3c82"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "uuid",
+]
+
+[[package]]
+name = "gethostname"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
+dependencies = [
+ "libc",
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
+
+[[package]]
+name = "glam"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94"
+
+[[package]]
+name = "godot"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1"
+dependencies = [
+ "godot-core",
+ "godot-macros",
+]
+
+[[package]]
+name = "godot-bindings"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1"
+dependencies = [
+ "gdextension-api",
+]
+
+[[package]]
+name = "godot-cell"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1"
+
+[[package]]
+name = "godot-codegen"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1"
+dependencies = [
+ "godot-bindings",
+ "heck 0.5.0",
+ "nanoserde",
+ "proc-macro2",
+ "quote",
+ "regex",
+]
+
+[[package]]
+name = "godot-core"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1"
+dependencies = [
+ "glam",
+ "godot-bindings",
+ "godot-cell",
+ "godot-codegen",
+ "godot-ffi",
+]
+
+[[package]]
+name = "godot-ffi"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1"
+dependencies = [
+ "gensym",
+ "godot-bindings",
+ "godot-codegen",
+ "libc",
+ "paste",
+]
+
+[[package]]
+name = "godot-macros"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1"
+dependencies = [
+ "godot-bindings",
+ "markdown",
+ "proc-macro2",
+ "quote",
+ "venial",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "hermit-abi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "indexmap"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "inotify"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
+dependencies = [
+ "bitflags",
+ "futures-core",
+ "inotify-sys",
+ "libc",
+ "tokio",
+]
+
+[[package]]
+name = "inotify-sys"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.158"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+
+[[package]]
+name = "markdown"
+version = "1.0.0-alpha.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "911a8325e6fb87b89890cd4529a2ab34c2669c026279e61c26b7140a3d821ccb"
+dependencies = [
+ "unicode-id",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
+dependencies = [
+ "hermit-abi 0.3.9",
+ "libc",
+ "wasi",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "nanoserde"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5de9cf844ab1e25a0353525bd74cb889843a6215fa4a0d156fd446f4857a1b99"
+dependencies = [
+ "nanoserde-derive",
+]
+
+[[package]]
+name = "nanoserde-derive"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e943b2c21337b7e3ec6678500687cdc741b7639ad457f234693352075c082204"
+
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "object"
+version = "0.36.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "opengamepadui-core"
+version = "0.1.0"
+dependencies = [
+ "futures-util",
+ "gamescope-x11-client",
+ "godot",
+ "inotify",
+ "nix",
+ "once_cell",
+ "tokio",
+ "zbus",
+ "zvariant",
+]
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "piper"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "polling"
+version = "3.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi 0.4.0",
+ "pin-project-lite",
+ "rustix",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustix"
+version = "0.38.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.208"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.208"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "socket2"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strum"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
+
+[[package]]
+name = "strum_macros"
+version = "0.25.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "tokio"
+version = "1.39.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
+
+[[package]]
+name = "toml_edit"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
+name = "uds_windows"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "winapi",
+]
+
+[[package]]
+name = "unicode-id"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "uuid"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "venial"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6816bc32f30bf8dd1b3adb04de8406c7bf187d2f923bd9e4c0b99365d012613f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "x11rb"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12"
+dependencies = [
+ "gethostname",
+ "rustix",
+ "x11rb-protocol",
+]
+
+[[package]]
+name = "x11rb-protocol"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
+
+[[package]]
+name = "xdg-home"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "zbus"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-fs",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "hex",
+ "nix",
+ "ordered-stream",
+ "rand",
+ "serde",
+ "serde_repr",
+ "sha1",
+ "static_assertions",
+ "tracing",
+ "uds_windows",
+ "windows-sys 0.52.0",
+ "xdg-home",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
+dependencies = [
+ "serde",
+ "static_assertions",
+ "zvariant",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "byteorder",
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zvariant"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "static_assertions",
+ "zvariant_derive",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/extensions/core/Cargo.toml b/extensions/core/Cargo.toml
new file mode 100644
index 00000000..1af21b54
--- /dev/null
+++ b/extensions/core/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "opengamepadui-core"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"] # Compile this crate to a dynamic C library.
+
+[dependencies]
+futures-util = "0.3.30"
+godot = { git = "https://github.com/godot-rust/gdext", branch = "master", features = [
+ "experimental-threads",
+ "register-docs",
+] }
+nix = { version = "0.29.0", features = ["term", "process"] }
+once_cell = "1.19.0"
+tokio = { version = "1.39.3", features = ["full"] }
+zbus = "4.4.0"
+zvariant = "4.2.0"
+gamescope-x11-client = { git = "https://github.com/ShadowBlip/gamescope-x11-client", branch = "main" }
+inotify = "0.11.0"
diff --git a/extensions/core/Makefile b/extensions/core/Makefile
new file mode 100644
index 00000000..917abf11
--- /dev/null
+++ b/extensions/core/Makefile
@@ -0,0 +1,50 @@
+PREFIX ?= addons
+EXT_NAME := $(shell grep 'name =' Cargo.toml | head -n 1 | cut -d'"' -f2)
+LIB_NAME := $(shell grep 'name =' Cargo.toml | head -n 1 | cut -d'"' -f2 | sed 's/-/_/g')
+ALL_RS := $(shell find ./src -name '*.rs')
+ADDON_PATH := ../../addons/core
+RELEASE_TARGET := $(ADDON_PATH)/bin/lib$(EXT_NAME).linux.template_release.x86_64.so
+DEBUG_TARGET := $(ADDON_PATH)/bin/lib$(EXT_NAME).linux.template_debug.x86_64.so
+
+##@ General
+
+# The help target prints out all targets with their descriptions organized
+# beneath their categories. The categories are represented by '##@' and the
+# target descriptions by '##'. The awk commands is responsible for reading the
+# entire set of makefiles included in this invocation, looking for lines of the
+# file as xyz: ## something, and then pretty-format the target and help. Then,
+# if there's a line with ##@ something, that gets pretty-printed as a category.
+# More info on the usage of ANSI control characters for terminal formatting:
+# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
+# More info on the awk command:
+# http://linuxcommand.org/lc3_adv_awk.php
+
+.PHONY: help
+help: ## Display this help.
+ @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
+
+
+.PHONY: build
+build: $(RELEASE_TARGET) $(DEBUG_TARGET) ## Build release and debug binaries
+
+
+.PHONY: clean
+clean: ## Clean build artifacts
+ rm $(RELEASE_TARGET) $(DEBUG_TARGET)
+ rm -rf target
+
+
+.PHONY: release
+release: $(RELEASE_TARGET) ## Build release binary
+$(RELEASE_TARGET): $(ALL_RS)
+ cargo build --release
+ mkdir -p $(@D)
+ cp target/release/lib$(LIB_NAME).so $@
+
+
+.PHONY: debug
+debug: $(DEBUG_TARGET) ## Build binary with debug symbols
+$(DEBUG_TARGET): $(ALL_RS)
+ cargo build
+ mkdir -p $(@D)
+ cp target/debug/lib$(LIB_NAME).so $@
diff --git a/extensions/core/src/bluetooth.rs b/extensions/core/src/bluetooth.rs
new file mode 100644
index 00000000..5447862b
--- /dev/null
+++ b/extensions/core/src/bluetooth.rs
@@ -0,0 +1 @@
+pub mod bluez;
diff --git a/extensions/core/src/bluetooth/bluez.rs b/extensions/core/src/bluetooth/bluez.rs
new file mode 100644
index 00000000..aaf35116
--- /dev/null
+++ b/extensions/core/src/bluetooth/bluez.rs
@@ -0,0 +1,379 @@
+pub mod adapter;
+pub mod device;
+
+use std::{
+ collections::HashMap,
+ error::Error,
+ sync::mpsc::{channel, Receiver, Sender, TryRecvError},
+ time::Duration,
+};
+
+use adapter::BluetoothAdapter;
+use device::BluetoothDevice;
+use futures_util::stream::StreamExt;
+use godot::{obj::WithBaseField, prelude::*};
+use zbus::fdo::ObjectManagerProxy;
+use zbus::{fdo::ManagedObjects, names::BusName};
+
+use crate::{
+ dbus::bluez::adapter1::Adapter1ProxyBlocking, get_dbus_system, get_dbus_system_blocking,
+ RUNTIME,
+};
+
+pub const BLUEZ_BUS: &str = "org.bluez";
+const BLUEZ_PATH: &str = "/org/bluez";
+const BLUEZ_MANAGER_PATH: &str = "/";
+
+/// Supported Bluez DBus objects
+#[derive(Debug)]
+enum ObjectType {
+ Unknown,
+ Adapter,
+ Device,
+}
+
+impl ObjectType {
+ /// Returns the object type from the list of implemented interfaces
+ fn from_ifaces(ifaces: Vec) -> Self {
+ if ifaces.contains(&"org.bluez.Device1".to_string()) {
+ Self::Device
+ } else if ifaces.contains(&"org.bluez.Adapter1".to_string()) {
+ Self::Adapter
+ } else {
+ Self::Unknown
+ }
+ }
+}
+
+/// Signals that can be emitted
+#[derive(Debug)]
+enum Signal {
+ Started,
+ Stopped,
+ ObjectAdded { path: String, ifaces: Vec },
+ ObjectRemoved { path: String, ifaces: Vec },
+}
+
+#[derive(GodotClass)]
+#[class(base=Resource)]
+pub struct BluezInstance {
+ base: Base,
+ rx: Receiver,
+ conn: Option,
+ adapters: HashMap>,
+ devices: HashMap>,
+}
+
+#[godot_api]
+impl BluezInstance {
+ /// Emitted when Bluez is detected as running
+ #[signal]
+ fn started();
+
+ /// Emitted when Bluez is detected as stopped
+ #[signal]
+ fn stopped();
+
+ /// Emitted when a new bluetooth adapter is discovered
+ #[signal]
+ fn adapter_added(device: Gd);
+
+ /// Emitted when a bluetooth adapter is removed
+ #[signal]
+ fn adapter_removed(path: GString);
+
+ /// Emitted when a new bluetooth device is discovered
+ #[signal]
+ fn device_added(device: Gd);
+
+ /// Emitted when a bluetooth device is removed
+ #[signal]
+ fn device_removed(path: GString);
+
+ /// Returns true if the Bluez service is currently running
+ #[func]
+ fn is_running(&self) -> bool {
+ let Some(conn) = self.conn.as_ref() else {
+ return false;
+ };
+ let bus = BusName::from_static_str(BLUEZ_BUS).unwrap();
+ let dbus = zbus::blocking::fdo::DBusProxy::new(conn).ok();
+ let Some(dbus) = dbus else {
+ return false;
+ };
+ dbus.name_has_owner(bus.clone()).unwrap_or_default()
+ }
+
+ /// Get managed objects
+ fn get_managed_objects(&self) -> Result {
+ let Some(conn) = self.conn.as_ref() else {
+ return Err(zbus::fdo::Error::Disconnected(
+ "No DBus connection found".into(),
+ ));
+ };
+
+ let bus = BusName::from_static_str(BLUEZ_BUS).unwrap();
+ let object_manager = zbus::blocking::fdo::ObjectManagerProxy::builder(conn)
+ .destination(bus)
+ .ok()
+ .and_then(|builder| builder.path(BLUEZ_MANAGER_PATH).ok())
+ .and_then(|builder| builder.build().ok());
+ let Some(object_manager) = object_manager else {
+ return Ok(ManagedObjects::new());
+ };
+
+ object_manager.get_managed_objects()
+ }
+
+ /// Return a list of currently discovered bluetooth adapters
+ #[func]
+ fn get_adapters(&self) -> Array> {
+ let mut adapters = array![];
+ for adapter in self.adapters.values() {
+ adapters.push(adapter.clone());
+ }
+
+ adapters
+ }
+
+ /// Return a list of currently discovered devices
+ #[func]
+ fn get_discovered_devices(&self) -> Array> {
+ let mut devices = array![];
+ for device in self.devices.values() {
+ devices.push(device.clone());
+ }
+
+ devices
+ }
+
+ /// Process Bluez signals and emit them as Godot signals. This method
+ /// should be called every frame in the "_process" loop of a node.
+ #[func]
+ fn process(&mut self) {
+ // Drain all messages from the channel to process them
+ loop {
+ let signal = match self.rx.try_recv() {
+ Ok(value) => value,
+ Err(e) => match e {
+ TryRecvError::Empty => break,
+ TryRecvError::Disconnected => {
+ godot_error!("Backend thread is not running!");
+ return;
+ }
+ },
+ };
+ self.process_signal(signal);
+ }
+
+ // Process signals on child objects
+ for adapter in self.adapters.values_mut() {
+ adapter.bind_mut().process();
+ }
+ for device in self.devices.values_mut() {
+ device.bind_mut().process();
+ }
+ }
+
+ /// Process and dispatch the given signal
+ fn process_signal(&mut self, signal: Signal) {
+ match signal {
+ Signal::Started => {
+ self.base_mut().emit_signal("started".into(), &[]);
+ }
+ Signal::Stopped => {
+ self.base_mut().emit_signal("stopped".into(), &[]);
+ }
+ Signal::ObjectAdded { path, ifaces } => {
+ let obj_type = ObjectType::from_ifaces(ifaces);
+ match obj_type {
+ ObjectType::Unknown => (),
+ ObjectType::Adapter => {
+ let adapter = BluetoothAdapter::new(path.as_str());
+ self.adapters.insert(path, adapter.clone());
+ self.base_mut()
+ .emit_signal("adapter_added".into(), &[adapter.to_variant()]);
+ }
+ ObjectType::Device => {
+ let device = BluetoothDevice::new(path.as_str());
+ self.devices.insert(path, device.clone());
+ self.base_mut()
+ .emit_signal("device_added".into(), &[device.to_variant()]);
+ }
+ }
+ }
+ Signal::ObjectRemoved { path, ifaces } => {
+ let obj_type = ObjectType::from_ifaces(ifaces);
+ match obj_type {
+ ObjectType::Unknown => (),
+ ObjectType::Adapter => {
+ self.adapters.remove(&path);
+ self.base_mut()
+ .emit_signal("adapter_removed".into(), &[path.to_variant()]);
+ }
+ ObjectType::Device => {
+ self.devices.remove(&path);
+ self.base_mut()
+ .emit_signal("device_removed".into(), &[path.to_variant()]);
+ }
+ }
+ }
+ }
+ }
+}
+
+#[godot_api]
+impl IResource for BluezInstance {
+ /// Called upon object initialization in the engine
+ fn init(base: Base) -> Self {
+ godot_print!("Initializing Bluez instance");
+
+ // Create a channel to communicate with the service
+ let (tx, rx) = channel();
+
+ // Spawn a task using the shared tokio runtime to listen for signals
+ RUNTIME.spawn(async move {
+ if let Err(e) = run(tx).await {
+ godot_error!("Failed to run Bluez task: ${e:?}");
+ }
+ });
+
+ // Create a new Bluez instance
+ let conn = get_dbus_system_blocking().ok();
+ let mut instance = Self {
+ base,
+ rx,
+ conn,
+ adapters: HashMap::new(),
+ devices: HashMap::new(),
+ };
+
+ // Perform initial object discovery
+ let mut adapters = HashMap::new();
+ let mut devices = HashMap::new();
+ let objects = instance.get_managed_objects().unwrap_or_default();
+ for (path, ifaces) in objects.into_iter() {
+ let path = path.to_string();
+ let ifaces: Vec = ifaces.into_keys().map(|v| v.to_string()).collect();
+ let obj_type = ObjectType::from_ifaces(ifaces);
+
+ match obj_type {
+ ObjectType::Unknown => (),
+ ObjectType::Adapter => {
+ let adapter = BluetoothAdapter::new(path.as_str());
+ adapters.insert(path, adapter);
+ }
+ ObjectType::Device => {
+ let device = BluetoothDevice::new(path.as_str());
+ devices.insert(path, device);
+ }
+ }
+ }
+
+ // Update the discovered objects
+ instance.adapters = adapters;
+ instance.devices = devices;
+
+ instance
+ }
+}
+
+/// Runs Bluez tasks in Tokio to listen for DBus signals and send them
+/// over the given channel so they can be processed during each engine frame.
+async fn run(tx: Sender) -> Result<(), Box> {
+ godot_print!("Spawning Bluez tasks");
+ // Establish a connection to the system bus
+ let conn = get_dbus_system().await?;
+
+ // Spawn a task to listen for Bluez start/stop
+ let dbus_conn = conn.clone();
+ let signals_tx = tx.clone();
+ RUNTIME.spawn(async move {
+ let bus = BusName::from_static_str(BLUEZ_BUS).unwrap();
+ let mut is_running = {
+ let dbus = zbus::fdo::DBusProxy::new(&dbus_conn).await.ok();
+ let Some(dbus) = dbus else {
+ return;
+ };
+ dbus.name_has_owner(bus.clone()).await.unwrap_or_default()
+ };
+
+ loop {
+ let dbus = zbus::fdo::DBusProxy::new(&dbus_conn).await.ok();
+ let Some(dbus) = dbus else {
+ break;
+ };
+ let running = dbus.name_has_owner(bus.clone()).await.unwrap_or_default();
+ if running != is_running {
+ let signal = if running {
+ Signal::Started
+ } else {
+ Signal::Stopped
+ };
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ is_running = running;
+ tokio::time::sleep(Duration::from_secs(5)).await;
+ }
+ });
+
+ // Get a proxy instance to ObjectManager
+ let bus = BusName::from_static_str(BLUEZ_BUS).unwrap();
+ let object_manager: ObjectManagerProxy = ObjectManagerProxy::builder(&conn)
+ .destination(bus)?
+ .path(BLUEZ_MANAGER_PATH)?
+ .build()
+ .await?;
+
+ // Spawn a task to listen for objects added
+ let mut ifaces_added = object_manager.receive_interfaces_added().await?;
+ let signals_tx = tx.clone();
+ RUNTIME.spawn(async move {
+ while let Some(signal) = ifaces_added.next().await {
+ let args = match signal.args() {
+ Ok(args) => args,
+ Err(e) => {
+ godot_warn!("Failed to get signal args: ${e:?}");
+ continue;
+ }
+ };
+
+ let path = args.object_path.to_string();
+ let ifaces = args
+ .interfaces_and_properties
+ .keys()
+ .map(|v| v.to_string())
+ .collect();
+ let signal = Signal::ObjectAdded { path, ifaces };
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ // Spawn a task to listen for objects removed
+ let mut ifaces_removed = object_manager.receive_interfaces_removed().await?;
+ let signals_tx = tx.clone();
+ RUNTIME.spawn(async move {
+ while let Some(signal) = ifaces_removed.next().await {
+ let args = match signal.args() {
+ Ok(args) => args,
+ Err(e) => {
+ godot_warn!("Failed to get signal args: ${e:?}");
+ continue;
+ }
+ };
+
+ let path = args.object_path.to_string();
+ let ifaces = args.interfaces.iter().map(|v| v.to_string()).collect();
+ let signal = Signal::ObjectRemoved { path, ifaces };
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ Ok(())
+}
diff --git a/extensions/core/src/bluetooth/bluez/adapter.rs b/extensions/core/src/bluetooth/bluez/adapter.rs
new file mode 100644
index 00000000..aa15e026
--- /dev/null
+++ b/extensions/core/src/bluetooth/bluez/adapter.rs
@@ -0,0 +1,572 @@
+use std::error::Error;
+use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
+
+use futures_util::StreamExt;
+use godot::obj::WithBaseField;
+use godot::prelude::*;
+
+use godot::classes::{Resource, ResourceLoader};
+use zvariant::ObjectPath;
+
+use crate::dbus::bluez::adapter1::{Adapter1Proxy, Adapter1ProxyBlocking};
+use crate::{get_dbus_system, get_dbus_system_blocking, RUNTIME};
+
+use super::device::BluetoothDevice;
+use super::BLUEZ_BUS;
+
+/// Signals that can be emitted
+#[derive(Debug)]
+enum Signal {
+ Discoverable { value: bool },
+ Discovering { value: bool },
+ Pairable { value: bool },
+ Powered { value: bool },
+ PowerState { value: String },
+}
+
+#[derive(GodotClass)]
+#[class(no_init, base=Resource)]
+pub struct BluetoothAdapter {
+ base: Base,
+ rx: Receiver,
+ conn: Option,
+
+ #[allow(dead_code)]
+ #[var(get = get_dbus_path)]
+ dbus_path: GString,
+ #[allow(dead_code)]
+ #[var(get = get_address)]
+ address: GString,
+ #[allow(dead_code)]
+ #[var(get = get_address_type)]
+ address_type: GString,
+ #[allow(dead_code)]
+ #[var(get = get_alias, set = set_alias)]
+ alias: GString,
+ #[allow(dead_code)]
+ #[var(get = get_class)]
+ class: u32,
+ #[allow(dead_code)]
+ #[var(get = get_discoverable, set = set_discoverable)]
+ discoverable: bool,
+ #[allow(dead_code)]
+ #[var(get = get_discoverable_timeout, set = set_discoverable_timeout)]
+ discoverable_timeout: u32,
+ #[allow(dead_code)]
+ #[var(get = get_discovering)]
+ discovering: bool,
+ #[allow(dead_code)]
+ #[var(get = get_experimental_features)]
+ experimental_features: PackedStringArray,
+ #[allow(dead_code)]
+ #[var(get = get_manufacturer)]
+ manufacturer: u16,
+ #[allow(dead_code)]
+ #[var(get = get_modalias)]
+ modalias: GString,
+ #[allow(dead_code)]
+ #[var(get = get_name)]
+ name: GString,
+ #[allow(dead_code)]
+ #[var(get = get_pairable, set = set_pairable)]
+ pairable: bool,
+ #[allow(dead_code)]
+ #[var(get = get_pairable_timeout, set = set_pairable_timeout)]
+ pairable_timeout: u32,
+ #[allow(dead_code)]
+ #[var(get = get_power_state)]
+ power_state: GString,
+ #[allow(dead_code)]
+ #[var(get = get_powered, set = set_powered)]
+ powered: bool,
+ #[allow(dead_code)]
+ #[var(get = get_roles)]
+ roles: PackedStringArray,
+ #[allow(dead_code)]
+ #[var(get = get_uuids)]
+ uuids: PackedStringArray,
+ #[allow(dead_code)]
+ #[var(get = get_version)]
+ version: u8,
+}
+
+#[godot_api]
+impl BluetoothAdapter {
+ #[signal]
+ fn discoverable_changed(value: bool);
+
+ #[signal]
+ fn discovering_changed(value: bool);
+
+ #[signal]
+ fn pairable_changed(value: bool);
+
+ #[signal]
+ fn powered_changed(value: bool);
+
+ #[signal]
+ fn power_state_changed(value: GString);
+
+ /// Create a new [BluetoothAdapter] with the given DBus path
+ pub fn from_path(path: GString) -> Gd {
+ // Create a channel to communicate with the signals task
+ godot_print!("BluetoothAdapter created with path: {path}");
+ let (tx, rx) = channel();
+ let dbus_path = path.clone().into();
+
+ // Spawn a task using the shared tokio runtime to listen for signals
+ RUNTIME.spawn(async move {
+ if let Err(e) = run(tx, dbus_path).await {
+ godot_error!("Failed to run DBusDevice task: ${e:?}");
+ }
+ });
+
+ Gd::from_init_fn(|base| {
+ // Create a connection to DBus
+ let conn = get_dbus_system_blocking().ok();
+
+ // Accept a base of type Base and directly forward it.
+ Self {
+ base,
+ rx,
+ conn,
+ dbus_path: path,
+ address: Default::default(),
+ address_type: Default::default(),
+ alias: Default::default(),
+ class: Default::default(),
+ discoverable: Default::default(),
+ discoverable_timeout: Default::default(),
+ discovering: Default::default(),
+ experimental_features: Default::default(),
+ manufacturer: Default::default(),
+ modalias: Default::default(),
+ name: Default::default(),
+ pairable: Default::default(),
+ pairable_timeout: Default::default(),
+ power_state: Default::default(),
+ powered: Default::default(),
+ roles: Default::default(),
+ uuids: Default::default(),
+ version: Default::default(),
+ }
+ })
+ }
+
+ /// Return a proxy instance to the adapter
+ fn get_proxy(&self) -> Option {
+ if let Some(conn) = self.conn.as_ref() {
+ let path: String = self.dbus_path.clone().into();
+ Adapter1ProxyBlocking::builder(conn)
+ .path(path)
+ .ok()
+ .and_then(|builder| builder.build().ok())
+ } else {
+ None
+ }
+ }
+
+ /// Get or create a [BluetoothAdapter] with the given DBus path. If an instance
+ /// already exists with the given path, then it will be loaded from the resource
+ /// cache.
+ pub fn new(path: &str) -> Gd {
+ let res_path = format!("dbus://{BLUEZ_BUS}{path}");
+
+ // Check to see if a resource already exists for this device
+ let mut resource_loader = ResourceLoader::singleton();
+ if resource_loader.exists(res_path.clone().into()) {
+ if let Some(res) = resource_loader.load(res_path.clone().into()) {
+ godot_print!(
+ "Resource already exists with path '{res_path}', loading that instead"
+ );
+ let device: Gd = res.cast();
+ device
+ } else {
+ let mut device = BluetoothAdapter::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ } else {
+ let mut device = BluetoothAdapter::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ }
+
+ /// Returns the DBus path to the [BluetoothAdapter]
+ #[func]
+ pub fn get_dbus_path(&self) -> GString {
+ self.dbus_path.clone()
+ }
+
+ #[func]
+ pub fn get_address(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.address().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_address_type(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.address_type().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_alias(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.alias().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn set_alias(&self, value: GString) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy
+ .set_alias(value.to_string().as_str())
+ .unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_class(&self) -> u32 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.class().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_discoverable(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.discoverable().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn set_discoverable(&self, value: bool) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.set_discoverable(value).unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_discoverable_timeout(&self) -> u32 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.discoverable_timeout().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn set_discoverable_timeout(&self, value: u32) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.set_discoverable_timeout(value).unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_discovering(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.discovering().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_experimental_features(&self) -> PackedStringArray {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ let values: Vec = proxy
+ .experimental_features()
+ .unwrap_or_default()
+ .into_iter()
+ .map(|v| v.to_godot())
+ .collect();
+ values.into()
+ }
+
+ #[func]
+ pub fn get_manufacturer(&self) -> u16 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.manufacturer().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_modalias(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.modalias().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_name(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.name().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_pairable(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.pairable().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn set_pairable(&self, value: bool) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.set_pairable(value).unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_pairable_timeout(&self) -> u32 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.pairable_timeout().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn set_pairable_timeout(&self, value: u32) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.set_pairable_timeout(value).unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_power_state(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.power_state().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_powered(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.powered().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn set_powered(&self, value: bool) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.set_powered(value).unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_roles(&self) -> PackedStringArray {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ let values: Vec = proxy
+ .roles()
+ .unwrap_or_default()
+ .into_iter()
+ .map(|v| v.to_godot())
+ .collect();
+ values.into()
+ }
+
+ #[func]
+ pub fn get_uuids(&self) -> PackedStringArray {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ let values: Vec = proxy
+ .uuids()
+ .unwrap_or_default()
+ .into_iter()
+ .map(|v| v.to_godot())
+ .collect();
+ values.into()
+ }
+
+ #[func]
+ pub fn get_version(&self) -> u8 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.version().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_discovery_filters(&self) -> PackedStringArray {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ let filters: Vec = proxy
+ .get_discovery_filters()
+ .unwrap_or_default()
+ .into_iter()
+ .map(|v| v.to_godot())
+ .collect();
+ filters.into()
+ }
+
+ #[func]
+ pub fn remove_device(&self, device: Gd) {
+ let Some(proxy) = self.get_proxy() else {
+ return;
+ };
+ let path = device.bind().get_dbus_path().to_string();
+ let path = ObjectPath::try_from(path).unwrap_or_default();
+ proxy.remove_device(&path).unwrap_or_default()
+ }
+
+ #[func]
+ pub fn start_discovery(&self) {
+ let Some(proxy) = self.get_proxy() else {
+ return;
+ };
+ proxy.start_discovery().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn stop_discovery(&self) {
+ let Some(proxy) = self.get_proxy() else {
+ return;
+ };
+ proxy.stop_discovery().unwrap_or_default()
+ }
+
+ /// Dispatches signals
+ pub fn process(&mut self) {
+ // Drain all messages from the channel to process them
+ loop {
+ let signal = match self.rx.try_recv() {
+ Ok(value) => value,
+ Err(e) => match e {
+ TryRecvError::Empty => break,
+ TryRecvError::Disconnected => {
+ godot_error!("Backend thread is not running!");
+ return;
+ }
+ },
+ };
+ self.process_signal(signal);
+ }
+ }
+
+ /// Process and dispatch the given signal
+ fn process_signal(&mut self, signal: Signal) {
+ match signal {
+ Signal::Discoverable { value } => {
+ self.base_mut()
+ .emit_signal("discoverable_changed".into(), &[value.to_variant()]);
+ }
+ Signal::Discovering { value } => {
+ self.base_mut()
+ .emit_signal("discovering_changed".into(), &[value.to_variant()]);
+ }
+ Signal::Pairable { value } => {
+ self.base_mut()
+ .emit_signal("pairable_changed".into(), &[value.to_variant()]);
+ }
+ Signal::Powered { value } => {
+ self.base_mut()
+ .emit_signal("powered_changed".into(), &[value.to_variant()]);
+ }
+ Signal::PowerState { value } => {
+ self.base_mut()
+ .emit_signal("power_state_changed".into(), &[value.to_variant()]);
+ }
+ }
+ }
+}
+
+impl Drop for BluetoothAdapter {
+ fn drop(&mut self) {
+ godot_print!("BluetoothAdapter '{}' is being destroyed!", self.dbus_path);
+ }
+}
+
+/// Run the signals task
+async fn run(tx: Sender, path: String) -> Result<(), Box> {
+ // Establish a connection to the system bus
+ let conn = get_dbus_system().await?;
+ let proxy = Adapter1Proxy::builder(&conn).path(path)?.build().await?;
+
+ let signals_tx = tx.clone();
+ let mut events = proxy.receive_discoverable_changed().await;
+ RUNTIME.spawn(async move {
+ while let Some(event) = events.next().await {
+ let value = event.get().await.unwrap_or_default();
+ let signal = Signal::Discoverable { value };
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ let signals_tx = tx.clone();
+ let mut events = proxy.receive_discovering_changed().await;
+ RUNTIME.spawn(async move {
+ while let Some(event) = events.next().await {
+ let value = event.get().await.unwrap_or_default();
+ let signal = Signal::Discovering { value };
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ let signals_tx = tx.clone();
+ let mut events = proxy.receive_pairable_changed().await;
+ RUNTIME.spawn(async move {
+ while let Some(event) = events.next().await {
+ let value = event.get().await.unwrap_or_default();
+ let signal = Signal::Pairable { value };
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ let signals_tx = tx.clone();
+ let mut events = proxy.receive_powered_changed().await;
+ RUNTIME.spawn(async move {
+ while let Some(event) = events.next().await {
+ let value = event.get().await.unwrap_or_default();
+ let signal = Signal::Powered { value };
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ let signals_tx = tx.clone();
+ let mut events = proxy.receive_power_state_changed().await;
+ RUNTIME.spawn(async move {
+ while let Some(event) = events.next().await {
+ let value = event.get().await.unwrap_or_default();
+ let signal = Signal::PowerState { value };
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ Ok(())
+}
diff --git a/extensions/core/src/bluetooth/bluez/device.rs b/extensions/core/src/bluetooth/bluez/device.rs
new file mode 100644
index 00000000..6f6cceeb
--- /dev/null
+++ b/extensions/core/src/bluetooth/bluez/device.rs
@@ -0,0 +1,516 @@
+use std::error::Error;
+use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
+
+use futures_util::StreamExt;
+use godot::prelude::*;
+
+use godot::classes::{Resource, ResourceLoader};
+
+use crate::dbus::bluez::device1::{Device1Proxy, Device1ProxyBlocking};
+use crate::{get_dbus_system, get_dbus_system_blocking, RUNTIME};
+
+use super::BLUEZ_BUS;
+
+/// Signals that can be emitted
+#[derive(Debug)]
+enum Signal {
+ ConnectedChanged { value: bool },
+ PairedChanged { value: bool },
+}
+
+#[derive(GodotClass)]
+#[class(no_init, base=Resource)]
+pub struct BluetoothDevice {
+ base: Base,
+ rx: Receiver,
+ conn: Option,
+
+ #[allow(dead_code)]
+ #[var(get = get_dbus_path)]
+ dbus_path: GString,
+ #[allow(dead_code)]
+ #[var(get = get_adapter)]
+ adapter: GString,
+ #[allow(dead_code)]
+ #[var(get = get_address)]
+ address: GString,
+ #[allow(dead_code)]
+ #[var(get = get_address_type)]
+ address_type: GString,
+ #[allow(dead_code)]
+ #[var(get = get_alias, set = set_alias)]
+ alias: GString,
+ #[allow(dead_code)]
+ #[var(get = get_appearance)]
+ appearance: u16,
+ #[allow(dead_code)]
+ #[var(get = get_blocked, set = set_blocked)]
+ blocked: bool,
+ #[allow(dead_code)]
+ #[var(get = get_bonded)]
+ bonded: bool,
+ #[allow(dead_code)]
+ #[var(get = get_class)]
+ class: u32,
+ #[allow(dead_code)]
+ #[var(get = get_connected)]
+ connected: bool,
+ #[allow(dead_code)]
+ #[var(get = get_icon)]
+ icon: GString,
+ #[allow(dead_code)]
+ #[var(get = get_legacy_pairing)]
+ legacy_pairing: bool,
+ #[allow(dead_code)]
+ #[var(get = get_modalias)]
+ modalias: GString,
+ #[allow(dead_code)]
+ #[var(get = get_name)]
+ name: GString,
+ #[allow(dead_code)]
+ #[var(get = get_paired)]
+ paired: bool,
+ #[allow(dead_code)]
+ #[var(get = get_rssi)]
+ rssi: i16,
+ #[allow(dead_code)]
+ #[var(get = get_services_resolved)]
+ services_resolved: bool,
+ #[allow(dead_code)]
+ #[var(get = get_trusted, set = set_trusted)]
+ trusted: bool,
+ #[allow(dead_code)]
+ #[var(get = get_tx_power)]
+ tx_power: i16,
+ #[allow(dead_code)]
+ #[var(get = get_uuids)]
+ uuids: PackedStringArray,
+ #[allow(dead_code)]
+ #[var(get = get_wake_allowed, set = set_wake_allowed)]
+ wake_allowed: bool,
+}
+
+#[godot_api]
+impl BluetoothDevice {
+ #[signal]
+ fn connected_changed(value: bool);
+
+ #[signal]
+ fn paired_changed(value: bool);
+
+ /// Create a new [BluetoothDevice] with the given DBus path
+ pub fn from_path(path: GString) -> Gd {
+ // Create a channel to communicate with the signals task
+ godot_print!("BluetoothDevice created with path: {path}");
+ let (tx, rx) = channel();
+ let dbus_path = path.clone().into();
+
+ // Spawn a task using the shared tokio runtime to listen for signals
+ RUNTIME.spawn(async move {
+ if let Err(e) = run(tx, dbus_path).await {
+ godot_error!("Failed to run BluetoothDevice task: ${e:?}");
+ }
+ });
+
+ Gd::from_init_fn(|base| {
+ // Create a connection to DBus
+ let conn = get_dbus_system_blocking().ok();
+
+ // Accept a base of type Base and directly forward it.
+ Self {
+ base,
+ rx,
+ conn,
+ dbus_path: path,
+ adapter: Default::default(),
+ address: Default::default(),
+ address_type: Default::default(),
+ alias: Default::default(),
+ appearance: Default::default(),
+ blocked: Default::default(),
+ bonded: Default::default(),
+ class: Default::default(),
+ connected: Default::default(),
+ icon: Default::default(),
+ legacy_pairing: Default::default(),
+ modalias: Default::default(),
+ name: Default::default(),
+ paired: Default::default(),
+ rssi: Default::default(),
+ services_resolved: Default::default(),
+ trusted: Default::default(),
+ tx_power: Default::default(),
+ uuids: Default::default(),
+ wake_allowed: Default::default(),
+ }
+ })
+ }
+
+ /// Return a proxy instance to the device
+ fn get_proxy(&self) -> Option {
+ if let Some(conn) = self.conn.as_ref() {
+ let path: String = self.dbus_path.clone().into();
+ Device1ProxyBlocking::builder(conn)
+ .path(path)
+ .ok()
+ .and_then(|builder| builder.build().ok())
+ } else {
+ None
+ }
+ }
+
+ /// Get or create a [BluetoothDevice] with the given DBus path. If an instance
+ /// already exists with the given path, then it will be loaded from the resource
+ /// cache.
+ pub fn new(path: &str) -> Gd {
+ let res_path = format!("dbus://{BLUEZ_BUS}{path}");
+
+ // Check to see if a resource already exists for this device
+ let mut resource_loader = ResourceLoader::singleton();
+ if resource_loader.exists(res_path.clone().into()) {
+ if let Some(res) = resource_loader.load(res_path.clone().into()) {
+ godot_print!(
+ "Resource already exists with path '{res_path}', loading that instead"
+ );
+ let device: Gd = res.cast();
+ device
+ } else {
+ let mut device = BluetoothDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ } else {
+ let mut device = BluetoothDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ }
+
+ /// Return the DBus path to the device
+ #[func]
+ pub fn get_dbus_path(&self) -> GString {
+ self.dbus_path.clone()
+ }
+
+ #[func]
+ pub fn cancel_pairing(&self) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.cancel_pairing().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn connect_to(&self) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.connect().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn connect_to_profile(&self, uuid: GString) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ let uuid = uuid.to_string();
+ proxy.connect_profile(uuid.as_str()).unwrap_or_default()
+ }
+
+ #[func]
+ pub fn disconnect_from(&self) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.disconnect().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn disconnect_from_profile(&self, uuid: GString) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ let uuid = uuid.to_string();
+ proxy.disconnect_profile(uuid.as_str()).unwrap_or_default()
+ }
+
+ #[func]
+ pub fn pair(&self) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.pair().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_wake_allowed(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.wake_allowed().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn set_wake_allowed(&self, allowed: bool) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.set_wake_allowed(allowed).unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_uuids(&self) -> PackedStringArray {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ let values: Vec = proxy
+ .uuids()
+ .unwrap_or_default()
+ .into_iter()
+ .map(|v| v.to_godot())
+ .collect();
+ values.into()
+ }
+
+ #[func]
+ pub fn get_tx_power(&self) -> i16 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.tx_power().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_trusted(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.trusted().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn set_trusted(&self, value: bool) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.set_trusted(value).unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_services_resolved(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.services_resolved().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_rssi(&self) -> i16 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.rssi().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_paired(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.paired().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_name(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.name().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_modalias(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.modalias().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_legacy_pairing(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.legacy_pairing().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_icon(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.icon().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_connected(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.connected().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_class(&self) -> u32 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.class().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_bonded(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.bonded().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_blocked(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.blocked().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn set_blocked(&self, value: bool) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.set_blocked(value).unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_appearance(&self) -> u16 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.appearance().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_alias(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.alias().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn set_alias(&self, value: GString) {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy
+ .set_alias(value.to_string().as_str())
+ .unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_address_type(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.address_type().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_address(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.address().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_adapter(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.adapter().unwrap_or_default().to_string().into()
+ }
+
+ /// Dispatches signals
+ pub fn process(&mut self) {
+ // Drain all messages from the channel to process them
+ loop {
+ let signal = match self.rx.try_recv() {
+ Ok(value) => value,
+ Err(e) => match e {
+ TryRecvError::Empty => break,
+ TryRecvError::Disconnected => {
+ godot_error!("Backend thread is not running!");
+ return;
+ }
+ },
+ };
+ self.process_signal(signal);
+ }
+ }
+
+ /// Process and dispatch the given signal
+ fn process_signal(&mut self, signal: Signal) {
+ godot_print!("Got signal: {signal:?}");
+ match signal {
+ Signal::ConnectedChanged { value } => {
+ self.base_mut()
+ .emit_signal("connected_changed".into(), &[value.to_variant()]);
+ }
+ Signal::PairedChanged { value } => {
+ self.base_mut()
+ .emit_signal("paired_changed".into(), &[value.to_variant()]);
+ }
+ }
+ }
+}
+
+impl Drop for BluetoothDevice {
+ fn drop(&mut self) {
+ godot_print!("BluetoothDevice '{}' is being destroyed!", self.dbus_path);
+ }
+}
+
+/// Run the signals task
+async fn run(tx: Sender, path: String) -> Result<(), Box> {
+ // Establish a connection to the system bus
+ let conn = get_dbus_system().await?;
+ let proxy = Device1Proxy::builder(&conn).path(path)?.build().await?;
+
+ let signals_tx = tx.clone();
+ let mut events = proxy.receive_connected_changed().await;
+ RUNTIME.spawn(async move {
+ while let Some(event) = events.next().await {
+ let value = event.get().await.unwrap_or_default();
+ let signal = Signal::ConnectedChanged { value };
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ let signals_tx = tx.clone();
+ let mut events = proxy.receive_paired_changed().await;
+ RUNTIME.spawn(async move {
+ while let Some(event) = events.next().await {
+ let value = event.get().await.unwrap_or_default();
+ let signal = Signal::PairedChanged { value };
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ Ok(())
+}
diff --git a/extensions/core/src/dbus.rs b/extensions/core/src/dbus.rs
new file mode 100644
index 00000000..320ccb59
--- /dev/null
+++ b/extensions/core/src/dbus.rs
@@ -0,0 +1,100 @@
+use godot::prelude::*;
+use zvariant::NoneValue;
+
+pub mod bluez;
+pub mod inputplumber;
+pub mod powerstation;
+pub mod upower;
+
+/// Possible DBus runtime errors
+#[derive(Debug)]
+pub enum RunError {
+ Zbus(zbus::Error),
+ ZbusFdo(zbus::fdo::Error),
+}
+
+impl From for RunError {
+ fn from(value: zbus::Error) -> Self {
+ RunError::Zbus(value)
+ }
+}
+
+impl From for RunError {
+ fn from(value: zbus::fdo::Error) -> Self {
+ RunError::ZbusFdo(value)
+ }
+}
+
+pub trait DBusVariant {
+ fn as_zvariant(&self) -> Option;
+}
+
+impl DBusVariant for Variant {
+ /// Convert the Godot variant type into a DBus variant type
+ fn as_zvariant(&self) -> Option {
+ match self.get_type() {
+ VariantType::NIL => {
+ let value = zvariant::Optional::<&str>::null_value();
+ Some(zvariant::Value::new(value))
+ }
+ VariantType::BOOL => {
+ let value: bool = self.to();
+ Some(zvariant::Value::new(value))
+ }
+ VariantType::INT => {
+ let value: i64 = self.to();
+ Some(zvariant::Value::new(value))
+ }
+ VariantType::FLOAT => {
+ let value: f64 = self.to();
+ Some(zvariant::Value::new(value))
+ }
+ VariantType::STRING => {
+ let value: GString = self.to();
+ let value: String = value.into();
+ Some(zvariant::Value::new(value))
+ }
+ VariantType::VECTOR2 => todo!(),
+ VariantType::VECTOR2I => todo!(),
+ VariantType::RECT2 => todo!(),
+ VariantType::RECT2I => todo!(),
+ VariantType::VECTOR3 => todo!(),
+ VariantType::VECTOR3I => todo!(),
+ VariantType::TRANSFORM2D => todo!(),
+ VariantType::VECTOR4 => todo!(),
+ VariantType::VECTOR4I => todo!(),
+ VariantType::PLANE => todo!(),
+ VariantType::QUATERNION => todo!(),
+ VariantType::AABB => todo!(),
+ VariantType::BASIS => todo!(),
+ VariantType::TRANSFORM3D => todo!(),
+ VariantType::PROJECTION => todo!(),
+ VariantType::COLOR => todo!(),
+ VariantType::STRING_NAME => todo!(),
+ VariantType::NODE_PATH => todo!(),
+ VariantType::RID => {
+ let value: i64 = self.to();
+ Some(zvariant::Value::new(value))
+ }
+ VariantType::OBJECT => todo!(),
+ VariantType::CALLABLE => todo!(),
+ VariantType::SIGNAL => todo!(),
+ VariantType::DICTIONARY => todo!(),
+ VariantType::ARRAY => todo!(),
+ VariantType::PACKED_BYTE_ARRAY => todo!(),
+ VariantType::PACKED_INT32_ARRAY => todo!(),
+ VariantType::PACKED_INT64_ARRAY => todo!(),
+ VariantType::PACKED_FLOAT32_ARRAY => todo!(),
+ VariantType::PACKED_FLOAT64_ARRAY => todo!(),
+ VariantType::PACKED_STRING_ARRAY => todo!(),
+ VariantType::PACKED_VECTOR2_ARRAY => todo!(),
+ VariantType::PACKED_VECTOR3_ARRAY => todo!(),
+ VariantType::PACKED_COLOR_ARRAY => todo!(),
+ VariantType::PACKED_VECTOR4_ARRAY => todo!(),
+ VariantType::MAX => todo!(),
+
+ // Unsupported conversion
+ _ => None,
+ }
+ }
+}
diff --git a/extensions/core/src/dbus/bluez.rs b/extensions/core/src/dbus/bluez.rs
new file mode 100644
index 00000000..bce2e071
--- /dev/null
+++ b/extensions/core/src/dbus/bluez.rs
@@ -0,0 +1,8 @@
+pub mod adapter1;
+pub mod battery_provider_manager1;
+pub mod device1;
+pub mod gatt_manager1;
+pub mod leadvertising_manager1;
+pub mod media1;
+pub mod media_control1;
+pub mod network_server1;
diff --git a/extensions/core/src/dbus/bluez/adapter1.rs b/extensions/core/src/dbus/bluez/adapter1.rs
new file mode 100644
index 00000000..79e76a19
--- /dev/null
+++ b/extensions/core/src/dbus/bluez/adapter1.rs
@@ -0,0 +1,129 @@
+//! # D-Bus interface proxy for: `org.bluez.Adapter1`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/bluez/hci0' from service 'org.bluez' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.bluez.Adapter1",
+ default_service = "org.bluez",
+ default_path = "/org/bluez/hci0"
+)]
+trait Adapter1 {
+ /// GetDiscoveryFilters method
+ fn get_discovery_filters(&self) -> zbus::Result>;
+
+ /// RemoveDevice method
+ fn remove_device(&self, device: &zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>;
+
+ /// SetDiscoveryFilter method
+ fn set_discovery_filter(
+ &self,
+ properties: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>,
+ ) -> zbus::Result<()>;
+
+ /// StartDiscovery method
+ fn start_discovery(&self) -> zbus::Result<()>;
+
+ /// StopDiscovery method
+ fn stop_discovery(&self) -> zbus::Result<()>;
+
+ /// Address property
+ #[zbus(property)]
+ fn address(&self) -> zbus::Result;
+
+ /// AddressType property
+ #[zbus(property)]
+ fn address_type(&self) -> zbus::Result;
+
+ /// Alias property
+ #[zbus(property)]
+ fn alias(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_alias(&self, value: &str) -> zbus::Result<()>;
+
+ /// Class property
+ #[zbus(property)]
+ fn class(&self) -> zbus::Result;
+
+ /// Discoverable property
+ #[zbus(property)]
+ fn discoverable(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_discoverable(&self, value: bool) -> zbus::Result<()>;
+
+ /// DiscoverableTimeout property
+ #[zbus(property)]
+ fn discoverable_timeout(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_discoverable_timeout(&self, value: u32) -> zbus::Result<()>;
+
+ /// Discovering property
+ #[zbus(property)]
+ fn discovering(&self) -> zbus::Result;
+
+ /// ExperimentalFeatures property
+ #[zbus(property)]
+ fn experimental_features(&self) -> zbus::Result>;
+
+ /// Manufacturer property
+ #[zbus(property)]
+ fn manufacturer(&self) -> zbus::Result;
+
+ /// Modalias property
+ #[zbus(property)]
+ fn modalias(&self) -> zbus::Result;
+
+ /// Name property
+ #[zbus(property)]
+ fn name(&self) -> zbus::Result;
+
+ /// Pairable property
+ #[zbus(property)]
+ fn pairable(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_pairable(&self, value: bool) -> zbus::Result<()>;
+
+ /// PairableTimeout property
+ #[zbus(property)]
+ fn pairable_timeout(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_pairable_timeout(&self, value: u32) -> zbus::Result<()>;
+
+ /// PowerState property
+ #[zbus(property)]
+ fn power_state(&self) -> zbus::Result;
+
+ /// Powered property
+ #[zbus(property)]
+ fn powered(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_powered(&self, value: bool) -> zbus::Result<()>;
+
+ /// Roles property
+ #[zbus(property)]
+ fn roles(&self) -> zbus::Result>;
+
+ /// UUIDs property
+ #[zbus(property, name = "UUIDs")]
+ fn uuids(&self) -> zbus::Result>;
+
+ /// Version property
+ #[zbus(property)]
+ fn version(&self) -> zbus::Result;
+}
diff --git a/extensions/core/src/dbus/bluez/battery_provider_manager1.rs b/extensions/core/src/dbus/bluez/battery_provider_manager1.rs
new file mode 100644
index 00000000..c47ac8f7
--- /dev/null
+++ b/extensions/core/src/dbus/bluez/battery_provider_manager1.rs
@@ -0,0 +1,39 @@
+//! # D-Bus interface proxy for: `org.bluez.BatteryProviderManager1`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/bluez/hci0' from service 'org.bluez' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.bluez.BatteryProviderManager1",
+ default_service = "org.bluez",
+ default_path = "/org/bluez/hci0"
+)]
+trait BatteryProviderManager1 {
+ /// RegisterBatteryProvider method
+ fn register_battery_provider(
+ &self,
+ provider: &zbus::zvariant::ObjectPath<'_>,
+ ) -> zbus::Result<()>;
+
+ /// UnregisterBatteryProvider method
+ fn unregister_battery_provider(
+ &self,
+ provider: &zbus::zvariant::ObjectPath<'_>,
+ ) -> zbus::Result<()>;
+}
diff --git a/extensions/core/src/dbus/bluez/device1.rs b/extensions/core/src/dbus/bluez/device1.rs
new file mode 100644
index 00000000..06c5300d
--- /dev/null
+++ b/extensions/core/src/dbus/bluez/device1.rs
@@ -0,0 +1,152 @@
+//! # D-Bus interface proxy for: `org.bluez.Device1`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/bluez/hci0/dev_XX...' from service 'org.bluez' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(interface = "org.bluez.Device1", default_service = "org.bluez")]
+trait Device1 {
+ /// CancelPairing method
+ fn cancel_pairing(&self) -> zbus::Result<()>;
+
+ /// Connect method
+ fn connect(&self) -> zbus::Result<()>;
+
+ /// ConnectProfile method
+ fn connect_profile(&self, UUID: &str) -> zbus::Result<()>;
+
+ /// Disconnect method
+ fn disconnect(&self) -> zbus::Result<()>;
+
+ /// DisconnectProfile method
+ fn disconnect_profile(&self, UUID: &str) -> zbus::Result<()>;
+
+ /// Pair method
+ fn pair(&self) -> zbus::Result<()>;
+
+ /// Adapter property
+ #[zbus(property)]
+ fn adapter(&self) -> zbus::Result;
+
+ /// Address property
+ #[zbus(property)]
+ fn address(&self) -> zbus::Result;
+
+ /// AddressType property
+ #[zbus(property)]
+ fn address_type(&self) -> zbus::Result;
+
+ /// Alias property
+ #[zbus(property)]
+ fn alias(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_alias(&self, value: &str) -> zbus::Result<()>;
+
+ /// Appearance property
+ #[zbus(property)]
+ fn appearance(&self) -> zbus::Result;
+
+ /// Blocked property
+ #[zbus(property)]
+ fn blocked(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_blocked(&self, value: bool) -> zbus::Result<()>;
+
+ /// Bonded property
+ #[zbus(property)]
+ fn bonded(&self) -> zbus::Result;
+
+ /// Class property
+ #[zbus(property)]
+ fn class(&self) -> zbus::Result;
+
+ /// Connected property
+ #[zbus(property)]
+ fn connected(&self) -> zbus::Result;
+
+ /// Icon property
+ #[zbus(property)]
+ fn icon(&self) -> zbus::Result;
+
+ /// LegacyPairing property
+ #[zbus(property)]
+ fn legacy_pairing(&self) -> zbus::Result;
+
+ /// ManufacturerData property
+ #[zbus(property)]
+ fn manufacturer_data(
+ &self,
+ ) -> zbus::Result>;
+
+ /// Modalias property
+ #[zbus(property)]
+ fn modalias(&self) -> zbus::Result;
+
+ /// Name property
+ #[zbus(property)]
+ fn name(&self) -> zbus::Result;
+
+ /// Paired property
+ #[zbus(property)]
+ fn paired(&self) -> zbus::Result;
+
+ /// RSSI property
+ #[zbus(property, name = "RSSI")]
+ fn rssi(&self) -> zbus::Result;
+
+ /// ServiceData property
+ #[zbus(property)]
+ fn service_data(
+ &self,
+ ) -> zbus::Result>;
+
+ /// ServicesResolved property
+ #[zbus(property)]
+ fn services_resolved(&self) -> zbus::Result;
+
+ /// Sets property
+ #[zbus(property)]
+ fn sets(
+ &self,
+ ) -> zbus::Result<
+ std::collections::HashMap<
+ zbus::zvariant::OwnedObjectPath,
+ std::collections::HashMap,
+ >,
+ >;
+
+ /// Trusted property
+ #[zbus(property)]
+ fn trusted(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_trusted(&self, value: bool) -> zbus::Result<()>;
+
+ /// TxPower property
+ #[zbus(property)]
+ fn tx_power(&self) -> zbus::Result;
+
+ /// UUIDs property
+ #[zbus(property, name = "UUIDs")]
+ fn uuids(&self) -> zbus::Result>;
+
+ /// WakeAllowed property
+ #[zbus(property)]
+ fn wake_allowed(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_wake_allowed(&self, value: bool) -> zbus::Result<()>;
+}
diff --git a/extensions/core/src/dbus/bluez/gatt_manager1.rs b/extensions/core/src/dbus/bluez/gatt_manager1.rs
new file mode 100644
index 00000000..bf0153d7
--- /dev/null
+++ b/extensions/core/src/dbus/bluez/gatt_manager1.rs
@@ -0,0 +1,40 @@
+//! # D-Bus interface proxy for: `org.bluez.GattManager1`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/bluez/hci0' from service 'org.bluez' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.bluez.GattManager1",
+ default_service = "org.bluez",
+ default_path = "/org/bluez/hci0"
+)]
+trait GattManager1 {
+ /// RegisterApplication method
+ fn register_application(
+ &self,
+ application: &zbus::zvariant::ObjectPath<'_>,
+ options: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>,
+ ) -> zbus::Result<()>;
+
+ /// UnregisterApplication method
+ fn unregister_application(
+ &self,
+ application: &zbus::zvariant::ObjectPath<'_>,
+ ) -> zbus::Result<()>;
+}
diff --git a/extensions/core/src/dbus/bluez/leadvertising_manager1.rs b/extensions/core/src/dbus/bluez/leadvertising_manager1.rs
new file mode 100644
index 00000000..da3ddbb6
--- /dev/null
+++ b/extensions/core/src/dbus/bluez/leadvertising_manager1.rs
@@ -0,0 +1,56 @@
+//! # D-Bus interface proxy for: `org.bluez.LEAdvertisingManager1`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/bluez/hci0' from service 'org.bluez' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.bluez.LEAdvertisingManager1",
+ default_service = "org.bluez",
+ default_path = "/org/bluez/hci0"
+)]
+trait LEAdvertisingManager1 {
+ /// RegisterAdvertisement method
+ fn register_advertisement(
+ &self,
+ advertisement: &zbus::zvariant::ObjectPath<'_>,
+ options: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>,
+ ) -> zbus::Result<()>;
+
+ /// UnregisterAdvertisement method
+ fn unregister_advertisement(
+ &self,
+ service: &zbus::zvariant::ObjectPath<'_>,
+ ) -> zbus::Result<()>;
+
+ /// ActiveInstances property
+ #[zbus(property)]
+ fn active_instances(&self) -> zbus::Result;
+
+ /// SupportedIncludes property
+ #[zbus(property)]
+ fn supported_includes(&self) -> zbus::Result>;
+
+ /// SupportedInstances property
+ #[zbus(property)]
+ fn supported_instances(&self) -> zbus::Result;
+
+ /// SupportedSecondaryChannels property
+ #[zbus(property)]
+ fn supported_secondary_channels(&self) -> zbus::Result>;
+}
diff --git a/extensions/core/src/dbus/bluez/media1.rs b/extensions/core/src/dbus/bluez/media1.rs
new file mode 100644
index 00000000..ba045686
--- /dev/null
+++ b/extensions/core/src/dbus/bluez/media1.rs
@@ -0,0 +1,64 @@
+//! # D-Bus interface proxy for: `org.bluez.Media1`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/bluez/hci0' from service 'org.bluez' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.bluez.Media1",
+ default_service = "org.bluez",
+ default_path = "/org/bluez/hci0"
+)]
+trait Media1 {
+ /// RegisterApplication method
+ fn register_application(
+ &self,
+ application: &zbus::zvariant::ObjectPath<'_>,
+ options: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>,
+ ) -> zbus::Result<()>;
+
+ /// RegisterEndpoint method
+ fn register_endpoint(
+ &self,
+ endpoint: &zbus::zvariant::ObjectPath<'_>,
+ properties: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>,
+ ) -> zbus::Result<()>;
+
+ /// RegisterPlayer method
+ fn register_player(
+ &self,
+ player: &zbus::zvariant::ObjectPath<'_>,
+ properties: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>,
+ ) -> zbus::Result<()>;
+
+ /// UnregisterApplication method
+ fn unregister_application(
+ &self,
+ application: &zbus::zvariant::ObjectPath<'_>,
+ ) -> zbus::Result<()>;
+
+ /// UnregisterEndpoint method
+ fn unregister_endpoint(&self, endpoint: &zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>;
+
+ /// UnregisterPlayer method
+ fn unregister_player(&self, player: &zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>;
+
+ /// SupportedUUIDs property
+ #[zbus(property, name = "SupportedUUIDs")]
+ fn supported_uuids(&self) -> zbus::Result>;
+}
diff --git a/extensions/core/src/dbus/bluez/media_control1.rs b/extensions/core/src/dbus/bluez/media_control1.rs
new file mode 100644
index 00000000..77583bff
--- /dev/null
+++ b/extensions/core/src/dbus/bluez/media_control1.rs
@@ -0,0 +1,58 @@
+//! # D-Bus interface proxy for: `org.bluez.MediaControl1`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/bluez/hci0/dev_XX...' from service 'org.bluez' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(interface = "org.bluez.MediaControl1", default_service = "org.bluez")]
+trait MediaControl1 {
+ /// FastForward method
+ fn fast_forward(&self) -> zbus::Result<()>;
+
+ /// Next method
+ fn next(&self) -> zbus::Result<()>;
+
+ /// Pause method
+ fn pause(&self) -> zbus::Result<()>;
+
+ /// Play method
+ fn play(&self) -> zbus::Result<()>;
+
+ /// Previous method
+ fn previous(&self) -> zbus::Result<()>;
+
+ /// Rewind method
+ fn rewind(&self) -> zbus::Result<()>;
+
+ /// Stop method
+ fn stop(&self) -> zbus::Result<()>;
+
+ /// VolumeDown method
+ fn volume_down(&self) -> zbus::Result<()>;
+
+ /// VolumeUp method
+ fn volume_up(&self) -> zbus::Result<()>;
+
+ /// Connected property
+ #[zbus(property)]
+ fn connected(&self) -> zbus::Result;
+
+ /// Player property
+ #[zbus(property)]
+ fn player(&self) -> zbus::Result;
+}
diff --git a/extensions/core/src/dbus/bluez/network_server1.rs b/extensions/core/src/dbus/bluez/network_server1.rs
new file mode 100644
index 00000000..22a9f13a
--- /dev/null
+++ b/extensions/core/src/dbus/bluez/network_server1.rs
@@ -0,0 +1,33 @@
+//! # D-Bus interface proxy for: `org.bluez.NetworkServer1`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/bluez/hci0' from service 'org.bluez' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.bluez.NetworkServer1",
+ default_service = "org.bluez",
+ default_path = "/org/bluez/hci0"
+)]
+trait NetworkServer1 {
+ /// Register method
+ fn register(&self, uuid: &str, bridge: &str) -> zbus::Result<()>;
+
+ /// Unregister method
+ fn unregister(&self, uuid: &str) -> zbus::Result<()>;
+}
diff --git a/extensions/core/src/dbus/inputplumber.rs b/extensions/core/src/dbus/inputplumber.rs
new file mode 100644
index 00000000..e722fce8
--- /dev/null
+++ b/extensions/core/src/dbus/inputplumber.rs
@@ -0,0 +1,6 @@
+pub mod composite_device;
+pub mod dbus_device;
+pub mod event_device;
+pub mod input_manager;
+pub mod keyboard;
+pub mod mouse;
diff --git a/extensions/core/src/dbus/inputplumber/composite_device.rs b/extensions/core/src/dbus/inputplumber/composite_device.rs
new file mode 100644
index 00000000..bfb9ea4b
--- /dev/null
+++ b/extensions/core/src/dbus/inputplumber/composite_device.rs
@@ -0,0 +1,87 @@
+//! # D-Bus interface proxy for: `org.shadowblip.Input.CompositeDevice`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/InputPlumber/CompositeDevice0' from service 'org.shadowblip.InputPlumber' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PeerProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.Input.CompositeDevice",
+ default_service = "org.shadowblip.InputPlumber",
+ default_path = "/org/shadowblip/InputPlumber/CompositeDevice0"
+)]
+trait CompositeDevice {
+ /// LoadProfileFromYaml method
+ fn load_profile_from_yaml(&self, profile: &str) -> zbus::Result<()>;
+
+ /// LoadProfilePath method
+ fn load_profile_path(&self, path: &str) -> zbus::Result<()>;
+
+ /// SendButtonChord method
+ fn send_button_chord(&self, events: &[&str]) -> zbus::Result<()>;
+
+ /// SendEvent method
+ fn send_event(&self, event: &str, value: &zbus::zvariant::Value<'_>) -> zbus::Result<()>;
+
+ /// SetInterceptActivation method
+ fn set_intercept_activation(
+ &self,
+ activation_events: &[&str],
+ target_event: &str,
+ ) -> zbus::Result<()>;
+
+ /// SetTargetDevices method
+ fn set_target_devices(&self, target_device_types: &[&str]) -> zbus::Result<()>;
+
+ /// Stop method
+ fn stop(&self) -> zbus::Result<()>;
+
+ /// Capabilities property
+ #[zbus(property)]
+ fn capabilities(&self) -> zbus::Result>;
+
+ /// DbusDevices property
+ #[zbus(property)]
+ fn dbus_devices(&self) -> zbus::Result>;
+
+ /// InterceptMode property
+ #[zbus(property)]
+ fn intercept_mode(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_intercept_mode(&self, value: u32) -> zbus::Result<()>;
+
+ /// Name property
+ #[zbus(property)]
+ fn name(&self) -> zbus::Result;
+
+ /// ProfileName property
+ #[zbus(property)]
+ fn profile_name(&self) -> zbus::Result;
+
+ /// SourceDevicePaths property
+ #[zbus(property)]
+ fn source_device_paths(&self) -> zbus::Result>;
+
+ /// TargetCapabilities property
+ #[zbus(property)]
+ fn target_capabilities(&self) -> zbus::Result>;
+
+ /// TargetDevices property
+ #[zbus(property)]
+ fn target_devices(&self) -> zbus::Result>;
+}
diff --git a/extensions/core/src/dbus/inputplumber/dbus_device.rs b/extensions/core/src/dbus/inputplumber/dbus_device.rs
new file mode 100644
index 00000000..56f05450
--- /dev/null
+++ b/extensions/core/src/dbus/inputplumber/dbus_device.rs
@@ -0,0 +1,48 @@
+//! # D-Bus interface proxy for: `org.shadowblip.Input.DBusDevice`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/InputPlumber/devices/target/dbus0' from service 'org.shadowblip.InputPlumber' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PeerProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.Input.DBusDevice",
+ default_service = "org.shadowblip.InputPlumber",
+ default_path = "/org/shadowblip/InputPlumber/devices/target/dbus0"
+)]
+trait DBusDevice {
+ /// InputEvent signal
+ #[zbus(signal)]
+ fn input_event(&self, event: &str, value: f64) -> zbus::Result<()>;
+
+ /// TouchEvent signal
+ #[zbus(signal)]
+ fn touch_event(
+ &self,
+ event: &str,
+ index: u32,
+ is_touching: bool,
+ pressure: f64,
+ x: f64,
+ y: f64,
+ ) -> zbus::Result<()>;
+
+ /// Name property
+ #[zbus(property)]
+ fn name(&self) -> zbus::Result;
+}
diff --git a/extensions/core/src/dbus/inputplumber/event_device.rs b/extensions/core/src/dbus/inputplumber/event_device.rs
new file mode 100644
index 00000000..5b435da7
--- /dev/null
+++ b/extensions/core/src/dbus/inputplumber/event_device.rs
@@ -0,0 +1,64 @@
+//! # D-Bus interface proxy for: `org.shadowblip.Input.Source.EventDevice`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/InputPlumber/devices/source/event9' from service 'org.shadowblip.InputPlumber' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PeerProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.Input.Source.EventDevice",
+ default_service = "org.shadowblip.InputPlumber",
+ default_path = "/org/shadowblip/InputPlumber/devices/source/event9"
+)]
+trait EventDevice {
+ /// DevicePath property
+ #[zbus(property)]
+ fn device_path(&self) -> zbus::Result;
+
+ /// IdBustype property
+ #[zbus(property)]
+ fn id_bustype(&self) -> zbus::Result;
+
+ /// IdProduct property
+ #[zbus(property)]
+ fn id_product(&self) -> zbus::Result;
+
+ /// IdVendor property
+ #[zbus(property)]
+ fn id_vendor(&self) -> zbus::Result;
+
+ /// IdVersion property
+ #[zbus(property)]
+ fn id_version(&self) -> zbus::Result;
+
+ /// Name property
+ #[zbus(property)]
+ fn name(&self) -> zbus::Result;
+
+ /// PhysPath property
+ #[zbus(property)]
+ fn phys_path(&self) -> zbus::Result;
+
+ /// SysfsPath property
+ #[zbus(property)]
+ fn sysfs_path(&self) -> zbus::Result;
+
+ /// UniqueId property
+ #[zbus(property)]
+ fn unique_id(&self) -> zbus::Result;
+}
diff --git a/extensions/core/src/dbus/inputplumber/gamepad.rs b/extensions/core/src/dbus/inputplumber/gamepad.rs
new file mode 100644
index 00000000..207174b1
--- /dev/null
+++ b/extensions/core/src/dbus/inputplumber/gamepad.rs
@@ -0,0 +1,32 @@
+//! # D-Bus interface proxy for: `org.shadowblip.Input.Gamepad`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/InputPlumber/devices/target/gamepad0' from service 'org.shadowblip.InputPlumber' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PeerProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.Input.Gamepad",
+ default_service = "org.shadowblip.InputPlumber",
+ default_path = "/org/shadowblip/InputPlumber/devices/target/gamepad0"
+)]
+trait Gamepad {
+ /// Name property
+ #[zbus(property)]
+ fn name(&self) -> zbus::Result;
+}
diff --git a/extensions/core/src/dbus/inputplumber/hidraw_device.rs b/extensions/core/src/dbus/inputplumber/hidraw_device.rs
new file mode 100644
index 00000000..f0a50da5
--- /dev/null
+++ b/extensions/core/src/dbus/inputplumber/hidraw_device.rs
@@ -0,0 +1,64 @@
+//! # D-Bus interface proxy for: `org.shadowblip.Input.Source.HIDRawDevice`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/InputPlumber/devices/source/hidraw0' from service 'org.shadowblip.InputPlumber' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::PeerProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.Input.Source.HIDRawDevice",
+ default_service = "org.shadowblip.InputPlumber",
+ default_path = "/org/shadowblip/InputPlumber/devices/source/hidraw0"
+)]
+trait HIDRawDevice {
+ /// DevPath property
+ #[zbus(property)]
+ fn dev_path(&self) -> zbus::Result;
+
+ /// IdProduct property
+ #[zbus(property)]
+ fn id_product(&self) -> zbus::Result;
+
+ /// IdVendor property
+ #[zbus(property)]
+ fn id_vendor(&self) -> zbus::Result;
+
+ /// InterfaceNumber property
+ #[zbus(property)]
+ fn interface_number(&self) -> zbus::Result;
+
+ /// Manufacturer property
+ #[zbus(property)]
+ fn manufacturer(&self) -> zbus::Result;
+
+ /// Name property
+ #[zbus(property)]
+ fn name(&self) -> zbus::Result;
+
+ /// Product property
+ #[zbus(property)]
+ fn product(&self) -> zbus::Result;
+
+ /// SerialNumber property
+ #[zbus(property)]
+ fn serial_number(&self) -> zbus::Result;
+
+ /// SysfsPath property
+ #[zbus(property)]
+ fn sysfs_path(&self) -> zbus::Result;
+}
diff --git a/extensions/core/src/dbus/inputplumber/iioimudevice.rs b/extensions/core/src/dbus/inputplumber/iioimudevice.rs
new file mode 100644
index 00000000..7dbfdfdf
--- /dev/null
+++ b/extensions/core/src/dbus/inputplumber/iioimudevice.rs
@@ -0,0 +1,76 @@
+//! # D-Bus interface proxy for: `org.shadowblip.Input.Source.IIOIMUDevice`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/InputPlumber/devices/source/iio_device0' from service 'org.shadowblip.InputPlumber' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PeerProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.Input.Source.IIOIMUDevice",
+ default_service = "org.shadowblip.InputPlumber",
+ default_path = "/org/shadowblip/InputPlumber/devices/source/iio_device0"
+)]
+trait IIOIMUDevice {
+ /// AccelSampleRate property
+ #[zbus(property)]
+ fn accel_sample_rate(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_accel_sample_rate(&self, value: f64) -> zbus::Result<()>;
+
+ /// AccelSampleRatesAvail property
+ #[zbus(property)]
+ fn accel_sample_rates_avail(&self) -> zbus::Result>;
+
+ /// AccelScale property
+ #[zbus(property)]
+ fn accel_scale(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_accel_scale(&self, value: f64) -> zbus::Result<()>;
+
+ /// AccelScalesAvail property
+ #[zbus(property)]
+ fn accel_scales_avail(&self) -> zbus::Result>;
+
+ /// AngvelSampleRate property
+ #[zbus(property)]
+ fn angvel_sample_rate(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_angvel_sample_rate(&self, value: f64) -> zbus::Result<()>;
+
+ /// AngvelSampleRatesAvail property
+ #[zbus(property)]
+ fn angvel_sample_rates_avail(&self) -> zbus::Result>;
+
+ /// AngvelScale property
+ #[zbus(property)]
+ fn angvel_scale(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_angvel_scale(&self, value: f64) -> zbus::Result<()>;
+
+ /// AngvelScalesAvail property
+ #[zbus(property)]
+ fn angvel_scales_avail(&self) -> zbus::Result>;
+
+ /// Id property
+ #[zbus(property)]
+ fn id(&self) -> zbus::Result;
+
+ /// Name property
+ #[zbus(property)]
+ fn name(&self) -> zbus::Result;
+}
diff --git a/extensions/core/src/dbus/inputplumber/input_manager.rs b/extensions/core/src/dbus/inputplumber/input_manager.rs
new file mode 100644
index 00000000..49b1c383
--- /dev/null
+++ b/extensions/core/src/dbus/inputplumber/input_manager.rs
@@ -0,0 +1,52 @@
+//! # D-Bus interface proxy for: `org.shadowblip.InputManager`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/InputPlumber/Manager' from service 'org.shadowblip.InputPlumber' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::PeerProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.InputManager",
+ default_service = "org.shadowblip.InputPlumber",
+ default_path = "/org/shadowblip/InputPlumber/Manager"
+)]
+trait InputManager {
+ /// AttachTargetDevice method
+ fn attach_target_device(&self, target_path: &str, composite_path: &str) -> zbus::Result<()>;
+
+ /// CreateCompositeDevice method
+ fn create_composite_device(&self, config_path: &str) -> zbus::Result;
+
+ /// CreateTargetDevice method
+ fn create_target_device(&self, kind: &str) -> zbus::Result;
+
+ /// StopTargetDevice method
+ fn stop_target_device(&self, path: &str) -> zbus::Result<()>;
+
+ /// InterceptMode property
+ #[zbus(property)]
+ fn intercept_mode(&self) -> zbus::Result;
+
+ /// SupportedTargetDeviceIds property
+ #[zbus(property)]
+ fn supported_target_device_ids(&self) -> zbus::Result>;
+
+ /// SupportedTargetDevices property
+ #[zbus(property)]
+ fn supported_target_devices(&self) -> zbus::Result>;
+}
diff --git a/extensions/core/src/dbus/inputplumber/keyboard.rs b/extensions/core/src/dbus/inputplumber/keyboard.rs
new file mode 100644
index 00000000..3da27d52
--- /dev/null
+++ b/extensions/core/src/dbus/inputplumber/keyboard.rs
@@ -0,0 +1,35 @@
+//! # D-Bus interface proxy for: `org.shadowblip.Input.Keyboard`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/InputPlumber/devices/target/keyboard0' from service 'org.shadowblip.InputPlumber' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PeerProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.Input.Keyboard",
+ default_service = "org.shadowblip.InputPlumber",
+ default_path = "/org/shadowblip/InputPlumber/devices/target/keyboard0"
+)]
+trait Keyboard {
+ /// SendKey method
+ fn send_key(&self, key: &str, value: bool) -> zbus::Result<()>;
+
+ /// Name property
+ #[zbus(property)]
+ fn name(&self) -> zbus::Result;
+}
diff --git a/extensions/core/src/dbus/inputplumber/mouse.rs b/extensions/core/src/dbus/inputplumber/mouse.rs
new file mode 100644
index 00000000..8ffe1b2d
--- /dev/null
+++ b/extensions/core/src/dbus/inputplumber/mouse.rs
@@ -0,0 +1,35 @@
+//! # D-Bus interface proxy for: `org.shadowblip.Input.Mouse`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/InputPlumber/devices/target/mouse0' from service 'org.shadowblip.InputPlumber' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::PeerProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.Input.Mouse",
+ default_service = "org.shadowblip.InputPlumber",
+ default_path = "/org/shadowblip/InputPlumber/devices/target/mouse0"
+)]
+trait Mouse {
+ /// MoveCursor method
+ fn move_cursor(&self, x: i32, y: i32) -> zbus::Result<()>;
+
+ /// Name property
+ #[zbus(property)]
+ fn name(&self) -> zbus::Result;
+}
diff --git a/extensions/core/src/dbus/powerstation.rs b/extensions/core/src/dbus/powerstation.rs
new file mode 100644
index 00000000..3ae387c0
--- /dev/null
+++ b/extensions/core/src/dbus/powerstation.rs
@@ -0,0 +1,6 @@
+pub mod card;
+pub mod connector;
+pub mod core;
+pub mod cpu;
+pub mod gpu;
+pub mod tdp;
diff --git a/extensions/core/src/dbus/powerstation/card.rs b/extensions/core/src/dbus/powerstation/card.rs
new file mode 100644
index 00000000..0634b4d5
--- /dev/null
+++ b/extensions/core/src/dbus/powerstation/card.rs
@@ -0,0 +1,105 @@
+//! # D-Bus interface proxy for: `org.shadowblip.GPU.Card`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/Performance/GPU/card0' from service 'org.shadowblip.PowerStation' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PeerProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.GPU.Card",
+ default_service = "org.shadowblip.PowerStation",
+ default_path = "/org/shadowblip/Performance/GPU/card0"
+)]
+trait Card {
+ /// EnumerateConnectors method
+ fn enumerate_connectors(&self) -> zbus::Result>;
+
+ /// Class property
+ #[zbus(property)]
+ fn class(&self) -> zbus::Result;
+
+ /// ClassId property
+ #[zbus(property)]
+ fn class_id(&self) -> zbus::Result;
+
+ /// ClockLimitMhzMax property
+ #[zbus(property)]
+ fn clock_limit_mhz_max(&self) -> zbus::Result;
+
+ /// ClockLimitMhzMin property
+ #[zbus(property)]
+ fn clock_limit_mhz_min(&self) -> zbus::Result;
+
+ /// ClockValueMhzMax property
+ #[zbus(property)]
+ fn clock_value_mhz_max(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_clock_value_mhz_max(&self, value: f64) -> zbus::Result<()>;
+
+ /// ClockValueMhzMin property
+ #[zbus(property)]
+ fn clock_value_mhz_min(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_clock_value_mhz_min(&self, value: f64) -> zbus::Result<()>;
+
+ /// Device property
+ #[zbus(property)]
+ fn device(&self) -> zbus::Result;
+
+ /// DeviceId property
+ #[zbus(property)]
+ fn device_id(&self) -> zbus::Result;
+
+ /// ManualClock property
+ #[zbus(property)]
+ fn manual_clock(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_manual_clock(&self, value: bool) -> zbus::Result<()>;
+
+ /// Name property
+ #[zbus(property)]
+ fn name(&self) -> zbus::Result;
+
+ /// Path property
+ #[zbus(property)]
+ fn path(&self) -> zbus::Result;
+
+ /// RevisionId property
+ #[zbus(property)]
+ fn revision_id(&self) -> zbus::Result;
+
+ /// Subdevice property
+ #[zbus(property)]
+ fn subdevice(&self) -> zbus::Result;
+
+ /// SubdeviceId property
+ #[zbus(property)]
+ fn subdevice_id(&self) -> zbus::Result;
+
+ /// SubvendorId property
+ #[zbus(property)]
+ fn subvendor_id(&self) -> zbus::Result;
+
+ /// Vendor property
+ #[zbus(property)]
+ fn vendor(&self) -> zbus::Result;
+
+ /// VendorId property
+ #[zbus(property)]
+ fn vendor_id(&self) -> zbus::Result;
+}
diff --git a/extensions/core/src/dbus/powerstation/connector.rs b/extensions/core/src/dbus/powerstation/connector.rs
new file mode 100644
index 00000000..aea3599a
--- /dev/null
+++ b/extensions/core/src/dbus/powerstation/connector.rs
@@ -0,0 +1,56 @@
+//! # D-Bus interface proxy for: `org.shadowblip.GPU.Card.Connector`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/Performance/GPU/card0/DP/1' from service 'org.shadowblip.PowerStation' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PeerProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.GPU.Card.Connector",
+ default_service = "org.shadowblip.PowerStation",
+ default_path = "/org/shadowblip/Performance/GPU/card0/DP/1"
+)]
+trait Connector {
+ /// DPMS property
+ #[zbus(property, name = "DPMS")]
+ fn dpms(&self) -> zbus::Result;
+
+ /// Enabled property
+ #[zbus(property)]
+ fn enabled(&self) -> zbus::Result;
+
+ /// Id property
+ #[zbus(property)]
+ fn id(&self) -> zbus::Result;
+
+ /// Modes property
+ #[zbus(property)]
+ fn modes(&self) -> zbus::Result>;
+
+ /// Name property
+ #[zbus(property)]
+ fn name(&self) -> zbus::Result;
+
+ /// Path property
+ #[zbus(property)]
+ fn path(&self) -> zbus::Result;
+
+ /// Status property
+ #[zbus(property)]
+ fn status(&self) -> zbus::Result;
+}
diff --git a/extensions/core/src/dbus/powerstation/core.rs b/extensions/core/src/dbus/powerstation/core.rs
new file mode 100644
index 00000000..3790bdb6
--- /dev/null
+++ b/extensions/core/src/dbus/powerstation/core.rs
@@ -0,0 +1,42 @@
+//! # D-Bus interface proxy for: `org.shadowblip.CPU.Core`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/Performance/CPU/Core0' from service 'org.shadowblip.PowerStation' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PeerProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.CPU.Core",
+ default_service = "org.shadowblip.PowerStation",
+ default_path = "/org/shadowblip/Performance/CPU/Core0"
+)]
+trait Core {
+ /// CoreId property
+ #[zbus(property)]
+ fn core_id(&self) -> zbus::Result;
+
+ /// Number property
+ #[zbus(property)]
+ fn number(&self) -> zbus::Result;
+
+ /// Online property
+ #[zbus(property)]
+ fn online(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_online(&self, value: bool) -> zbus::Result<()>;
+}
diff --git a/extensions/core/src/dbus/powerstation/cpu.rs b/extensions/core/src/dbus/powerstation/cpu.rs
new file mode 100644
index 00000000..b4f9d117
--- /dev/null
+++ b/extensions/core/src/dbus/powerstation/cpu.rs
@@ -0,0 +1,60 @@
+//! # D-Bus interface proxy for: `org.shadowblip.CPU`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/Performance/CPU' from service 'org.shadowblip.PowerStation' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PeerProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.CPU",
+ default_service = "org.shadowblip.PowerStation",
+ default_path = "/org/shadowblip/Performance/CPU"
+)]
+trait CPU {
+ /// EnumerateCores method
+ fn enumerate_cores(&self) -> zbus::Result>;
+
+ /// HasFeature method
+ fn has_feature(&self, flag: &str) -> zbus::Result;
+
+ /// BoostEnabled property
+ #[zbus(property)]
+ fn boost_enabled(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_boost_enabled(&self, value: bool) -> zbus::Result<()>;
+
+ /// CoresCount property
+ #[zbus(property)]
+ fn cores_count(&self) -> zbus::Result;
+
+ /// CoresEnabled property
+ #[zbus(property)]
+ fn cores_enabled(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_cores_enabled(&self, value: u32) -> zbus::Result<()>;
+
+ /// Features property
+ #[zbus(property)]
+ fn features(&self) -> zbus::Result>;
+
+ /// SmtEnabled property
+ #[zbus(property)]
+ fn smt_enabled(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_smt_enabled(&self, value: bool) -> zbus::Result<()>;
+}
diff --git a/extensions/core/src/dbus/powerstation/gpu.rs b/extensions/core/src/dbus/powerstation/gpu.rs
new file mode 100644
index 00000000..f85c00cc
--- /dev/null
+++ b/extensions/core/src/dbus/powerstation/gpu.rs
@@ -0,0 +1,31 @@
+//! # D-Bus interface proxy for: `org.shadowblip.GPU`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/Performance/GPU' from service 'org.shadowblip.PowerStation' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PeerProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.GPU",
+ default_service = "org.shadowblip.PowerStation",
+ default_path = "/org/shadowblip/Performance/GPU"
+)]
+trait GPU {
+ /// EnumerateCards method
+ fn enumerate_cards(&self) -> zbus::Result>;
+}
diff --git a/extensions/core/src/dbus/powerstation/tdp.rs b/extensions/core/src/dbus/powerstation/tdp.rs
new file mode 100644
index 00000000..29ce4a73
--- /dev/null
+++ b/extensions/core/src/dbus/powerstation/tdp.rs
@@ -0,0 +1,52 @@
+//! # D-Bus interface proxy for: `org.shadowblip.GPU.Card.TDP`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/shadowblip/Performance/GPU/card0' from service 'org.shadowblip.PowerStation' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PeerProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.shadowblip.GPU.Card.TDP",
+ default_service = "org.shadowblip.PowerStation",
+ default_path = "/org/shadowblip/Performance/GPU/card0"
+)]
+trait TDP {
+ /// Boost property
+ #[zbus(property)]
+ fn boost(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_boost(&self, value: f64) -> zbus::Result<()>;
+
+ /// PowerProfile property
+ #[zbus(property)]
+ fn power_profile(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_power_profile(&self, value: &str) -> zbus::Result<()>;
+
+ /// TDP property
+ #[zbus(property, name = "TDP")]
+ fn tdp(&self) -> zbus::Result;
+ #[zbus(property, name = "TDP")]
+ fn set_tdp(&self, value: f64) -> zbus::Result<()>;
+
+ /// ThermalThrottleLimitC property
+ #[zbus(property)]
+ fn thermal_throttle_limit_c(&self) -> zbus::Result;
+ #[zbus(property)]
+ fn set_thermal_throttle_limit_c(&self, value: f64) -> zbus::Result<()>;
+}
diff --git a/extensions/core/src/dbus/upower.rs b/extensions/core/src/dbus/upower.rs
new file mode 100644
index 00000000..18cf2051
--- /dev/null
+++ b/extensions/core/src/dbus/upower.rs
@@ -0,0 +1,2 @@
+pub mod device;
+pub mod upower;
diff --git a/extensions/core/src/dbus/upower/device.rs b/extensions/core/src/dbus/upower/device.rs
new file mode 100644
index 00000000..5ea61037
--- /dev/null
+++ b/extensions/core/src/dbus/upower/device.rs
@@ -0,0 +1,162 @@
+//! # D-Bus interface proxy for: `org.freedesktop.UPower.Device`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/freedesktop/UPower/devices/DisplayDevice' from service 'org.freedesktop.UPower' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PeerProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.freedesktop.UPower.Device",
+ default_service = "org.freedesktop.UPower",
+ default_path = "/org/freedesktop/UPower/devices/DisplayDevice"
+)]
+trait Device {
+ /// GetHistory method
+ fn get_history(
+ &self,
+ type_: &str,
+ timespan: u32,
+ resolution: u32,
+ ) -> zbus::Result>;
+
+ /// GetStatistics method
+ fn get_statistics(&self, type_: &str) -> zbus::Result>;
+
+ /// Refresh method
+ fn refresh(&self) -> zbus::Result<()>;
+
+ /// BatteryLevel property
+ #[zbus(property)]
+ fn battery_level(&self) -> zbus::Result;
+
+ /// Capacity property
+ #[zbus(property)]
+ fn capacity(&self) -> zbus::Result;
+
+ /// ChargeCycles property
+ #[zbus(property)]
+ fn charge_cycles(&self) -> zbus::Result;
+
+ /// Energy property
+ #[zbus(property)]
+ fn energy(&self) -> zbus::Result;
+
+ /// EnergyEmpty property
+ #[zbus(property)]
+ fn energy_empty(&self) -> zbus::Result;
+
+ /// EnergyFull property
+ #[zbus(property)]
+ fn energy_full(&self) -> zbus::Result;
+
+ /// EnergyFullDesign property
+ #[zbus(property)]
+ fn energy_full_design(&self) -> zbus::Result;
+
+ /// EnergyRate property
+ #[zbus(property)]
+ fn energy_rate(&self) -> zbus::Result;
+
+ /// HasHistory property
+ #[zbus(property)]
+ fn has_history(&self) -> zbus::Result;
+
+ /// HasStatistics property
+ #[zbus(property)]
+ fn has_statistics(&self) -> zbus::Result;
+
+ /// IconName property
+ #[zbus(property)]
+ fn icon_name(&self) -> zbus::Result;
+
+ /// IsPresent property
+ #[zbus(property)]
+ fn is_present(&self) -> zbus::Result;
+
+ /// IsRechargeable property
+ #[zbus(property)]
+ fn is_rechargeable(&self) -> zbus::Result;
+
+ /// Luminosity property
+ #[zbus(property)]
+ fn luminosity(&self) -> zbus::Result;
+
+ /// Model property
+ #[zbus(property)]
+ fn model(&self) -> zbus::Result;
+
+ /// NativePath property
+ #[zbus(property)]
+ fn native_path(&self) -> zbus::Result;
+
+ /// Online property
+ #[zbus(property)]
+ fn online(&self) -> zbus::Result;
+
+ /// Percentage property
+ #[zbus(property)]
+ fn percentage(&self) -> zbus::Result;
+
+ /// PowerSupply property
+ #[zbus(property)]
+ fn power_supply(&self) -> zbus::Result;
+
+ /// Serial property
+ #[zbus(property)]
+ fn serial(&self) -> zbus::Result;
+
+ /// State property
+ #[zbus(property)]
+ fn state(&self) -> zbus::Result;
+
+ /// Technology property
+ #[zbus(property)]
+ fn technology(&self) -> zbus::Result;
+
+ /// Temperature property
+ #[zbus(property)]
+ fn temperature(&self) -> zbus::Result;
+
+ /// TimeToEmpty property
+ #[zbus(property)]
+ fn time_to_empty(&self) -> zbus::Result;
+
+ /// TimeToFull property
+ #[zbus(property)]
+ fn time_to_full(&self) -> zbus::Result;
+
+ /// Type property
+ #[zbus(property)]
+ fn type_(&self) -> zbus::Result;
+
+ /// UpdateTime property
+ #[zbus(property)]
+ fn update_time(&self) -> zbus::Result;
+
+ /// Vendor property
+ #[zbus(property)]
+ fn vendor(&self) -> zbus::Result;
+
+ /// Voltage property
+ #[zbus(property)]
+ fn voltage(&self) -> zbus::Result;
+
+ /// WarningLevel property
+ #[zbus(property)]
+ fn warning_level(&self) -> zbus::Result;
+}
diff --git a/extensions/core/src/dbus/upower/upower.rs b/extensions/core/src/dbus/upower/upower.rs
new file mode 100644
index 00000000..b6f80298
--- /dev/null
+++ b/extensions/core/src/dbus/upower/upower.rs
@@ -0,0 +1,61 @@
+//! # D-Bus interface proxy for: `org.freedesktop.UPower`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/freedesktop/UPower' from service 'org.freedesktop.UPower' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PropertiesProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PeerProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.freedesktop.UPower",
+ default_service = "org.freedesktop.UPower",
+ default_path = "/org/freedesktop/UPower"
+)]
+trait UPower {
+ /// EnumerateDevices method
+ fn enumerate_devices(&self) -> zbus::Result>;
+
+ /// GetCriticalAction method
+ fn get_critical_action(&self) -> zbus::Result;
+
+ /// GetDisplayDevice method
+ fn get_display_device(&self) -> zbus::Result;
+
+ /// DeviceAdded signal
+ #[zbus(signal)]
+ fn device_added(&self, device: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>;
+
+ /// DeviceRemoved signal
+ #[zbus(signal)]
+ fn device_removed(&self, device: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>;
+
+ /// DaemonVersion property
+ #[zbus(property)]
+ fn daemon_version(&self) -> zbus::Result;
+
+ /// LidIsClosed property
+ #[zbus(property)]
+ fn lid_is_closed(&self) -> zbus::Result;
+
+ /// LidIsPresent property
+ #[zbus(property)]
+ fn lid_is_present(&self) -> zbus::Result;
+
+ /// OnBattery property
+ #[zbus(property)]
+ fn on_battery(&self) -> zbus::Result;
+}
diff --git a/extensions/core/src/gamescope.rs b/extensions/core/src/gamescope.rs
new file mode 100644
index 00000000..55da9cf0
--- /dev/null
+++ b/extensions/core/src/gamescope.rs
@@ -0,0 +1,147 @@
+pub mod x11_client;
+
+use std::collections::HashMap;
+use std::env;
+use x11_client::GamescopeXWayland;
+
+use godot::prelude::*;
+
+use godot::classes::Resource;
+
+#[derive(GodotClass)]
+#[class(base=Resource)]
+pub struct GamescopeInstance {
+ base: Base,
+ xwaylands: HashMap>,
+ xwayland_primary: String,
+ xwayland_ogui: String,
+ xwayland_game: String,
+}
+
+#[godot_api]
+impl GamescopeInstance {
+ /// Primary Gamescope xwayland instance
+ #[constant]
+ const XWAYLAND_TYPE_PRIMARY: u32 = 0;
+
+ /// Xwayland instance that OpenGamepadUI is running on
+ #[constant]
+ const XWAYLAND_TYPE_OGUI: u32 = 1;
+
+ /// Xwayland instance where games run
+ #[constant]
+ const XWAYLAND_TYPE_GAME: u32 = 2;
+
+ /// Gamescope is hard-coded to look for STEAM_GAME=769 to determine if it is the
+ /// overlay app.
+ #[constant]
+ const OVERLAY_GAME_ID: u32 = 769;
+
+ /// Return the Gamescope XWayland of the given type.
+ #[func]
+ pub fn get_xwayland(&self, kind: u32) -> Option> {
+ match kind {
+ GamescopeInstance::XWAYLAND_TYPE_PRIMARY => {
+ let xwayland = self.xwaylands.get(&self.xwayland_primary);
+ xwayland.cloned()
+ }
+ GamescopeInstance::XWAYLAND_TYPE_OGUI => {
+ let xwayland = self.xwaylands.get(&self.xwayland_ogui);
+ xwayland.cloned()
+ }
+ GamescopeInstance::XWAYLAND_TYPE_GAME => {
+ let xwayland = self.xwaylands.get(&self.xwayland_game);
+ xwayland.cloned()
+ }
+ _ => None,
+ }
+ }
+
+ /// Return all known XWayland instances
+ #[func]
+ pub fn get_xwaylands(&self) -> Array> {
+ let mut xwaylands = array![];
+ for xwayland in self.xwaylands.values() {
+ let item = xwayland.clone();
+ xwaylands.push(item);
+ }
+
+ xwaylands
+ }
+
+ /// Returns the XWayland display with the given name (e.g. ":0")
+ #[func]
+ pub fn get_xwayland_by_name(&self, name: GString) -> Option> {
+ let name: String = name.into();
+ self.xwaylands.get(&name).cloned()
+ }
+
+ /// Process Gamescope signals and emit them as Godot signals. This method
+ /// should be called every frame in the "_process" loop of a node.
+ #[func]
+ pub fn process(&mut self) {
+ for (_, xwayland) in self.xwaylands.iter_mut() {
+ xwayland.bind_mut().process();
+ }
+ }
+}
+
+#[godot_api]
+impl IResource for GamescopeInstance {
+ /// Called upon object initialization in the engine
+ fn init(base: Base) -> Self {
+ godot_print!("Initializing Gamescope instance");
+
+ // Discover any gamescope instances
+ let result = gamescope_x11_client::discover_gamescope_displays();
+ let x11_displays = match result {
+ Ok(displays) => displays,
+ Err(e) => {
+ godot_error!("Failed to get Gamescope displays: {e:?}");
+ return Self {
+ base,
+ xwaylands: HashMap::new(),
+ xwayland_primary: Default::default(),
+ xwayland_ogui: Default::default(),
+ xwayland_game: Default::default(),
+ };
+ }
+ };
+
+ // Get the X11 display that the process knows about
+ let ogui_display = env::var("DISPLAY").unwrap_or(":0".into());
+
+ // Keep track of discovered XWaylands
+ let mut xwaylands = HashMap::new();
+ let mut xwayland_primary = Default::default();
+ let mut xwayland_ogui = Default::default();
+ let mut xwayland_game = Default::default();
+
+ // Create an XWayland instance for each discovered XWayland display
+ for display in x11_displays {
+ godot_print!("Discovered XWayland display: {display}");
+ let xwayland = GamescopeXWayland::new(display.as_str());
+
+ // Categorize the discovered displays
+ if display == ogui_display {
+ xwayland_ogui = display.clone();
+ }
+ if xwayland.bind().get_is_primary() {
+ xwayland_primary = display.clone();
+ } else {
+ xwayland_game = display.clone();
+ }
+
+ xwaylands.insert(display, xwayland);
+ }
+
+ // Create a new Gamescope instance
+ Self {
+ base,
+ xwaylands,
+ xwayland_ogui,
+ xwayland_game,
+ xwayland_primary,
+ }
+ }
+}
diff --git a/extensions/core/src/gamescope/x11_client.rs b/extensions/core/src/gamescope/x11_client.rs
new file mode 100644
index 00000000..caae60bf
--- /dev/null
+++ b/extensions/core/src/gamescope/x11_client.rs
@@ -0,0 +1,960 @@
+use gamescope_x11_client::{
+ atoms::GamescopeAtom,
+ xwayland::{BlurMode, Primary, XWayland},
+};
+use std::{
+ collections::HashMap,
+ sync::mpsc::{channel, Receiver, Sender, TryRecvError},
+ time::Duration,
+};
+use tokio::task::AbortHandle;
+
+use godot::{obj::WithBaseField, prelude::*};
+
+use godot::classes::{Resource, ResourceLoader};
+
+use crate::RUNTIME;
+
+/// Signals that can be emitted
+#[derive(Debug)]
+enum Signal {
+ WindowCreated { window_id: u32 },
+ WindowPropertyChanged { window_id: u32, property: String },
+ PropertyChanged { property: String },
+}
+
+#[derive(GodotClass)]
+#[class(no_init, base=Resource)]
+pub struct GamescopeXWayland {
+ base: Base,
+ rx: Receiver,
+ tx: Sender,
+ xwayland: XWayland,
+ window_watch_handles: HashMap,
+
+ /// The name of the XWayland instance (e.g. ":0")
+ #[var]
+ name: GString,
+ /// Returns true if this [GamescopeXWayland] is the primary instance
+ #[var]
+ is_primary: bool,
+ /// Returns the root window id of the [GamescopeXWayland] instance
+ #[var]
+ root_window_id: u32,
+ /// List of windows currently being watched for property changes
+ #[var(get = get_watched_windows)]
+ watched_windows: PackedInt64Array,
+ /// List of focusable apps
+ #[var(get = get_focusable_apps)]
+ focusable_apps: PackedInt64Array,
+ /// List of focusable windows
+ #[var(get = get_focusable_windows)]
+ focusable_windows: PackedInt64Array,
+ /// List of focusable window names
+ #[var(get = get_focusable_window_names)]
+ focusable_window_names: PackedStringArray,
+ /// Currently focused window id
+ #[var(get = get_focused_window)]
+ focused_window: u32,
+ /// Currently focused app id
+ #[var(get = get_focused_app)]
+ focused_app: u32,
+ /// Currently focused gfx app id
+ #[var(get = get_focused_app_gfx)]
+ focused_app_gfx: u32,
+ /// Whether or not the overlay window is currently focused
+ #[var(get = get_overlay_focused)]
+ overlay_focused: bool,
+ /// Current Gamescope FPS limit
+ #[var(get = get_fps_limit, set = set_fps_limit)]
+ fps_limit: u32,
+ /// Gamecope blur mode (0 - off, 1 - cond, 2 - always)
+ #[var(get = get_blur_mode, set = set_blur_mode)]
+ blur_mode: u32,
+ /// Gamescope blur radius
+ #[var(get = get_blur_radius, set = set_blur_radius)]
+ blur_radius: u32,
+ /// Whether or not Gamescope should be allowed to screen tear
+ #[var(get = get_allow_tearing, set = set_allow_tearing)]
+ allow_tearing: bool,
+ /// Current manually focused window
+ #[var(get = get_baselayer_window, set = set_baselayer_window)]
+ baselayer_window: u32,
+}
+
+#[godot_api]
+impl GamescopeXWayland {
+ #[constant]
+ const BLUR_MODE_OFF: u32 = 0;
+ #[constant]
+ const BLUR_MODE_COND: u32 = 1;
+ #[constant]
+ const BLUR_MODE_ALWAYS: u32 = 2;
+
+ #[signal]
+ fn window_created(window_id: u32);
+
+ #[signal]
+ fn window_property_updated(window_id: u32, property: GString);
+
+ #[signal]
+ fn focused_app_updated();
+
+ #[signal]
+ fn focused_app_gfx_updated();
+
+ #[signal]
+ fn focusable_apps_updated();
+
+ #[signal]
+ fn focused_window_updated();
+
+ #[signal]
+ fn focusable_windows_updated();
+
+ #[signal]
+ fn baselayer_window_updated();
+
+ /// Create a new [GamescopeXWayland] with the given name (e.g. ":0")
+ pub fn from_name(name: GString) -> Gd {
+ // Create a channel to communicate with the signals task
+ godot_print!("Gamescope XWayland created with name: {name}");
+ let (tx, rx) = channel();
+
+ // Create an XWayland client instance for this display
+ let mut xwayland = XWayland::new(name.clone().into());
+ if let Err(e) = xwayland.connect() {
+ godot_error!("Failed to connect to XWayland display '{name}': {e:?}");
+ }
+ let is_primary = xwayland.is_primary_instance().unwrap_or_default();
+ let root_window_id = xwayland.get_root_window_id().unwrap_or_default();
+
+ // If this XWayland instance is a primary instance, listen for signals
+ if is_primary {
+ // Spawn a task to listen for property changes
+ if let Ok((_, property_rx)) = xwayland.listen_for_property_changes() {
+ let signals_tx = tx.clone();
+ RUNTIME.spawn_blocking(move || {
+ for event in property_rx.into_iter() {
+ let signal = Signal::PropertyChanged { property: event };
+ if let Err(e) = signals_tx.send(signal) {
+ godot_error!("Error sending property changed signal: {e:?}");
+ break;
+ }
+ }
+ });
+ } else {
+ godot_error!("Failed to listen for XWayland property changes");
+ }
+ }
+
+ // Spawn a task to listen for window creation events
+ if let Ok((_, windows_rx)) = xwayland.listen_for_window_created() {
+ let signals_tx = tx.clone();
+ RUNTIME.spawn_blocking(move || {
+ for window_id in windows_rx.into_iter() {
+ let signal = Signal::WindowCreated { window_id };
+ if let Err(e) = signals_tx.send(signal) {
+ godot_error!("Error sending window created signal: {e:?}");
+ break;
+ }
+ }
+ });
+ } else {
+ godot_error!("Failed to listen for XWayland windows created");
+ }
+
+ // Setup the initial state
+ Gd::from_init_fn(|base| {
+ // Accept a base of type Base and directly forward it.
+ Self {
+ base,
+ rx,
+ tx,
+ name,
+ xwayland,
+ is_primary,
+ root_window_id,
+ watched_windows: Default::default(),
+ window_watch_handles: Default::default(),
+ focusable_apps: Default::default(),
+ focusable_windows: Default::default(),
+ focusable_window_names: Default::default(),
+ focused_window: Default::default(),
+ focused_app: Default::default(),
+ focused_app_gfx: Default::default(),
+ overlay_focused: Default::default(),
+ fps_limit: Default::default(),
+ blur_mode: Default::default(),
+ blur_radius: Default::default(),
+ allow_tearing: Default::default(),
+ baselayer_window: Default::default(),
+ }
+ })
+ }
+
+ /// Get or create a [GamescopeXWayland] with the given name. If an instance
+ /// already exists with the given path, then it will be loaded from the resource
+ /// cache.
+ pub fn new(name: &str) -> Gd {
+ let res_path = format!("gamescope://xwayland/{name}");
+
+ // Check to see if a resource already exists for this device
+ let mut resource_loader = ResourceLoader::singleton();
+ if resource_loader.exists(res_path.clone().into()) {
+ if let Some(res) = resource_loader.load(res_path.clone().into()) {
+ godot_print!(
+ "Resource already exists with path '{res_path}', loading that instead"
+ );
+ let device: Gd = res.cast();
+ device
+ } else {
+ let mut device = GamescopeXWayland::from_name(name.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ } else {
+ let mut device = GamescopeXWayland::from_name(name.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ }
+
+ /// Returns the list of currently watched windows.
+ #[func]
+ pub fn get_watched_windows(&self) -> PackedInt64Array {
+ self.watched_windows.clone()
+ }
+
+ /// Start watching the given window. The [WindowPropertyChanged] signal
+ /// will fire whenever a window property changes on the window. Use
+ /// [unwatch_window] to stop watching the given window.
+ #[func]
+ pub fn watch_window(&mut self, window_id: u32) -> i32 {
+ if self.watched_windows.contains(&(window_id as i64)) {
+ godot_warn!("Window {window_id} is already being watched");
+ return 0;
+ }
+ self.watched_windows.push(window_id as i64);
+
+ // Spawn a new thread to listen for window property changes
+ let (_, rx) = match self.xwayland.listen_for_window_property_changes(window_id) {
+ Ok(result) => result,
+ Err(e) => {
+ godot_error!("Failed to watch window properties for window '{window_id}': {e:?}");
+ return -1;
+ }
+ };
+
+ // Spawn a task to listen for window changes and emit signals
+ let signals_tx = self.tx.clone();
+ let task = RUNTIME.spawn(async move {
+ godot_print!("Started listening for property changes on window: {window_id}");
+ // NOTE: only async tasks support abort, so we need to resort to polling here
+ 'outer: loop {
+ // Consume all messages from the channel and emit signals
+ 'inner: loop {
+ let event = match rx.try_recv() {
+ Ok(value) => value,
+ Err(e) => match e {
+ TryRecvError::Empty => break 'inner,
+ TryRecvError::Disconnected => {
+ godot_error!("Backend thread is not running!");
+ return;
+ }
+ },
+ };
+ let signal = Signal::WindowPropertyChanged {
+ window_id,
+ property: event,
+ };
+ if let Err(e) = signals_tx.send(signal) {
+ godot_error!("Failed to send property change signal: {e:?}");
+ break 'outer;
+ }
+ }
+
+ tokio::time::sleep(Duration::from_millis(40)).await;
+ }
+
+ godot_print!("Stopped listening for property changes on window: {window_id}");
+ });
+
+ // Keep a list of abort handles so the watch task can be cancelled.
+ self.window_watch_handles
+ .insert(window_id, task.abort_handle());
+
+ 0
+ }
+
+ /// Stop watching the given window. The [WindowPropertyChanged] signal will
+ /// no longer fire for the given window.
+ #[func]
+ pub fn unwatch_window(&mut self, window_id: u32) -> i32 {
+ let window_id = window_id as i64;
+ if !self.watched_windows.contains(&window_id) {
+ return 0;
+ }
+ if let Some(idx) = self.watched_windows.find(&window_id, None) {
+ self.watched_windows.remove(idx);
+ }
+
+ // Cancel the listener task
+ let Some(task) = self.window_watch_handles.get(&(window_id as u32)) else {
+ godot_error!("Task wasn't found but was being watched: {window_id}");
+ return -1;
+ };
+
+ task.abort();
+
+ 0
+ }
+
+ /// Discover the process IDs that are associated with the given window
+ #[func]
+ pub fn get_pids_for_window(&self, window_id: u32) -> PackedInt64Array {
+ let pids = match self.xwayland.get_pids_for_window(window_id) {
+ Ok(pids) => pids,
+ Err(e) => {
+ godot_error!("Failed to get pids for window '{window_id}': {e:?}");
+ return PackedInt64Array::new();
+ }
+ };
+ let pids: Vec = pids.into_iter().map(|pid| pid as i64).collect();
+
+ pids.into()
+ }
+
+ /// Returns the window id(s) for the given process ID.
+ #[func]
+ pub fn get_windows_for_pid(&self, pid: u32) -> PackedInt64Array {
+ let windows = match self.xwayland.get_windows_for_pid(pid) {
+ Ok(windows) => windows,
+ Err(e) => {
+ godot_error!("Failed to get windows for pid '{pid}': {e:?}");
+ return PackedInt64Array::new();
+ }
+ };
+ let windows: Vec = windows.into_iter().map(|id| id as i64).collect();
+
+ windows.into()
+ }
+
+ /// Returns the window name of the given window
+ #[func]
+ fn get_window_name(&self, window_id: u32) -> GString {
+ let name = match self.xwayland.get_window_name(window_id) {
+ Ok(name) => name,
+ Err(e) => {
+ godot_error!("Failed to get window name for window '{window_id}': {e:?}");
+ return "".into();
+ }
+ };
+
+ name.unwrap_or_default().into()
+ }
+
+ /// Returns the window ids of the children of the given window
+ #[func]
+ fn get_window_children(&self, window_id: u32) -> PackedInt64Array {
+ let windows = match self.xwayland.get_window_children(window_id) {
+ Ok(windows) => windows,
+ Err(e) => {
+ godot_error!("Failed to get window children for window '{window_id}': {e:?}");
+ return PackedInt64Array::new();
+ }
+ };
+ let windows: Vec = windows.into_iter().map(|id| id as i64).collect();
+
+ windows.into()
+ }
+
+ /// Recursively returns all child windows of the given window id
+ #[func]
+ fn get_all_windows(&self, window_id: u32) -> PackedInt64Array {
+ let windows = match self.xwayland.get_all_windows(window_id) {
+ Ok(windows) => windows,
+ Err(e) => {
+ godot_error!("Failed to get all window children for window '{window_id}': {e:?}");
+ return PackedInt64Array::new();
+ }
+ };
+ let windows: Vec = windows.into_iter().map(|id| id as i64).collect();
+
+ windows.into()
+ }
+
+ /// Returns the currently set app ID on the given window. Returns zero if no
+ /// app id was found.
+ #[func]
+ fn get_app_id(&self, window_id: u32) -> u32 {
+ match self.xwayland.get_app_id(window_id) {
+ Ok(app_id) => app_id.unwrap_or_default(),
+ Err(e) => {
+ godot_error!("Failed to get app id for window '{window_id}': {e:?}");
+ 0
+ }
+ }
+ }
+
+ /// Sets the app ID on the given window. Returns zero if operation succeeds.
+ #[func]
+ fn set_app_id(&self, window_id: u32, app_id: u32) -> i32 {
+ if let Err(e) = self.xwayland.set_app_id(window_id, app_id) {
+ godot_error!("Failed to set app id {app_id} on window '{window_id}': {e:?}");
+ return -1;
+ }
+ 0
+ }
+
+ /// Removes the app ID on the given window. Returns zero if operation succeeds.
+ #[func]
+ fn remove_app_id(&self, window_id: u32) -> i32 {
+ if let Err(e) = self
+ .xwayland
+ .remove_xprop(window_id, GamescopeAtom::SteamGame)
+ {
+ godot_error!("Failed to remove app id from window '{window_id}': {e:?}");
+ return -1;
+ }
+ 0
+ }
+
+ /// Returns whether or not the given window has an app ID set
+ #[func]
+ fn has_app_id(&self, window_id: u32) -> bool {
+ match self.xwayland.has_app_id(window_id) {
+ Ok(v) => v,
+ Err(e) => {
+ godot_error!("Failed to check window '{window_id}' for app id: {e:?}");
+ false
+ }
+ }
+ }
+
+ /// --- XWayland Primary ---
+
+ /// Return a list of focusable apps
+ #[func]
+ fn get_focusable_apps(&mut self) -> PackedInt64Array {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ let value = match self.xwayland.get_focusable_apps() {
+ Ok(value) => value,
+ Err(e) => {
+ godot_error!("Failed to get focusable apps: {e:?}");
+ return Default::default();
+ }
+ };
+ let Some(focusable) = value else {
+ return Default::default();
+ };
+ let focusable: Vec = focusable.into_iter().map(|v| v as i64).collect();
+ self.focusable_apps = focusable.into();
+ self.focusable_apps.clone()
+ }
+
+ /// Return a list of focusable windows
+ #[func]
+ fn get_focusable_windows(&mut self) -> PackedInt64Array {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ let value = match self.xwayland.get_focusable_windows() {
+ Ok(value) => value,
+ Err(e) => {
+ godot_error!("Failed to get focusable windows: {e:?}");
+ return Default::default();
+ }
+ };
+ let Some(focusable) = value else {
+ return Default::default();
+ };
+ let focusable: Vec = focusable.into_iter().map(|v| v as i64).collect();
+ self.focusable_windows = focusable.into();
+ self.focusable_windows.clone()
+ }
+
+ /// Returns a list of focusable window names
+ #[func]
+ fn get_focusable_window_names(&mut self) -> PackedStringArray {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ let value = match self.xwayland.get_focusable_window_names() {
+ Ok(value) => value,
+ Err(e) => {
+ godot_error!("Failed to get focusable windows: {e:?}");
+ return Default::default();
+ }
+ };
+ let value: Vec = value.into_iter().map(GString::from).collect();
+ self.focusable_window_names = value.into();
+ self.focusable_window_names.clone()
+ }
+
+ /// Return the currently focused window id.
+ #[func]
+ fn get_focused_window(&mut self) -> u32 {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ let value = match self.xwayland.get_focused_window() {
+ Ok(value) => value,
+ Err(e) => {
+ godot_error!("Failed to get focused window: {e:?}");
+ return Default::default();
+ }
+ };
+
+ self.focused_window = value.unwrap_or_default();
+ self.focused_window
+ }
+
+ /// Return the currently focused app id.
+ #[func]
+ fn get_focused_app(&mut self) -> u32 {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ let value = match self.xwayland.get_focused_app() {
+ Ok(value) => value,
+ Err(e) => {
+ godot_error!("Failed to get focused app: {e:?}");
+ return Default::default();
+ }
+ };
+
+ self.focused_app = value.unwrap_or_default();
+ self.focused_app
+ }
+
+ /// Return the currently focused gfx app id.
+ #[func]
+ fn get_focused_app_gfx(&mut self) -> u32 {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ let value = match self.xwayland.get_focused_app_gfx() {
+ Ok(value) => value,
+ Err(e) => {
+ godot_error!("Failed to get focused app gfx: {e:?}");
+ return Default::default();
+ }
+ };
+
+ self.focused_app_gfx = value.unwrap_or_default();
+ self.focused_app_gfx
+ }
+
+ /// Returns whether or not the overlay window is currently focused
+ #[func]
+ fn get_overlay_focused(&mut self) -> bool {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+
+ let focused = match self.xwayland.is_overlay_focused() {
+ Ok(value) => value,
+ Err(e) => {
+ godot_error!("Failed to get overlay focused: {e:?}");
+ Default::default()
+ }
+ };
+ self.overlay_focused = focused;
+ self.overlay_focused
+ }
+
+ /// The current Gamescope FPS limit
+ #[func]
+ fn get_fps_limit(&mut self) -> u32 {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ let value = match self.xwayland.get_fps_limit() {
+ Ok(value) => value,
+ Err(e) => {
+ godot_error!("Failed to get fps limit: {e:?}");
+ return Default::default();
+ }
+ };
+
+ self.fps_limit = value.unwrap_or_default();
+ self.fps_limit
+ }
+
+ /// Sets the current Gamescope FPS limit
+ #[func]
+ fn set_fps_limit(&mut self, fps: u32) {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return;
+ }
+ if let Err(e) = self.xwayland.set_fps_limit(fps) {
+ godot_error!("Failed to set FPS limit to {fps}: {e:?}");
+ }
+ self.fps_limit = fps;
+ }
+
+ /// The Gamescope blur mode (0 - off, 1 - cond, 2 - always)
+ #[func]
+ fn get_blur_mode(&mut self) -> u32 {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ let value = match self.xwayland.get_blur_mode() {
+ Ok(value) => value,
+ Err(e) => {
+ godot_error!("Failed to get blur mode: {e:?}");
+ return Default::default();
+ }
+ };
+ let Some(mode) = value else {
+ return Default::default();
+ };
+
+ let blur_mode = match mode {
+ BlurMode::Off => 0,
+ BlurMode::Cond => 1,
+ BlurMode::Always => 2,
+ };
+ self.blur_mode = blur_mode;
+ self.blur_mode
+ }
+
+ /// Sets the Gamescope blur mode
+ #[func]
+ fn set_blur_mode(&mut self, mode: u32) {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ let blur_mode = match mode {
+ 0 => BlurMode::Off,
+ 1 => BlurMode::Cond,
+ 2 => BlurMode::Always,
+ _ => BlurMode::Off,
+ };
+ if let Err(e) = self.xwayland.set_blur_mode(blur_mode) {
+ godot_error!("Failed to set blur mode to: {mode}: {e:?}");
+ }
+ self.blur_mode = mode;
+ }
+
+ // The blur radius size
+ #[func]
+ fn get_blur_radius(&self) -> u32 {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ self.blur_radius
+ }
+
+ /// Sets the blur radius size
+ #[func]
+ fn set_blur_radius(&mut self, radius: u32) {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return;
+ }
+ if let Err(e) = self.xwayland.set_blur_radius(radius) {
+ godot_error!("Failed to set blur radius to: {radius}: {e:?}");
+ }
+ self.blur_radius = radius;
+ }
+
+ /// Whether or not Gamescope should be allowed to screen tear
+ #[func]
+ fn get_allow_tearing(&self) -> bool {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ self.allow_tearing
+ }
+
+ /// Sets whether or not Gamescope should be allowed to screen tear
+ #[func]
+ fn set_allow_tearing(&mut self, allow: bool) {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return;
+ }
+ if let Err(e) = self.xwayland.set_allow_tearing(allow) {
+ godot_error!("Failed to set allow tearing to: {allow}: {e:?}");
+ }
+ self.allow_tearing = allow;
+ }
+
+ /// Returns true if the window with the given window ID exists in focusable apps
+ #[func]
+ fn is_focusable_app(&self, window_id: u32) -> bool {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ match self.xwayland.is_focusable_app(window_id) {
+ Ok(is_focusable) => is_focusable,
+ Err(e) => {
+ godot_error!("Failed to check if window '{window_id}' is focusable app: {e:?}");
+ Default::default()
+ }
+ }
+ }
+
+ /// Sets the given window as the main launcher app. This will set an X window
+ /// property called STEAM_GAME to 769 (Steam), which will make Gamescope
+ /// treat the window as the main overlay.
+ #[func]
+ fn set_main_app(&self, window_id: u32) -> i32 {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ if let Err(e) = self.xwayland.set_main_app(window_id) {
+ godot_error!("Failed to set window '{window_id}' as main app: {e:?}");
+ return -1;
+ }
+ 0
+ }
+
+ /// Set the given window as the primary overlay input focus. This should be set to
+ /// "1" whenever the overlay wants to intercept input from a game.
+ #[func]
+ fn set_input_focus(&self, window_id: u32, value: u32) -> i32 {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ if let Err(e) = self.xwayland.set_input_focus(window_id, value) {
+ godot_error!("Failed to set input focus on '{window_id}' to '{value}': {e:?}");
+ return -1;
+ }
+ 0
+ }
+
+ /// Get the overlay status for the given window
+ #[func]
+ fn get_overlay(&self, window_id: u32) -> u32 {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ match self.xwayland.get_overlay(window_id) {
+ Ok(value) => value.unwrap_or_default(),
+ Err(e) => {
+ godot_error!("Failed to get overlay status for window '{window_id}': {e:?}");
+ 0
+ }
+ }
+ }
+
+ /// Set the given window as the main overlay window
+ #[func]
+ fn set_overlay(&self, window_id: u32, value: u32) -> i32 {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ if let Err(e) = self.xwayland.set_overlay(window_id, value) {
+ godot_error!("Failed to set overlay on '{window_id}' to '{value}': {e:?}");
+ return -1;
+ }
+ 0
+ }
+
+ /// Set the given window as a notification. This should be set to "1" when some
+ /// UI wants to be shown but not intercept input.
+ #[func]
+ fn set_notification(&self, window_id: u32, value: u32) -> i32 {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ if let Err(e) = self.xwayland.set_notification(window_id, value) {
+ godot_error!("Failed to set notification on '{window_id}' to '{value}': {e:?}");
+ return -1;
+ }
+ 0
+ }
+
+ /// Set the given window as an external overlay window
+ #[func]
+ fn set_external_overlay(&self, window_id: u32, value: u32) -> i32 {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ if let Err(e) = self.xwayland.set_external_overlay(window_id, value) {
+ godot_error!("Failed to set external overlay on '{window_id}' to '{value}': {e:?}");
+ return -1;
+ }
+ 0
+ }
+
+ /// Returns the currently set manual focus
+ #[func]
+ fn get_baselayer_window(&mut self) -> u32 {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return Default::default();
+ }
+ let value = match self.xwayland.get_baselayer_window() {
+ Ok(value) => value,
+ Err(e) => {
+ godot_error!("Failed to get baselayer window: {e:?}");
+ return Default::default();
+ }
+ };
+
+ self.baselayer_window = value.unwrap_or_default();
+ self.baselayer_window
+ }
+
+ /// Focuses the given window
+ #[func]
+ fn set_baselayer_window(&mut self, window_id: u32) {
+ if !self.is_primary {
+ godot_error!("XWayland instance is not primary!");
+ return;
+ }
+ if let Err(e) = self.xwayland.set_baselayer_window(window_id) {
+ godot_error!("Failed to set baselayer window to {window_id}: {e:?}");
+ }
+ self.baselayer_window = window_id;
+ }
+
+ /// Removes the baselayer property to un-focus windows
+ #[func]
+ fn remove_baselayer_window(&mut self) {
+ if let Err(e) = self.xwayland.remove_baselayer_window() {
+ godot_error!("Failed to remove baselayer window: {e:?}");
+ }
+ self.baselayer_window = 0;
+ }
+
+ /// Request a screenshot from Gamescope
+ #[func]
+ fn request_screenshot(&self) {
+ if let Err(e) = self.xwayland.request_screenshot() {
+ godot_error!("Failed to request screenshot: {e:?}");
+ }
+ }
+
+ /// Dispatches signals, called by [GamescopeInstance]
+ pub fn process(&mut self) {
+ // Drain all messages from the channel to process them
+ loop {
+ let signal = match self.rx.try_recv() {
+ Ok(value) => value,
+ Err(e) => match e {
+ TryRecvError::Empty => break,
+ TryRecvError::Disconnected => {
+ godot_error!("Backend thread is not running!");
+ return;
+ }
+ },
+ };
+ self.process_signal(signal);
+ }
+ }
+
+ /// Process and dispatch the given signal
+ fn process_signal(&mut self, signal: Signal) {
+ godot_print!("Got signal: {signal:?}");
+ match signal {
+ Signal::WindowCreated { window_id } => {
+ self.base_mut()
+ .emit_signal("window_created".into(), &[window_id.to_variant()]);
+ }
+ Signal::WindowPropertyChanged {
+ window_id,
+ property,
+ } => {
+ self.base_mut().emit_signal(
+ "window_property_updated".into(),
+ &[window_id.to_variant(), property.into_godot().to_variant()],
+ );
+ }
+ Signal::PropertyChanged { property } => {
+ match property {
+ property if property == GamescopeAtom::FocusedApp.to_string() => {
+ let from = self.focused_app;
+ let to = self.get_focused_app();
+ self.base_mut().emit_signal(
+ "focused_app_updated".into(),
+ &[from.to_variant(), to.to_variant()],
+ );
+ }
+ property if property == GamescopeAtom::FocusedAppGFX.to_string() => {
+ let from = self.focused_app_gfx;
+ let to = self.get_focused_app_gfx();
+ self.base_mut().emit_signal(
+ "focused_app_gfx_updated".into(),
+ &[from.to_variant(), to.to_variant()],
+ );
+ }
+ property if property == GamescopeAtom::FocusableApps.to_string() => {
+ let from = self.focusable_apps.clone();
+ let to = self.get_focusable_apps();
+ self.base_mut().emit_signal(
+ "focusable_apps_updated".into(),
+ &[from.to_variant(), to.to_variant()],
+ );
+ }
+ property if property == GamescopeAtom::FocusedWindow.to_string() => {
+ let from = self.focused_window;
+ let to = self.get_focused_window();
+ self.base_mut().emit_signal(
+ "focused_window_updated".into(),
+ &[from.to_variant(), to.to_variant()],
+ );
+ }
+ property if property == GamescopeAtom::FocusableWindows.to_string() => {
+ let from = self.focusable_windows.clone();
+ let to = self.get_focusable_windows();
+ self.base_mut().emit_signal(
+ "focusable_windows_updated".into(),
+ &[from.to_variant(), to.to_variant()],
+ );
+ }
+ property if property == GamescopeAtom::BaselayerWindow.to_string() => {
+ let from = self.baselayer_window;
+ let to = self.get_baselayer_window();
+ self.base_mut().emit_signal(
+ "baselayer_window_updated".into(),
+ &[from.to_variant(), to.to_variant()],
+ );
+ }
+ _ => {
+ // Unknown prop changed
+ }
+ }
+ }
+ }
+ }
+}
+
+#[godot_api]
+impl IResource for GamescopeXWayland {
+ fn to_string(&self) -> GString {
+ format!("", self.name).into()
+ }
+}
+
+impl Drop for GamescopeXWayland {
+ fn drop(&mut self) {
+ godot_print!("Gamescope XWayland '{}' is being destroyed!", self.name);
+ }
+}
diff --git a/extensions/core/src/input.rs b/extensions/core/src/input.rs
new file mode 100644
index 00000000..f672b911
--- /dev/null
+++ b/extensions/core/src/input.rs
@@ -0,0 +1 @@
+pub mod inputplumber;
diff --git a/extensions/core/src/input/inputplumber.rs b/extensions/core/src/input/inputplumber.rs
new file mode 100644
index 00000000..a9b942f7
--- /dev/null
+++ b/extensions/core/src/input/inputplumber.rs
@@ -0,0 +1,529 @@
+pub mod composite_device;
+pub mod dbus_device;
+pub mod event_device;
+pub mod keyboard_device;
+pub mod mouse_device;
+
+use dbus_device::DBusDevice;
+use futures_util::stream::StreamExt;
+use std::collections::HashMap;
+use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
+use std::time::Duration;
+
+use composite_device::CompositeDevice;
+use godot::prelude::*;
+
+use godot::classes::Resource;
+use zbus::fdo::ObjectManagerProxy;
+use zbus::names::BusName;
+
+use crate::dbus::RunError;
+use crate::{get_dbus_system, get_dbus_system_blocking, RUNTIME};
+
+const INPUT_PLUMBER_BUS: &str = "org.shadowblip.InputPlumber";
+const INPUT_PLUMBER_PATH: &str = "/org/shadowblip/InputPlumber";
+
+/// Supported InputPlumber DBus objects
+#[derive(Debug)]
+enum ObjectType {
+ Unknown,
+ CompositeDevice,
+ SourceEventDevice,
+ SourceHidRawDevice,
+ SourceIioDevice,
+ TargetDBusDevice,
+ TargetGamepadDevice,
+ TargetKeyboardDevice,
+ TargetMouseDevice,
+}
+
+impl ObjectType {
+ fn from_dbus_path(path: &str) -> Self {
+ if path.contains("CompositeDevice") {
+ return Self::CompositeDevice;
+ }
+ if path.contains("dbus") {
+ return Self::TargetDBusDevice;
+ }
+ if path.contains("target") && path.contains("mouse") {
+ return Self::TargetMouseDevice;
+ }
+ if path.contains("target") && path.contains("keyboard") {
+ return Self::TargetKeyboardDevice;
+ }
+ if path.contains("target") && path.contains("gamepad") {
+ return Self::TargetGamepadDevice;
+ }
+ if path.contains("source") && path.contains("event") {
+ return Self::SourceEventDevice;
+ }
+ if path.contains("source") && path.contains("hidraw") {
+ return Self::SourceHidRawDevice;
+ }
+ if path.contains("source") && path.contains("iio") {
+ return Self::SourceIioDevice;
+ }
+ Self::Unknown
+ }
+}
+
+/// Signals that can be emitted
+#[derive(Debug)]
+enum Signal {
+ Started,
+ Stopped,
+ ObjectAdded { path: String, kind: ObjectType },
+ ObjectRemoved { path: String, kind: ObjectType },
+}
+
+/// Instance representing a client connection to InputPlumber over DBus. This
+/// is represented as a resource so it can be accessed from anywhere in the scene
+/// tree, but there must be a node that calls 'process()' on this resource every
+/// frame in order to emit signals and process messages.
+#[derive(GodotClass)]
+#[class(base=Resource)]
+pub struct InputPlumberInstance {
+ base: Base,
+ rx: Receiver,
+ conn: Option,
+ /// Map of DBus path to composite device resource. E.g.
+ /// {"/org/shadowblip/InputPlumber/CompositeDevice0": }
+ composite_devices: HashMap>,
+ /// Map of DBus path to dbus device resource. E.g.
+ /// {"/org/shadowblip/InputPlumber/target/dbus0": }
+ dbus_devices: HashMap>,
+ /// The current intercept mode set for all devices
+ #[var(get = get_intercept_mode, set = set_intercept_mode)]
+ intercept_mode: i64,
+ /// The current events that will trigger intercept mode
+ #[var(get = get_intercept_triggers, set = set_intercept_triggers)]
+ intercept_triggers: PackedStringArray,
+ /// The current target event for intercept mode
+ #[var(get = get_intercept_target, set = set_intercept_target)]
+ intercept_target: GString,
+}
+
+#[godot_api]
+impl InputPlumberInstance {
+ #[constant]
+ const INTERCEPT_MODE_NONE: i32 = 0;
+ #[constant]
+ const INTERCEPT_MODE_PASS: i32 = 1;
+ #[constant]
+ const INTERCEPT_MODE_ALL: i32 = 2;
+
+ /// Emitted when InputPlumber is detected as running
+ #[signal]
+ fn started();
+
+ /// Emitted when InputPlumber is detected as stopped
+ #[signal]
+ fn stopped();
+
+ /// Emitted when a CompositeDevice is dicovered and identified as a new device
+ #[signal]
+ fn composite_device_added(device: Gd);
+
+ /// Emitted when a CompositeDevice is removed
+ #[signal]
+ fn composite_device_removed(dbus_path: GString);
+
+ /// Returns true if the InputPlumber service is currently running
+ #[func]
+ fn is_running(&self) -> bool {
+ let Some(conn) = self.conn.as_ref() else {
+ return false;
+ };
+ let bus = BusName::from_static_str(INPUT_PLUMBER_BUS).unwrap();
+ let dbus = zbus::blocking::fdo::DBusProxy::new(conn).ok();
+ let Some(dbus) = dbus else {
+ return false;
+ };
+ dbus.name_has_owner(bus.clone()).unwrap_or_default()
+ }
+
+ /// Returns the [CompositeDevice] with the given DBus path. If the device
+ /// does not exist, null will be returned.
+ #[func]
+ fn get_composite_device(&self, dbus_path: GString) -> Option> {
+ let path = String::from(dbus_path);
+ let device = self.composite_devices.get(&path)?;
+ Some(device.clone())
+ }
+
+ /// Return all current composite devices
+ #[func]
+ fn get_composite_devices(&mut self) -> Array