diff --git a/Makefile b/Makefile index 51c7784e..d3fcbea5 100644 --- a/Makefile +++ b/Makefile @@ -15,12 +15,12 @@ EXPORT_TEMPLATE ?= $(HOME)/.local/share/godot/export_templates/$(GODOT_REVISION) #EXPORT_TEMPLATE_URL ?= https://downloads.tuxfamily.org/godotengine/$(GODOT_VERSION)/Godot_v$(GODOT_VERSION)-$(GODOT_RELEASE)_export_templates.tpz EXPORT_TEMPLATE_URL ?= https://github.com/godotengine/godot/releases/download/$(GODOT_VERSION)-$(GODOT_RELEASE)/Godot_v$(GODOT_VERSION)-$(GODOT_RELEASE)_export_templates.tpz -ALL_ADDONS := ./addons/dbus/bin/libdbus.linux.template_$(BUILD_TYPE).x86_64.so ./addons/linuxthread/bin/liblinuxthread.linux.template_$(BUILD_TYPE).x86_64.so ./addons/pty/bin/libpty.linux.template_$(BUILD_TYPE).x86_64.so ./addons/unixsock/bin/libunixsock.linux.template_$(BUILD_TYPE).x86_64.so ./addons/xlib/bin/libxlib.linux.template_$(BUILD_TYPE).x86_64.so -ALL_ADDON_FILES := $(shell find ./addons -regex '.*\(\.cpp\|\.h\|\.hpp\)$$') +ALL_EXTENSIONS := ./addons/core/bin/libopengamepadui-core.linux.template_$(BUILD_TYPE).x86_64.so +ALL_EXTENSION_FILES := $(shell find ./extensions/ -regex '.*\(\.rs|\.toml\|\.lock\)$$') ALL_GDSCRIPT := $(shell find ./ -name '*.gd') ALL_SCENES := $(shell find ./ -name '*.tscn') ALL_RESOURCES := $(shell find ./ -regex '.*\(\.tres\|\.svg\|\.png\)$$') -PROJECT_FILES := $(ALL_ADDONS) $(ALL_GDSCRIPT) $(ALL_SCENES) $(ALL_RESOURCES) +PROJECT_FILES := $(ALL_EXTENSIONS) $(ALL_GDSCRIPT) $(ALL_SCENES) $(ALL_RESOURCES) # Docker image variables IMAGE_NAME ?= ghcr.io/shadowblip/opengamepadui-builder @@ -144,24 +144,24 @@ build/metadata.json: build/opengamepad-ui.x86_64 assets/crypto/keys/opengamepadu .PHONY: import import: $(IMPORT_DIR) ## Import project assets -$(IMPORT_DIR): $(ALL_ADDONS) +$(IMPORT_DIR): $(ALL_EXTENSIONS) @echo "Importing project assets. This will take some time..." command -v $(GODOT) > /dev/null 2>&1 timeout --foreground 40 $(GODOT) --headless --editor . > /dev/null 2>&1 || echo "Finished" touch $(IMPORT_DIR) .PHONY: force-import -force-import: $(ALL_ADDONS) +force-import: $(ALL_EXTENSIONS) @echo "Force importing project assets. This will take some time..." command -v $(GODOT) > /dev/null 2>&1 timeout --foreground 40 $(GODOT) --headless --editor . > /dev/null 2>&1 || echo "Finished" timeout --foreground 40 $(GODOT) --headless --editor . > /dev/null 2>&1 || echo "Finished" -.PHONY: addons -addons: $(ALL_ADDONS) ## Build GDExtension addons -$(ALL_ADDONS) &: $(ALL_ADDON_FILES) - @echo "Building native GDExtension addons..." - cd ./gdext && $(MAKE) build +.PHONY: extensions +extensions: $(ALL_EXTENSIONS) ## Build native extensions +$(ALL_EXTENSIONS) &: $(ALL_EXTENSION_FILES) + @echo "Building native extensions..." + cd ./extensions/core && $(MAKE) build .PHONY: edit edit: $(IMPORT_DIR) ## Open the project in the Godot editor @@ -174,7 +174,7 @@ clean: ## Remove build artifacts rm -rf $(CACHE_DIR) rm -rf dist rm -rf $(IMPORT_DIR) - cd ./gdext && $(MAKE) clean + cd ./extensions/core && $(MAKE) clean .PHONY: run run-force run: build/opengamepad-ui.x86_64 run-force ## Run the project in gamescope diff --git a/addons/.gitignore b/addons/.gitignore index 99eef491..140f8cf8 100644 --- a/addons/.gitignore +++ b/addons/.gitignore @@ -1,5 +1 @@ -dbus/ -linuxthread/ -pty/ -unixsock/ -xlib/ +*.so diff --git a/addons/core/assets/icons/inputplumber.svg b/addons/core/assets/icons/inputplumber.svg new file mode 100644 index 00000000..4ea3ae1e --- /dev/null +++ b/addons/core/assets/icons/inputplumber.svg @@ -0,0 +1,76 @@ + + diff --git a/addons/core/assets/icons/inputplumber.svg.import b/addons/core/assets/icons/inputplumber.svg.import new file mode 100644 index 00000000..b1198507 --- /dev/null +++ b/addons/core/assets/icons/inputplumber.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dbdyxgqmyeg1f" +path="res://.godot/imported/inputplumber.svg-930e8ab4c0d3c4d458c32ef96742bdaf.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/core/assets/icons/inputplumber.svg" +dest_files=["res://.godot/imported/inputplumber.svg-930e8ab4c0d3c4d458c32ef96742bdaf.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/core/assets/icons/library.svg b/addons/core/assets/icons/library.svg new file mode 100644 index 00000000..a4c3cef4 --- /dev/null +++ b/addons/core/assets/icons/library.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/core/assets/icons/library.svg.import b/addons/core/assets/icons/library.svg.import new file mode 100644 index 00000000..36d12ed4 --- /dev/null +++ b/addons/core/assets/icons/library.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bn4jiyx7afl3b" +path="res://.godot/imported/library.svg-8fcfe8437951fa126b5ab9f50dba6f13.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/core/assets/icons/library.svg" +dest_files=["res://.godot/imported/library.svg-8fcfe8437951fa126b5ab9f50dba6f13.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/core/core.gdextension b/addons/core/core.gdextension new file mode 100644 index 00000000..e4c355d7 --- /dev/null +++ b/addons/core/core.gdextension @@ -0,0 +1,12 @@ +[configuration] +entry_symbol = "gdext_rust_init" +compatibility_minimum = 4.3 +reloadable = false + +[libraries] +linux.debug.x86_64 = "res://addons/core/bin/libopengamepadui-core.linux.template_debug.x86_64.so" +linux.release.x86_64 = "res://addons/core/bin/libopengamepadui-core.linux.template_release.x86_64.so" + +[icons] +LibraryItem = "res://addons/core/assets/icons/library.svg" +LibraryLaunchItem = "res://addons/core/assets/icons/library.svg" diff --git a/addons/gut/fonts/AnonymousPro-Bold.ttf.import b/addons/gut/fonts/AnonymousPro-Bold.ttf.import index a3eb4791..de1351f6 100644 --- a/addons/gut/fonts/AnonymousPro-Bold.ttf.import +++ b/addons/gut/fonts/AnonymousPro-Bold.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/AnonymousPro-Bold.ttf-9d8fef4d357af5b52cd60af Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import b/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import index ef28dd80..bdde2072 100644 --- a/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import +++ b/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/AnonymousPro-BoldItalic.ttf-4274bf704d3d6b9cd Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/AnonymousPro-Italic.ttf.import b/addons/gut/fonts/AnonymousPro-Italic.ttf.import index 1779af17..ce3e5b91 100644 --- a/addons/gut/fonts/AnonymousPro-Italic.ttf.import +++ b/addons/gut/fonts/AnonymousPro-Italic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/AnonymousPro-Italic.ttf-9989590b02137b799e13d Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/AnonymousPro-Regular.ttf.import b/addons/gut/fonts/AnonymousPro-Regular.ttf.import index 1e2975b1..a567498c 100644 --- a/addons/gut/fonts/AnonymousPro-Regular.ttf.import +++ b/addons/gut/fonts/AnonymousPro-Regular.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/AnonymousPro-Regular.ttf-856c843fd6f89964d2ca Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/CourierPrime-Bold.ttf.import b/addons/gut/fonts/CourierPrime-Bold.ttf.import index 7d60fb0a..cb05171d 100644 --- a/addons/gut/fonts/CourierPrime-Bold.ttf.import +++ b/addons/gut/fonts/CourierPrime-Bold.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/CourierPrime-Bold.ttf-1f003c66d63ebed70964e77 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import b/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import index 4678c9eb..0a9a7b77 100644 --- a/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import +++ b/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/CourierPrime-BoldItalic.ttf-65ebcc61dd5e1dfa8 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/CourierPrime-Italic.ttf.import b/addons/gut/fonts/CourierPrime-Italic.ttf.import index 522e2950..89412fc9 100644 --- a/addons/gut/fonts/CourierPrime-Italic.ttf.import +++ b/addons/gut/fonts/CourierPrime-Italic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/CourierPrime-Italic.ttf-baa9156a73770735a0f72 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/CourierPrime-Regular.ttf.import b/addons/gut/fonts/CourierPrime-Regular.ttf.import index 38174660..9fde40b1 100644 --- a/addons/gut/fonts/CourierPrime-Regular.ttf.import +++ b/addons/gut/fonts/CourierPrime-Regular.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/CourierPrime-Regular.ttf-3babe7e4a7a588dfc9a8 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/LobsterTwo-Bold.ttf.import b/addons/gut/fonts/LobsterTwo-Bold.ttf.import index 7548ad04..673d1515 100644 --- a/addons/gut/fonts/LobsterTwo-Bold.ttf.import +++ b/addons/gut/fonts/LobsterTwo-Bold.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/LobsterTwo-Bold.ttf-7c7f734103b58a32491a47881 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import b/addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import index 4b609e80..62048b0e 100644 --- a/addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import +++ b/addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/LobsterTwo-BoldItalic.ttf-227406a33e84448e6aa Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/LobsterTwo-Italic.ttf.import b/addons/gut/fonts/LobsterTwo-Italic.ttf.import index 5899b797..d3ca2728 100644 --- a/addons/gut/fonts/LobsterTwo-Italic.ttf.import +++ b/addons/gut/fonts/LobsterTwo-Italic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/LobsterTwo-Italic.ttf-f93abf6c25390c85ad5fb6c Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/LobsterTwo-Regular.ttf.import b/addons/gut/fonts/LobsterTwo-Regular.ttf.import index 45a12c8a..9cc75421 100644 --- a/addons/gut/fonts/LobsterTwo-Regular.ttf.import +++ b/addons/gut/fonts/LobsterTwo-Regular.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/LobsterTwo-Regular.ttf-f3fcfa01cd671c8da433dd Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/assets/editor-icons/inputplumber.svg b/assets/editor-icons/inputplumber.svg new file mode 100644 index 00000000..4e63ef66 --- /dev/null +++ b/assets/editor-icons/inputplumber.svg @@ -0,0 +1,50 @@ + + diff --git a/assets/editor-icons/inputplumber.svg.import b/assets/editor-icons/inputplumber.svg.import new file mode 100644 index 00000000..4f82c89c --- /dev/null +++ b/assets/editor-icons/inputplumber.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dhwvdnbmrvqu4" +path="res://.godot/imported/inputplumber.svg-f9a02e1ec85cdf7dc3601d7003f9de2a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/editor-icons/inputplumber.svg" +dest_files=["res://.godot/imported/inputplumber.svg-f9a02e1ec85cdf7dc3601d7003f9de2a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=0.063 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/assets/editor-icons/powerstation.svg b/assets/editor-icons/powerstation.svg new file mode 100644 index 00000000..1a16dd9a --- /dev/null +++ b/assets/editor-icons/powerstation.svg @@ -0,0 +1,45 @@ + + + + + + + + diff --git a/assets/editor-icons/powerstation.svg.import b/assets/editor-icons/powerstation.svg.import new file mode 100644 index 00000000..98abeedb --- /dev/null +++ b/assets/editor-icons/powerstation.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://2vefe1d32i3a" +path="res://.godot/imported/powerstation.svg-9e86562cff9f28db8f5fb4ce0386cdf7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/editor-icons/powerstation.svg" +dest_files=["res://.godot/imported/powerstation.svg-9e86562cff9f28db8f5fb4ce0386cdf7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=0.063 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/assets/editor-icons/streamline--desktop-game-solid.svg b/assets/editor-icons/streamline--desktop-game-solid.svg new file mode 100644 index 00000000..d8ebec7e --- /dev/null +++ b/assets/editor-icons/streamline--desktop-game-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/editor-icons/streamline--desktop-game-solid.svg.import b/assets/editor-icons/streamline--desktop-game-solid.svg.import new file mode 100644 index 00000000..b575a272 --- /dev/null +++ b/assets/editor-icons/streamline--desktop-game-solid.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cav2x6q8apups" +path="res://.godot/imported/streamline--desktop-game-solid.svg-ff379b75f0c49d1fc9be24f364b571c9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/editor-icons/streamline--desktop-game-solid.svg" +dest_files=["res://.godot/imported/streamline--desktop-game-solid.svg-ff379b75f0c49d1fc9be24f364b571c9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/core/global/dbus_manager.gd b/core/global/dbus_manager.gd deleted file mode 100644 index 4786b3bd..00000000 --- a/core/global/dbus_manager.gd +++ /dev/null @@ -1,311 +0,0 @@ -extends Resource -class_name DBusManager - -## DBusManager is a helper class for using DBus -## -## Use this class to interface with DBus. - -enum BUS_TYPE { - SYSTEM = dbus.DBUS_BUS_SYSTEM, - SESSION = dbus.DBUS_BUS_SESSION, - STARTER = dbus.DBUS_BUS_STARTER, -} - -const DBUS_BUS := "org.freedesktop.DBus" -const DBUS_PATH := "/org/freedesktop/DBus" - -const IFACE_DBUS := "org.freedesktop.DBus" -const IFACE_PROPERTIES := "org.freedesktop.DBus.Properties" -const IFACE_OBJECT_MANAGER := "org.freedesktop.DBus.ObjectManager" - -## Type of bus to connect to -@export var bus_type := BUS_TYPE.SYSTEM -## Shared thread to process DBus messages on -@export var thread: SharedThread = load("res://core/systems/threading/system_thread.tres") - -var logger := Log.get_logger("DBusManager") -var well_known_names := [] -var dbus := DBus.new() -var dbus_proxy := DBusProxy.new(create_proxy(DBUS_BUS, DBUS_PATH)) - - -func _init() -> void: - if dbus.connect(bus_type) != OK: - logger.warn("Unable to connect to dbus") - return - thread.add_process(_process) - thread.start() - - -## Process messages on the bus that are being watched and dispatch them. -func _process(_delta: float): - var messages: Array[DBusMessage] = [] - var msg := dbus.pop_message() - while msg != null: - logger.debug("Received DBus message on " + msg.get_sender() + " from " + msg.get_path() + ": " + str(msg.get_args())) - messages.append(msg) - msg = dbus.pop_message() - - for message in messages: - _process_message(message) - - -## Dispatch the given message to any proxy objects -func _process_message(msg: DBusMessage) -> void: - # Try looking up the well-known name of the message sender - var known_names := get_names_for_owner(msg.get_sender()) - - # Try constructing the resource path to the proxy and see if it exists - for known_name in known_names: - var res_path := "dbus://" + known_name + msg.get_path() - if not ResourceLoader.exists(res_path): - logger.debug("No proxy resource found to send message to at: " + res_path) - continue - logger.debug("Found proxy to send message signal to at: " + res_path) - var proxy := load(res_path) as Proxy - if not proxy: - logger.warn("Failed to load proxy from resource cache: " + res_path) - continue - var send_signal := func(message: DBusMessage) -> void: - proxy.message_received.emit(message) - send_signal.call_deferred(msg) - break - - -## Creates a reference to a DBus object on the given bus at the given path. -## E.g. create_proxy("org.bluez", "/org/bluez/hci0") -func create_proxy(bus: String, path: String) -> Proxy: - # Try to load the proxy if it already exists - var res_path := "dbus://" + bus + path - logger.debug("Creating proxy with resource path: " + res_path) - var proxy: Proxy - if ResourceLoader.exists(res_path): - logger.debug("Resource already exists. Returning existing instance.") - proxy = load(res_path) - return proxy - - proxy = Proxy.new(dbus, bus, path) - proxy.take_over_path(res_path) - - # Keep track of bus names so they can be referenced later - if not bus in well_known_names: - well_known_names.append(bus) - - return proxy - - -## Returns true if the given well-known name has an owner. -func bus_exists(name: String) -> bool: - return dbus.name_has_owner(name) - -# TODO: This is deprecated. Remove this and all refrences to it. -## Returns a dictionary of manages objects for the given bus and path -func get_managed_objects(bus: String, path: String) -> Array[ManagedObject]: - var obj := create_proxy(bus, path) - var result := obj.call_method(IFACE_OBJECT_MANAGER, "GetManagedObjects", [], "") - if not result: - return [] - var args := result.get_args() - if args.size() != 1: - return [] - if not args[0] is Dictionary: - return [] - - var objs_dict := args[0] as Dictionary - var objects: Array[ManagedObject] = [] - - # Convert the objects dictionary into an array of objects - for obj_path in objs_dict.keys(): - var obj_data := objs_dict[obj_path] as Dictionary - var object := ManagedObject.new(obj_path as String, obj_data) - objects.append(object) - - return objects - - -## Tries to resolve well-known names (e.g. "org.bluez") from the given owner (e.g. ":1.5"). -## This will return an array of well-known names. -func get_names_for_owner(owner: String) -> PackedStringArray: - var names := PackedStringArray() - for name in well_known_names: - var name_owner := dbus_proxy.get_name_owner(name as String) - if name_owner == owner: - names.append(name as String) - - return names - - -func _to_string() -> String: - var bus_string := "System" if bus_type == BUS_TYPE.SYSTEM else "Session" - return "".format([bus_string]) - - -class ObjectManager extends Resource: - signal interfaces_added(dbus_path: String) - signal interfaces_removed(dbus_path: String) - - var _proxy: Proxy - - func _init(proxy: Proxy) -> void: - _proxy = proxy - _proxy.message_received.connect(_on_message_received) - _proxy.thread.exec(_proxy.watch.bind(IFACE_OBJECT_MANAGER, "InterfacesAdded")) - _proxy.thread.exec(_proxy.watch.bind(IFACE_OBJECT_MANAGER, "InterfacesRemoved")) - - ## Returns a dictionary of manages objects for the given bus and path - func get_managed_objects(_bus: String, _path: String) -> Array[ManagedObject]: - var result := _proxy.call_method(IFACE_OBJECT_MANAGER, "GetManagedObjects", [], "") - if not result: - return [] - var args := result.get_args() - if args.size() != 1: - return [] - if not args[0] is Dictionary: - return [] - - var objs_dict := args[0] as Dictionary - var objects: Array[ManagedObject] = [] - - # Convert the objects dictionary into an array of objects - for obj_path in objs_dict.keys(): - var obj_data := objs_dict[obj_path] as Dictionary - var object := ManagedObject.new(obj_path as String, obj_data) - objects.append(object) - - return objects - - func _on_message_received(msg: DBusMessage) -> void: - #print("Got message: " + str(msg)) - if not msg: - return - var args := msg.get_args() - #print("Got args: " + str(args)) - if args.size() != 2: - return - #print("Got args big enough") - if msg.get_member() == "InterfacesAdded": - #print("Got InterfacesAdded") - interfaces_added.emit(args[0]) - - if msg.get_member() == "InterfacesRemoved": - #print("Got InterfacesRemoved") - interfaces_removed.emit(args[0]) - - -## A Proxy provides an interface to call methods on a DBus object. -class Proxy extends Resource: - signal message_received(msg: DBusMessage) - signal properties_changed(iface: String, props: Dictionary) - var _dbus: DBus - var bus_name: String - var path: String - var rules := PackedStringArray() - var logger := Log.get_logger("DBusProxy") - var thread: SharedThread = load("res://core/systems/threading/system_thread.tres") - - func _init(conn: DBus, bus: String, obj_path: String) -> void: - _dbus = conn - bus_name = bus - path = obj_path - message_received.connect(_on_property_changed) - thread.exec(watch.bind(IFACE_PROPERTIES, "PropertiesChanged")) - - func _on_property_changed(msg: DBusMessage) -> void: - if not msg: - return - if msg.get_member() != "PropertiesChanged": - return - var args := msg.get_args() - if args.size() < 2: - return - properties_changed.emit(args[0], args[1]) - - func _notification(what: int) -> void: - if what != NOTIFICATION_PREDELETE: - return - for rule in rules: - logger.debug("Removing watch rule: " + rule) - _dbus.remove_match(rule) - - ## Call the given method - func call_method(iface: String, method: String, args: Array = [], signature: String = "") -> DBusMessage: - logger.debug("Calling method: " + iface + "::" + method + "(" + str(args) + ")") - return _dbus.send_with_reply_and_block(bus_name, path, iface, method, args, signature) - - ## Set the given property - func set_property(iface: String, property: String, value: Variant) -> void: - logger.debug("Set property " + property + " to " + str(value) + " for interface " + iface) - call_method(IFACE_PROPERTIES, "Set", [iface, property, value], "ssv") - - ## Get the given property - func get_property(iface: String, property: String) -> Variant: - var response := call_method(IFACE_PROPERTIES, "Get", [iface, property], "ss") - if not response: - return null - var args := response.get_args() - if args.size() == 0: - return null - - return args[0] - - ## Get all properties for the given interface - func get_properties(iface: String) -> Dictionary: - var response := call_method(IFACE_PROPERTIES, "GetAll", [iface], "s") - if not response: - return {} - var args := response.get_args() - if args.size() == 0: - return {} - - return args[0] - - ## Watch the bus for particular signals - func watch(iface: String, member: String = "PropertiesChanged") -> int: - var rule := "type='signal',interface='{0}',path='{1}',member='{2}'".format( - [iface, path, member] - ) - rules.append(rule) - logger.debug("Adding watch rule: " + rule) - var err := _dbus.add_match(rule) - if err != OK: - logger.error("Unable to watch " + path) - return err - - -## A ManagedObject is a simple structure used with GetManagedObjects -class ManagedObject: - var path: String - var data: Dictionary - - func _init(obj_path: String, obj_data: Dictionary) -> void: - path = obj_path - data = obj_data - - func has_interface(name: String) -> bool: - return name in data - - func has_interface_attr(iface: String, name: String) -> bool: - if not iface in data: - return false - if not name in data[iface]: - return false - return true - - -## Proxy to manage /org/freedesktop/DBus on the org.freedesktop.DBus bus. -class DBusProxy: - var _proxy: Proxy - - func _init(proxy: Proxy) -> void: - _proxy = proxy - - ## Return the connection name (e.g. ":1.1270") from the given well-known name - func get_name_owner(name: String) -> String: - var msg := _proxy.call_method(IFACE_DBUS, "GetNameOwner", [name], "s") - if not msg: - return "" - var args := msg.get_args() - if args.size() != 1: - return "" - - return args[0] diff --git a/core/global/dbus_system.tres b/core/global/dbus_system.tres deleted file mode 100644 index 95c70f0d..00000000 --- a/core/global/dbus_system.tres +++ /dev/null @@ -1,9 +0,0 @@ -[gd_resource type="Resource" script_class="DBusManager" load_steps=3 format=3 uid="uid://dskqww1130rb2"] - -[ext_resource type="Script" path="res://core/global/dbus_manager.gd" id="1_6xf1o"] -[ext_resource type="Resource" uid="uid://ct7pi0cx0r832" path="res://core/systems/threading/system_thread.tres" id="2_p3brt"] - -[resource] -script = ExtResource("1_6xf1o") -bus_type = 1 -thread = ExtResource("2_p3brt") diff --git a/core/global/gamescope.gd b/core/global/gamescope.gd deleted file mode 100644 index d8ae55f7..00000000 --- a/core/global/gamescope.gd +++ /dev/null @@ -1,751 +0,0 @@ -extends Resource -class_name Gamescope - -## Interact with Gamescope windows and properties -## -## The Gamescope class is responsible for interacting with Gamescope, usually -## via the means of setting gamescope-specific window properties. It can be -## used to discover Gamescope displays, list windows and their children, and set -## gamescope-specific window atoms to switch windows, set blur, limit FPS, etc. -## [br][br] -## For example, to limit the FPS, you can do the following: -## [codeblock] -## Gamescope.set_fps_limit(display, 30) -## [/codeblock] -## [br][br] -## Most of the core functionality of this class is provided by the [Xlib] -## module, which is a GDExtension that exposes Xlib methods to Godot. - -signal blur_mode_updated(from: int, to: int) -signal display_is_external_updated(from: int, to: int) -signal focused_window_updated(from: int, to: int) -signal focusable_windows_updated(from: PackedInt32Array, to: PackedInt32Array) -signal focused_app_updated(from: int, to: int) -signal focused_app_gfx_updated(from: int, to: int) -signal focusable_apps_updated(from: PackedInt32Array, to: PackedInt32Array) - -## Gamescope Blur modes -enum BLUR_MODE { - OFF = 0, ## Turns off blur of running games - COND = 1, ## Conditionally blurs running games - ALWAYS = 2, ## Turns blurring of running games on -} - -## Specifies which Gamescope xwayland server to perform an operation on. -enum XWAYLAND { - PRIMARY, ## Primary Gamescope xwayland instance - OGUI, ## Xwayland instance that OpenGamepadUI is running on - GAME, ## Xwayland instance where games run -} - -## Gamescope is hard-coded to look for STEAM_GAME=769 to determine if it is the -## overlay app. -const OVERLAY_GAME_ID := 769 - -@export var log_level := Log.LEVEL.INFO -## The primary xwayland is the primary Gamescope xwayland session that contains -## Gamescope properties on the root window. -var xwayland_primary: Xlib -## The OGUI xwayland is the xwayland instance that OGUI is running under. -var xwayland_ogui: Xlib -## The Game xwayland is the xwayland instance that games are launched under. -var xwayland_game: Xlib -## Array of all discovered xwayland instances -var xwaylands: Array[Xlib] = [] -var logger := Log.get_logger("Gamescope", log_level) - -# Gamescope properties -## Blur mode (read-only) -var blur_mode: int: - set(v): - var prev_value := blur_mode - blur_mode = v - if prev_value != v: - blur_mode_updated.emit(prev_value, v) -var baselayer_window: int -var input_counter: int -var display_is_external: int -var vrr_enabled: int -var vrr_feedback: int -var vrr_capable: int -var keyboard_focus_display: PackedInt32Array -var mouse_focus_display: PackedInt32Array -var focus_display: PackedInt32Array -var focused_window: int: - set(v): - var prev_value := focused_window - focused_window = v - if prev_value != v: - focused_window_updated.emit(prev_value, v) -var focused_app_gfx: int: - set(v): - var prev_value := focused_app_gfx - focused_app_gfx = v - if prev_value != v: - focused_app_gfx_updated.emit(prev_value, v) -var focused_app: int: - set(v): - var prev_value := focused_app - focused_app = v - if prev_value != v: - focused_app_updated.emit(prev_value, v) -var focusable_windows: PackedInt32Array: - set(v): - var prev_value := focusable_windows - focusable_windows = v - if prev_value != v: - focusable_windows_updated.emit(prev_value, v) -var focusable_apps: PackedInt32Array: - set(v): - var prev_value := focusable_apps - focusable_apps = v - if prev_value != v: - focusable_apps_updated.emit(prev_value, v) -var cursor_visible_feedback: int - - -# Connects to all gamescope xwayland instances -func _init() -> void: - # Don't initialize if run from the editor (during doc generation) - if Engine.is_editor_hint(): - logger.info("Not initializing. Ran from editor.") - return - - # Connect to the xwayland instance that OGUI is running on - var ogui_display := OS.get_environment("DISPLAY") - xwayland_ogui = Xlib.new() - if xwayland_ogui.open(ogui_display) != OK: - logger.error("Failed to open OGUI X server: " + ogui_display) - return - if _is_gamescope_xwayland(xwayland_ogui): - logger.debug("OpenGamepadUI is running in Gamescope") - xwayland_game = xwayland_ogui - if _is_gamescope_xwayland_primary(xwayland_ogui): - logger.debug("OpenGamepadUI is running on the primary Gamescope xwayland") - xwayland_primary = xwayland_ogui - xwaylands.push_front(xwayland_ogui) - - # Discover all other xwayland displays - var displays := discover_gamescope_displays() - for display in displays: - if _has_xwayland(display): - logger.debug("Already discovered xwayland: " + display) - continue - var xwayland := Xlib.new() - if xwayland.open(display) != OK: - logger.debug("Failed to open X server: " + display) - continue - if _is_gamescope_xwayland_primary(xwayland): - logger.debug("Display " + display + " is the primary gamescope instance") - xwayland_primary = xwayland - if xwayland_primary != xwayland: - xwayland_game = xwayland - xwaylands.append(xwayland) - - # If we haven't discovered any gamescope displays, set everything to use - # the OGUI xwayland instance - if not xwayland_primary: - logger.warn("OpenGamepadUI is not running in Gamescope. Unexpected behavior expected.") - xwayland_primary = xwayland_ogui - if not xwayland_game: - xwayland_game = xwayland_ogui - - logger.debug("Primary xwayland is " + xwayland_primary.get_name()) - logger.debug("OGUI xwayland is " + xwayland_ogui.get_name()) - logger.debug("Game xwayland is " + xwayland_game.get_name()) - - -## Returns all gamescope xwayland names (E.g. [":0", ":1"]) -# TODO: This seems brittle. Is there any other way we can discover Gamescope displays? -func discover_gamescope_displays() -> PackedStringArray: - logger.debug("Discovering xwaylands!") - - # X11 displays have a corresponding socket in /tmp/.X11-unix - # The sockets are named like: X0, X1, X2, etc. - var dir := DirAccess.open("/tmp/.X11-unix") - var sockets := dir.get_files() - - # Loop through each socket file and derrive the display number. - var display_names: PackedInt32Array = [] - for socket in sockets: - var suffix := (socket as String).trim_prefix("X") - if not suffix.is_valid_int(): - logger.warn("Skipping X11 socket with a weird name: " + socket) - continue - display_names.append(suffix.to_int()) - - # Check to see if the root window of these displays has gamescope-specific properties - var gamescope_displays := PackedStringArray() - for display_num in display_names: - var display := ":{0}".format([display_num]) - var xwayland := Xlib.new() - if xwayland.open(display) != OK: - logger.debug("Failed to open X server: " + display) - continue - if _is_gamescope_xwayland(xwayland): - gamescope_displays.append(display) - logger.debug("Discovered X server display: " + display) - xwayland.close() - return gamescope_displays - - -## Updates the Gamescope state. Should be called in a loop to keep the Gamescope -## state up-to-date. -func update() -> void: - blur_mode = get_blur_mode() - focused_window = get_focused_window() - focused_app = get_focused_app() - focused_app_gfx = get_focused_app_gfx() - focusable_windows = get_focusable_windows() - focusable_apps = get_focusable_apps() - baselayer_window = get_baselayer_window() - if not baselayer_window in get_focusable_windows(): - baselayer_window = -1 - remove_baselayer_window() - - -## Returns the name of the given xwayland display (e.g. ":1") -func get_display_name(display: XWAYLAND) -> String: - var xwayland := get_xwayland(display) - if not xwayland: - return "" - return xwayland.get_name() - - -## Returns true if the given X property exists on the given window. -func has_xprop(window_id: int, key: String, display: XWAYLAND) -> bool: - var xwayland := get_xwayland(display) - if not xwayland: - return false - return xwayland.has_xprop(window_id, key) - - -## Returns a list of X properties on the given window -func list_xprops(window_id: int, display: XWAYLAND) -> PackedStringArray: - var xwayland := get_xwayland(display) - if not xwayland: - return [] - return xwayland.list_xprops(window_id) - - -## Returns the name of the given window. -func get_window_name(window_id: int, display: XWAYLAND = XWAYLAND.GAME) -> String: - var xwayland := get_xwayland(display) - if not xwayland: - return "" - return xwayland.get_window_name(window_id) - - -## Returns the PID of the given window. Returns -1 if no PID was found. -func get_window_pid(window_id: int, display: XWAYLAND) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - return xwayland.get_window_pid(window_id) - - -## Returns the xwayland window ID for the given process. Returns -1 if no -## window was found. -func get_window_id(pid: int, display: XWAYLAND) -> int: - var display_name := get_display_name(display) - logger.trace("Getting Window ID for {0} on display {1}".format([pid, display_name])) - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - var all_windows := get_all_windows(root_id, display) - for window_id in all_windows: - var window_pid := xwayland.get_window_pid(window_id) - if pid == window_pid: - return window_id - window_pid = xwayland.get_xprop(window_id, "_NET_WM_PID") - if pid == window_pid: - return window_id - - return -1 - - -## Returns the xwayland window ID(s) for the given process using multiple methods -## to try and discover. -func get_window_ids(pid: int, display: XWAYLAND) -> PackedInt32Array: - var window_ids := PackedInt32Array() - var display_name := get_display_name(display) - logger.trace("Getting Window ID for {0} on display {1}".format([pid, display_name])) - var xwayland := get_xwayland(display) - if not xwayland: - return window_ids - var root_id := xwayland.get_root_window_id() - var all_windows := get_all_windows(root_id, display) - for window_id in all_windows: - var window_pid := xwayland.get_window_pid(window_id) - if pid == window_pid and not window_id in window_ids: - window_ids.append(window_id) - var net_window_pid := xwayland.get_xprop(window_id, "_NET_WM_PID") - if pid == net_window_pid and not net_window_pid in window_ids: - window_ids.append(net_window_pid) - - return window_ids - - -## Returns the child window ids of the given window -func get_window_children(window_id: int, display: XWAYLAND) -> PackedInt32Array: - var xwayland := get_xwayland(display) - if not xwayland: - return PackedInt32Array() - var children := xwayland.get_window_children(window_id) - if len(children) == 0: - return PackedInt32Array() - return children - - -## Recursively returns all child windows of the given window id -func get_all_windows(window_id: int, display: XWAYLAND) -> PackedInt32Array: - var xwayland := get_xwayland(display) - if not xwayland: - return PackedInt32Array() - var children := xwayland.get_window_children(window_id) - if len(children) == 0: - return PackedInt32Array([]) - - var leaves := PackedInt32Array() - for child in children: - leaves.append(child) - leaves.append_array(get_all_windows(child, display)) - - return leaves - - -## Returns true if the window with the given window ID exists -func is_focusable_app(window_id: int, display: XWAYLAND = XWAYLAND.PRIMARY) -> bool: - var focusable := get_focusable_apps(display) - if window_id in focusable: - return true - return false - - -## Returns the root window ID of the given display -func get_root_window_id(display: XWAYLAND) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - return xwayland.get_root_window_id() - - -## Returns a list of focusable app window ids -func get_focusable_apps(display: XWAYLAND = XWAYLAND.PRIMARY) -> PackedInt32Array: - var xwayland := get_xwayland(display) - if not xwayland: - return PackedInt32Array() - var root_id := xwayland.get_root_window_id() - return _get_xprop_array(xwayland, root_id, "GAMESCOPE_FOCUSABLE_APPS") - - -## Returns a list of focusable window ids -func get_focusable_windows(display: XWAYLAND = XWAYLAND.PRIMARY) -> PackedInt32Array: - var xwayland := get_xwayland(display) - if not xwayland: - return PackedInt32Array() - var root_id := xwayland.get_root_window_id() - return _get_xprop_array(xwayland, root_id, "GAMESCOPE_FOCUSABLE_WINDOWS") - - -## Returns a list of focusable window names -func get_focusable_window_names(display: XWAYLAND = XWAYLAND.PRIMARY) -> PackedStringArray: - var focusable := get_focusable_windows(display) - var results := PackedStringArray() - for window_id in focusable: - var name := get_window_name(window_id, display) - results.append(name) - return results - - -## Return the currently focused window id. -func get_focused_window(display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - var results := _get_xprop_array(xwayland, root_id, "GAMESCOPE_FOCUSED_WINDOW") - if results.size() == 0: - return 0 - return results[0] - - -## Return the currently focused app id. -func get_focused_app(display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - var results := _get_xprop_array(xwayland, root_id, "GAMESCOPE_FOCUSED_APP") - if results.size() == 0: - return 0 - return results[0] - - -## Return the currently focused gfx app id. -func get_focused_app_gfx(display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - var results := _get_xprop_array(xwayland, root_id, "GAMESCOPE_FOCUSED_APP_GFX") - if results.size() == 0: - return 0 - return results[0] - - -## Sets the given window as the main launcher app. -## Gamescope is hard-coded to look for appId 769 -func set_main_app(window_id: int, display: XWAYLAND = XWAYLAND.OGUI) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - return _set_xprop(xwayland, window_id, "STEAM_GAME", OVERLAY_GAME_ID) - - -## Set the given window as the primary overlay input focus. This should be set to -## "1" whenever the overlay wants to intercept input from a game. -func set_input_focus(window_id: int, value: int, display: XWAYLAND = XWAYLAND.OGUI) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - return _set_xprop(xwayland, window_id, "STEAM_INPUT_FOCUS", value) - - -## Returns whether or not the overlay window is currently focused -func is_overlay_focused(display: XWAYLAND = XWAYLAND.OGUI) -> bool: - return get_focused_app(display) == OVERLAY_GAME_ID - - -## Get the overlay status for the given window -func get_overlay(window_id: int, display: XWAYLAND = XWAYLAND.OGUI) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - return _get_xprop(xwayland, window_id, "STEAM_OVERLAY") - - -## Set the given window as an overlay -func set_overlay(window_id: int, value: int, display: XWAYLAND = XWAYLAND.OGUI) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - return _set_xprop(xwayland, window_id, "STEAM_OVERLAY", value) - - -## Set the given window as a notification. This should be set to "1" when some -## UI wants to be shown but not intercept input. -func set_notification(window_id: int, value: int, display: XWAYLAND = XWAYLAND.OGUI) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - return _set_xprop(xwayland, window_id, "STEAM_NOTIFICATION", value) - - -## Set the given window as an external overlay -func set_external_overlay(window_id: int, value: int, display: XWAYLAND = XWAYLAND.OGUI) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - return _set_xprop(xwayland, window_id, "GAMESCOPE_EXTERNAL_OVERLAY", value) - - -## Returns the currently set app ID on the given window -func get_app_id(window_id: int, display: XWAYLAND = XWAYLAND.GAME) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - return _get_xprop(xwayland, window_id, "STEAM_GAME") - - -## Sets the app ID on the given window -func set_app_id(window_id: int, app_id: int, display: XWAYLAND = XWAYLAND.GAME) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - return _set_xprop(xwayland, window_id, "STEAM_GAME", app_id) - - -## Returns whether or not the given window has an app ID set -func has_app_id(window_id: int, display: XWAYLAND = XWAYLAND.GAME) -> bool: - return has_xprop(window_id, "STEAM_GAME", display) - - -## Sets the Gamescope FPS limit -func set_fps_limit(fps: int = 60, display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - logger.debug("Setting FPS to: {0}".format([fps])) - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - return _set_xprop(xwayland, root_id, "GAMESCOPE_FPS_LIMIT", fps) - - -## Returns the Gamescope FPS limit -func get_fps_limit(display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - return _get_xprop(xwayland, root_id, "GAMESCOPE_FPS_LIMIT") - - -## Returns the current Gamescope blur mode -func get_blur_mode(display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - return _get_xprop(xwayland, root_id, "GAMESCOPE_BLUR_MODE") - - -## Sets the Gamescope blur mode -func set_blur_mode(mode: BLUR_MODE = BLUR_MODE.OFF, display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - logger.debug("Setting blur mode to: {0}".format([mode])) - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - var err := _set_xprop(xwayland, root_id, "GAMESCOPE_BLUR_MODE", mode) - blur_mode = mode - return err - - -## Sets the Gamescope blur radius when blur is active -func set_blur_radius(radius: int, display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - logger.debug("Setting blur radius to: {0}".format([radius])) - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - return _set_xprop(xwayland, root_id, "GAMESCOPE_BLUR_RADIUS", radius) - - -## Configures Gamescope to allow tearing or not -func set_allow_tearing(allow: bool, display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - logger.debug("Setting tearing to: {0}".format([allow])) - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var value := 1 if allow else 0 - var root_id := xwayland.get_root_window_id() - return _set_xprop(xwayland, root_id, "GAMESCOPE_ALLOW_TEARING", value) - - -## Returns the currently set manual focus -func get_baselayer_window(display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - return _get_xprop(xwayland, root_id, "GAMESCOPECTRL_BASELAYER_WINDOW") - - -## Focuses the given window -func set_baselayer_window(window_id: int, display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - return _set_xprop(xwayland, root_id, "GAMESCOPECTRL_BASELAYER_WINDOW", window_id) - - -## Removes the baselayer property to un-focus windows -func remove_baselayer_window(display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - return _remove_xprop(xwayland, root_id, "GAMESCOPECTRL_BASELAYER_WINDOW") - - -## Request a screenshot from gamescope -func request_screenshot(display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - return _set_xprop(xwayland, root_id, "GAMESCOPECTRL_REQUEST_SCREENSHOT", 1) - - -## Sets the xwayland mode resolution on the given xwayland display -## number (default: XWAYLAND.GAME). -func set_resolution(resolution: Vector2i, allow_super: bool = false, display: XWAYLAND = XWAYLAND.GAME) -> int: - var xwayland := get_xwayland(XWAYLAND.PRIMARY) - if not xwayland: - return -1 - - var target_display := get_display_number(display) - var root_id := xwayland.get_root_window_id() - var allow_super_value := 1 if allow_super else 0 - var args := PackedInt32Array([target_display, resolution.x, resolution.y, allow_super_value]) - return _set_xprop_array(xwayland, root_id, "GAMESCOPE_XWAYLAND_MODE_CONTROL", args) - - -## Returns the currently set gamescope saturation -# Based on vibrantDeck by Scrumplex -func get_saturation(display: XWAYLAND = XWAYLAND.PRIMARY) -> float: - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - - var matrix := _get_xprop_array(xwayland, root_id, "GAMESCOPE_COLOR_MATRIX") - if matrix.size() < 2: - return -1 - - # [1065353216, 0, 0, 0, 1065353216, 0, 0, 0, 1065353216] - var matrix_arr: Array[int] = Array(matrix) - # Convert the array of longs to floats - # [1.0, 0, 0, 0, 1.0, 0, 0, 0, 1.0] - var coeffs := matrix_arr.map(_long_to_float) - var saturation := snappedf(coeffs[0] - coeffs[1], 0.01) - - return saturation - - -## Set the gamescope saturation -# Based on vibrantDeck by Scrumplex -func set_saturation(saturation: float, display: XWAYLAND = XWAYLAND.PRIMARY) -> int: - saturation = maxf(saturation, 0.0) - saturation = minf(saturation, 4.0) - logger.debug("Setting saturation to: " + str(saturation)) - - # Generate color transformation matrix - var coeffs := _saturation_to_coeffs(saturation) - - # Represent floats as integars (long) - var long_coeffs := coeffs.map(_float_to_long) - - # Get the xwayland to set the property on - var xwayland := get_xwayland(display) - if not xwayland: - return -1 - var root_id := xwayland.get_root_window_id() - - logger.debug("Setting color matrix coeffs: " + str(long_coeffs)) - return _set_xprop_array(xwayland, root_id, "GAMESCOPE_COLOR_MATRIX", long_coeffs) - - -func _saturation_to_coeffs(saturation: float) -> Array[float]: - var coeff := (1.0 - saturation) / 3.0 - - var coeffs: Array[float] = [] - coeffs.resize(9) - coeffs.fill(coeff) - coeffs[0] += saturation - coeffs[4] += saturation - coeffs[8] += saturation - - return coeffs - - -func _float_to_long(x: float) -> int: - var bytes := PackedByteArray() - bytes.resize(4) - bytes.encode_float(0, x) - return bytes.decode_u32(0) - - -func _long_to_float(x: int) -> float: - var bytes := PackedByteArray() - bytes.resize(4) - bytes.encode_u32(0, x) - return bytes.decode_float(0) - - -## Returns the display type for the given display name -func get_display_type(name: String) -> XWAYLAND: - if xwayland_primary.get_name() == name: - return XWAYLAND.PRIMARY - if xwayland_ogui.get_name() == name: - return XWAYLAND.OGUI - return XWAYLAND.GAME - - -## Returns the name of the given xwayland display -func get_display_number(display: XWAYLAND) -> int: - var name := get_display_name(display) - var clean_name := name.replace(":", "") - if clean_name.is_valid_int(): - return clean_name.to_int() - logger.error("Unable to determine display number from name: " + name) - return 0 - - -## Returns the xwayland instance for the given display type -func get_xwayland(display: XWAYLAND) -> Xlib: - if xwaylands.size() == 0: - return null - if display == XWAYLAND.PRIMARY: - return xwayland_primary - if display == XWAYLAND.OGUI: - return xwayland_ogui - return xwayland_game - - -## Returns true if Gamescope is tracking the given display -func _has_xwayland(display: String) -> bool: - for xwayland in xwaylands: - if xwayland.get_name() == display: - return true - return false - - -## Returns true if the given xwayland instance is the primary gamescope instance -func _is_gamescope_xwayland_primary(xwayland: Xlib) -> bool: - var root_id := xwayland.get_root_window_id() - if xwayland.has_xprop(root_id, "GAMESCOPE_FOCUSED_WINDOW"): - return true - return false - - -## Returns true if the given xwayland instance is a gamescope instance -func _is_gamescope_xwayland(xwayland: Xlib) -> bool: - var root_id := xwayland.get_root_window_id() - if xwayland.has_xprop(root_id, "GAMESCOPE_CURSOR_VISIBLE_FEEDBACK"): - return true - return false - - -## Sets the given X property on the given window. -## Example: -## [codeblock] -## Gamescope._set_xprop(":0", 1234, "STEAM_INPUT", 1) -## [/codeblock] -func _set_xprop(xwayland: Xlib, window_id: int, key: String, value: int) -> int: - var msg_args := [window_id, key, value, xwayland.get_name()] - logger.debug("Setting window {0} key {1} to {2} on display {3}".format(msg_args)) - return xwayland.set_xprop(window_id, key, value) - - -## Sets the given X property with the given array of values -func _set_xprop_array(xwayland: Xlib, window_id: int, key: String, values: PackedInt32Array) -> int: - var msg_args := [window_id, key, str(values), xwayland.get_name()] - logger.debug("Setting window {0} key {1} to {2} on display {3}".format(msg_args)) - # TODO: Fix set_xprop_array and use that instead of xprop - var values_str_arr := [] - for value in values: - values_str_arr.append(str(value)) - var values_str := ",".join(values_str_arr) - var cmd := "env" - var args := ["DISPLAY="+xwayland.get_name(), "xprop", "-id", str(window_id), "-f", key, "32c", "-set", key, values_str] - return OS.execute(cmd, args) - - -## Returns the value of the given X property for the given window. Returns -## [member Xlib.ERR_XPROP_NOT_FOUND] if property doesn't exist. -func _get_xprop(xwayland: Xlib, window_id: int, key: String) -> int: - return xwayland.get_xprop(window_id, key) - - -## Removes the given X property for the given window. -func _remove_xprop(xwayland: Xlib, window_id: int, key: String) -> int: - return xwayland.remove_xprop(window_id, key) - - -## Returns an array of values for the given X property for the given window. -## Returns an empty array if property was not found. -func _get_xprop_array(xwayland: Xlib, window_id: int, key: String) -> PackedInt32Array: - return xwayland.get_xprop_array(window_id, key) diff --git a/core/global/gamescope.tres b/core/global/gamescope.tres deleted file mode 100644 index f394fbb1..00000000 --- a/core/global/gamescope.tres +++ /dev/null @@ -1,7 +0,0 @@ -[gd_resource type="Resource" script_class="Gamescope" load_steps=2 format=3 uid="uid://blr55dcwc05m1"] - -[ext_resource type="Script" path="res://core/global/gamescope.gd" id="1_ipl5o"] - -[resource] -script = ExtResource("1_ipl5o") -log_level = 4 diff --git a/core/global/gamescope_test.gd b/core/global/gamescope_test.gd deleted file mode 100644 index e64b41dd..00000000 --- a/core/global/gamescope_test.gd +++ /dev/null @@ -1,15 +0,0 @@ -extends GutTest - -var gamescope := Gamescope.new() - - -func test_float_to_long() -> void: - var to_int := gamescope._float_to_long(1.3) - gut.p(to_int) - assert_eq(to_int, 1067869798, "should be converted to a long") - - -func test_long_to_float() -> void: - var to_float := gamescope._long_to_float(1067869798) - gut.p(to_float) - assert_almost_eq(to_float, 1.3, 0.01, "should be approximately 1.3") diff --git a/core/global/launch_manager.gd b/core/global/launch_manager.gd index 7d65adff..75f64370 100644 --- a/core/global/launch_manager.gd +++ b/core/global/launch_manager.gd @@ -12,21 +12,21 @@ class_name LaunchManager ## periodically check on launched games to see if they have exited, or are ## opening new windows that might need attention. Example: ## [codeblock] -## const LaunchManager := preload("res://core/global/launch_manager.tres") +## var launch_manager := load("res://core/global/launch_manager.tres") as LaunchManager ## ... ## # Create a LibraryLaunchItem to run something ## var item := LibraryLaunchItem.new() ## item.command = "vkcube" ## ## # Launch the app with LaunchManager -## var running_app := LaunchManager.launch(item) +## var running_app := launch_manager.launch(item) ## ## # Get a list of running apps -## var running := LaunchManager.get_running() +## var running := launch_manager.get_running() ## print(running) ## ## # Stop an app with LaunchManager -## LaunchManager.stop(running_app) +## launch_manager.stop(running_app) ## [/codeblock] signal app_launched(app: RunningApp) @@ -35,59 +35,69 @@ signal app_switched(from: RunningApp, to: RunningApp) signal recent_apps_changed() const settings_manager := preload("res://core/global/settings_manager.tres") -const NotificationManager := preload("res://core/global/notification_manager.tres") +const notification_manager := preload("res://core/global/notification_manager.tres") -var gamescope := preload("res://core/global/gamescope.tres") as Gamescope -var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber +var gamescope := preload("res://core/systems/gamescope/gamescope.tres") as GamescopeInstance +var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance var state_machine := preload("res://assets/state/state_machines/global_state_machine.tres") as StateMachine var in_game_state := preload("res://assets/state/states/in_game.tres") as State var in_game_menu_state := preload("res://assets/state/states/in_game_menu.tres") as State var PID: int = OS.get_process_id() +var _xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) +var _xwayland_ogui := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_OGUI) +var _xwayland_game := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_GAME) var _sandbox := Sandbox.get_sandbox() var _current_app: RunningApp var _pid_to_windows := {} var _running: Array[RunningApp] = [] -var _stopping: Array[RunningApp] = [] var _apps_by_pid: Dictionary = {} var _apps_by_name: Dictionary = {} var _data_dir: String = ProjectSettings.get_setting("OpenGamepadUI/data/directory") var _persist_path: String = "/".join([_data_dir, "launcher.json"]) var _persist_data: Dictionary = {"version": 1} -var _ogui_window_id := gamescope.get_window_id(PID, gamescope.XWAYLAND.OGUI) +var _ogui_window_id := 0 var should_manage_overlay := true var logger := Log.get_logger("LaunchManager", Log.LEVEL.INFO) # Connect to Gamescope signals func _init() -> void: - # When window focus changes, update the current app and gamepad profile - var on_focus_changed := func(from: int, to: int): - logger.info("Window focus changed from " + str(from) + " to: " + str(to)) - var last_app := _current_app - _current_app = _detect_running_app(to) - - logger.debug("Last app: " + str(last_app) + " current_app: " + str(_current_app)) - app_switched.emit(last_app, _current_app) - - # If the app has a gamepad profile, set it - if to != _ogui_window_id and _current_app: - set_app_gamepad_profile(_current_app) - - # If we don't want LaunchManager to manage overlay (I.E. overlay mode), return false always. - if not should_manage_overlay: - return - - gamescope.focused_window_updated.connect(on_focus_changed) - - # Debug print when the focused app changed - var on_focused_app_changed := func(from: int, to: int) -> void: - logger.debug("Focused app changed from " + str(from) + " to " + str(to)) - gamescope.focused_app_updated.connect(on_focused_app_changed) + # Get the window ID of OpenGamepadUI + if _xwayland_ogui: + var ogui_windows := _xwayland_ogui.get_windows_for_pid(PID) + if not ogui_windows.is_empty(): + _ogui_window_id = ogui_windows[0] + + # Listen for signals from the primary Gamescope XWayland + if _xwayland_primary: + # When window focus changes, update the current app and gamepad profile + var on_focus_changed := func(from: int, to: int): + logger.info("Window focus changed from " + str(from) + " to: " + str(to)) + var last_app := _current_app + _current_app = _detect_running_app(to) + + logger.debug("Last app: " + str(last_app) + " current_app: " + str(_current_app)) + app_switched.emit(last_app, _current_app) + + # If the app has a gamepad profile, set it + if to != _ogui_window_id and _current_app: + set_app_gamepad_profile(_current_app) + + # If we don't want LaunchManager to manage overlay (I.E. overlay mode), return false always. + if not should_manage_overlay: + return + + _xwayland_primary.focused_window_updated.connect(on_focus_changed) + + # Debug print when the focused app changed + var on_focused_app_changed := func(from: int, to: int) -> void: + logger.debug("Focused app changed from " + str(from) + " to " + str(to)) + _xwayland_primary.focused_app_updated.connect(on_focused_app_changed) # Whenever the in-game state is entered, set the gamepad profile - var on_game_state_entered := func(from: State): + var on_game_state_entered := func(_from: State): if _current_app: set_app_gamepad_profile(_current_app) @@ -95,8 +105,9 @@ func _init() -> void: if not should_manage_overlay: return - logger.debug("Enabling STEAM_OVERLAY atom") - gamescope.set_overlay(_ogui_window_id, 0) + if _xwayland_ogui: + logger.debug("Enabling STEAM_OVERLAY atom") + _xwayland_ogui.set_overlay(_ogui_window_id, 0) var on_game_state_exited := func(_to: State): # Set the gamepad profile to the global profile @@ -106,8 +117,9 @@ func _init() -> void: if not should_manage_overlay: return - logger.debug("Disabling STEAM_OVERLAY atom") - gamescope.set_overlay(_ogui_window_id, 1) + if _xwayland_ogui: + logger.debug("Disabling STEAM_OVERLAY atom") + _xwayland_ogui.set_overlay(_ogui_window_id, 1) in_game_state.state_entered.connect(on_game_state_entered) in_game_state.state_exited.connect(on_game_state_exited) @@ -170,7 +182,10 @@ func launch(app: LibraryLaunchItem) -> RunningApp: # Set the display environment if one was not set. if not "DISPLAY" in env: - env["DISPLAY"] = gamescope.get_display_name(Gamescope.XWAYLAND.GAME) + if _xwayland_game: + env["DISPLAY"] = _xwayland_game.name + else: + env["DISPLAY"] = "" var display := env["DISPLAY"] as String # Set the OGUI ID environment variable @@ -196,7 +211,7 @@ func launch(app: LibraryLaunchItem) -> RunningApp: logger.info("Launching game with command: {0} {1}".format([exec, str(command)])) # Launch the application process - var pid = Reaper.create_process(exec, command) + var pid := Reaper.create_process(exec, command) logger.info("Launched with PID: {0}".format([pid])) # Create a running app instance @@ -289,7 +304,7 @@ func set_app_gamepad_profile(app: RunningApp) -> void: # Check to see if this game has any gamepad profiles. If so, set our # gamepads to use them. var section := ".".join(["game", app.launch_item.name.to_lower()]) - var profile_path = settings_manager.get_value(section, "gamepad_profile", "") + var profile_path := settings_manager.get_value(section, "gamepad_profile", "") as String var profile_gamepad := settings_manager.get_value(section, "gamepad_profile_target", "") as String logger.debug("Using profile '" + profile_path + "' with gamepad type '" + profile_gamepad + "'") set_gamepad_profile(profile_path, profile_gamepad) @@ -310,7 +325,7 @@ func set_gamepad_profile(path: String, target_gamepad: String = "") -> void: profile_path = InputPlumber.DEFAULT_GLOBAL_PROFILE logger.info("Loading global gamepad profile: " + profile_path) - for gamepad in input_plumber.composite_devices: + for gamepad in input_plumber.get_composite_devices(): gamepad.target_modify_profile(profile_path, profile_modifier) # Set the target gamepad if one was specified @@ -338,7 +353,7 @@ func set_gamepad_profile(path: String, target_gamepad: String = "") -> void: return # TODO: Save profiles for individual controllers? - for gamepad in input_plumber.composite_devices: + for gamepad in input_plumber.get_composite_devices(): gamepad.target_modify_profile(path, profile_modifier) # Set the target gamepad if one was specified @@ -353,11 +368,11 @@ func set_gamepad_profile(path: String, target_gamepad: String = "") -> void: gamepad.set_target_devices(target_devices) var notify := Notification.new("Using gamepad profile: " + profile.name) - NotificationManager.show(notify) + notification_manager.show(notify) ## Sets the given running app as the current app -func set_current_app(app: RunningApp, switch_baselayer: bool = true) -> void: +func set_current_app(app: RunningApp, _switch_baselayer: bool = true) -> void: if app == null: return app.grab_focus() @@ -410,13 +425,12 @@ func _remove_running(app: RunningApp): # Checks for running apps and updates our state accordingly func check_running() -> void: # Find the root window - var root_id := gamescope.get_root_window_id(Gamescope.XWAYLAND.GAME) + if not _xwayland_game: + return + var root_id := _xwayland_game.root_window_id if root_id < 0: return - # Update the Gamescope state - gamescope.update() - # Update our view of running processes and what windows they have _update_pids(root_id) @@ -428,24 +442,28 @@ func check_running() -> void: # Updates our mapping of PIDs to Windows. This gives us a good view of what # processes are running, and what windows they have. func _update_pids(root_id: int): + if not _xwayland_game: + return var pids := {} - var all_windows := gamescope.get_all_windows(root_id, Gamescope.XWAYLAND.GAME) + var all_windows := _xwayland_game.get_all_windows(root_id) for window in all_windows: - var pid := gamescope.get_window_pid(window, Gamescope.XWAYLAND.GAME) - if not pid in pids: - pids[pid] = [] - pids[pid].append(window) + var window_pids := _xwayland_game.get_pids_for_window(window) + for window_pid in window_pids: + if not window_pid in pids: + pids[window_pid] = [] + pids[window_pid].append(window) _pid_to_windows = pids # Below functions detect launched game from other processes # Returns the process ID func _get_pid_from_focused_window(window_id: int) -> int: - var pid := -1 - logger.debug(str(gamescope.get_xwayland(Gamescope.XWAYLAND.GAME).list_xprops(window_id))) - pid = gamescope.get_xwayland(Gamescope.XWAYLAND.GAME).get_xprop(window_id, "_NET_WM_PID") - logger.debug("PID: " + str(pid)) - return pid + if not _xwayland_game: + return -1 + var window_pids := _xwayland_game.get_pids_for_window(window_id) + if window_pids.is_empty(): + return -1 + return window_pids[0] # First method, try to get the name from the window_id using gamescope. @@ -459,16 +477,18 @@ func _get_app_name_from_proc(pid: int) -> String: var process_name: String var path := "/proc/" + str(pid) + "/cwd" var output := [] - var exit_code := OS.execute("ls", ["-l", path], output) + var _exit_code := OS.execute("ls", ["-l", path], output) var process_path: PackedStringArray = output[0].strip_edges().split("/") process_name = process_path[process_path.size()-1] return process_name -# Primary nw app ID method. Identifies the running app from steam's library.vdf +# Primary new app ID method. Identifies the running app from steam's library.vdf # and appmanifest_.acf files. func _get_name_from_steam_library() -> String: - var missing_app_id := gamescope.get_focused_app() + var missing_app_id := -1 + if _xwayland_primary: + missing_app_id = _xwayland_primary.focused_app logger.debug("Found unclaimed app id: " +str(missing_app_id)) var steam_library_path := OS.get_environment("HOME") +"/.steam/steam" @@ -540,7 +560,7 @@ func _make_running_app_from_process(name: String, pid: int, window_id: int) -> R logger.debug("Creating running app from process") # Create a dummy LibraryLaunchItem to make our RunningApp. - var lauch_dict = { + var lauch_dict := { "_id": "", "_provider_id": "", "provider_app_id": "", @@ -553,7 +573,9 @@ func _make_running_app_from_process(name: String, pid: int, window_id: int) -> R } var launch_item: LibraryLaunchItem = LibraryLaunchItem.from_dict(lauch_dict) - var display:= gamescope.get_display_name(Gamescope.XWAYLAND.GAME) + var display := "" + if _xwayland_game: + display = _xwayland_game.name var running_app: RunningApp = _make_running_app(launch_item, pid, display) running_app.window_id = window_id running_app.state = RunningApp.STATE.RUNNING diff --git a/core/systems/gamescope/gamescope.gd b/core/systems/gamescope/gamescope.gd new file mode 100644 index 00000000..185d62df --- /dev/null +++ b/core/systems/gamescope/gamescope.gd @@ -0,0 +1,26 @@ +@icon("res://assets/editor-icons/streamline--desktop-game-solid.svg") +extends Node +class_name Gamescope + +## Manages gamescope. +## +## The [Gamescope] class is responsible for loading a [GamescopeInstance] and +## calling its 'process()' method each frame. + +@export var instance: GamescopeInstance = load("res://core/systems/gamescope/gamescope.tres") as GamescopeInstance + +# Keep a reference to xwayland instances so they are not cleaned up automatically +var _xwaylands: Array[GamescopeXWayland] +var logger := Log.get_logger("Gamescope") + + +func _ready() -> void: + _xwaylands = instance.get_xwaylands() + if _xwaylands.is_empty(): + logger.warn("Gamescope not detected. Unexpected behavior expected.") + + +func _process(_delta: float) -> void: + if not instance: + return + instance.process() diff --git a/core/systems/gamescope/gamescope.tres b/core/systems/gamescope/gamescope.tres new file mode 100644 index 00000000..1e22d2c3 --- /dev/null +++ b/core/systems/gamescope/gamescope.tres @@ -0,0 +1,3 @@ +[gd_resource type="GamescopeInstance" format=3 uid="uid://chd0nc6gbfnw0"] + +[resource] diff --git a/core/systems/gamescope/gamescope_test.gd b/core/systems/gamescope/gamescope_test.gd new file mode 100644 index 00000000..96fd0b73 --- /dev/null +++ b/core/systems/gamescope/gamescope_test.gd @@ -0,0 +1,21 @@ +extends Node2D + +var gamescope := load("res://core/systems/gamescope/gamescope.tres") as GamescopeInstance + +var PID := OS.get_process_id() + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + print(gamescope.get_xwaylands()) + var xwayland := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_OGUI) + if not xwayland: + print("XWayland not found") + return + + var ogui_window_ids := xwayland.get_windows_for_pid(PID) + if ogui_window_ids.is_empty(): + print("Unable to find window id for OGUI") + return + var ogui_window_id := ogui_window_ids[0] + xwayland.set_main_app(ogui_window_id) + print("Found window ids for OGUI: ", ogui_window_ids) diff --git a/core/systems/gamescope/gamescope_test.tscn b/core/systems/gamescope/gamescope_test.tscn new file mode 100644 index 00000000..73d96e76 --- /dev/null +++ b/core/systems/gamescope/gamescope_test.tscn @@ -0,0 +1,17 @@ +[gd_scene load_steps=5 format=3 uid="uid://83606lflpyo5"] + +[ext_resource type="Script" path="res://core/systems/gamescope/gamescope_test.gd" id="1_e433b"] +[ext_resource type="Script" path="res://core/systems/gamescope/gamescope.gd" id="1_ukgov"] +[ext_resource type="GamescopeInstance" uid="uid://chd0nc6gbfnw0" path="res://core/systems/gamescope/gamescope.tres" id="2_fbm81"] +[ext_resource type="Texture2D" uid="uid://djy4rejy21s6g" path="res://icon.svg" id="4_j1wlf"] + +[node name="GamescopeTest" type="Node2D"] +script = ExtResource("1_e433b") + +[node name="Gamescope" type="Node" parent="."] +script = ExtResource("1_ukgov") +instance = ExtResource("2_fbm81") + +[node name="Icon" type="Sprite2D" parent="."] +position = Vector2(609, 401) +texture = ExtResource("4_j1wlf") diff --git a/core/systems/input/input_icon_manager.gd b/core/systems/input/input_icon_manager.gd index f60d1b40..6caa3787 100644 --- a/core/systems/input/input_icon_manager.gd +++ b/core/systems/input/input_icon_manager.gd @@ -20,7 +20,7 @@ enum InputType { } var in_game_state := load("res://assets/state/states/in_game.tres") as State -var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber +var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance var logger := Log.get_logger("InputIconManager", Log.LEVEL.INFO) ## Disable/Enable signaling on input type changes @@ -154,7 +154,7 @@ func _init(): in_game_state.state_exited.connect(on_in_game_exited) # Listen for InputPlumber device change events - var on_comp_device_added := func(_device: InputPlumber.CompositeDevice): + var on_comp_device_added := func(_device: CompositeDevice): _on_joy_connection_changed(true) input_plumber.composite_device_added.connect(on_comp_device_added) var on_comp_device_removed := func(_path: String): diff --git a/core/systems/input/input_icon_processor.gd b/core/systems/input/input_icon_processor.gd index 767f57f9..c4017668 100644 --- a/core/systems/input/input_icon_processor.gd +++ b/core/systems/input/input_icon_processor.gd @@ -6,7 +6,7 @@ class_name InputIconProcessor const DEADZONE := 0.4 -var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber +var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance var icon_manager := load("res://core/systems/input/input_icon_manager.tres") as InputIconManager @@ -23,8 +23,9 @@ func _input(event: InputEvent) -> void: # If this is an InputPlumber event, use the name from the device if event.has_meta("dbus_path") and not event.get_meta("dbus_path", "").is_empty(): var dbus_path := event.get_meta("dbus_path") as String - var device := input_plumber.get_device(dbus_path) - device_name = device.name + var device := input_plumber.get_composite_device(dbus_path) + if device: + device_name = device.name # Otherwise, use the detected device name else: device_name = Input.get_joy_name(event.device) diff --git a/core/systems/input/input_manager.gd b/core/systems/input/input_manager.gd index 7925a935..d18f1b1f 100644 --- a/core/systems/input/input_manager.gd +++ b/core/systems/input/input_manager.gd @@ -18,7 +18,7 @@ var osk := load("res://core/global/keyboard_instance.tres") as KeyboardInstance ## The audio manager to use to adjust the audio when audio input events happen. var audio_manager := load("res://core/global/audio_manager.tres") as AudioManager ## InputPlumber receives and sends DBus input events. -var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber +var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance ## LaunchManager provides context on the currently running app so we can switch profiles var launch_manager := load("res://core/global/launch_manager.tres") as LaunchManager ## The Global State Machine @@ -270,7 +270,7 @@ func _audio_input(event: InputEvent) -> void: return -func _watch_dbus_device(device: InputPlumber.CompositeDevice) -> void: +func _watch_dbus_device(device: CompositeDevice) -> void: for target in device.dbus_targets: logger.debug("Adding watch for " + device.name + " " + target.name) logger.debug(str(target.get_instance_id())) diff --git a/core/systems/input/input_manager.tscn b/core/systems/input/input_manager.tscn index a32585d5..000a852e 100644 --- a/core/systems/input/input_manager.tscn +++ b/core/systems/input/input_manager.tscn @@ -1,6 +1,12 @@ -[gd_scene load_steps=2 format=3 uid="uid://n83wlhmmsu3j"] +[gd_scene load_steps=4 format=3 uid="uid://n83wlhmmsu3j"] [ext_resource type="Script" path="res://core/systems/input/input_manager.gd" id="1_3s67x"] +[ext_resource type="Script" path="res://core/systems/input/input_plumber.gd" id="2_v1kl3"] +[ext_resource type="InputPlumberInstance" uid="uid://e2bevy4j4rx2" path="res://core/systems/input/input_plumber.tres" id="3_eclcy"] [node name="InputManager" type="Node"] script = ExtResource("1_3s67x") + +[node name="InputPlumber" type="Node" parent="."] +script = ExtResource("2_v1kl3") +instance = ExtResource("3_eclcy") diff --git a/core/systems/input/input_plumber.gd b/core/systems/input/input_plumber.gd index f21c8e0a..9840ca47 100644 --- a/core/systems/input/input_plumber.gd +++ b/core/systems/input/input_plumber.gd @@ -1,5 +1,5 @@ -@icon("res://assets/icons/game-controller.svg") -extends Resource +@icon("res://addons/core/assets/icons/inputplumber.svg") +extends Node class_name InputPlumber ## Manages routing input to and from InputPlumber. @@ -7,690 +7,41 @@ class_name InputPlumber ## The InputPlumberManager class is responsible for handling dbus messages to and ## from the InputPlumber input manager daemon. -const INPUT_PLUMBER_BUS := "org.shadowblip.InputPlumber" -const INPUT_PLUMBER_PATH := "/org/shadowblip/InputPlumber" -const INPUT_PLUMBER_PREFIX := INPUT_PLUMBER_PATH + "/devices" -const INPUT_PLUMBER_MANAGER_PATH := "/org/shadowblip/InputPlumber/Manager" -const IFACE_MANAGER := "org.shadowblip.InputManager" -const IFACE_COMPOSITE_DEVICE := "org.shadowblip.Input.CompositeDevice" -const IFACE_EVENT_DEVICE := "org.shadowblip.Input.Source.EventDevice" -const IFACE_HIDRAW_DEVICE := "org.shadowblip.Input.Source.HIDRawDevice" -const IFACE_IIO_DEVICE := "org.shadowblip.Input.Source.IIODevice" -const IFACE_DBUS_DEVICE := "org.shadowblip.Input.DBusDevice" -const IFACE_GAMEPAD_DEVICE := "org.shadowblip.Input.Gamepad" -const IFACE_KEYBOARD_DEVICE := "org.shadowblip.Input.Keyboard" -const IFACE_MOUSE_DEVICE := "org.shadowblip.Input.Mouse" - const DEFAULT_PROFILE := "res://assets/gamepad/profiles/default.json" const DEFAULT_GLOBAL_PROFILE := "user://data/gamepad/profiles/global_default.json" const PROFILES_DIR := "user://data/gamepad/profiles" -enum INTERCEPT_MODE { - NONE, - PASS, - ALL, -} - -var logger := Log.get_logger("InputPlumber", Log.LEVEL.INFO) - -var dbus := load("res://core/global/dbus_system.tres") as DBusManager -var manager := Manager.new(dbus.create_proxy(INPUT_PLUMBER_BUS, INPUT_PLUMBER_MANAGER_PATH)) -var object_manager := dbus.ObjectManager.new(dbus.create_proxy(INPUT_PLUMBER_BUS, INPUT_PLUMBER_PATH)) -var system_thread := load("res://core/systems/threading/system_thread.tres") as SharedThread - -var is_running := false -var composite_devices: Array[CompositeDevice] = [] -var composite_devices_map: Dictionary = {} -var intercept_mode_current: INTERCEPT_MODE = INTERCEPT_MODE.NONE -var intercept_triggers_current: PackedStringArray = ["Gamepad:Button:Guide"] -var intercept_target_current: String = "Gamepad:Button:Guide" - -## Emitted when InputPlumber is detected as running -signal started -## Emitted when InputPlumber is detected as stopped -signal stopped -## Emitted when a CompositeDevice is dicovered and identified as a new device -signal composite_device_added(device: CompositeDevice) -## Emitted when a CompositeDevice is dicovered over dbus but already exists in -## the local map -signal composite_device_changed(device: CompositeDevice) -## Emitted when a CompositeDevice is removed -signal composite_device_removed(dbus_path: String) - - -func _init() -> void: - logger.debug("Initalizing InputPlumber. Found composite devices: " + str(composite_devices)) - object_manager.interfaces_added.connect(_on_interfaces_added) - object_manager.interfaces_removed.connect(_on_interfaces_removed) - composite_devices = get_devices() - for device in composite_devices: - composite_devices_map[device.dbus_path] = device - - # Ensure the global default config is created - var profiles_dir := ProjectSettings.globalize_path(PROFILES_DIR) - if DirAccess.make_dir_recursive_absolute(profiles_dir) != OK: - logger.error("Failed to create user profiles directory: " + profiles_dir) - return - var default_profile := FileAccess.open(DEFAULT_PROFILE, FileAccess.READ) - var default_profile_data := default_profile.get_as_text() - logger.debug("Writing default global profile to: " + DEFAULT_GLOBAL_PROFILE) - var global_default_profile := FileAccess.open(DEFAULT_GLOBAL_PROFILE, FileAccess.WRITE) - global_default_profile.store_string(default_profile_data) - - # Start a task that monitors whether or not InputPlumber is started or stopped, - # and emits signals when its running state changes. - started.connect(_on_inputplumber_started) - stopped.connect(_on_inputplumber_stopped) - var running_check := func(): - var update_running := func(): - var running := dbus.bus_exists(INPUT_PLUMBER_BUS) - if running == self.is_running: - return - self.is_running = running - if running: - started.emit() - else: - stopped.emit() - update_running.call_deferred() - system_thread.exec(running_check) - system_thread.scheduled_exec(running_check, 5000, SharedThread.ScheduledTaskType.RECURRING) - - -func _on_inputplumber_started() -> void: - logger.info("InputPlumber started") - - # Remove any Godot gamepad input maps and rely completely on InputPlumber for - # gamepad input - var actions := InputMap.get_actions() - var to_erase: Array[InputEvent] = [] - for action in actions: - var events := InputMap.action_get_events(action) - for event in events: - if event is InputEventJoypadButton or event is InputEventJoypadMotion: - logger.debug("Erasing mapping for", action, ":", event) - InputMap.action_erase_event(action, event) - - -func _on_inputplumber_stopped() -> void: - logger.warn("InputPlumber stopped") - - # Reload Godot input mappings if InputPlumber shuts down - logger.debug("Restoring input mappings from project settings") - InputMap.load_from_project_settings() - - -func _on_interfaces_added(dbus_path: String) -> void: - logger.debug("Interfaces Added: " + str(dbus_path)) - if not "CompositeDevice" in dbus_path: - return - composite_devices = get_devices(dbus_path) - composite_devices_map.clear() - for device in composite_devices: - composite_devices_map[device.dbus_path] = device - - -func _on_interfaces_removed(dbus_path: String) -> void: - logger.debug("Interfaces Removed: " + str(dbus_path)) - if not "CompositeDevice" in dbus_path: - return - composite_devices = get_devices() - composite_devices_map.clear() - for device in composite_devices: - composite_devices_map[device.dbus_path] = device - if dbus_path.contains("CompositeDevice"): - composite_device_removed.emit(dbus_path) - - -## Returns true if InputPlumber can be used on this system -func supports_input_plumber() -> bool: - return dbus.bus_exists(INPUT_PLUMBER_BUS) - - -func get_objects_of(pattern: String) -> Array: - var devices: Array = [] - var device_paths := dbus.get_managed_objects(INPUT_PLUMBER_BUS, INPUT_PLUMBER_PATH) - logger.debug("Searching for " + pattern + " objects.") - # Loop through all objects on the bus - for obj in device_paths: - - var object := obj as DBusManager.ManagedObject - var path := object.path - var proxy := dbus.create_proxy(INPUT_PLUMBER_BUS, path) - #logger.debug("Found object: " + str(object) + " with path " + path) - if path.contains(pattern) and pattern == "CompositeDevice": - logger.debug("Found " + pattern + " in " + path) - var device := CompositeDevice.new(proxy) - devices.append(device) - continue - - if path.contains(pattern) and pattern == "event": - var device := EventDevice.new(proxy) - devices.append(device) - continue - - if path.contains(pattern) and pattern == "hidraw": - var device := HIDRawDevice.new(proxy) - devices.append(device) - continue - - if path.contains(pattern) and pattern == "iio": - var device := IIODevice.new(proxy) - devices.append(device) - continue - - if path.contains(pattern) and pattern == "dbus": - var device := DBusDevice.new(proxy) - devices.append(device) - continue - - if path.contains(pattern) and pattern == "gamepad": - var device := GamepadDevice.new(proxy) - devices.append(device) - continue - - if path.contains(pattern) and pattern == "keyboard": - var device := KeyboardDevice.new(proxy) - devices.append(device) - continue - - if path.contains(pattern) and pattern == "mouse": - var device := MouseDevice.new(proxy) - devices.append(device) - continue - logger.debug("Returning devices: " + str(devices)) - return devices - - -## Retrieves all CompositeDevices currently on the InputPlumber DBus interface. Will -## emit composite_device_added if the given dbus path is a new device, or -## composite_device_changed if it already existed -func get_devices(dbus_path: String = "") -> Array[CompositeDevice]: - logger.debug("Getting all composite devices.") - var new_device_list: Array[CompositeDevice] - new_device_list.assign(get_objects_of("CompositeDevice")) - var existing_devices: Array[CompositeDevice] - - # Only return new devices. Overriding devices breaks signaling. - for device in new_device_list: - var found: bool = false - for old_dev in composite_devices: - if old_dev.dbus_path == device.dbus_path: - existing_devices.append(old_dev) - found = true - if dbus_path == device.dbus_path: - composite_device_changed.emit(device) - break - - # New device found - if not found: - existing_devices.append(device) - set_intercept_mode_single(intercept_mode_current, device) - set_intercept_activation_single(intercept_triggers_current, \ - intercept_target_current, device) - composite_device_added.emit(device) - - return existing_devices - - -## Returns the [CompositeDevice] with the given DBus path -func get_device(dbus_path: String) -> CompositeDevice: - if dbus_path in composite_devices_map: - return composite_devices_map[dbus_path] - return null - - -## Sets all composite devices to the specified intercept mode. -func set_intercept_mode(mode: INTERCEPT_MODE) -> void: - logger.debug("Setting all composite devices to mode: " + str(mode)) - intercept_mode_current = mode - for d in composite_devices: - var device := d as CompositeDevice - set_intercept_mode_single(mode, device) - - -func set_intercept_mode_single(mode: INTERCEPT_MODE, device: CompositeDevice) -> void: - logger.debug("Setting composite device "+ device.dbus_path + " to mode: " + str(mode)) - match mode: - INTERCEPT_MODE.NONE: - device.intercept_mode = 0 - INTERCEPT_MODE.PASS: - device.intercept_mode = 1 - INTERCEPT_MODE.ALL: - device.intercept_mode = 2 - - -## Sets all composite devices to use the specified intercept actions. -func set_intercept_activation(triggers: PackedStringArray, target: String) -> void: - logger.debug("Setting all composite devices to intercept triggers: " + str(triggers) + " and target event: " + target) - intercept_triggers_current = triggers - intercept_target_current = target - for d in composite_devices: - var device := d as CompositeDevice - set_intercept_activation_single(triggers, target, device) - - -func set_intercept_activation_single(triggers: PackedStringArray, target: String, device: CompositeDevice) -> void: - logger.debug("Setting composite device "+ device.dbus_path + " intercept triggers: " + str(triggers) + " and target event: " + target) - device.set_intercept_activation(triggers, target) - - -class Manager extends Resource: - signal updated - var _proxy: DBusManager.Proxy - - func _init(proxy: DBusManager.Proxy) -> void: - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - func create_composite_device(config_path: String) -> String: - var result := _proxy.call_method(IFACE_MANAGER, "CreateCompositeDevice", [config_path]) - if not result: - return "" - var args := result.get_args() - if args.size() != 1: - return "" - if not args[0] is String: - return "" - return args[0] - - -class CompositeDevice extends Resource: - signal updated - - var _proxy: DBusManager.Proxy - var dbus_targets: Array[DBusDevice] - var dbus_path: String - var logger := Log.get_logger("InputPlumber CompositeDevice", Log.LEVEL.INFO) - - func _init(proxy: DBusManager.Proxy) -> void: - var dbus := load("res://core/global/dbus_system.tres") as DBusManager - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - for path in self.dbus_devices: - var device := DBusDevice.new(dbus.create_proxy(INPUT_PLUMBER_BUS, path)) - dbus_targets.append(device) - dbus_path = _proxy.path - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - var name: String: - get: - var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "Name") - if not property is String: - return "" - return property - - var profile_name: String: - get: - var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "ProfileName") - if not property is String: - return "" - return property - - var intercept_mode: int: - set(v): - #print("Setting mode " + str(v) + " on " + self.dbus_path) - _proxy.set_property(IFACE_COMPOSITE_DEVICE, "InterceptMode", DBus.uint32(v)) - get: - var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "InterceptMode") - if not property is int: - return -1 - return property - - var capabilities: PackedStringArray: - get: - var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "Capabilities") - if not property is Array: - return [] - return property - - var target_capabilities: PackedStringArray: - get: - var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "TargetCapabilities") - if not property is Array: - return [] - return property - - var dbus_devices: PackedStringArray: - get: - var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "DbusDevices") - if not property is Array: - return [] - return property - - var source_device_paths: PackedStringArray: - get: - var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "SourceDevicePaths") - if not property is Array: - return [] - return property +@export var instance: InputPlumberInstance = load("res://core/systems/input/input_plumber.tres") - var target_devices: PackedStringArray: - get: - var property = _proxy.get_property(IFACE_COMPOSITE_DEVICE, "TargetDevices") - if not property is Array: - return [] - return property +# Keep a reference to dbus devices so they are not cleaned up automatically +var _dbus_devices := {} - func set_target_devices(devices: PackedStringArray) -> void: - _proxy.call_method( IFACE_COMPOSITE_DEVICE, "SetTargetDevices", [devices], "as") - ## Load the given profile on the composite device, optionally specifying a profile - ## modifier, which is a target device string (e.g. "deck", "ds5-edge", etc.) to - ## adapt the profile for. This will update the profile with target-specific - ## defaults, like mapping left/right pads to the DualSense center pad if no - ## other mappings are defined. - func target_modify_profile(path: String, profile_modifier: String = "") -> void: - logger.debug("Loading Profile:", path) - if path == "" or not path.ends_with(".json") or not FileAccess.file_exists(path): - logger.error("Profile path:", path," is not a valid profile path.") +func _ready() -> void: + # Add listeners for any new devices + var on_device_added := func(device: CompositeDevice): + var dbus_devices := device.dbus_devices + if dbus_devices.is_empty(): return - if profile_modifier.is_empty(): - load_profile_path(path) - return - - var profile := InputPlumberProfile.load(path) - - var c_pad_cap = "Touchpad:CenterPad:Motion" - var l_pad_cap = "Touchpad:LeftPad:Motion" - var r_pad_cap = "Touchpad:RightPad:Motion" - var mouse_cap = "Mouse:Motion" - - if !profile_modifier.is_empty(): - var mapped_capabilities := profile.to_json() - logger.debug("Mapped Capabilities (before):", mapped_capabilities) - match profile_modifier: - "deck": - logger.debug("Steam Deck Profile") - if c_pad_cap not in mapped_capabilities: - logger.debug("Map", c_pad_cap) - var c_pad_map := InputPlumberMapping.from_source_capability(c_pad_cap) - var r_pad_event := InputPlumberEvent.from_capability(r_pad_cap) - c_pad_map.target_events = [r_pad_event] - profile.mapping.append(c_pad_map) - - "ds5", "ds5-edge": - logger.debug("Dualsense Profile") - if l_pad_cap not in mapped_capabilities: - logger.debug("Map", l_pad_cap) - var l_pad_map := InputPlumberMapping.from_source_capability(l_pad_cap) - var c_pad_event := InputPlumberEvent.from_capability(c_pad_cap) - l_pad_map.target_events = [c_pad_event] - profile.mapping.append(l_pad_map) - if r_pad_cap not in mapped_capabilities: - logger.debug("Map", r_pad_cap) - var r_pad_map := InputPlumberMapping.from_source_capability(r_pad_cap) - var c_pad_event := InputPlumberEvent.from_capability(c_pad_cap) - r_pad_map.target_events = [c_pad_event] - profile.mapping.append(r_pad_map) - - _: - logger.debug("Target device needs no modifications:", profile_modifier) - - mapped_capabilities = profile.to_json() - logger.debug("Mapped Capabilities (after):", mapped_capabilities) - - path = path.rstrip(".json") + profile_modifier + ".json" - if profile.save(path) != OK: - logger.error("Failed to save", profile.name, "to", path) + var dbus_device := dbus_devices[0] + var dbus_path := device.dbus_path + _dbus_devices[dbus_path] = dbus_device + instance.composite_device_added.connect(on_device_added) + + # Add listeners when devices are removed + var on_device_removed := func(dbus_path: String): + if not _dbus_devices.has(dbus_path): return - load_profile_path(path) - - func load_profile_path(path: String) -> void: - # Get the absolute path if this is a resource path - var absolute_path := path - if path.begins_with("res://") or path.begins_with("user://"): - absolute_path = ProjectSettings.globalize_path(path) - _proxy.call_method(IFACE_COMPOSITE_DEVICE, "LoadProfilePath", [absolute_path], "s") - - func send_event(action: String, value: Variant) -> void: - _proxy.call_method( IFACE_COMPOSITE_DEVICE, "SendEvent", [action, value], "sv") - - func send_button_chord(actions: PackedStringArray) -> void: - _proxy.call_method( IFACE_COMPOSITE_DEVICE, "SendButtonChord", [actions], "as") - - func set_intercept_activation(triggers: PackedStringArray, target_event: String) -> void: - _proxy.call_method( IFACE_COMPOSITE_DEVICE, "SetInterceptActivation", [triggers, target_event], "ass") - - -class EventDevice extends Resource: - signal updated - var _proxy: DBusManager.Proxy - - func _init(proxy: DBusManager.Proxy) -> void: - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - var name: String: - get: - var property = _proxy.get_property(IFACE_EVENT_DEVICE, "Name") - if not property is String: - return "" - return property - - var device_path: String: - get: - var property = _proxy.get_property(IFACE_EVENT_DEVICE, "DevicePath") - if not property is String: - return "" - return property - - var phys_path: String: - get: - var property = _proxy.get_property(IFACE_EVENT_DEVICE, "PhysPath") - if not property is String: - return "" - return property - - var sysfs_path: String: - get: - var property = _proxy.get_property(IFACE_EVENT_DEVICE, "SysfsPath") - if not property is String: - return "" - return property - - var unique_id: String: - get: - var property = _proxy.get_property(IFACE_EVENT_DEVICE, "UniqueId") - if not property is String: - return "" - return property - - var handlers: PackedStringArray: - get: - var property = _proxy.get_property(IFACE_EVENT_DEVICE, "Handlers") - if not property is Array: - return [] - return property - - -class HIDRawDevice extends Resource: - signal updated - var _proxy: DBusManager.Proxy - - func _init(proxy: DBusManager.Proxy) -> void: - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - var interface_number: int: - get: - var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "InterfaceNumber") - if not property is int: - return -1 - return property - - var manufacturer: String: - get: - var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "Manufacturer") - if not property is String: - return "" - return property - - var path: String: - get: - var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "Path") - if not property is String: - return "" - return property - - var product: String: - get: - var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "Product") - if not property is String: - return "" - return property - - var product_id: String: - get: - var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "ProductId") - if not property is String: - return "" - return property + _dbus_devices.erase(dbus_path) + instance.composite_device_removed.connect(on_device_removed) - var release_number: String: - get: - var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "ReleaseNumber") - if not property is String: - return "" - return property + # Find all composite devices + var devices := instance.get_composite_devices() + for device in devices: + on_device_added.call(device) - var serial_number: String: - get: - var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "SerialNumber") - if not property is String: - return "" - return property - var vendor_id: String: - get: - var property = _proxy.get_property(IFACE_HIDRAW_DEVICE, "VendorId") - if not property is String: - return "" - return property - - -class IIODevice extends Resource: - signal updated - var _proxy: DBusManager.Proxy - - func _init(proxy: DBusManager.Proxy) -> void: - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - var id: String: - get: - var property = _proxy.get_property(IFACE_IIO_DEVICE, "Id") - if not property is int: - return "" - return property - - var name: String: - get: - var property = _proxy.get_property(IFACE_IIO_DEVICE, "Name") - if not property is String: - return "" - return property - - -class KeyboardDevice extends Resource: - signal updated - var _proxy: DBusManager.Proxy - - func _init(proxy: DBusManager.Proxy) -> void: - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - var name: String: - get: - var property = _proxy.get_property(IFACE_KEYBOARD_DEVICE, "Name") - if not property is String: - return "" - return property - - func send_key(key: String, pressed: bool) -> void: - _proxy.call_method(IFACE_KEYBOARD_DEVICE, "SendKey", [key, pressed]) - - -class MouseDevice extends Resource: - signal updated - var _proxy: DBusManager.Proxy - - func _init(proxy: DBusManager.Proxy) -> void: - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - var name: String: - get: - var property = _proxy.get_property(IFACE_MOUSE_DEVICE, "Name") - if not property is String: - return "" - return property - - -class GamepadDevice extends Resource: - signal updated - var _proxy: DBusManager.Proxy - - func _init(proxy: DBusManager.Proxy) -> void: - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - var name: String: - get: - var property = _proxy.get_property(IFACE_GAMEPAD_DEVICE, "Name") - if not property is String: - return "" - return property - - -class DBusDevice extends Resource: - signal input_event(type_code: String, value: float) - var _proxy: DBusManager.Proxy - - func _init(proxy: DBusManager.Proxy) -> void: - #print("Creating DBusDevice!") - _proxy = proxy - _proxy.message_received.connect(_on_message_received) - _proxy.thread.exec(_proxy.watch.bind(IFACE_DBUS_DEVICE, "InputEvent")) - - func _on_message_received(msg: DBusMessage) -> void: - if not msg: - return - if msg.get_member() != "InputEvent": - return - var args := msg.get_args() - if args.size() < 2: - return - #print("Got InputEvent " + str(args)) - #print(str(self.get_instance_id())) - #print(str(self.get_rid())) - input_event.emit(args[0], args[1]) - - var name: String: - get: - var property = _proxy.get_property(IFACE_DBUS_DEVICE, "Name") - if not property is String: - return "" - return property +func _process(_delta: float) -> void: + if not instance: + return + instance.process() diff --git a/core/systems/input/input_plumber.tres b/core/systems/input/input_plumber.tres index 60a89d18..64f50b4a 100644 --- a/core/systems/input/input_plumber.tres +++ b/core/systems/input/input_plumber.tres @@ -1,6 +1,3 @@ -[gd_resource type="Resource" script_class="InputPlumber" load_steps=2 format=3 uid="uid://ct1hxnffwtqvl"] - -[ext_resource type="Script" path="res://core/systems/input/input_plumber.gd" id="1_0njvc"] +[gd_resource type="InputPlumberInstance" format=3 uid="uid://e2bevy4j4rx2"] [resource] -script = ExtResource("1_0njvc") diff --git a/core/systems/input/input_plumber_test.gd b/core/systems/input/input_plumber_test.gd index 33642059..238dd6b0 100644 --- a/core/systems/input/input_plumber_test.gd +++ b/core/systems/input/input_plumber_test.gd @@ -1,45 +1,51 @@ -extends Node2D - -var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber - - -# Called when the node enters the scene tree for the first time. -func _ready(): - for compo in input_plumber.composite_devices: - print(compo.dbus_targets) - for dt in compo.dbus_targets: - dt.input_event.connect(_on_input_event) - - input_plumber.composite_device_added.connect(_on_dev_add) - input_plumber.composite_device_removed.connect(_on_dev_rm) - input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.ALL) - get_tree().root.mode = Window.MODE_MINIMIZED - - -# Called every frame. 'delta' is the elapsed time since the previous frame. -func _process(delta): - pass - - -func _on_input_event(event: String, value: float) -> void: - print("Got input event " + event + " with value " + str(value)) - if event == "ui_guide": - var pressed = value == 1.0 - for device in input_plumber.composite_devices: - device.intercept_mode = 0 - device.send_event("Gamepad:Button:Guide", pressed) - device.intercept_mode = InputPlumber.INTERCEPT_MODE.ALL as int - - if event == "ui_accept": - for device in input_plumber.composite_devices: - var intercept_mode = device.intercept_mode - device.intercept_mode = 0 - device.send_button_chord(["Gamepad:Button:Guide", "Gamepad:Button:South"]) - device.intercept_mode = InputPlumber.INTERCEPT_MODE.ALL as int - - -func _on_dev_add() -> void: - print("Newbie doobie") - -func _on_dev_rm() -> void: - print("bubye") +extends GutTest + + +# Configure the given composite device to set intercept mode and print input +# events +func _watch_device(device: CompositeDevice) -> void: + gut.p(" Found device " + str(device) + " at " + device.dbus_path) + device.intercept_mode = InputPlumberInstance.INTERCEPT_MODE_ALL + assert_eq(device.intercept_mode, InputPlumberInstance.INTERCEPT_MODE_ALL) + var dbus_devices := device.dbus_devices + if dbus_devices.is_empty(): + gut.p(" No dbus devices found for device") + return + var dbus_device := dbus_devices[0] + gut.p(" Found DBus device: " + str(dbus_device) + " at " + dbus_device.dbus_path) + var on_input_event := func(event: String, value: float): + gut.p("[DBus Event] " + event + ": " + str(value)) + dbus_device.input_event.connect(on_input_event) + + +func test_inputplumber() -> void: + var inputplumber := InputPlumber.new() + inputplumber.instance = load("res://core/systems/input/input_plumber.tres") + add_child_autoqfree(inputplumber) + + if not inputplumber.instance.is_running(): + pass_test("InputPlumber is not running. Skipping tests.") + return + + # Set intercept mode + gut.p("Setting intercept mode to ALL") + inputplumber.instance.intercept_mode = InputPlumberInstance.INTERCEPT_MODE_ALL + assert_eq(inputplumber.instance.intercept_mode, InputPlumberInstance.INTERCEPT_MODE_ALL) + + # Find all composite devices + gut.p("Discovering all composite devices") + var devices := inputplumber.instance.get_composite_devices() + for device in devices: + _watch_device(device) + + # Add listeners for any new devices + inputplumber.instance.composite_device_added.connect(_watch_device) + + # Add listeners when devices are removed + var on_device_removed := func(dbus_path: String): + gut.p("Device was removed: " + dbus_path) + inputplumber.instance.composite_device_removed.connect(on_device_removed) + + await wait_seconds(30, "Waiting 30s... Press buttons to test") + + inputplumber.instance.intercept_mode = InputPlumberInstance.INTERCEPT_MODE_NONE diff --git a/core/systems/input/input_plumber_test.tscn b/core/systems/input/input_plumber_test.tscn deleted file mode 100644 index 659851bf..00000000 --- a/core/systems/input/input_plumber_test.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3 uid="uid://djylrf6b7nxyx"] - -[ext_resource type="Script" path="res://core/systems/input/input_plumber_test.gd" id="1_x2aky"] - -[node name="InputPlumberTest" type="Node2D"] -script = ExtResource("1_x2aky") diff --git a/core/systems/input/input_plumber_test_old.tscn b/core/systems/input/input_plumber_test_old.tscn new file mode 100644 index 00000000..88349a4e --- /dev/null +++ b/core/systems/input/input_plumber_test_old.tscn @@ -0,0 +1,52 @@ +[gd_scene load_steps=2 format=3 uid="uid://b401m3qcqa8yd"] + +[sub_resource type="GDScript" id="GDScript_k2tux"] +script/source = "extends Node2D + +var input_plumber := load(\"res://core/systems/input/input_plumber.tres\") as InputPlumber + + +# Called when the node enters the scene tree for the first time. +func _ready(): + for compo in input_plumber.composite_devices: + print(compo.dbus_targets) + for dt in compo.dbus_targets: + dt.input_event.connect(_on_input_event) + + input_plumber.composite_device_added.connect(_on_dev_add) + input_plumber.composite_device_removed.connect(_on_dev_rm) + input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.ALL) + get_tree().root.mode = Window.MODE_MINIMIZED + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(delta): + pass + + +func _on_input_event(event: String, value: float) -> void: + print(\"Got input event \" + event + \" with value \" + str(value)) + if event == \"ui_guide\": + var pressed = value == 1.0 + for device in input_plumber.composite_devices: + device.intercept_mode = 0 + device.send_event(\"Gamepad:Button:Guide\", pressed) + device.intercept_mode = InputPlumber.INTERCEPT_MODE.ALL as int + + if event == \"ui_accept\": + for device in input_plumber.composite_devices: + var intercept_mode = device.intercept_mode + device.intercept_mode = 0 + device.send_button_chord([\"Gamepad:Button:Guide\", \"Gamepad:Button:South\"]) + device.intercept_mode = InputPlumber.INTERCEPT_MODE.ALL as int + + +func _on_dev_add() -> void: + print(\"Newbie doobie\") + +func _on_dev_rm() -> void: + print(\"bubye\") +" + +[node name="InputPlumberTest" type="Node2D"] +script = SubResource("GDScript_k2tux") diff --git a/core/systems/input/overlay_mode_input_manager.gd b/core/systems/input/overlay_mode_input_manager.gd index f060c8fe..20706a36 100644 --- a/core/systems/input/overlay_mode_input_manager.gd +++ b/core/systems/input/overlay_mode_input_manager.gd @@ -14,7 +14,7 @@ class_name OverlayInputManager ## The audio manager to use to adjust the audio when audio input events happen. var audio_manager := load("res://core/global/audio_manager.tres") as AudioManager ## InputPlumber receives and sends DBus input events. -var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber +var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance ## LaunchManager provides context on the currently running app so we can switch profiles var launch_manager := load("res://core/global/launch_manager.tres") as LaunchManager ## The Global State Machine @@ -321,7 +321,7 @@ func _return_chord(actions: PackedStringArray) -> void: # Input.parse_input_event so we don't do this terrible loop. This is awful. logger.debug("Return events to InputPlumber: " + str(actions)) for device in input_plumber.composite_devices: - device.intercept_mode = InputPlumber.INTERCEPT_MODE.PASS + device.intercept_mode = InputPlumberInstance.INTERCEPT_MODE_PASS device.send_button_chord(actions) @@ -347,7 +347,7 @@ func _audio_input(event: InputEvent) -> void: return -func _watch_dbus_device(device: InputPlumber.CompositeDevice) -> void: +func _watch_dbus_device(device: CompositeDevice) -> void: for target in device.dbus_targets: logger.debug("Adding watch for " + device.name + " " + target.name) logger.debug(str(target.get_instance_id())) diff --git a/core/systems/launcher/reaper.gd b/core/systems/launcher/reaper.gd index fb05d0d5..67aa61e7 100644 --- a/core/systems/launcher/reaper.gd +++ b/core/systems/launcher/reaper.gd @@ -12,8 +12,9 @@ enum SIG { ## Spawn a process with PR_SET_CHILD_SUBREAPER set so child processes will ## reparent themselves to OpenGamepadUI. Returns the PID of the spawned process. static func create_process(cmd: String, args: PackedStringArray) -> int: - #return OS.create_process(cmd, args) - return LinuxThread.subreaper_create_process(cmd, args) + return OS.create_process(cmd, args) + # TODO: Re-implement subreaper + #return LinuxThread.subreaper_create_process(cmd, args) # Kills the given PID and all its descendants diff --git a/core/systems/launcher/running_app.gd b/core/systems/launcher/running_app.gd index 145fefff..160b3a2e 100644 --- a/core/systems/launcher/running_app.gd +++ b/core/systems/launcher/running_app.gd @@ -5,7 +5,7 @@ class_name RunningApp ## ## RunningApp contains details and methods around running applications -const Gamescope := preload("res://core/global/gamescope.tres") +const gamescope := preload("res://core/systems/gamescope/gamescope.tres") ## Emitted when all child processes of the app are no longer running @@ -187,27 +187,35 @@ func suspend(enable: bool) -> void: func get_window_title(win_id: int) -> String: if not win_id in window_ids: return "" - return Gamescope.get_window_name(win_id) + var xwayland := gamescope.get_xwayland_by_name(display) + if not xwayland: + return "" + return xwayland.get_window_name(win_id) ## Attempt to discover the window ID from the PID of the given application func get_window_id_from_pid() -> int: - var display_type := Gamescope.get_display_type(display) - return Gamescope.get_window_id(pid, display_type) + var xwayland := gamescope.get_xwayland_by_name(display) + if not xwayland: + return -1 + var windows := xwayland.get_windows_for_pid(pid) + if windows.is_empty(): + return -1 + return windows[0] ## Attempt to discover all window IDs from the PID of the given application and ## the PIDs of all processes in the same process group. func get_all_window_ids() -> PackedInt32Array: var app_name := launch_item.name - var display_type := Gamescope.get_display_type(display) var window_ids := PackedInt32Array() var pids := get_child_pids() + var xwayland := gamescope.get_xwayland_by_name(display) pids.append(pid) logger.trace(app_name + " found related PIDs: " + str(pids)) for process_id in pids: - var windows := Gamescope.get_window_ids(process_id, display_type) + var windows := xwayland.get_windows_for_pid(process_id) for window in windows: if window < 0: continue @@ -287,7 +295,10 @@ func can_focus() -> bool: func is_focused() -> bool: if not can_focus(): return false - var focused_window := Gamescope.get_focused_window() + var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) + if not xwayland_primary: + return false + var focused_window := xwayland_primary.focused_window return window_id == focused_window or focused_window in window_ids @@ -295,7 +306,10 @@ func is_focused() -> bool: func grab_focus() -> void: if not can_focus(): return - Gamescope.set_baselayer_window(window_id) + var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) + if not xwayland_primary: + return + xwayland_primary.set_baselayer_window(window_id) focused = true @@ -305,9 +319,14 @@ func switch_window(win_id: int, focus: bool = true) -> int: # Error if the window does not belong to the running app if not win_id in window_ids: return ERR_DOES_NOT_EXIST - + + # Get the primary XWayland instance + var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) + if not xwayland_primary: + return ERR_UNAVAILABLE + # Check if this app is a focusable window. - if not win_id in Gamescope.get_focusable_windows(): + if not win_id in xwayland_primary.focusable_windows: return ERR_UNAVAILABLE # Update the window ID and optionally grab focus @@ -332,6 +351,11 @@ func _ensure_app_id() -> void: if is_steam_app() or not is_ogui_managed: return + # Get the xwayland instance this app is running on + var xwayland := gamescope.get_xwayland_by_name(display) + if not xwayland: + return + # Get all windows associated with the running app var possible_windows := window_ids.duplicate() @@ -339,25 +363,32 @@ func _ensure_app_id() -> void: # gamescope will make these windows available as focusable windows. var app_name := launch_item.name for window in possible_windows: - var display_type := Gamescope.get_display_type(display) - if Gamescope.has_app_id(window, display_type): + if xwayland.has_app_id(window): continue - Gamescope.set_app_id(window, window, display_type) + xwayland.set_app_id(window, window) ## Returns whether or not the window id of the running app needs to be discovered func needs_window_id() -> bool: + var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) + if not xwayland_primary: + return false + if window_id <= 0: logger.trace(launch_item.name + " has a bad window ID: " + str(window_id)) return true - var focusable_windows := Gamescope.get_focusable_windows() + var focusable_windows := xwayland_primary.focusable_windows if not window_id in focusable_windows: logger.trace(str(window_id) + " is not in the list of focusable windows") return true + var xwayland := gamescope.get_xwayland_by_name(display) + if not xwayland: + return false + # Check if the current window ID exists in the list of open windows - var root_window := Gamescope.get_root_window_id(Gamescope.XWAYLAND.GAME) - var all_windows := Gamescope.get_all_windows(root_window, Gamescope.XWAYLAND.GAME) + var root_window := xwayland.root_window_id + var all_windows := xwayland.get_all_windows(root_window) if not window_id in all_windows: logger.trace(str(window_id) + " is not in the list of all windows") return true @@ -365,12 +396,11 @@ func needs_window_id() -> bool: # If this is a Steam app, the only acceptable window will have its STEAM_GAME # property set. if is_steam_app(): - var display_type := Gamescope.get_display_type(display) var steam_app_id := get_meta("steam_app_id") as int - if not Gamescope.has_app_id(window_id, display_type): + if not xwayland.has_app_id(window_id): logger.trace(str(window_id) + " does not have an app ID already set by Steam") return true - if Gamescope.get_app_id(window_id) != steam_app_id: + if xwayland.get_app_id(window_id) != steam_app_id: logger.trace(str(window_id) + " has an app ID but it does not match " + str(steam_app_id)) return true @@ -392,9 +422,14 @@ func _discover_window_id() -> int: # Get all windows associated with the running app var possible_windows := window_ids.duplicate() - + + # Get the primary XWayland instance + var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) + if not xwayland_primary: + return -1 + # Look for the app window in the list of focusable windows - var focusable := Gamescope.get_focusable_windows() + var focusable := xwayland_primary.focusable_windows for window in possible_windows: if window in focusable: return window diff --git a/core/systems/performance/performance_manager.gd b/core/systems/performance/performance_manager.gd index b7166f76..96f8cff2 100644 --- a/core/systems/performance/performance_manager.gd +++ b/core/systems/performance/performance_manager.gd @@ -11,7 +11,7 @@ signal profile_loaded(profile: PerformanceProfile) signal profile_saved(profile: PerformanceProfile, path: String) const USER_PROFILES := "user://data/performance/profiles" -const DOCKED_STATES := [PowerManager.DEVICE_STATE.CHARGING, PowerManager.DEVICE_STATE.FULLY_CHARGED, PowerManager.DEVICE_STATE.PENDING_CHARGE] +const DOCKED_STATES := [UPowerDevice.STATE_CHARGING, UPowerDevice.STATE_FULLY_CHARGED, UPowerDevice.STATE_PENDING_CHARGE] ## Performance profiles are separated into these states, so users can have different ## performance depending on whether or not they are plugged in to an external power @@ -22,11 +22,12 @@ enum PROFILE_STATE { } var _hardware_manager := load("res://core/systems/hardware/hardware_manager.tres") as HardwareManager -var _power_manager := load("res://core/systems/power/power_manager.tres") as PowerManager +var _power_manager := load("res://core/systems/power/power_manager.tres") as UPowerInstance var _settings_manager := load("res://core/global/settings_manager.tres") as SettingsManager -var _power_station := load("res://core/systems/performance/power_station.tres") as PowerStation +var _power_station := load("res://core/systems/performance/power_station.tres") as PowerStationInstance var _launch_manager := load("res://core/global/launch_manager.tres") as LaunchManager +var display_device := _power_manager.get_display_device() var current_profile: PerformanceProfile var current_profile_state: PROFILE_STATE # docked or undocked var logger := Log.get_logger("PerformanceManager", Log.LEVEL.INFO) @@ -39,15 +40,13 @@ func _init() -> void: # Connect to battery state changes to switch between "docked" and "undocked" # performance profiles. - var batteries := _power_manager.get_devices_by_type(PowerManager.DEVICE_TYPE.BATTERY) - if batteries.size() > 1: - logger.warn("You somehow have more than one battery. We don't know what to do with that.") - if batteries.size() > 0: - var battery := batteries[0] - battery.updated.connect(_on_battery_updated.bind(battery)) - + if display_device: + display_device.updated.connect(_on_battery_updated.bind(display_device)) + + # TODO: Listen for signals if PowerStation starts/stops + # Do nothing if PowerStation is not detected - if not _power_station.supports_power_station(): + if not _power_station.is_running(): return # Load and apply the default profile @@ -109,14 +108,14 @@ func create_profile(library_item: LibraryLaunchItem = null) -> PerformanceProfil profile.cpu_smt_enabled = _power_station.cpu.smt_enabled # Detect all GPU cards - var cards: Array[PowerStation.GPUCard] = [] + var cards: Array[GpuCard] = [] if _power_station.gpu: cards = _power_station.gpu.get_cards() # Configure GPU settings # TODO: Support multiple GPUs? for card in cards: - if card.class_type != "integrated": + if card.class != "integrated": continue profile.tdp_current = card.tdp @@ -161,7 +160,7 @@ func load_or_create_profile(profile_path: String, library_item: LibraryLaunchIte ## Applies the given performance profile to the system func apply_profile(profile: PerformanceProfile) -> void: - if not _power_station.supports_power_station(): + if not _power_station.is_running(): logger.info("Unable to apply performance profile. PowerStation not detected.") return @@ -178,14 +177,14 @@ func apply_profile(profile: PerformanceProfile) -> void: _power_station.cpu.cores_enabled = profile.cpu_core_count_current # Detect all GPU cards - var cards: Array[PowerStation.GPUCard] = [] + var cards: Array[GpuCard] = [] if _power_station.gpu: cards = _power_station.gpu.get_cards() # Configure GPU settings # TODO: Support mutliple GPUs? for card in cards: - if card.class_type != "integrated": + if card.class != "integrated": continue logger.debug("Applying GPU performance settings from profile") if card.manual_clock != profile.gpu_manual_enabled: @@ -242,26 +241,22 @@ func apply_and_save_profile(profile: PerformanceProfile) -> void: ## Returns the current profile state. I.e. whether or not the "docked" or "undocked" ## performance profiles should be used. func get_profile_state() -> PROFILE_STATE: - var batteries := _power_manager.get_devices_by_type(PowerManager.DEVICE_TYPE.BATTERY) - if batteries.size() > 1: - logger.warn("You somehow have more than one battery. We don't know what to do with that.") - if batteries.size() > 0: - var battery := batteries[0] - return get_profile_state_from_battery(battery) + if display_device: + return get_profile_state_from_battery(display_device) return PROFILE_STATE.DOCKED ## Returns the current profile state. I.e. whether or not the "docked" or "undocked" ## performance profiles should be used. -func get_profile_state_from_battery(battery: PowerManager.Device) -> PROFILE_STATE: +func get_profile_state_from_battery(battery: UPowerDevice) -> PROFILE_STATE: if battery.state not in DOCKED_STATES: return PROFILE_STATE.UNDOCKED return PROFILE_STATE.DOCKED ## Called whenever a battery is updated -func _on_battery_updated(battery: PowerManager.Device) -> void: +func _on_battery_updated(battery: UPowerDevice) -> void: # Get the current profile state to see if we need to load the docked or # undocked profile. var profile_state := get_profile_state_from_battery(battery) diff --git a/core/systems/performance/power_station.gd b/core/systems/performance/power_station.gd index 2c912779..393c8695 100644 --- a/core/systems/performance/power_station.gd +++ b/core/systems/performance/power_station.gd @@ -1,4 +1,5 @@ -extends Resource +@icon("res://assets/editor-icons/powerstation.svg") +extends Node class_name PowerStation ## Proxy interface to PowerStation over DBus @@ -6,404 +7,20 @@ class_name PowerStation ## Provides wrapper classes and methods for interacting with PowerStation over ## DBus to control CPU and GPU performance. -const POWERSTATION_BUS := "org.shadowblip.PowerStation" -const PERFORMANCE_PATH := "/org/shadowblip/Performance" -const CPU_PATH := "/org/shadowblip/Performance/CPU" -const GPU_PATH := "/org/shadowblip/Performance/GPU" -const IFACE_CPU := "org.shadowblip.CPU" -const IFACE_CPU_CORE := "org.shadowblip.CPU.Core" -const IFACE_GPU := "org.shadowblip.GPU" -const IFACE_GPU_CARD := "org.shadowblip.GPU.Card" -const IFACE_GPU_TDP := "org.shadowblip.GPU.Card.TDP" -const IFACE_GPU_CONNECTOR := "org.shadowblip.GPU.Card.Connector" +@export var instance: PowerStationInstance = load("res://core/systems/performance/power_station.tres") as PowerStationInstance +# Keep a reference to instances so they are not cleaned up automatically +var _cpu: Cpu +var _gpu: Gpu var logger := Log.get_logger("PowerStation") -var dbus := load("res://core/global/dbus_system.tres") as DBusManager -var cpu := CPUBus.new(dbus.create_proxy(POWERSTATION_BUS, CPU_PATH)) -var gpu := GPUBus.new(dbus.create_proxy(POWERSTATION_BUS, GPU_PATH)) -## Returns true if PowerStation can be used on this system -func supports_power_station() -> bool: - return dbus.bus_exists(POWERSTATION_BUS) +func _ready() -> void: + _cpu = instance.get_cpu() + _gpu = instance.get_gpu() -## CPUBus provides a DBus connection to the CPU bus for CPU controls -class CPUBus extends Resource: - signal properties_changed - signal updated - var _proxy: DBusManager.Proxy - - func _init(proxy: DBusManager.Proxy) -> void: - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - ## Returns a list of DBus object paths to every detected core - func enumerate_cores() -> PackedStringArray: - var result := _proxy.call_method(IFACE_CPU, "EnumerateCores") - if not result: - return [] - var args := result.get_args() - if args.size() != 1: - return [] - if not args[0] is Array: - return [] - return args[0] - - ## Returns true if the CPU has the given feature - func has_feature(feature: String) -> bool: - var result := _proxy.call_method(IFACE_CPU, "HasFeature", [feature], "s") - if not result: - return false - var args := result.get_args() - if args.size() != 1: - return false - if not args[0] is bool: - return false - return args[0] - - var boost_enabled: bool: - set(v): - _proxy.set_property(IFACE_CPU, "BoostEnabled", v) - get: - var property = _proxy.get_property(IFACE_CPU, "BoostEnabled") - if not property is bool: - return false - return property - - var cores_count: int: - get: - var property = _proxy.get_property(IFACE_CPU, "CoresCount") - if not property is int: - return -1 - return property - - var cores_enabled: int: - set(v): - _proxy.set_property(IFACE_CPU, "CoresEnabled", DBus.uint32(v)) - get: - var property = _proxy.get_property(IFACE_CPU, "CoresEnabled") - if not property is int: - return -1 - return property - - var features: PackedStringArray: - get: - var property = _proxy.get_property(IFACE_CPU, "Features") - if not property is Array: - return [] - return property - - var smt_enabled: bool: - set(v): - _proxy.set_property(IFACE_CPU, "SmtEnabled", v) - get: - var property = _proxy.get_property(IFACE_CPU, "SmtEnabled") - if not property is bool: - return false - return property - - -## Provides an interface to enumerate all detected GPU cards -class GPUBus extends Resource: - signal properties_changed - signal updated - var _proxy: DBusManager.Proxy - - func _init(proxy: DBusManager.Proxy) -> void: - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - ## Returns a list of DBus object paths to every detected GPU card - func enumerate_cards() -> PackedStringArray: - var result := _proxy.call_method(IFACE_GPU, "EnumerateCards") - if not result: - return [] - var args := result.get_args() - if args.size() != 1: - return [] - if not args[0] is Array: - return [] - return args[0] - - ## Returns a list of all GPUCard objects - func get_cards() -> Array[GPUCard]: - var dbus := load("res://core/global/dbus_system.tres") as DBusManager - var cards: Array[GPUCard] = [] - var paths := self.enumerate_cards() - for path in paths: - var card = GPUCard.new(dbus.create_proxy(POWERSTATION_BUS, path)) - cards.append(card) - - return cards - - -## GPUCard provides a DBus connection to the GPU for GPU control -class GPUCard extends Resource: - signal properties_changed - signal updated - var _proxy: DBusManager.Proxy - - func _init(proxy: DBusManager.Proxy) -> void: - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - ## Returns true if the card supports TDP control - func supports_tdp() -> bool: - return self.tdp > 1 - - ## Returns a list of DBus object paths to every detected GPU connector - func enumerate_connectors() -> PackedStringArray: - var result := _proxy.call_method(IFACE_GPU_CARD, "EnumerateConnectors") - if not result: - return [] - var args := result.get_args() - if args.size() != 1: - return [] - if not args[0] is Array: - return [] - return args[0] - - ## Returns a list of all GPUConnector objects - func get_connectors() -> Array[GPUConnector]: - var dbus := load("res://core/global/dbus_system.tres") as DBusManager - var connectors: Array[GPUConnector] = [] - var paths := self.enumerate_connectors() - for path in paths: - var card = GPUConnector.new(dbus.create_proxy(POWERSTATION_BUS, path)) - connectors.append(card) - - return connectors - - var class_type: String: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "Class") - if not property is String: - return "" - return property - - var class_id: String: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "ClassId") - if not property is String: - return "" - return property - - var clock_limit_mhz_max: float: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "ClockLimitMhzMax") - if not property is float: - return -1 - return property - - var clock_limit_mhz_min: float: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "ClockLimitMhzMin") - if not property is float: - return -1 - return property - - var clock_value_mhz_max: float: - set(v): - _proxy.set_property(IFACE_GPU_CARD, "ClockValueMhzMax", float(v)) - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "ClockValueMhzMax") - if not property is float: - return -1 - return property - - var clock_value_mhz_min: float: - set(v): - _proxy.set_property(IFACE_GPU_CARD, "ClockValueMhzMin", float(v)) - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "ClockValueMhzMin") - if not property is float: - return -1 - return property - - var device: String: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "Device") - if not property is String: - return "" - return property - - var device_id: String: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "DeviceId") - if not property is String: - return "" - return property - - var manual_clock: bool: - set(v): - _proxy.set_property(IFACE_GPU_CARD, "ManualClock", v) - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "ManualClock") - if not property is bool: - return false - return property - - var name: String: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "Name") - if not property is String: - return "" - return property - - var path: String: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "Path") - if not property is String: - return "" - return property - - var revision_id: String: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "RevisionId") - if not property is String: - return "" - return property - - var subdevice: String: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "Subdevice") - if not property is String: - return "" - return property - - var subdevice_id: String: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "SubdeviceId") - if not property is String: - return "" - return property - - var subvendor_id: String: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "SubvendorId") - if not property is String: - return "" - return property - - var vendor: String: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "Vendor") - if not property is String: - return "" - return property - - var vendor_id: String: - get: - var property = _proxy.get_property(IFACE_GPU_CARD, "VendorId") - if not property is String: - return "" - return property - - var boost: float: - set(v): - _proxy.set_property(IFACE_GPU_TDP, "Boost", float(v)) - get: - var property = _proxy.get_property(IFACE_GPU_TDP, "Boost") - if not property is float: - return -1 - return property - - var power_profile: String: - set(v): - _proxy.set_property(IFACE_GPU_TDP, "PowerProfile", v) - get: - var property = _proxy.get_property(IFACE_GPU_TDP, "PowerProfile") - if not property is String: - return "" - return property - - var tdp: float: - set(v): - _proxy.set_property(IFACE_GPU_TDP, "TDP", float(v)) - get: - var property = _proxy.get_property(IFACE_GPU_TDP, "TDP") - if not property is float: - return -1 - return property - - var thermal_throttle_limit_c: float: - set(v): - _proxy.set_property(IFACE_GPU_TDP, "ThermalThrottleLimitC", float(v)) - get: - var property = _proxy.get_property(IFACE_GPU_TDP, "ThermalThrottleLimitC") - if not property is float: - return -1 - return property - - -## GPUConnector provides a DBus connection to a GPU connector -class GPUConnector extends Resource: - signal properties_changed - signal updated - var _proxy: DBusManager.Proxy - - func _init(proxy: DBusManager.Proxy) -> void: - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - var dpms: bool: - get: - var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "DPMS") - if not property is bool: - return false - return property - - var enabled: bool: - get: - var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "Enabled") - if not property is bool: - return false - return property - - var id: int: - get: - var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "Id") - if not property is int: - return -1 - return property - - var modes: PackedStringArray: - get: - var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "Modes") - if not property is Array: - return [] - return property - - var name: String: - get: - var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "Name") - if not property is String: - return "" - return property - - var path: String: - get: - var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "Path") - if not property is String: - return "" - return property - - var status: String: - get: - var property = _proxy.get_property(IFACE_GPU_CONNECTOR, "Status") - if not property is String: - return "" - return property +func _process(_delta: float) -> void: + if not instance: + return + instance.process() diff --git a/core/systems/performance/power_station.tres b/core/systems/performance/power_station.tres index 20b69611..14a60180 100644 --- a/core/systems/performance/power_station.tres +++ b/core/systems/performance/power_station.tres @@ -1,6 +1,3 @@ -[gd_resource type="Resource" script_class="PowerStation" load_steps=2 format=3 uid="uid://cdyeux8mdf6l6"] - -[ext_resource type="Script" path="res://core/systems/performance/power_station.gd" id="1_bdrpq"] +[gd_resource type="PowerStationInstance" format=3 uid="uid://c2mmrnh3rcs58"] [resource] -script = ExtResource("1_bdrpq") diff --git a/core/systems/performance/power_station_test.gd b/core/systems/performance/power_station_test.gd index d20f159e..1444dcac 100644 --- a/core/systems/performance/power_station_test.gd +++ b/core/systems/performance/power_station_test.gd @@ -1,39 +1,43 @@ extends GutTest -var power_station := load("res://core/systems/performance/power_station.tres") as PowerStation - func test_cpu() -> void: - if not power_station.supports_power_station(): + var powerstation := PowerStation.new() + powerstation.instance = load("res://core/systems/performance/power_station.tres") as PowerStationInstance + add_child_autoqfree(powerstation) + + if not powerstation.instance.is_running(): pass_test("PowerStation is not running, skipping") return - var cpu := power_station.cpu + var cpu := powerstation.instance.get_cpu() assert_not_null(cpu, "should return CPU instance") # Test getting total number of cpu cores - var num_cores = cpu.cores_count + var num_cores := cpu.cores_count + gut.p("Total CPU cores: " + str(num_cores)) assert_ne(num_cores, -1, "should have returned total core count") - # Test that CPU cores get set - cpu.cores_enabled = num_cores - 1 - assert_eq(cpu.cores_enabled, num_cores - 1, "should have disabled cores") - - # Set the cores back - cpu.cores_enabled = num_cores - assert_eq(cpu.cores_enabled, num_cores, "should have re-enabled all cores") - - # Test enumerating cores - var enumerated := cpu.enumerate_cores() - assert_gt(enumerated.size(), 0, "should return at least 1 cpu core path") - # Test getting features - var features = cpu.features + var features := cpu.features + gut.p("Found CPU features: " + str(features)) assert_gt(features.size(), 1, "should return CPU features") if features.size() > 1: var feature := features[0] as String assert_true(cpu.has_feature(feature), "should have CPU feature") assert_false(cpu.has_feature("IdontXsist!"), "should not have CPU feature") + # Test that CPU cores get set + gut.p("Disabling one CPU core") + cpu.cores_enabled = num_cores - 1 + gut.p("Cores enabled: " + str(cpu.cores_enabled)) + assert_eq(cpu.cores_enabled, num_cores - 1, "should have disabled cores") + + # Set the cores back + gut.p("Re-enabling CPU core") + cpu.cores_enabled = num_cores + gut.p("Cores enabled: " + str(cpu.cores_enabled)) + assert_eq(cpu.cores_enabled, num_cores, "should have re-enabled all cores") + # Test setting SMT cpu.smt_enabled = false assert_false(cpu.smt_enabled, "should have disabled SMT") @@ -44,24 +48,32 @@ func test_cpu() -> void: # Test setting boost cpu.boost_enabled = false - assert_false(cpu.boost_enabled, "should have disabled boost") + #assert_false(cpu.boost_enabled, "should have disabled boost") await wait_frames(1, "wait for change") cpu.boost_enabled = true - assert_true(cpu.boost_enabled, "should have enabled boost") + #assert_true(cpu.boost_enabled, "should have enabled boost") await wait_frames(1, "wait for change") + # Test enumerating cores + var cores := cpu.get_cores() + assert_gt(cores.size(), 0, "should return at least 1 cpu core") + for core in cores: + gut.p("CPU Core: " + str(core.number)) + gut.p(" ID: " + str(core.core_id)) + gut.p(" Online: " + str(core.online)) + func test_gpu() -> void: - if not power_station.supports_power_station(): + var powerstation := PowerStation.new() + powerstation.instance = load("res://core/systems/performance/power_station.tres") as PowerStationInstance + add_child_autoqfree(powerstation) + + if not powerstation.instance.is_running(): pass_test("PowerStation is not running, skipping") return - var gpu := power_station.gpu + var gpu := powerstation.instance.get_gpu() assert_not_null(gpu, "should return GPU instance") - # Test enumerating cards - var card_paths := gpu.enumerate_cards() - #assert_gt(cards.size(), 0, "should return at least 1 gpu") - # Test all GPU card methods var cards := gpu.get_cards() for card in cards: diff --git a/core/systems/power/power_manager.gd b/core/systems/power/power_manager.gd index 371ff69a..f300d3f7 100644 --- a/core/systems/power/power_manager.gd +++ b/core/systems/power/power_manager.gd @@ -1,352 +1,26 @@ -extends Resource +extends Node class_name PowerManager -const POWER_BUS := "org.freedesktop.UPower" -const UPOWER_PATH := "/org/freedesktop/UPower" -const POWER_PREFIX := "/org/freedesktop/UPower/devices" -const IFACE_UPOWER := "org.freedesktop.UPower" -const IFACE_DEVICE := "org.freedesktop.UPower.Device" +## Manages power settings. +## +## The [PowerManager] class is responsible for loading a [UPowerInstance] and +## calling its 'process()' method each frame. -enum DEVICE_TYPE { - UNKNOWN, - LINE_POWER, - BATTERY, - UPS, - MONITOR, - MOUSE, - KEYBOARD, - PDA, - PHONE, -} +@export var instance: UPowerInstance = load("res://core/systems/power/power_manager.tres") as UPowerInstance -enum DEVICE_STATE { - UNKNOWN, - CHARGING, - DISCHARGING, - EMPTY, - FULLY_CHARGED, - PENDING_CHARGE, - PENDING_DISCHARGE, -} +# Keep a reference to device instances so they are not cleaned up automatically +var _devices: Array[UPowerDevice] +var logger := Log.get_logger("PowerManager") -enum DEVICE_WARNING_LEVEL { - UNKNOWN, - NONE, - DISCHARGING, - LOW, - CRITICAL, - ACTION, -} -enum DEVICE_BATTERY_LEVEL { - UNKNOWN, - NONE, - LOW, - CRITICAL, - NORMAL, - HIGH, - FULL,Z -} +func _ready() -> void: + var display_device := instance.get_display_device() + _devices.push_back(display_device) + if _devices.is_empty(): + logger.warn("UPower not detected.") -enum DEVICE_TECHNOLOGY { - UNKNOWN, - LITHIUM_ION, - LITHIUM_POLYMER, - LITHIUM_IRON_PHOSPHATE, - LEAD_ACID, - NICKLE_CADMIUM, - NICKLE_METAL_HYDRIDE, -} -var dbus := load("res://core/global/dbus_system.tres") as DBusManager -var upower := UPower.new(dbus.create_proxy(POWER_BUS, UPOWER_PATH)) - - -func get_devices() -> Array[Device]: - var devices: Array[Device] = [] - var device_paths := upower.enumerate_devices() - - # Loop through all objects on the bus - for path in device_paths: - # Create a power Device from this object - var proxy := dbus.create_proxy(POWER_BUS, path) - var device := Device.new(proxy) - - devices.append(device) - - return devices - - -func get_devices_by_type(type: DEVICE_TYPE) -> Array[Device]: - var all_devices := get_devices() - var type_devices : Array[Device] - for device in all_devices: - if device.type == type: - type_devices.append(device) - return type_devices - - -func get_device(device_name: String) -> Device: - var device_path := "/".join([POWER_PREFIX, device_name]) - var proxy := dbus.create_proxy(POWER_BUS, device_path) - var device := Device.new(proxy) - - return device - - -## Returns true if bluetooth can be used on this system -func supports_power() -> bool: - return dbus.bus_exists(POWER_BUS) - - -class UPower extends Resource: - signal updated - var _proxy: DBusManager.Proxy - var on_battery: bool: - get: - var property = _proxy.get_property(IFACE_UPOWER, "OnBattery") - if not property is bool: - return false - return property - - func _init(proxy: DBusManager.Proxy) -> void: - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - func enumerate_devices() -> Array: - var result := _proxy.call_method(IFACE_UPOWER, "EnumerateDevices") - var args := result.get_args() - if args.size() != 1: - return [] - if not args[0] is Array: - return [] - return args[0] - - -class Device extends Resource: - signal porperties_changed - signal updated - var _proxy: DBusManager.Proxy - - func _init(proxy: DBusManager.Proxy) -> void: - _proxy = proxy - _proxy.properties_changed.connect(_on_properties_changed) - - func _on_properties_changed(iface: String, props: Dictionary) -> void: - updated.emit() - - func refresh() -> void: - _proxy.call_method(IFACE_DEVICE, "Refresh") - - var native_path: String: - get: - var property = _proxy.get_property(IFACE_DEVICE, "NativePath") - if not property is String: - return "" - return property - - var vendor: String: - get: - var property = _proxy.get_property(IFACE_DEVICE, "Vendor") - if not property is String: - return "" - return property - - var model: String: - get: - var property = _proxy.get_property(IFACE_DEVICE, "Model") - if not property is String: - return "" - return property - - var serial: String: - get: - var property = _proxy.get_property(IFACE_DEVICE, "Serial") - if not property is String: - return "" - return property - - var update_time: int: - get: - var property = _proxy.get_property(IFACE_DEVICE, "UpdateTime") - if not property is int: - return 0 - return property - - var type: int: - get: - var property = _proxy.get_property(IFACE_DEVICE, "Type") - if not property is int: - return 0 - return property - - var power_supply: bool: - get: - var property = _proxy.get_property(IFACE_DEVICE, "PowerSupply") - if not property is bool: - return false - return property - - var has_history: bool: - get: - var property = _proxy.get_property(IFACE_DEVICE, "HasHistory") - if not property is bool: - return false - return property - - var has_statistics: bool: - get: - var property = _proxy.get_property(IFACE_DEVICE, "HasStatisics") - if not property is bool: - return false - return property - - var online: bool: - get: - var property = _proxy.get_property(IFACE_DEVICE, "Online") - if not property is bool: - return false - return property - - var energy: float: - get: - var property = _proxy.get_property(IFACE_DEVICE, "Energy") - if not property is float: - return 0.0 - return property - - var energy_empty: float: - get: - var property = _proxy.get_property(IFACE_DEVICE, "EnergyEmpty") - if not property is float: - return 0.0 - return property - - var energy_full: float: - get: - var property = _proxy.get_property(IFACE_DEVICE, "EnergyFull") - if not property is float: - return 0.0 - return property - - var energy_full_design: float: - get: - var property = _proxy.get_property(IFACE_DEVICE, "EnergyFullDesign") - if not property is float: - return 0.0 - return property - - var energy_rate: float: - get: - var property = _proxy.get_property(IFACE_DEVICE, "EnergyRate") - if not property is float: - return 0.0 - return property - - var voltage: float: - get: - var property = _proxy.get_property(IFACE_DEVICE, "Voltage") - if not property is float: - return 0.0 - return property - - var charge_cycles: int: - get: - var property = _proxy.get_property(IFACE_DEVICE, "ChargeCycles") - if not property is int: - return 0 - return property - - var luminosity: float: - get: - var property = _proxy.get_property(IFACE_DEVICE, "Luminosity") - if not property is float: - return 0.0 - return property - - var time_to_empty: int: - get: - var property = _proxy.get_property(IFACE_DEVICE, "TimeToEmpty") - if not property is int: - return 0 - return property - - var time_to_full: int: - get: - var property = _proxy.get_property(IFACE_DEVICE, "TimeToFull") - if not property is int: - return 0 - return property - - var percentage: float: - get: - var property = _proxy.get_property(IFACE_DEVICE, "Percentage") - if not property is float: - return 0.0 - return property - - var temperature: float: - get: - var property = _proxy.get_property(IFACE_DEVICE, "Temperature") - if not property is float: - return 0.0 - return property - - var is_present: bool: - get: - var property = _proxy.get_property(IFACE_DEVICE, "IsPresent") - if not property is bool: - return false - return property - - var state: int: - get: - var property = _proxy.get_property(IFACE_DEVICE, "State") - if not property is int: - return 0 - return property - - var is_rechargable: bool: - get: - var property = _proxy.get_property(IFACE_DEVICE, "IsRechargeable") - if not property is bool: - return false - return property - - var capacity: float: - get: - var property = _proxy.get_property(IFACE_DEVICE, "Capacity") - if not property is float: - return 0.0 - return property - - var technology: int: - get: - var property = _proxy.get_property(IFACE_DEVICE, "Technology") - if not property is int: - return 0 - return property - - var warning_level: int: - get: - var property = _proxy.get_property(IFACE_DEVICE, "WarningLevel") - if not property is int: - return 0 - return property - - var battery_level: int: - get: - var property = _proxy.get_property(IFACE_DEVICE, "BatteryLevel") - if not property is int: - return 0 - return property - - var icon_name: String: - get: - var property = _proxy.get_property(IFACE_DEVICE, "IconName") - if not property is String: - return "" - return property +func _process(_delta: float) -> void: + if not instance: + return + instance.process() diff --git a/core/systems/power/power_manager.tres b/core/systems/power/power_manager.tres index a13360ea..79b53674 100644 --- a/core/systems/power/power_manager.tres +++ b/core/systems/power/power_manager.tres @@ -1,6 +1,3 @@ -[gd_resource type="Resource" script_class="PowerManager" load_steps=2 format=3 uid="uid://dxa4156di7yk2"] - -[ext_resource type="Script" path="res://core/systems/power/power_manager.gd" id="1_57v16"] +[gd_resource type="UPowerInstance" format=3 uid="uid://pgiv8yxro3n8"] [resource] -script = ExtResource("1_57v16") diff --git a/core/systems/power/power_manager_test.gd b/core/systems/power/power_manager_test.gd new file mode 100644 index 00000000..76ef3a8c --- /dev/null +++ b/core/systems/power/power_manager_test.gd @@ -0,0 +1,20 @@ +extends GutTest + + +func test_upower() -> void: + var power_manager := PowerManager.new() + power_manager.instance = load("res://core/systems/power/power_manager.tres") + add_child_autoqfree(power_manager) + + if not power_manager.instance.is_running(): + gut.p("InputPlumber is not running. Skipping tests.") + return + + # Get the display device + var display_device := power_manager.instance.get_display_device() + assert_eq(display_device.dbus_path, "/org/freedesktop/UPower/devices/DisplayDevice") + gut.p("DBus Path: " + str(display_device.dbus_path)) + gut.p("Battery level: " + str(display_device.battery_level)) + gut.p("Icon name: " + str(display_device.icon_name)) + gut.p("Percentage: " + str(display_device.percentage)) + gut.p("State: " + str(display_device.state)) diff --git a/core/systems/power/power_saver.gd b/core/systems/power/power_saver.gd index a3a5a91b..ba6dbdc2 100644 --- a/core/systems/power/power_saver.gd +++ b/core/systems/power/power_saver.gd @@ -6,7 +6,7 @@ class_name PowerSaver var display := load("res://core/global/display_manager.tres") as DisplayManager var settings := load("res://core/global/settings_manager.tres") as SettingsManager -var power_manager := load("res://core/systems/power/power_manager.tres") as PowerManager +var power_manager := load("res://core/systems/power/power_manager.tres") as UPowerInstance const MINUTE := 60 @@ -27,14 +27,13 @@ var dimmed := false var prev_brightness := {} var supports_brightness := display.supports_brightness() var has_battery := false -var batteries : Array[PowerManager.Device] +var display_device := power_manager.get_display_device() var logger := Log.get_logger("PowerSaver") func _ready() -> void: - batteries = power_manager.get_devices_by_type(PowerManager.DEVICE_TYPE.BATTERY) - if batteries.size() > 0: - has_battery = true + if display_device: + has_battery = display_device.is_present if dim_screen_enabled and supports_brightness: dim_timer.timeout.connect(_on_dim_timer_timeout) @@ -46,9 +45,9 @@ func _ready() -> void: func _on_dim_timer_timeout() -> void: # If dimming is disabled when charging, check the battery state - if has_battery and not dim_when_charging: - var status: int = batteries[0].state - if status in [PowerManager.DEVICE_STATE.CHARGING, PowerManager.DEVICE_STATE.FULLY_CHARGED]: + if has_battery and display_device and not dim_when_charging: + var status := display_device.state + if status in [UPowerDevice.STATE_CHARGING, UPowerDevice.STATE_FULLY_CHARGED]: logger.debug("Not dimming because battery is charging") return if not has_battery and not dim_when_charging: @@ -71,9 +70,9 @@ func _on_dim_timer_timeout() -> void: func _on_suspend_timer_timeout() -> void: # If suspend is disabled when charging, check the battery state - if has_battery and not suspend_when_charging: - var status: int = batteries[0].state - if status in [PowerManager.DEVICE_STATE.CHARGING, PowerManager.DEVICE_STATE.FULLY_CHARGED]: + if has_battery and display_device and not suspend_when_charging: + var status := display_device.state + if status in [UPowerDevice.STATE_CHARGING, UPowerDevice.STATE_FULLY_CHARGED]: logger.debug("Not suspending because battery is charging") return if not has_battery and not suspend_when_charging: diff --git a/core/systems/threading/pty_test.gd b/core/systems/threading/pty_test.gd new file mode 100644 index 00000000..6386fa7d --- /dev/null +++ b/core/systems/threading/pty_test.gd @@ -0,0 +1,24 @@ +extends GutTest + + +func test_pty() -> void: + var pty := Pty.new() + add_child_autoqfree(pty) + + # Listen for output from the command + var on_line_written := func(line: String): + gut.p("Line: " + line) + if line.contains("Type something:"): + pty.write_line("Hello World") + pty.line_written.connect(on_line_written) + + # Execute the command in the PTY + var result := pty.exec("bash", PackedStringArray(["-c", "read -p 'Type something: ' foo; echo 'You typed:' $foo"])) + assert_eq(result, OK) + + # Wait for the command to exit + var exit_code = await pty.finished + + gut.p("Command finished with exit code: " + str(exit_code)) + + await wait_seconds(5, "Waiting") diff --git a/core/ui/card_ui/card_ui.gd b/core/ui/card_ui/card_ui.gd index 44327f30..a1d62c2f 100644 --- a/core/ui/card_ui/card_ui.gd +++ b/core/ui/card_ui/card_ui.gd @@ -1,10 +1,10 @@ extends Control var platform := load("res://core/global/platform.tres") as Platform -var gamescope := load("res://core/global/gamescope.tres") as Gamescope +var gamescope := load("res://core/systems/gamescope/gamescope.tres") as GamescopeInstance var library_manager := load("res://core/global/library_manager.tres") as LibraryManager var settings_manager := load("res://core/global/settings_manager.tres") as SettingsManager -var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber +var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance var state_machine := ( preload("res://assets/state/state_machines/global_state_machine.tres") as StateMachine @@ -20,7 +20,9 @@ var osk_state := preload("res://assets/state/states/osk.tres") as State var power_state := preload("res://assets/state/states/power_menu.tres") as State var PID: int = OS.get_process_id() -var overlay_window_id := gamescope.get_window_id(PID, gamescope.XWAYLAND.OGUI) +var _xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) +var _xwayland_ogui := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_OGUI) +var overlay_window_id := 0 @onready var panel := $%Panel as Panel @onready var ui_container := $%MenuContent as MarginContainer @@ -33,6 +35,12 @@ var overlay_window_id := gamescope.get_window_id(PID, gamescope.XWAYLAND.OGUI) var logger = Log.get_logger("Main", Log.LEVEL.INFO) func _init() -> void: + # Discover the window id of OpenGamepadUI + if _xwayland_ogui: + var ogui_windows := _xwayland_ogui.get_windows_for_pid(PID) + if not ogui_windows.is_empty(): + overlay_window_id = ogui_windows[0] + # Tell gamescope that we're an overlay if overlay_window_id <= 0: logger.error("Unable to detect Window ID. Overlay is not going to work!") @@ -47,10 +55,10 @@ func _setup(window_id: int) -> void: return # Pretend to be Steam # Gamescope is hard-coded to look for appId 769 - if gamescope.set_main_app(window_id) != OK: + if _xwayland_primary.set_main_app(window_id) != OK: logger.error("Unable to set STEAM_GAME atom!") # Sets ourselves to the input focus - if gamescope.set_input_focus(window_id, 1) != OK: + if _xwayland_primary.set_input_focus(window_id, 1) != OK: logger.error("Unable to set STEAM_INPUT_FOCUS atom!") @@ -112,18 +120,20 @@ func _ready() -> void: library_manager.reload_library() # Set the initial intercept mode - input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.ALL) - var on_device_changed := func(device: InputPlumber.CompositeDevice): - var intercept_mode : InputPlumber.INTERCEPT_MODE = input_plumber.intercept_mode_current - logger.debug("Setting intercept mode to: " + str(intercept_mode)) - input_plumber.set_intercept_mode_single(intercept_mode, device) - input_plumber.composite_device_changed.connect(on_device_changed) + input_plumber.set_intercept_mode(InputPlumberInstance.INTERCEPT_MODE_ALL) + #var on_device_changed := func(device: CompositeDevice): + # var intercept_mode := input_plumber.intercept_mode + # logger.debug("Setting intercept mode to: " + str(intercept_mode)) + # device.intercept_mode = intercept_mode + ## TODO: Do we still need this..? + #input_plumber.composite_device_changed.connect(on_device_changed) # Set the theme if one was set var theme_path := settings_manager.get_value("general", "theme", "res://assets/themes/card_ui-dracula.tres") as String logger.debug("Setting theme to: " + theme_path) var loaded_theme = load(theme_path) if loaded_theme != null: + @warning_ignore("unsafe_call_argument") set_theme(loaded_theme) else: logger.debug("Unable to load theme") @@ -137,13 +147,13 @@ func _on_focus_changed(control: Control) -> void: ## Invoked when the in-game state was entered func _on_game_state_entered(_from: State) -> void: # Pass all gamepad input to the game - input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.PASS) + input_plumber.set_intercept_mode(InputPlumberInstance.INTERCEPT_MODE_PASS) # Turn off gamescope blur effect - _set_blur(gamescope.BLUR_MODE.OFF) + _set_blur(GamescopeXWayland.BLUR_MODE_OFF) # Set gamescope input focus to off so the user can interact with the game - if gamescope.set_input_focus(overlay_window_id, 0) != OK: + if _xwayland_ogui and _xwayland_ogui.set_input_focus(overlay_window_id, 0) != OK: logger.error("Unable to set STEAM_INPUT_FOCUS atom!") # Ensure panel is invisible @@ -160,7 +170,7 @@ func _on_game_state_entered(_from: State) -> void: ## Invoked when the in-game state is exited func _on_game_state_exited(to: State) -> void: # Intercept all gamepad input when not in-game - input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.ALL) + input_plumber.set_intercept_mode(InputPlumberInstance.INTERCEPT_MODE_ALL) # Revert back to the default gamepad profile #gamepad_manager.set_gamepads_profile(null) @@ -171,16 +181,18 @@ func _on_game_state_exited(to: State) -> void: if current_popup == osk_state: return - if gamescope.set_input_focus(overlay_window_id, 1) != OK: - logger.error("Unable to set STEAM_INPUT_FOCUS atom!") + if _xwayland_primary: + if _xwayland_primary.set_input_focus(overlay_window_id, 1) != OK: + logger.error("Unable to set STEAM_INPUT_FOCUS atom!") # If the in-game state still exists in the stack, set the blur state. if state_machine.has_state(in_game_state): panel.visible = false # Only blur if the focused GFX app is set - var should_blur := settings_manager.get_value("display", "enable_overlay_blur", true) as bool - if should_blur and gamescope.get_focused_app_gfx() != Gamescope.OVERLAY_GAME_ID: - _set_blur(gamescope.BLUR_MODE.ALWAYS) + if _xwayland_primary: + var should_blur := settings_manager.get_value("display", "enable_overlay_blur", true) as bool + if should_blur and _xwayland_primary.get_focused_app_gfx() != gamescope.OVERLAY_GAME_ID: + _set_blur(GamescopeXWayland.BLUR_MODE_ALWAYS) else: _on_game_state_removed() @@ -195,7 +207,7 @@ func _on_game_state_exited(to: State) -> void: ## Invoked when the in-game state is removed func _on_game_state_removed() -> void: # Turn off gamescope blur - _set_blur(gamescope.BLUR_MODE.OFF) + _set_blur(GamescopeXWayland.BLUR_MODE_OFF) # Un-hide the background panel panel.visible = true @@ -208,12 +220,12 @@ func _on_game_state_removed() -> void: # Sets the blur mode in gamescope -func _set_blur(mode: Gamescope.BLUR_MODE) -> void: +func _set_blur(mode: int) -> void: # Sometimes setting this may fail when Steam closes. Retry several times. for try in range(10): - if gamescope.set_blur_mode(mode) != OK: - logger.warn("Unable to set blur mode atom!") - var current := gamescope.get_blur_mode() + if _xwayland_primary: + _xwayland_primary.set_blur_mode(mode) + var current := _xwayland_primary.get_blur_mode() if mode == current: break logger.warn("Retrying in " + str(try) + "ms") @@ -255,7 +267,7 @@ func _input(event: InputEvent) -> void: if not power_timer.is_stopped(): logger.info("Received suspend signal") for connection in power_timer.timeout.get_connections(): - power_timer.timeout.disconnect(connection["callable"]) + power_timer.timeout.disconnect(connection["callable"] as Callable) power_timer.stop() var output: Array = [] if OS.execute("systemctl", ["suspend"], output) != OK: @@ -263,12 +275,12 @@ func _input(event: InputEvent) -> void: # Removes specified child elements from the given Node. -func _remove_children(remove_list: PackedStringArray, parent:Node) -> void: +func _remove_children(remove_list: PackedStringArray, parent: Node) -> void: var child_count := parent.get_child_count() - var to_remove_list := [] + var to_remove_list: Array[Node] for child_idx in child_count: - var child = parent.get_child(child_idx) + var child := parent.get_child(child_idx) logger.trace("Checking if " + child.name + " in remove list...") if child.name in remove_list: logger.trace(child.name + " queued for removal!") diff --git a/core/ui/card_ui/card_ui.tscn b/core/ui/card_ui/card_ui.tscn index 80c61b45..0af538e6 100644 --- a/core/ui/card_ui/card_ui.tscn +++ b/core/ui/card_ui/card_ui.tscn @@ -1,23 +1,29 @@ -[gd_scene load_steps=38 format=3 uid="uid://fhriwlhm0lcj"] +[gd_scene load_steps=44 format=3 uid="uid://fhriwlhm0lcj"] [ext_resource type="PackedScene" uid="uid://n83wlhmmsu3j" path="res://core/systems/input/input_manager.tscn" id="1_34t85"] [ext_resource type="Script" path="res://core/ui/card_ui/card_ui.gd" id="1_f8851"] [ext_resource type="PackedScene" uid="uid://dlegwm7jqfe2i" path="res://core/systems/boxart/boxart_local.tscn" id="2_600i0"] [ext_resource type="PackedScene" uid="uid://ch6qw6obetalo" path="res://core/systems/library/library_desktop.tscn" id="3_68bes"] [ext_resource type="Script" path="res://core/systems/input/input_icon_processor.gd" id="3_y116l"] +[ext_resource type="Script" path="res://core/systems/gamescope/gamescope.gd" id="4_ksi1t"] [ext_resource type="PackedScene" uid="uid://cbboox5bujlx1" path="res://core/systems/launcher/launch_manager.tscn" id="4_tgw75"] [ext_resource type="PackedScene" uid="uid://uam46dtvo2yh" path="res://core/systems/plugin/plugin_manager.tscn" id="5_dv70s"] +[ext_resource type="GamescopeInstance" uid="uid://chd0nc6gbfnw0" path="res://core/systems/gamescope/gamescope.tres" id="5_wmkau"] [ext_resource type="PackedScene" uid="uid://o70x5igrlq30" path="res://core/ui/card_ui/home/cardui_home.tscn" id="6_12lhu"] [ext_resource type="PackedScene" uid="uid://dnq5j20fbcrwx" path="res://core/systems/power/power_saver.tscn" id="8_hyc1j"] [ext_resource type="PackedScene" uid="uid://bcdk1lj6enq3l" path="res://core/ui/card_ui/launch/game_launch_menu.tscn" id="9_m34in"] [ext_resource type="PackedScene" uid="uid://d2jiecrd5sw4s" path="res://core/ui/card_ui/settings/settings_menu.tscn" id="10_1ruvi"] [ext_resource type="PackedScene" uid="uid://d4bmkauhrlhq0" path="res://core/ui/card_ui/navigation/search_bar_menu.tscn" id="10_7bl6b"] [ext_resource type="PackedScene" uid="uid://58qlqqbh58im" path="res://core/ui/card_ui/launch/game_settings.tscn" id="10_7cj6o"] +[ext_resource type="Script" path="res://core/systems/power/power_manager.gd" id="10_opsp8"] [ext_resource type="PackedScene" uid="uid://uqkwpeq7f1o" path="res://core/ui/card_ui/library/library_menu.tscn" id="10_uqodp"] +[ext_resource type="UPowerInstance" uid="uid://pgiv8yxro3n8" path="res://core/systems/power/power_manager.tres" id="11_nk5v7"] [ext_resource type="PackedScene" uid="uid://by0i08fw1fwty" path="res://core/ui/card_ui/navigation/top_button_menu.tscn" id="11_x7ns7"] [ext_resource type="PackedScene" uid="uid://cd2p3lu01aric" path="res://core/ui/card_ui/navigation/context_bar_menu.tscn" id="12_bstc8"] [ext_resource type="PackedScene" uid="uid://jfacx7uys32r" path="res://core/ui/card_ui/main-menu/main_menu.tscn" id="13_46tck"] [ext_resource type="PackedScene" uid="uid://cwarv58ju0sow" path="res://core/ui/card_ui/gamepad/gamepad_settings.tscn" id="13_n64ve"] +[ext_resource type="Script" path="res://core/systems/performance/power_station.gd" id="13_tag7s"] +[ext_resource type="PowerStationInstance" uid="uid://c2mmrnh3rcs58" path="res://core/systems/performance/power_station.tres" id="14_a5vk3"] [ext_resource type="PackedScene" uid="uid://b30stcxjwk3od" path="res://core/ui/card_ui/ootbe/first_boot_menu.tscn" id="14_dpfu1"] [ext_resource type="PackedScene" uid="uid://hroo3ll4inrb" path="res://core/ui/card_ui/quick_bar/quick_bar_menu.tscn" id="14_lsaok"] [ext_resource type="PackedScene" uid="uid://dj1fooc3gh13l" path="res://core/ui/card_ui/help/help_menu.tscn" id="15_m1wp2"] @@ -89,6 +95,10 @@ script = ExtResource("1_f8851") [node name="InputIconProcessor" type="Node" parent="."] script = ExtResource("3_y116l") +[node name="Gamescope" type="Node" parent="."] +script = ExtResource("4_ksi1t") +instance = ExtResource("5_wmkau") + [node name="BoxArtLocal" parent="." instance=ExtResource("2_600i0")] [node name="DesktopLibrary" parent="." instance=ExtResource("3_68bes")] @@ -97,8 +107,16 @@ script = ExtResource("3_y116l") [node name="PluginManager" parent="." instance=ExtResource("5_dv70s")] +[node name="PowerManager" type="Node" parent="."] +script = ExtResource("10_opsp8") +instance = ExtResource("11_nk5v7") + [node name="PowerSaver" parent="." instance=ExtResource("8_hyc1j")] +[node name="PowerStation" type="Node" parent="."] +script = ExtResource("13_tag7s") +instance = ExtResource("14_a5vk3") + [node name="PowerTimer" type="Timer" parent="."] unique_name_in_owner = true wait_time = 1.5 diff --git a/core/ui/card_ui/gamepad/gamepad_settings.gd b/core/ui/card_ui/gamepad/gamepad_settings.gd index d5dbdab0..14882219 100644 --- a/core/ui/card_ui/gamepad/gamepad_settings.gd +++ b/core/ui/card_ui/gamepad/gamepad_settings.gd @@ -12,13 +12,13 @@ var notification_manager := load("res://core/global/notification_manager.tres") var settings_manager := load("res://core/global/settings_manager.tres") as SettingsManager var global_state_machine := load("res://assets/state/state_machines/menu_state_machine.tres") as StateMachine var state_machine := load("res://assets/state/state_machines/gamepad_settings_state_machine.tres") as StateMachine -var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumber +var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance var input_icons := load("res://core/systems/input/input_icon_manager.tres") as InputIconManager var button_scene := load("res://core/ui/components/card_mapping_button.tscn") as PackedScene var container_scene := load("res://core/ui/components/card_mapping_button_group.tscn") as PackedScene var expandable_scene := load("res://core/ui/card_ui/quick_bar/qb_card.tscn") as PackedScene -var gamepad: InputPlumber.CompositeDevice +var gamepad: CompositeDevice var profile: InputPlumberProfile var profile_gamepad: String var library_item: LibraryItem @@ -114,7 +114,7 @@ func _on_state_entered(_from: State) -> void: var dbus_path := gamepad_state.get_meta("dbus_path") as String # Find the composite device to configure - for device: InputPlumber.CompositeDevice in input_plumber.composite_devices: + for device: CompositeDevice in input_plumber.composite_devices: if device.dbus_path == dbus_path: gamepad = device break @@ -196,7 +196,7 @@ func _on_state_exited(_to: State) -> void: ## Populates the button mappings for the given gamepad -func populate_mappings_for(gamepad: InputPlumber.CompositeDevice) -> void: +func populate_mappings_for(gamepad: CompositeDevice) -> void: var gamepad_name := gamepad.name var capabilities := gamepad.capabilities @@ -595,7 +595,7 @@ func get_target_gamepad_text(gamepad_type: InputPlumberProfile.TargetDevice) -> # Set the given profile for the given composte device. -func _set_gamepad_profile(gamepad: InputPlumber.CompositeDevice, profile_path: String = "") -> void: +func _set_gamepad_profile(gamepad: CompositeDevice, profile_path: String = "") -> void: if profile_path == "": if gamepad_state.has_meta("item"): library_item = gamepad_state.get_meta("item") as LibraryItem diff --git a/core/ui/card_ui_overlay_mode/card_ui_overlay_mode.gd b/core/ui/card_ui_overlay_mode/card_ui_overlay_mode.gd index c83c7c43..7b1dc9b9 100644 --- a/core/ui/card_ui_overlay_mode/card_ui_overlay_mode.gd +++ b/core/ui/card_ui_overlay_mode/card_ui_overlay_mode.gd @@ -4,7 +4,7 @@ var platform := preload("res://core/global/platform.tres") as Platform var gamescope := preload("res://core/global/gamescope.tres") as Gamescope var launch_manager := preload("res://core/global/launch_manager.tres") as LaunchManager var settings_manager := preload("res://core/global/settings_manager.tres") as SettingsManager -var input_plumber := preload("res://core/systems/input/input_plumber.tres") as InputPlumber +var input_plumber := preload("res://core/systems/input/input_plumber.tres") as InputPlumberInstance var state_machine := ( preload("res://assets/state/state_machines/global_state_machine.tres") as StateMachine ) @@ -160,15 +160,15 @@ func _setup_overlay_mode(args: Array) -> void: _remove_children(settings_remove_list, settings_menu) # Setup inputplumber to receive guide presses. - input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.PASS) + input_plumber.set_intercept_mode(InputPlumberInstance.INTERCEPT_MODE_PASS) input_plumber.set_intercept_activation(["Gamepad:Button:Guide", "Gamepad:Button:East"], "Gamepad:Button:QuickAccess2") # Sets the intercept mode and intercept activation keys to what overlay_mode expects. - var on_device_changed := func(device: InputPlumber.CompositeDevice): - var intercept_mode : InputPlumber.INTERCEPT_MODE = input_plumber.intercept_mode_current + var on_device_changed := func(device: CompositeDevice): + var intercept_mode := input_plumber.intercept_mode logger.debug("Setting intercept mode to: " + str(intercept_mode)) - input_plumber.set_intercept_mode_single(intercept_mode, device) - input_plumber.set_intercept_activation_single(["Gamepad:Button:Guide", "Gamepad:Button:East"], "Gamepad:Button:QuickAccess2", device) + device.intercept_mode = intercept_mode + device.set_intercept_activation(["Gamepad:Button:Guide", "Gamepad:Button:East"], "Gamepad:Button:QuickAccess2") input_plumber.composite_device_changed.connect(on_device_changed) @@ -258,7 +258,7 @@ func _find_underlay_window_id() -> void: ## Called when the base state is entered. func _on_base_state_entered(from: State) -> void: # Manage input focus - input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.PASS) + input_plumber.set_intercept_mode(InputPlumberInstance.INTERCEPT_MODE_PASS) if gamescope.set_input_focus(overlay_window_id, 0) != OK: logger.error("Unable to set STEAM_INPUT_FOCUS atom!") @@ -270,7 +270,7 @@ func _on_base_state_entered(from: State) -> void: ## Called when a the base state is exited. func _on_base_state_exited(to: State) -> void: # Manage input focus - input_plumber.set_intercept_mode(InputPlumber.INTERCEPT_MODE.ALL) + input_plumber.set_intercept_mode(InputPlumberInstance.INTERCEPT_MODE_ALL) if gamescope.set_input_focus(overlay_window_id, 1) != OK: logger.error("Unable to set STEAM_INPUT_FOCUS atom!") diff --git a/core/ui/components/battery_container.gd b/core/ui/components/battery_container.gd index 463bb8d0..109e8a42 100644 --- a/core/ui/components/battery_container.gd +++ b/core/ui/components/battery_container.gd @@ -7,8 +7,8 @@ const icon_half = preload("res://assets/ui/icons/battery-half.svg") const icon_low = preload("res://assets/ui/icons/battery-low.svg") const icon_empty = preload("res://assets/ui/icons/battery-empty.svg") -var power_manager := load("res://core/systems/power/power_manager.tres") as PowerManager -var batteries : Array[PowerManager.Device] +var power_manager := load("res://core/systems/power/power_manager.tres") as UPowerInstance +var display_device := power_manager.get_display_device() var logger := Log.get_logger("BatteryContainer", Log.LEVEL.INFO) @@ -17,20 +17,16 @@ var logger := Log.get_logger("BatteryContainer", Log.LEVEL.INFO) func _ready(): - batteries = power_manager.get_devices_by_type(PowerManager.DEVICE_TYPE.BATTERY) - if batteries.size() > 1: - logger.warn("You somehow have more than one battery. We don't know what to do with that.") - if batteries.size() == 0: + if not display_device: logger.debug("No battery detected. nothing to do.") visible = false return - var battery := batteries[0] - _on_update_device(battery) - battery.updated.connect(_on_update_device.bind(battery)) + _on_update_device(display_device) + display_device.updated.connect(_on_update_device.bind(display_device)) -func _on_update_device(item: PowerManager.Device): +func _on_update_device(item: UPowerDevice): var capacity := item.percentage var state := item.state battery_icon.texture = get_capacity_texture(capacity, state) @@ -42,8 +38,8 @@ func _on_update_device(item: PowerManager.Device): ## Returns the texture reflecting the given battery capacity -static func get_capacity_texture(capacity: int, state: PowerManager.DEVICE_STATE) -> Texture2D: - if state in [PowerManager.DEVICE_STATE.CHARGING, PowerManager.DEVICE_STATE.FULLY_CHARGED]: +static func get_capacity_texture(capacity: int, state: int) -> Texture2D: + if state in [UPowerDevice.STATE_CHARGING, UPowerDevice.STATE_FULLY_CHARGED]: return icon_charging if capacity >= 90: return icon_full diff --git a/gdext/.gdignore b/extensions/.gdignore similarity index 100% rename from gdext/.gdignore rename to extensions/.gdignore diff --git a/extensions/core/.gitignore b/extensions/core/.gitignore new file mode 100644 index 00000000..54b02da4 --- /dev/null +++ b/extensions/core/.gitignore @@ -0,0 +1,2 @@ +/target +*.so diff --git a/extensions/core/Cargo.lock b/extensions/core/Cargo.lock new file mode 100644 index 00000000..9af6fe5a --- /dev/null +++ b/extensions/core/Cargo.lock @@ -0,0 +1,1582 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "async-broadcast" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a07789659a4d385b79b18b9127fc27e1a59e1e89117c78c5ea3b806f016374" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cc" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cpufeatures" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gamescope-x11-client" +version = "0.1.0" +source = "git+https://github.com/ShadowBlip/gamescope-x11-client?branch=main#3a0cbe64ba60dffb5ad85f156101c056f764e659" +dependencies = [ + "log", + "strum", + "strum_macros", + "x11rb", +] + +[[package]] +name = "gdextension-api" +version = "0.2.0" +source = "git+https://github.com/godot-rust/godot4-prebuilt?branch=releases#6d902e8a6060007f4ab94cd78882247ae2558d96" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gensym" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913dce4c5f06c2ea40fc178c06f777ac89fc6b1383e90c254fafb1abe4ba3c82" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "uuid", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "glam" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94" + +[[package]] +name = "godot" +version = "0.1.3" +source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1" +dependencies = [ + "godot-core", + "godot-macros", +] + +[[package]] +name = "godot-bindings" +version = "0.1.3" +source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1" +dependencies = [ + "gdextension-api", +] + +[[package]] +name = "godot-cell" +version = "0.1.3" +source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1" + +[[package]] +name = "godot-codegen" +version = "0.1.3" +source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1" +dependencies = [ + "godot-bindings", + "heck 0.5.0", + "nanoserde", + "proc-macro2", + "quote", + "regex", +] + +[[package]] +name = "godot-core" +version = "0.1.3" +source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1" +dependencies = [ + "glam", + "godot-bindings", + "godot-cell", + "godot-codegen", + "godot-ffi", +] + +[[package]] +name = "godot-ffi" +version = "0.1.3" +source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1" +dependencies = [ + "gensym", + "godot-bindings", + "godot-codegen", + "libc", + "paste", +] + +[[package]] +name = "godot-macros" +version = "0.1.3" +source = "git+https://github.com/godot-rust/gdext?branch=master#406a5d9b6c44b42b4134251c9b732b4fdbb343a1" +dependencies = [ + "godot-bindings", + "markdown", + "proc-macro2", + "quote", + "venial", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags", + "futures-core", + "inotify-sys", + "libc", + "tokio", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "markdown" +version = "1.0.0-alpha.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911a8325e6fb87b89890cd4529a2ab34c2669c026279e61c26b7140a3d821ccb" +dependencies = [ + "unicode-id", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "nanoserde" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de9cf844ab1e25a0353525bd74cb889843a6215fa4a0d156fd446f4857a1b99" +dependencies = [ + "nanoserde-derive", +] + +[[package]] +name = "nanoserde-derive" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e943b2c21337b7e3ec6678500687cdc741b7639ad457f234693352075c082204" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "object" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opengamepadui-core" +version = "0.1.0" +dependencies = [ + "futures-util", + "gamescope-x11-client", + "godot", + "inotify", + "nix", + "once_cell", + "tokio", + "zbus", + "zvariant", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.208" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.208" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio" +version = "1.39.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-id" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", +] + +[[package]] +name = "venial" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6816bc32f30bf8dd1b3adb04de8406c7bf187d2f923bd9e4c0b99365d012613f" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/extensions/core/Cargo.toml b/extensions/core/Cargo.toml new file mode 100644 index 00000000..1af21b54 --- /dev/null +++ b/extensions/core/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "opengamepadui-core" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] # Compile this crate to a dynamic C library. + +[dependencies] +futures-util = "0.3.30" +godot = { git = "https://github.com/godot-rust/gdext", branch = "master", features = [ + "experimental-threads", + "register-docs", +] } +nix = { version = "0.29.0", features = ["term", "process"] } +once_cell = "1.19.0" +tokio = { version = "1.39.3", features = ["full"] } +zbus = "4.4.0" +zvariant = "4.2.0" +gamescope-x11-client = { git = "https://github.com/ShadowBlip/gamescope-x11-client", branch = "main" } +inotify = "0.11.0" diff --git a/extensions/core/Makefile b/extensions/core/Makefile new file mode 100644 index 00000000..917abf11 --- /dev/null +++ b/extensions/core/Makefile @@ -0,0 +1,50 @@ +PREFIX ?= addons +EXT_NAME := $(shell grep 'name =' Cargo.toml | head -n 1 | cut -d'"' -f2) +LIB_NAME := $(shell grep 'name =' Cargo.toml | head -n 1 | cut -d'"' -f2 | sed 's/-/_/g') +ALL_RS := $(shell find ./src -name '*.rs') +ADDON_PATH := ../../addons/core +RELEASE_TARGET := $(ADDON_PATH)/bin/lib$(EXT_NAME).linux.template_release.x86_64.so +DEBUG_TARGET := $(ADDON_PATH)/bin/lib$(EXT_NAME).linux.template_debug.x86_64.so + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + + +.PHONY: build +build: $(RELEASE_TARGET) $(DEBUG_TARGET) ## Build release and debug binaries + + +.PHONY: clean +clean: ## Clean build artifacts + rm $(RELEASE_TARGET) $(DEBUG_TARGET) + rm -rf target + + +.PHONY: release +release: $(RELEASE_TARGET) ## Build release binary +$(RELEASE_TARGET): $(ALL_RS) + cargo build --release + mkdir -p $(@D) + cp target/release/lib$(LIB_NAME).so $@ + + +.PHONY: debug +debug: $(DEBUG_TARGET) ## Build binary with debug symbols +$(DEBUG_TARGET): $(ALL_RS) + cargo build + mkdir -p $(@D) + cp target/debug/lib$(LIB_NAME).so $@ diff --git a/extensions/core/src/bluetooth.rs b/extensions/core/src/bluetooth.rs new file mode 100644 index 00000000..5447862b --- /dev/null +++ b/extensions/core/src/bluetooth.rs @@ -0,0 +1 @@ +pub mod bluez; diff --git a/extensions/core/src/bluetooth/bluez.rs b/extensions/core/src/bluetooth/bluez.rs new file mode 100644 index 00000000..aaf35116 --- /dev/null +++ b/extensions/core/src/bluetooth/bluez.rs @@ -0,0 +1,379 @@ +pub mod adapter; +pub mod device; + +use std::{ + collections::HashMap, + error::Error, + sync::mpsc::{channel, Receiver, Sender, TryRecvError}, + time::Duration, +}; + +use adapter::BluetoothAdapter; +use device::BluetoothDevice; +use futures_util::stream::StreamExt; +use godot::{obj::WithBaseField, prelude::*}; +use zbus::fdo::ObjectManagerProxy; +use zbus::{fdo::ManagedObjects, names::BusName}; + +use crate::{ + dbus::bluez::adapter1::Adapter1ProxyBlocking, get_dbus_system, get_dbus_system_blocking, + RUNTIME, +}; + +pub const BLUEZ_BUS: &str = "org.bluez"; +const BLUEZ_PATH: &str = "/org/bluez"; +const BLUEZ_MANAGER_PATH: &str = "/"; + +/// Supported Bluez DBus objects +#[derive(Debug)] +enum ObjectType { + Unknown, + Adapter, + Device, +} + +impl ObjectType { + /// Returns the object type from the list of implemented interfaces + fn from_ifaces(ifaces: Vec) -> Self { + if ifaces.contains(&"org.bluez.Device1".to_string()) { + Self::Device + } else if ifaces.contains(&"org.bluez.Adapter1".to_string()) { + Self::Adapter + } else { + Self::Unknown + } + } +} + +/// Signals that can be emitted +#[derive(Debug)] +enum Signal { + Started, + Stopped, + ObjectAdded { path: String, ifaces: Vec }, + ObjectRemoved { path: String, ifaces: Vec }, +} + +#[derive(GodotClass)] +#[class(base=Resource)] +pub struct BluezInstance { + base: Base, + rx: Receiver, + conn: Option, + adapters: HashMap>, + devices: HashMap>, +} + +#[godot_api] +impl BluezInstance { + /// Emitted when Bluez is detected as running + #[signal] + fn started(); + + /// Emitted when Bluez is detected as stopped + #[signal] + fn stopped(); + + /// Emitted when a new bluetooth adapter is discovered + #[signal] + fn adapter_added(device: Gd); + + /// Emitted when a bluetooth adapter is removed + #[signal] + fn adapter_removed(path: GString); + + /// Emitted when a new bluetooth device is discovered + #[signal] + fn device_added(device: Gd); + + /// Emitted when a bluetooth device is removed + #[signal] + fn device_removed(path: GString); + + /// Returns true if the Bluez service is currently running + #[func] + fn is_running(&self) -> bool { + let Some(conn) = self.conn.as_ref() else { + return false; + }; + let bus = BusName::from_static_str(BLUEZ_BUS).unwrap(); + let dbus = zbus::blocking::fdo::DBusProxy::new(conn).ok(); + let Some(dbus) = dbus else { + return false; + }; + dbus.name_has_owner(bus.clone()).unwrap_or_default() + } + + /// Get managed objects + fn get_managed_objects(&self) -> Result { + let Some(conn) = self.conn.as_ref() else { + return Err(zbus::fdo::Error::Disconnected( + "No DBus connection found".into(), + )); + }; + + let bus = BusName::from_static_str(BLUEZ_BUS).unwrap(); + let object_manager = zbus::blocking::fdo::ObjectManagerProxy::builder(conn) + .destination(bus) + .ok() + .and_then(|builder| builder.path(BLUEZ_MANAGER_PATH).ok()) + .and_then(|builder| builder.build().ok()); + let Some(object_manager) = object_manager else { + return Ok(ManagedObjects::new()); + }; + + object_manager.get_managed_objects() + } + + /// Return a list of currently discovered bluetooth adapters + #[func] + fn get_adapters(&self) -> Array> { + let mut adapters = array![]; + for adapter in self.adapters.values() { + adapters.push(adapter.clone()); + } + + adapters + } + + /// Return a list of currently discovered devices + #[func] + fn get_discovered_devices(&self) -> Array> { + let mut devices = array![]; + for device in self.devices.values() { + devices.push(device.clone()); + } + + devices + } + + /// Process Bluez signals and emit them as Godot signals. This method + /// should be called every frame in the "_process" loop of a node. + #[func] + fn process(&mut self) { + // Drain all messages from the channel to process them + loop { + let signal = match self.rx.try_recv() { + Ok(value) => value, + Err(e) => match e { + TryRecvError::Empty => break, + TryRecvError::Disconnected => { + godot_error!("Backend thread is not running!"); + return; + } + }, + }; + self.process_signal(signal); + } + + // Process signals on child objects + for adapter in self.adapters.values_mut() { + adapter.bind_mut().process(); + } + for device in self.devices.values_mut() { + device.bind_mut().process(); + } + } + + /// Process and dispatch the given signal + fn process_signal(&mut self, signal: Signal) { + match signal { + Signal::Started => { + self.base_mut().emit_signal("started".into(), &[]); + } + Signal::Stopped => { + self.base_mut().emit_signal("stopped".into(), &[]); + } + Signal::ObjectAdded { path, ifaces } => { + let obj_type = ObjectType::from_ifaces(ifaces); + match obj_type { + ObjectType::Unknown => (), + ObjectType::Adapter => { + let adapter = BluetoothAdapter::new(path.as_str()); + self.adapters.insert(path, adapter.clone()); + self.base_mut() + .emit_signal("adapter_added".into(), &[adapter.to_variant()]); + } + ObjectType::Device => { + let device = BluetoothDevice::new(path.as_str()); + self.devices.insert(path, device.clone()); + self.base_mut() + .emit_signal("device_added".into(), &[device.to_variant()]); + } + } + } + Signal::ObjectRemoved { path, ifaces } => { + let obj_type = ObjectType::from_ifaces(ifaces); + match obj_type { + ObjectType::Unknown => (), + ObjectType::Adapter => { + self.adapters.remove(&path); + self.base_mut() + .emit_signal("adapter_removed".into(), &[path.to_variant()]); + } + ObjectType::Device => { + self.devices.remove(&path); + self.base_mut() + .emit_signal("device_removed".into(), &[path.to_variant()]); + } + } + } + } + } +} + +#[godot_api] +impl IResource for BluezInstance { + /// Called upon object initialization in the engine + fn init(base: Base) -> Self { + godot_print!("Initializing Bluez instance"); + + // Create a channel to communicate with the service + let (tx, rx) = channel(); + + // Spawn a task using the shared tokio runtime to listen for signals + RUNTIME.spawn(async move { + if let Err(e) = run(tx).await { + godot_error!("Failed to run Bluez task: ${e:?}"); + } + }); + + // Create a new Bluez instance + let conn = get_dbus_system_blocking().ok(); + let mut instance = Self { + base, + rx, + conn, + adapters: HashMap::new(), + devices: HashMap::new(), + }; + + // Perform initial object discovery + let mut adapters = HashMap::new(); + let mut devices = HashMap::new(); + let objects = instance.get_managed_objects().unwrap_or_default(); + for (path, ifaces) in objects.into_iter() { + let path = path.to_string(); + let ifaces: Vec = ifaces.into_keys().map(|v| v.to_string()).collect(); + let obj_type = ObjectType::from_ifaces(ifaces); + + match obj_type { + ObjectType::Unknown => (), + ObjectType::Adapter => { + let adapter = BluetoothAdapter::new(path.as_str()); + adapters.insert(path, adapter); + } + ObjectType::Device => { + let device = BluetoothDevice::new(path.as_str()); + devices.insert(path, device); + } + } + } + + // Update the discovered objects + instance.adapters = adapters; + instance.devices = devices; + + instance + } +} + +/// Runs Bluez tasks in Tokio to listen for DBus signals and send them +/// over the given channel so they can be processed during each engine frame. +async fn run(tx: Sender) -> Result<(), Box> { + godot_print!("Spawning Bluez tasks"); + // Establish a connection to the system bus + let conn = get_dbus_system().await?; + + // Spawn a task to listen for Bluez start/stop + let dbus_conn = conn.clone(); + let signals_tx = tx.clone(); + RUNTIME.spawn(async move { + let bus = BusName::from_static_str(BLUEZ_BUS).unwrap(); + let mut is_running = { + let dbus = zbus::fdo::DBusProxy::new(&dbus_conn).await.ok(); + let Some(dbus) = dbus else { + return; + }; + dbus.name_has_owner(bus.clone()).await.unwrap_or_default() + }; + + loop { + let dbus = zbus::fdo::DBusProxy::new(&dbus_conn).await.ok(); + let Some(dbus) = dbus else { + break; + }; + let running = dbus.name_has_owner(bus.clone()).await.unwrap_or_default(); + if running != is_running { + let signal = if running { + Signal::Started + } else { + Signal::Stopped + }; + if signals_tx.send(signal).is_err() { + break; + } + } + is_running = running; + tokio::time::sleep(Duration::from_secs(5)).await; + } + }); + + // Get a proxy instance to ObjectManager + let bus = BusName::from_static_str(BLUEZ_BUS).unwrap(); + let object_manager: ObjectManagerProxy = ObjectManagerProxy::builder(&conn) + .destination(bus)? + .path(BLUEZ_MANAGER_PATH)? + .build() + .await?; + + // Spawn a task to listen for objects added + let mut ifaces_added = object_manager.receive_interfaces_added().await?; + let signals_tx = tx.clone(); + RUNTIME.spawn(async move { + while let Some(signal) = ifaces_added.next().await { + let args = match signal.args() { + Ok(args) => args, + Err(e) => { + godot_warn!("Failed to get signal args: ${e:?}"); + continue; + } + }; + + let path = args.object_path.to_string(); + let ifaces = args + .interfaces_and_properties + .keys() + .map(|v| v.to_string()) + .collect(); + let signal = Signal::ObjectAdded { path, ifaces }; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + // Spawn a task to listen for objects removed + let mut ifaces_removed = object_manager.receive_interfaces_removed().await?; + let signals_tx = tx.clone(); + RUNTIME.spawn(async move { + while let Some(signal) = ifaces_removed.next().await { + let args = match signal.args() { + Ok(args) => args, + Err(e) => { + godot_warn!("Failed to get signal args: ${e:?}"); + continue; + } + }; + + let path = args.object_path.to_string(); + let ifaces = args.interfaces.iter().map(|v| v.to_string()).collect(); + let signal = Signal::ObjectRemoved { path, ifaces }; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + Ok(()) +} diff --git a/extensions/core/src/bluetooth/bluez/adapter.rs b/extensions/core/src/bluetooth/bluez/adapter.rs new file mode 100644 index 00000000..aa15e026 --- /dev/null +++ b/extensions/core/src/bluetooth/bluez/adapter.rs @@ -0,0 +1,572 @@ +use std::error::Error; +use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError}; + +use futures_util::StreamExt; +use godot::obj::WithBaseField; +use godot::prelude::*; + +use godot::classes::{Resource, ResourceLoader}; +use zvariant::ObjectPath; + +use crate::dbus::bluez::adapter1::{Adapter1Proxy, Adapter1ProxyBlocking}; +use crate::{get_dbus_system, get_dbus_system_blocking, RUNTIME}; + +use super::device::BluetoothDevice; +use super::BLUEZ_BUS; + +/// Signals that can be emitted +#[derive(Debug)] +enum Signal { + Discoverable { value: bool }, + Discovering { value: bool }, + Pairable { value: bool }, + Powered { value: bool }, + PowerState { value: String }, +} + +#[derive(GodotClass)] +#[class(no_init, base=Resource)] +pub struct BluetoothAdapter { + base: Base, + rx: Receiver, + conn: Option, + + #[allow(dead_code)] + #[var(get = get_dbus_path)] + dbus_path: GString, + #[allow(dead_code)] + #[var(get = get_address)] + address: GString, + #[allow(dead_code)] + #[var(get = get_address_type)] + address_type: GString, + #[allow(dead_code)] + #[var(get = get_alias, set = set_alias)] + alias: GString, + #[allow(dead_code)] + #[var(get = get_class)] + class: u32, + #[allow(dead_code)] + #[var(get = get_discoverable, set = set_discoverable)] + discoverable: bool, + #[allow(dead_code)] + #[var(get = get_discoverable_timeout, set = set_discoverable_timeout)] + discoverable_timeout: u32, + #[allow(dead_code)] + #[var(get = get_discovering)] + discovering: bool, + #[allow(dead_code)] + #[var(get = get_experimental_features)] + experimental_features: PackedStringArray, + #[allow(dead_code)] + #[var(get = get_manufacturer)] + manufacturer: u16, + #[allow(dead_code)] + #[var(get = get_modalias)] + modalias: GString, + #[allow(dead_code)] + #[var(get = get_name)] + name: GString, + #[allow(dead_code)] + #[var(get = get_pairable, set = set_pairable)] + pairable: bool, + #[allow(dead_code)] + #[var(get = get_pairable_timeout, set = set_pairable_timeout)] + pairable_timeout: u32, + #[allow(dead_code)] + #[var(get = get_power_state)] + power_state: GString, + #[allow(dead_code)] + #[var(get = get_powered, set = set_powered)] + powered: bool, + #[allow(dead_code)] + #[var(get = get_roles)] + roles: PackedStringArray, + #[allow(dead_code)] + #[var(get = get_uuids)] + uuids: PackedStringArray, + #[allow(dead_code)] + #[var(get = get_version)] + version: u8, +} + +#[godot_api] +impl BluetoothAdapter { + #[signal] + fn discoverable_changed(value: bool); + + #[signal] + fn discovering_changed(value: bool); + + #[signal] + fn pairable_changed(value: bool); + + #[signal] + fn powered_changed(value: bool); + + #[signal] + fn power_state_changed(value: GString); + + /// Create a new [BluetoothAdapter] with the given DBus path + pub fn from_path(path: GString) -> Gd { + // Create a channel to communicate with the signals task + godot_print!("BluetoothAdapter created with path: {path}"); + let (tx, rx) = channel(); + let dbus_path = path.clone().into(); + + // Spawn a task using the shared tokio runtime to listen for signals + RUNTIME.spawn(async move { + if let Err(e) = run(tx, dbus_path).await { + godot_error!("Failed to run DBusDevice task: ${e:?}"); + } + }); + + Gd::from_init_fn(|base| { + // Create a connection to DBus + let conn = get_dbus_system_blocking().ok(); + + // Accept a base of type Base and directly forward it. + Self { + base, + rx, + conn, + dbus_path: path, + address: Default::default(), + address_type: Default::default(), + alias: Default::default(), + class: Default::default(), + discoverable: Default::default(), + discoverable_timeout: Default::default(), + discovering: Default::default(), + experimental_features: Default::default(), + manufacturer: Default::default(), + modalias: Default::default(), + name: Default::default(), + pairable: Default::default(), + pairable_timeout: Default::default(), + power_state: Default::default(), + powered: Default::default(), + roles: Default::default(), + uuids: Default::default(), + version: Default::default(), + } + }) + } + + /// Return a proxy instance to the adapter + fn get_proxy(&self) -> Option { + if let Some(conn) = self.conn.as_ref() { + let path: String = self.dbus_path.clone().into(); + Adapter1ProxyBlocking::builder(conn) + .path(path) + .ok() + .and_then(|builder| builder.build().ok()) + } else { + None + } + } + + /// Get or create a [BluetoothAdapter] with the given DBus path. If an instance + /// already exists with the given path, then it will be loaded from the resource + /// cache. + pub fn new(path: &str) -> Gd { + let res_path = format!("dbus://{BLUEZ_BUS}{path}"); + + // Check to see if a resource already exists for this device + let mut resource_loader = ResourceLoader::singleton(); + if resource_loader.exists(res_path.clone().into()) { + if let Some(res) = resource_loader.load(res_path.clone().into()) { + godot_print!( + "Resource already exists with path '{res_path}', loading that instead" + ); + let device: Gd = res.cast(); + device + } else { + let mut device = BluetoothAdapter::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } else { + let mut device = BluetoothAdapter::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } + + /// Returns the DBus path to the [BluetoothAdapter] + #[func] + pub fn get_dbus_path(&self) -> GString { + self.dbus_path.clone() + } + + #[func] + pub fn get_address(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.address().unwrap_or_default().into() + } + + #[func] + pub fn get_address_type(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.address_type().unwrap_or_default().into() + } + + #[func] + pub fn get_alias(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.alias().unwrap_or_default().into() + } + + #[func] + pub fn set_alias(&self, value: GString) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy + .set_alias(value.to_string().as_str()) + .unwrap_or_default() + } + + #[func] + pub fn get_class(&self) -> u32 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.class().unwrap_or_default() + } + + #[func] + pub fn get_discoverable(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.discoverable().unwrap_or_default() + } + + #[func] + pub fn set_discoverable(&self, value: bool) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_discoverable(value).unwrap_or_default() + } + + #[func] + pub fn get_discoverable_timeout(&self) -> u32 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.discoverable_timeout().unwrap_or_default() + } + + #[func] + pub fn set_discoverable_timeout(&self, value: u32) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_discoverable_timeout(value).unwrap_or_default() + } + + #[func] + pub fn get_discovering(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.discovering().unwrap_or_default() + } + + #[func] + pub fn get_experimental_features(&self) -> PackedStringArray { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + let values: Vec = proxy + .experimental_features() + .unwrap_or_default() + .into_iter() + .map(|v| v.to_godot()) + .collect(); + values.into() + } + + #[func] + pub fn get_manufacturer(&self) -> u16 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.manufacturer().unwrap_or_default() + } + + #[func] + pub fn get_modalias(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.modalias().unwrap_or_default().into() + } + + #[func] + pub fn get_name(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.name().unwrap_or_default().into() + } + + #[func] + pub fn get_pairable(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.pairable().unwrap_or_default() + } + + #[func] + pub fn set_pairable(&self, value: bool) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_pairable(value).unwrap_or_default() + } + + #[func] + pub fn get_pairable_timeout(&self) -> u32 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.pairable_timeout().unwrap_or_default() + } + + #[func] + pub fn set_pairable_timeout(&self, value: u32) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_pairable_timeout(value).unwrap_or_default() + } + + #[func] + pub fn get_power_state(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.power_state().unwrap_or_default().into() + } + + #[func] + pub fn get_powered(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.powered().unwrap_or_default() + } + + #[func] + pub fn set_powered(&self, value: bool) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_powered(value).unwrap_or_default() + } + + #[func] + pub fn get_roles(&self) -> PackedStringArray { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + let values: Vec = proxy + .roles() + .unwrap_or_default() + .into_iter() + .map(|v| v.to_godot()) + .collect(); + values.into() + } + + #[func] + pub fn get_uuids(&self) -> PackedStringArray { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + let values: Vec = proxy + .uuids() + .unwrap_or_default() + .into_iter() + .map(|v| v.to_godot()) + .collect(); + values.into() + } + + #[func] + pub fn get_version(&self) -> u8 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.version().unwrap_or_default() + } + + #[func] + pub fn get_discovery_filters(&self) -> PackedStringArray { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + let filters: Vec = proxy + .get_discovery_filters() + .unwrap_or_default() + .into_iter() + .map(|v| v.to_godot()) + .collect(); + filters.into() + } + + #[func] + pub fn remove_device(&self, device: Gd) { + let Some(proxy) = self.get_proxy() else { + return; + }; + let path = device.bind().get_dbus_path().to_string(); + let path = ObjectPath::try_from(path).unwrap_or_default(); + proxy.remove_device(&path).unwrap_or_default() + } + + #[func] + pub fn start_discovery(&self) { + let Some(proxy) = self.get_proxy() else { + return; + }; + proxy.start_discovery().unwrap_or_default() + } + + #[func] + pub fn stop_discovery(&self) { + let Some(proxy) = self.get_proxy() else { + return; + }; + proxy.stop_discovery().unwrap_or_default() + } + + /// Dispatches signals + pub fn process(&mut self) { + // Drain all messages from the channel to process them + loop { + let signal = match self.rx.try_recv() { + Ok(value) => value, + Err(e) => match e { + TryRecvError::Empty => break, + TryRecvError::Disconnected => { + godot_error!("Backend thread is not running!"); + return; + } + }, + }; + self.process_signal(signal); + } + } + + /// Process and dispatch the given signal + fn process_signal(&mut self, signal: Signal) { + match signal { + Signal::Discoverable { value } => { + self.base_mut() + .emit_signal("discoverable_changed".into(), &[value.to_variant()]); + } + Signal::Discovering { value } => { + self.base_mut() + .emit_signal("discovering_changed".into(), &[value.to_variant()]); + } + Signal::Pairable { value } => { + self.base_mut() + .emit_signal("pairable_changed".into(), &[value.to_variant()]); + } + Signal::Powered { value } => { + self.base_mut() + .emit_signal("powered_changed".into(), &[value.to_variant()]); + } + Signal::PowerState { value } => { + self.base_mut() + .emit_signal("power_state_changed".into(), &[value.to_variant()]); + } + } + } +} + +impl Drop for BluetoothAdapter { + fn drop(&mut self) { + godot_print!("BluetoothAdapter '{}' is being destroyed!", self.dbus_path); + } +} + +/// Run the signals task +async fn run(tx: Sender, path: String) -> Result<(), Box> { + // Establish a connection to the system bus + let conn = get_dbus_system().await?; + let proxy = Adapter1Proxy::builder(&conn).path(path)?.build().await?; + + let signals_tx = tx.clone(); + let mut events = proxy.receive_discoverable_changed().await; + RUNTIME.spawn(async move { + while let Some(event) = events.next().await { + let value = event.get().await.unwrap_or_default(); + let signal = Signal::Discoverable { value }; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut events = proxy.receive_discovering_changed().await; + RUNTIME.spawn(async move { + while let Some(event) = events.next().await { + let value = event.get().await.unwrap_or_default(); + let signal = Signal::Discovering { value }; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut events = proxy.receive_pairable_changed().await; + RUNTIME.spawn(async move { + while let Some(event) = events.next().await { + let value = event.get().await.unwrap_or_default(); + let signal = Signal::Pairable { value }; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut events = proxy.receive_powered_changed().await; + RUNTIME.spawn(async move { + while let Some(event) = events.next().await { + let value = event.get().await.unwrap_or_default(); + let signal = Signal::Powered { value }; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut events = proxy.receive_power_state_changed().await; + RUNTIME.spawn(async move { + while let Some(event) = events.next().await { + let value = event.get().await.unwrap_or_default(); + let signal = Signal::PowerState { value }; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + Ok(()) +} diff --git a/extensions/core/src/bluetooth/bluez/device.rs b/extensions/core/src/bluetooth/bluez/device.rs new file mode 100644 index 00000000..6f6cceeb --- /dev/null +++ b/extensions/core/src/bluetooth/bluez/device.rs @@ -0,0 +1,516 @@ +use std::error::Error; +use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError}; + +use futures_util::StreamExt; +use godot::prelude::*; + +use godot::classes::{Resource, ResourceLoader}; + +use crate::dbus::bluez::device1::{Device1Proxy, Device1ProxyBlocking}; +use crate::{get_dbus_system, get_dbus_system_blocking, RUNTIME}; + +use super::BLUEZ_BUS; + +/// Signals that can be emitted +#[derive(Debug)] +enum Signal { + ConnectedChanged { value: bool }, + PairedChanged { value: bool }, +} + +#[derive(GodotClass)] +#[class(no_init, base=Resource)] +pub struct BluetoothDevice { + base: Base, + rx: Receiver, + conn: Option, + + #[allow(dead_code)] + #[var(get = get_dbus_path)] + dbus_path: GString, + #[allow(dead_code)] + #[var(get = get_adapter)] + adapter: GString, + #[allow(dead_code)] + #[var(get = get_address)] + address: GString, + #[allow(dead_code)] + #[var(get = get_address_type)] + address_type: GString, + #[allow(dead_code)] + #[var(get = get_alias, set = set_alias)] + alias: GString, + #[allow(dead_code)] + #[var(get = get_appearance)] + appearance: u16, + #[allow(dead_code)] + #[var(get = get_blocked, set = set_blocked)] + blocked: bool, + #[allow(dead_code)] + #[var(get = get_bonded)] + bonded: bool, + #[allow(dead_code)] + #[var(get = get_class)] + class: u32, + #[allow(dead_code)] + #[var(get = get_connected)] + connected: bool, + #[allow(dead_code)] + #[var(get = get_icon)] + icon: GString, + #[allow(dead_code)] + #[var(get = get_legacy_pairing)] + legacy_pairing: bool, + #[allow(dead_code)] + #[var(get = get_modalias)] + modalias: GString, + #[allow(dead_code)] + #[var(get = get_name)] + name: GString, + #[allow(dead_code)] + #[var(get = get_paired)] + paired: bool, + #[allow(dead_code)] + #[var(get = get_rssi)] + rssi: i16, + #[allow(dead_code)] + #[var(get = get_services_resolved)] + services_resolved: bool, + #[allow(dead_code)] + #[var(get = get_trusted, set = set_trusted)] + trusted: bool, + #[allow(dead_code)] + #[var(get = get_tx_power)] + tx_power: i16, + #[allow(dead_code)] + #[var(get = get_uuids)] + uuids: PackedStringArray, + #[allow(dead_code)] + #[var(get = get_wake_allowed, set = set_wake_allowed)] + wake_allowed: bool, +} + +#[godot_api] +impl BluetoothDevice { + #[signal] + fn connected_changed(value: bool); + + #[signal] + fn paired_changed(value: bool); + + /// Create a new [BluetoothDevice] with the given DBus path + pub fn from_path(path: GString) -> Gd { + // Create a channel to communicate with the signals task + godot_print!("BluetoothDevice created with path: {path}"); + let (tx, rx) = channel(); + let dbus_path = path.clone().into(); + + // Spawn a task using the shared tokio runtime to listen for signals + RUNTIME.spawn(async move { + if let Err(e) = run(tx, dbus_path).await { + godot_error!("Failed to run BluetoothDevice task: ${e:?}"); + } + }); + + Gd::from_init_fn(|base| { + // Create a connection to DBus + let conn = get_dbus_system_blocking().ok(); + + // Accept a base of type Base and directly forward it. + Self { + base, + rx, + conn, + dbus_path: path, + adapter: Default::default(), + address: Default::default(), + address_type: Default::default(), + alias: Default::default(), + appearance: Default::default(), + blocked: Default::default(), + bonded: Default::default(), + class: Default::default(), + connected: Default::default(), + icon: Default::default(), + legacy_pairing: Default::default(), + modalias: Default::default(), + name: Default::default(), + paired: Default::default(), + rssi: Default::default(), + services_resolved: Default::default(), + trusted: Default::default(), + tx_power: Default::default(), + uuids: Default::default(), + wake_allowed: Default::default(), + } + }) + } + + /// Return a proxy instance to the device + fn get_proxy(&self) -> Option { + if let Some(conn) = self.conn.as_ref() { + let path: String = self.dbus_path.clone().into(); + Device1ProxyBlocking::builder(conn) + .path(path) + .ok() + .and_then(|builder| builder.build().ok()) + } else { + None + } + } + + /// Get or create a [BluetoothDevice] with the given DBus path. If an instance + /// already exists with the given path, then it will be loaded from the resource + /// cache. + pub fn new(path: &str) -> Gd { + let res_path = format!("dbus://{BLUEZ_BUS}{path}"); + + // Check to see if a resource already exists for this device + let mut resource_loader = ResourceLoader::singleton(); + if resource_loader.exists(res_path.clone().into()) { + if let Some(res) = resource_loader.load(res_path.clone().into()) { + godot_print!( + "Resource already exists with path '{res_path}', loading that instead" + ); + let device: Gd = res.cast(); + device + } else { + let mut device = BluetoothDevice::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } else { + let mut device = BluetoothDevice::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } + + /// Return the DBus path to the device + #[func] + pub fn get_dbus_path(&self) -> GString { + self.dbus_path.clone() + } + + #[func] + pub fn cancel_pairing(&self) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.cancel_pairing().unwrap_or_default() + } + + #[func] + pub fn connect_to(&self) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.connect().unwrap_or_default() + } + + #[func] + pub fn connect_to_profile(&self, uuid: GString) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + let uuid = uuid.to_string(); + proxy.connect_profile(uuid.as_str()).unwrap_or_default() + } + + #[func] + pub fn disconnect_from(&self) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.disconnect().unwrap_or_default() + } + + #[func] + pub fn disconnect_from_profile(&self, uuid: GString) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + let uuid = uuid.to_string(); + proxy.disconnect_profile(uuid.as_str()).unwrap_or_default() + } + + #[func] + pub fn pair(&self) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.pair().unwrap_or_default() + } + + #[func] + pub fn get_wake_allowed(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.wake_allowed().unwrap_or_default() + } + + #[func] + pub fn set_wake_allowed(&self, allowed: bool) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_wake_allowed(allowed).unwrap_or_default() + } + + #[func] + pub fn get_uuids(&self) -> PackedStringArray { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + let values: Vec = proxy + .uuids() + .unwrap_or_default() + .into_iter() + .map(|v| v.to_godot()) + .collect(); + values.into() + } + + #[func] + pub fn get_tx_power(&self) -> i16 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.tx_power().unwrap_or_default() + } + + #[func] + pub fn get_trusted(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.trusted().unwrap_or_default() + } + + #[func] + pub fn set_trusted(&self, value: bool) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_trusted(value).unwrap_or_default() + } + + #[func] + pub fn get_services_resolved(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.services_resolved().unwrap_or_default() + } + + #[func] + pub fn get_rssi(&self) -> i16 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.rssi().unwrap_or_default() + } + + #[func] + pub fn get_paired(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.paired().unwrap_or_default() + } + + #[func] + pub fn get_name(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.name().unwrap_or_default().into() + } + + #[func] + pub fn get_modalias(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.modalias().unwrap_or_default().into() + } + + #[func] + pub fn get_legacy_pairing(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.legacy_pairing().unwrap_or_default() + } + + #[func] + pub fn get_icon(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.icon().unwrap_or_default().into() + } + + #[func] + pub fn get_connected(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.connected().unwrap_or_default() + } + + #[func] + pub fn get_class(&self) -> u32 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.class().unwrap_or_default() + } + + #[func] + pub fn get_bonded(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.bonded().unwrap_or_default() + } + + #[func] + pub fn get_blocked(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.blocked().unwrap_or_default() + } + + #[func] + pub fn set_blocked(&self, value: bool) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_blocked(value).unwrap_or_default() + } + + #[func] + pub fn get_appearance(&self) -> u16 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.appearance().unwrap_or_default() + } + + #[func] + pub fn get_alias(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.alias().unwrap_or_default().into() + } + + #[func] + pub fn set_alias(&self, value: GString) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy + .set_alias(value.to_string().as_str()) + .unwrap_or_default() + } + + #[func] + pub fn get_address_type(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.address_type().unwrap_or_default().into() + } + + #[func] + pub fn get_address(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.address().unwrap_or_default().into() + } + + #[func] + pub fn get_adapter(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.adapter().unwrap_or_default().to_string().into() + } + + /// Dispatches signals + pub fn process(&mut self) { + // Drain all messages from the channel to process them + loop { + let signal = match self.rx.try_recv() { + Ok(value) => value, + Err(e) => match e { + TryRecvError::Empty => break, + TryRecvError::Disconnected => { + godot_error!("Backend thread is not running!"); + return; + } + }, + }; + self.process_signal(signal); + } + } + + /// Process and dispatch the given signal + fn process_signal(&mut self, signal: Signal) { + godot_print!("Got signal: {signal:?}"); + match signal { + Signal::ConnectedChanged { value } => { + self.base_mut() + .emit_signal("connected_changed".into(), &[value.to_variant()]); + } + Signal::PairedChanged { value } => { + self.base_mut() + .emit_signal("paired_changed".into(), &[value.to_variant()]); + } + } + } +} + +impl Drop for BluetoothDevice { + fn drop(&mut self) { + godot_print!("BluetoothDevice '{}' is being destroyed!", self.dbus_path); + } +} + +/// Run the signals task +async fn run(tx: Sender, path: String) -> Result<(), Box> { + // Establish a connection to the system bus + let conn = get_dbus_system().await?; + let proxy = Device1Proxy::builder(&conn).path(path)?.build().await?; + + let signals_tx = tx.clone(); + let mut events = proxy.receive_connected_changed().await; + RUNTIME.spawn(async move { + while let Some(event) = events.next().await { + let value = event.get().await.unwrap_or_default(); + let signal = Signal::ConnectedChanged { value }; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut events = proxy.receive_paired_changed().await; + RUNTIME.spawn(async move { + while let Some(event) = events.next().await { + let value = event.get().await.unwrap_or_default(); + let signal = Signal::PairedChanged { value }; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + Ok(()) +} diff --git a/extensions/core/src/dbus.rs b/extensions/core/src/dbus.rs new file mode 100644 index 00000000..320ccb59 --- /dev/null +++ b/extensions/core/src/dbus.rs @@ -0,0 +1,100 @@ +use godot::prelude::*; +use zvariant::NoneValue; + +pub mod bluez; +pub mod inputplumber; +pub mod powerstation; +pub mod upower; + +/// Possible DBus runtime errors +#[derive(Debug)] +pub enum RunError { + Zbus(zbus::Error), + ZbusFdo(zbus::fdo::Error), +} + +impl From for RunError { + fn from(value: zbus::Error) -> Self { + RunError::Zbus(value) + } +} + +impl From for RunError { + fn from(value: zbus::fdo::Error) -> Self { + RunError::ZbusFdo(value) + } +} + +pub trait DBusVariant { + fn as_zvariant(&self) -> Option; +} + +impl DBusVariant for Variant { + /// Convert the Godot variant type into a DBus variant type + fn as_zvariant(&self) -> Option { + match self.get_type() { + VariantType::NIL => { + let value = zvariant::Optional::<&str>::null_value(); + Some(zvariant::Value::new(value)) + } + VariantType::BOOL => { + let value: bool = self.to(); + Some(zvariant::Value::new(value)) + } + VariantType::INT => { + let value: i64 = self.to(); + Some(zvariant::Value::new(value)) + } + VariantType::FLOAT => { + let value: f64 = self.to(); + Some(zvariant::Value::new(value)) + } + VariantType::STRING => { + let value: GString = self.to(); + let value: String = value.into(); + Some(zvariant::Value::new(value)) + } + VariantType::VECTOR2 => todo!(), + VariantType::VECTOR2I => todo!(), + VariantType::RECT2 => todo!(), + VariantType::RECT2I => todo!(), + VariantType::VECTOR3 => todo!(), + VariantType::VECTOR3I => todo!(), + VariantType::TRANSFORM2D => todo!(), + VariantType::VECTOR4 => todo!(), + VariantType::VECTOR4I => todo!(), + VariantType::PLANE => todo!(), + VariantType::QUATERNION => todo!(), + VariantType::AABB => todo!(), + VariantType::BASIS => todo!(), + VariantType::TRANSFORM3D => todo!(), + VariantType::PROJECTION => todo!(), + VariantType::COLOR => todo!(), + VariantType::STRING_NAME => todo!(), + VariantType::NODE_PATH => todo!(), + VariantType::RID => { + let value: i64 = self.to(); + Some(zvariant::Value::new(value)) + } + VariantType::OBJECT => todo!(), + VariantType::CALLABLE => todo!(), + VariantType::SIGNAL => todo!(), + VariantType::DICTIONARY => todo!(), + VariantType::ARRAY => todo!(), + VariantType::PACKED_BYTE_ARRAY => todo!(), + VariantType::PACKED_INT32_ARRAY => todo!(), + VariantType::PACKED_INT64_ARRAY => todo!(), + VariantType::PACKED_FLOAT32_ARRAY => todo!(), + VariantType::PACKED_FLOAT64_ARRAY => todo!(), + VariantType::PACKED_STRING_ARRAY => todo!(), + VariantType::PACKED_VECTOR2_ARRAY => todo!(), + VariantType::PACKED_VECTOR3_ARRAY => todo!(), + VariantType::PACKED_COLOR_ARRAY => todo!(), + VariantType::PACKED_VECTOR4_ARRAY => todo!(), + VariantType::MAX => todo!(), + + // Unsupported conversion + _ => None, + } + } +} diff --git a/extensions/core/src/dbus/bluez.rs b/extensions/core/src/dbus/bluez.rs new file mode 100644 index 00000000..bce2e071 --- /dev/null +++ b/extensions/core/src/dbus/bluez.rs @@ -0,0 +1,8 @@ +pub mod adapter1; +pub mod battery_provider_manager1; +pub mod device1; +pub mod gatt_manager1; +pub mod leadvertising_manager1; +pub mod media1; +pub mod media_control1; +pub mod network_server1; diff --git a/extensions/core/src/dbus/bluez/adapter1.rs b/extensions/core/src/dbus/bluez/adapter1.rs new file mode 100644 index 00000000..79e76a19 --- /dev/null +++ b/extensions/core/src/dbus/bluez/adapter1.rs @@ -0,0 +1,129 @@ +//! # D-Bus interface proxy for: `org.bluez.Adapter1` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/bluez/hci0' from service 'org.bluez' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.bluez.Adapter1", + default_service = "org.bluez", + default_path = "/org/bluez/hci0" +)] +trait Adapter1 { + /// GetDiscoveryFilters method + fn get_discovery_filters(&self) -> zbus::Result>; + + /// RemoveDevice method + fn remove_device(&self, device: &zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; + + /// SetDiscoveryFilter method + fn set_discovery_filter( + &self, + properties: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>, + ) -> zbus::Result<()>; + + /// StartDiscovery method + fn start_discovery(&self) -> zbus::Result<()>; + + /// StopDiscovery method + fn stop_discovery(&self) -> zbus::Result<()>; + + /// Address property + #[zbus(property)] + fn address(&self) -> zbus::Result; + + /// AddressType property + #[zbus(property)] + fn address_type(&self) -> zbus::Result; + + /// Alias property + #[zbus(property)] + fn alias(&self) -> zbus::Result; + #[zbus(property)] + fn set_alias(&self, value: &str) -> zbus::Result<()>; + + /// Class property + #[zbus(property)] + fn class(&self) -> zbus::Result; + + /// Discoverable property + #[zbus(property)] + fn discoverable(&self) -> zbus::Result; + #[zbus(property)] + fn set_discoverable(&self, value: bool) -> zbus::Result<()>; + + /// DiscoverableTimeout property + #[zbus(property)] + fn discoverable_timeout(&self) -> zbus::Result; + #[zbus(property)] + fn set_discoverable_timeout(&self, value: u32) -> zbus::Result<()>; + + /// Discovering property + #[zbus(property)] + fn discovering(&self) -> zbus::Result; + + /// ExperimentalFeatures property + #[zbus(property)] + fn experimental_features(&self) -> zbus::Result>; + + /// Manufacturer property + #[zbus(property)] + fn manufacturer(&self) -> zbus::Result; + + /// Modalias property + #[zbus(property)] + fn modalias(&self) -> zbus::Result; + + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; + + /// Pairable property + #[zbus(property)] + fn pairable(&self) -> zbus::Result; + #[zbus(property)] + fn set_pairable(&self, value: bool) -> zbus::Result<()>; + + /// PairableTimeout property + #[zbus(property)] + fn pairable_timeout(&self) -> zbus::Result; + #[zbus(property)] + fn set_pairable_timeout(&self, value: u32) -> zbus::Result<()>; + + /// PowerState property + #[zbus(property)] + fn power_state(&self) -> zbus::Result; + + /// Powered property + #[zbus(property)] + fn powered(&self) -> zbus::Result; + #[zbus(property)] + fn set_powered(&self, value: bool) -> zbus::Result<()>; + + /// Roles property + #[zbus(property)] + fn roles(&self) -> zbus::Result>; + + /// UUIDs property + #[zbus(property, name = "UUIDs")] + fn uuids(&self) -> zbus::Result>; + + /// Version property + #[zbus(property)] + fn version(&self) -> zbus::Result; +} diff --git a/extensions/core/src/dbus/bluez/battery_provider_manager1.rs b/extensions/core/src/dbus/bluez/battery_provider_manager1.rs new file mode 100644 index 00000000..c47ac8f7 --- /dev/null +++ b/extensions/core/src/dbus/bluez/battery_provider_manager1.rs @@ -0,0 +1,39 @@ +//! # D-Bus interface proxy for: `org.bluez.BatteryProviderManager1` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/bluez/hci0' from service 'org.bluez' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.bluez.BatteryProviderManager1", + default_service = "org.bluez", + default_path = "/org/bluez/hci0" +)] +trait BatteryProviderManager1 { + /// RegisterBatteryProvider method + fn register_battery_provider( + &self, + provider: &zbus::zvariant::ObjectPath<'_>, + ) -> zbus::Result<()>; + + /// UnregisterBatteryProvider method + fn unregister_battery_provider( + &self, + provider: &zbus::zvariant::ObjectPath<'_>, + ) -> zbus::Result<()>; +} diff --git a/extensions/core/src/dbus/bluez/device1.rs b/extensions/core/src/dbus/bluez/device1.rs new file mode 100644 index 00000000..06c5300d --- /dev/null +++ b/extensions/core/src/dbus/bluez/device1.rs @@ -0,0 +1,152 @@ +//! # D-Bus interface proxy for: `org.bluez.Device1` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/bluez/hci0/dev_XX...' from service 'org.bluez' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy(interface = "org.bluez.Device1", default_service = "org.bluez")] +trait Device1 { + /// CancelPairing method + fn cancel_pairing(&self) -> zbus::Result<()>; + + /// Connect method + fn connect(&self) -> zbus::Result<()>; + + /// ConnectProfile method + fn connect_profile(&self, UUID: &str) -> zbus::Result<()>; + + /// Disconnect method + fn disconnect(&self) -> zbus::Result<()>; + + /// DisconnectProfile method + fn disconnect_profile(&self, UUID: &str) -> zbus::Result<()>; + + /// Pair method + fn pair(&self) -> zbus::Result<()>; + + /// Adapter property + #[zbus(property)] + fn adapter(&self) -> zbus::Result; + + /// Address property + #[zbus(property)] + fn address(&self) -> zbus::Result; + + /// AddressType property + #[zbus(property)] + fn address_type(&self) -> zbus::Result; + + /// Alias property + #[zbus(property)] + fn alias(&self) -> zbus::Result; + #[zbus(property)] + fn set_alias(&self, value: &str) -> zbus::Result<()>; + + /// Appearance property + #[zbus(property)] + fn appearance(&self) -> zbus::Result; + + /// Blocked property + #[zbus(property)] + fn blocked(&self) -> zbus::Result; + #[zbus(property)] + fn set_blocked(&self, value: bool) -> zbus::Result<()>; + + /// Bonded property + #[zbus(property)] + fn bonded(&self) -> zbus::Result; + + /// Class property + #[zbus(property)] + fn class(&self) -> zbus::Result; + + /// Connected property + #[zbus(property)] + fn connected(&self) -> zbus::Result; + + /// Icon property + #[zbus(property)] + fn icon(&self) -> zbus::Result; + + /// LegacyPairing property + #[zbus(property)] + fn legacy_pairing(&self) -> zbus::Result; + + /// ManufacturerData property + #[zbus(property)] + fn manufacturer_data( + &self, + ) -> zbus::Result>; + + /// Modalias property + #[zbus(property)] + fn modalias(&self) -> zbus::Result; + + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; + + /// Paired property + #[zbus(property)] + fn paired(&self) -> zbus::Result; + + /// RSSI property + #[zbus(property, name = "RSSI")] + fn rssi(&self) -> zbus::Result; + + /// ServiceData property + #[zbus(property)] + fn service_data( + &self, + ) -> zbus::Result>; + + /// ServicesResolved property + #[zbus(property)] + fn services_resolved(&self) -> zbus::Result; + + /// Sets property + #[zbus(property)] + fn sets( + &self, + ) -> zbus::Result< + std::collections::HashMap< + zbus::zvariant::OwnedObjectPath, + std::collections::HashMap, + >, + >; + + /// Trusted property + #[zbus(property)] + fn trusted(&self) -> zbus::Result; + #[zbus(property)] + fn set_trusted(&self, value: bool) -> zbus::Result<()>; + + /// TxPower property + #[zbus(property)] + fn tx_power(&self) -> zbus::Result; + + /// UUIDs property + #[zbus(property, name = "UUIDs")] + fn uuids(&self) -> zbus::Result>; + + /// WakeAllowed property + #[zbus(property)] + fn wake_allowed(&self) -> zbus::Result; + #[zbus(property)] + fn set_wake_allowed(&self, value: bool) -> zbus::Result<()>; +} diff --git a/extensions/core/src/dbus/bluez/gatt_manager1.rs b/extensions/core/src/dbus/bluez/gatt_manager1.rs new file mode 100644 index 00000000..bf0153d7 --- /dev/null +++ b/extensions/core/src/dbus/bluez/gatt_manager1.rs @@ -0,0 +1,40 @@ +//! # D-Bus interface proxy for: `org.bluez.GattManager1` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/bluez/hci0' from service 'org.bluez' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.bluez.GattManager1", + default_service = "org.bluez", + default_path = "/org/bluez/hci0" +)] +trait GattManager1 { + /// RegisterApplication method + fn register_application( + &self, + application: &zbus::zvariant::ObjectPath<'_>, + options: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>, + ) -> zbus::Result<()>; + + /// UnregisterApplication method + fn unregister_application( + &self, + application: &zbus::zvariant::ObjectPath<'_>, + ) -> zbus::Result<()>; +} diff --git a/extensions/core/src/dbus/bluez/leadvertising_manager1.rs b/extensions/core/src/dbus/bluez/leadvertising_manager1.rs new file mode 100644 index 00000000..da3ddbb6 --- /dev/null +++ b/extensions/core/src/dbus/bluez/leadvertising_manager1.rs @@ -0,0 +1,56 @@ +//! # D-Bus interface proxy for: `org.bluez.LEAdvertisingManager1` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/bluez/hci0' from service 'org.bluez' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.bluez.LEAdvertisingManager1", + default_service = "org.bluez", + default_path = "/org/bluez/hci0" +)] +trait LEAdvertisingManager1 { + /// RegisterAdvertisement method + fn register_advertisement( + &self, + advertisement: &zbus::zvariant::ObjectPath<'_>, + options: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>, + ) -> zbus::Result<()>; + + /// UnregisterAdvertisement method + fn unregister_advertisement( + &self, + service: &zbus::zvariant::ObjectPath<'_>, + ) -> zbus::Result<()>; + + /// ActiveInstances property + #[zbus(property)] + fn active_instances(&self) -> zbus::Result; + + /// SupportedIncludes property + #[zbus(property)] + fn supported_includes(&self) -> zbus::Result>; + + /// SupportedInstances property + #[zbus(property)] + fn supported_instances(&self) -> zbus::Result; + + /// SupportedSecondaryChannels property + #[zbus(property)] + fn supported_secondary_channels(&self) -> zbus::Result>; +} diff --git a/extensions/core/src/dbus/bluez/media1.rs b/extensions/core/src/dbus/bluez/media1.rs new file mode 100644 index 00000000..ba045686 --- /dev/null +++ b/extensions/core/src/dbus/bluez/media1.rs @@ -0,0 +1,64 @@ +//! # D-Bus interface proxy for: `org.bluez.Media1` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/bluez/hci0' from service 'org.bluez' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.bluez.Media1", + default_service = "org.bluez", + default_path = "/org/bluez/hci0" +)] +trait Media1 { + /// RegisterApplication method + fn register_application( + &self, + application: &zbus::zvariant::ObjectPath<'_>, + options: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>, + ) -> zbus::Result<()>; + + /// RegisterEndpoint method + fn register_endpoint( + &self, + endpoint: &zbus::zvariant::ObjectPath<'_>, + properties: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>, + ) -> zbus::Result<()>; + + /// RegisterPlayer method + fn register_player( + &self, + player: &zbus::zvariant::ObjectPath<'_>, + properties: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>, + ) -> zbus::Result<()>; + + /// UnregisterApplication method + fn unregister_application( + &self, + application: &zbus::zvariant::ObjectPath<'_>, + ) -> zbus::Result<()>; + + /// UnregisterEndpoint method + fn unregister_endpoint(&self, endpoint: &zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; + + /// UnregisterPlayer method + fn unregister_player(&self, player: &zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; + + /// SupportedUUIDs property + #[zbus(property, name = "SupportedUUIDs")] + fn supported_uuids(&self) -> zbus::Result>; +} diff --git a/extensions/core/src/dbus/bluez/media_control1.rs b/extensions/core/src/dbus/bluez/media_control1.rs new file mode 100644 index 00000000..77583bff --- /dev/null +++ b/extensions/core/src/dbus/bluez/media_control1.rs @@ -0,0 +1,58 @@ +//! # D-Bus interface proxy for: `org.bluez.MediaControl1` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/bluez/hci0/dev_XX...' from service 'org.bluez' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy(interface = "org.bluez.MediaControl1", default_service = "org.bluez")] +trait MediaControl1 { + /// FastForward method + fn fast_forward(&self) -> zbus::Result<()>; + + /// Next method + fn next(&self) -> zbus::Result<()>; + + /// Pause method + fn pause(&self) -> zbus::Result<()>; + + /// Play method + fn play(&self) -> zbus::Result<()>; + + /// Previous method + fn previous(&self) -> zbus::Result<()>; + + /// Rewind method + fn rewind(&self) -> zbus::Result<()>; + + /// Stop method + fn stop(&self) -> zbus::Result<()>; + + /// VolumeDown method + fn volume_down(&self) -> zbus::Result<()>; + + /// VolumeUp method + fn volume_up(&self) -> zbus::Result<()>; + + /// Connected property + #[zbus(property)] + fn connected(&self) -> zbus::Result; + + /// Player property + #[zbus(property)] + fn player(&self) -> zbus::Result; +} diff --git a/extensions/core/src/dbus/bluez/network_server1.rs b/extensions/core/src/dbus/bluez/network_server1.rs new file mode 100644 index 00000000..22a9f13a --- /dev/null +++ b/extensions/core/src/dbus/bluez/network_server1.rs @@ -0,0 +1,33 @@ +//! # D-Bus interface proxy for: `org.bluez.NetworkServer1` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/bluez/hci0' from service 'org.bluez' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.bluez.NetworkServer1", + default_service = "org.bluez", + default_path = "/org/bluez/hci0" +)] +trait NetworkServer1 { + /// Register method + fn register(&self, uuid: &str, bridge: &str) -> zbus::Result<()>; + + /// Unregister method + fn unregister(&self, uuid: &str) -> zbus::Result<()>; +} diff --git a/extensions/core/src/dbus/inputplumber.rs b/extensions/core/src/dbus/inputplumber.rs new file mode 100644 index 00000000..e722fce8 --- /dev/null +++ b/extensions/core/src/dbus/inputplumber.rs @@ -0,0 +1,6 @@ +pub mod composite_device; +pub mod dbus_device; +pub mod event_device; +pub mod input_manager; +pub mod keyboard; +pub mod mouse; diff --git a/extensions/core/src/dbus/inputplumber/composite_device.rs b/extensions/core/src/dbus/inputplumber/composite_device.rs new file mode 100644 index 00000000..bfb9ea4b --- /dev/null +++ b/extensions/core/src/dbus/inputplumber/composite_device.rs @@ -0,0 +1,87 @@ +//! # D-Bus interface proxy for: `org.shadowblip.Input.CompositeDevice` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/InputPlumber/CompositeDevice0' from service 'org.shadowblip.InputPlumber' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.Input.CompositeDevice", + default_service = "org.shadowblip.InputPlumber", + default_path = "/org/shadowblip/InputPlumber/CompositeDevice0" +)] +trait CompositeDevice { + /// LoadProfileFromYaml method + fn load_profile_from_yaml(&self, profile: &str) -> zbus::Result<()>; + + /// LoadProfilePath method + fn load_profile_path(&self, path: &str) -> zbus::Result<()>; + + /// SendButtonChord method + fn send_button_chord(&self, events: &[&str]) -> zbus::Result<()>; + + /// SendEvent method + fn send_event(&self, event: &str, value: &zbus::zvariant::Value<'_>) -> zbus::Result<()>; + + /// SetInterceptActivation method + fn set_intercept_activation( + &self, + activation_events: &[&str], + target_event: &str, + ) -> zbus::Result<()>; + + /// SetTargetDevices method + fn set_target_devices(&self, target_device_types: &[&str]) -> zbus::Result<()>; + + /// Stop method + fn stop(&self) -> zbus::Result<()>; + + /// Capabilities property + #[zbus(property)] + fn capabilities(&self) -> zbus::Result>; + + /// DbusDevices property + #[zbus(property)] + fn dbus_devices(&self) -> zbus::Result>; + + /// InterceptMode property + #[zbus(property)] + fn intercept_mode(&self) -> zbus::Result; + #[zbus(property)] + fn set_intercept_mode(&self, value: u32) -> zbus::Result<()>; + + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; + + /// ProfileName property + #[zbus(property)] + fn profile_name(&self) -> zbus::Result; + + /// SourceDevicePaths property + #[zbus(property)] + fn source_device_paths(&self) -> zbus::Result>; + + /// TargetCapabilities property + #[zbus(property)] + fn target_capabilities(&self) -> zbus::Result>; + + /// TargetDevices property + #[zbus(property)] + fn target_devices(&self) -> zbus::Result>; +} diff --git a/extensions/core/src/dbus/inputplumber/dbus_device.rs b/extensions/core/src/dbus/inputplumber/dbus_device.rs new file mode 100644 index 00000000..56f05450 --- /dev/null +++ b/extensions/core/src/dbus/inputplumber/dbus_device.rs @@ -0,0 +1,48 @@ +//! # D-Bus interface proxy for: `org.shadowblip.Input.DBusDevice` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/InputPlumber/devices/target/dbus0' from service 'org.shadowblip.InputPlumber' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.Input.DBusDevice", + default_service = "org.shadowblip.InputPlumber", + default_path = "/org/shadowblip/InputPlumber/devices/target/dbus0" +)] +trait DBusDevice { + /// InputEvent signal + #[zbus(signal)] + fn input_event(&self, event: &str, value: f64) -> zbus::Result<()>; + + /// TouchEvent signal + #[zbus(signal)] + fn touch_event( + &self, + event: &str, + index: u32, + is_touching: bool, + pressure: f64, + x: f64, + y: f64, + ) -> zbus::Result<()>; + + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; +} diff --git a/extensions/core/src/dbus/inputplumber/event_device.rs b/extensions/core/src/dbus/inputplumber/event_device.rs new file mode 100644 index 00000000..5b435da7 --- /dev/null +++ b/extensions/core/src/dbus/inputplumber/event_device.rs @@ -0,0 +1,64 @@ +//! # D-Bus interface proxy for: `org.shadowblip.Input.Source.EventDevice` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/InputPlumber/devices/source/event9' from service 'org.shadowblip.InputPlumber' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.Input.Source.EventDevice", + default_service = "org.shadowblip.InputPlumber", + default_path = "/org/shadowblip/InputPlumber/devices/source/event9" +)] +trait EventDevice { + /// DevicePath property + #[zbus(property)] + fn device_path(&self) -> zbus::Result; + + /// IdBustype property + #[zbus(property)] + fn id_bustype(&self) -> zbus::Result; + + /// IdProduct property + #[zbus(property)] + fn id_product(&self) -> zbus::Result; + + /// IdVendor property + #[zbus(property)] + fn id_vendor(&self) -> zbus::Result; + + /// IdVersion property + #[zbus(property)] + fn id_version(&self) -> zbus::Result; + + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; + + /// PhysPath property + #[zbus(property)] + fn phys_path(&self) -> zbus::Result; + + /// SysfsPath property + #[zbus(property)] + fn sysfs_path(&self) -> zbus::Result; + + /// UniqueId property + #[zbus(property)] + fn unique_id(&self) -> zbus::Result; +} diff --git a/extensions/core/src/dbus/inputplumber/gamepad.rs b/extensions/core/src/dbus/inputplumber/gamepad.rs new file mode 100644 index 00000000..207174b1 --- /dev/null +++ b/extensions/core/src/dbus/inputplumber/gamepad.rs @@ -0,0 +1,32 @@ +//! # D-Bus interface proxy for: `org.shadowblip.Input.Gamepad` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/InputPlumber/devices/target/gamepad0' from service 'org.shadowblip.InputPlumber' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.Input.Gamepad", + default_service = "org.shadowblip.InputPlumber", + default_path = "/org/shadowblip/InputPlumber/devices/target/gamepad0" +)] +trait Gamepad { + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; +} diff --git a/extensions/core/src/dbus/inputplumber/hidraw_device.rs b/extensions/core/src/dbus/inputplumber/hidraw_device.rs new file mode 100644 index 00000000..f0a50da5 --- /dev/null +++ b/extensions/core/src/dbus/inputplumber/hidraw_device.rs @@ -0,0 +1,64 @@ +//! # D-Bus interface proxy for: `org.shadowblip.Input.Source.HIDRawDevice` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/InputPlumber/devices/source/hidraw0' from service 'org.shadowblip.InputPlumber' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.Input.Source.HIDRawDevice", + default_service = "org.shadowblip.InputPlumber", + default_path = "/org/shadowblip/InputPlumber/devices/source/hidraw0" +)] +trait HIDRawDevice { + /// DevPath property + #[zbus(property)] + fn dev_path(&self) -> zbus::Result; + + /// IdProduct property + #[zbus(property)] + fn id_product(&self) -> zbus::Result; + + /// IdVendor property + #[zbus(property)] + fn id_vendor(&self) -> zbus::Result; + + /// InterfaceNumber property + #[zbus(property)] + fn interface_number(&self) -> zbus::Result; + + /// Manufacturer property + #[zbus(property)] + fn manufacturer(&self) -> zbus::Result; + + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; + + /// Product property + #[zbus(property)] + fn product(&self) -> zbus::Result; + + /// SerialNumber property + #[zbus(property)] + fn serial_number(&self) -> zbus::Result; + + /// SysfsPath property + #[zbus(property)] + fn sysfs_path(&self) -> zbus::Result; +} diff --git a/extensions/core/src/dbus/inputplumber/iioimudevice.rs b/extensions/core/src/dbus/inputplumber/iioimudevice.rs new file mode 100644 index 00000000..7dbfdfdf --- /dev/null +++ b/extensions/core/src/dbus/inputplumber/iioimudevice.rs @@ -0,0 +1,76 @@ +//! # D-Bus interface proxy for: `org.shadowblip.Input.Source.IIOIMUDevice` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/InputPlumber/devices/source/iio_device0' from service 'org.shadowblip.InputPlumber' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.Input.Source.IIOIMUDevice", + default_service = "org.shadowblip.InputPlumber", + default_path = "/org/shadowblip/InputPlumber/devices/source/iio_device0" +)] +trait IIOIMUDevice { + /// AccelSampleRate property + #[zbus(property)] + fn accel_sample_rate(&self) -> zbus::Result; + #[zbus(property)] + fn set_accel_sample_rate(&self, value: f64) -> zbus::Result<()>; + + /// AccelSampleRatesAvail property + #[zbus(property)] + fn accel_sample_rates_avail(&self) -> zbus::Result>; + + /// AccelScale property + #[zbus(property)] + fn accel_scale(&self) -> zbus::Result; + #[zbus(property)] + fn set_accel_scale(&self, value: f64) -> zbus::Result<()>; + + /// AccelScalesAvail property + #[zbus(property)] + fn accel_scales_avail(&self) -> zbus::Result>; + + /// AngvelSampleRate property + #[zbus(property)] + fn angvel_sample_rate(&self) -> zbus::Result; + #[zbus(property)] + fn set_angvel_sample_rate(&self, value: f64) -> zbus::Result<()>; + + /// AngvelSampleRatesAvail property + #[zbus(property)] + fn angvel_sample_rates_avail(&self) -> zbus::Result>; + + /// AngvelScale property + #[zbus(property)] + fn angvel_scale(&self) -> zbus::Result; + #[zbus(property)] + fn set_angvel_scale(&self, value: f64) -> zbus::Result<()>; + + /// AngvelScalesAvail property + #[zbus(property)] + fn angvel_scales_avail(&self) -> zbus::Result>; + + /// Id property + #[zbus(property)] + fn id(&self) -> zbus::Result; + + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; +} diff --git a/extensions/core/src/dbus/inputplumber/input_manager.rs b/extensions/core/src/dbus/inputplumber/input_manager.rs new file mode 100644 index 00000000..49b1c383 --- /dev/null +++ b/extensions/core/src/dbus/inputplumber/input_manager.rs @@ -0,0 +1,52 @@ +//! # D-Bus interface proxy for: `org.shadowblip.InputManager` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/InputPlumber/Manager' from service 'org.shadowblip.InputPlumber' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.InputManager", + default_service = "org.shadowblip.InputPlumber", + default_path = "/org/shadowblip/InputPlumber/Manager" +)] +trait InputManager { + /// AttachTargetDevice method + fn attach_target_device(&self, target_path: &str, composite_path: &str) -> zbus::Result<()>; + + /// CreateCompositeDevice method + fn create_composite_device(&self, config_path: &str) -> zbus::Result; + + /// CreateTargetDevice method + fn create_target_device(&self, kind: &str) -> zbus::Result; + + /// StopTargetDevice method + fn stop_target_device(&self, path: &str) -> zbus::Result<()>; + + /// InterceptMode property + #[zbus(property)] + fn intercept_mode(&self) -> zbus::Result; + + /// SupportedTargetDeviceIds property + #[zbus(property)] + fn supported_target_device_ids(&self) -> zbus::Result>; + + /// SupportedTargetDevices property + #[zbus(property)] + fn supported_target_devices(&self) -> zbus::Result>; +} diff --git a/extensions/core/src/dbus/inputplumber/keyboard.rs b/extensions/core/src/dbus/inputplumber/keyboard.rs new file mode 100644 index 00000000..3da27d52 --- /dev/null +++ b/extensions/core/src/dbus/inputplumber/keyboard.rs @@ -0,0 +1,35 @@ +//! # D-Bus interface proxy for: `org.shadowblip.Input.Keyboard` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/InputPlumber/devices/target/keyboard0' from service 'org.shadowblip.InputPlumber' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.Input.Keyboard", + default_service = "org.shadowblip.InputPlumber", + default_path = "/org/shadowblip/InputPlumber/devices/target/keyboard0" +)] +trait Keyboard { + /// SendKey method + fn send_key(&self, key: &str, value: bool) -> zbus::Result<()>; + + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; +} diff --git a/extensions/core/src/dbus/inputplumber/mouse.rs b/extensions/core/src/dbus/inputplumber/mouse.rs new file mode 100644 index 00000000..8ffe1b2d --- /dev/null +++ b/extensions/core/src/dbus/inputplumber/mouse.rs @@ -0,0 +1,35 @@ +//! # D-Bus interface proxy for: `org.shadowblip.Input.Mouse` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/InputPlumber/devices/target/mouse0' from service 'org.shadowblip.InputPlumber' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::PeerProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.Input.Mouse", + default_service = "org.shadowblip.InputPlumber", + default_path = "/org/shadowblip/InputPlumber/devices/target/mouse0" +)] +trait Mouse { + /// MoveCursor method + fn move_cursor(&self, x: i32, y: i32) -> zbus::Result<()>; + + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; +} diff --git a/extensions/core/src/dbus/powerstation.rs b/extensions/core/src/dbus/powerstation.rs new file mode 100644 index 00000000..3ae387c0 --- /dev/null +++ b/extensions/core/src/dbus/powerstation.rs @@ -0,0 +1,6 @@ +pub mod card; +pub mod connector; +pub mod core; +pub mod cpu; +pub mod gpu; +pub mod tdp; diff --git a/extensions/core/src/dbus/powerstation/card.rs b/extensions/core/src/dbus/powerstation/card.rs new file mode 100644 index 00000000..0634b4d5 --- /dev/null +++ b/extensions/core/src/dbus/powerstation/card.rs @@ -0,0 +1,105 @@ +//! # D-Bus interface proxy for: `org.shadowblip.GPU.Card` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/Performance/GPU/card0' from service 'org.shadowblip.PowerStation' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.GPU.Card", + default_service = "org.shadowblip.PowerStation", + default_path = "/org/shadowblip/Performance/GPU/card0" +)] +trait Card { + /// EnumerateConnectors method + fn enumerate_connectors(&self) -> zbus::Result>; + + /// Class property + #[zbus(property)] + fn class(&self) -> zbus::Result; + + /// ClassId property + #[zbus(property)] + fn class_id(&self) -> zbus::Result; + + /// ClockLimitMhzMax property + #[zbus(property)] + fn clock_limit_mhz_max(&self) -> zbus::Result; + + /// ClockLimitMhzMin property + #[zbus(property)] + fn clock_limit_mhz_min(&self) -> zbus::Result; + + /// ClockValueMhzMax property + #[zbus(property)] + fn clock_value_mhz_max(&self) -> zbus::Result; + #[zbus(property)] + fn set_clock_value_mhz_max(&self, value: f64) -> zbus::Result<()>; + + /// ClockValueMhzMin property + #[zbus(property)] + fn clock_value_mhz_min(&self) -> zbus::Result; + #[zbus(property)] + fn set_clock_value_mhz_min(&self, value: f64) -> zbus::Result<()>; + + /// Device property + #[zbus(property)] + fn device(&self) -> zbus::Result; + + /// DeviceId property + #[zbus(property)] + fn device_id(&self) -> zbus::Result; + + /// ManualClock property + #[zbus(property)] + fn manual_clock(&self) -> zbus::Result; + #[zbus(property)] + fn set_manual_clock(&self, value: bool) -> zbus::Result<()>; + + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; + + /// Path property + #[zbus(property)] + fn path(&self) -> zbus::Result; + + /// RevisionId property + #[zbus(property)] + fn revision_id(&self) -> zbus::Result; + + /// Subdevice property + #[zbus(property)] + fn subdevice(&self) -> zbus::Result; + + /// SubdeviceId property + #[zbus(property)] + fn subdevice_id(&self) -> zbus::Result; + + /// SubvendorId property + #[zbus(property)] + fn subvendor_id(&self) -> zbus::Result; + + /// Vendor property + #[zbus(property)] + fn vendor(&self) -> zbus::Result; + + /// VendorId property + #[zbus(property)] + fn vendor_id(&self) -> zbus::Result; +} diff --git a/extensions/core/src/dbus/powerstation/connector.rs b/extensions/core/src/dbus/powerstation/connector.rs new file mode 100644 index 00000000..aea3599a --- /dev/null +++ b/extensions/core/src/dbus/powerstation/connector.rs @@ -0,0 +1,56 @@ +//! # D-Bus interface proxy for: `org.shadowblip.GPU.Card.Connector` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/Performance/GPU/card0/DP/1' from service 'org.shadowblip.PowerStation' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PeerProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.GPU.Card.Connector", + default_service = "org.shadowblip.PowerStation", + default_path = "/org/shadowblip/Performance/GPU/card0/DP/1" +)] +trait Connector { + /// DPMS property + #[zbus(property, name = "DPMS")] + fn dpms(&self) -> zbus::Result; + + /// Enabled property + #[zbus(property)] + fn enabled(&self) -> zbus::Result; + + /// Id property + #[zbus(property)] + fn id(&self) -> zbus::Result; + + /// Modes property + #[zbus(property)] + fn modes(&self) -> zbus::Result>; + + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; + + /// Path property + #[zbus(property)] + fn path(&self) -> zbus::Result; + + /// Status property + #[zbus(property)] + fn status(&self) -> zbus::Result; +} diff --git a/extensions/core/src/dbus/powerstation/core.rs b/extensions/core/src/dbus/powerstation/core.rs new file mode 100644 index 00000000..3790bdb6 --- /dev/null +++ b/extensions/core/src/dbus/powerstation/core.rs @@ -0,0 +1,42 @@ +//! # D-Bus interface proxy for: `org.shadowblip.CPU.Core` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/Performance/CPU/Core0' from service 'org.shadowblip.PowerStation' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.CPU.Core", + default_service = "org.shadowblip.PowerStation", + default_path = "/org/shadowblip/Performance/CPU/Core0" +)] +trait Core { + /// CoreId property + #[zbus(property)] + fn core_id(&self) -> zbus::Result; + + /// Number property + #[zbus(property)] + fn number(&self) -> zbus::Result; + + /// Online property + #[zbus(property)] + fn online(&self) -> zbus::Result; + #[zbus(property)] + fn set_online(&self, value: bool) -> zbus::Result<()>; +} diff --git a/extensions/core/src/dbus/powerstation/cpu.rs b/extensions/core/src/dbus/powerstation/cpu.rs new file mode 100644 index 00000000..b4f9d117 --- /dev/null +++ b/extensions/core/src/dbus/powerstation/cpu.rs @@ -0,0 +1,60 @@ +//! # D-Bus interface proxy for: `org.shadowblip.CPU` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/Performance/CPU' from service 'org.shadowblip.PowerStation' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.CPU", + default_service = "org.shadowblip.PowerStation", + default_path = "/org/shadowblip/Performance/CPU" +)] +trait CPU { + /// EnumerateCores method + fn enumerate_cores(&self) -> zbus::Result>; + + /// HasFeature method + fn has_feature(&self, flag: &str) -> zbus::Result; + + /// BoostEnabled property + #[zbus(property)] + fn boost_enabled(&self) -> zbus::Result; + #[zbus(property)] + fn set_boost_enabled(&self, value: bool) -> zbus::Result<()>; + + /// CoresCount property + #[zbus(property)] + fn cores_count(&self) -> zbus::Result; + + /// CoresEnabled property + #[zbus(property)] + fn cores_enabled(&self) -> zbus::Result; + #[zbus(property)] + fn set_cores_enabled(&self, value: u32) -> zbus::Result<()>; + + /// Features property + #[zbus(property)] + fn features(&self) -> zbus::Result>; + + /// SmtEnabled property + #[zbus(property)] + fn smt_enabled(&self) -> zbus::Result; + #[zbus(property)] + fn set_smt_enabled(&self, value: bool) -> zbus::Result<()>; +} diff --git a/extensions/core/src/dbus/powerstation/gpu.rs b/extensions/core/src/dbus/powerstation/gpu.rs new file mode 100644 index 00000000..f85c00cc --- /dev/null +++ b/extensions/core/src/dbus/powerstation/gpu.rs @@ -0,0 +1,31 @@ +//! # D-Bus interface proxy for: `org.shadowblip.GPU` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/Performance/GPU' from service 'org.shadowblip.PowerStation' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PeerProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.GPU", + default_service = "org.shadowblip.PowerStation", + default_path = "/org/shadowblip/Performance/GPU" +)] +trait GPU { + /// EnumerateCards method + fn enumerate_cards(&self) -> zbus::Result>; +} diff --git a/extensions/core/src/dbus/powerstation/tdp.rs b/extensions/core/src/dbus/powerstation/tdp.rs new file mode 100644 index 00000000..29ce4a73 --- /dev/null +++ b/extensions/core/src/dbus/powerstation/tdp.rs @@ -0,0 +1,52 @@ +//! # D-Bus interface proxy for: `org.shadowblip.GPU.Card.TDP` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/shadowblip/Performance/GPU/card0' from service 'org.shadowblip.PowerStation' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.shadowblip.GPU.Card.TDP", + default_service = "org.shadowblip.PowerStation", + default_path = "/org/shadowblip/Performance/GPU/card0" +)] +trait TDP { + /// Boost property + #[zbus(property)] + fn boost(&self) -> zbus::Result; + #[zbus(property)] + fn set_boost(&self, value: f64) -> zbus::Result<()>; + + /// PowerProfile property + #[zbus(property)] + fn power_profile(&self) -> zbus::Result; + #[zbus(property)] + fn set_power_profile(&self, value: &str) -> zbus::Result<()>; + + /// TDP property + #[zbus(property, name = "TDP")] + fn tdp(&self) -> zbus::Result; + #[zbus(property, name = "TDP")] + fn set_tdp(&self, value: f64) -> zbus::Result<()>; + + /// ThermalThrottleLimitC property + #[zbus(property)] + fn thermal_throttle_limit_c(&self) -> zbus::Result; + #[zbus(property)] + fn set_thermal_throttle_limit_c(&self, value: f64) -> zbus::Result<()>; +} diff --git a/extensions/core/src/dbus/upower.rs b/extensions/core/src/dbus/upower.rs new file mode 100644 index 00000000..18cf2051 --- /dev/null +++ b/extensions/core/src/dbus/upower.rs @@ -0,0 +1,2 @@ +pub mod device; +pub mod upower; diff --git a/extensions/core/src/dbus/upower/device.rs b/extensions/core/src/dbus/upower/device.rs new file mode 100644 index 00000000..5ea61037 --- /dev/null +++ b/extensions/core/src/dbus/upower/device.rs @@ -0,0 +1,162 @@ +//! # D-Bus interface proxy for: `org.freedesktop.UPower.Device` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/freedesktop/UPower/devices/DisplayDevice' from service 'org.freedesktop.UPower' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PeerProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.freedesktop.UPower.Device", + default_service = "org.freedesktop.UPower", + default_path = "/org/freedesktop/UPower/devices/DisplayDevice" +)] +trait Device { + /// GetHistory method + fn get_history( + &self, + type_: &str, + timespan: u32, + resolution: u32, + ) -> zbus::Result>; + + /// GetStatistics method + fn get_statistics(&self, type_: &str) -> zbus::Result>; + + /// Refresh method + fn refresh(&self) -> zbus::Result<()>; + + /// BatteryLevel property + #[zbus(property)] + fn battery_level(&self) -> zbus::Result; + + /// Capacity property + #[zbus(property)] + fn capacity(&self) -> zbus::Result; + + /// ChargeCycles property + #[zbus(property)] + fn charge_cycles(&self) -> zbus::Result; + + /// Energy property + #[zbus(property)] + fn energy(&self) -> zbus::Result; + + /// EnergyEmpty property + #[zbus(property)] + fn energy_empty(&self) -> zbus::Result; + + /// EnergyFull property + #[zbus(property)] + fn energy_full(&self) -> zbus::Result; + + /// EnergyFullDesign property + #[zbus(property)] + fn energy_full_design(&self) -> zbus::Result; + + /// EnergyRate property + #[zbus(property)] + fn energy_rate(&self) -> zbus::Result; + + /// HasHistory property + #[zbus(property)] + fn has_history(&self) -> zbus::Result; + + /// HasStatistics property + #[zbus(property)] + fn has_statistics(&self) -> zbus::Result; + + /// IconName property + #[zbus(property)] + fn icon_name(&self) -> zbus::Result; + + /// IsPresent property + #[zbus(property)] + fn is_present(&self) -> zbus::Result; + + /// IsRechargeable property + #[zbus(property)] + fn is_rechargeable(&self) -> zbus::Result; + + /// Luminosity property + #[zbus(property)] + fn luminosity(&self) -> zbus::Result; + + /// Model property + #[zbus(property)] + fn model(&self) -> zbus::Result; + + /// NativePath property + #[zbus(property)] + fn native_path(&self) -> zbus::Result; + + /// Online property + #[zbus(property)] + fn online(&self) -> zbus::Result; + + /// Percentage property + #[zbus(property)] + fn percentage(&self) -> zbus::Result; + + /// PowerSupply property + #[zbus(property)] + fn power_supply(&self) -> zbus::Result; + + /// Serial property + #[zbus(property)] + fn serial(&self) -> zbus::Result; + + /// State property + #[zbus(property)] + fn state(&self) -> zbus::Result; + + /// Technology property + #[zbus(property)] + fn technology(&self) -> zbus::Result; + + /// Temperature property + #[zbus(property)] + fn temperature(&self) -> zbus::Result; + + /// TimeToEmpty property + #[zbus(property)] + fn time_to_empty(&self) -> zbus::Result; + + /// TimeToFull property + #[zbus(property)] + fn time_to_full(&self) -> zbus::Result; + + /// Type property + #[zbus(property)] + fn type_(&self) -> zbus::Result; + + /// UpdateTime property + #[zbus(property)] + fn update_time(&self) -> zbus::Result; + + /// Vendor property + #[zbus(property)] + fn vendor(&self) -> zbus::Result; + + /// Voltage property + #[zbus(property)] + fn voltage(&self) -> zbus::Result; + + /// WarningLevel property + #[zbus(property)] + fn warning_level(&self) -> zbus::Result; +} diff --git a/extensions/core/src/dbus/upower/upower.rs b/extensions/core/src/dbus/upower/upower.rs new file mode 100644 index 00000000..b6f80298 --- /dev/null +++ b/extensions/core/src/dbus/upower/upower.rs @@ -0,0 +1,61 @@ +//! # D-Bus interface proxy for: `org.freedesktop.UPower` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/freedesktop/UPower' from service 'org.freedesktop.UPower' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PeerProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + interface = "org.freedesktop.UPower", + default_service = "org.freedesktop.UPower", + default_path = "/org/freedesktop/UPower" +)] +trait UPower { + /// EnumerateDevices method + fn enumerate_devices(&self) -> zbus::Result>; + + /// GetCriticalAction method + fn get_critical_action(&self) -> zbus::Result; + + /// GetDisplayDevice method + fn get_display_device(&self) -> zbus::Result; + + /// DeviceAdded signal + #[zbus(signal)] + fn device_added(&self, device: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; + + /// DeviceRemoved signal + #[zbus(signal)] + fn device_removed(&self, device: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; + + /// DaemonVersion property + #[zbus(property)] + fn daemon_version(&self) -> zbus::Result; + + /// LidIsClosed property + #[zbus(property)] + fn lid_is_closed(&self) -> zbus::Result; + + /// LidIsPresent property + #[zbus(property)] + fn lid_is_present(&self) -> zbus::Result; + + /// OnBattery property + #[zbus(property)] + fn on_battery(&self) -> zbus::Result; +} diff --git a/extensions/core/src/gamescope.rs b/extensions/core/src/gamescope.rs new file mode 100644 index 00000000..55da9cf0 --- /dev/null +++ b/extensions/core/src/gamescope.rs @@ -0,0 +1,147 @@ +pub mod x11_client; + +use std::collections::HashMap; +use std::env; +use x11_client::GamescopeXWayland; + +use godot::prelude::*; + +use godot::classes::Resource; + +#[derive(GodotClass)] +#[class(base=Resource)] +pub struct GamescopeInstance { + base: Base, + xwaylands: HashMap>, + xwayland_primary: String, + xwayland_ogui: String, + xwayland_game: String, +} + +#[godot_api] +impl GamescopeInstance { + /// Primary Gamescope xwayland instance + #[constant] + const XWAYLAND_TYPE_PRIMARY: u32 = 0; + + /// Xwayland instance that OpenGamepadUI is running on + #[constant] + const XWAYLAND_TYPE_OGUI: u32 = 1; + + /// Xwayland instance where games run + #[constant] + const XWAYLAND_TYPE_GAME: u32 = 2; + + /// Gamescope is hard-coded to look for STEAM_GAME=769 to determine if it is the + /// overlay app. + #[constant] + const OVERLAY_GAME_ID: u32 = 769; + + /// Return the Gamescope XWayland of the given type. + #[func] + pub fn get_xwayland(&self, kind: u32) -> Option> { + match kind { + GamescopeInstance::XWAYLAND_TYPE_PRIMARY => { + let xwayland = self.xwaylands.get(&self.xwayland_primary); + xwayland.cloned() + } + GamescopeInstance::XWAYLAND_TYPE_OGUI => { + let xwayland = self.xwaylands.get(&self.xwayland_ogui); + xwayland.cloned() + } + GamescopeInstance::XWAYLAND_TYPE_GAME => { + let xwayland = self.xwaylands.get(&self.xwayland_game); + xwayland.cloned() + } + _ => None, + } + } + + /// Return all known XWayland instances + #[func] + pub fn get_xwaylands(&self) -> Array> { + let mut xwaylands = array![]; + for xwayland in self.xwaylands.values() { + let item = xwayland.clone(); + xwaylands.push(item); + } + + xwaylands + } + + /// Returns the XWayland display with the given name (e.g. ":0") + #[func] + pub fn get_xwayland_by_name(&self, name: GString) -> Option> { + let name: String = name.into(); + self.xwaylands.get(&name).cloned() + } + + /// Process Gamescope signals and emit them as Godot signals. This method + /// should be called every frame in the "_process" loop of a node. + #[func] + pub fn process(&mut self) { + for (_, xwayland) in self.xwaylands.iter_mut() { + xwayland.bind_mut().process(); + } + } +} + +#[godot_api] +impl IResource for GamescopeInstance { + /// Called upon object initialization in the engine + fn init(base: Base) -> Self { + godot_print!("Initializing Gamescope instance"); + + // Discover any gamescope instances + let result = gamescope_x11_client::discover_gamescope_displays(); + let x11_displays = match result { + Ok(displays) => displays, + Err(e) => { + godot_error!("Failed to get Gamescope displays: {e:?}"); + return Self { + base, + xwaylands: HashMap::new(), + xwayland_primary: Default::default(), + xwayland_ogui: Default::default(), + xwayland_game: Default::default(), + }; + } + }; + + // Get the X11 display that the process knows about + let ogui_display = env::var("DISPLAY").unwrap_or(":0".into()); + + // Keep track of discovered XWaylands + let mut xwaylands = HashMap::new(); + let mut xwayland_primary = Default::default(); + let mut xwayland_ogui = Default::default(); + let mut xwayland_game = Default::default(); + + // Create an XWayland instance for each discovered XWayland display + for display in x11_displays { + godot_print!("Discovered XWayland display: {display}"); + let xwayland = GamescopeXWayland::new(display.as_str()); + + // Categorize the discovered displays + if display == ogui_display { + xwayland_ogui = display.clone(); + } + if xwayland.bind().get_is_primary() { + xwayland_primary = display.clone(); + } else { + xwayland_game = display.clone(); + } + + xwaylands.insert(display, xwayland); + } + + // Create a new Gamescope instance + Self { + base, + xwaylands, + xwayland_ogui, + xwayland_game, + xwayland_primary, + } + } +} diff --git a/extensions/core/src/gamescope/x11_client.rs b/extensions/core/src/gamescope/x11_client.rs new file mode 100644 index 00000000..caae60bf --- /dev/null +++ b/extensions/core/src/gamescope/x11_client.rs @@ -0,0 +1,960 @@ +use gamescope_x11_client::{ + atoms::GamescopeAtom, + xwayland::{BlurMode, Primary, XWayland}, +}; +use std::{ + collections::HashMap, + sync::mpsc::{channel, Receiver, Sender, TryRecvError}, + time::Duration, +}; +use tokio::task::AbortHandle; + +use godot::{obj::WithBaseField, prelude::*}; + +use godot::classes::{Resource, ResourceLoader}; + +use crate::RUNTIME; + +/// Signals that can be emitted +#[derive(Debug)] +enum Signal { + WindowCreated { window_id: u32 }, + WindowPropertyChanged { window_id: u32, property: String }, + PropertyChanged { property: String }, +} + +#[derive(GodotClass)] +#[class(no_init, base=Resource)] +pub struct GamescopeXWayland { + base: Base, + rx: Receiver, + tx: Sender, + xwayland: XWayland, + window_watch_handles: HashMap, + + /// The name of the XWayland instance (e.g. ":0") + #[var] + name: GString, + /// Returns true if this [GamescopeXWayland] is the primary instance + #[var] + is_primary: bool, + /// Returns the root window id of the [GamescopeXWayland] instance + #[var] + root_window_id: u32, + /// List of windows currently being watched for property changes + #[var(get = get_watched_windows)] + watched_windows: PackedInt64Array, + /// List of focusable apps + #[var(get = get_focusable_apps)] + focusable_apps: PackedInt64Array, + /// List of focusable windows + #[var(get = get_focusable_windows)] + focusable_windows: PackedInt64Array, + /// List of focusable window names + #[var(get = get_focusable_window_names)] + focusable_window_names: PackedStringArray, + /// Currently focused window id + #[var(get = get_focused_window)] + focused_window: u32, + /// Currently focused app id + #[var(get = get_focused_app)] + focused_app: u32, + /// Currently focused gfx app id + #[var(get = get_focused_app_gfx)] + focused_app_gfx: u32, + /// Whether or not the overlay window is currently focused + #[var(get = get_overlay_focused)] + overlay_focused: bool, + /// Current Gamescope FPS limit + #[var(get = get_fps_limit, set = set_fps_limit)] + fps_limit: u32, + /// Gamecope blur mode (0 - off, 1 - cond, 2 - always) + #[var(get = get_blur_mode, set = set_blur_mode)] + blur_mode: u32, + /// Gamescope blur radius + #[var(get = get_blur_radius, set = set_blur_radius)] + blur_radius: u32, + /// Whether or not Gamescope should be allowed to screen tear + #[var(get = get_allow_tearing, set = set_allow_tearing)] + allow_tearing: bool, + /// Current manually focused window + #[var(get = get_baselayer_window, set = set_baselayer_window)] + baselayer_window: u32, +} + +#[godot_api] +impl GamescopeXWayland { + #[constant] + const BLUR_MODE_OFF: u32 = 0; + #[constant] + const BLUR_MODE_COND: u32 = 1; + #[constant] + const BLUR_MODE_ALWAYS: u32 = 2; + + #[signal] + fn window_created(window_id: u32); + + #[signal] + fn window_property_updated(window_id: u32, property: GString); + + #[signal] + fn focused_app_updated(); + + #[signal] + fn focused_app_gfx_updated(); + + #[signal] + fn focusable_apps_updated(); + + #[signal] + fn focused_window_updated(); + + #[signal] + fn focusable_windows_updated(); + + #[signal] + fn baselayer_window_updated(); + + /// Create a new [GamescopeXWayland] with the given name (e.g. ":0") + pub fn from_name(name: GString) -> Gd { + // Create a channel to communicate with the signals task + godot_print!("Gamescope XWayland created with name: {name}"); + let (tx, rx) = channel(); + + // Create an XWayland client instance for this display + let mut xwayland = XWayland::new(name.clone().into()); + if let Err(e) = xwayland.connect() { + godot_error!("Failed to connect to XWayland display '{name}': {e:?}"); + } + let is_primary = xwayland.is_primary_instance().unwrap_or_default(); + let root_window_id = xwayland.get_root_window_id().unwrap_or_default(); + + // If this XWayland instance is a primary instance, listen for signals + if is_primary { + // Spawn a task to listen for property changes + if let Ok((_, property_rx)) = xwayland.listen_for_property_changes() { + let signals_tx = tx.clone(); + RUNTIME.spawn_blocking(move || { + for event in property_rx.into_iter() { + let signal = Signal::PropertyChanged { property: event }; + if let Err(e) = signals_tx.send(signal) { + godot_error!("Error sending property changed signal: {e:?}"); + break; + } + } + }); + } else { + godot_error!("Failed to listen for XWayland property changes"); + } + } + + // Spawn a task to listen for window creation events + if let Ok((_, windows_rx)) = xwayland.listen_for_window_created() { + let signals_tx = tx.clone(); + RUNTIME.spawn_blocking(move || { + for window_id in windows_rx.into_iter() { + let signal = Signal::WindowCreated { window_id }; + if let Err(e) = signals_tx.send(signal) { + godot_error!("Error sending window created signal: {e:?}"); + break; + } + } + }); + } else { + godot_error!("Failed to listen for XWayland windows created"); + } + + // Setup the initial state + Gd::from_init_fn(|base| { + // Accept a base of type Base and directly forward it. + Self { + base, + rx, + tx, + name, + xwayland, + is_primary, + root_window_id, + watched_windows: Default::default(), + window_watch_handles: Default::default(), + focusable_apps: Default::default(), + focusable_windows: Default::default(), + focusable_window_names: Default::default(), + focused_window: Default::default(), + focused_app: Default::default(), + focused_app_gfx: Default::default(), + overlay_focused: Default::default(), + fps_limit: Default::default(), + blur_mode: Default::default(), + blur_radius: Default::default(), + allow_tearing: Default::default(), + baselayer_window: Default::default(), + } + }) + } + + /// Get or create a [GamescopeXWayland] with the given name. If an instance + /// already exists with the given path, then it will be loaded from the resource + /// cache. + pub fn new(name: &str) -> Gd { + let res_path = format!("gamescope://xwayland/{name}"); + + // Check to see if a resource already exists for this device + let mut resource_loader = ResourceLoader::singleton(); + if resource_loader.exists(res_path.clone().into()) { + if let Some(res) = resource_loader.load(res_path.clone().into()) { + godot_print!( + "Resource already exists with path '{res_path}', loading that instead" + ); + let device: Gd = res.cast(); + device + } else { + let mut device = GamescopeXWayland::from_name(name.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } else { + let mut device = GamescopeXWayland::from_name(name.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } + + /// Returns the list of currently watched windows. + #[func] + pub fn get_watched_windows(&self) -> PackedInt64Array { + self.watched_windows.clone() + } + + /// Start watching the given window. The [WindowPropertyChanged] signal + /// will fire whenever a window property changes on the window. Use + /// [unwatch_window] to stop watching the given window. + #[func] + pub fn watch_window(&mut self, window_id: u32) -> i32 { + if self.watched_windows.contains(&(window_id as i64)) { + godot_warn!("Window {window_id} is already being watched"); + return 0; + } + self.watched_windows.push(window_id as i64); + + // Spawn a new thread to listen for window property changes + let (_, rx) = match self.xwayland.listen_for_window_property_changes(window_id) { + Ok(result) => result, + Err(e) => { + godot_error!("Failed to watch window properties for window '{window_id}': {e:?}"); + return -1; + } + }; + + // Spawn a task to listen for window changes and emit signals + let signals_tx = self.tx.clone(); + let task = RUNTIME.spawn(async move { + godot_print!("Started listening for property changes on window: {window_id}"); + // NOTE: only async tasks support abort, so we need to resort to polling here + 'outer: loop { + // Consume all messages from the channel and emit signals + 'inner: loop { + let event = match rx.try_recv() { + Ok(value) => value, + Err(e) => match e { + TryRecvError::Empty => break 'inner, + TryRecvError::Disconnected => { + godot_error!("Backend thread is not running!"); + return; + } + }, + }; + let signal = Signal::WindowPropertyChanged { + window_id, + property: event, + }; + if let Err(e) = signals_tx.send(signal) { + godot_error!("Failed to send property change signal: {e:?}"); + break 'outer; + } + } + + tokio::time::sleep(Duration::from_millis(40)).await; + } + + godot_print!("Stopped listening for property changes on window: {window_id}"); + }); + + // Keep a list of abort handles so the watch task can be cancelled. + self.window_watch_handles + .insert(window_id, task.abort_handle()); + + 0 + } + + /// Stop watching the given window. The [WindowPropertyChanged] signal will + /// no longer fire for the given window. + #[func] + pub fn unwatch_window(&mut self, window_id: u32) -> i32 { + let window_id = window_id as i64; + if !self.watched_windows.contains(&window_id) { + return 0; + } + if let Some(idx) = self.watched_windows.find(&window_id, None) { + self.watched_windows.remove(idx); + } + + // Cancel the listener task + let Some(task) = self.window_watch_handles.get(&(window_id as u32)) else { + godot_error!("Task wasn't found but was being watched: {window_id}"); + return -1; + }; + + task.abort(); + + 0 + } + + /// Discover the process IDs that are associated with the given window + #[func] + pub fn get_pids_for_window(&self, window_id: u32) -> PackedInt64Array { + let pids = match self.xwayland.get_pids_for_window(window_id) { + Ok(pids) => pids, + Err(e) => { + godot_error!("Failed to get pids for window '{window_id}': {e:?}"); + return PackedInt64Array::new(); + } + }; + let pids: Vec = pids.into_iter().map(|pid| pid as i64).collect(); + + pids.into() + } + + /// Returns the window id(s) for the given process ID. + #[func] + pub fn get_windows_for_pid(&self, pid: u32) -> PackedInt64Array { + let windows = match self.xwayland.get_windows_for_pid(pid) { + Ok(windows) => windows, + Err(e) => { + godot_error!("Failed to get windows for pid '{pid}': {e:?}"); + return PackedInt64Array::new(); + } + }; + let windows: Vec = windows.into_iter().map(|id| id as i64).collect(); + + windows.into() + } + + /// Returns the window name of the given window + #[func] + fn get_window_name(&self, window_id: u32) -> GString { + let name = match self.xwayland.get_window_name(window_id) { + Ok(name) => name, + Err(e) => { + godot_error!("Failed to get window name for window '{window_id}': {e:?}"); + return "".into(); + } + }; + + name.unwrap_or_default().into() + } + + /// Returns the window ids of the children of the given window + #[func] + fn get_window_children(&self, window_id: u32) -> PackedInt64Array { + let windows = match self.xwayland.get_window_children(window_id) { + Ok(windows) => windows, + Err(e) => { + godot_error!("Failed to get window children for window '{window_id}': {e:?}"); + return PackedInt64Array::new(); + } + }; + let windows: Vec = windows.into_iter().map(|id| id as i64).collect(); + + windows.into() + } + + /// Recursively returns all child windows of the given window id + #[func] + fn get_all_windows(&self, window_id: u32) -> PackedInt64Array { + let windows = match self.xwayland.get_all_windows(window_id) { + Ok(windows) => windows, + Err(e) => { + godot_error!("Failed to get all window children for window '{window_id}': {e:?}"); + return PackedInt64Array::new(); + } + }; + let windows: Vec = windows.into_iter().map(|id| id as i64).collect(); + + windows.into() + } + + /// Returns the currently set app ID on the given window. Returns zero if no + /// app id was found. + #[func] + fn get_app_id(&self, window_id: u32) -> u32 { + match self.xwayland.get_app_id(window_id) { + Ok(app_id) => app_id.unwrap_or_default(), + Err(e) => { + godot_error!("Failed to get app id for window '{window_id}': {e:?}"); + 0 + } + } + } + + /// Sets the app ID on the given window. Returns zero if operation succeeds. + #[func] + fn set_app_id(&self, window_id: u32, app_id: u32) -> i32 { + if let Err(e) = self.xwayland.set_app_id(window_id, app_id) { + godot_error!("Failed to set app id {app_id} on window '{window_id}': {e:?}"); + return -1; + } + 0 + } + + /// Removes the app ID on the given window. Returns zero if operation succeeds. + #[func] + fn remove_app_id(&self, window_id: u32) -> i32 { + if let Err(e) = self + .xwayland + .remove_xprop(window_id, GamescopeAtom::SteamGame) + { + godot_error!("Failed to remove app id from window '{window_id}': {e:?}"); + return -1; + } + 0 + } + + /// Returns whether or not the given window has an app ID set + #[func] + fn has_app_id(&self, window_id: u32) -> bool { + match self.xwayland.has_app_id(window_id) { + Ok(v) => v, + Err(e) => { + godot_error!("Failed to check window '{window_id}' for app id: {e:?}"); + false + } + } + } + + /// --- XWayland Primary --- + + /// Return a list of focusable apps + #[func] + fn get_focusable_apps(&mut self) -> PackedInt64Array { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + let value = match self.xwayland.get_focusable_apps() { + Ok(value) => value, + Err(e) => { + godot_error!("Failed to get focusable apps: {e:?}"); + return Default::default(); + } + }; + let Some(focusable) = value else { + return Default::default(); + }; + let focusable: Vec = focusable.into_iter().map(|v| v as i64).collect(); + self.focusable_apps = focusable.into(); + self.focusable_apps.clone() + } + + /// Return a list of focusable windows + #[func] + fn get_focusable_windows(&mut self) -> PackedInt64Array { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + let value = match self.xwayland.get_focusable_windows() { + Ok(value) => value, + Err(e) => { + godot_error!("Failed to get focusable windows: {e:?}"); + return Default::default(); + } + }; + let Some(focusable) = value else { + return Default::default(); + }; + let focusable: Vec = focusable.into_iter().map(|v| v as i64).collect(); + self.focusable_windows = focusable.into(); + self.focusable_windows.clone() + } + + /// Returns a list of focusable window names + #[func] + fn get_focusable_window_names(&mut self) -> PackedStringArray { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + let value = match self.xwayland.get_focusable_window_names() { + Ok(value) => value, + Err(e) => { + godot_error!("Failed to get focusable windows: {e:?}"); + return Default::default(); + } + }; + let value: Vec = value.into_iter().map(GString::from).collect(); + self.focusable_window_names = value.into(); + self.focusable_window_names.clone() + } + + /// Return the currently focused window id. + #[func] + fn get_focused_window(&mut self) -> u32 { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + let value = match self.xwayland.get_focused_window() { + Ok(value) => value, + Err(e) => { + godot_error!("Failed to get focused window: {e:?}"); + return Default::default(); + } + }; + + self.focused_window = value.unwrap_or_default(); + self.focused_window + } + + /// Return the currently focused app id. + #[func] + fn get_focused_app(&mut self) -> u32 { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + let value = match self.xwayland.get_focused_app() { + Ok(value) => value, + Err(e) => { + godot_error!("Failed to get focused app: {e:?}"); + return Default::default(); + } + }; + + self.focused_app = value.unwrap_or_default(); + self.focused_app + } + + /// Return the currently focused gfx app id. + #[func] + fn get_focused_app_gfx(&mut self) -> u32 { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + let value = match self.xwayland.get_focused_app_gfx() { + Ok(value) => value, + Err(e) => { + godot_error!("Failed to get focused app gfx: {e:?}"); + return Default::default(); + } + }; + + self.focused_app_gfx = value.unwrap_or_default(); + self.focused_app_gfx + } + + /// Returns whether or not the overlay window is currently focused + #[func] + fn get_overlay_focused(&mut self) -> bool { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + + let focused = match self.xwayland.is_overlay_focused() { + Ok(value) => value, + Err(e) => { + godot_error!("Failed to get overlay focused: {e:?}"); + Default::default() + } + }; + self.overlay_focused = focused; + self.overlay_focused + } + + /// The current Gamescope FPS limit + #[func] + fn get_fps_limit(&mut self) -> u32 { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + let value = match self.xwayland.get_fps_limit() { + Ok(value) => value, + Err(e) => { + godot_error!("Failed to get fps limit: {e:?}"); + return Default::default(); + } + }; + + self.fps_limit = value.unwrap_or_default(); + self.fps_limit + } + + /// Sets the current Gamescope FPS limit + #[func] + fn set_fps_limit(&mut self, fps: u32) { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return; + } + if let Err(e) = self.xwayland.set_fps_limit(fps) { + godot_error!("Failed to set FPS limit to {fps}: {e:?}"); + } + self.fps_limit = fps; + } + + /// The Gamescope blur mode (0 - off, 1 - cond, 2 - always) + #[func] + fn get_blur_mode(&mut self) -> u32 { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + let value = match self.xwayland.get_blur_mode() { + Ok(value) => value, + Err(e) => { + godot_error!("Failed to get blur mode: {e:?}"); + return Default::default(); + } + }; + let Some(mode) = value else { + return Default::default(); + }; + + let blur_mode = match mode { + BlurMode::Off => 0, + BlurMode::Cond => 1, + BlurMode::Always => 2, + }; + self.blur_mode = blur_mode; + self.blur_mode + } + + /// Sets the Gamescope blur mode + #[func] + fn set_blur_mode(&mut self, mode: u32) { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + let blur_mode = match mode { + 0 => BlurMode::Off, + 1 => BlurMode::Cond, + 2 => BlurMode::Always, + _ => BlurMode::Off, + }; + if let Err(e) = self.xwayland.set_blur_mode(blur_mode) { + godot_error!("Failed to set blur mode to: {mode}: {e:?}"); + } + self.blur_mode = mode; + } + + // The blur radius size + #[func] + fn get_blur_radius(&self) -> u32 { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + self.blur_radius + } + + /// Sets the blur radius size + #[func] + fn set_blur_radius(&mut self, radius: u32) { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return; + } + if let Err(e) = self.xwayland.set_blur_radius(radius) { + godot_error!("Failed to set blur radius to: {radius}: {e:?}"); + } + self.blur_radius = radius; + } + + /// Whether or not Gamescope should be allowed to screen tear + #[func] + fn get_allow_tearing(&self) -> bool { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + self.allow_tearing + } + + /// Sets whether or not Gamescope should be allowed to screen tear + #[func] + fn set_allow_tearing(&mut self, allow: bool) { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return; + } + if let Err(e) = self.xwayland.set_allow_tearing(allow) { + godot_error!("Failed to set allow tearing to: {allow}: {e:?}"); + } + self.allow_tearing = allow; + } + + /// Returns true if the window with the given window ID exists in focusable apps + #[func] + fn is_focusable_app(&self, window_id: u32) -> bool { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + match self.xwayland.is_focusable_app(window_id) { + Ok(is_focusable) => is_focusable, + Err(e) => { + godot_error!("Failed to check if window '{window_id}' is focusable app: {e:?}"); + Default::default() + } + } + } + + /// Sets the given window as the main launcher app. This will set an X window + /// property called STEAM_GAME to 769 (Steam), which will make Gamescope + /// treat the window as the main overlay. + #[func] + fn set_main_app(&self, window_id: u32) -> i32 { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + if let Err(e) = self.xwayland.set_main_app(window_id) { + godot_error!("Failed to set window '{window_id}' as main app: {e:?}"); + return -1; + } + 0 + } + + /// Set the given window as the primary overlay input focus. This should be set to + /// "1" whenever the overlay wants to intercept input from a game. + #[func] + fn set_input_focus(&self, window_id: u32, value: u32) -> i32 { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + if let Err(e) = self.xwayland.set_input_focus(window_id, value) { + godot_error!("Failed to set input focus on '{window_id}' to '{value}': {e:?}"); + return -1; + } + 0 + } + + /// Get the overlay status for the given window + #[func] + fn get_overlay(&self, window_id: u32) -> u32 { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + match self.xwayland.get_overlay(window_id) { + Ok(value) => value.unwrap_or_default(), + Err(e) => { + godot_error!("Failed to get overlay status for window '{window_id}': {e:?}"); + 0 + } + } + } + + /// Set the given window as the main overlay window + #[func] + fn set_overlay(&self, window_id: u32, value: u32) -> i32 { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + if let Err(e) = self.xwayland.set_overlay(window_id, value) { + godot_error!("Failed to set overlay on '{window_id}' to '{value}': {e:?}"); + return -1; + } + 0 + } + + /// Set the given window as a notification. This should be set to "1" when some + /// UI wants to be shown but not intercept input. + #[func] + fn set_notification(&self, window_id: u32, value: u32) -> i32 { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + if let Err(e) = self.xwayland.set_notification(window_id, value) { + godot_error!("Failed to set notification on '{window_id}' to '{value}': {e:?}"); + return -1; + } + 0 + } + + /// Set the given window as an external overlay window + #[func] + fn set_external_overlay(&self, window_id: u32, value: u32) -> i32 { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + if let Err(e) = self.xwayland.set_external_overlay(window_id, value) { + godot_error!("Failed to set external overlay on '{window_id}' to '{value}': {e:?}"); + return -1; + } + 0 + } + + /// Returns the currently set manual focus + #[func] + fn get_baselayer_window(&mut self) -> u32 { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return Default::default(); + } + let value = match self.xwayland.get_baselayer_window() { + Ok(value) => value, + Err(e) => { + godot_error!("Failed to get baselayer window: {e:?}"); + return Default::default(); + } + }; + + self.baselayer_window = value.unwrap_or_default(); + self.baselayer_window + } + + /// Focuses the given window + #[func] + fn set_baselayer_window(&mut self, window_id: u32) { + if !self.is_primary { + godot_error!("XWayland instance is not primary!"); + return; + } + if let Err(e) = self.xwayland.set_baselayer_window(window_id) { + godot_error!("Failed to set baselayer window to {window_id}: {e:?}"); + } + self.baselayer_window = window_id; + } + + /// Removes the baselayer property to un-focus windows + #[func] + fn remove_baselayer_window(&mut self) { + if let Err(e) = self.xwayland.remove_baselayer_window() { + godot_error!("Failed to remove baselayer window: {e:?}"); + } + self.baselayer_window = 0; + } + + /// Request a screenshot from Gamescope + #[func] + fn request_screenshot(&self) { + if let Err(e) = self.xwayland.request_screenshot() { + godot_error!("Failed to request screenshot: {e:?}"); + } + } + + /// Dispatches signals, called by [GamescopeInstance] + pub fn process(&mut self) { + // Drain all messages from the channel to process them + loop { + let signal = match self.rx.try_recv() { + Ok(value) => value, + Err(e) => match e { + TryRecvError::Empty => break, + TryRecvError::Disconnected => { + godot_error!("Backend thread is not running!"); + return; + } + }, + }; + self.process_signal(signal); + } + } + + /// Process and dispatch the given signal + fn process_signal(&mut self, signal: Signal) { + godot_print!("Got signal: {signal:?}"); + match signal { + Signal::WindowCreated { window_id } => { + self.base_mut() + .emit_signal("window_created".into(), &[window_id.to_variant()]); + } + Signal::WindowPropertyChanged { + window_id, + property, + } => { + self.base_mut().emit_signal( + "window_property_updated".into(), + &[window_id.to_variant(), property.into_godot().to_variant()], + ); + } + Signal::PropertyChanged { property } => { + match property { + property if property == GamescopeAtom::FocusedApp.to_string() => { + let from = self.focused_app; + let to = self.get_focused_app(); + self.base_mut().emit_signal( + "focused_app_updated".into(), + &[from.to_variant(), to.to_variant()], + ); + } + property if property == GamescopeAtom::FocusedAppGFX.to_string() => { + let from = self.focused_app_gfx; + let to = self.get_focused_app_gfx(); + self.base_mut().emit_signal( + "focused_app_gfx_updated".into(), + &[from.to_variant(), to.to_variant()], + ); + } + property if property == GamescopeAtom::FocusableApps.to_string() => { + let from = self.focusable_apps.clone(); + let to = self.get_focusable_apps(); + self.base_mut().emit_signal( + "focusable_apps_updated".into(), + &[from.to_variant(), to.to_variant()], + ); + } + property if property == GamescopeAtom::FocusedWindow.to_string() => { + let from = self.focused_window; + let to = self.get_focused_window(); + self.base_mut().emit_signal( + "focused_window_updated".into(), + &[from.to_variant(), to.to_variant()], + ); + } + property if property == GamescopeAtom::FocusableWindows.to_string() => { + let from = self.focusable_windows.clone(); + let to = self.get_focusable_windows(); + self.base_mut().emit_signal( + "focusable_windows_updated".into(), + &[from.to_variant(), to.to_variant()], + ); + } + property if property == GamescopeAtom::BaselayerWindow.to_string() => { + let from = self.baselayer_window; + let to = self.get_baselayer_window(); + self.base_mut().emit_signal( + "baselayer_window_updated".into(), + &[from.to_variant(), to.to_variant()], + ); + } + _ => { + // Unknown prop changed + } + } + } + } + } +} + +#[godot_api] +impl IResource for GamescopeXWayland { + fn to_string(&self) -> GString { + format!("", self.name).into() + } +} + +impl Drop for GamescopeXWayland { + fn drop(&mut self) { + godot_print!("Gamescope XWayland '{}' is being destroyed!", self.name); + } +} diff --git a/extensions/core/src/input.rs b/extensions/core/src/input.rs new file mode 100644 index 00000000..f672b911 --- /dev/null +++ b/extensions/core/src/input.rs @@ -0,0 +1 @@ +pub mod inputplumber; diff --git a/extensions/core/src/input/inputplumber.rs b/extensions/core/src/input/inputplumber.rs new file mode 100644 index 00000000..a9b942f7 --- /dev/null +++ b/extensions/core/src/input/inputplumber.rs @@ -0,0 +1,529 @@ +pub mod composite_device; +pub mod dbus_device; +pub mod event_device; +pub mod keyboard_device; +pub mod mouse_device; + +use dbus_device::DBusDevice; +use futures_util::stream::StreamExt; +use std::collections::HashMap; +use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError}; +use std::time::Duration; + +use composite_device::CompositeDevice; +use godot::prelude::*; + +use godot::classes::Resource; +use zbus::fdo::ObjectManagerProxy; +use zbus::names::BusName; + +use crate::dbus::RunError; +use crate::{get_dbus_system, get_dbus_system_blocking, RUNTIME}; + +const INPUT_PLUMBER_BUS: &str = "org.shadowblip.InputPlumber"; +const INPUT_PLUMBER_PATH: &str = "/org/shadowblip/InputPlumber"; + +/// Supported InputPlumber DBus objects +#[derive(Debug)] +enum ObjectType { + Unknown, + CompositeDevice, + SourceEventDevice, + SourceHidRawDevice, + SourceIioDevice, + TargetDBusDevice, + TargetGamepadDevice, + TargetKeyboardDevice, + TargetMouseDevice, +} + +impl ObjectType { + fn from_dbus_path(path: &str) -> Self { + if path.contains("CompositeDevice") { + return Self::CompositeDevice; + } + if path.contains("dbus") { + return Self::TargetDBusDevice; + } + if path.contains("target") && path.contains("mouse") { + return Self::TargetMouseDevice; + } + if path.contains("target") && path.contains("keyboard") { + return Self::TargetKeyboardDevice; + } + if path.contains("target") && path.contains("gamepad") { + return Self::TargetGamepadDevice; + } + if path.contains("source") && path.contains("event") { + return Self::SourceEventDevice; + } + if path.contains("source") && path.contains("hidraw") { + return Self::SourceHidRawDevice; + } + if path.contains("source") && path.contains("iio") { + return Self::SourceIioDevice; + } + Self::Unknown + } +} + +/// Signals that can be emitted +#[derive(Debug)] +enum Signal { + Started, + Stopped, + ObjectAdded { path: String, kind: ObjectType }, + ObjectRemoved { path: String, kind: ObjectType }, +} + +/// Instance representing a client connection to InputPlumber over DBus. This +/// is represented as a resource so it can be accessed from anywhere in the scene +/// tree, but there must be a node that calls 'process()' on this resource every +/// frame in order to emit signals and process messages. +#[derive(GodotClass)] +#[class(base=Resource)] +pub struct InputPlumberInstance { + base: Base, + rx: Receiver, + conn: Option, + /// Map of DBus path to composite device resource. E.g. + /// {"/org/shadowblip/InputPlumber/CompositeDevice0": } + composite_devices: HashMap>, + /// Map of DBus path to dbus device resource. E.g. + /// {"/org/shadowblip/InputPlumber/target/dbus0": } + dbus_devices: HashMap>, + /// The current intercept mode set for all devices + #[var(get = get_intercept_mode, set = set_intercept_mode)] + intercept_mode: i64, + /// The current events that will trigger intercept mode + #[var(get = get_intercept_triggers, set = set_intercept_triggers)] + intercept_triggers: PackedStringArray, + /// The current target event for intercept mode + #[var(get = get_intercept_target, set = set_intercept_target)] + intercept_target: GString, +} + +#[godot_api] +impl InputPlumberInstance { + #[constant] + const INTERCEPT_MODE_NONE: i32 = 0; + #[constant] + const INTERCEPT_MODE_PASS: i32 = 1; + #[constant] + const INTERCEPT_MODE_ALL: i32 = 2; + + /// Emitted when InputPlumber is detected as running + #[signal] + fn started(); + + /// Emitted when InputPlumber is detected as stopped + #[signal] + fn stopped(); + + /// Emitted when a CompositeDevice is dicovered and identified as a new device + #[signal] + fn composite_device_added(device: Gd); + + /// Emitted when a CompositeDevice is removed + #[signal] + fn composite_device_removed(dbus_path: GString); + + /// Returns true if the InputPlumber service is currently running + #[func] + fn is_running(&self) -> bool { + let Some(conn) = self.conn.as_ref() else { + return false; + }; + let bus = BusName::from_static_str(INPUT_PLUMBER_BUS).unwrap(); + let dbus = zbus::blocking::fdo::DBusProxy::new(conn).ok(); + let Some(dbus) = dbus else { + return false; + }; + dbus.name_has_owner(bus.clone()).unwrap_or_default() + } + + /// Returns the [CompositeDevice] with the given DBus path. If the device + /// does not exist, null will be returned. + #[func] + fn get_composite_device(&self, dbus_path: GString) -> Option> { + let path = String::from(dbus_path); + let device = self.composite_devices.get(&path)?; + Some(device.clone()) + } + + /// Return all current composite devices + #[func] + fn get_composite_devices(&mut self) -> Array> { + let mut devices = array![]; + let objects = match self.get_managed_objects() { + Ok(paths) => paths, + Err(e) => { + godot_error!("Failed to get managed objects: {e:?}"); + return devices; + } + }; + + for path in objects { + if !path.contains("CompositeDevice") { + continue; + } + let device = CompositeDevice::new(path.as_str()); + devices.push(device); + } + + devices + } + + /// Returns the [DBusDevice] with the given DBus path. If the device + /// does not exist, null will be returned. + #[func] + fn get_dbus_device(&self, dbus_path: GString) -> Option> { + let path = String::from(dbus_path); + let device = self.dbus_devices.get(&path)?; + Some(device.clone()) + } + + /// Return all current dbus devices + #[func] + fn get_dbus_devices(&mut self) -> Array> { + let mut devices = array![]; + let objects = match self.get_managed_objects() { + Ok(paths) => paths, + Err(e) => { + godot_error!("Failed to get managed objects: {e:?}"); + return devices; + } + }; + + for path in objects { + if !path.contains("target/dbus") { + continue; + } + let device = DBusDevice::new(path.as_str()); + devices.push(device); + } + + devices + } + + /// Get managed objects + fn get_managed_objects(&self) -> Result, zbus::fdo::Error> { + let Some(conn) = self.conn.as_ref() else { + return Err(zbus::fdo::Error::Disconnected( + "No DBus connection found".into(), + )); + }; + + let bus = BusName::from_static_str(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 Ok(Vec::new()); + }; + + Ok(object_manager + .get_managed_objects()? + .keys() + .map(|v| v.to_string()) + .collect()) + } + + /// 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 signals from tracked devices + for (_, device) in self.dbus_devices.iter_mut() { + device.bind_mut().process(); + } + for (_, device) in self.composite_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 => { + godot_print!("CompositeDevice added: {path}"); + 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 => { + godot_print!("DBusDevice added: {path}"); + 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 => { + godot_print!("CompositeDevice device removed: {path}"); + 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 => { + godot_print!("DBusDevice device removed: {path}"); + 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 = get_dbus_system_blocking().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); + } + let dbus_devices = instance.get_dbus_devices(); + for dbus_device in dbus_devices.iter_shared() { + let path = dbus_device.bind().get_dbus_path(); + instance + .dbus_devices + .insert(path.into(), dbus_device.clone()); + } + 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 = get_dbus_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..b278c66a --- /dev/null +++ b/extensions/core/src/input/inputplumber/composite_device.rs @@ -0,0 +1,325 @@ +use godot::prelude::*; + +use godot::classes::{ProjectSettings, Resource, ResourceLoader}; + +use crate::dbus::inputplumber::composite_device::CompositeDeviceProxyBlocking; +use crate::dbus::DBusVariant; +use crate::get_dbus_system_blocking; + +use super::dbus_device::DBusDevice; +use super::{InputPlumberInstance, 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 = get_dbus_system_blocking().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 with path '{res_path}', 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..7808f4f9 --- /dev/null +++ b/extensions/core/src/input/inputplumber/dbus_device.rs @@ -0,0 +1,219 @@ +use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError}; + +use futures_util::StreamExt; +use godot::prelude::*; + +use godot::classes::{Resource, ResourceLoader}; + +use crate::dbus::inputplumber::dbus_device::DBusDeviceProxy; +use crate::{get_dbus_system, 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 + godot_print!("DBusDevice created with path: {path}"); + let (tx, rx) = channel(); + let dbus_path = path.clone().into(); + + // Spawn a task using the shared tokio runtime to listen for signals + RUNTIME.spawn(async move { + if let Err(e) = run(tx, dbus_path).await { + godot_error!("Failed to run DBusDevice task: ${e:?}"); + } + }); + + Gd::from_init_fn(|base| { + // 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 with path '{res_path}', 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) { + godot_print!("Got 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(), + ], + ); + } + } + } +} + +impl Drop for DBusDevice { + fn drop(&mut self) { + godot_print!("DBusDevice '{}' is being destroyed!", self.dbus_path); + } +} + +/// Run the signals task +async fn run(tx: Sender, path: String) -> Result<(), RunError> { + // Establish a connection to the system bus + let conn = get_dbus_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; + } + } + godot_print!("DBusDevice input_event task stopped"); + }); + + 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; + } + } + godot_print!("DBusDevice touch_event task stopped"); + }); + + 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..5dcba5cf --- /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, get_dbus_system_blocking}; + +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 = get_dbus_system_blocking().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..c6c06629 --- /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, get_dbus_system_blocking}; + +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 = get_dbus_system_blocking().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..4ef93135 --- /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, get_dbus_system_blocking}; + +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 = get_dbus_system_blocking().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..520a984e --- /dev/null +++ b/extensions/core/src/lib.rs @@ -0,0 +1,118 @@ +pub mod bluetooth; +pub mod dbus; +pub mod gamescope; +pub mod input; +pub mod performance; +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, + }, +}; +use zbus::{ + blocking::{self}, + Connection, +}; + +/// Channel for shutting down the tokio runtime +type Channel = (Sender<()>, Arc>>); + +/// Global tokio runtime instance +pub static RUNTIME: Lazy = Lazy::new(tokio_init); +/// Shared connection to the DBus system bus +static DBUS_SYSTEM: Lazy>>> = Lazy::new(dbus_system_init); +/// Shared blocking connection to the DBus system bus +static DBUS_SYSTEM_BLOCKING: Lazy> = + Lazy::new(dbus_system_blocking_init); +/// Channel used to signal shutting down tokio runtime +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))) +} + +fn dbus_system_init() -> Arc>> { + Arc::new(Mutex::new(None)) +} + +fn dbus_system_blocking_init() -> Option { + blocking::Connection::system().ok() +} + +/// Return or create a shared connection to the DBus system bus +pub async fn get_dbus_system() -> Result { + let mut conn = DBUS_SYSTEM.lock().await; + if conn.is_some() { + let conn_clone = conn.as_ref().unwrap().clone(); + Ok(conn_clone) + } else { + let new_conn = Connection::system().await?; + let conn_clone = new_conn.clone(); + *conn = Some(new_conn); + Ok(conn_clone) + } +} + +/// Return or create a shared blocking connection to the DBus system bus +pub fn get_dbus_system_blocking() -> Result { + if let Some(conn) = DBUS_SYSTEM_BLOCKING.as_ref() { + return Ok(conn.clone()); + } + blocking::Connection::system() +} diff --git a/extensions/core/src/performance.rs b/extensions/core/src/performance.rs new file mode 100644 index 00000000..6ac1c52c --- /dev/null +++ b/extensions/core/src/performance.rs @@ -0,0 +1 @@ +pub mod powerstation; diff --git a/extensions/core/src/performance/powerstation.rs b/extensions/core/src/performance/powerstation.rs new file mode 100644 index 00000000..24305163 --- /dev/null +++ b/extensions/core/src/performance/powerstation.rs @@ -0,0 +1,213 @@ +pub mod cpu; +pub mod cpu_core; +pub mod gpu; +pub mod gpu_card; +pub mod gpu_connector; + +use std::{ + sync::mpsc::{channel, Receiver, Sender, TryRecvError}, + time::Duration, +}; + +use cpu::Cpu; +use godot::prelude::*; +use gpu::Gpu; +use zbus::names::BusName; + +use crate::{dbus::RunError, get_dbus_system, get_dbus_system_blocking, RUNTIME}; + +pub const POWERSTATION_BUS: &str = "org.shadowblip.PowerStation"; +const POWERSTATION_CPU_PATH: &str = "/org/shadowblip/Performance/CPU"; +const POWERSTATION_GPU_PATH: &str = "/org/shadowblip/Performance/GPU"; + +/// Signals that can be emitted +#[derive(Debug)] +enum Signal { + Started, + Stopped, +} + +#[derive(GodotClass)] +#[class(base=Resource)] +pub struct PowerStationInstance { + base: Base, + rx: Receiver, + conn: Option, + cpu_instance: Option>, + gpu_instance: Option>, + + #[allow(dead_code)] + #[var(get = get_cpu)] + cpu: Option>, + #[allow(dead_code)] + #[var(get = get_gpu)] + gpu: Option>, +} + +#[godot_api] +impl PowerStationInstance { + /// Emitted when PowerStation is detected as running + #[signal] + fn started(); + + /// Emitted when PowerStation is detected as stopped + #[signal] + fn stopped(); + + /// Returns true if the PowerStation 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(POWERSTATION_BUS).unwrap(); + let dbus = zbus::blocking::fdo::DBusProxy::new(conn).ok(); + let Some(dbus) = dbus else { + return false; + }; + dbus.name_has_owner(bus.clone()).unwrap_or_default() + } + + /// Returns an instance of the CPU + #[func] + fn get_cpu(&self) -> Option> { + self.cpu_instance.clone() + } + + /// Returns an instance of the GPU + #[func] + fn get_gpu(&self) -> Option> { + self.gpu_instance.clone() + } + + /// 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 any signals for the CPU instance + if let Some(cpu) = self.cpu_instance.as_mut() { + cpu.bind_mut().process(); + } + // Process any signals for the GPU instance + if let Some(gpu) = self.gpu_instance.as_mut() { + gpu.bind_mut().process(); + } + } + + /// Process and dispatch the given signal + fn process_signal(&mut self, signal: Signal) { + match signal { + Signal::Started => { + // Create an instance for the CPU + self.cpu_instance = Some(Cpu::new(POWERSTATION_CPU_PATH)); + self.base_mut().emit_signal("started".into(), &[]); + } + Signal::Stopped => { + self.cpu_instance = None; + self.base_mut().emit_signal("stopped".into(), &[]); + } + } + } +} + +#[godot_api] +impl IResource for PowerStationInstance { + /// Called upon object initialization in the engine + fn init(base: Base) -> Self { + godot_print!("Initializing PowerStation 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 PowerStation task: ${e:?}"); + } + }); + + // Create CPU instance + let cpu = Some(Cpu::new(POWERSTATION_CPU_PATH)); + // Create GPU instance + let gpu = Some(Gpu::new(POWERSTATION_GPU_PATH)); + + // Create a new PowerStation instance + let conn = get_dbus_system_blocking().ok(); + Self { + base, + rx, + conn, + cpu_instance: cpu, + gpu_instance: gpu, + cpu: None, + gpu: None, + } + } +} + +/// Runs PowerStation 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 PowerStation tasks"); + // Establish a connection to the system bus + let conn = get_dbus_system().await?; + + // Spawn a task to listen for PowerStation start/stop + let dbus_conn = conn.clone(); + let signals_tx = tx.clone(); + RUNTIME.spawn(async move { + let bus = BusName::from_static_str(POWERSTATION_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() + }; + let signal = if is_running { + Signal::Started + } else { + Signal::Stopped + }; + if signals_tx.send(signal).is_err() { + return; + } + + 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/performance/powerstation/cpu.rs b/extensions/core/src/performance/powerstation/cpu.rs new file mode 100644 index 00000000..f9acdb8d --- /dev/null +++ b/extensions/core/src/performance/powerstation/cpu.rs @@ -0,0 +1,332 @@ +use std::{ + collections::HashMap, + error::Error, + sync::mpsc::{channel, Receiver, Sender, TryRecvError}, +}; + +use futures_util::StreamExt; +use godot::{classes::ResourceLoader, prelude::*}; + +use crate::{ + dbus::powerstation::cpu::{CPUProxy, CPUProxyBlocking}, + get_dbus_system, get_dbus_system_blocking, RUNTIME, +}; + +use super::{cpu_core::CpuCore, POWERSTATION_BUS}; + +/// Signals that can be emitted +#[derive(Debug)] +enum Signal { + Updated, +} + +#[derive(GodotClass)] +#[class(no_init, base=Resource)] +pub struct Cpu { + base: Base, + path: String, + conn: Option, + rx: Receiver, + cores: HashMap>, + + #[allow(dead_code)] + #[var(get = get_boost_enabled, set = set_boost_enabled)] + boost_enabled: bool, + #[allow(dead_code)] + #[var(get = get_cores_count)] + cores_count: u32, + #[allow(dead_code)] + #[var(get = get_cores_enabled, set = set_cores_enabled)] + cores_enabled: u32, + #[allow(dead_code)] + #[var(get = get_features)] + features: PackedStringArray, + #[allow(dead_code)] + #[var(get = get_smt_enabled, set = set_smt_enabled)] + smt_enabled: bool, +} + +#[godot_api] +impl Cpu { + /// Create a new [Cpu] instance with the given DBus path + fn from_path(path: GString) -> Gd { + Gd::from_init_fn(|base| { + // Create a connection to DBus + let conn = get_dbus_system_blocking().ok(); + let (tx, rx) = channel(); + + // Spawn a task to listen for CPU signals + let dbus_path = path.clone().into(); + RUNTIME.spawn(async move { + if let Err(e) = run(tx, dbus_path).await { + godot_error!("Failed to run CPU task: ${e:?}"); + } + }); + + // Accept a base of type Base and directly forward it. + let mut instance = Self { + base, + conn, + path: path.clone().into(), + cores: HashMap::new(), + rx, + boost_enabled: Default::default(), + cores_count: Default::default(), + cores_enabled: Default::default(), + features: Default::default(), + smt_enabled: Default::default(), + }; + + // Discover any CPU cores + let mut cores = HashMap::new(); + if let Some(cpu) = instance.get_proxy() { + if let Ok(core_paths) = cpu.enumerate_cores() { + for core_path in core_paths { + let core = CpuCore::new(core_path.as_str()); + cores.insert(core_path.to_string(), core); + } + } + } + instance.cores = cores; + + instance + }) + } + + /// Return a proxy instance to the composite device + fn get_proxy(&self) -> Option { + if let Some(conn) = self.conn.as_ref() { + CPUProxyBlocking::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://{POWERSTATION_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 = Cpu::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } else { + let mut device = Cpu::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } + + /// Return the DBus path to the CPU instance + #[func] + pub fn get_dbus_path(&self) -> GString { + self.path.clone().into() + } + + /// Return all the CPU cores for the CPU + #[func] + pub fn get_cores(&self) -> Array> { + let mut cores = array![]; + for core in self.cores.values() { + cores.push(core.clone()); + } + + cores + } + + /// Returns whether or not the CPU has the given feature flag + #[func] + pub fn has_feature(&self, flag: GString) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy + .has_feature(flag.to_string().as_str()) + .unwrap_or(false) + } + + /// Returns whether or not boost is enabled + #[func] + pub fn get_boost_enabled(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.boost_enabled().unwrap_or_default() + } + + /// Sets boost to the given value + #[func] + pub fn set_boost_enabled(&self, enabled: bool) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_boost_enabled(enabled).ok(); + } + + /// Returns the total number of detected CPU cores + #[func] + pub fn get_cores_count(&self) -> u32 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.cores_count().unwrap_or_default() + } + + /// Returns the number of enabled CPU cores + #[func] + pub fn get_cores_enabled(&self) -> u32 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.cores_enabled().unwrap_or_default() + } + + /// Set the number of enabled CPU cores. Cannot be less than 1. + #[func] + pub fn set_cores_enabled(&self, enabled_count: u32) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_cores_enabled(enabled_count).unwrap_or_default() + } + + /// Returns a list of supported CPU feature flags + #[func] + pub fn get_features(&self) -> PackedStringArray { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + let features = proxy.features().unwrap_or_default(); + let features: Vec = features.into_iter().map(|f| f.into_godot()).collect(); + features.into() + } + + /// Returns whether or not SMT is enabled + #[func] + pub fn get_smt_enabled(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.smt_enabled().unwrap_or_default() + } + + /// Set SMT to the given value + #[func] + pub fn set_smt_enabled(&self, enabled: bool) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_smt_enabled(enabled).ok(); + } + + /// 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 signals for any child cores + for (_, core) in self.cores.iter_mut() { + core.bind_mut().process(); + } + } + + /// Process and dispatch the given signal + fn process_signal(&mut self, signal: Signal) { + godot_print!("Got signal: {signal:?}"); + match signal { + Signal::Updated => { + self.base_mut().emit_signal("updated".into(), &[]); + } + } + } +} + +/// Runs CPU 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, path: String) -> Result<(), Box> { + // Establish a connection to the system bus + let conn = get_dbus_system().await?; + let proxy = CPUProxy::builder(&conn).path(path)?.build().await?; + + let signals_tx = tx.clone(); + let mut property_changed = proxy.receive_features_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut property_changed = proxy.receive_cores_count_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut property_changed = proxy.receive_smt_enabled_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut property_changed = proxy.receive_boost_enabled_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut property_changed = proxy.receive_cores_enabled_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + Ok(()) +} diff --git a/extensions/core/src/performance/powerstation/cpu_core.rs b/extensions/core/src/performance/powerstation/cpu_core.rs new file mode 100644 index 00000000..15567879 --- /dev/null +++ b/extensions/core/src/performance/powerstation/cpu_core.rs @@ -0,0 +1,198 @@ +use std::{ + error::Error, + sync::mpsc::{channel, Receiver, Sender, TryRecvError}, +}; + +use futures_util::StreamExt; +use godot::{classes::ResourceLoader, prelude::*}; + +use crate::{ + dbus::powerstation::core::{CoreProxy, CoreProxyBlocking}, + get_dbus_system, get_dbus_system_blocking, RUNTIME, +}; + +use super::POWERSTATION_BUS; + +/// Signals that can be emitted +#[derive(Debug)] +enum Signal { + Updated, +} + +#[derive(GodotClass)] +#[class(no_init, base=Resource)] +pub struct CpuCore { + base: Base, + path: String, + conn: Option, + rx: Receiver, + + #[allow(dead_code)] + #[var(get = get_core_id)] + core_id: u32, + #[allow(dead_code)] + #[var(get = get_number)] + number: u32, + #[allow(dead_code)] + #[var(get = get_online, set = set_online)] + online: bool, +} + +#[godot_api] +impl CpuCore { + /// 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 = get_dbus_system_blocking().ok(); + let (tx, rx) = channel(); + + // Spawn a task to listen for CPU signals + let dbus_path = path.clone().into(); + RUNTIME.spawn(async move { + if let Err(e) = run(tx, dbus_path).await { + godot_error!("Failed to run CPU Core task: ${e:?}"); + } + }); + + // Accept a base of type Base and directly forward it. + Self { + base, + conn, + path: path.clone().into(), + rx, + core_id: Default::default(), + number: Default::default(), + online: Default::default(), + } + }) + } + + /// Return a proxy instance to the composite device + fn get_proxy(&self) -> Option { + if let Some(conn) = self.conn.as_ref() { + CoreProxyBlocking::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://{POWERSTATION_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 = CpuCore::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } else { + let mut device = CpuCore::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } + + /// Return the DBus path to the CPU Core instance + #[func] + pub fn get_dbus_path(&self) -> GString { + self.path.clone().into() + } + + /// Return the core id of the CPU core + #[func] + pub fn get_core_id(&self) -> u32 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.core_id().unwrap_or_default() + } + + /// Return the core number of the CPU core + #[func] + pub fn get_number(&self) -> u32 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.number().unwrap_or_default() + } + + /// Return whether or not the CPU core is online + #[func] + pub fn get_online(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.online().unwrap_or_default() + } + + /// Set the online status of the core to the given value + #[func] + pub fn set_online(&self, online: bool) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_online(online).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) { + godot_print!("Got signal: {signal:?}"); + match signal { + Signal::Updated => { + self.base_mut().emit_signal("updated".into(), &[]); + } + } + } +} + +/// Runs CPU 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, path: String) -> Result<(), Box> { + // Establish a connection to the system bus + let conn = get_dbus_system().await?; + let proxy = CoreProxy::builder(&conn).path(path)?.build().await?; + + let signals_tx = tx.clone(); + let mut property_changed = proxy.receive_online_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + Ok(()) +} diff --git a/extensions/core/src/performance/powerstation/gpu.rs b/extensions/core/src/performance/powerstation/gpu.rs new file mode 100644 index 00000000..e6603658 --- /dev/null +++ b/extensions/core/src/performance/powerstation/gpu.rs @@ -0,0 +1,110 @@ +use std::collections::HashMap; + +use godot::{classes::ResourceLoader, prelude::*}; + +use crate::{dbus::powerstation::gpu::GPUProxyBlocking, get_dbus_system_blocking}; + +use super::{gpu_card::GpuCard, POWERSTATION_BUS}; + +#[derive(GodotClass)] +#[class(no_init, base=Resource)] +pub struct Gpu { + base: Base, + path: String, + conn: Option, + cards: HashMap>, +} + +#[godot_api] +impl Gpu { + /// Create a new [Cpu] instance with the given DBus path + fn from_path(path: GString) -> Gd { + Gd::from_init_fn(|base| { + // Create a connection to DBus + let conn = get_dbus_system_blocking().ok(); + + // Accept a base of type Base and directly forward it. + let mut instance = Self { + base, + conn, + path: path.clone().into(), + cards: HashMap::new(), + }; + + // Discover any CPU cores + let mut cards = HashMap::new(); + if let Some(gpu) = instance.get_proxy() { + if let Ok(card_paths) = gpu.enumerate_cards() { + for card_path in card_paths { + let core = GpuCard::new(card_path.as_str()); + cards.insert(card_path.to_string(), core); + } + } + } + instance.cards = cards; + + instance + }) + } + + /// Return a proxy instance to the composite device + fn get_proxy(&self) -> Option { + if let Some(conn) = self.conn.as_ref() { + GPUProxyBlocking::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://{POWERSTATION_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 = Gpu::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } else { + let mut device = Gpu::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } + + /// Return the DBus path to the CPU instance + #[func] + pub fn get_dbus_path(&self) -> GString { + self.path.clone().into() + } + + #[func] + pub fn get_cards(&self) -> Array> { + let mut cores = array![]; + for core in self.cards.values() { + cores.push(core.clone()); + } + + cores + } + + /// Dispatches signals + pub fn process(&mut self) { + // Process signals for any child cores + for (_, card) in self.cards.iter_mut() { + card.bind_mut().process(); + } + } +} diff --git a/extensions/core/src/performance/powerstation/gpu_card.rs b/extensions/core/src/performance/powerstation/gpu_card.rs new file mode 100644 index 00000000..9913638c --- /dev/null +++ b/extensions/core/src/performance/powerstation/gpu_card.rs @@ -0,0 +1,612 @@ +use std::{ + collections::HashMap, + error::Error, + sync::mpsc::{channel, Receiver, Sender, TryRecvError}, +}; + +use futures_util::StreamExt; +use godot::{classes::ResourceLoader, prelude::*}; + +use crate::{ + dbus::powerstation::{ + card::{CardProxy, CardProxyBlocking}, + tdp::{TDPProxy, TDPProxyBlocking}, + }, + get_dbus_system, get_dbus_system_blocking, RUNTIME, +}; + +use super::{gpu_connector::GpuConnector, POWERSTATION_BUS}; + +/// Signals that can be emitted +#[derive(Debug)] +enum Signal { + Updated, +} + +#[derive(GodotClass)] +#[class(no_init, base=Resource)] +pub struct GpuCard { + base: Base, + dbus_path: String, + conn: Option, + rx: Receiver, + connectors: HashMap>, + + #[allow(dead_code)] + #[var(get = get_class)] + class: GString, + + #[allow(dead_code)] + #[var(get = get_class_id)] + class_id: GString, + + #[allow(dead_code)] + #[var(get = get_clock_limit_mhz_max)] + clock_limit_mhz_max: f64, + + #[allow(dead_code)] + #[var(get = get_clock_limit_mhz_min)] + clock_limit_mhz_min: f64, + + #[allow(dead_code)] + #[var(get = get_clock_value_mhz_max, set = set_clock_value_mhz_max)] + clock_value_mhz_max: f64, + + #[allow(dead_code)] + #[var(get = get_clock_value_mhz_min, set = set_clock_value_mhz_min)] + clock_value_mhz_min: f64, + + #[allow(dead_code)] + #[var(get = get_device)] + device: GString, + + #[allow(dead_code)] + #[var(get = get_device_id)] + device_id: GString, + + #[allow(dead_code)] + #[var(get = get_manual_clock, set = set_manual_clock)] + manual_clock: bool, + + #[allow(dead_code)] + #[var(get = get_name)] + name: GString, + + #[allow(dead_code)] + #[var(get = get_path)] + path: GString, + + #[allow(dead_code)] + #[var(get = get_revision_id)] + revision_id: GString, + + #[allow(dead_code)] + #[var(get = get_subdevice)] + subdevice: GString, + + #[allow(dead_code)] + #[var(get = get_subdevice_id)] + subdevice_id: GString, + + #[allow(dead_code)] + #[var(get = get_subvendor_id)] + subvendor_id: GString, + + #[allow(dead_code)] + #[var(get = get_vendor)] + vendor: GString, + + #[allow(dead_code)] + #[var(get = get_vendor_id)] + vendor_id: GString, + + #[allow(dead_code)] + #[var(get = get_boost, set = set_boost)] + boost: f64, + + #[allow(dead_code)] + #[var(get = get_power_profile, set = set_power_profile)] + power_profile: GString, + + #[allow(dead_code)] + #[var(get = get_tdp, set = set_tdp)] + tdp: f64, + + #[allow(dead_code)] + #[var(get = get_thermal_throttle_limit_c, set = set_thermal_throttle_limit_c)] + thermal_throttle_limit_c: f64, +} + +#[godot_api] +impl GpuCard { + /// 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 = get_dbus_system_blocking().ok(); + let (tx, rx) = channel(); + + // Spawn a task to listen for GPU Card signals + let dbus_path = path.clone().into(); + RUNTIME.spawn(async move { + if let Err(e) = run(tx, dbus_path).await { + godot_error!("Failed to run GPU Card task: ${e:?}"); + } + }); + + // Accept a base of type Base and directly forward it. + let mut instance = Self { + base, + conn, + dbus_path: path.clone().into(), + rx, + connectors: HashMap::new(), + class: Default::default(), + class_id: Default::default(), + clock_limit_mhz_max: Default::default(), + clock_limit_mhz_min: Default::default(), + clock_value_mhz_max: Default::default(), + clock_value_mhz_min: Default::default(), + device: Default::default(), + device_id: Default::default(), + manual_clock: Default::default(), + name: Default::default(), + path: Default::default(), + revision_id: Default::default(), + subdevice: Default::default(), + subdevice_id: Default::default(), + subvendor_id: Default::default(), + vendor: Default::default(), + vendor_id: Default::default(), + boost: Default::default(), + power_profile: Default::default(), + tdp: Default::default(), + thermal_throttle_limit_c: Default::default(), + }; + + // Discover any connectors + let mut connectors = HashMap::new(); + if let Some(card) = instance.get_proxy() { + if let Ok(connector_paths) = card.enumerate_connectors() { + for conn_path in connector_paths { + let connector = GpuConnector::new(conn_path.as_str()); + connectors.insert(conn_path.to_string(), connector); + } + } + } + instance.connectors = connectors; + + instance + }) + } + + /// Return a proxy instance to the GPU card interface + fn get_proxy(&self) -> Option { + if let Some(conn) = self.conn.as_ref() { + CardProxyBlocking::builder(conn) + .path(self.dbus_path.clone()) + .ok() + .and_then(|builder| builder.build().ok()) + } else { + None + } + } + + /// Return a proxy instance to the TDP interface + fn get_tdp_proxy(&self) -> Option { + if let Some(conn) = self.conn.as_ref() { + TDPProxyBlocking::builder(conn) + .path(self.dbus_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://{POWERSTATION_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 = GpuCard::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } else { + let mut device = GpuCard::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } + + /// Return the DBus path to the GPU connector instance + #[func] + pub fn get_dbus_path(&self) -> GString { + self.dbus_path.clone().into() + } + + /// Returns true if the card supports tdp + #[func] + pub fn supports_tdp(&self) -> bool { + let tdp = self.get_tdp(); + tdp > 1.0 + } + + /// Returns the connectors associated with this GPU card + #[func] + pub fn get_connectors(&self) -> Array> { + let mut connectors = array![]; + let Some(proxy) = self.get_proxy() else { + return connectors; + }; + let paths = proxy.enumerate_connectors().unwrap_or_default(); + for path in paths { + let connector = GpuConnector::new(path.as_str()); + connectors.push(connector); + } + + connectors + } + + #[func] + pub fn get_name(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.name().unwrap_or_default().into() + } + + #[func] + pub fn get_path(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.path().unwrap_or_default().into() + } + + #[func] + pub fn get_thermal_throttle_limit_c(&self) -> f64 { + let Some(proxy) = self.get_tdp_proxy() else { + return Default::default(); + }; + proxy.thermal_throttle_limit_c().unwrap_or_default() + } + + #[func] + pub fn set_thermal_throttle_limit_c(&self, value: f64) { + let Some(proxy) = self.get_tdp_proxy() else { + return Default::default(); + }; + proxy + .set_thermal_throttle_limit_c(value) + .unwrap_or_default() + } + + #[func] + pub fn get_power_profile(&self) -> GString { + let Some(proxy) = self.get_tdp_proxy() else { + return Default::default(); + }; + proxy.power_profile().unwrap_or_default().into() + } + + #[func] + pub fn set_power_profile(&self, value: GString) { + let Some(proxy) = self.get_tdp_proxy() else { + return Default::default(); + }; + let value = value.to_string(); + proxy.set_power_profile(value.as_str()).unwrap_or_default() + } + + #[func] + pub fn get_boost(&self) -> f64 { + let Some(proxy) = self.get_tdp_proxy() else { + return Default::default(); + }; + proxy.boost().unwrap_or_default() + } + + #[func] + pub fn set_boost(&self, value: f64) { + let Some(proxy) = self.get_tdp_proxy() else { + return Default::default(); + }; + proxy.set_boost(value).unwrap_or_default() + } + + #[func] + pub fn get_manual_clock(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.manual_clock().unwrap_or_default() + } + + #[func] + pub fn set_manual_clock(&self, value: bool) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_manual_clock(value).unwrap_or_default() + } + + #[func] + pub fn get_clock_value_mhz_min(&self) -> f64 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.clock_value_mhz_min().unwrap_or_default() + } + + #[func] + pub fn set_clock_value_mhz_min(&self, value: f64) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_clock_value_mhz_min(value).unwrap_or_default() + } + + #[func] + pub fn get_clock_value_mhz_max(&self) -> f64 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.clock_value_mhz_max().unwrap_or_default() + } + + #[func] + pub fn set_clock_value_mhz_max(&self, value: f64) { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.set_clock_value_mhz_max(value).unwrap_or_default() + } + + #[func] + pub fn get_device_id(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.device_id().unwrap_or_default().into() + } + + #[func] + pub fn get_subdevice_id(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.subdevice_id().unwrap_or_default().into() + } + + #[func] + pub fn get_subvendor_id(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.subvendor_id().unwrap_or_default().into() + } + + #[func] + pub fn get_vendor(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.vendor().unwrap_or_default().into() + } + + #[func] + pub fn get_vendor_id(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.vendor_id().unwrap_or_default().into() + } + + #[func] + pub fn get_tdp(&self) -> f64 { + let Some(proxy) = self.get_tdp_proxy() else { + return Default::default(); + }; + proxy.tdp().unwrap_or_default() + } + + #[func] + pub fn set_tdp(&self, value: f64) { + let Some(proxy) = self.get_tdp_proxy() else { + return Default::default(); + }; + proxy.set_tdp(value).unwrap_or_default() + } + + #[func] + pub fn get_subdevice(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.subdevice().unwrap_or_default().into() + } + + #[func] + pub fn get_revision_id(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.revision_id().unwrap_or_default().into() + } + + #[func] + pub fn get_device(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.device().unwrap_or_default().into() + } + + #[func] + pub fn get_clock_limit_mhz_min(&self) -> f64 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.clock_limit_mhz_min().unwrap_or_default() + } + + #[func] + pub fn get_clock_limit_mhz_max(&self) -> f64 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.clock_limit_mhz_max().unwrap_or_default() + } + + #[func] + pub fn get_class_id(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.class_id().unwrap_or_default().into() + } + + #[func] + pub fn get_class(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.class().unwrap_or_default().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 child connector signals + for connector in self.connectors.values_mut() { + connector.bind_mut().process(); + } + } + + /// Process and dispatch the given signal + fn process_signal(&mut self, signal: Signal) { + godot_print!("Got signal: {signal:?}"); + match signal { + Signal::Updated => { + self.base_mut().emit_signal("updated".into(), &[]); + } + } + } +} + +/// Runs GPU connector 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, path: String) -> Result<(), Box> { + // Establish a connection to the system bus + let conn = get_dbus_system().await?; + let proxy = CardProxy::builder(&conn) + .path(path.clone())? + .build() + .await?; + + let signals_tx = tx.clone(); + let mut property_changed = proxy.receive_clock_value_mhz_min_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut property_changed = proxy.receive_clock_value_mhz_max_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut property_changed = proxy.receive_manual_clock_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let tdp_proxy = TDPProxy::builder(&conn).path(path)?.build().await?; + + let signals_tx = tx.clone(); + let mut property_changed = tdp_proxy.receive_tdp_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut property_changed = tdp_proxy.receive_boost_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut property_changed = tdp_proxy.receive_power_profile_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut property_changed = tdp_proxy.receive_thermal_throttle_limit_c_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + Ok(()) +} diff --git a/extensions/core/src/performance/powerstation/gpu_connector.rs b/extensions/core/src/performance/powerstation/gpu_connector.rs new file mode 100644 index 00000000..2f47373b --- /dev/null +++ b/extensions/core/src/performance/powerstation/gpu_connector.rs @@ -0,0 +1,258 @@ +use std::{ + error::Error, + sync::mpsc::{channel, Receiver, Sender, TryRecvError}, +}; + +use futures_util::StreamExt; +use godot::{classes::ResourceLoader, prelude::*}; + +use crate::{ + dbus::powerstation::connector::{ConnectorProxy, ConnectorProxyBlocking}, + get_dbus_system, get_dbus_system_blocking, RUNTIME, +}; + +use super::POWERSTATION_BUS; + +/// Signals that can be emitted +#[derive(Debug)] +enum Signal { + Updated, +} + +#[derive(GodotClass)] +#[class(no_init, base=Resource)] +pub struct GpuConnector { + base: Base, + dbus_path: String, + conn: Option, + rx: Receiver, + + #[allow(dead_code)] + #[var(get = get_dpms)] + dpms: bool, + #[allow(dead_code)] + #[var(get = get_enabled)] + enabled: bool, + #[allow(dead_code)] + #[var(get = get_id)] + id: u32, + #[allow(dead_code)] + #[var(get = get_modes)] + modes: PackedStringArray, + #[allow(dead_code)] + #[var(get = get_name)] + name: GString, + #[allow(dead_code)] + #[var(get = get_path)] + path: GString, + #[allow(dead_code)] + #[var(get = get_status)] + status: GString, +} + +#[godot_api] +impl GpuConnector { + /// 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 = get_dbus_system_blocking().ok(); + let (tx, rx) = channel(); + + // Spawn a task to listen for CPU signals + let dbus_path = path.clone().into(); + RUNTIME.spawn(async move { + if let Err(e) = run(tx, dbus_path).await { + godot_error!("Failed to run CPU Core task: ${e:?}"); + } + }); + + // Accept a base of type Base and directly forward it. + Self { + base, + conn, + dbus_path: path.clone().into(), + rx, + dpms: Default::default(), + path: Default::default(), + enabled: Default::default(), + id: Default::default(), + modes: Default::default(), + name: Default::default(), + status: Default::default(), + } + }) + } + + /// Return a proxy instance to the composite device + fn get_proxy(&self) -> Option { + if let Some(conn) = self.conn.as_ref() { + ConnectorProxyBlocking::builder(conn) + .path(self.dbus_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://{POWERSTATION_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 = GpuConnector::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } else { + let mut device = GpuConnector::from_path(path.to_string().into()); + device.take_over_path(res_path.into()); + device + } + } + + /// Return the DBus path to the GPU connector instance + #[func] + pub fn get_dbus_path(&self) -> GString { + self.dbus_path.clone().into() + } + + #[func] + pub fn get_dpms(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.dpms().unwrap_or_default() + } + + #[func] + pub fn get_enabled(&self) -> bool { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.enabled().unwrap_or_default() + } + + #[func] + pub fn get_id(&self) -> u32 { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.id().unwrap_or_default() + } + + #[func] + pub fn get_modes(&self) -> PackedStringArray { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + let modes = proxy.modes().unwrap_or_default(); + let modes: Vec = modes.into_iter().map(|m| m.to_godot()).collect(); + modes.into() + } + + #[func] + pub fn get_name(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.name().unwrap_or_default().into() + } + + #[func] + pub fn get_path(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.path().unwrap_or_default().into() + } + + #[func] + pub fn get_status(&self) -> GString { + let Some(proxy) = self.get_proxy() else { + return Default::default(); + }; + proxy.status().unwrap_or_default().into() + } + + /// Dispatches signals + pub fn process(&mut self) { + // Drain all messages from the channel to process them + loop { + let signal = match self.rx.try_recv() { + Ok(value) => value, + Err(e) => match e { + TryRecvError::Empty => break, + TryRecvError::Disconnected => { + godot_error!("Backend thread is not running!"); + return; + } + }, + }; + self.process_signal(signal); + } + } + + /// Process and dispatch the given signal + fn process_signal(&mut self, signal: Signal) { + godot_print!("Got signal: {signal:?}"); + match signal { + Signal::Updated => { + self.base_mut().emit_signal("updated".into(), &[]); + } + } + } +} + +/// Runs GPU connector 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, path: String) -> Result<(), Box> { + // Establish a connection to the system bus + let conn = get_dbus_system().await?; + let proxy = ConnectorProxy::builder(&conn).path(path)?.build().await?; + + let signals_tx = tx.clone(); + let mut property_changed = proxy.receive_modes_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut property_changed = proxy.receive_status_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + let signals_tx = tx.clone(); + let mut property_changed = proxy.receive_enabled_changed().await; + RUNTIME.spawn(async move { + while (property_changed.next().await).is_some() { + let signal = Signal::Updated; + if signals_tx.send(signal).is_err() { + break; + } + } + }); + + Ok(()) +} 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..492277c6 --- /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, + }, + get_dbus_system, get_dbus_system_blocking, 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 = get_dbus_system_blocking().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 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 = get_dbus_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..01de0bf8 --- /dev/null +++ b/extensions/core/src/power/upower.rs @@ -0,0 +1,206 @@ +use std::{ + collections::HashMap, + sync::mpsc::{channel, Receiver, Sender, TryRecvError}, + time::Duration, +}; + +use godot::prelude::*; +use zbus::names::BusName; + +use crate::{ + dbus::{upower::upower::UPowerProxyBlocking, RunError}, + get_dbus_system, get_dbus_system_blocking, 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 true if the UPower 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(UPOWER_BUS).unwrap(); + let dbus = zbus::blocking::fdo::DBusProxy::new(conn).ok(); + let Some(dbus) = dbus else { + return false; + }; + dbus.name_has_owner(bus.clone()).unwrap_or_default() + } + + /// Returns 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 { + if let Some(device) = self.devices.get(DISPLAY_DEVICE_PATH) { + return device.clone(); + } + 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 UPower object + 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 = get_dbus_system_blocking().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 = get_dbus_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..e3beddbb --- /dev/null +++ b/extensions/core/src/system.rs @@ -0,0 +1,3 @@ +pub mod command; +pub mod pty; +pub mod subreaper; 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..ae4eb360 --- /dev/null +++ b/extensions/core/src/system/pty.rs @@ -0,0 +1,376 @@ +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::{Child, 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 }, +} + +/// Commands that can be sent to a running child process +#[derive(Debug)] +enum ProcessCommand { + Kill, +} + +#[derive(GodotClass)] +#[class(base=Node)] +pub struct Pty { + base: Base, + /// Receiver to listen for signals emitted from the async runtime + rx: Receiver, + /// Transmitter to send signals from the async runtime + tx: Sender, + /// Transmitter to send PTY writes to the PTY async task + pty_tx: Option>, + /// Transmitter to send commands to the Process async task + cmd_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 + } + + /// Write the given bytes to the running PTY. Returns an error code if the + /// PTY is not currently executing a process. + #[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) { + godot_error!("Error sending write line to PTY: {e:?}"); + return -1; + } + + 0 + } + + /// Kill the currently running child process running in the PTY. Returns an + /// error code if the PTY is not currently executing a process. + #[func] + fn kill(&self) -> i32 { + let Some(cmd_tx) = self.cmd_tx.as_ref() else { + godot_error!("PTY is not open to kill process"); + return -1; + }; + let command = ProcessCommand::Kill; + if let Err(e) = cmd_tx.blocking_send(command) { + godot_error!("Error sending kill command 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; + + // Create a channel so process commands can be sent to the running process task + let (cmd_tx, cmd_rx) = tokio::sync::mpsc::channel(64); + self.cmd_tx = Some(cmd_tx); + + // 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 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 = Pty::process_child(child, cmd_rx).await; + + // 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 task + 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 waiting for the child process and any process commands (like kill). + /// Returns the exit code of the child when it finishes. + async fn process_child( + mut child: Child, + mut cmd_rx: tokio::sync::mpsc::Receiver, + ) -> i32 { + loop { + select! { + // Handle waiting for child exit + child_result = child.wait() => { + let status = match child_result { + Ok(status) => status, + Err(e) => { + godot_error!("Error executing child: {e:?}"); + break -1; + } + }; + let exit_code = status.code().unwrap_or(0); + break exit_code; + } + // Handle process commands + Some(cmd) = cmd_rx.recv() => { + match cmd { + ProcessCommand::Kill => { + child.start_kill().unwrap_or_default(); + } + } + } + } + } + } + + /// 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.cmd_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, + cmd_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/extensions/core/src/system/subreaper.rs b/extensions/core/src/system/subreaper.rs new file mode 100644 index 00000000..6a373d00 --- /dev/null +++ b/extensions/core/src/system/subreaper.rs @@ -0,0 +1,23 @@ +//https://iximiuz.com/en/posts/dealing-with-processes-termination-in-Linux/ +// +//https://github.com/nix-rust/nix/pull/1550 + +use nix::{ + sys::prctl, + unistd::{fork, ForkResult}, +}; + +fn foo() { + match unsafe { fork() } { + Ok(ForkResult::Parent { child }) => { + // Parent process of the fork. Should return the subreaper process id + } + Ok(ForkResult::Child) => { + // Child reaper process + prctl::set_child_subreaper(true); + + // Spawn the desired process + } + Err(err) => todo!(), + } +} 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/README.md b/gdext/README.md deleted file mode 100644 index e69de29b..00000000 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={