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..3248941d
--- /dev/null
+++ b/addons/core/core.gdextension
@@ -0,0 +1,12 @@
+[configuration]
+entry_symbol = "gdext_rust_init"
+compatibility_minimum = 4.3
+reloadable = true
+
+[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/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..7ab833db
--- /dev/null
+++ b/extensions/core/Cargo.lock
@@ -0,0 +1,1426 @@
+# 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 = "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 = "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",
+ "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.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 = "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 = "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",
+ "godot",
+ "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",
+]
+
+[[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 = "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 = "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",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[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.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[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.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[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.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[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 = "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..6424198b
--- /dev/null
+++ b/extensions/core/Cargo.toml
@@ -0,0 +1,19 @@
+[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"] }
+once_cell = "1.19.0"
+tokio = { version = "1.39.3", features = ["full"] }
+zbus = "4.4.0"
+zvariant = "4.2.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/gdext/README.md b/extensions/core/src/bluetooth.rs
similarity index 100%
rename from gdext/README.md
rename to extensions/core/src/bluetooth.rs
diff --git a/extensions/core/src/dbus.rs b/extensions/core/src/dbus.rs
new file mode 100644
index 00000000..613ea199
--- /dev/null
+++ b/extensions/core/src/dbus.rs
@@ -0,0 +1,98 @@
+use godot::prelude::*;
+use zvariant::NoneValue;
+
+pub mod inputplumber;
+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/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/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/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..f3da3737
--- /dev/null
+++ b/extensions/core/src/input/inputplumber.rs
@@ -0,0 +1,468 @@
+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 zbus::Connection;
+
+use crate::dbus::RunError;
+use crate::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()
+ }
+
+ /// Return all current composite devices
+ #[func]
+ fn get_composite_devices(&mut self) -> Array> {
+ let mut devices = array![];
+ let Some(conn) = self.conn.as_ref() else {
+ return devices;
+ };
+
+ let bus = BusName::from_static_str(INPUT_PLUMBER_BUS).unwrap();
+ let object_manager = zbus::blocking::fdo::ObjectManagerProxy::builder(conn)
+ .destination(bus)
+ .ok()
+ .and_then(|builder| builder.path(INPUT_PLUMBER_PATH).ok())
+ .and_then(|builder| builder.build().ok());
+ let Some(object_manager) = object_manager else {
+ return devices;
+ };
+
+ let objects = match object_manager.get_managed_objects() {
+ Ok(objs) => objs,
+ Err(_) => {
+ return devices;
+ }
+ };
+
+ for path in objects.keys() {
+ if !path.contains("CompositeDevice") {
+ continue;
+ }
+
+ let device = CompositeDevice::new(path);
+ devices.push(device);
+ }
+
+ devices
+ }
+
+ /// Process InputPlumber 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 any composite devices
+ for (_, device) in self.composite_devices.iter_mut() {
+ device.bind_mut().process();
+ }
+ for (_, device) in self.dbus_devices.iter_mut() {
+ device.bind_mut().process();
+ }
+ }
+
+ /// Gets the current intercept mode for all composite devices
+ #[func]
+ fn get_intercept_mode(&self) -> i64 {
+ self.intercept_mode
+ }
+
+ /// Sets all composite devices to the specified intercept mode.
+ #[func]
+ fn set_intercept_mode(&mut self, mode: i64) {
+ if !(0..=2).contains(&mode) {
+ godot_error!("Invalid intercept mode: {mode}");
+ return;
+ }
+ self.intercept_mode = mode;
+ for (_, device) in self.composite_devices.iter() {
+ device.bind().set_intercept_mode(mode as i32);
+ }
+ }
+
+ /// Gets the current triggers for activating intercept mode for all devices
+ #[func]
+ fn get_intercept_triggers(&self) -> PackedStringArray {
+ self.intercept_triggers.clone()
+ }
+
+ /// Sets the current triggers for activating intercept mode for all devices
+ #[func]
+ fn set_intercept_triggers(&mut self, triggers: PackedStringArray) {
+ self.intercept_triggers = triggers;
+ }
+
+ /// Gets the current target event for activating intercept mode for all devices
+ #[func]
+ fn get_intercept_target(&self) -> GString {
+ self.intercept_target.clone()
+ }
+
+ /// Sets the current target event for activating intercept mode for all devices
+ #[func]
+ fn set_intercept_target(&mut self, target_event: GString) {
+ self.intercept_target = target_event;
+ }
+
+ /// Sets all composite devices to use the specified intercept actions.
+ #[func]
+ fn set_intercept_activation(&mut self, triggers: PackedStringArray, target_event: GString) {
+ self.set_intercept_triggers(triggers.clone());
+ self.set_intercept_target(target_event.clone());
+ for (_, device) in self.composite_devices.iter() {
+ device
+ .bind()
+ .set_intercept_activation(triggers.clone(), target_event.clone())
+ }
+ }
+
+ /// 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 => {
+ // Clear all known devices
+ self.composite_devices.clear();
+ self.dbus_devices.clear();
+ self.base_mut().emit_signal("stopped".into(), &[]);
+ }
+ Signal::ObjectAdded { path, kind } => {
+ self.on_object_added(path, kind);
+ }
+ Signal::ObjectRemoved { path, kind } => {
+ self.on_object_removed(path, kind);
+ }
+ }
+ }
+
+ /// Track the given object and emit signals
+ fn on_object_added(&mut self, path: String, kind: ObjectType) {
+ match kind {
+ ObjectType::Unknown => (),
+ ObjectType::CompositeDevice => {
+ let device = CompositeDevice::new(path.as_str());
+ self.composite_devices.insert(path, device.clone());
+ self.base_mut()
+ .emit_signal("composite_device_added".into(), &[device.to_variant()]);
+ }
+ ObjectType::SourceEventDevice => (),
+ ObjectType::SourceHidRawDevice => (),
+ ObjectType::SourceIioDevice => (),
+ ObjectType::TargetDBusDevice => {
+ let device = DBusDevice::new(path.as_str());
+ self.dbus_devices.insert(path, device);
+ }
+ ObjectType::TargetGamepadDevice => (),
+ ObjectType::TargetKeyboardDevice => (),
+ ObjectType::TargetMouseDevice => (),
+ }
+ }
+
+ /// Remove the given object and emit signals
+ fn on_object_removed(&mut self, path: String, kind: ObjectType) {
+ match kind {
+ ObjectType::Unknown => (),
+ ObjectType::CompositeDevice => {
+ self.composite_devices.remove(&path);
+ self.base_mut().emit_signal(
+ "composite_device_removed".into(),
+ &[GString::from(path).to_variant()],
+ );
+ }
+ ObjectType::SourceEventDevice => (),
+ ObjectType::SourceHidRawDevice => (),
+ ObjectType::SourceIioDevice => (),
+ ObjectType::TargetDBusDevice => {
+ self.dbus_devices.remove(&path);
+ }
+ ObjectType::TargetGamepadDevice => (),
+ ObjectType::TargetKeyboardDevice => (),
+ ObjectType::TargetMouseDevice => (),
+ }
+ }
+}
+
+#[godot_api]
+impl IResource for InputPlumberInstance {
+ /// Called upon object initialization in the engine
+ fn init(base: Base) -> Self {
+ godot_print!("Initializing InputPlumber 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 InputPlumber task: ${e:?}");
+ }
+ });
+
+ // Create a new InputPlumber instance
+ let conn = zbus::blocking::Connection::system().ok();
+ let mut instance = Self {
+ base,
+ rx,
+ conn,
+ composite_devices: HashMap::new(),
+ dbus_devices: HashMap::new(),
+ intercept_mode: 0,
+ intercept_triggers: PackedStringArray::from(&["Gamepad:Button:Guide".into()]),
+ intercept_target: "Gamepad:Button:Guide".into(),
+ };
+
+ // Do initial device discovery
+ let devices = instance.get_composite_devices();
+ for device in devices.iter_shared() {
+ let path = device.bind().get_dbus_path();
+ instance.composite_devices.insert(path.into(), device);
+ }
+
+ instance
+ }
+}
+
+/// Runs InputPlumber 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<(), RunError> {
+ godot_print!("Spawning inputplumber");
+ // Establish a connection to the system bus
+ let conn = Connection::system().await?;
+
+ // Spawn a task to listen for InputPlumber start/stop
+ let dbus_conn = conn.clone();
+ let signals_tx = tx.clone();
+ RUNTIME.spawn(async move {
+ let bus = BusName::from_static_str(INPUT_PLUMBER_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(INPUT_PLUMBER_BUS).unwrap();
+ let object_manager: ObjectManagerProxy = ObjectManagerProxy::builder(&conn)
+ .destination(bus)?
+ .path(INPUT_PLUMBER_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 kind = ObjectType::from_dbus_path(path.as_str());
+ let signal = Signal::ObjectAdded { path, kind };
+ 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 kind = ObjectType::from_dbus_path(path.as_str());
+ let signal = Signal::ObjectRemoved { path, kind };
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ Ok(())
+}
diff --git a/extensions/core/src/input/inputplumber/composite_device.rs b/extensions/core/src/input/inputplumber/composite_device.rs
new file mode 100644
index 00000000..25f97043
--- /dev/null
+++ b/extensions/core/src/input/inputplumber/composite_device.rs
@@ -0,0 +1,322 @@
+use godot::prelude::*;
+
+use godot::classes::{ProjectSettings, Resource, ResourceLoader};
+
+use crate::dbus::inputplumber::composite_device::CompositeDeviceProxyBlocking;
+use crate::dbus::DBusVariant;
+
+use super::dbus_device::DBusDevice;
+use super::INPUT_PLUMBER_BUS;
+
+#[derive(GodotClass)]
+#[class(no_init, base=Resource)]
+pub struct CompositeDevice {
+ base: Base,
+
+ conn: Option,
+ path: String,
+
+ /// The DBus path of the [CompositeDevice]
+ #[allow(dead_code)]
+ #[var(get = get_dbus_path)]
+ dbus_path: GString,
+ /// Name of the [CompositeDevice]
+ #[allow(dead_code)]
+ #[var(get = get_name)]
+ name: GString,
+ /// Name of the input profile that the [CompositeDevice] is using
+ #[allow(dead_code)]
+ #[var(get = get_profile_name)]
+ profile_name: GString,
+ /// Intercept mode of the [CompositeDevice]
+ #[allow(dead_code)]
+ #[var(get = get_intercept_mode, set = set_intercept_mode)]
+ intercept_mode: i32,
+ /// Capabilities from all source devices
+ #[allow(dead_code)]
+ #[var(get = get_capabilities)]
+ capabilities: PackedStringArray,
+ /// Capabilities from all target devices
+ #[allow(dead_code)]
+ #[var(get = get_target_capabilities)]
+ target_capabilities: PackedStringArray,
+ /// Target DBus devices associated with this composite device
+ #[allow(dead_code)]
+ #[var(get = get_dbus_devices)]
+ dbus_devices: Array>,
+ /// The source device paths of the composite device (e.g. /dev/input/event0)
+ #[allow(dead_code)]
+ #[var(get = get_source_device_paths)]
+ source_device_paths: PackedStringArray,
+ /// Get the target device types for the composite device (e.g. "keyboard", "mouse", etc.)
+ #[allow(dead_code)]
+ #[var(get = get_target_devices, set = set_target_devices)]
+ target_devices: PackedStringArray,
+}
+
+#[godot_api]
+impl CompositeDevice {
+ /// Create a new [CompositeDevice] with the given DBus path
+ pub fn from_path(path: GString) -> Gd {
+ Gd::from_init_fn(|base| {
+ // Create a connection to DBus
+ let conn = zbus::blocking::Connection::system().ok();
+
+ // Accept a base of type Base and directly forward it.
+ Self {
+ conn,
+ path: path.clone().into(), // Convert GString -> String.
+ dbus_path: path,
+ name: Default::default(),
+ profile_name: Default::default(),
+ intercept_mode: Default::default(),
+ capabilities: Default::default(),
+ target_capabilities: Default::default(),
+ dbus_devices: Default::default(),
+ source_device_paths: Default::default(),
+ target_devices: Default::default(),
+ base,
+ }
+ })
+ }
+
+ /// Return a proxy instance to the composite device
+ fn get_proxy(&self) -> Option {
+ if let Some(conn) = self.conn.as_ref() {
+ CompositeDeviceProxyBlocking::builder(conn)
+ .path(self.path.clone())
+ .ok()
+ .and_then(|builder| builder.build().ok())
+ } else {
+ None
+ }
+ }
+
+ /// Get or create a [CompositeDevice] 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://{INPUT_PLUMBER_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, loading that instead");
+ let device: Gd = res.cast();
+ device
+ } else {
+ let mut device = CompositeDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ } else {
+ let mut device = CompositeDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ }
+
+ /// Get the name of the [CompositeDevice]
+ #[func]
+ pub fn get_name(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return "".into();
+ };
+ proxy.name().ok().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_profile_name(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return "".into();
+ };
+ proxy.profile_name().ok().unwrap_or_default().into()
+ }
+
+ /// Get the intercept mode of the composite device
+ #[func]
+ pub fn get_intercept_mode(&self) -> i32 {
+ let Some(proxy) = self.get_proxy() else {
+ return -1;
+ };
+ proxy.intercept_mode().ok().unwrap_or_default() as i32
+ }
+
+ /// Set the intercept mode of the composite device
+ #[func]
+ pub fn set_intercept_mode(&self, mode: i32) {
+ let Some(proxy) = self.get_proxy() else {
+ return;
+ };
+ let mode = mode as u32;
+ proxy.set_intercept_mode(mode).ok();
+ }
+
+ /// Get capabilities from all source devices
+ #[func]
+ pub fn get_capabilities(&self) -> PackedStringArray {
+ let Some(proxy) = self.get_proxy() else {
+ return PackedStringArray::new();
+ };
+ let caps: Vec = proxy
+ .capabilities()
+ .ok()
+ .unwrap_or_default()
+ .into_iter()
+ .map(GString::from)
+ .collect();
+ PackedStringArray::from(caps.as_slice())
+ }
+
+ /// Get capabilities from all target devices
+ #[func]
+ pub fn get_target_capabilities(&self) -> PackedStringArray {
+ let Some(proxy) = self.get_proxy() else {
+ return PackedStringArray::new();
+ };
+ let caps: Vec = proxy
+ .target_capabilities()
+ .ok()
+ .unwrap_or_default()
+ .into_iter()
+ .map(GString::from)
+ .collect();
+ PackedStringArray::from(caps.as_slice())
+ }
+
+ #[func]
+ pub fn get_dbus_devices(&self) -> Array> {
+ let mut devices = array![];
+ let paths = self.get_dbus_devices_paths();
+ for path in paths.as_slice() {
+ let dbus_path = String::from(path);
+ let device = DBusDevice::new(dbus_path.as_str());
+ devices.push(device);
+ }
+ devices
+ }
+
+ #[func]
+ pub fn get_dbus_devices_paths(&self) -> PackedStringArray {
+ let Some(proxy) = self.get_proxy() else {
+ return PackedStringArray::new();
+ };
+ let values: Vec = proxy
+ .dbus_devices()
+ .ok()
+ .unwrap_or_default()
+ .into_iter()
+ .map(GString::from)
+ .collect();
+ PackedStringArray::from(values.as_slice())
+ }
+
+ /// Get the source device paths of the composite device (e.g. /dev/input/event0)
+ #[func]
+ pub fn get_source_device_paths(&self) -> PackedStringArray {
+ let Some(proxy) = self.get_proxy() else {
+ return PackedStringArray::new();
+ };
+ let values: Vec = proxy
+ .source_device_paths()
+ .ok()
+ .unwrap_or_default()
+ .into_iter()
+ .map(GString::from)
+ .collect();
+ PackedStringArray::from(values.as_slice())
+ }
+
+ /// Get the target device types for the composite device (e.g. "keyboard", "mouse", etc.)
+ #[func]
+ pub fn get_target_devices(&self) -> PackedStringArray {
+ let Some(proxy) = self.get_proxy() else {
+ return PackedStringArray::new();
+ };
+ let values: Vec = proxy
+ .target_devices()
+ .ok()
+ .unwrap_or_default()
+ .into_iter()
+ .map(GString::from)
+ .collect();
+ PackedStringArray::from(values.as_slice())
+ }
+
+ /// get the target device types for the composite device (e.g. "keyboard", "mouse", etc.)
+ #[func]
+ pub fn set_target_devices(&self, devices: PackedStringArray) {
+ let Some(proxy) = self.get_proxy() else {
+ return;
+ };
+ let device_types: Vec = devices.to_vec().into_iter().map(|v| v.into()).collect();
+ let target_devices: Vec<&str> = device_types.iter().map(|v| v.as_str()).collect();
+ proxy.set_target_devices(target_devices.as_slice()).ok();
+ }
+
+ /// Returns the DBus path to the [CompositeDevice]
+ #[func]
+ pub fn get_dbus_path(&self) -> GString {
+ self.path.clone().into()
+ }
+
+ /// Load the device profile from the given path
+ #[func]
+ pub fn load_profile_path(&self, path: GString) {
+ let Some(proxy) = self.get_proxy() else {
+ return;
+ };
+ let path = String::from(path);
+ let absolute_path = if path.starts_with("res://") || path.starts_with("user://") {
+ let project_settings = ProjectSettings::singleton();
+ project_settings.globalize_path(path.into()).into()
+ } else {
+ path
+ };
+ proxy.load_profile_path(absolute_path.as_str()).ok();
+ }
+
+ /// Write the given event to the appropriate target device, bypassing intercept
+ /// logic.
+ #[func]
+ pub fn send_event(&self, action: GString, value: Variant) {
+ let Some(proxy) = self.get_proxy() else {
+ return;
+ };
+ let Some(value) = value.as_zvariant() else {
+ return;
+ };
+ let event = String::from(action);
+ proxy.send_event(event.as_str(), &value).ok();
+ }
+
+ /// Write the given set of events as a button chord
+ #[func]
+ pub fn send_button_chord(&self, actions: PackedStringArray) {
+ let Some(proxy) = self.get_proxy() else {
+ return;
+ };
+ let values: Vec = actions.to_vec().into_iter().map(|v| v.into()).collect();
+ let str_values: Vec<&str> = values.iter().map(|v| v.as_str()).collect();
+ proxy.send_button_chord(str_values.as_slice()).ok();
+ }
+
+ /// Set the events to look for to activate input interception while in
+ /// "PASS" mode.
+ #[func]
+ pub fn set_intercept_activation(&self, triggers: PackedStringArray, target_event: GString) {
+ let Some(proxy) = self.get_proxy() else {
+ return;
+ };
+ let values: Vec = triggers.to_vec().into_iter().map(|v| v.into()).collect();
+ let str_values: Vec<&str> = values.iter().map(|v| v.as_str()).collect();
+ let target_event: String = target_event.into();
+ proxy
+ .set_intercept_activation(str_values.as_slice(), target_event.as_str())
+ .ok();
+ }
+
+ /// Dispatches signals
+ pub fn process(&mut self) {}
+}
diff --git a/extensions/core/src/input/inputplumber/dbus_device.rs b/extensions/core/src/input/inputplumber/dbus_device.rs
new file mode 100644
index 00000000..2d84bec6
--- /dev/null
+++ b/extensions/core/src/input/inputplumber/dbus_device.rs
@@ -0,0 +1,208 @@
+use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
+
+use futures_util::StreamExt;
+use godot::prelude::*;
+
+use godot::classes::{Resource, ResourceLoader};
+use zbus::Connection;
+
+use crate::dbus::inputplumber::dbus_device::DBusDeviceProxy;
+use crate::RUNTIME;
+
+use super::{RunError, INPUT_PLUMBER_BUS};
+
+/// Signals that can be emitted
+#[derive(Debug)]
+enum Signal {
+ InputEvent {
+ type_code: String,
+ value: f64,
+ },
+ TouchEvent {
+ type_code: String,
+ index: u32,
+ is_touching: bool,
+ pressure: f64,
+ x: f64,
+ y: f64,
+ },
+}
+
+#[derive(GodotClass)]
+#[class(no_init, base=Resource)]
+pub struct DBusDevice {
+ base: Base,
+ path: String,
+ rx: Receiver,
+
+ #[allow(dead_code)]
+ #[var(get = get_dbus_path)]
+ dbus_path: GString,
+}
+
+#[godot_api]
+impl DBusDevice {
+ #[signal]
+ fn input_event(type_code: GString, value: f64);
+
+ #[signal]
+ fn touch_event(
+ type_code: GString,
+ index: i64,
+ is_touching: bool,
+ pressure: f64,
+ x: f64,
+ y: f64,
+ );
+
+ /// Create a new [DBusDevice] with the given DBus path
+ pub fn from_path(path: GString) -> Gd {
+ // Create a channel to communicate with the signals task
+ 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| {
+ // Accept a base of type Base and directly forward it.
+ Self {
+ base,
+ path: path.clone().into(), // Convert GString -> String.
+ rx,
+ dbus_path: path,
+ }
+ })
+ }
+
+ /// Get or create a [DBusDevice] 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://{INPUT_PLUMBER_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, loading that instead");
+ let device: Gd = res.cast();
+ device
+ } else {
+ let mut device = DBusDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ } else {
+ let mut device = DBusDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ }
+
+ #[func]
+ pub fn get_dbus_path(&self) -> GString {
+ self.path.clone().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) {
+ match signal {
+ Signal::InputEvent { type_code, value } => {
+ self.base_mut().emit_signal(
+ "input_event".into(),
+ &[type_code.into_godot().to_variant(), value.to_variant()],
+ );
+ }
+ Signal::TouchEvent {
+ type_code,
+ index,
+ is_touching,
+ pressure,
+ x,
+ y,
+ } => {
+ self.base_mut().emit_signal(
+ "touch_event".into(),
+ &[
+ type_code.into_godot().to_variant(),
+ index.to_variant(),
+ is_touching.to_variant(),
+ pressure.to_variant(),
+ x.to_variant(),
+ y.to_variant(),
+ ],
+ );
+ }
+ }
+ }
+}
+
+/// Run the signals task
+async fn run(tx: Sender, path: String) -> Result<(), RunError> {
+ // Establish a connection to the system bus
+ let conn = Connection::system().await?;
+ let proxy = DBusDeviceProxy::builder(&conn).path(path)?.build().await?;
+
+ let signals_tx = tx.clone();
+ let mut input_events = proxy.receive_input_event().await?;
+ RUNTIME.spawn(async move {
+ while let Some(event) = input_events.next().await {
+ let Some(args) = event.args().ok() else {
+ break;
+ };
+ let signal = Signal::InputEvent {
+ type_code: args.event.to_string(),
+ value: args.value,
+ };
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ let signals_tx = tx.clone();
+ let mut touch_events = proxy.receive_touch_event().await?;
+ RUNTIME.spawn(async move {
+ while let Some(event) = touch_events.next().await {
+ let Some(args) = event.args().ok() else {
+ break;
+ };
+ let signal = Signal::TouchEvent {
+ type_code: args.event.to_string(),
+ index: args.index,
+ is_touching: args.is_touching,
+ pressure: args.pressure,
+ x: args.x,
+ y: args.y,
+ };
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ Ok(())
+}
diff --git a/extensions/core/src/input/inputplumber/event_device.rs b/extensions/core/src/input/inputplumber/event_device.rs
new file mode 100644
index 00000000..a2cd1650
--- /dev/null
+++ b/extensions/core/src/input/inputplumber/event_device.rs
@@ -0,0 +1,139 @@
+use godot::{classes::ResourceLoader, prelude::*};
+
+use crate::dbus::inputplumber::event_device::EventDeviceProxyBlocking;
+
+use super::INPUT_PLUMBER_BUS;
+
+#[derive(GodotClass)]
+#[class(no_init, base=Resource)]
+pub struct EventDevice {
+ base: Base,
+ path: String,
+ conn: Option,
+
+ #[allow(dead_code)]
+ #[var(get = get_dbus_path)]
+ dbus_path: GString,
+ #[allow(dead_code)]
+ #[var(get = get_name)]
+ name: GString,
+ #[allow(dead_code)]
+ #[var(get = get_device_path)]
+ device_path: GString,
+ #[allow(dead_code)]
+ #[var(get = get_phys_path)]
+ phys_path: GString,
+ #[allow(dead_code)]
+ #[var(get = get_sysfs_path)]
+ sysfs_path: GString,
+ #[allow(dead_code)]
+ #[var(get = get_unique_id)]
+ unique_id: GString,
+}
+
+#[godot_api]
+impl EventDevice {
+ /// Create a new [EventDevice] with the given DBus path
+ fn from_path(path: GString) -> Gd {
+ Gd::from_init_fn(|base| {
+ // Create a connection to DBus
+ let conn = zbus::blocking::Connection::system().ok();
+
+ // Accept a base of type Base and directly forward it.
+ Self {
+ base,
+ conn,
+ path: path.clone().into(),
+ dbus_path: path,
+ name: Default::default(),
+ device_path: Default::default(),
+ phys_path: Default::default(),
+ sysfs_path: Default::default(),
+ unique_id: Default::default(),
+ }
+ })
+ }
+
+ /// Return a proxy instance to the composite device
+ fn get_proxy(&self) -> Option {
+ if let Some(conn) = self.conn.as_ref() {
+ EventDeviceProxyBlocking::builder(conn)
+ .path(self.path.clone())
+ .ok()
+ .and_then(|builder| builder.build().ok())
+ } else {
+ None
+ }
+ }
+
+ /// Get or create a [DBusDevice] 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://{INPUT_PLUMBER_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, loading that instead");
+ let device: Gd = res.cast();
+ device
+ } else {
+ let mut device = EventDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ } else {
+ let mut device = EventDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ }
+
+ #[func]
+ pub fn get_dbus_path(&self) -> GString {
+ self.path.clone().into()
+ }
+
+ /// Get the name of the [EventDevice]
+ #[func]
+ pub fn get_name(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return "".into();
+ };
+ proxy.name().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_device_path(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return "".into();
+ };
+ proxy.device_path().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_phys_path(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return "".into();
+ };
+ proxy.phys_path().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_sysfs_path(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return "".into();
+ };
+ proxy.sysfs_path().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_unique_id(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return "".into();
+ };
+ proxy.unique_id().unwrap_or_default().into()
+ }
+}
diff --git a/extensions/core/src/input/inputplumber/keyboard_device.rs b/extensions/core/src/input/inputplumber/keyboard_device.rs
new file mode 100644
index 00000000..07bdd427
--- /dev/null
+++ b/extensions/core/src/input/inputplumber/keyboard_device.rs
@@ -0,0 +1,100 @@
+use godot::{classes::ResourceLoader, prelude::*};
+
+use crate::dbus::inputplumber::keyboard::KeyboardProxyBlocking;
+
+use super::INPUT_PLUMBER_BUS;
+
+#[derive(GodotClass)]
+#[class(no_init, base=Resource)]
+pub struct KeyboardDevice {
+ base: Base,
+ path: String,
+ conn: Option,
+
+ #[allow(dead_code)]
+ #[var(get = get_dbus_path)]
+ dbus_path: GString,
+ #[allow(dead_code)]
+ #[var(get = get_name)]
+ name: GString,
+}
+
+#[godot_api]
+impl KeyboardDevice {
+ /// Create a new [KeyboardDevice] with the given DBus path
+ fn from_path(path: GString) -> Gd {
+ Gd::from_init_fn(|base| {
+ // Create a connection to DBus
+ let conn = zbus::blocking::Connection::system().ok();
+
+ // Accept a base of type Base and directly forward it.
+ Self {
+ base,
+ conn,
+ path: path.clone().into(),
+ dbus_path: path,
+ name: Default::default(),
+ }
+ })
+ }
+
+ /// Return a proxy instance to the composite device
+ fn get_proxy(&self) -> Option {
+ if let Some(conn) = self.conn.as_ref() {
+ KeyboardProxyBlocking::builder(conn)
+ .path(self.path.clone())
+ .ok()
+ .and_then(|builder| builder.build().ok())
+ } else {
+ None
+ }
+ }
+
+ /// Get or create a [KeyboardDevice] 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://{INPUT_PLUMBER_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, loading that instead");
+ let device: Gd = res.cast();
+ device
+ } else {
+ let mut device = KeyboardDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ } else {
+ let mut device = KeyboardDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ }
+
+ #[func]
+ pub fn get_dbus_path(&self) -> GString {
+ self.path.clone().into()
+ }
+
+ /// Get the name of the [KeyboardDevice]
+ #[func]
+ pub fn get_name(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return "".into();
+ };
+ proxy.name().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn send_key(&self, key: GString, value: bool) {
+ let Some(proxy) = self.get_proxy() else {
+ return;
+ };
+ let key_code: String = key.into();
+ proxy.send_key(key_code.as_str(), value).ok();
+ }
+}
diff --git a/extensions/core/src/input/inputplumber/mouse_device.rs b/extensions/core/src/input/inputplumber/mouse_device.rs
new file mode 100644
index 00000000..4fae1bcc
--- /dev/null
+++ b/extensions/core/src/input/inputplumber/mouse_device.rs
@@ -0,0 +1,99 @@
+use godot::{classes::ResourceLoader, prelude::*};
+
+use crate::dbus::inputplumber::mouse::MouseProxyBlocking;
+
+use super::INPUT_PLUMBER_BUS;
+
+#[derive(GodotClass)]
+#[class(no_init, base=Resource)]
+pub struct MouseDevice {
+ base: Base,
+ path: String,
+ conn: Option,
+
+ #[allow(dead_code)]
+ #[var(get = get_dbus_path)]
+ dbus_path: GString,
+ #[allow(dead_code)]
+ #[var(get = get_name)]
+ name: GString,
+}
+
+#[godot_api]
+impl MouseDevice {
+ /// Create a new [MouseDevice] with the given DBus path
+ fn from_path(path: GString) -> Gd {
+ Gd::from_init_fn(|base| {
+ // Create a connection to DBus
+ let conn = zbus::blocking::Connection::system().ok();
+
+ // Accept a base of type Base and directly forward it.
+ Self {
+ base,
+ conn,
+ path: path.clone().into(),
+ dbus_path: path,
+ name: Default::default(),
+ }
+ })
+ }
+
+ /// Return a proxy instance to the composite device
+ fn get_proxy(&self) -> Option {
+ if let Some(conn) = self.conn.as_ref() {
+ MouseProxyBlocking::builder(conn)
+ .path(self.path.clone())
+ .ok()
+ .and_then(|builder| builder.build().ok())
+ } else {
+ None
+ }
+ }
+
+ /// Get or create a [KeyboardDevice] 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://{INPUT_PLUMBER_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, loading that instead");
+ let device: Gd = res.cast();
+ device
+ } else {
+ let mut device = MouseDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ } else {
+ let mut device = MouseDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ }
+
+ #[func]
+ pub fn get_dbus_path(&self) -> GString {
+ self.path.clone().into()
+ }
+
+ /// Get the name of the [KeyboardDevice]
+ #[func]
+ pub fn get_name(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return "".into();
+ };
+ proxy.name().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn move_cursor(&self, x: i64, y: i64) {
+ let Some(proxy) = self.get_proxy() else {
+ return;
+ };
+ proxy.move_cursor(x as i32, y as i32).ok();
+ }
+}
diff --git a/extensions/core/src/lib.rs b/extensions/core/src/lib.rs
new file mode 100644
index 00000000..614879d1
--- /dev/null
+++ b/extensions/core/src/lib.rs
@@ -0,0 +1,75 @@
+pub mod dbus;
+pub mod input;
+pub mod power;
+pub mod system;
+
+use std::{sync::Arc, time::Duration};
+
+use godot::prelude::*;
+use once_cell::sync::Lazy;
+use tokio::{
+ runtime::{Builder, Handle},
+ sync::{
+ mpsc::{channel, Receiver, Sender},
+ Mutex,
+ },
+};
+
+/// Channel for shutting down the tokio runtime
+type Channel = (Sender<()>, Arc>>);
+
+/// Global tokio runtime instance
+pub static RUNTIME: Lazy = Lazy::new(tokio_init);
+static CHANNEL: Lazy = Lazy::new(get_channel);
+
+struct OpenGamepadUICore {}
+
+#[gdextension]
+unsafe impl ExtensionLibrary for OpenGamepadUICore {
+ fn on_level_init(level: InitLevel) {
+ if level != InitLevel::Scene {
+ return;
+ }
+ godot_print!("Initializing OpenGamepadUI Core");
+ }
+
+ fn on_level_deinit(level: InitLevel) {
+ if level != InitLevel::Scene {
+ return;
+ }
+ godot_print!("De-initializing OpenGamepadUI Core");
+ tokio_deinit();
+ }
+}
+
+fn tokio_init() -> Handle {
+ godot_print!("Initializing tokio runtime");
+ let runtime = Builder::new_multi_thread().enable_all().build().unwrap();
+ let handle = runtime.handle().clone();
+
+ let rx = CHANNEL.1.clone();
+
+ std::thread::spawn(move || {
+ runtime.block_on(async {
+ godot_print!("Tokio runtime started");
+ let _ = rx.lock().await.recv().await;
+ });
+ godot_print!("Shutting down Tokio runtime");
+ runtime.shutdown_timeout(Duration::from_secs(1));
+ godot_print!("Tokio runtime stopped");
+ });
+
+ handle
+}
+
+fn tokio_deinit() {
+ let result = CHANNEL.0.clone().blocking_send(());
+ if let Err(e) = result {
+ godot_print!("Failed to shut down tokio runtime: {e}");
+ }
+}
+
+fn get_channel() -> (Sender<()>, Arc>>) {
+ let (tx, rx) = channel(1);
+ (tx, Arc::new(Mutex::new(rx)))
+}
diff --git a/extensions/core/src/performance.rs b/extensions/core/src/performance.rs
new file mode 100644
index 00000000..e69de29b
diff --git a/extensions/core/src/power.rs b/extensions/core/src/power.rs
new file mode 100644
index 00000000..18cf2051
--- /dev/null
+++ b/extensions/core/src/power.rs
@@ -0,0 +1,2 @@
+pub mod device;
+pub mod upower;
diff --git a/extensions/core/src/power/device.rs b/extensions/core/src/power/device.rs
new file mode 100644
index 00000000..3362e9c7
--- /dev/null
+++ b/extensions/core/src/power/device.rs
@@ -0,0 +1,669 @@
+use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
+
+use futures_util::StreamExt;
+use godot::{classes::ResourceLoader, prelude::*};
+use zbus::Connection;
+
+use crate::{
+ dbus::{
+ upower::device::{DeviceProxy, DeviceProxyBlocking},
+ RunError,
+ },
+ RUNTIME,
+};
+
+use super::upower::UPOWER_BUS;
+
+/// Signals that can be emitted
+#[derive(Debug)]
+enum Signal {
+ Updated,
+}
+
+#[derive(GodotClass)]
+#[class(no_init, base=Resource)]
+pub struct UPowerDevice {
+ base: Base,
+ rx: Receiver,
+ conn: Option,
+ #[var]
+ dbus_path: GString,
+ #[allow(dead_code)]
+ #[var(get = get_battery_level)]
+ battery_level: u32,
+ #[allow(dead_code)]
+ #[var(get = get_charge_cycles)]
+ charge_cycles: i32,
+ #[allow(dead_code)]
+ #[var(get = get_energy)]
+ energy: f64,
+ #[allow(dead_code)]
+ #[var(get = get_energy_empty)]
+ energy_empty: f64,
+ #[allow(dead_code)]
+ #[var(get = get_energy_full)]
+ energy_full: f64,
+ #[allow(dead_code)]
+ #[var(get = get_energy_full_design)]
+ energy_full_design: f64,
+ #[allow(dead_code)]
+ #[var(get = get_energy_rate)]
+ energy_rate: f64,
+ #[allow(dead_code)]
+ #[var(get = get_has_history)]
+ has_history: bool,
+ #[allow(dead_code)]
+ #[var(get = get_has_statistics)]
+ has_statistics: bool,
+ #[allow(dead_code)]
+ #[var(get = get_icon_name)]
+ icon_name: GString,
+ #[allow(dead_code)]
+ #[var(get = get_is_present)]
+ is_present: bool,
+ #[allow(dead_code)]
+ #[var(get = get_is_rechargeable)]
+ is_rechargeable: bool,
+ #[allow(dead_code)]
+ #[var(get = get_luminosity)]
+ luminosity: f64,
+ #[allow(dead_code)]
+ #[var(get = get_model)]
+ model: GString,
+ #[allow(dead_code)]
+ #[var(get = get_native_path)]
+ native_path: GString,
+ #[allow(dead_code)]
+ #[var(get = get_online)]
+ online: bool,
+ #[allow(dead_code)]
+ #[var(get = get_percentage)]
+ percentage: f64,
+ #[allow(dead_code)]
+ #[var(get = get_power_supply)]
+ power_supply: bool,
+ #[allow(dead_code)]
+ #[var(get = get_serial)]
+ serial: GString,
+ #[allow(dead_code)]
+ #[var(get = get_state)]
+ state: u32,
+ #[allow(dead_code)]
+ #[var(get = get_technology)]
+ technology: u32,
+ #[allow(dead_code)]
+ #[var(get = get_temperature)]
+ temperature: f64,
+ #[allow(dead_code)]
+ #[var(get = get_time_to_empty)]
+ time_to_empty: i64,
+ #[allow(dead_code)]
+ #[var(get = get_time_to_full)]
+ time_to_full: i64,
+ #[allow(dead_code)]
+ #[var(get = get_type)]
+ type_: u32,
+ #[allow(dead_code)]
+ #[var(get = get_update_time)]
+ update_time: i64,
+ #[allow(dead_code)]
+ #[var(get = get_vendor)]
+ vendor: GString,
+ #[allow(dead_code)]
+ #[var(get = get_voltage)]
+ voltage: f64,
+ #[allow(dead_code)]
+ #[var(get = get_warning_level)]
+ warning_level: u32,
+}
+
+#[godot_api]
+impl UPowerDevice {
+ #[constant]
+ const TYPE_UNKNOWN: i32 = 0;
+ #[constant]
+ const TYPE_LINE_POWER: i32 = 1;
+ #[constant]
+ const TYPE_BATTERY: i32 = 2;
+ #[constant]
+ const TYPE_UPS: i32 = 3;
+ #[constant]
+ const TYPE_MONITOR: i32 = 4;
+ #[constant]
+ const TYPE_MOUSE: i32 = 5;
+ #[constant]
+ const TYPE_KEYBOARD: i32 = 6;
+ #[constant]
+ const TYPE_PDA: i32 = 7;
+ #[constant]
+ const TYPE_PHONE: i32 = 8;
+ #[constant]
+ const TYPE_MEDIA_PLAYER: i32 = 9;
+ #[constant]
+ const TYPE_TABLET: i32 = 10;
+ #[constant]
+ const TYPE_COMPUTER: i32 = 11;
+ #[constant]
+ const TYPE_GAMING_INPUT: i32 = 12;
+ #[constant]
+ const TYPE_PEN: i32 = 13;
+ #[constant]
+ const TYPE_TOUCHPAD: i32 = 14;
+ #[constant]
+ const TYPE_MODEM: i32 = 15;
+ #[constant]
+ const TYPE_NETWORK: i32 = 16;
+ #[constant]
+ const TYPE_HEADSET: i32 = 17;
+ #[constant]
+ const TYPE_SPEAKERS: i32 = 18;
+ #[constant]
+ const TYPE_HEADPHONES: i32 = 19;
+ #[constant]
+ const TYPE_VIDEO: i32 = 20;
+ #[constant]
+ const TYPE_OTHER_AUDIO: i32 = 21;
+ #[constant]
+ const TYPE_REMOTE_CONTROL: i32 = 22;
+ #[constant]
+ const TYPE_PRINTER: i32 = 23;
+ #[constant]
+ const TYPE_SCANNER: i32 = 24;
+ #[constant]
+ const TYPE_CAMERA: i32 = 25;
+ #[constant]
+ const TYPE_WEARABLE: i32 = 26;
+ #[constant]
+ const TYPE_TOY: i32 = 27;
+ #[constant]
+ const TYPE_BLUETOOTH_GENREIC: i32 = 28;
+
+ #[constant]
+ const STATE_UNKNOWN: i32 = 0;
+ #[constant]
+ const STATE_CHARGING: i32 = 1;
+ #[constant]
+ const STATE_DISCHARGING: i32 = 2;
+ #[constant]
+ const STATE_EMPTY: i32 = 3;
+ #[constant]
+ const STATE_FULLY_CHARGED: i32 = 4;
+ #[constant]
+ const STATE_PENDING_CHARGE: i32 = 5;
+ #[constant]
+ const STATE_PENDING_DISCHARGE: i32 = 6;
+
+ #[constant]
+ const TECHNOLOGY_UNKNOWN: i32 = 0;
+ #[constant]
+ const TECHNOLOGY_LITHIUM_ION: i32 = 1;
+ #[constant]
+ const TECHNOLOGY_LITHIUM_POLYMER: i32 = 2;
+ #[constant]
+ const TECHNOLOGY_LITHIUM_IRON_PHOSPHATE: i32 = 3;
+ #[constant]
+ const TECHNOLOGY_LEAD_ACID: i32 = 4;
+ #[constant]
+ const TECHNOLOGY_NICKEL_CADMIUM: i32 = 5;
+ #[constant]
+ const TECHNOLOGY_NICKEL_METAL_HYDRIDE: i32 = 6;
+
+ #[constant]
+ const WARNING_LEVEL_UNKNOWN: i32 = 0;
+ #[constant]
+ const WARNING_LEVEL_NONE: i32 = 1;
+ #[constant]
+ const WARNING_LEVEL_DISCHARGING: i32 = 2;
+ #[constant]
+ const WARNING_LEVEL_LOW: i32 = 3;
+ #[constant]
+ const WARNING_LEVEL_CRITICAL: i32 = 4;
+ #[constant]
+ const WARNING_LEVEL_ACTION: i32 = 5;
+
+ #[constant]
+ const BATTERY_LEVEL_UNKNOWN: i32 = 0;
+ #[constant]
+ const BATTERY_LEVEL_NONE: i32 = 1;
+ #[constant]
+ const BATTERY_LEVEL_LOW: i32 = 3;
+ #[constant]
+ const BATTERY_LEVEL_CRITICAL: i32 = 4;
+ #[constant]
+ const BATTERY_LEVEL_NORMAL: i32 = 6;
+ #[constant]
+ const BATTERY_LEVEL_HIGH: i32 = 7;
+ #[constant]
+ const BATTERY_LEVEL_FULL: i32 = 8;
+
+ #[signal]
+ fn updated();
+
+ /// Create a new [UPowerDevice] with the given DBus path
+ pub fn from_path(path: GString) -> Gd {
+ // Create a channel to communicate with the signals task
+ 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 UPowerDevice task: ${e:?}");
+ }
+ });
+
+ Gd::from_init_fn(|base| {
+ // Create a connection to DBus
+ let conn = zbus::blocking::Connection::system().ok();
+
+ // Accept a base of type Base and directly forward it.
+ Self {
+ base,
+ rx,
+ conn,
+ dbus_path: path,
+ battery_level: Default::default(),
+ charge_cycles: Default::default(),
+ energy: Default::default(),
+ energy_empty: Default::default(),
+ energy_full: Default::default(),
+ energy_full_design: Default::default(),
+ energy_rate: Default::default(),
+ has_history: Default::default(),
+ has_statistics: Default::default(),
+ icon_name: Default::default(),
+ is_present: Default::default(),
+ is_rechargeable: Default::default(),
+ luminosity: Default::default(),
+ model: Default::default(),
+ native_path: Default::default(),
+ online: Default::default(),
+ percentage: Default::default(),
+ power_supply: Default::default(),
+ serial: Default::default(),
+ state: Default::default(),
+ technology: Default::default(),
+ temperature: Default::default(),
+ time_to_empty: Default::default(),
+ time_to_full: Default::default(),
+ type_: Default::default(),
+ update_time: Default::default(),
+ vendor: Default::default(),
+ voltage: Default::default(),
+ warning_level: Default::default(),
+ }
+ })
+ }
+
+ /// Return a proxy instance to the composite device
+ fn get_proxy(&self) -> Option {
+ if let Some(conn) = self.conn.as_ref() {
+ let path: String = self.dbus_path.clone().into();
+ DeviceProxyBlocking::builder(conn)
+ .path(path)
+ .ok()
+ .and_then(|builder| builder.build().ok())
+ } else {
+ None
+ }
+ }
+
+ /// Get or create a [UPowerDevice] 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://{UPOWER_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, loading that instead");
+ let device: Gd = res.cast();
+ device
+ } else {
+ let mut device = UPowerDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ } else {
+ let mut device = UPowerDevice::from_path(path.to_string().into());
+ device.take_over_path(res_path.into());
+ device
+ }
+ }
+
+ #[func]
+ pub fn get_battery_level(&self) -> u32 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.battery_level().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_charge_cycles(&self) -> i32 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.charge_cycles().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_energy(&self) -> f64 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.energy().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_energy_empty(&self) -> f64 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.energy_empty().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_energy_full(&self) -> f64 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.energy_full().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_energy_full_design(&self) -> f64 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.energy_full_design().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_energy_rate(&self) -> f64 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.energy_rate().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_has_history(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.has_history().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_has_statistics(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.has_statistics().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_icon_name(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.icon_name().ok().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_is_present(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.is_present().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_is_rechargeable(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.is_rechargeable().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_luminosity(&self) -> f64 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.luminosity().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_model(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.model().ok().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_native_path(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.native_path().ok().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_online(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.online().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_percentage(&self) -> f64 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.percentage().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_power_supply(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.power_supply().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_serial(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.serial().ok().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_state(&self) -> u32 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.state().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_technology(&self) -> u32 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.technology().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_temperature(&self) -> f64 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.temperature().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_time_to_empty(&self) -> i64 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.time_to_empty().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_time_to_full(&self) -> i64 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.time_to_full().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_type(&self) -> u32 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.type_().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_update_time(&self) -> i64 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.update_time().ok().unwrap_or_default() as i64
+ }
+
+ #[func]
+ pub fn get_vendor(&self) -> GString {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.vendor().ok().unwrap_or_default().into()
+ }
+
+ #[func]
+ pub fn get_voltage(&self) -> f64 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.voltage().ok().unwrap_or_default()
+ }
+
+ #[func]
+ pub fn get_warning_level(&self) -> u32 {
+ let Some(proxy) = self.get_proxy() else {
+ return Default::default();
+ };
+ proxy.warning_level().ok().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::Updated => {
+ self.base_mut().emit_signal("updated".into(), &[]);
+ }
+ }
+ }
+}
+
+/// Run the signals task
+async fn run(tx: Sender, path: String) -> Result<(), RunError> {
+ // Establish a connection to the system bus
+ let conn = Connection::system().await?;
+ let proxy = DeviceProxy::builder(&conn).path(path)?.build().await?;
+
+ let signals_tx = tx.clone();
+ let mut events = proxy.receive_percentage_changed().await;
+ RUNTIME.spawn(async move {
+ while let Some(_event) = events.next().await {
+ let signal = Signal::Updated;
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ let signals_tx = tx.clone();
+ let mut events = proxy.receive_icon_name_changed().await;
+ RUNTIME.spawn(async move {
+ while let Some(_event) = events.next().await {
+ let signal = Signal::Updated;
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ let signals_tx = tx.clone();
+ let mut events = proxy.receive_state_changed().await;
+ RUNTIME.spawn(async move {
+ while let Some(_event) = events.next().await {
+ let signal = Signal::Updated;
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ let signals_tx = tx.clone();
+ let mut events = proxy.receive_time_to_full_changed().await;
+ RUNTIME.spawn(async move {
+ while let Some(_event) = events.next().await {
+ let signal = Signal::Updated;
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ let signals_tx = tx.clone();
+ let mut events = proxy.receive_time_to_empty_changed().await;
+ RUNTIME.spawn(async move {
+ while let Some(_event) = events.next().await {
+ let signal = Signal::Updated;
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ let signals_tx = tx.clone();
+ let mut events = proxy.receive_battery_level_changed().await;
+ RUNTIME.spawn(async move {
+ while let Some(_event) = events.next().await {
+ let signal = Signal::Updated;
+ if signals_tx.send(signal).is_err() {
+ break;
+ }
+ }
+ });
+
+ Ok(())
+}
diff --git a/extensions/core/src/power/upower.rs b/extensions/core/src/power/upower.rs
new file mode 100644
index 00000000..c6eb64c4
--- /dev/null
+++ b/extensions/core/src/power/upower.rs
@@ -0,0 +1,189 @@
+use std::{
+ collections::HashMap,
+ sync::mpsc::{channel, Receiver, Sender, TryRecvError},
+ time::Duration,
+};
+
+use godot::prelude::*;
+use zbus::{names::BusName, Connection};
+
+use crate::{
+ dbus::{upower::upower::UPowerProxyBlocking, RunError},
+ RUNTIME,
+};
+
+use super::device::UPowerDevice;
+
+pub const UPOWER_BUS: &str = "org.freedesktop.UPower";
+const UPOWER_PATH: &str = "/org/freedesktop/UPower";
+const DISPLAY_DEVICE_PATH: &str = "/org/freedesktop/UPower/devices/DisplayDevice";
+
+/// Signals that can be emitted
+#[derive(Debug)]
+enum Signal {
+ Started,
+ Stopped,
+}
+
+#[derive(GodotClass)]
+#[class(base=Resource)]
+pub struct UPowerInstance {
+ base: Base,
+ rx: Receiver,
+ conn: Option,
+ devices: HashMap>,
+ #[allow(dead_code)]
+ #[var(get = get_on_battery)]
+ on_battery: bool,
+}
+
+#[godot_api]
+impl UPowerInstance {
+ /// Emitted when UPower is detected as running
+ #[signal]
+ fn started();
+
+ /// Emitted when UPower is detected as stopped
+ #[signal]
+ fn stopped();
+
+ /// Returns whether or not the device is running on battery power
+ #[func]
+ pub fn get_on_battery(&self) -> bool {
+ let Some(proxy) = self.get_proxy() else {
+ return false;
+ };
+ proxy.on_battery().ok().unwrap_or_default()
+ }
+
+ /// Get the object to the "display device", a composite device that represents
+ /// the status icon to show in desktop environments.
+ #[func]
+ pub fn get_display_device(&mut self) -> Gd {
+ let device = UPowerDevice::new(DISPLAY_DEVICE_PATH);
+ self.devices
+ .insert(DISPLAY_DEVICE_PATH.to_string(), device.clone());
+ device
+ }
+
+ /// Process UPower 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 for other known DBus objects
+ for (_, device) in self.devices.iter_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(), &[]);
+ }
+ }
+ }
+
+ /// Return a proxy instance to the composite device
+ fn get_proxy(&self) -> Option {
+ if let Some(conn) = self.conn.as_ref() {
+ UPowerProxyBlocking::builder(conn)
+ .path(UPOWER_PATH)
+ .ok()
+ .and_then(|builder| builder.build().ok())
+ } else {
+ None
+ }
+ }
+}
+
+#[godot_api]
+impl IResource for UPowerInstance {
+ /// Called upon object initialization in the engine
+ fn init(base: Base) -> Self {
+ godot_print!("Initializing UPower 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 UPower task: ${e:?}");
+ }
+ });
+
+ // Create a new UPower instance
+ let conn = zbus::blocking::Connection::system().ok();
+ Self {
+ base,
+ rx,
+ conn,
+ devices: HashMap::new(),
+ on_battery: false,
+ }
+ }
+}
+
+/// Runs UPower 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<(), RunError> {
+ godot_print!("Spawning UPower tasks");
+ // Establish a connection to the system bus
+ let conn = Connection::system().await?;
+
+ // Spawn a task to listen for UPower start/stop
+ let dbus_conn = conn.clone();
+ let signals_tx = tx.clone();
+ RUNTIME.spawn(async move {
+ let bus = BusName::from_static_str(UPOWER_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;
+ }
+ });
+
+ Ok(())
+}
diff --git a/extensions/core/src/system.rs b/extensions/core/src/system.rs
new file mode 100644
index 00000000..cfd505c1
--- /dev/null
+++ b/extensions/core/src/system.rs
@@ -0,0 +1,2 @@
+pub mod command;
+pub mod pty;
diff --git a/extensions/core/src/system/command.rs b/extensions/core/src/system/command.rs
new file mode 100644
index 00000000..72a783a2
--- /dev/null
+++ b/extensions/core/src/system/command.rs
@@ -0,0 +1,44 @@
+use std::sync::mpsc::Receiver;
+
+use godot::prelude::*;
+
+//// Signals that can be emitted
+//#[derive(Debug)]
+//enum Signal {
+// InputEvent {
+// type_code: String,
+// value: f64,
+// },
+// TouchEvent {
+// type_code: String,
+// index: u32,
+// is_touching: bool,
+// pressure: f64,
+// x: f64,
+// y: f64,
+// },
+//}
+//
+//#[derive(GodotClass)]
+//#[class(base=RefCounted)]
+//pub struct Command {
+// base: Base,
+// path: String,
+// rx: Receiver,
+//}
+//
+//#[godot_api]
+//impl Command {
+// #[signal]
+// fn input_event(type_code: GString, value: f64);
+//
+// #[signal]
+// fn touch_event(
+// type_code: GString,
+// index: i64,
+// is_touching: bool,
+// pressure: f64,
+// x: f64,
+// y: f64,
+// );
+//}
diff --git a/extensions/core/src/system/pty.rs b/extensions/core/src/system/pty.rs
new file mode 100644
index 00000000..f4d06c83
--- /dev/null
+++ b/extensions/core/src/system/pty.rs
@@ -0,0 +1,316 @@
+use nix::pty::{openpty, Winsize};
+use std::{
+ ffi::OsString,
+ sync::mpsc::{channel, Receiver, Sender, TryRecvError},
+};
+use tokio::{
+ fs::File,
+ io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter},
+ process::Command,
+ select,
+};
+
+use godot::{obj::WithBaseField, prelude::*};
+
+use crate::RUNTIME;
+
+/// Signals that can be emitted
+#[derive(Debug)]
+enum Signal {
+ Started { pid: u32 },
+ Finished { exit_code: i32 },
+ LineWritten { line: String },
+}
+
+/// Commands that can be sent to a running PTY session
+#[derive(Debug)]
+enum PtyCommand {
+ Write { data: Vec },
+ WriteLine { line: String },
+}
+
+#[derive(GodotClass)]
+#[class(base=Node)]
+pub struct Pty {
+ base: Base,
+ rx: Receiver,
+ tx: Sender,
+ pty_tx: Option>,
+
+ /// Whether or not a process is currently running in the PTY
+ #[var(get = get_running)]
+ running: bool,
+ /// Number of rows the pseudo terminal should have
+ #[export]
+ rows: i32,
+ /// Number of columns the psuedo terminal should have
+ #[export]
+ columns: i32,
+ /// Width of the pseudo terminal in pixels
+ #[export]
+ width_px: i32,
+ /// Height of the pseudo terminal in pixels
+ #[export]
+ height_px: i32,
+}
+
+#[godot_api]
+impl Pty {
+ /// Emitted when a process is started in the PTY. Returns the PID of the
+ /// started process.
+ #[signal]
+ fn started(pid: i32);
+
+ /// Emitted when a line is written to the PTY stdout
+ #[signal]
+ fn line_written(line: GString);
+
+ /// Emitted when the underlying command has exited. Returns the exit code
+ /// of the child process.
+ #[signal]
+ fn finished(exit_code: i32);
+
+ /// Returns whether or not the PTY is currently executing a process
+ #[func]
+ fn get_running(&self) -> bool {
+ self.running
+ }
+
+ #[func]
+ fn write(&self, data: PackedByteArray) -> i32 {
+ let Some(pty_tx) = self.pty_tx.as_ref() else {
+ godot_error!("PTY is not open to write line");
+ return -1;
+ };
+ let slice = data.as_slice();
+ let data = slice.to_vec();
+ let command = PtyCommand::Write { data };
+ if let Err(e) = pty_tx.blocking_send(command) {
+ println!("Error sending write line to PTY: {e:?}");
+ return -1;
+ }
+
+ 0
+ }
+
+ /// Write the given line to the running PTY. Returns an error code if the
+ /// PTY is not currently executing a process.
+ #[func]
+ fn write_line(&self, line: GString) -> i32 {
+ let Some(pty_tx) = self.pty_tx.as_ref() else {
+ godot_error!("PTY is not open to write line");
+ return -1;
+ };
+ let command = PtyCommand::WriteLine { line: line.into() };
+ if let Err(e) = pty_tx.blocking_send(command) {
+ println!("Error sending write line to PTY: {e:?}");
+ return -1;
+ }
+
+ 0
+ }
+
+ /// Execute the given command inside the PTY. This command is executed
+ /// asyncronously and will emit signals whenever new output is available.
+ #[func]
+ fn exec(&mut self, command: GString, args: PackedStringArray) -> i32 {
+ if self.running {
+ godot_error!("PTY is already running a process");
+ return -1;
+ }
+
+ // Open a new PTY with the given dimensions
+ let window_size = Winsize {
+ ws_row: self.rows as u16,
+ ws_col: self.columns as u16,
+ ws_xpixel: self.width_px as u16,
+ ws_ypixel: self.height_px as u16,
+ };
+ let pty = match openpty(Some(&window_size), None) {
+ Ok(pty) => pty,
+ Err(e) => {
+ godot_error!("Failed to open pty: {e}");
+ return -1;
+ }
+ };
+
+ godot_print!("Executing command async in pty");
+ let command: String = command.into();
+ let command = OsString::from(command);
+ let args: Vec = args.as_slice().iter().map(String::from).collect();
+
+ // Assign the different sides of the PTY
+ let master = pty.master;
+ let slave = pty.slave;
+ let stdin = slave.try_clone().unwrap();
+ let stdout = slave.try_clone().unwrap();
+ let stderr = slave;
+
+ // Spawn a task to run the command
+ let signals_tx = self.tx.clone();
+ RUNTIME.spawn(async move {
+ let mut binding = Command::new(command);
+ let cmd = binding
+ .args(args)
+ .stdin(stdin)
+ .stdout(stdout)
+ .stderr(stderr);
+ let mut child = cmd.spawn().unwrap();
+
+ // Get the PID of the process and emit a started signal
+ let pid = child.id();
+ if let Some(pid) = pid {
+ let signal = Signal::Started { pid };
+ if let Err(e) = signals_tx.send(signal) {
+ println!("Error sending started signal: {e:?}");
+ }
+ }
+
+ // Wait for the process to finish
+ let exit_code = match child.wait().await {
+ Ok(code) => code,
+ Err(e) => {
+ println!("Error executing child: {e:?}");
+ return;
+ }
+ };
+ let exit_code = exit_code.code().unwrap_or(0);
+
+ // Send the exit code with the finished signal
+ let signal = Signal::Finished { exit_code };
+ if let Err(e) = signals_tx.send(signal) {
+ println!("Error sending exit code: {e:?}");
+ }
+ });
+
+ // Create a channel so input commands can be sent to the running PTY
+ let (pty_tx, mut pty_rx) = tokio::sync::mpsc::channel(8192);
+ self.pty_tx = Some(pty_tx);
+
+ // Spawn a task to read/write from/to the PTY
+ let signals_tx = self.tx.clone();
+ RUNTIME.spawn(async move {
+ println!("Task spawned to read/write PTY");
+
+ // Create readers/writers
+ let output = std::fs::File::from(master.try_clone().unwrap());
+ let output: File = output.into();
+ let input = std::fs::File::from(master);
+ let input: File = input.into();
+
+ let mut reader = BufReader::new(output);
+ let mut writer = BufWriter::new(input);
+
+ // Select between read and write operations in a loop
+ loop {
+ let mut buffer = [0; 4096];
+ select! {
+ // Handle stdout output
+ read_result = reader.read(&mut buffer[..]) => {
+ let bytes_read = match read_result {
+ Ok(n) => n,
+ Err(_e) => break,
+ };
+ Pty::process_read(&buffer, bytes_read, &signals_tx);
+ }
+ // Handle stdin commands over channel
+ Some(cmd) = pty_rx.recv() => {
+ Pty::process_write(&mut writer, cmd).await;
+ }
+ }
+ }
+ println!("Finished");
+ });
+ self.running = true;
+
+ 0
+ }
+
+ /// Process reading output from the PTY
+ fn process_read(buffer: &[u8], bytes_read: usize, signals_tx: &Sender) {
+ let data = &buffer[..bytes_read];
+ let text = String::from_utf8_lossy(data).to_string();
+ let text = text.replace('\r', "");
+ let lines = text.split('\n');
+ for line in lines {
+ let line = line.to_string();
+ let signal = Signal::LineWritten { line };
+ if let Err(e) = signals_tx.send(signal) {
+ println!("Error sending line: {e:?}");
+ }
+ }
+ }
+
+ /// Process writing input to the PTY
+ async fn process_write(writer: &mut BufWriter, cmd: PtyCommand) {
+ match cmd {
+ PtyCommand::Write { data } => {
+ writer.write_all(data.as_slice()).await.unwrap();
+ }
+ PtyCommand::WriteLine { line } => {
+ let line = format!("{line}\r");
+ writer.write_all(line.as_bytes()).await.unwrap();
+ }
+ };
+ writer.flush().await.unwrap();
+ }
+
+ /// Process and dispatch the given signal
+ fn process_signal(&mut self, signal: Signal) {
+ match signal {
+ Signal::Started { pid } => {
+ self.base_mut()
+ .emit_signal("started".into(), &[pid.to_variant()]);
+ }
+ Signal::Finished { exit_code } => {
+ self.running = false;
+ self.pty_tx = None;
+ self.base_mut()
+ .emit_signal("finished".into(), &[exit_code.to_variant()]);
+ }
+ Signal::LineWritten { line } => {
+ self.base_mut()
+ .emit_signal("line_written".into(), &[line.to_godot().to_variant()]);
+ }
+ }
+ }
+}
+
+#[godot_api]
+impl INode for Pty {
+ /// Called upon object initialization in the engine
+ fn init(base: Base) -> Self {
+ // Create a channel to communicate with the async runtime
+ let (tx, rx) = channel();
+
+ Self {
+ base,
+ rx,
+ tx,
+ pty_tx: None,
+ running: false,
+ rows: 8000,
+ columns: 8000,
+ width_px: 8000,
+ height_px: 8000,
+ }
+ }
+
+ /// Executed every engine frame
+ fn process(&mut self, _delta: f64) {
+ // 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);
+ }
+ }
+}
diff --git a/gdext/.gitignore b/gdext/.gitignore
deleted file mode 100644
index 5b2d4627..00000000
--- a/gdext/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-.sconsign.dblite
diff --git a/gdext/Makefile b/gdext/Makefile
deleted file mode 100644
index e540f8b0..00000000
--- a/gdext/Makefile
+++ /dev/null
@@ -1,129 +0,0 @@
-NUM_CPU := $(shell nproc)
-
-# Variables to define all the extensions to build. If a new extension is added,
-# they should be added to these lists.
-ADDONS_PATH = ../addons
-ALL_EXT_PATHS = ../addons/dbus ../addons/linuxthread ../addons/pty ../addons/unixsock ../addons/xlib
-ALL_CPP_FILES = $(shell find ./godot-cpp -regex '.*\(\.cpp\|\.h\|\.hpp\)$$') godot-cpp/SConstruct
-
-ALL_SCONS_FILES = godot-cpp/SConstruct \
- godot-dbus/SConstruct \
- godot-linuxthread/SConstruct \
- godot-pty/SConstruct \
- godot-unix-socket/SConstruct \
- godot-xlib/SConstruct
-
-ALL_DEBUG_EXT = $(ADDONS_PATH)/dbus/bin/libdbus.linux.template_debug.x86_64.so \
- $(ADDONS_PATH)/linuxthread/bin/liblinuxthread.linux.template_debug.x86_64.so \
- $(ADDONS_PATH)/pty/bin/libpty.linux.template_debug.x86_64.so \
- $(ADDONS_PATH)/unixsock/bin/libunixsock.linux.template_debug.x86_64.so \
- $(ADDONS_PATH)/xlib/bin/libxlib.linux.template_debug.x86_64.so
-
-ALL_RELEASE_EXT = $(ADDONS_PATH)/dbus/bin/libdbus.linux.template_release.x86_64.so \
- $(ADDONS_PATH)/linuxthread/bin/liblinuxthread.linux.template_release.x86_64.so \
- $(ADDONS_PATH)/pty/bin/libpty.linux.template_release.x86_64.so \
- $(ADDONS_PATH)/unixsock/bin/libunixsock.linux.template_release.x86_64.so \
- $(ADDONS_PATH)/xlib/bin/libxlib.linux.template_release.x86_64.so
-
-ALL_GDEXT_FILES = $(ADDONS_PATH)/dbus/dbus.gdextension \
- $(ADDONS_PATH)/linuxthread/linuxthread.gdextension \
- $(ADDONS_PATH)/pty/pty.gdextension \
- $(ADDONS_PATH)/unixsock/unixsock.gdextension \
- $(ADDONS_PATH)/xlib/xlib.gdextension
-
-##@ 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: ## Build all GDExtensions
- $(MAKE) release debug
-
-.PHONY: release
-release: $(ALL_RELEASE_EXT)
-$(ALL_RELEASE_EXT) &: $(ALL_GDEXT_FILES) $(ALL_CPP_FILES)
- scons platform=linux -j$(NUM_CPU) target=template_release
-
-.PHONY: debug
-debug: $(ALL_DEBUG_EXT)
-$(ALL_DEBUG_EXT) &: $(ALL_GDEXT_FILES) $(ALL_CPP_FILES)
- scons platform=linux -j$(NUM_CPU) target=template_debug
-
-$(ALL_GDEXT_FILES) &: $(ALL_SCONS_FILES)
- mkdir -p $(ALL_EXT_PATHS)
- cp ./godot-dbus/addons/dbus/dbus.gdextension $(ADDONS_PATH)/dbus
- cp ./godot-linuxthread/addons/linuxthread/linuxthread.gdextension $(ADDONS_PATH)/linuxthread
- cp ./godot-pty/addons/pty/pty.gdextension $(ADDONS_PATH)/pty
- cp ./godot-unix-socket/addons/unixsock/unixsock.gdextension $(ADDONS_PATH)/unixsock
- cp ./godot-xlib/addons/xlib/xlib.gdextension $(ADDONS_PATH)/xlib
-
-.PHONY: clean
-clean: ## Clean all build artifacts
- rm -rf $(ALL_EXT_PATHS)
- find ./ -type f -name '*.o' -delete
- find ./ -type f -name '*.a' -delete
- find ./ -type f -name '*.os' -delete
- find ./ -type f -name '*.so' -delete
-
-godot-cpp/SConstruct:
- git submodule update --init godot-cpp
-
-godot-dbus/SConstruct:
- git submodule update --init godot-dbus
-
-godot-linuxthread/SConstruct:
- git submodule update --init godot-linuxthread
-
-godot-pty/SConstruct:
- git submodule update --init godot-pty
-
-godot-unix-socket/SConstruct:
- git submodule update --init godot-unix-socket
-
-godot-xlib/SConstruct:
- git submodule update --init godot-xlib
-
-##@ Updates
-
-.PHONY: update-dbus
-update-dbus: ## Update godot-dbus
- cd godot-dbus
- git fetch
- git rebase origin/main
-
-.PHONY: update-linuxthread
-update-linuxthread: ## Update godot-linuxthread
- cd godot-linuxthread
- git fetch
- git rebase origin/main
-
-.PHONY: update-pty
-update-pty: ## Update godot-pty
- cd godot-pty
- git fetch
- git rebase origin/main
-
-.PHONY: update-unixsock
-update-unixsock: ## Update godot-unixsock
- cd godot-unixsock
- git fetch
- git rebase origin/main
-
-.PHONY: update-xlib
-update-xlib: ## Update godot-xlib
- cd godot-xlib
- git fetch
- git rebase origin/main
diff --git a/gdext/SConstruct b/gdext/SConstruct
deleted file mode 100644
index df56d6dd..00000000
--- a/gdext/SConstruct
+++ /dev/null
@@ -1,114 +0,0 @@
-#!/usr/bin/env python
-from SCons import __version__ as scons_raw_version
-import os
-import sys
-
-# Define path to godot-cpp dependency
-godot_cpp_path = "godot-cpp"
-if 'GODOT_CPP_PATH' in os.environ:
- godot_cpp_path = os.environ['GODOT_CPP_PATH']
-
-# Setup a standard path to output the extension
-EXT_PATH = "../addons/{}/bin/lib{}{}{}"
-
-# Setup the environments from godot-cpp
-env = SConscript(godot_cpp_path + "/SConstruct")
-dbus_env = env.Clone()
-thread_env = env.Clone()
-pty_env = env.Clone()
-unixsock_env = env.Clone()
-xlib_env = env.Clone()
-
-
-# --- godot-dbus ---
-
-# tweak this if you want to use different folders, or more folders, to store your source code in.
-dbus_env.Append(CPPPATH=["godot-dbus/src/"])
-dbus_sources = Glob("godot-dbus/src/*.cpp")
-
-# Include dependency libraries for dbus
-if 'PKG_CONFIG_PATH' in os.environ:
- dbus_env['ENV']['PKG_CONFIG_PATH'] = os.environ['PKG_CONFIG_PATH']
-dbus_env.ParseConfig("pkg-config dbus-1 --cflags --libs")
-
-# Build the shared library
-libdbus = dbus_env.SharedLibrary(
- EXT_PATH.format(
- "dbus", "dbus", dbus_env["suffix"], dbus_env["SHLIBSUFFIX"]
- ),
- source=dbus_sources,
-)
-
-Default(libdbus)
-
-
-# --- godot-linuxthread ---
-
-# tweak this if you want to use different folders, or more folders, to store your source code in.
-thread_env.Append(CPPPATH=["godot-linuxthread/src/"])
-thread_sources = Glob("godot-linuxthread/src/*.cpp")
-
-# Build the shared library
-libthread = thread_env.SharedLibrary(
- EXT_PATH.format("linuxthread",
- "linuxthread", thread_env["suffix"], thread_env["SHLIBSUFFIX"]),
- source=thread_sources,
-)
-
-Default(libthread)
-
-
-# --- godot-pty ---
-
-# tweak this if you want to use different folders, or more folders, to store your source code in.
-pty_env.Append(CPPPATH=["godot-pty/src/"])
-pty_sources = Glob("godot-pty/src/*.cpp")
-
-# Build the shared library
-libpty = pty_env.SharedLibrary(
- EXT_PATH.format("pty",
- "pty", pty_env["suffix"], pty_env["SHLIBSUFFIX"]),
- source=pty_sources,
-)
-
-Default(libpty)
-
-
-# --- godot-unix-socket ---
-
-# tweak this if you want to use different folders, or more folders, to store your source code in.
-unixsock_env.Append(CPPPATH=["godot-unix-socket/src/"])
-unixsock_sources = Glob("godot-unix-socket/src/*.cpp")
-
-# Build the shared library
-libunixsock = unixsock_env.SharedLibrary(
- EXT_PATH.format("unixsock",
- "unixsock", unixsock_env["suffix"], unixsock_env["SHLIBSUFFIX"]),
- source=unixsock_sources,
-)
-
-Default(libunixsock)
-
-
-# --- godot-unix-socket ---
-
-# tweak this if you want to use different folders, or more folders, to store your source code in.
-xlib_env.Append(CPPPATH=["godot-xlib/src/"])
-xlib_sources = Glob("godot-xlib/src/*.cpp")
-
-# Include dependency libraries for the extension
-if 'PKG_CONFIG_PATH' in os.environ:
- xlib_env['ENV']['PKG_CONFIG_PATH'] = os.environ['PKG_CONFIG_PATH']
-xlib_env.ParseConfig("pkg-config x11 --cflags --libs")
-xlib_env.ParseConfig("pkg-config xres --cflags --libs")
-xlib_env.ParseConfig("pkg-config xtst --cflags --libs")
-xlib_env.ParseConfig("pkg-config xi --cflags --libs")
-
-# Build the shared library
-libx11 = xlib_env.SharedLibrary(
- EXT_PATH.format("xlib",
- "xlib", xlib_env["suffix"], xlib_env["SHLIBSUFFIX"]),
- source=xlib_sources,
-)
-
-Default(libx11)
diff --git a/gdext/godot-cpp b/gdext/godot-cpp
deleted file mode 160000
index d6e5286c..00000000
--- a/gdext/godot-cpp
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit d6e5286cc19bbd5b2c626207d3b01a8f145c0f76
diff --git a/gdext/godot-dbus b/gdext/godot-dbus
deleted file mode 160000
index a8f62141..00000000
--- a/gdext/godot-dbus
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit a8f62141612046b9bfff9f3b7d153ab7a122c899
diff --git a/gdext/godot-linuxthread b/gdext/godot-linuxthread
deleted file mode 160000
index dbe78542..00000000
--- a/gdext/godot-linuxthread
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit dbe785424ed5eb62aca2ee33a87922f22c017641
diff --git a/gdext/godot-pty b/gdext/godot-pty
deleted file mode 160000
index cd3128ac..00000000
--- a/gdext/godot-pty
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit cd3128ac07eb3407e877dacea4e93cc524039112
diff --git a/gdext/godot-unix-socket b/gdext/godot-unix-socket
deleted file mode 160000
index 3ce07d78..00000000
--- a/gdext/godot-unix-socket
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 3ce07d7868cc6cd1028ce0ec681b2f6789b04221
diff --git a/gdext/godot-xlib b/gdext/godot-xlib
deleted file mode 160000
index 95e8237d..00000000
--- a/gdext/godot-xlib
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 95e8237dd472bbed6acb1d18fa8b9e9514cbd33b
diff --git a/project.godot b/project.godot
index 2919adce..399a1e4a 100644
--- a/project.godot
+++ b/project.godot
@@ -20,7 +20,7 @@ config/name="Open Gamepad UI"
run/main_scene="res://entrypoint.tscn"
config/use_custom_user_dir=true
config/custom_user_dir_name="opengamepadui"
-config/features=PackedStringArray("4.2", "Forward Plus")
+config/features=PackedStringArray("4.3", "Forward Plus")
run/low_processor_mode=true
boot_splash/bg_color=Color(0, 0, 0, 0)
boot_splash/show_image=false
@@ -56,53 +56,53 @@ import/blender/enabled=false
ui_accept={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194309,"physical_keycode":0,"key_label":0,"unicode":4194309,"echo":false,"script":null)
-, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194310,"physical_keycode":0,"key_label":0,"unicode":4194310,"echo":false,"script":null)
-, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":32,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194309,"physical_keycode":0,"key_label":0,"unicode":4194309,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194310,"physical_keycode":0,"key_label":0,"unicode":4194310,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":true,"script":null)
]
}
ui_select={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":32,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
]
}
ui_focus_next={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194306,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194306,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":10,"pressure":0.0,"pressed":true,"script":null)
]
}
ui_focus_prev={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194306,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194306,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":9,"pressure":0.0,"pressed":true,"script":null)
]
}
ui_left={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":4194319,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":4194319,"location":0,"echo":false,"script":null)
, null, null, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":13,"pressure":0.0,"pressed":true,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null)
]
}
ui_right={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":4194321,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":4194321,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":14,"pressure":0.0,"pressed":true,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null)
]
}
ui_up={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":4194320,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":4194320,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":11,"pressure":0.0,"pressed":true,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null)
]
}
ui_down={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":4194322,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":4194322,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":12,"pressure":0.0,"pressed":true,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":1.0,"script":null)
]
@@ -114,13 +114,13 @@ ogui_guide={
}
ogui_tab_right={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194324,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194324,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":10,"pressure":0.0,"pressed":true,"script":null)
]
}
ogui_tab_left={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194323,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194323,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":9,"pressure":0.0,"pressed":true,"script":null)
]
}
@@ -141,7 +141,7 @@ ogui_west={
}
ogui_east={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":true,"script":null)
]
}
@@ -151,23 +151,23 @@ ogui_guide_action={
}
ogui_osk={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":52,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":52,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
ogui_qb={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194333,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194333,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
ogui_menu={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194332,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194332,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
ogui_back={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194308,"key_label":0,"unicode":0,"echo":false,"script":null)
-, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194308,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":true,"script":null)
]
}
@@ -183,12 +183,12 @@ ogui_right_trigger={
}
ogui_modifier={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
ogui_guide_action_qb={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194333,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194333,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
ogui_power={
@@ -213,22 +213,22 @@ ogui_scroll_right={
}
ogui_search={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194336,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194336,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
ogui_volume_up={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194382,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194382,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
ogui_volume_down={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194380,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194380,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
ogui_volume_mute={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194381,"key_label":0,"unicode":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194381,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
ogui_qam_ov={