From a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5 Mon Sep 17 00:00:00 2001 From: Nippy Date: Sat, 9 May 2026 13:33:09 +0200 Subject: raveos update --- .../core/internal/server/apppicker/handlers.go | 51 - .../core/internal/server/apppicker/manager.go | 48 - .../core/internal/server/apppicker/models.go | 8 - .../core/internal/server/bluez/agent.go | 341 ---- .../core/internal/server/bluez/broker.go | 21 - .../core/internal/server/bluez/broker_test.go | 220 --- .../core/internal/server/bluez/handlers.go | 237 --- .../core/internal/server/bluez/handlers_test.go | 41 - .../core/internal/server/bluez/manager.go | 588 ------ .../internal/server/bluez/subscription_broker.go | 86 - .../core/internal/server/bluez/types.go | 78 - .../core/internal/server/bluez/types_test.go | 210 --- .../core/internal/server/brightness/ddc.go | 510 ------ .../core/internal/server/brightness/ddc_filter.go | 135 -- .../internal/server/brightness/ddc_filter_test.go | 122 -- .../core/internal/server/brightness/ddc_test.go | 135 -- .../core/internal/server/brightness/handlers.go | 124 -- .../core/internal/server/brightness/logind.go | 66 - .../core/internal/server/brightness/logind_test.go | 95 - .../core/internal/server/brightness/manager.go | 389 ---- .../internal/server/brightness/manager_test.go | 11 - .../core/internal/server/brightness/sysfs.go | 267 --- .../server/brightness/sysfs_logind_test.go | 278 --- .../core/internal/server/brightness/sysfs_test.go | 180 -- .../core/internal/server/brightness/types.go | 184 -- .../core/internal/server/brightness/udev.go | 237 --- .../core/internal/server/brightness/udev_test.go | 260 --- .../core/internal/server/browser/handlers.go | 22 - .../core/internal/server/browser/manager.go | 49 - .../core/internal/server/browser/models.go | 5 - .../core/internal/server/clipboard/handlers.go | 325 ---- .../core/internal/server/clipboard/manager.go | 1911 -------------------- .../core/internal/server/clipboard/manager_test.go | 531 ------ .../core/internal/server/clipboard/types.go | 196 -- .../core/internal/server/cups/actions.go | 479 ----- .../internal/server/cups/actions_pkhelper_test.go | 235 --- .../core/internal/server/cups/actions_test.go | 450 ----- .../core/internal/server/cups/handlers.go | 487 ----- .../core/internal/server/cups/handlers_test.go | 711 -------- .../core/internal/server/cups/manager.go | 390 ---- .../core/internal/server/cups/manager_test.go | 363 ---- .../core/internal/server/cups/pkhelper.go | 184 -- .../core/internal/server/cups/pkhelper_test.go | 95 - .../core/internal/server/cups/subscription.go | 245 --- .../core/internal/server/cups/subscription_dbus.go | 295 --- .../core/internal/server/cups/test_connection.go | 176 -- .../internal/server/cups/test_connection_test.go | 397 ---- .../core/internal/server/cups/testing.go | 13 - .../core/internal/server/cups/types.go | 132 -- .../core/internal/server/dbus/handlers.go | 237 --- .../core/internal/server/dbus/manager.go | 392 ---- .../core/internal/server/dbus/types.go | 52 - .../core/internal/server/dwl/handlers.go | 138 -- .../core/internal/server/dwl/manager.go | 522 ------ .../core/internal/server/dwl/manager_test.go | 366 ---- .../core/internal/server/dwl/types.go | 176 -- .../core/internal/server/evdev/handlers.go | 20 - .../core/internal/server/evdev/handlers_test.go | 130 -- .../core/internal/server/evdev/manager.go | 413 ----- .../core/internal/server/evdev/manager_test.go | 344 ---- .../core/internal/server/evdev/models.go | 6 - .../core/internal/server/extworkspace/handlers.go | 134 -- .../core/internal/server/extworkspace/manager.go | 598 ------ .../internal/server/extworkspace/manager_test.go | 392 ---- .../core/internal/server/extworkspace/types.go | 169 -- .../core/internal/server/freedesktop/actions.go | 112 -- .../internal/server/freedesktop/actions_test.go | 145 -- .../core/internal/server/freedesktop/constants.go | 23 - .../core/internal/server/freedesktop/handlers.go | 154 -- .../internal/server/freedesktop/handlers_test.go | 581 ------ .../core/internal/server/freedesktop/manager.go | 206 --- .../internal/server/freedesktop/manager_test.go | 143 -- .../internal/server/freedesktop/screensaver.go | 349 ---- .../server/freedesktop/screensaver_test.go | 102 -- .../core/internal/server/freedesktop/types.go | 66 - .../core/internal/server/freedesktop/types_test.go | 70 - .../core/internal/server/location/handlers.go | 61 - .../core/internal/server/location/manager.go | 175 -- .../core/internal/server/location/types.go | 28 - .../core/internal/server/loginctl/actions.go | 88 - .../core/internal/server/loginctl/constants.go | 10 - .../core/internal/server/loginctl/handlers.go | 156 -- .../core/internal/server/loginctl/handlers_test.go | 500 ----- .../core/internal/server/loginctl/manager.go | 723 -------- .../core/internal/server/loginctl/manager_test.go | 296 --- .../core/internal/server/loginctl/monitor.go | 155 -- .../core/internal/server/loginctl/monitor_test.go | 298 --- .../core/internal/server/loginctl/types.go | 76 - .../core/internal/server/loginctl/types_test.go | 50 - .../core/internal/server/matugen_handler.go | 79 - .../core/internal/server/models/types.go | 47 - .../core/internal/server/models/types_test.go | 52 - .../core/internal/server/network/API.md | 552 ------ .../core/internal/server/network/agent_iwd.go | 306 ---- .../server/network/agent_networkmanager.go | 897 --------- .../server/network/agent_networkmanager_test.go | 355 ---- .../core/internal/server/network/backend.go | 80 - .../server/network/backend_hybrid_iwd_networkd.go | 245 --- .../internal/server/network/backend_hybrid_test.go | 135 -- .../core/internal/server/network/backend_iwd.go | 238 --- .../internal/server/network/backend_iwd_signals.go | 355 ---- .../internal/server/network/backend_iwd_test.go | 212 --- .../server/network/backend_iwd_unimplemented.go | 133 -- .../internal/server/network/backend_iwd_wifi.go | 662 ------- .../internal/server/network/backend_networkd.go | 301 --- .../server/network/backend_networkd_ethernet.go | 120 -- .../server/network/backend_networkd_signals.go | 68 - .../server/network/backend_networkd_test.go | 147 -- .../network/backend_networkd_unimplemented.go | 99 - .../server/network/backend_networkmanager.go | 405 ----- .../network/backend_networkmanager_ethernet.go | 402 ---- .../backend_networkmanager_ethernet_test.go | 134 -- .../network/backend_networkmanager_gp_saml.go | 203 --- .../network/backend_networkmanager_gp_saml_test.go | 169 -- .../network/backend_networkmanager_signals.go | 530 ------ .../network/backend_networkmanager_signals_test.go | 255 --- .../server/network/backend_networkmanager_state.go | 277 --- .../network/backend_networkmanager_state_test.go | 82 - .../server/network/backend_networkmanager_test.go | 154 -- .../server/network/backend_networkmanager_vpn.go | 1394 -------------- .../network/backend_networkmanager_vpn_test.go | 138 -- .../server/network/backend_networkmanager_wifi.go | 1173 ------------ .../network/backend_networkmanager_wifi_test.go | 202 --- .../core/internal/server/network/broker.go | 22 - .../internal/server/network/connection_test.go | 109 -- .../core/internal/server/network/detect.go | 89 - .../core/internal/server/network/detect_test.go | 41 - .../core/internal/server/network/handlers.go | 630 ------- .../core/internal/server/network/handlers_test.go | 269 --- .../core/internal/server/network/helpers.go | 53 - .../core/internal/server/network/manager.go | 626 ------- .../core/internal/server/network/manager_test.go | 191 -- .../core/internal/server/network/monitor.go | 1 - .../core/internal/server/network/priority.go | 187 -- .../core/internal/server/network/priority_test.go | 50 - .../internal/server/network/subscription_broker.go | 125 -- .../core/internal/server/network/testing.go | 14 - .../core/internal/server/network/types.go | 267 --- .../core/internal/server/network/types_test.go | 178 -- .../core/internal/server/network/wifi_qrcode.go | 59 - .../core/internal/server/network/wifi_test.go | 148 -- .../core/internal/server/network/wired_test.go | 51 - .../core/internal/server/params/params.go | 113 -- .../core/internal/server/params/params_test.go | 154 -- .../core/internal/server/plugins/handlers.go | 27 - .../core/internal/server/plugins/handlers_test.go | 196 -- .../core/internal/server/plugins/install.go | 69 - .../core/internal/server/plugins/list.go | 53 - .../core/internal/server/plugins/list_installed.go | 77 - .../core/internal/server/plugins/search.go | 74 - .../core/internal/server/plugins/types.go | 25 - .../core/internal/server/plugins/uninstall.go | 66 - .../core/internal/server/plugins/update.go | 64 - .../core/internal/server/plugins/utils.go | 17 - .../core/internal/server/router.go | 250 --- .../core/internal/server/server.go | 1758 ------------------ .../core/internal/server/server_test.go | 180 -- .../core/internal/server/thememode/handlers.go | 154 -- .../core/internal/server/thememode/manager.go | 462 ----- .../core/internal/server/thememode/types.go | 23 - .../core/internal/server/themes/handlers.go | 27 - .../core/internal/server/themes/install.go | 52 - .../core/internal/server/themes/list.go | 54 - .../core/internal/server/themes/list_installed.go | 149 -- .../core/internal/server/themes/search.go | 53 - .../core/internal/server/themes/types.go | 47 - .../core/internal/server/themes/uninstall.go | 63 - .../core/internal/server/themes/update.go | 57 - .../core/internal/server/trayrecovery/manager.go | 93 - .../core/internal/server/trayrecovery/recovery.go | 262 --- .../core/internal/server/wayland/gamma.go | 167 -- .../core/internal/server/wayland/gamma_test.go | 120 -- .../core/internal/server/wayland/geolocation.go | 50 - .../core/internal/server/wayland/handlers.go | 182 -- .../core/internal/server/wayland/manager.go | 1256 ------------- .../core/internal/server/wayland/manager_test.go | 414 ----- .../core/internal/server/wayland/suncalc.go | 122 -- .../core/internal/server/wayland/suncalc_test.go | 387 ---- .../core/internal/server/wayland/types.go | 216 --- .../core/internal/server/wayland/types_test.go | 330 ---- .../core/internal/server/wlcontext/context.go | 200 -- .../core/internal/server/wlcontext/context_test.go | 128 -- .../core/internal/server/wlroutput/handlers.go | 282 --- .../core/internal/server/wlroutput/manager.go | 492 ----- .../core/internal/server/wlroutput/manager_test.go | 414 ----- .../core/internal/server/wlroutput/types.go | 192 -- 186 files changed, 44551 deletions(-) delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/apppicker/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/apppicker/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/apppicker/models.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/agent.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/broker.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/broker_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/handlers_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/subscription_broker.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/types_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_logind_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/browser/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/browser/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/browser/models.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/actions.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/actions_pkhelper_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/actions_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/handlers_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/manager_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/pkhelper.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/pkhelper_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/subscription.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/subscription_dbus.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/test_connection.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/test_connection_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/testing.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dbus/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dbus/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dbus/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/models.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/manager_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/constants.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/location/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/location/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/location/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/actions.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/constants.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/matugen_handler.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/models/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/models/types_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/API.md delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/agent_iwd.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/agent_networkmanager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/agent_networkmanager_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_hybrid_iwd_networkd.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_hybrid_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_signals.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_unimplemented.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_wifi.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_ethernet.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_signals.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_unimplemented.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_ethernet.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_ethernet_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_gp_saml.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_gp_saml_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_signals.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_signals_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_state.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_state_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_vpn.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_vpn_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_wifi.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_wifi_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/broker.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/connection_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/detect.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/detect_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/handlers_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/helpers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/manager_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/monitor.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/priority.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/priority_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/subscription_broker.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/testing.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/types_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/wifi_qrcode.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/wifi_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/wired_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/params/params.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/params/params_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/handlers_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/install.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/list.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/list_installed.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/search.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/uninstall.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/update.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/utils.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/router.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/server.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/server_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/install.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/list.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/list_installed.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/search.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/uninstall.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/update.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/trayrecovery/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/trayrecovery/recovery.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/geolocation.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlcontext/context.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlcontext/context_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/handlers.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager_test.go delete mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/types.go (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server') diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/apppicker/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/apppicker/handlers.go deleted file mode 100644 index 007ab3e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/apppicker/handlers.go +++ /dev/null @@ -1,51 +0,0 @@ -package apppicker - -import ( - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - switch req.Method { - case "apppicker.open", "browser.open": - handleOpen(conn, req, manager) - default: - models.RespondError(conn, req.ID, "unknown method") - } -} - -func handleOpen(conn net.Conn, req models.Request, manager *Manager) { - log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params) - - target, ok := models.Get[string](req, "target") - if !ok { - target, ok = models.Get[string](req, "url") - if !ok { - log.Warnf("AppPicker: Invalid target parameter in request") - models.RespondError(conn, req.ID, "invalid target parameter") - return - } - } - - event := OpenEvent{ - Target: target, - RequestType: models.GetOr(req, "requestType", "url"), - MimeType: models.GetOr(req, "mimeType", ""), - } - - if categories, ok := models.Get[[]any](req, "categories"); ok { - event.Categories = make([]string, 0, len(categories)) - for _, cat := range categories { - if catStr, ok := cat.(string); ok { - event.Categories = append(event.Categories, catStr) - } - } - } - - log.Infof("AppPicker: Broadcasting event: %+v", event) - manager.RequestOpen(event) - models.Respond(conn, req.ID, "ok") - log.Infof("AppPicker: Request handled successfully") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/apppicker/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/apppicker/manager.go deleted file mode 100644 index bde0846..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/apppicker/manager.go +++ /dev/null @@ -1,48 +0,0 @@ -package apppicker - -import ( - "sync" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" -) - -type Manager struct { - subscribers syncmap.Map[string, chan OpenEvent] - closeOnce sync.Once -} - -func NewManager() *Manager { - return &Manager{} -} - -func (m *Manager) Subscribe(id string) chan OpenEvent { - ch := make(chan OpenEvent, 16) - m.subscribers.Store(id, ch) - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) RequestOpen(event OpenEvent) { - m.subscribers.Range(func(key string, ch chan OpenEvent) bool { - select { - case ch <- event: - default: - } - return true - }) -} - -func (m *Manager) Close() { - m.closeOnce.Do(func() { - m.subscribers.Range(func(key string, ch chan OpenEvent) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/apppicker/models.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/apppicker/models.go deleted file mode 100644 index 8861461..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/apppicker/models.go +++ /dev/null @@ -1,8 +0,0 @@ -package apppicker - -type OpenEvent struct { - Target string `json:"target"` - MimeType string `json:"mimeType,omitempty"` - Categories []string `json:"categories,omitempty"` - RequestType string `json:"requestType"` -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/agent.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/agent.go deleted file mode 100644 index b630a19..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/agent.go +++ /dev/null @@ -1,341 +0,0 @@ -package bluez - -import ( - "context" - "errors" - "fmt" - "strconv" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/godbus/dbus/v5" -) - -const ( - bluezService = "org.bluez" - agentManagerPath = "/org/bluez" - agentManagerIface = "org.bluez.AgentManager1" - agent1Iface = "org.bluez.Agent1" - device1Iface = "org.bluez.Device1" - agentPath = "/com/danklinux/bluez/agent" - agentCapability = "KeyboardDisplay" -) - -const introspectXML = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -` - -type BluezAgent struct { - conn *dbus.Conn - broker PromptBroker -} - -func NewBluezAgent(broker PromptBroker) (*BluezAgent, error) { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return nil, fmt.Errorf("system bus connection failed: %w", err) - } - - agent := &BluezAgent{ - conn: conn, - broker: broker, - } - - if err := conn.Export(agent, dbus.ObjectPath(agentPath), agent1Iface); err != nil { - conn.Close() - return nil, fmt.Errorf("agent export failed: %w", err) - } - - if err := conn.Export(agent, dbus.ObjectPath(agentPath), "org.freedesktop.DBus.Introspectable"); err != nil { - conn.Close() - return nil, fmt.Errorf("introspection export failed: %w", err) - } - - mgr := conn.Object(bluezService, dbus.ObjectPath(agentManagerPath)) - if err := mgr.Call(agentManagerIface+".RegisterAgent", 0, dbus.ObjectPath(agentPath), agentCapability).Err; err != nil { - conn.Close() - return nil, fmt.Errorf("agent registration failed: %w", err) - } - - if err := mgr.Call(agentManagerIface+".RequestDefaultAgent", 0, dbus.ObjectPath(agentPath)).Err; err != nil { - log.Debugf("[BluezAgent] not default agent: %v", err) - } - - log.Infof("[BluezAgent] registered at %s with capability %s", agentPath, agentCapability) - return agent, nil -} - -func (a *BluezAgent) Close() { - if a.conn == nil { - return - } - mgr := a.conn.Object(bluezService, dbus.ObjectPath(agentManagerPath)) - mgr.Call(agentManagerIface+".UnregisterAgent", 0, dbus.ObjectPath(agentPath)) - a.conn.Close() -} - -func (a *BluezAgent) Release() *dbus.Error { - log.Infof("[BluezAgent] Release called") - return nil -} - -func (a *BluezAgent) RequestPinCode(device dbus.ObjectPath) (string, *dbus.Error) { - log.Infof("[BluezAgent] RequestPinCode: device=%s", device) - - secrets, err := a.promptFor(device, "pin", []string{"pin"}, nil) - if err != nil { - log.Warnf("[BluezAgent] RequestPinCode failed: %v", err) - return "", a.errorFrom(err) - } - - pin := secrets["pin"] - log.Infof("[BluezAgent] RequestPinCode returning PIN (len=%d)", len(pin)) - return pin, nil -} - -func (a *BluezAgent) RequestPasskey(device dbus.ObjectPath) (uint32, *dbus.Error) { - log.Infof("[BluezAgent] RequestPasskey: device=%s", device) - - secrets, err := a.promptFor(device, "passkey", []string{"passkey"}, nil) - if err != nil { - log.Warnf("[BluezAgent] RequestPasskey failed: %v", err) - return 0, a.errorFrom(err) - } - - passkey, err := strconv.ParseUint(secrets["passkey"], 10, 32) - if err != nil { - log.Warnf("[BluezAgent] invalid passkey format: %v", err) - return 0, dbus.MakeFailedError(fmt.Errorf("invalid passkey: %w", err)) - } - - log.Infof("[BluezAgent] RequestPasskey returning: %d", passkey) - return uint32(passkey), nil -} - -func (a *BluezAgent) DisplayPinCode(device dbus.ObjectPath, pincode string) *dbus.Error { - log.Infof("[BluezAgent] DisplayPinCode: device=%s, pin=%s", device, pincode) - - _, err := a.promptFor(device, "display-pin", []string{}, &pincode) - if err != nil { - log.Warnf("[BluezAgent] DisplayPinCode acknowledgment failed: %v", err) - } - - return nil -} - -func (a *BluezAgent) DisplayPasskey(device dbus.ObjectPath, passkey uint32, entered uint16) *dbus.Error { - log.Infof("[BluezAgent] DisplayPasskey: device=%s, passkey=%06d, entered=%d", device, passkey, entered) - - if entered == 0 { - passkeyStr := strconv.FormatUint(uint64(passkey), 10) - _, err := a.promptFor(device, "display-passkey", []string{}, &passkeyStr) - if err != nil { - log.Warnf("[BluezAgent] DisplayPasskey acknowledgment failed: %v", err) - } - } - - return nil -} - -func (a *BluezAgent) RequestConfirmation(device dbus.ObjectPath, passkey uint32) *dbus.Error { - log.Infof("[BluezAgent] RequestConfirmation: device=%s, passkey=%06d", device, passkey) - - passkeyStr := strconv.FormatUint(uint64(passkey), 10) - secrets, err := a.promptFor(device, "confirm", []string{"decision"}, &passkeyStr) - if err != nil { - log.Warnf("[BluezAgent] RequestConfirmation failed: %v", err) - return a.errorFrom(err) - } - - if secrets["decision"] != "yes" && secrets["decision"] != "accept" { - log.Debugf("[BluezAgent] RequestConfirmation rejected by user") - return dbus.NewError("org.bluez.Error.Rejected", nil) - } - - log.Infof("[BluezAgent] RequestConfirmation accepted") - return nil -} - -func (a *BluezAgent) RequestAuthorization(device dbus.ObjectPath) *dbus.Error { - log.Infof("[BluezAgent] RequestAuthorization: device=%s", device) - - secrets, err := a.promptFor(device, "authorize", []string{"decision"}, nil) - if err != nil { - log.Warnf("[BluezAgent] RequestAuthorization failed: %v", err) - return a.errorFrom(err) - } - - if secrets["decision"] != "yes" && secrets["decision"] != "accept" { - log.Debugf("[BluezAgent] RequestAuthorization rejected by user") - return dbus.NewError("org.bluez.Error.Rejected", nil) - } - - log.Infof("[BluezAgent] RequestAuthorization accepted") - return nil -} - -func (a *BluezAgent) AuthorizeService(device dbus.ObjectPath, uuid string) *dbus.Error { - log.Infof("[BluezAgent] AuthorizeService: device=%s, uuid=%s", device, uuid) - - secrets, err := a.promptFor(device, "authorize-service:"+uuid, []string{"decision"}, nil) - if err != nil { - log.Warnf("[BluezAgent] AuthorizeService failed: %v", err) - return a.errorFrom(err) - } - - if secrets["decision"] != "yes" && secrets["decision"] != "accept" { - log.Debugf("[BluezAgent] AuthorizeService rejected by user") - return dbus.NewError("org.bluez.Error.Rejected", nil) - } - - log.Infof("[BluezAgent] AuthorizeService accepted") - return nil -} - -func (a *BluezAgent) Cancel() *dbus.Error { - log.Infof("[BluezAgent] Cancel called") - return nil -} - -func (a *BluezAgent) Introspect() (string, *dbus.Error) { - return introspectXML, nil -} - -func (a *BluezAgent) promptFor(device dbus.ObjectPath, requestType string, fields []string, displayValue *string) (map[string]string, error) { - if a.broker == nil { - return nil, fmt.Errorf("broker not initialized") - } - - deviceName, deviceAddr := a.getDeviceInfo(device) - hints := []string{} - if displayValue != nil { - hints = append(hints, *displayValue) - } - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - - var passkey *uint32 - if requestType == "confirm" || requestType == "display-passkey" { - if displayValue != nil { - if pk, err := strconv.ParseUint(*displayValue, 10, 32); err == nil { - pk32 := uint32(pk) - passkey = &pk32 - } - } - } - - token, err := a.broker.Ask(ctx, PromptRequest{ - DevicePath: string(device), - DeviceName: deviceName, - DeviceAddr: deviceAddr, - RequestType: requestType, - Fields: fields, - Hints: hints, - Passkey: passkey, - }) - if err != nil { - return nil, fmt.Errorf("prompt creation failed: %w", err) - } - - log.Infof("[BluezAgent] waiting for user response (token=%s)", token) - reply, err := a.broker.Wait(ctx, token) - if err != nil { - if errors.Is(err, errdefs.ErrSecretPromptTimeout) { - return nil, err - } - if reply.Cancel || errors.Is(err, errdefs.ErrSecretPromptCancelled) { - return nil, errdefs.ErrSecretPromptCancelled - } - return nil, err - } - - if !reply.Accept && len(fields) > 0 { - return nil, errdefs.ErrSecretPromptCancelled - } - - return reply.Secrets, nil -} - -func (a *BluezAgent) getDeviceInfo(device dbus.ObjectPath) (string, string) { - obj := a.conn.Object(bluezService, device) - - var name, alias, addr string - - nameVar, err := obj.GetProperty(device1Iface + ".Name") - if err == nil { - if n, ok := nameVar.Value().(string); ok { - name = n - } - } - - aliasVar, err := obj.GetProperty(device1Iface + ".Alias") - if err == nil { - if a, ok := aliasVar.Value().(string); ok { - alias = a - } - } - - addrVar, err := obj.GetProperty(device1Iface + ".Address") - if err == nil { - if a, ok := addrVar.Value().(string); ok { - addr = a - } - } - - if alias != "" { - return alias, addr - } - if name != "" { - return name, addr - } - return addr, addr -} - -func (a *BluezAgent) errorFrom(err error) *dbus.Error { - if errors.Is(err, errdefs.ErrSecretPromptTimeout) { - return dbus.NewError("org.bluez.Error.Canceled", nil) - } - if errors.Is(err, errdefs.ErrSecretPromptCancelled) { - return dbus.NewError("org.bluez.Error.Canceled", nil) - } - return dbus.MakeFailedError(err) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/broker.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/broker.go deleted file mode 100644 index f817707..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/broker.go +++ /dev/null @@ -1,21 +0,0 @@ -package bluez - -import ( - "context" - "crypto/rand" - "encoding/hex" -) - -type PromptBroker interface { - Ask(ctx context.Context, req PromptRequest) (token string, err error) - Wait(ctx context.Context, token string) (PromptReply, error) - Resolve(token string, reply PromptReply) error -} - -func generateToken() (string, error) { - bytes := make([]byte, 16) - if _, err := rand.Read(bytes); err != nil { - return "", err - } - return hex.EncodeToString(bytes), nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/broker_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/broker_test.go deleted file mode 100644 index 9eebe61..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/broker_test.go +++ /dev/null @@ -1,220 +0,0 @@ -package bluez - -import ( - "context" - "testing" - "time" -) - -func TestSubscriptionBrokerAskWait(t *testing.T) { - promptReceived := false - broker := NewSubscriptionBroker(func(p PairingPrompt) { - promptReceived = true - if p.Token == "" { - t.Error("expected token to be non-empty") - } - if p.DeviceName != "TestDevice" { - t.Errorf("expected DeviceName=TestDevice, got %s", p.DeviceName) - } - }) - - ctx := context.Background() - req := PromptRequest{ - DevicePath: "/org/bluez/test", - DeviceName: "TestDevice", - DeviceAddr: "AA:BB:CC:DD:EE:FF", - RequestType: "pin", - Fields: []string{"pin"}, - } - - token, err := broker.Ask(ctx, req) - if err != nil { - t.Fatalf("Ask failed: %v", err) - } - - if token == "" { - t.Fatal("expected non-empty token") - } - - if !promptReceived { - t.Fatal("expected prompt broadcast to be called") - } - - go func() { - time.Sleep(50 * time.Millisecond) - broker.Resolve(token, PromptReply{ - Secrets: map[string]string{"pin": "1234"}, - Accept: true, - }) - }() - - reply, err := broker.Wait(ctx, token) - if err != nil { - t.Fatalf("Wait failed: %v", err) - } - - if reply.Secrets["pin"] != "1234" { - t.Errorf("expected pin=1234, got %s", reply.Secrets["pin"]) - } - - if !reply.Accept { - t.Error("expected Accept=true") - } -} - -func TestSubscriptionBrokerTimeout(t *testing.T) { - broker := NewSubscriptionBroker(nil) - - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel() - - req := PromptRequest{ - DevicePath: "/org/bluez/test", - DeviceName: "TestDevice", - RequestType: "passkey", - Fields: []string{"passkey"}, - } - - token, err := broker.Ask(ctx, req) - if err != nil { - t.Fatalf("Ask failed: %v", err) - } - - _, err = broker.Wait(ctx, token) - if err == nil { - t.Fatal("expected timeout error") - } -} - -func TestSubscriptionBrokerCancel(t *testing.T) { - broker := NewSubscriptionBroker(nil) - - ctx := context.Background() - req := PromptRequest{ - DevicePath: "/org/bluez/test", - DeviceName: "TestDevice", - RequestType: "confirm", - Fields: []string{"decision"}, - } - - token, err := broker.Ask(ctx, req) - if err != nil { - t.Fatalf("Ask failed: %v", err) - } - - go func() { - time.Sleep(50 * time.Millisecond) - broker.Resolve(token, PromptReply{ - Cancel: true, - }) - }() - - _, err = broker.Wait(ctx, token) - if err == nil { - t.Fatal("expected cancelled error") - } -} - -func TestSubscriptionBrokerUnknownToken(t *testing.T) { - broker := NewSubscriptionBroker(nil) - - ctx := context.Background() - _, err := broker.Wait(ctx, "invalid-token") - if err == nil { - t.Fatal("expected error for unknown token") - } -} - -func TestGenerateToken(t *testing.T) { - token1, err := generateToken() - if err != nil { - t.Fatalf("generateToken failed: %v", err) - } - - token2, err := generateToken() - if err != nil { - t.Fatalf("generateToken failed: %v", err) - } - - if token1 == token2 { - t.Error("expected unique tokens") - } - - if len(token1) != 32 { - t.Errorf("expected token length 32, got %d", len(token1)) - } -} - -func TestSubscriptionBrokerResolveUnknownToken(t *testing.T) { - broker := NewSubscriptionBroker(nil) - - err := broker.Resolve("unknown-token", PromptReply{ - Secrets: map[string]string{"test": "value"}, - }) - if err == nil { - t.Fatal("expected error for unknown token") - } -} - -func TestSubscriptionBrokerMultipleRequests(t *testing.T) { - broker := NewSubscriptionBroker(nil) - ctx := context.Background() - - req1 := PromptRequest{ - DevicePath: "/org/bluez/test1", - DeviceName: "Device1", - RequestType: "pin", - Fields: []string{"pin"}, - } - - req2 := PromptRequest{ - DevicePath: "/org/bluez/test2", - DeviceName: "Device2", - RequestType: "passkey", - Fields: []string{"passkey"}, - } - - token1, err := broker.Ask(ctx, req1) - if err != nil { - t.Fatalf("Ask1 failed: %v", err) - } - - token2, err := broker.Ask(ctx, req2) - if err != nil { - t.Fatalf("Ask2 failed: %v", err) - } - - if token1 == token2 { - t.Error("expected different tokens") - } - - go func() { - time.Sleep(50 * time.Millisecond) - broker.Resolve(token1, PromptReply{ - Secrets: map[string]string{"pin": "1234"}, - Accept: true, - }) - broker.Resolve(token2, PromptReply{ - Secrets: map[string]string{"passkey": "567890"}, - Accept: true, - }) - }() - - reply1, err := broker.Wait(ctx, token1) - if err != nil { - t.Fatalf("Wait1 failed: %v", err) - } - - reply2, err := broker.Wait(ctx, token2) - if err != nil { - t.Fatalf("Wait2 failed: %v", err) - } - - if reply1.Secrets["pin"] != "1234" { - t.Errorf("expected pin=1234, got %s", reply1.Secrets["pin"]) - } - - if reply2.Secrets["passkey"] != "567890" { - t.Errorf("expected passkey=567890, got %s", reply2.Secrets["passkey"]) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/handlers.go deleted file mode 100644 index be5a1c4..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/handlers.go +++ /dev/null @@ -1,237 +0,0 @@ -package bluez - -import ( - "encoding/json" - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -type BluetoothEvent struct { - Type string `json:"type"` - Data BluetoothState `json:"data"` -} - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - switch req.Method { - case "bluetooth.getState": - handleGetState(conn, req, manager) - case "bluetooth.startDiscovery": - handleStartDiscovery(conn, req, manager) - case "bluetooth.stopDiscovery": - handleStopDiscovery(conn, req, manager) - case "bluetooth.setPowered": - handleSetPowered(conn, req, manager) - case "bluetooth.pair": - handlePairDevice(conn, req, manager) - case "bluetooth.connect": - handleConnectDevice(conn, req, manager) - case "bluetooth.disconnect": - handleDisconnectDevice(conn, req, manager) - case "bluetooth.remove": - handleRemoveDevice(conn, req, manager) - case "bluetooth.trust": - handleTrustDevice(conn, req, manager) - case "bluetooth.untrust": - handleUntrustDevice(conn, req, manager) - case "bluetooth.subscribe": - handleSubscribe(conn, req, manager) - case "bluetooth.pairing.submit": - handlePairingSubmit(conn, req, manager) - case "bluetooth.pairing.cancel": - handlePairingCancel(conn, req, manager) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleGetState(conn net.Conn, req models.Request, manager *Manager) { - models.Respond(conn, req.ID, manager.GetState()) -} - -func handleStartDiscovery(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.StartDiscovery(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery started"}) -} - -func handleStopDiscovery(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.StopDiscovery(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery stopped"}) -} - -func handleSetPowered(conn net.Conn, req models.Request, manager *Manager) { - powered, err := params.Bool(req.Params, "powered") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetPowered(powered); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "powered state updated"}) -} - -func handlePairDevice(conn net.Conn, req models.Request, manager *Manager) { - devicePath, err := params.String(req.Params, "device") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.PairDevice(devicePath); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing initiated"}) -} - -func handleConnectDevice(conn net.Conn, req models.Request, manager *Manager) { - devicePath, err := params.String(req.Params, "device") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.ConnectDevice(devicePath); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"}) -} - -func handleDisconnectDevice(conn net.Conn, req models.Request, manager *Manager) { - devicePath, err := params.String(req.Params, "device") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.DisconnectDevice(devicePath); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"}) -} - -func handleRemoveDevice(conn net.Conn, req models.Request, manager *Manager) { - devicePath, err := params.String(req.Params, "device") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.RemoveDevice(devicePath); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device removed"}) -} - -func handleTrustDevice(conn net.Conn, req models.Request, manager *Manager) { - devicePath, err := params.String(req.Params, "device") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.TrustDevice(devicePath, true); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device trusted"}) -} - -func handleUntrustDevice(conn net.Conn, req models.Request, manager *Manager) { - devicePath, err := params.String(req.Params, "device") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.TrustDevice(devicePath, false); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device untrusted"}) -} - -func handlePairingSubmit(conn net.Conn, req models.Request, manager *Manager) { - token, err := params.String(req.Params, "token") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - secrets := params.StringMapOpt(req.Params, "secrets") - accept := params.BoolOpt(req.Params, "accept", false) - - if err := manager.SubmitPairing(token, secrets, accept); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing response submitted"}) -} - -func handlePairingCancel(conn net.Conn, req models.Request, manager *Manager) { - token, err := params.String(req.Params, "token") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.CancelPairing(token); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing cancelled"}) -} - -func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) { - clientID := fmt.Sprintf("client-%p", conn) - stateChan := manager.Subscribe(clientID) - defer manager.Unsubscribe(clientID) - - initialState := manager.GetState() - event := BluetoothEvent{ - Type: "state_changed", - Data: initialState, - } - - if err := json.NewEncoder(conn).Encode(models.Response[BluetoothEvent]{ - ID: req.ID, - Result: &event, - }); err != nil { - return - } - - for state := range stateChan { - event := BluetoothEvent{ - Type: "state_changed", - Data: state, - } - if err := json.NewEncoder(conn).Encode(models.Response[BluetoothEvent]{ - Result: &event, - }); err != nil { - return - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/handlers_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/handlers_test.go deleted file mode 100644 index 1846eb0..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/handlers_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package bluez - -import ( - "context" - "testing" - "time" -) - -func TestBrokerIntegration(t *testing.T) { - broker := NewSubscriptionBroker(nil) - ctx := context.Background() - - req := PromptRequest{ - DevicePath: "/org/bluez/test", - DeviceName: "TestDevice", - RequestType: "pin", - Fields: []string{"pin"}, - } - - token, err := broker.Ask(ctx, req) - if err != nil { - t.Fatalf("Ask failed: %v", err) - } - - go func() { - time.Sleep(50 * time.Millisecond) - broker.Resolve(token, PromptReply{ - Secrets: map[string]string{"pin": "1234"}, - Accept: true, - }) - }() - - reply, err := broker.Wait(ctx, token) - if err != nil { - t.Fatalf("Wait failed: %v", err) - } - - if reply.Secrets["pin"] != "1234" { - t.Errorf("expected pin=1234, got %s", reply.Secrets["pin"]) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/manager.go deleted file mode 100644 index 4408d25..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/manager.go +++ /dev/null @@ -1,588 +0,0 @@ -package bluez - -import ( - "fmt" - "strings" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil" - "github.com/godbus/dbus/v5" -) - -const ( - adapter1Iface = "org.bluez.Adapter1" - objectMgrIface = "org.freedesktop.DBus.ObjectManager" - propertiesIface = "org.freedesktop.DBus.Properties" -) - -func NewManager() (*Manager, error) { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return nil, fmt.Errorf("system bus connection failed: %w", err) - } - - m := &Manager{ - state: &BluetoothState{ - Powered: false, - Discovering: false, - Devices: []Device{}, - PairedDevices: []Device{}, - ConnectedDevices: []Device{}, - }, - stateMutex: sync.RWMutex{}, - - stopChan: make(chan struct{}), - dbusConn: conn, - signals: make(chan *dbus.Signal, 256), - dirty: make(chan struct{}, 1), - eventQueue: make(chan func(), 32), - } - - broker := NewSubscriptionBroker(m.broadcastPairingPrompt) - m.promptBroker = broker - - adapter, err := m.findAdapter() - if err != nil { - conn.Close() - return nil, fmt.Errorf("no bluetooth adapter found: %w", err) - } - m.adapterPath = adapter - - if err := m.initialize(); err != nil { - conn.Close() - return nil, err - } - - if err := m.startAgent(); err != nil { - conn.Close() - return nil, fmt.Errorf("agent start failed: %w", err) - } - - if err := m.startSignalPump(); err != nil { - m.Close() - return nil, err - } - - m.notifierWg.Add(1) - go m.notifier() - - m.eventWg.Add(1) - go m.eventWorker() - - return m, nil -} - -func (m *Manager) findAdapter() (dbus.ObjectPath, error) { - obj := m.dbusConn.Object(bluezService, dbus.ObjectPath("/")) - var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant - - if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil { - return "", err - } - - for path, interfaces := range objects { - if _, ok := interfaces[adapter1Iface]; ok { - log.Infof("[BluezManager] found adapter: %s", path) - return path, nil - } - } - - return "", fmt.Errorf("no adapter found") -} - -func (m *Manager) initialize() error { - if err := m.updateAdapterState(); err != nil { - return err - } - - if err := m.updateDevices(); err != nil { - return err - } - - return nil -} - -func (m *Manager) updateAdapterState() error { - obj := m.dbusConn.Object(bluezService, m.adapterPath) - - poweredVar, err := obj.GetProperty(adapter1Iface + ".Powered") - if err != nil { - return err - } - - discoveringVar, err := obj.GetProperty(adapter1Iface + ".Discovering") - if err != nil { - return err - } - - m.stateMutex.Lock() - m.state.Powered = dbusutil.AsOr(poweredVar, false) - m.state.Discovering = dbusutil.AsOr(discoveringVar, false) - m.stateMutex.Unlock() - - return nil -} - -func (m *Manager) updateDevices() error { - obj := m.dbusConn.Object(bluezService, dbus.ObjectPath("/")) - var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant - - if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil { - return err - } - - devices := []Device{} - paired := []Device{} - connected := []Device{} - - for path, interfaces := range objects { - devProps, ok := interfaces[device1Iface] - if !ok { - continue - } - - if !strings.HasPrefix(string(path), string(m.adapterPath)+"/") { - continue - } - - dev := m.deviceFromProps(string(path), devProps) - devices = append(devices, dev) - - if dev.Paired { - paired = append(paired, dev) - } - if dev.Connected { - connected = append(connected, dev) - } - } - - m.stateMutex.Lock() - m.state.Devices = devices - m.state.PairedDevices = paired - m.state.ConnectedDevices = connected - m.stateMutex.Unlock() - - return nil -} - -func (m *Manager) deviceFromProps(path string, props map[string]dbus.Variant) Device { - return Device{ - Path: path, - Address: dbusutil.GetOr(props, "Address", ""), - Name: dbusutil.GetOr(props, "Name", ""), - Alias: dbusutil.GetOr(props, "Alias", ""), - Paired: dbusutil.GetOr(props, "Paired", false), - Trusted: dbusutil.GetOr(props, "Trusted", false), - Blocked: dbusutil.GetOr(props, "Blocked", false), - Connected: dbusutil.GetOr(props, "Connected", false), - Class: dbusutil.GetOr(props, "Class", uint32(0)), - Icon: dbusutil.GetOr(props, "Icon", ""), - RSSI: dbusutil.GetOr(props, "RSSI", int16(0)), - LegacyPairing: dbusutil.GetOr(props, "LegacyPairing", false), - } -} - -func (m *Manager) startAgent() error { - if m.promptBroker == nil { - return fmt.Errorf("prompt broker not initialized") - } - - agent, err := NewBluezAgent(m.promptBroker) - if err != nil { - return err - } - - m.agent = agent - return nil -} - -func (m *Manager) startSignalPump() error { - m.dbusConn.Signal(m.signals) - - if err := m.dbusConn.AddMatchSignal( - dbus.WithMatchInterface(propertiesIface), - dbus.WithMatchMember("PropertiesChanged"), - ); err != nil { - return err - } - - if err := m.dbusConn.AddMatchSignal( - dbus.WithMatchInterface(objectMgrIface), - dbus.WithMatchMember("InterfacesAdded"), - ); err != nil { - return err - } - - if err := m.dbusConn.AddMatchSignal( - dbus.WithMatchInterface(objectMgrIface), - dbus.WithMatchMember("InterfacesRemoved"), - ); err != nil { - return err - } - - m.sigWG.Add(1) - go func() { - defer m.sigWG.Done() - for { - select { - case <-m.stopChan: - return - case sig, ok := <-m.signals: - if !ok { - return - } - if sig == nil { - continue - } - m.handleSignal(sig) - } - } - }() - - return nil -} - -func (m *Manager) handleSignal(sig *dbus.Signal) { - switch sig.Name { - case propertiesIface + ".PropertiesChanged": - if len(sig.Body) < 2 { - return - } - - iface, ok := sig.Body[0].(string) - if !ok { - return - } - - changed, ok := sig.Body[1].(map[string]dbus.Variant) - if !ok { - return - } - - switch iface { - case adapter1Iface: - if strings.HasPrefix(string(sig.Path), string(m.adapterPath)) { - m.handleAdapterPropertiesChanged(changed) - } - case device1Iface: - m.handleDevicePropertiesChanged(sig.Path, changed) - } - - case objectMgrIface + ".InterfacesAdded": - m.notifySubscribers() - - case objectMgrIface + ".InterfacesRemoved": - m.notifySubscribers() - } -} - -func (m *Manager) handleAdapterPropertiesChanged(changed map[string]dbus.Variant) { - m.stateMutex.Lock() - dirty := false - - if powered, ok := dbusutil.Get[bool](changed, "Powered"); ok { - m.state.Powered = powered - dirty = true - } - if discovering, ok := dbusutil.Get[bool](changed, "Discovering"); ok { - m.state.Discovering = discovering - dirty = true - } - - m.stateMutex.Unlock() - - if dirty { - m.notifySubscribers() - } -} - -func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed map[string]dbus.Variant) { - paired, hasPaired := dbusutil.Get[bool](changed, "Paired") - _, hasConnected := changed["Connected"] - _, hasTrusted := changed["Trusted"] - - if hasPaired { - devicePath := string(path) - if paired { - _, wasPending := m.pendingPairings.LoadAndDelete(devicePath) - if wasPending { - select { - case m.eventQueue <- func() { - time.Sleep(300 * time.Millisecond) - log.Infof("[Bluetooth] Auto-trusting newly paired device: %s", devicePath) - if err := m.TrustDevice(devicePath, true); err != nil { - log.Warnf("[Bluetooth] Auto-trust failed: %v", err) - } - log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath) - if err := m.ConnectDevice(devicePath); err != nil { - log.Warnf("[Bluetooth] Auto-connect failed: %v", err) - } - }: - default: - } - } - } else { - m.pendingPairings.Delete(devicePath) - } - } - - if hasPaired || hasConnected || hasTrusted { - select { - case m.eventQueue <- func() { - time.Sleep(100 * time.Millisecond) - m.updateDevices() - m.notifySubscribers() - }: - default: - } - } -} - -func (m *Manager) eventWorker() { - defer m.eventWg.Done() - for { - select { - case <-m.stopChan: - return - case event := <-m.eventQueue: - event() - } - } -} - -func (m *Manager) notifier() { - defer m.notifierWg.Done() - const minGap = 200 * time.Millisecond - timer := time.NewTimer(minGap) - timer.Stop() - var pending bool - - for { - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.dirty: - if pending { - continue - } - pending = true - timer.Reset(minGap) - case <-timer.C: - if !pending { - continue - } - m.updateDevices() - - currentState := m.snapshotState() - - if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, ¤tState) { - pending = false - continue - } - - m.subscribers.Range(func(key string, ch chan BluetoothState) bool { - select { - case ch <- currentState: - default: - } - return true - }) - - stateCopy := currentState - m.lastNotifiedState = &stateCopy - pending = false - } - } -} - -func (m *Manager) notifySubscribers() { - select { - case m.dirty <- struct{}{}: - default: - } -} - -func (m *Manager) GetState() BluetoothState { - return m.snapshotState() -} - -func (m *Manager) snapshotState() BluetoothState { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - - s := *m.state - s.Devices = append([]Device(nil), m.state.Devices...) - s.PairedDevices = append([]Device(nil), m.state.PairedDevices...) - s.ConnectedDevices = append([]Device(nil), m.state.ConnectedDevices...) - return s -} - -func (m *Manager) Subscribe(id string) chan BluetoothState { - ch := make(chan BluetoothState, 64) - m.subscribers.Store(id, ch) - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if ch, ok := m.subscribers.LoadAndDelete(id); ok { - close(ch) - } -} - -func (m *Manager) SubscribePairing(id string) chan PairingPrompt { - ch := make(chan PairingPrompt, 16) - m.pairingSubscribers.Store(id, ch) - return ch -} - -func (m *Manager) UnsubscribePairing(id string) { - if ch, ok := m.pairingSubscribers.LoadAndDelete(id); ok { - close(ch) - } -} - -func (m *Manager) broadcastPairingPrompt(prompt PairingPrompt) { - m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool { - select { - case ch <- prompt: - default: - } - return true - }) -} - -func (m *Manager) SubmitPairing(token string, secrets map[string]string, accept bool) error { - if m.promptBroker == nil { - return fmt.Errorf("prompt broker not initialized") - } - - return m.promptBroker.Resolve(token, PromptReply{ - Secrets: secrets, - Accept: accept, - Cancel: false, - }) -} - -func (m *Manager) CancelPairing(token string) error { - if m.promptBroker == nil { - return fmt.Errorf("prompt broker not initialized") - } - - return m.promptBroker.Resolve(token, PromptReply{ - Cancel: true, - }) -} - -func (m *Manager) StartDiscovery() error { - obj := m.dbusConn.Object(bluezService, m.adapterPath) - return obj.Call(adapter1Iface+".StartDiscovery", 0).Err -} - -func (m *Manager) StopDiscovery() error { - obj := m.dbusConn.Object(bluezService, m.adapterPath) - return obj.Call(adapter1Iface+".StopDiscovery", 0).Err -} - -func (m *Manager) SetPowered(powered bool) error { - obj := m.dbusConn.Object(bluezService, m.adapterPath) - return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err -} - -func (m *Manager) PairDevice(devicePath string) error { - m.pendingPairings.Store(devicePath, true) - - obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath)) - err := obj.Call(device1Iface+".Pair", 0).Err - - if err != nil { - m.pendingPairings.Delete(devicePath) - } - - return err -} - -func (m *Manager) ConnectDevice(devicePath string) error { - obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath)) - return obj.Call(device1Iface+".Connect", 0).Err -} - -func (m *Manager) DisconnectDevice(devicePath string) error { - obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath)) - return obj.Call(device1Iface+".Disconnect", 0).Err -} - -func (m *Manager) RemoveDevice(devicePath string) error { - obj := m.dbusConn.Object(bluezService, m.adapterPath) - return obj.Call(adapter1Iface+".RemoveDevice", 0, dbus.ObjectPath(devicePath)).Err -} - -func (m *Manager) TrustDevice(devicePath string, trusted bool) error { - obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath)) - return obj.Call(propertiesIface+".Set", 0, device1Iface, "Trusted", dbus.MakeVariant(trusted)).Err -} - -func (m *Manager) Close() { - close(m.stopChan) - m.notifierWg.Wait() - m.eventWg.Wait() - - m.sigWG.Wait() - - if m.signals != nil { - m.dbusConn.RemoveSignal(m.signals) - close(m.signals) - } - - if m.agent != nil { - m.agent.Close() - } - - m.subscribers.Range(func(key string, ch chan BluetoothState) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - - m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool { - close(ch) - m.pairingSubscribers.Delete(key) - return true - }) - - if m.dbusConn != nil { - m.dbusConn.Close() - } -} - -func stateChanged(old, new *BluetoothState) bool { - if old.Powered != new.Powered { - return true - } - if old.Discovering != new.Discovering { - return true - } - if len(old.Devices) != len(new.Devices) { - return true - } - if len(old.PairedDevices) != len(new.PairedDevices) { - return true - } - if len(old.ConnectedDevices) != len(new.ConnectedDevices) { - return true - } - for i := range old.Devices { - if old.Devices[i].Path != new.Devices[i].Path { - return true - } - if old.Devices[i].Paired != new.Devices[i].Paired { - return true - } - if old.Devices[i].Connected != new.Devices[i].Connected { - return true - } - } - return false -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/subscription_broker.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/subscription_broker.go deleted file mode 100644 index ea04a19..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/subscription_broker.go +++ /dev/null @@ -1,86 +0,0 @@ -package bluez - -import ( - "context" - "fmt" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" -) - -type SubscriptionBroker struct { - pending syncmap.Map[string, chan PromptReply] - requests syncmap.Map[string, PromptRequest] - broadcastPrompt func(PairingPrompt) -} - -func NewSubscriptionBroker(broadcastPrompt func(PairingPrompt)) PromptBroker { - return &SubscriptionBroker{ - broadcastPrompt: broadcastPrompt, - } -} - -func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string, error) { - token, err := generateToken() - if err != nil { - return "", err - } - - replyChan := make(chan PromptReply, 1) - b.pending.Store(token, replyChan) - b.requests.Store(token, req) - - if b.broadcastPrompt != nil { - prompt := PairingPrompt{ - Token: token, - DevicePath: req.DevicePath, - DeviceName: req.DeviceName, - DeviceAddr: req.DeviceAddr, - RequestType: req.RequestType, - Fields: req.Fields, - Hints: req.Hints, - Passkey: req.Passkey, - } - b.broadcastPrompt(prompt) - } - - return token, nil -} - -func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) { - replyChan, exists := b.pending.Load(token) - if !exists { - return PromptReply{}, fmt.Errorf("unknown token: %s", token) - } - - select { - case <-ctx.Done(): - b.cleanup(token) - return PromptReply{}, errdefs.ErrSecretPromptTimeout - case reply := <-replyChan: - b.cleanup(token) - if reply.Cancel { - return reply, errdefs.ErrSecretPromptCancelled - } - return reply, nil - } -} - -func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error { - replyChan, exists := b.pending.Load(token) - if !exists { - return fmt.Errorf("unknown or expired token: %s", token) - } - - select { - case replyChan <- reply: - return nil - default: - return fmt.Errorf("failed to deliver reply for token: %s", token) - } -} - -func (b *SubscriptionBroker) cleanup(token string) { - b.pending.Delete(token) - b.requests.Delete(token) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/types.go deleted file mode 100644 index 663acca..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/types.go +++ /dev/null @@ -1,78 +0,0 @@ -package bluez - -import ( - "sync" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" - "github.com/godbus/dbus/v5" -) - -type BluetoothState struct { - Powered bool `json:"powered"` - Discovering bool `json:"discovering"` - Devices []Device `json:"devices"` - PairedDevices []Device `json:"pairedDevices"` - ConnectedDevices []Device `json:"connectedDevices"` -} - -type Device struct { - Path string `json:"path"` - Address string `json:"address"` - Name string `json:"name"` - Alias string `json:"alias"` - Paired bool `json:"paired"` - Trusted bool `json:"trusted"` - Blocked bool `json:"blocked"` - Connected bool `json:"connected"` - Class uint32 `json:"class"` - Icon string `json:"icon"` - RSSI int16 `json:"rssi"` - LegacyPairing bool `json:"legacyPairing"` -} - -type PromptRequest struct { - DevicePath string `json:"devicePath"` - DeviceName string `json:"deviceName"` - DeviceAddr string `json:"deviceAddr"` - RequestType string `json:"requestType"` - Fields []string `json:"fields"` - Hints []string `json:"hints"` - Passkey *uint32 `json:"passkey,omitempty"` -} - -type PromptReply struct { - Secrets map[string]string `json:"secrets"` - Accept bool `json:"accept"` - Cancel bool `json:"cancel"` -} - -type PairingPrompt struct { - Token string `json:"token"` - DevicePath string `json:"devicePath"` - DeviceName string `json:"deviceName"` - DeviceAddr string `json:"deviceAddr"` - RequestType string `json:"requestType"` - Fields []string `json:"fields"` - Hints []string `json:"hints"` - Passkey *uint32 `json:"passkey,omitempty"` -} - -type Manager struct { - state *BluetoothState - stateMutex sync.RWMutex - subscribers syncmap.Map[string, chan BluetoothState] - stopChan chan struct{} - dbusConn *dbus.Conn - signals chan *dbus.Signal - sigWG sync.WaitGroup - agent *BluezAgent - promptBroker PromptBroker - pairingSubscribers syncmap.Map[string, chan PairingPrompt] - dirty chan struct{} - notifierWg sync.WaitGroup - lastNotifiedState *BluetoothState - adapterPath dbus.ObjectPath - pendingPairings syncmap.Map[string, bool] - eventQueue chan func() - eventWg sync.WaitGroup -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/types_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/types_test.go deleted file mode 100644 index ab5b857..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/bluez/types_test.go +++ /dev/null @@ -1,210 +0,0 @@ -package bluez - -import ( - "encoding/json" - "testing" -) - -func TestBluetoothStateJSON(t *testing.T) { - state := BluetoothState{ - Powered: true, - Discovering: false, - Devices: []Device{ - { - Path: "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF", - Address: "AA:BB:CC:DD:EE:FF", - Name: "TestDevice", - Alias: "My Device", - Paired: true, - Trusted: false, - Connected: true, - Class: 0x240418, - Icon: "audio-headset", - RSSI: -50, - }, - }, - PairedDevices: []Device{ - { - Path: "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF", - Address: "AA:BB:CC:DD:EE:FF", - Paired: true, - }, - }, - ConnectedDevices: []Device{ - { - Path: "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF", - Address: "AA:BB:CC:DD:EE:FF", - Connected: true, - }, - }, - } - - data, err := json.Marshal(state) - if err != nil { - t.Fatalf("failed to marshal state: %v", err) - } - - var decoded BluetoothState - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("failed to unmarshal state: %v", err) - } - - if decoded.Powered != state.Powered { - t.Errorf("expected Powered=%v, got %v", state.Powered, decoded.Powered) - } - - if len(decoded.Devices) != 1 { - t.Fatalf("expected 1 device, got %d", len(decoded.Devices)) - } - - if decoded.Devices[0].Address != "AA:BB:CC:DD:EE:FF" { - t.Errorf("expected address AA:BB:CC:DD:EE:FF, got %s", decoded.Devices[0].Address) - } -} - -func TestDeviceJSON(t *testing.T) { - device := Device{ - Path: "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF", - Address: "AA:BB:CC:DD:EE:FF", - Name: "TestDevice", - Alias: "My Device", - Paired: true, - Trusted: true, - Blocked: false, - Connected: true, - Class: 0x240418, - Icon: "audio-headset", - RSSI: -50, - LegacyPairing: false, - } - - data, err := json.Marshal(device) - if err != nil { - t.Fatalf("failed to marshal device: %v", err) - } - - var decoded Device - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("failed to unmarshal device: %v", err) - } - - if decoded.Address != device.Address { - t.Errorf("expected Address=%s, got %s", device.Address, decoded.Address) - } - - if decoded.Name != device.Name { - t.Errorf("expected Name=%s, got %s", device.Name, decoded.Name) - } - - if decoded.Paired != device.Paired { - t.Errorf("expected Paired=%v, got %v", device.Paired, decoded.Paired) - } - - if decoded.RSSI != device.RSSI { - t.Errorf("expected RSSI=%d, got %d", device.RSSI, decoded.RSSI) - } -} - -func TestPairingPromptJSON(t *testing.T) { - passkey := uint32(123456) - prompt := PairingPrompt{ - Token: "test-token", - DevicePath: "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF", - DeviceName: "TestDevice", - DeviceAddr: "AA:BB:CC:DD:EE:FF", - RequestType: "confirm", - Fields: []string{"decision"}, - Hints: []string{}, - Passkey: &passkey, - } - - data, err := json.Marshal(prompt) - if err != nil { - t.Fatalf("failed to marshal prompt: %v", err) - } - - var decoded PairingPrompt - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("failed to unmarshal prompt: %v", err) - } - - if decoded.Token != prompt.Token { - t.Errorf("expected Token=%s, got %s", prompt.Token, decoded.Token) - } - - if decoded.DeviceName != prompt.DeviceName { - t.Errorf("expected DeviceName=%s, got %s", prompt.DeviceName, decoded.DeviceName) - } - - if decoded.Passkey == nil { - t.Fatal("expected non-nil Passkey") - } - - if *decoded.Passkey != *prompt.Passkey { - t.Errorf("expected Passkey=%d, got %d", *prompt.Passkey, *decoded.Passkey) - } -} - -func TestPromptReplyJSON(t *testing.T) { - reply := PromptReply{ - Secrets: map[string]string{ - "pin": "1234", - "passkey": "567890", - }, - Accept: true, - Cancel: false, - } - - data, err := json.Marshal(reply) - if err != nil { - t.Fatalf("failed to marshal reply: %v", err) - } - - var decoded PromptReply - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("failed to unmarshal reply: %v", err) - } - - if decoded.Secrets["pin"] != reply.Secrets["pin"] { - t.Errorf("expected pin=%s, got %s", reply.Secrets["pin"], decoded.Secrets["pin"]) - } - - if decoded.Accept != reply.Accept { - t.Errorf("expected Accept=%v, got %v", reply.Accept, decoded.Accept) - } -} - -func TestPromptRequestJSON(t *testing.T) { - passkey := uint32(123456) - req := PromptRequest{ - DevicePath: "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF", - DeviceName: "TestDevice", - DeviceAddr: "AA:BB:CC:DD:EE:FF", - RequestType: "confirm", - Fields: []string{"decision"}, - Hints: []string{"hint1", "hint2"}, - Passkey: &passkey, - } - - data, err := json.Marshal(req) - if err != nil { - t.Fatalf("failed to marshal request: %v", err) - } - - var decoded PromptRequest - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("failed to unmarshal request: %v", err) - } - - if decoded.DevicePath != req.DevicePath { - t.Errorf("expected DevicePath=%s, got %s", req.DevicePath, decoded.DevicePath) - } - - if decoded.RequestType != req.RequestType { - t.Errorf("expected RequestType=%s, got %s", req.RequestType, decoded.RequestType) - } - - if len(decoded.Fields) != len(req.Fields) { - t.Errorf("expected %d fields, got %d", len(req.Fields), len(decoded.Fields)) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc.go deleted file mode 100644 index 00cf2d9..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc.go +++ /dev/null @@ -1,510 +0,0 @@ -package brightness - -import ( - "encoding/binary" - "fmt" - "math" - "os" - "strings" - "syscall" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "golang.org/x/sys/unix" -) - -const ( - I2C_SLAVE = 0x0703 - DDCCI_ADDR = 0x37 - DDCCI_VCP_GET = 0x01 - DDCCI_VCP_SET = 0x03 - VCP_BRIGHTNESS = 0x10 - DDC_SOURCE_ADDR = 0x51 -) - -func NewDDCBackend() (*DDCBackend, error) { - b := &DDCBackend{ - scanInterval: 30 * time.Second, - debounceTimers: make(map[string]*time.Timer), - debouncePending: make(map[string]ddcPendingSet), - } - - if err := b.scanI2CDevices(); err != nil { - return nil, err - } - - return b, nil -} - -func (b *DDCBackend) scanI2CDevices() error { - return b.scanI2CDevicesInternal(false) -} - -func (b *DDCBackend) ForceRescan() error { - return b.scanI2CDevicesInternal(true) -} - -func (b *DDCBackend) scanI2CDevicesInternal(force bool) error { - b.scanMutex.Lock() - defer b.scanMutex.Unlock() - - if !force && time.Since(b.lastScan) < b.scanInterval { - return nil - } - - activeBuses := make(map[int]bool) - - for i := 0; i < 32; i++ { - busPath := fmt.Sprintf("/dev/i2c-%d", i) - if _, err := os.Stat(busPath); os.IsNotExist(err) { - continue - } - - if isIgnorableI2CBus(i) { - log.Debugf("Skipping ignorable i2c-%d", i) - continue - } - - activeBuses[i] = true - id := fmt.Sprintf("ddc:i2c-%d", i) - - dev, err := b.probeDDCDevice(i) - if err != nil || dev == nil { - continue - } - - dev.id = id - b.devices.Store(id, dev) - log.Debugf("found DDC device on i2c-%d", i) - } - - b.devices.Range(func(id string, dev *ddcDevice) bool { - if !activeBuses[dev.bus] { - b.devices.Delete(id) - log.Debugf("removed DDC device %s (bus no longer exists)", id) - } - return true - }) - - b.lastScan = time.Now() - - return nil -} - -func (b *DDCBackend) probeDDCDevice(bus int) (*ddcDevice, error) { - busPath := fmt.Sprintf("/dev/i2c-%d", bus) - - fd, err := syscall.Open(busPath, syscall.O_RDWR, 0) - if err != nil { - return nil, err - } - defer syscall.Close(fd) - - if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), I2C_SLAVE, uintptr(DDCCI_ADDR)); errno != 0 { - return nil, errno - } - - dummy := make([]byte, 32) - syscall.Read(fd, dummy) //nolint:errcheck - - writebuf := []byte{0x00} - n, err := syscall.Write(fd, writebuf) - if err == nil && n == len(writebuf) { - name := b.getDDCName(bus) - dev := &ddcDevice{ - bus: bus, - addr: DDCCI_ADDR, - name: name, - } - b.readInitialBrightness(fd, dev) - return dev, nil - } - - readbuf := make([]byte, 4) - n, err = syscall.Read(fd, readbuf) - if err != nil || n == 0 { - return nil, fmt.Errorf("x37 unresponsive") - } - - name := b.getDDCName(bus) - - dev := &ddcDevice{ - bus: bus, - addr: DDCCI_ADDR, - name: name, - } - b.readInitialBrightness(fd, dev) - return dev, nil -} - -func (b *DDCBackend) getDDCName(bus int) string { - sysfsPath := fmt.Sprintf("/sys/class/i2c-adapter/i2c-%d/name", bus) - data, err := os.ReadFile(sysfsPath) - if err != nil { - return fmt.Sprintf("I2C-%d", bus) - } - - name := strings.TrimSpace(string(data)) - if name == "" { - name = fmt.Sprintf("I2C-%d", bus) - } - - return name -} - -func (b *DDCBackend) readInitialBrightness(fd int, dev *ddcDevice) { - cap, err := b.getVCPFeature(fd, VCP_BRIGHTNESS) - if err != nil { - log.Debugf("failed to read initial brightness for %s: %v", dev.name, err) - return - } - - dev.max = cap.max - dev.lastBrightness = cap.current - log.Debugf("initialized %s with brightness %d/%d", dev.name, cap.current, cap.max) -} - -func (b *DDCBackend) GetDevices() ([]Device, error) { - if err := b.scanI2CDevices(); err != nil { - log.Debugf("DDC scan error: %v", err) - } - - devices := make([]Device, 0) - - b.devices.Range(func(id string, dev *ddcDevice) bool { - devices = append(devices, Device{ - Class: ClassDDC, - ID: id, - Name: dev.name, - Current: dev.lastBrightness, - Max: dev.max, - CurrentPercent: dev.lastBrightness, - Backend: "ddc", - }) - return true - }) - - return devices, nil -} - -func (b *DDCBackend) SetBrightness(id string, value int, exponential bool, callback func()) error { - return b.SetBrightnessWithExponent(id, value, exponential, 1.2, callback) -} - -func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential bool, exponent float64, callback func()) error { - _, ok := b.devices.Load(id) - - if !ok { - if err := b.scanI2CDevicesInternal(true); err != nil { - log.Debugf("rescan failed for %s: %v", id, err) - } - _, ok = b.devices.Load(id) - } - - if !ok { - return fmt.Errorf("device not found: %s", id) - } - - if value < 0 { - return fmt.Errorf("value out of range: %d", value) - } - - b.debounceMutex.Lock() - b.debouncePending[id] = ddcPendingSet{ - percent: value, - callback: callback, - } - - if timer, exists := b.debounceTimers[id]; exists { - timer.Reset(200 * time.Millisecond) - } else { - b.debounceWg.Add(1) - b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() { - defer b.debounceWg.Done() - b.debounceMutex.Lock() - pending, exists := b.debouncePending[id] - if exists { - delete(b.debouncePending, id) - } - b.debounceMutex.Unlock() - - if !exists { - return - } - - err := b.setBrightnessImmediateWithExponent(id, pending.percent) - if err != nil { - log.Debugf("Failed to set brightness for %s: %v", id, err) - } - - if pending.callback != nil { - pending.callback() - } - }) - } - b.debounceMutex.Unlock() - - return nil -} - -func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) error { - dev, ok := b.devices.Load(id) - - if !ok { - if err := b.scanI2CDevicesInternal(true); err != nil { - log.Debugf("rescan failed for %s: %v", id, err) - } - dev, ok = b.devices.Load(id) - } - - if !ok { - return fmt.Errorf("device not found: %s", id) - } - - busPath := fmt.Sprintf("/dev/i2c-%d", dev.bus) - - if _, err := os.Stat(busPath); os.IsNotExist(err) { - b.devices.Delete(id) - log.Debugf("removed stale DDC device %s (bus no longer exists)", id) - return fmt.Errorf("device disconnected: %s", id) - } - - fd, err := syscall.Open(busPath, syscall.O_RDWR, 0) - if err != nil { - b.devices.Delete(id) - log.Debugf("removed DDC device %s (open failed: %v)", id, err) - return fmt.Errorf("open i2c device: %w", err) - } - defer syscall.Close(fd) - - if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), I2C_SLAVE, uintptr(dev.addr)); errno != 0 { - return fmt.Errorf("set i2c slave addr: %w", errno) - } - - max := dev.max - if max == 0 { - cap, err := b.getVCPFeature(fd, VCP_BRIGHTNESS) - if err != nil { - return fmt.Errorf("get current capability: %w", err) - } - max = cap.max - dev.max = max - b.devices.Store(id, dev) - } - - if err := b.setVCPFeature(fd, VCP_BRIGHTNESS, value); err != nil { - return fmt.Errorf("set vcp feature: %w", err) - } - - log.Debugf("set %s to %d/%d", id, value, max) - - dev.max = max - dev.lastBrightness = value - b.devices.Store(id, dev) - - return nil -} - -func (b *DDCBackend) getVCPFeature(fd int, vcp byte) (*ddcCapability, error) { - for flushTry := 0; flushTry < 3; flushTry++ { - dummy := make([]byte, 32) - n, _ := syscall.Read(fd, dummy) - if n == 0 { - break - } - time.Sleep(20 * time.Millisecond) - } - - data := []byte{ - DDCCI_VCP_GET, - vcp, - } - - payload := []byte{ - DDC_SOURCE_ADDR, - byte(len(data)) | 0x80, - } - payload = append(payload, data...) - payload = append(payload, ddcciChecksum(payload)) - - n, err := syscall.Write(fd, payload) - if err != nil || n != len(payload) { - return nil, fmt.Errorf("write i2c: %w", err) - } - - time.Sleep(50 * time.Millisecond) - - pollFds := []unix.PollFd{ - { - Fd: int32(fd), - Events: unix.POLLIN, - }, - } - - pollTimeout := 200 - pollResult, err := unix.Poll(pollFds, pollTimeout) - if err != nil { - return nil, fmt.Errorf("poll i2c: %w", err) - } - if pollResult == 0 { - return nil, fmt.Errorf("poll timeout after %dms", pollTimeout) - } - if pollFds[0].Revents&unix.POLLIN == 0 { - return nil, fmt.Errorf("poll returned but POLLIN not set") - } - - response := make([]byte, 12) - n, err = syscall.Read(fd, response) - if err != nil || n < 8 { - return nil, fmt.Errorf("read i2c: %w", err) - } - - if response[0] != 0x6E || response[2] != 0x02 { - return nil, fmt.Errorf("invalid ddc response") - } - - resultCode := response[3] - if resultCode != 0x00 { - return nil, fmt.Errorf("vcp feature not supported") - } - - responseVCP := response[4] - if responseVCP != vcp { - return nil, fmt.Errorf("vcp mismatch: wanted 0x%02x, got 0x%02x", vcp, responseVCP) - } - - maxHigh := response[6] - maxLow := response[7] - currentHigh := response[8] - currentLow := response[9] - - max := int(binary.BigEndian.Uint16([]byte{maxHigh, maxLow})) - current := int(binary.BigEndian.Uint16([]byte{currentHigh, currentLow})) - - return &ddcCapability{ - vcp: vcp, - max: max, - current: current, - }, nil -} - -func ddcciChecksum(payload []byte) byte { - sum := byte(0x6E) - for _, b := range payload { - sum ^= b - } - return sum -} - -func (b *DDCBackend) setVCPFeature(fd int, vcp byte, value int) error { - data := []byte{ - DDCCI_VCP_SET, - vcp, - byte(value >> 8), - byte(value & 0xFF), - } - - payload := []byte{ - DDC_SOURCE_ADDR, - byte(len(data)) | 0x80, - } - payload = append(payload, data...) - payload = append(payload, ddcciChecksum(payload)) - - if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), I2C_SLAVE, uintptr(DDCCI_ADDR)); errno != 0 { - return fmt.Errorf("set i2c slave for write: %w", errno) - } - - n, err := syscall.Write(fd, payload) - if err != nil || n != len(payload) { - return fmt.Errorf("write i2c: wrote %d/%d: %w", n, len(payload), err) - } - - time.Sleep(50 * time.Millisecond) - - return nil -} - -func (b *DDCBackend) percentToValue(percent int, max int, exponential bool) int { - const minValue = 1 - - if percent == 0 { - return minValue - } - - usableRange := max - minValue - var value int - - if exponential { - const exponent = 2.0 - normalizedPercent := float64(percent) / 100.0 - hardwarePercent := math.Pow(normalizedPercent, 1.0/exponent) - value = minValue + int(math.Round(hardwarePercent*float64(usableRange))) - } else { - value = minValue + ((percent - 1) * usableRange / 99) - } - - if value < minValue { - value = minValue - } - if value > max { - value = max - } - - return value -} - -func (b *DDCBackend) valueToPercent(value int, max int, exponential bool) int { - const minValue = 1 - - if max == 0 { - return 0 - } - - if value <= minValue { - return 1 - } - - usableRange := max - minValue - if usableRange == 0 { - return 100 - } - - var percent int - - if exponential { - const exponent = 2.0 - linearPercent := 1 + ((value - minValue) * 99 / usableRange) - normalizedLinear := float64(linearPercent) / 100.0 - expPercent := math.Pow(normalizedLinear, exponent) - percent = int(math.Round(expPercent * 100.0)) - } else { - percent = 1 + ((value - minValue) * 99 / usableRange) - } - - if percent > 100 { - percent = 100 - } - if percent < 1 { - percent = 1 - } - - return percent -} - -func (b *DDCBackend) WaitPending() { - done := make(chan struct{}) - go func() { - b.debounceWg.Wait() - close(done) - }() - - select { - case <-done: - case <-time.After(5 * time.Second): - log.Debug("WaitPending timed out waiting for DDC writes") - } -} - -func (b *DDCBackend) Close() { -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter.go deleted file mode 100644 index 12fc535..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter.go +++ /dev/null @@ -1,135 +0,0 @@ -package brightness - -import ( - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" -) - -// isIgnorableI2CBus checks if an I2C bus should be skipped during DDC probing. -// Based on ddcutil's sysfs_is_ignorable_i2c_device() (sysfs_base.c:1441) -func isIgnorableI2CBus(busno int) bool { - name := getI2CDeviceSysfsName(busno) - driver := getI2CSysfsDriver(busno) - - if name != "" && isIgnorableI2CDeviceName(name, driver) { - log.Debugf("i2c-%d: ignoring '%s' (driver: %s)", busno, name, driver) - return true - } - - // Only probe display adapters (0x03xxxx) and docking stations (0x0axxxx) - class := getI2CDeviceSysfsClass(busno) - if class != 0 { - classHigh := class & 0xFFFF0000 - ignorable := (classHigh != 0x030000 && classHigh != 0x0A0000) - if ignorable { - log.Debugf("i2c-%d: ignoring class 0x%08x", busno, class) - } - return ignorable - } - - return false -} - -// Based on ddcutil's ignorable_i2c_device_sysfs_name() (sysfs_base.c:1408) -func isIgnorableI2CDeviceName(name, driver string) bool { - ignorablePrefixes := []string{ - "SMBus", - "Synopsys DesignWare", - "soc:i2cdsi", - "smu", - "mac-io", - "u4", - "AMDGPU SMU", // AMD Navi2+ - probing hangs GPU - } - - for _, prefix := range ignorablePrefixes { - if strings.HasPrefix(name, prefix) { - return true - } - } - - // nouveau driver: only nvkm-* buses are valid - if driver == "nouveau" && !strings.HasPrefix(name, "nvkm-") { - return true - } - - return false -} - -// Based on ddcutil's get_i2c_device_sysfs_name() (sysfs_base.c:1175) -func getI2CDeviceSysfsName(busno int) string { - path := fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d/name", busno) - data, err := os.ReadFile(path) - if err != nil { - return "" - } - return strings.TrimSpace(string(data)) -} - -// Based on ddcutil's get_i2c_device_sysfs_class() (sysfs_base.c:1380) -func getI2CDeviceSysfsClass(busno int) uint32 { - classPath := fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d/device/class", busno) - data, err := os.ReadFile(classPath) - if err != nil { - classPath = fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d/device/device/device/class", busno) - data, err = os.ReadFile(classPath) - if err != nil { - return 0 - } - } - - classStr := strings.TrimSpace(string(data)) - classStr = strings.TrimPrefix(classStr, "0x") - - class, err := strconv.ParseUint(classStr, 16, 32) - if err != nil { - return 0 - } - - return uint32(class) -} - -// Based on ddcutil's get_i2c_sysfs_driver_by_busno() (sysfs_base.c:1284) -func getI2CSysfsDriver(busno int) string { - devicePath := fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d", busno) - adapterPath, err := findI2CAdapter(devicePath) - if err != nil { - return "" - } - - driverLink := filepath.Join(adapterPath, "driver") - target, err := os.Readlink(driverLink) - if err != nil { - return "" - } - - return filepath.Base(target) -} - -func findI2CAdapter(devicePath string) (string, error) { - currentPath := devicePath - - for depth := 0; depth < 10; depth++ { - if _, err := os.Stat(filepath.Join(currentPath, "name")); err == nil { - return currentPath, nil - } - - deviceLink := filepath.Join(currentPath, "device") - target, err := os.Readlink(deviceLink) - if err != nil { - break - } - - if !filepath.IsAbs(target) { - target = filepath.Join(filepath.Dir(currentPath), target) - } - currentPath = filepath.Clean(target) - } - - return "", fmt.Errorf("could not find adapter for %s", devicePath) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter_test.go deleted file mode 100644 index dc3751f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package brightness - -import ( - "testing" -) - -func TestIsIgnorableI2CDeviceName(t *testing.T) { - tests := []struct { - name string - deviceName string - driver string - want bool - }{ - { - name: "AMDGPU SMU should be ignored", - deviceName: "AMDGPU SMU", - driver: "amdgpu", - want: true, - }, - { - name: "SMBus should be ignored", - deviceName: "SMBus I801 adapter", - driver: "", - want: true, - }, - { - name: "Synopsys DesignWare should be ignored", - deviceName: "Synopsys DesignWare I2C adapter", - driver: "", - want: true, - }, - { - name: "smu prefix should be ignored (Mac G5)", - deviceName: "smu-i2c-controller", - driver: "", - want: true, - }, - { - name: "Regular NVIDIA DDC should not be ignored", - deviceName: "NVIDIA i2c adapter 1", - driver: "nvidia", - want: false, - }, - { - name: "nouveau nvkm bus should not be ignored", - deviceName: "nvkm-0000:01:00.0-bus-0000", - driver: "nouveau", - want: false, - }, - { - name: "nouveau non-nvkm bus should be ignored", - deviceName: "nouveau-other-bus", - driver: "nouveau", - want: true, - }, - { - name: "Regular AMD display adapter should not be ignored", - deviceName: "AMDGPU DM i2c hw bus 0", - driver: "amdgpu", - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := isIgnorableI2CDeviceName(tt.deviceName, tt.driver) - if got != tt.want { - t.Errorf("isIgnorableI2CDeviceName(%q, %q) = %v, want %v", - tt.deviceName, tt.driver, got, tt.want) - } - }) - } -} - -func TestClassFiltering(t *testing.T) { - tests := []struct { - name string - class uint32 - want bool - }{ - { - name: "Display adapter class should not be ignored", - class: 0x030000, - want: false, - }, - { - name: "Docking station class should not be ignored", - class: 0x0a0000, - want: false, - }, - { - name: "Display adapter with subclass should not be ignored", - class: 0x030001, - want: false, - }, - { - name: "SMBus class should be ignored", - class: 0x0c0500, - want: true, - }, - { - name: "Bridge class should be ignored", - class: 0x060400, - want: true, - }, - { - name: "Generic system peripheral should be ignored", - class: 0x088000, - want: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - classHigh := tt.class & 0xFFFF0000 - ignorable := (classHigh != 0x030000 && classHigh != 0x0A0000) - if ignorable != tt.want { - t.Errorf("class 0x%08x: ignorable = %v, want %v", tt.class, ignorable, tt.want) - } - }) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_test.go deleted file mode 100644 index 7f85510..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package brightness - -import ( - "testing" -) - -func TestDDCBackend_PercentConversions(t *testing.T) { - tests := []struct { - name string - max int - percent int - wantValue int - }{ - { - name: "0% should map to minValue=1", - max: 100, - percent: 0, - wantValue: 1, - }, - { - name: "1% should be 1", - max: 100, - percent: 1, - wantValue: 1, - }, - { - name: "50% should be ~50", - max: 100, - percent: 50, - wantValue: 50, - }, - { - name: "100% should be max", - max: 100, - percent: 100, - wantValue: 100, - }, - } - - b := &DDCBackend{} - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := b.percentToValue(tt.percent, tt.max, false) - diff := got - tt.wantValue - if diff < 0 { - diff = -diff - } - if diff > 1 { - t.Errorf("percentToValue() = %v, want %v (±1)", got, tt.wantValue) - } - }) - } -} - -func TestDDCBackend_ValueToPercent(t *testing.T) { - tests := []struct { - name string - max int - value int - wantPercent int - tolerance int - }{ - { - name: "zero value should be 1%", - max: 100, - value: 0, - wantPercent: 1, - tolerance: 0, - }, - { - name: "min value should be 1%", - max: 100, - value: 1, - wantPercent: 1, - tolerance: 0, - }, - { - name: "mid value should be ~50%", - max: 100, - value: 50, - wantPercent: 50, - tolerance: 2, - }, - { - name: "max value should be 100%", - max: 100, - value: 100, - wantPercent: 100, - tolerance: 0, - }, - } - - b := &DDCBackend{} - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := b.valueToPercent(tt.value, tt.max, false) - diff := got - tt.wantPercent - if diff < 0 { - diff = -diff - } - if diff > tt.tolerance { - t.Errorf("valueToPercent() = %v, want %v (±%d)", got, tt.wantPercent, tt.tolerance) - } - }) - } -} - -func TestDDCBackend_RoundTrip(t *testing.T) { - b := &DDCBackend{} - - tests := []struct { - name string - max int - percent int - }{ - {"1%", 100, 1}, - {"25%", 100, 25}, - {"50%", 100, 50}, - {"75%", 100, 75}, - {"100%", 100, 100}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - value := b.percentToValue(tt.percent, tt.max, false) - gotPercent := b.valueToPercent(value, tt.max, false) - - if diff := tt.percent - gotPercent; diff < -1 || diff > 1 { - t.Errorf("round trip failed: wanted %d%%, got %d%% (value=%d)", tt.percent, gotPercent, value) - } - }) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/handlers.go deleted file mode 100644 index c71163e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/handlers.go +++ /dev/null @@ -1,124 +0,0 @@ -package brightness - -import ( - "encoding/json" - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -func HandleRequest(conn net.Conn, req models.Request, m *Manager) { - switch req.Method { - case "brightness.getState": - handleGetState(conn, req, m) - case "brightness.setBrightness": - handleSetBrightness(conn, req, m) - case "brightness.increment": - handleIncrement(conn, req, m) - case "brightness.decrement": - handleDecrement(conn, req, m) - case "brightness.rescan": - handleRescan(conn, req, m) - case "brightness.subscribe": - handleSubscribe(conn, req, m) - default: - models.RespondError(conn, req.ID, "unknown method: "+req.Method) - } -} - -func handleGetState(conn net.Conn, req models.Request, m *Manager) { - models.Respond(conn, req.ID, m.GetState()) -} - -func handleSetBrightness(conn net.Conn, req models.Request, m *Manager) { - device, err := params.String(req.Params, "device") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - percent, err := params.Int(req.Params, "percent") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - exponential := params.BoolOpt(req.Params, "exponential", false) - exponent := params.FloatOpt(req.Params, "exponent", 1.2) - - if err := m.SetBrightnessWithExponent(device, percent, exponential, exponent); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, m.GetState()) -} - -func handleIncrement(conn net.Conn, req models.Request, m *Manager) { - device, err := params.String(req.Params, "device") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - step := params.IntOpt(req.Params, "step", 10) - exponential := params.BoolOpt(req.Params, "exponential", false) - exponent := params.FloatOpt(req.Params, "exponent", 1.2) - - if err := m.IncrementBrightnessWithExponent(device, step, exponential, exponent); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, m.GetState()) -} - -func handleDecrement(conn net.Conn, req models.Request, m *Manager) { - device, err := params.String(req.Params, "device") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - step := params.IntOpt(req.Params, "step", 10) - exponential := params.BoolOpt(req.Params, "exponential", false) - exponent := params.FloatOpt(req.Params, "exponent", 1.2) - - if err := m.IncrementBrightnessWithExponent(device, -step, exponential, exponent); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, m.GetState()) -} - -func handleRescan(conn net.Conn, req models.Request, m *Manager) { - m.Rescan() - models.Respond(conn, req.ID, m.GetState()) -} - -func handleSubscribe(conn net.Conn, req models.Request, m *Manager) { - clientID := fmt.Sprintf("brightness-%d", req.ID) - - ch := m.Subscribe(clientID) - defer m.Unsubscribe(clientID) - - initialState := m.GetState() - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - ID: req.ID, - Result: &initialState, - }); err != nil { - return - } - - for state := range ch { - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - ID: req.ID, - Result: &state, - }); err != nil { - return - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind.go deleted file mode 100644 index 8449ab5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind.go +++ /dev/null @@ -1,66 +0,0 @@ -package brightness - -import ( - "fmt" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/godbus/dbus/v5" -) - -type DBusConn interface { - Object(dest string, path dbus.ObjectPath) dbus.BusObject - Close() error -} - -type LogindBackend struct { - conn DBusConn -} - -func NewLogindBackend() (*LogindBackend, error) { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return nil, fmt.Errorf("connect to system bus: %w", err) - } - - obj := conn.Object("org.freedesktop.login1", "/org/freedesktop/login1/session/auto") - call := obj.Call("org.freedesktop.DBus.Peer.Ping", 0) - if call.Err != nil { - conn.Close() - return nil, fmt.Errorf("logind not available: %w", call.Err) - } - - conn.Close() - - return &LogindBackend{}, nil -} - -func NewLogindBackendWithConn(conn DBusConn) *LogindBackend { - return &LogindBackend{ - conn: conn, - } -} - -func (b *LogindBackend) SetBrightness(subsystem, name string, brightness uint32) error { - if b.conn == nil { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return fmt.Errorf("connect to system bus: %w", err) - } - b.conn = conn - } - - obj := b.conn.Object("org.freedesktop.login1", "/org/freedesktop/login1/session/auto") - call := obj.Call("org.freedesktop.login1.Session.SetBrightness", 0, subsystem, name, brightness) - if call.Err != nil { - return fmt.Errorf("dbus call failed: %w", call.Err) - } - - log.Debugf("logind: set %s/%s to %d", subsystem, name, brightness) - return nil -} - -func (b *LogindBackend) Close() { - if b.conn != nil { - b.conn.Close() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind_test.go deleted file mode 100644 index 3d4c2ee..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package brightness - -import ( - "errors" - "testing" - - mocks_brightness "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/brightness" - mock_dbus "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/godbus/dbus/v5" - "github.com/godbus/dbus/v5" - "github.com/stretchr/testify/mock" -) - -func TestLogindBackend_SetBrightness_Success(t *testing.T) { - mockConn := mocks_brightness.NewMockDBusConn(t) - mockObj := mock_dbus.NewMockBusObject(t) - - backend := NewLogindBackendWithConn(mockConn) - - mockConn.EXPECT(). - Object("org.freedesktop.login1", dbus.ObjectPath("/org/freedesktop/login1/session/auto")). - Return(mockObj). - Once() - - mockObj.EXPECT(). - Call("org.freedesktop.login1.Session.SetBrightness", dbus.Flags(0), "backlight", "nvidia_0", uint32(75)). - Return(&dbus.Call{Err: nil}). - Once() - - err := backend.SetBrightness("backlight", "nvidia_0", 75) - if err != nil { - t.Errorf("SetBrightness() error = %v, want nil", err) - } -} - -func TestLogindBackend_SetBrightness_DBusError(t *testing.T) { - mockConn := mocks_brightness.NewMockDBusConn(t) - mockObj := mock_dbus.NewMockBusObject(t) - - backend := NewLogindBackendWithConn(mockConn) - - mockConn.EXPECT(). - Object("org.freedesktop.login1", dbus.ObjectPath("/org/freedesktop/login1/session/auto")). - Return(mockObj). - Once() - - dbusErr := errors.New("permission denied") - mockObj.EXPECT(). - Call("org.freedesktop.login1.Session.SetBrightness", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(&dbus.Call{Err: dbusErr}). - Once() - - err := backend.SetBrightness("backlight", "test_device", 50) - if err == nil { - t.Error("SetBrightness() error = nil, want error") - } -} - -func TestLogindBackend_SetBrightness_LEDDevice(t *testing.T) { - mockConn := mocks_brightness.NewMockDBusConn(t) - mockObj := mock_dbus.NewMockBusObject(t) - - backend := NewLogindBackendWithConn(mockConn) - - mockConn.EXPECT(). - Object("org.freedesktop.login1", dbus.ObjectPath("/org/freedesktop/login1/session/auto")). - Return(mockObj). - Once() - - mockObj.EXPECT(). - Call("org.freedesktop.login1.Session.SetBrightness", dbus.Flags(0), "leds", "test_led", uint32(128)). - Return(&dbus.Call{Err: nil}). - Once() - - err := backend.SetBrightness("leds", "test_led", 128) - if err != nil { - t.Errorf("SetBrightness() error = %v, want nil", err) - } -} - -func TestLogindBackend_Close(t *testing.T) { - mockConn := mocks_brightness.NewMockDBusConn(t) - backend := NewLogindBackendWithConn(mockConn) - - mockConn.EXPECT(). - Close(). - Return(nil). - Once() - - backend.Close() -} - -func TestLogindBackend_Close_NilConn(t *testing.T) { - backend := &LogindBackend{conn: nil} - backend.Close() -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager.go deleted file mode 100644 index 4cf7dd4..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager.go +++ /dev/null @@ -1,389 +0,0 @@ -package brightness - -import ( - "fmt" - "sort" - "strings" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" -) - -func NewManager() (*Manager, error) { - return NewManagerWithOptions(false) -} - -func NewManagerWithOptions(exponential bool) (*Manager, error) { - m := &Manager{ - stopChan: make(chan struct{}), - exponential: exponential, - } - - go m.initLogind() - go m.initSysfs() - go m.initDDC() - - return m, nil -} - -func (m *Manager) initLogind() { - log.Debug("Initializing logind backend...") - logind, err := NewLogindBackend() - if err != nil { - log.Infof("Logind backend not available: %v", err) - log.Info("Will use direct sysfs access for brightness control") - return - } - - m.logindBackend = logind - m.logindReady = true - log.Info("Logind backend initialized - will use for brightness control") -} - -func (m *Manager) initSysfs() { - log.Debug("Initializing sysfs backend...") - sysfs, err := NewSysfsBackend() - if err != nil { - log.Warnf("Failed to initialize sysfs backend: %v", err) - return - } - - devices, err := sysfs.GetDevices() - if err != nil { - log.Warnf("Failed to get initial sysfs devices: %v", err) - m.sysfsBackend = sysfs - m.sysfsReady = true - m.updateState() - m.initUdev() - return - } - - log.Infof("Sysfs backend initialized with %d devices", len(devices)) - for _, d := range devices { - log.Debugf(" - %s: %s (%d%%)", d.ID, d.Name, d.CurrentPercent) - } - - m.sysfsBackend = sysfs - m.sysfsReady = true - m.updateState() - m.initUdev() -} - -func (m *Manager) initUdev() { - m.udevMonitor = NewUdevMonitor(m) -} - -func (m *Manager) initDDC() { - ddc, err := NewDDCBackend() - if err != nil { - log.Debugf("Failed to initialize DDC backend: %v", err) - return - } - - m.ddcBackend = ddc - m.ddcReady = true - log.Info("DDC backend initialized") - - m.updateState() -} - -func (m *Manager) Rescan() { - log.Debug("Rescanning brightness devices...") - - if m.ddcReady && m.ddcBackend != nil { - if err := m.ddcBackend.ForceRescan(); err != nil { - log.Debugf("DDC force rescan failed: %v", err) - } - } - - if m.sysfsReady && m.sysfsBackend != nil { - if err := m.sysfsBackend.Rescan(); err != nil { - log.Debugf("Sysfs rescan failed: %v", err) - } - } - - m.updateState() -} - -func sortDevices(devices []Device) { - sort.Slice(devices, func(i, j int) bool { - classOrder := map[DeviceClass]int{ - ClassBacklight: 0, - ClassDDC: 1, - ClassLED: 2, - } - - orderI := classOrder[devices[i].Class] - orderJ := classOrder[devices[j].Class] - - if orderI != orderJ { - return orderI < orderJ - } - - return devices[i].Name < devices[j].Name - }) -} - -func stateChanged(old, new State) bool { - if len(old.Devices) != len(new.Devices) { - return true - } - - oldMap := make(map[string]Device) - for _, d := range old.Devices { - oldMap[d.ID] = d - } - - for _, newDev := range new.Devices { - oldDev, exists := oldMap[newDev.ID] - if !exists { - return true - } - if oldDev.Current != newDev.Current || oldDev.Max != newDev.Max { - return true - } - } - - return false -} - -func (m *Manager) updateState() { - allDevices := make([]Device, 0) - - if m.sysfsReady && m.sysfsBackend != nil { - devices, err := m.sysfsBackend.GetDevices() - if err != nil { - log.Debugf("Failed to get sysfs devices: %v", err) - } - if err == nil { - allDevices = append(allDevices, devices...) - } - } - - if m.ddcReady && m.ddcBackend != nil { - devices, err := m.ddcBackend.GetDevices() - if err != nil { - log.Debugf("Failed to get DDC devices: %v", err) - } - if err == nil { - allDevices = append(allDevices, devices...) - } - } - - sortDevices(allDevices) - - m.stateMutex.Lock() - oldState := m.state - newState := State{Devices: allDevices} - - if !stateChanged(oldState, newState) { - m.stateMutex.Unlock() - return - } - - m.state = newState - m.stateMutex.Unlock() - log.Debugf("State changed, notifying subscribers") - m.NotifySubscribers() -} - -func (m *Manager) SetBrightness(deviceID string, percent int) error { - return m.SetBrightnessWithMode(deviceID, percent, m.exponential) -} - -func (m *Manager) SetBrightnessWithMode(deviceID string, percent int, exponential bool) error { - return m.SetBrightnessWithExponent(deviceID, percent, exponential, 1.2) -} - -func (m *Manager) SetBrightnessWithExponent(deviceID string, percent int, exponential bool, exponent float64) error { - if percent < 0 { - return fmt.Errorf("percent out of range: %d", percent) - } - - log.Debugf("SetBrightness: %s to %d%%", deviceID, percent) - - m.stateMutex.Lock() - currentState := m.state - var found bool - var deviceClass DeviceClass - var deviceIndex int - - log.Debugf("Current state has %d devices", len(currentState.Devices)) - - for i, dev := range currentState.Devices { - if dev.ID == deviceID { - found = true - deviceClass = dev.Class - deviceIndex = i - break - } - } - - if !found { - m.stateMutex.Unlock() - log.Debugf("Device not found in state: %s", deviceID) - return fmt.Errorf("device not found: %s", deviceID) - } - - newDevices := make([]Device, len(currentState.Devices)) - copy(newDevices, currentState.Devices) - newDevices[deviceIndex].CurrentPercent = percent - m.state = State{Devices: newDevices} - m.stateMutex.Unlock() - - var err error - if deviceClass == ClassDDC { - log.Debugf("Calling DDC backend for %s", deviceID) - err = m.ddcBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, func() { - m.updateState() - m.debouncedBroadcast(deviceID) - }) - } else if m.logindReady && m.logindBackend != nil { - log.Debugf("Calling logind backend for %s", deviceID) - err = m.setViaSysfsWithLogindWithExponent(deviceID, percent, exponential, exponent) - } else { - log.Debugf("Calling sysfs backend for %s", deviceID) - err = m.sysfsBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent) - } - - if err != nil { - m.updateState() - return fmt.Errorf("failed to set brightness: %w", err) - } - - if deviceClass != ClassDDC { - log.Debugf("Queueing broadcast for %s", deviceID) - m.debouncedBroadcast(deviceID) - } - return nil -} - -func (m *Manager) IncrementBrightness(deviceID string, step int) error { - return m.IncrementBrightnessWithMode(deviceID, step, m.exponential) -} - -func (m *Manager) IncrementBrightnessWithMode(deviceID string, step int, exponential bool) error { - return m.IncrementBrightnessWithExponent(deviceID, step, exponential, 1.2) -} - -func (m *Manager) IncrementBrightnessWithExponent(deviceID string, step int, exponential bool, exponent float64) error { - m.stateMutex.RLock() - currentState := m.state - m.stateMutex.RUnlock() - - var currentPercent int - var found bool - - for _, dev := range currentState.Devices { - if dev.ID == deviceID { - currentPercent = dev.CurrentPercent - found = true - break - } - } - - if !found { - return fmt.Errorf("device not found: %s", deviceID) - } - - newPercent := currentPercent + step - if newPercent > 100 { - newPercent = 100 - } - if newPercent < 0 { - newPercent = 0 - } - - return m.SetBrightnessWithExponent(deviceID, newPercent, exponential, exponent) -} - -func (m *Manager) DecrementBrightness(deviceID string, step int) error { - return m.IncrementBrightness(deviceID, -step) -} - -func (m *Manager) setViaSysfsWithLogindWithExponent(deviceID string, percent int, exponential bool, exponent float64) error { - parts := strings.SplitN(deviceID, ":", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid device id: %s", deviceID) - } - - subsystem := parts[0] - name := parts[1] - - dev, err := m.sysfsBackend.GetDevice(deviceID) - if err != nil { - return err - } - - value := m.sysfsBackend.PercentToValueWithExponent(percent, dev, exponential, exponent) - - if m.logindBackend == nil { - return m.sysfsBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent) - } - - err = m.logindBackend.SetBrightness(subsystem, name, uint32(value)) - if err != nil { - log.Debugf("logind SetBrightness failed, falling back to direct sysfs: %v", err) - return m.sysfsBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent) - } - - log.Debugf("set %s to %d%% (%d/%d) via logind", deviceID, percent, value, dev.maxBrightness) - return nil -} - -func (m *Manager) debouncedBroadcast(deviceID string) { - m.broadcastMutex.Lock() - defer m.broadcastMutex.Unlock() - - m.broadcastPending = true - m.pendingDeviceID = deviceID - - if m.broadcastTimer == nil { - m.broadcastTimer = time.AfterFunc(150*time.Millisecond, func() { - m.broadcastMutex.Lock() - pending := m.broadcastPending - deviceID := m.pendingDeviceID - m.broadcastPending = false - m.pendingDeviceID = "" - m.broadcastMutex.Unlock() - - if !pending || deviceID == "" { - return - } - - m.broadcastDeviceUpdate(deviceID) - }) - } else { - m.broadcastTimer.Reset(150 * time.Millisecond) - } -} - -func (m *Manager) broadcastDeviceUpdate(deviceID string) { - m.stateMutex.RLock() - var targetDevice *Device - for _, dev := range m.state.Devices { - if dev.ID == deviceID { - devCopy := dev - targetDevice = &devCopy - break - } - } - m.stateMutex.RUnlock() - - if targetDevice == nil { - log.Debugf("Device not found for broadcast: %s", deviceID) - return - } - - update := DeviceUpdate{Device: *targetDevice} - - log.Debugf("Broadcasting device update: %s at %d%%", deviceID, targetDevice.CurrentPercent) - - m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool { - select { - case ch <- update: - default: - } - return true - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager_test.go deleted file mode 100644 index a8ae0f8..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package brightness - -import ( - "testing" -) - -// Manager tests can be added here as needed -func TestManager_Placeholder(t *testing.T) { - // Placeholder test to keep the test file valid - t.Skip("No tests implemented yet") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs.go deleted file mode 100644 index 1ff38f9..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs.go +++ /dev/null @@ -1,267 +0,0 @@ -package brightness - -import ( - "fmt" - "math" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" -) - -func NewSysfsBackend() (*SysfsBackend, error) { - b := &SysfsBackend{ - basePath: "/sys/class", - classes: []string{"backlight", "leds"}, - } - - if err := b.scanDevices(); err != nil { - return nil, err - } - - return b, nil -} - -func (b *SysfsBackend) scanDevices() error { - for _, class := range b.classes { - classPath := filepath.Join(b.basePath, class) - entries, err := os.ReadDir(classPath) - if err != nil { - if os.IsNotExist(err) { - continue - } - return fmt.Errorf("read %s: %w", classPath, err) - } - - for _, entry := range entries { - devicePath := filepath.Join(classPath, entry.Name()) - - stat, err := os.Stat(devicePath) - if err != nil || !stat.IsDir() { - continue - } - maxPath := filepath.Join(devicePath, "max_brightness") - - maxData, err := os.ReadFile(maxPath) - if err != nil { - log.Debugf("skip %s/%s: no max_brightness", class, entry.Name()) - continue - } - - maxBrightness, err := strconv.Atoi(strings.TrimSpace(string(maxData))) - if err != nil || maxBrightness <= 0 { - log.Debugf("skip %s/%s: invalid max_brightness", class, entry.Name()) - continue - } - - deviceClass := ClassBacklight - minValue := 1 - if class == "leds" { - deviceClass = ClassLED - minValue = 0 - } - - deviceID := fmt.Sprintf("%s:%s", class, entry.Name()) - b.deviceCache.Store(deviceID, &sysfsDevice{ - class: deviceClass, - id: deviceID, - name: entry.Name(), - maxBrightness: maxBrightness, - minValue: minValue, - }) - - log.Debugf("found %s device: %s (max=%d)", class, entry.Name(), maxBrightness) - } - } - - return nil -} - -func shouldSuppressDevice(name string) bool { - if strings.HasSuffix(name, "::lan") { - return true - } - - keyboardLEDs := []string{ - "::scrolllock", - "::capslock", - "::numlock", - "::kana", - "::compose", - } - - for _, suffix := range keyboardLEDs { - if strings.HasSuffix(name, suffix) { - return true - } - } - - return false -} - -func (b *SysfsBackend) Rescan() error { - return b.scanDevices() -} - -func (b *SysfsBackend) GetDevices() ([]Device, error) { - devices := make([]Device, 0) - - b.deviceCache.Range(func(key string, dev *sysfsDevice) bool { - if shouldSuppressDevice(dev.name) { - return true - } - - parts := strings.SplitN(dev.id, ":", 2) - if len(parts) != 2 { - return true - } - - class := parts[0] - name := parts[1] - - devicePath := filepath.Join(b.basePath, class, name) - brightnessPath := filepath.Join(devicePath, "brightness") - - brightnessData, err := os.ReadFile(brightnessPath) - if err != nil { - log.Debugf("failed to read brightness for %s: %v", dev.id, err) - return true - } - - current, err := strconv.Atoi(strings.TrimSpace(string(brightnessData))) - if err != nil { - log.Debugf("failed to parse brightness for %s: %v", dev.id, err) - return true - } - - percent := b.ValueToPercent(current, dev, false) - - devices = append(devices, Device{ - Class: dev.class, - ID: dev.id, - Name: dev.name, - Current: current, - Max: dev.maxBrightness, - CurrentPercent: percent, - Backend: "sysfs", - }) - return true - }) - - return devices, nil -} - -func (b *SysfsBackend) GetDevice(id string) (*sysfsDevice, error) { - dev, ok := b.deviceCache.Load(id) - if !ok { - return nil, fmt.Errorf("device not found: %s", id) - } - - return dev, nil -} - -func (b *SysfsBackend) SetBrightness(id string, percent int, exponential bool) error { - return b.SetBrightnessWithExponent(id, percent, exponential, 1.2) -} - -func (b *SysfsBackend) SetBrightnessWithExponent(id string, percent int, exponential bool, exponent float64) error { - dev, err := b.GetDevice(id) - if err != nil { - return err - } - - if percent < 0 { - return fmt.Errorf("percent out of range: %d", percent) - } - - value := b.PercentToValueWithExponent(percent, dev, exponential, exponent) - - parts := strings.SplitN(id, ":", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid device id: %s", id) - } - - class := parts[0] - name := parts[1] - - devicePath := filepath.Join(b.basePath, class, name) - brightnessPath := filepath.Join(devicePath, "brightness") - - data := []byte(fmt.Sprintf("%d", value)) - if err := os.WriteFile(brightnessPath, data, 0o644); err != nil { - return fmt.Errorf("write brightness: %w", err) - } - - log.Debugf("set %s to %d%% (%d/%d) via direct sysfs", id, percent, value, dev.maxBrightness) - - return nil -} - -func (b *SysfsBackend) PercentToValue(percent int, dev *sysfsDevice, exponential bool) int { - return b.PercentToValueWithExponent(percent, dev, exponential, 1.2) -} - -func (b *SysfsBackend) PercentToValueWithExponent(percent int, dev *sysfsDevice, exponential bool, exponent float64) int { - if percent == 0 { - return dev.minValue - } - - usableRange := dev.maxBrightness - dev.minValue - var value int - - if exponential { - normalizedPercent := float64(percent-1) / 99.0 - hardwarePercent := math.Pow(normalizedPercent, exponent) - value = dev.minValue + int(math.Round(hardwarePercent*float64(usableRange))) - } else { - value = dev.minValue + ((percent - 1) * usableRange / 99) - } - - if value < dev.minValue { - value = dev.minValue - } - if value > dev.maxBrightness { - value = dev.maxBrightness - } - - return value -} - -func (b *SysfsBackend) ValueToPercent(value int, dev *sysfsDevice, exponential bool) int { - return b.ValueToPercentWithExponent(value, dev, exponential, 1.2) -} - -func (b *SysfsBackend) ValueToPercentWithExponent(value int, dev *sysfsDevice, exponential bool, exponent float64) int { - if value <= dev.minValue { - if dev.minValue == 0 && value == 0 { - return 0 - } - return 1 - } - - usableRange := dev.maxBrightness - dev.minValue - if usableRange == 0 { - return 100 - } - - var percent int - - if exponential { - hardwarePercent := float64(value-dev.minValue) / float64(usableRange) - normalizedPercent := math.Pow(hardwarePercent, 1.0/exponent) - percent = 1 + int(math.Round(normalizedPercent*99.0)) - } else { - percent = 1 + int(math.Round(float64(value-dev.minValue)*99.0/float64(usableRange))) - } - - if percent > 100 { - percent = 100 - } - if percent < 1 { - percent = 1 - } - - return percent -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_logind_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_logind_test.go deleted file mode 100644 index 6ba84b8..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_logind_test.go +++ /dev/null @@ -1,278 +0,0 @@ -package brightness - -import ( - "os" - "path/filepath" - "testing" - - mocks_brightness "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/brightness" - mock_dbus "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/godbus/dbus/v5" - "github.com/godbus/dbus/v5" - "github.com/stretchr/testify/mock" -) - -func TestManager_SetBrightness_LogindSuccess(t *testing.T) { - tmpDir := t.TempDir() - - backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight") - if err := os.MkdirAll(backlightDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil { - t.Fatal(err) - } - - mockConn := mocks_brightness.NewMockDBusConn(t) - mockObj := mock_dbus.NewMockBusObject(t) - - mockLogind := NewLogindBackendWithConn(mockConn) - - sysfs := &SysfsBackend{ - basePath: tmpDir, - classes: []string{"backlight"}, - } - - if err := sysfs.scanDevices(); err != nil { - t.Fatal(err) - } - - m := &Manager{ - logindBackend: mockLogind, - sysfsBackend: sysfs, - logindReady: true, - sysfsReady: true, - stopChan: make(chan struct{}), - } - - m.state = State{ - Devices: []Device{ - { - Class: ClassBacklight, - ID: "backlight:test_backlight", - Name: "test_backlight", - Current: 50, - Max: 100, - CurrentPercent: 50, - Backend: "sysfs", - }, - }, - } - - mockConn.EXPECT(). - Object("org.freedesktop.login1", dbus.ObjectPath("/org/freedesktop/login1/session/auto")). - Return(mockObj). - Once() - - mockObj.EXPECT(). - Call("org.freedesktop.login1.Session.SetBrightness", mock.Anything, "backlight", "test_backlight", uint32(75)). - Return(&dbus.Call{Err: nil}). - Once() - - err := m.SetBrightness("backlight:test_backlight", 75) - if err != nil { - t.Errorf("SetBrightness() with logind error = %v, want nil", err) - } - - data, _ := os.ReadFile(filepath.Join(backlightDir, "brightness")) - if string(data) == "75\n" { - t.Error("Direct sysfs write occurred when logind should have been used") - } -} - -func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) { - tmpDir := t.TempDir() - - backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight") - if err := os.MkdirAll(backlightDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil { - t.Fatal(err) - } - - mockConn := mocks_brightness.NewMockDBusConn(t) - mockObj := mock_dbus.NewMockBusObject(t) - - mockLogind := NewLogindBackendWithConn(mockConn) - - sysfs := &SysfsBackend{ - basePath: tmpDir, - classes: []string{"backlight"}, - } - - if err := sysfs.scanDevices(); err != nil { - t.Fatal(err) - } - - m := &Manager{ - logindBackend: mockLogind, - sysfsBackend: sysfs, - logindReady: true, - sysfsReady: true, - stopChan: make(chan struct{}), - } - - m.state = State{ - Devices: []Device{ - { - Class: ClassBacklight, - ID: "backlight:test_backlight", - Name: "test_backlight", - Current: 50, - Max: 100, - CurrentPercent: 50, - Backend: "sysfs", - }, - }, - } - - mockConn.EXPECT(). - Object("org.freedesktop.login1", dbus.ObjectPath("/org/freedesktop/login1/session/auto")). - Return(mockObj). - Once() - - mockObj.EXPECT(). - Call("org.freedesktop.login1.Session.SetBrightness", mock.Anything, "backlight", "test_backlight", mock.Anything). - Return(&dbus.Call{Err: dbus.ErrMsgNoObject}). - Once() - - err := m.SetBrightness("backlight:test_backlight", 75) - if err != nil { - t.Errorf("SetBrightness() with fallback error = %v, want nil", err) - } - - data, _ := os.ReadFile(filepath.Join(backlightDir, "brightness")) - brightness := string(data) - if brightness != "75" { - t.Errorf("Fallback sysfs write did not occur, got brightness = %q, want %q", brightness, "75") - } -} - -func TestManager_SetBrightness_NoLogind(t *testing.T) { - tmpDir := t.TempDir() - - backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight") - if err := os.MkdirAll(backlightDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil { - t.Fatal(err) - } - - sysfs := &SysfsBackend{ - basePath: tmpDir, - classes: []string{"backlight"}, - } - - if err := sysfs.scanDevices(); err != nil { - t.Fatal(err) - } - - m := &Manager{ - logindBackend: nil, - sysfsBackend: sysfs, - logindReady: false, - sysfsReady: true, - stopChan: make(chan struct{}), - } - - m.state = State{ - Devices: []Device{ - { - Class: ClassBacklight, - ID: "backlight:test_backlight", - Name: "test_backlight", - Current: 50, - Max: 100, - CurrentPercent: 50, - Backend: "sysfs", - }, - }, - } - - err := m.SetBrightness("backlight:test_backlight", 75) - if err != nil { - t.Errorf("SetBrightness() without logind error = %v, want nil", err) - } - - data, _ := os.ReadFile(filepath.Join(backlightDir, "brightness")) - brightness := string(data) - if brightness != "75" { - t.Errorf("Direct sysfs write = %q, want %q", brightness, "75") - } -} - -func TestManager_SetBrightness_LEDWithLogind(t *testing.T) { - tmpDir := t.TempDir() - - ledsDir := filepath.Join(tmpDir, "leds", "test_led") - if err := os.MkdirAll(ledsDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0o644); err != nil { - t.Fatal(err) - } - - mockConn := mocks_brightness.NewMockDBusConn(t) - mockObj := mock_dbus.NewMockBusObject(t) - - mockLogind := NewLogindBackendWithConn(mockConn) - - sysfs := &SysfsBackend{ - basePath: tmpDir, - classes: []string{"leds"}, - } - - if err := sysfs.scanDevices(); err != nil { - t.Fatal(err) - } - - m := &Manager{ - logindBackend: mockLogind, - sysfsBackend: sysfs, - logindReady: true, - sysfsReady: true, - stopChan: make(chan struct{}), - } - - m.state = State{ - Devices: []Device{ - { - Class: ClassLED, - ID: "leds:test_led", - Name: "test_led", - Current: 128, - Max: 255, - CurrentPercent: 50, - Backend: "sysfs", - }, - }, - } - - mockConn.EXPECT(). - Object("org.freedesktop.login1", dbus.ObjectPath("/org/freedesktop/login1/session/auto")). - Return(mockObj). - Once() - - mockObj.EXPECT(). - Call("org.freedesktop.login1.Session.SetBrightness", mock.Anything, "leds", "test_led", uint32(0)). - Return(&dbus.Call{Err: nil}). - Once() - - err := m.SetBrightness("leds:test_led", 0) - if err != nil { - t.Errorf("SetBrightness() LED with logind error = %v, want nil", err) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_test.go deleted file mode 100644 index 8061513..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_test.go +++ /dev/null @@ -1,180 +0,0 @@ -package brightness - -import ( - "os" - "path/filepath" - "testing" -) - -func TestSysfsBackend_PercentConversions(t *testing.T) { - tests := []struct { - name string - device *sysfsDevice - percent int - wantValue int - tolerance int - }{ - { - name: "backlight 0% should be minValue=1", - device: &sysfsDevice{maxBrightness: 100, minValue: 1, class: ClassBacklight}, - percent: 0, - wantValue: 1, - tolerance: 0, - }, - { - name: "backlight 1% should be minValue=1", - device: &sysfsDevice{maxBrightness: 100, minValue: 1, class: ClassBacklight}, - percent: 1, - wantValue: 1, - tolerance: 0, - }, - { - name: "backlight 50% should be ~50", - device: &sysfsDevice{maxBrightness: 100, minValue: 1, class: ClassBacklight}, - percent: 50, - wantValue: 50, - tolerance: 1, - }, - { - name: "backlight 100% should be max", - device: &sysfsDevice{maxBrightness: 100, minValue: 1, class: ClassBacklight}, - percent: 100, - wantValue: 100, - tolerance: 0, - }, - { - name: "led 0% should be 0", - device: &sysfsDevice{maxBrightness: 255, minValue: 0, class: ClassLED}, - percent: 0, - wantValue: 0, - tolerance: 0, - }, - { - name: "led 1% should be ~2-3", - device: &sysfsDevice{maxBrightness: 255, minValue: 0, class: ClassLED}, - percent: 1, - wantValue: 2, - tolerance: 3, - }, - { - name: "led 50% should be ~127", - device: &sysfsDevice{maxBrightness: 255, minValue: 0, class: ClassLED}, - percent: 50, - wantValue: 127, - tolerance: 2, - }, - { - name: "led 100% should be max", - device: &sysfsDevice{maxBrightness: 255, minValue: 0, class: ClassLED}, - percent: 100, - wantValue: 255, - tolerance: 0, - }, - } - - b := &SysfsBackend{} - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := b.PercentToValue(tt.percent, tt.device, false) - diff := got - tt.wantValue - if diff < 0 { - diff = -diff - } - if diff > tt.tolerance { - t.Errorf("percentToValue() = %v, want %v (±%d)", got, tt.wantValue, tt.tolerance) - } - - gotPercent := b.ValueToPercent(got, tt.device, false) - if tt.percent > 1 && gotPercent == 0 { - t.Errorf("valueToPercent() returned 0 for non-zero input (percent=%d, got value=%d)", tt.percent, got) - } - }) - } -} - -func TestSysfsBackend_ValueToPercent(t *testing.T) { - tests := []struct { - name string - device *sysfsDevice - value int - wantPercent int - }{ - { - name: "backlight min value", - device: &sysfsDevice{maxBrightness: 100, minValue: 1, class: ClassBacklight}, - value: 1, - wantPercent: 1, - }, - { - name: "backlight max value", - device: &sysfsDevice{maxBrightness: 100, minValue: 1, class: ClassBacklight}, - value: 100, - wantPercent: 100, - }, - { - name: "led zero", - device: &sysfsDevice{maxBrightness: 255, minValue: 0, class: ClassLED}, - value: 0, - wantPercent: 0, - }, - } - - b := &SysfsBackend{} - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := b.ValueToPercent(tt.value, tt.device, false) - if got != tt.wantPercent { - t.Errorf("valueToPercent() = %v, want %v", got, tt.wantPercent) - } - }) - } -} - -func TestSysfsBackend_ScanDevices(t *testing.T) { - tmpDir := t.TempDir() - - backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight") - if err := os.MkdirAll(backlightDir, 0o755); err != nil { - t.Fatal(err) - } - - if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil { - t.Fatal(err) - } - - ledsDir := filepath.Join(tmpDir, "leds", "test_led") - if err := os.MkdirAll(ledsDir, 0o755); err != nil { - t.Fatal(err) - } - - if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0o644); err != nil { - t.Fatal(err) - } - - b := &SysfsBackend{ - basePath: tmpDir, - classes: []string{"backlight", "leds"}, - } - - if err := b.scanDevices(); err != nil { - t.Fatalf("scanDevices() error = %v", err) - } - - backlightID := "backlight:test_backlight" - if _, ok := b.deviceCache.Load(backlightID); !ok { - t.Errorf("backlight device not found") - } - - ledID := "leds:test_led" - if _, ok := b.deviceCache.Load(ledID); !ok { - t.Errorf("LED device not found") - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/types.go deleted file mode 100644 index 81e00cf..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/types.go +++ /dev/null @@ -1,184 +0,0 @@ -package brightness - -import ( - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" -) - -type DeviceClass string - -const ( - ClassBacklight DeviceClass = "backlight" - ClassLED DeviceClass = "leds" - ClassDDC DeviceClass = "ddc" -) - -type Device struct { - Class DeviceClass `json:"class"` - ID string `json:"id"` - Name string `json:"name"` - Current int `json:"current"` - Max int `json:"max"` - CurrentPercent int `json:"currentPercent"` - Backend string `json:"backend"` -} - -type State struct { - Devices []Device `json:"devices"` -} - -type DeviceUpdate struct { - Device Device `json:"device"` -} - -type Manager struct { - logindBackend *LogindBackend - sysfsBackend *SysfsBackend - ddcBackend *DDCBackend - udevMonitor *UdevMonitor - - logindReady bool - sysfsReady bool - ddcReady bool - - exponential bool - - stateMutex sync.RWMutex - state State - - subscribers syncmap.Map[string, chan State] - updateSubscribers syncmap.Map[string, chan DeviceUpdate] - - broadcastMutex sync.Mutex - broadcastTimer *time.Timer - broadcastPending bool - pendingDeviceID string - - stopChan chan struct{} -} - -type SysfsBackend struct { - basePath string - classes []string - - deviceCache syncmap.Map[string, *sysfsDevice] -} - -type sysfsDevice struct { - class DeviceClass - id string - name string - maxBrightness int - minValue int -} - -type DDCBackend struct { - devices syncmap.Map[string, *ddcDevice] - - scanMutex sync.Mutex - lastScan time.Time - scanInterval time.Duration - - debounceMutex sync.Mutex - debounceTimers map[string]*time.Timer - debouncePending map[string]ddcPendingSet - debounceWg sync.WaitGroup -} - -type ddcPendingSet struct { - percent int - callback func() -} - -type ddcDevice struct { - bus int - addr int - id string - name string - max int - lastBrightness int -} - -type ddcCapability struct { - vcp byte - max int - current int -} - -func (m *Manager) Subscribe(id string) chan State { - ch := make(chan State, 16) - - m.subscribers.Store(id, ch) - - return ch -} - -func (m *Manager) Unsubscribe(id string) { - - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - - } - -} - -func (m *Manager) SubscribeUpdates(id string) chan DeviceUpdate { - ch := make(chan DeviceUpdate, 16) - m.updateSubscribers.Store(id, ch) - return ch -} - -func (m *Manager) UnsubscribeUpdates(id string) { - if val, ok := m.updateSubscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) NotifySubscribers() { - m.stateMutex.RLock() - state := m.state - m.stateMutex.RUnlock() - - m.subscribers.Range(func(key string, ch chan State) bool { - select { - case ch <- state: - default: - } - return true - }) -} - -func (m *Manager) GetState() State { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - return m.state -} - -func (m *Manager) Close() { - close(m.stopChan) - - m.subscribers.Range(func(key string, ch chan State) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool { - close(ch) - m.updateSubscribers.Delete(key) - return true - }) - - if m.udevMonitor != nil { - m.udevMonitor.Close() - } - - if m.logindBackend != nil { - m.logindBackend.Close() - } - - if m.ddcBackend != nil { - m.ddcBackend.Close() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev.go deleted file mode 100644 index b7f02a8..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev.go +++ /dev/null @@ -1,237 +0,0 @@ -package brightness - -import ( - "os" - "path/filepath" - "strconv" - "strings" - "sync" - "syscall" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/pilebones/go-udev/netlink" -) - -const ( - udevRecvBufSize = 8 * 1024 * 1024 - udevMaxRetries = 5 - udevBaseDelay = 2 * time.Second - udevMaxDelay = 60 * time.Second -) - -type UdevMonitor struct { - stop chan struct{} - rescanMutex sync.Mutex - rescanTimer *time.Timer - rescanPending bool -} - -func NewUdevMonitor(manager *Manager) *UdevMonitor { - m := &UdevMonitor{ - stop: make(chan struct{}), - } - - go m.run(manager) - return m -} - -func (m *UdevMonitor) run(manager *Manager) { - matcher := &netlink.RuleDefinitions{ - Rules: []netlink.RuleDefinition{ - {Env: map[string]string{"SUBSYSTEM": "backlight"}}, - {Env: map[string]string{"SUBSYSTEM": "drm"}}, - {Env: map[string]string{"SUBSYSTEM": "i2c"}}, - }, - } - if err := matcher.Compile(); err != nil { - log.Errorf("Failed to compile udev matcher: %v", err) - return - } - - failures := 0 - for { - if err := m.monitorLoop(manager, matcher); err != nil { - log.Errorf("Udev monitor error: %v", err) - } - - select { - case <-m.stop: - return - default: - } - - failures++ - if failures > udevMaxRetries { - log.Errorf("Udev monitor exceeded %d retries, giving up", udevMaxRetries) - return - } - - delay := min(udevBaseDelay*time.Duration(1<<(failures-1)), udevMaxDelay) - log.Infof("Udev monitor reconnecting in %v (attempt %d/%d)", delay, failures, udevMaxRetries) - - select { - case <-m.stop: - return - case <-time.After(delay): - } - } -} - -func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinitions) error { - conn := &netlink.UEventConn{} - if err := conn.Connect(netlink.UdevEvent); err != nil { - return err - } - defer conn.Close() - - if err := syscall.SetsockoptInt(conn.Fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, udevRecvBufSize); err != nil { - log.Warnf("Failed to set udev socket receive buffer: %v", err) - } - - events := make(chan netlink.UEvent) - errs := make(chan error) - conn.Monitor(events, errs, matcher) - - log.Info("Udev monitor started for backlight/drm/i2c events") - - for { - select { - case <-m.stop: - return nil - case err := <-errs: - return err - case event := <-events: - m.handleEvent(manager, event) - } - } -} - -func (m *UdevMonitor) handleEvent(manager *Manager, event netlink.UEvent) { - subsystem := event.Env["SUBSYSTEM"] - devpath := event.Env["DEVPATH"] - - if subsystem == "" || devpath == "" { - return - } - - sysname := filepath.Base(devpath) - action := string(event.Action) - - switch subsystem { - case "drm", "i2c": - m.handleDisplayEvent(manager, action, subsystem, sysname) - case "backlight": - m.handleBacklightEvent(manager, action, sysname) - } -} - -func (m *UdevMonitor) handleDisplayEvent(manager *Manager, action, subsystem, sysname string) { - switch action { - case "add", "remove", "change": - log.Debugf("Udev %s event: %s:%s - queueing DDC rescan", action, subsystem, sysname) - m.debouncedRescan(manager) - } -} - -func (m *UdevMonitor) debouncedRescan(manager *Manager) { - m.rescanMutex.Lock() - defer m.rescanMutex.Unlock() - - m.rescanPending = true - - if m.rescanTimer != nil { - m.rescanTimer.Reset(2 * time.Second) - return - } - - m.rescanTimer = time.AfterFunc(2*time.Second, func() { - m.rescanMutex.Lock() - pending := m.rescanPending - m.rescanPending = false - m.rescanMutex.Unlock() - - if !pending { - return - } - - log.Debug("Executing debounced DDC rescan") - manager.Rescan() - }) -} - -func (m *UdevMonitor) handleBacklightEvent(manager *Manager, action, sysname string) { - switch action { - case "change": - m.handleChange(manager, "backlight", sysname) - case "add", "remove": - log.Debugf("Udev %s event: backlight:%s - triggering rescan", action, sysname) - manager.Rescan() - } -} - -func (m *UdevMonitor) handleChange(manager *Manager, subsystem, sysname string) { - deviceID := subsystem + ":" + sysname - - if manager.sysfsBackend == nil { - return - } - - brightnessPath := filepath.Join(manager.sysfsBackend.basePath, subsystem, sysname, "brightness") - data, err := os.ReadFile(brightnessPath) - if err != nil { - log.Debugf("Udev change event for %s but failed to read brightness: %v", deviceID, err) - return - } - - brightness, err := strconv.Atoi(strings.TrimSpace(string(data))) - if err != nil { - log.Debugf("Failed to parse brightness for %s: %v", deviceID, err) - return - } - - manager.handleUdevBrightnessChange(deviceID, brightness) -} - -func (m *UdevMonitor) Close() { - close(m.stop) -} - -func (m *Manager) handleUdevBrightnessChange(deviceID string, rawBrightness int) { - if m.sysfsBackend == nil { - return - } - - dev, err := m.sysfsBackend.GetDevice(deviceID) - if err != nil { - log.Debugf("Udev event for unknown device %s: %v", deviceID, err) - return - } - - percent := m.sysfsBackend.ValueToPercent(rawBrightness, dev, false) - - m.stateMutex.Lock() - var found bool - for i, d := range m.state.Devices { - if d.ID != deviceID { - continue - } - found = true - if d.Current == rawBrightness { - m.stateMutex.Unlock() - return - } - m.state.Devices[i].Current = rawBrightness - m.state.Devices[i].CurrentPercent = percent - break - } - m.stateMutex.Unlock() - - if !found { - log.Debugf("Udev event for device not in state: %s", deviceID) - return - } - - log.Debugf("Udev brightness change: %s -> %d (%d%%)", deviceID, rawBrightness, percent) - m.broadcastDeviceUpdate(deviceID) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev_test.go deleted file mode 100644 index a5c406b..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev_test.go +++ /dev/null @@ -1,260 +0,0 @@ -package brightness - -import ( - "os" - "path/filepath" - "testing" - "time" - - "github.com/pilebones/go-udev/netlink" -) - -func setupTestManager(t *testing.T) (*Manager, string) { - tmpDir := t.TempDir() - - backlightDir := filepath.Join(tmpDir, "backlight", "intel_backlight") - if err := os.MkdirAll(backlightDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("1000\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("500\n"), 0o644); err != nil { - t.Fatal(err) - } - - sysfs := &SysfsBackend{ - basePath: tmpDir, - classes: []string{"backlight"}, - } - if err := sysfs.scanDevices(); err != nil { - t.Fatal(err) - } - - m := &Manager{ - sysfsBackend: sysfs, - sysfsReady: true, - stopChan: make(chan struct{}), - } - - m.state = State{ - Devices: []Device{ - { - Class: ClassBacklight, - ID: "backlight:intel_backlight", - Name: "intel_backlight", - Current: 500, - Max: 1000, - CurrentPercent: 50, - Backend: "sysfs", - }, - }, - } - - return m, tmpDir -} - -func TestHandleUdevBrightnessChange_UpdatesState(t *testing.T) { - m, _ := setupTestManager(t) - - m.handleUdevBrightnessChange("backlight:intel_backlight", 750) - - state := m.GetState() - if len(state.Devices) != 1 { - t.Fatalf("expected 1 device, got %d", len(state.Devices)) - } - - dev := state.Devices[0] - if dev.Current != 750 { - t.Errorf("expected Current=750, got %d", dev.Current) - } - if dev.CurrentPercent != 75 { - t.Errorf("expected CurrentPercent=75, got %d", dev.CurrentPercent) - } -} - -func TestHandleUdevBrightnessChange_NoChangeWhenSameValue(t *testing.T) { - m, _ := setupTestManager(t) - - updateCh := m.SubscribeUpdates("test") - defer m.UnsubscribeUpdates("test") - - m.handleUdevBrightnessChange("backlight:intel_backlight", 500) - - select { - case <-updateCh: - t.Error("should not broadcast when brightness unchanged") - case <-time.After(50 * time.Millisecond): - } -} - -func TestHandleUdevBrightnessChange_BroadcastsOnChange(t *testing.T) { - m, _ := setupTestManager(t) - - updateCh := m.SubscribeUpdates("test") - defer m.UnsubscribeUpdates("test") - - m.handleUdevBrightnessChange("backlight:intel_backlight", 750) - - select { - case update := <-updateCh: - if update.Device.Current != 750 { - t.Errorf("broadcast had wrong Current: got %d, want 750", update.Device.Current) - } - case <-time.After(100 * time.Millisecond): - t.Error("expected broadcast on brightness change") - } -} - -func TestHandleUdevBrightnessChange_UnknownDevice(t *testing.T) { - m, _ := setupTestManager(t) - - m.handleUdevBrightnessChange("backlight:unknown_device", 500) - - state := m.GetState() - if len(state.Devices) != 1 { - t.Errorf("state should be unchanged, got %d devices", len(state.Devices)) - } -} - -func TestHandleUdevBrightnessChange_NilSysfsBackend(t *testing.T) { - m := &Manager{ - sysfsBackend: nil, - stopChan: make(chan struct{}), - } - - m.handleUdevBrightnessChange("backlight:test", 500) -} - -func TestHandleUdevBrightnessChange_DeviceNotInState(t *testing.T) { - m, _ := setupTestManager(t) - - m.sysfsBackend.deviceCache.Store("backlight:other_device", &sysfsDevice{ - class: ClassBacklight, - id: "backlight:other_device", - name: "other_device", - maxBrightness: 100, - minValue: 1, - }) - - m.handleUdevBrightnessChange("backlight:other_device", 50) - - state := m.GetState() - for _, d := range state.Devices { - if d.ID == "backlight:other_device" { - t.Error("device should not be added to state via udev change event") - } - } -} - -func TestHandleEvent_ChangeAction(t *testing.T) { - m, tmpDir := setupTestManager(t) - um := &UdevMonitor{stop: make(chan struct{})} - - brightnessPath := filepath.Join(tmpDir, "backlight", "intel_backlight", "brightness") - if err := os.WriteFile(brightnessPath, []byte("800\n"), 0o644); err != nil { - t.Fatal(err) - } - - event := netlink.UEvent{ - Action: netlink.CHANGE, - Env: map[string]string{ - "SUBSYSTEM": "backlight", - "DEVPATH": "/devices/pci0000:00/0000:00:02.0/drm/card0/card0-eDP-1/intel_backlight", - }, - } - - um.handleEvent(m, event) - - state := m.GetState() - if state.Devices[0].Current != 800 { - t.Errorf("expected Current=800 after change event, got %d", state.Devices[0].Current) - } -} - -func TestHandleEvent_MissingEnvVars(t *testing.T) { - m, _ := setupTestManager(t) - um := &UdevMonitor{stop: make(chan struct{})} - - event := netlink.UEvent{ - Action: netlink.CHANGE, - Env: map[string]string{}, - } - - um.handleEvent(m, event) - - state := m.GetState() - if state.Devices[0].Current != 500 { - t.Error("state should be unchanged with missing env vars") - } -} - -func TestHandleEvent_MissingSubsystem(t *testing.T) { - m, _ := setupTestManager(t) - um := &UdevMonitor{stop: make(chan struct{})} - - event := netlink.UEvent{ - Action: netlink.CHANGE, - Env: map[string]string{ - "DEVPATH": "/devices/foo/bar", - }, - } - - um.handleEvent(m, event) - - state := m.GetState() - if state.Devices[0].Current != 500 { - t.Error("state should be unchanged with missing SUBSYSTEM") - } -} - -func TestHandleChange_BrightnessFileNotFound(t *testing.T) { - m, _ := setupTestManager(t) - um := &UdevMonitor{stop: make(chan struct{})} - - um.handleChange(m, "backlight", "nonexistent_device") - - state := m.GetState() - if state.Devices[0].Current != 500 { - t.Error("state should be unchanged when brightness file not found") - } -} - -func TestHandleChange_InvalidBrightnessValue(t *testing.T) { - m, tmpDir := setupTestManager(t) - um := &UdevMonitor{stop: make(chan struct{})} - - brightnessPath := filepath.Join(tmpDir, "backlight", "intel_backlight", "brightness") - if err := os.WriteFile(brightnessPath, []byte("not_a_number\n"), 0o644); err != nil { - t.Fatal(err) - } - - um.handleChange(m, "backlight", "intel_backlight") - - state := m.GetState() - if state.Devices[0].Current != 500 { - t.Error("state should be unchanged with invalid brightness value") - } -} - -func TestUdevMonitor_Close(t *testing.T) { - um := &UdevMonitor{stop: make(chan struct{})} - - um.Close() - - select { - case <-um.stop: - default: - t.Error("stop channel should be closed") - } -} - -func TestHandleChange_NilSysfsBackend(t *testing.T) { - m := &Manager{ - sysfsBackend: nil, - stopChan: make(chan struct{}), - } - um := &UdevMonitor{stop: make(chan struct{})} - - um.handleChange(m, "backlight", "test_device") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/browser/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/browser/handlers.go deleted file mode 100644 index 0b4159a..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/browser/handlers.go +++ /dev/null @@ -1,22 +0,0 @@ -package browser - -import ( - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - switch req.Method { - case "browser.open": - url, ok := models.Get[string](req, "url") - if !ok { - models.RespondError(conn, req.ID, "invalid url parameter") - return - } - manager.RequestOpen(url) - models.Respond(conn, req.ID, "ok") - default: - models.RespondError(conn, req.ID, "unknown method") - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/browser/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/browser/manager.go deleted file mode 100644 index 5306759..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/browser/manager.go +++ /dev/null @@ -1,49 +0,0 @@ -package browser - -import ( - "sync" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" -) - -type Manager struct { - subscribers syncmap.Map[string, chan OpenEvent] - closeOnce sync.Once -} - -func NewManager() *Manager { - return &Manager{} -} - -func (m *Manager) Subscribe(id string) chan OpenEvent { - ch := make(chan OpenEvent, 16) - m.subscribers.Store(id, ch) - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) RequestOpen(url string) { - event := OpenEvent{URL: url} - m.subscribers.Range(func(key string, ch chan OpenEvent) bool { - select { - case ch <- event: - default: - } - return true - }) -} - -func (m *Manager) Close() { - m.closeOnce.Do(func() { - m.subscribers.Range(func(key string, ch chan OpenEvent) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/browser/models.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/browser/models.go deleted file mode 100644 index fc63519..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/browser/models.go +++ /dev/null @@ -1,5 +0,0 @@ -package browser - -type OpenEvent struct { - URL string `json:"url"` -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/handlers.go deleted file mode 100644 index 1ccc710..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/handlers.go +++ /dev/null @@ -1,325 +0,0 @@ -package clipboard - -import ( - "encoding/json" - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -func HandleRequest(conn net.Conn, req models.Request, m *Manager) { - switch req.Method { - case "clipboard.getState": - handleGetState(conn, req, m) - case "clipboard.getHistory": - handleGetHistory(conn, req, m) - case "clipboard.getEntry": - handleGetEntry(conn, req, m) - case "clipboard.deleteEntry": - handleDeleteEntry(conn, req, m) - case "clipboard.clearHistory": - handleClearHistory(conn, req, m) - case "clipboard.copy": - handleCopy(conn, req, m) - case "clipboard.copyEntry": - handleCopyEntry(conn, req, m) - case "clipboard.paste": - handlePaste(conn, req, m) - case "clipboard.subscribe": - handleSubscribe(conn, req, m) - case "clipboard.search": - handleSearch(conn, req, m) - case "clipboard.getConfig": - handleGetConfig(conn, req, m) - case "clipboard.setConfig": - handleSetConfig(conn, req, m) - case "clipboard.store": - handleStore(conn, req, m) - case "clipboard.pinEntry": - handlePinEntry(conn, req, m) - case "clipboard.unpinEntry": - handleUnpinEntry(conn, req, m) - case "clipboard.getPinnedEntries": - handleGetPinnedEntries(conn, req, m) - case "clipboard.getPinnedCount": - handleGetPinnedCount(conn, req, m) - case "clipboard.copyFile": - handleCopyFile(conn, req, m) - default: - models.RespondError(conn, req.ID, "unknown method: "+req.Method) - } -} - -func handleGetState(conn net.Conn, req models.Request, m *Manager) { - models.Respond(conn, req.ID, m.GetState()) -} - -func handleGetHistory(conn net.Conn, req models.Request, m *Manager) { - history := m.GetHistory() - for i := range history { - history[i].Data = nil - } - models.Respond(conn, req.ID, history) -} - -func handleGetEntry(conn net.Conn, req models.Request, m *Manager) { - id, err := params.Int(req.Params, "id") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - entry, err := m.GetEntry(uint64(id)) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, entry) -} - -func handleDeleteEntry(conn net.Conn, req models.Request, m *Manager) { - id, err := params.Int(req.Params, "id") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := m.DeleteEntry(uint64(id)); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry deleted"}) -} - -func handleClearHistory(conn net.Conn, req models.Request, m *Manager) { - m.ClearHistory() - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "history cleared"}) -} - -func handleCopy(conn net.Conn, req models.Request, m *Manager) { - text, err := params.String(req.Params, "text") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := m.CopyText(text); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"}) -} - -func handleCopyEntry(conn net.Conn, req models.Request, m *Manager) { - id, err := params.Int(req.Params, "id") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - entry, err := m.GetEntry(uint64(id)) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - filePath := m.EntryToFile(entry) - if filePath != "" { - if err := m.CopyFile(filePath); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, map[string]any{ - "success": true, - "filePath": filePath, - }) - return - } - - if err := m.SetClipboard(entry.Data, entry.MimeType); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if entry.Pinned { - if err := m.CreateHistoryEntryFromPinned(entry); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - } else { - if err := m.TouchEntry(uint64(id)); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"}) -} - -func handlePaste(conn net.Conn, req models.Request, m *Manager) { - text, err := m.PasteText() - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, map[string]string{"text": text}) -} - -func handleSubscribe(conn net.Conn, req models.Request, m *Manager) { - clientID := fmt.Sprintf("clipboard-%d", req.ID) - - ch := m.Subscribe(clientID) - defer m.Unsubscribe(clientID) - - initialState := m.GetState() - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - ID: req.ID, - Result: &initialState, - }); err != nil { - return - } - - for state := range ch { - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - ID: req.ID, - Result: &state, - }); err != nil { - return - } - } -} - -func handleSearch(conn net.Conn, req models.Request, m *Manager) { - p := SearchParams{ - Query: params.StringOpt(req.Params, "query", ""), - MimeType: params.StringOpt(req.Params, "mimeType", ""), - Limit: params.IntOpt(req.Params, "limit", 50), - Offset: params.IntOpt(req.Params, "offset", 0), - } - - if img, ok := models.Get[bool](req, "isImage"); ok { - p.IsImage = &img - } - if b, ok := models.Get[float64](req, "before"); ok { - v := int64(b) - p.Before = &v - } - if a, ok := models.Get[float64](req, "after"); ok { - v := int64(a) - p.After = &v - } - - models.Respond(conn, req.ID, m.Search(p)) -} - -func handleGetConfig(conn net.Conn, req models.Request, m *Manager) { - models.Respond(conn, req.ID, m.GetConfig()) -} - -func handleSetConfig(conn net.Conn, req models.Request, m *Manager) { - cfg := m.GetConfig() - - if v, ok := models.Get[float64](req, "maxHistory"); ok { - cfg.MaxHistory = int(v) - } - if v, ok := models.Get[float64](req, "maxEntrySize"); ok { - cfg.MaxEntrySize = int64(v) - } - if v, ok := models.Get[float64](req, "autoClearDays"); ok { - cfg.AutoClearDays = int(v) - } - if v, ok := models.Get[bool](req, "clearAtStartup"); ok { - cfg.ClearAtStartup = v - } - if v, ok := models.Get[bool](req, "disabled"); ok { - cfg.Disabled = v - } - if v, ok := models.Get[float64](req, "maxPinned"); ok { - cfg.MaxPinned = int(v) - } - - if err := m.SetConfig(cfg); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "config updated"}) -} - -func handleStore(conn net.Conn, req models.Request, m *Manager) { - data, err := params.String(req.Params, "data") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - mimeType := params.StringOpt(req.Params, "mimeType", "text/plain;charset=utf-8") - - if err := m.StoreData([]byte(data), mimeType); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "stored"}) -} - -func handlePinEntry(conn net.Conn, req models.Request, m *Manager) { - id, err := params.Int(req.Params, "id") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := m.PinEntry(uint64(id)); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry pinned"}) -} - -func handleUnpinEntry(conn net.Conn, req models.Request, m *Manager) { - id, err := params.Int(req.Params, "id") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := m.UnpinEntry(uint64(id)); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry unpinned"}) -} - -func handleGetPinnedEntries(conn net.Conn, req models.Request, m *Manager) { - pinned := m.GetPinnedEntries() - models.Respond(conn, req.ID, pinned) -} - -func handleGetPinnedCount(conn net.Conn, req models.Request, m *Manager) { - count := m.GetPinnedCount() - models.Respond(conn, req.ID, map[string]int{"count": count}) -} - -func handleCopyFile(conn net.Conn, req models.Request, m *Manager) { - filePath, err := params.String(req.Params, "filePath") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := m.CopyFile(filePath); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied"}) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager.go deleted file mode 100644 index f5f528b..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager.go +++ /dev/null @@ -1,1911 +0,0 @@ -package clipboard - -import ( - "bytes" - "encoding/binary" - "fmt" - "image" - _ "image/gif" - _ "image/jpeg" - _ "image/png" - "io" - "os" - "os/exec" - "path/filepath" - "slices" - "strings" - "syscall" - "time" - - "hash/fnv" - - "github.com/fsnotify/fsnotify" - "github.com/godbus/dbus/v5" - _ "golang.org/x/image/bmp" - _ "golang.org/x/image/tiff" - _ "golang.org/x/image/webp" - - bolt "go.etcd.io/bbolt" - - clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext" - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" -) - -// These mime types won't be stored in history -var sensitiveMimeTypes = []string{ - "x-kde-passwordManagerHint", -} - -func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error) { - display := wlCtx.Display() - dbPath, err := clipboardstore.GetDBPath() - if err != nil { - return nil, fmt.Errorf("failed to get db path: %w", err) - } - - configPath, _ := getConfigPath() - - m := &Manager{ - config: config, - configPath: configPath, - display: display, - wlCtx: wlCtx, - stopChan: make(chan struct{}), - subscribers: make(map[string]chan State), - dirty: make(chan struct{}, 1), - offerMimeTypes: make(map[any][]string), - offerRegistry: make(map[uint32]any), - dbPath: dbPath, - } - - if !config.Disabled { - if err := m.setupRegistry(); err != nil { - return nil, err - } - } - - m.notifierWg.Add(1) - go m.notifier() - - go m.watchConfig() - - db, err := openDB(dbPath) - if err != nil { - return nil, fmt.Errorf("failed to open db: %w", err) - } - m.db = db - - if err := m.migrateHashes(); err != nil { - log.Errorf("Failed to migrate hashes: %v", err) - } - - if !config.Disabled { - if config.ClearAtStartup { - if err := m.clearHistoryInternal(); err != nil { - log.Errorf("Failed to clear history at startup: %v", err) - } - } - - if config.AutoClearDays > 0 { - if err := m.clearOldEntries(config.AutoClearDays); err != nil { - log.Errorf("Failed to clear old entries: %v", err) - } - } - } - - m.alive = true - m.updateState() - - if !config.Disabled && m.dataControlMgr != nil && m.seat != nil { - m.setupDataDeviceSync() - } - - return m, nil -} - -func openDB(path string) (*bolt.DB, error) { - db, err := bolt.Open(path, 0o644, &bolt.Options{ - Timeout: 1 * time.Second, - }) - if err != nil { - return nil, err - } - - err = db.Update(func(tx *bolt.Tx) error { - _, err := tx.CreateBucketIfNotExists([]byte("clipboard")) - return err - }) - if err != nil { - db.Close() - return nil, err - } - - return db, nil -} - -func (m *Manager) post(fn func()) { - m.wlCtx.Post(fn) -} - -func (m *Manager) setupRegistry() error { - ctx := m.display.Context() - - registry, err := m.display.GetRegistry() - if err != nil { - return fmt.Errorf("failed to get registry: %w", err) - } - m.registry = registry - - registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { - switch e.Interface { - case "ext_data_control_manager_v1": - if e.Version < 1 { - return - } - dataControlMgr := ext_data_control.NewExtDataControlManagerV1(ctx) - if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil { - log.Errorf("Failed to bind ext_data_control_manager_v1: %v", err) - return - } - m.dataControlMgr = dataControlMgr - log.Info("Bound ext_data_control_manager_v1") - case "wl_seat": - seat := wlclient.NewSeat(ctx) - if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil { - log.Errorf("Failed to bind wl_seat: %v", err) - return - } - m.seat = seat - m.seatName = e.Name - log.Info("Bound wl_seat") - } - }) - - m.display.Roundtrip() - m.display.Roundtrip() - - if m.dataControlMgr == nil { - return fmt.Errorf("compositor does not support ext_data_control_manager_v1") - } - - if m.seat == nil { - return fmt.Errorf("no seat available") - } - - return nil -} - -func (m *Manager) setupDataDeviceSync() { - if m.dataControlMgr == nil || m.seat == nil { - return - } - - ctx := m.display.Context() - dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1) - - dataDevice := ext_data_control.NewExtDataControlDeviceV1(ctx) - - dataDevice.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) { - if e.Id == nil { - return - } - - m.offerMutex.Lock() - m.offerRegistry[e.Id.ID()] = e.Id - m.offerMimeTypes[e.Id] = make([]string, 0) - m.offerMutex.Unlock() - - e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) { - m.offerMutex.Lock() - m.offerMimeTypes[e.Id] = append(m.offerMimeTypes[e.Id], me.MimeType) - m.offerMutex.Unlock() - }) - }) - - dataDevice.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) { - if !m.initialized { - m.initialized = true - return - } - - var offer any - if e.Id != nil { - offer = e.Id - } else if e.OfferId != 0 { - m.offerMutex.RLock() - offer = m.offerRegistry[e.OfferId] - m.offerMutex.RUnlock() - } - - m.ownerLock.Lock() - wasOwner := m.isOwner - m.ownerLock.Unlock() - - if offer == nil { - return - } - - if wasOwner { - return - } - - prevOffer := m.currentOffer - m.currentOffer = offer - - if prevOffer != nil && prevOffer != offer { - m.offerMutex.Lock() - delete(m.offerMimeTypes, prevOffer) - m.offerMutex.Unlock() - } - - m.offerMutex.RLock() - mimes := m.offerMimeTypes[offer] - m.offerMutex.RUnlock() - - m.mimeTypes = mimes - - if len(mimes) == 0 { - return - } - - if m.hasSensitiveMimeType(mimes) { - return - } - - preferredMime := m.selectMimeType(mimes) - if preferredMime == "" { - return - } - - typedOffer := offer.(*ext_data_control.ExtDataControlOfferV1) - - r, w, err := os.Pipe() - if err != nil { - return - } - - if err := typedOffer.Receive(preferredMime, int(w.Fd())); err != nil { - r.Close() - w.Close() - return - } - w.Close() - - go m.readAndStore(r, preferredMime) - }) - - if err := dataMgr.GetDataDeviceWithProxy(dataDevice, m.seat); err != nil { - log.Errorf("Failed to send get_data_device request: %v", err) - return - } - - m.dataDevice = dataDevice - - if err := ctx.Dispatch(); err != nil { - log.Errorf("Failed to dispatch initial events: %v", err) - return - } - - log.Info("Data device setup complete") -} - -func (m *Manager) readAndStore(r *os.File, mimeType string) { - defer r.Close() - - cfg := m.getConfig() - - done := make(chan []byte, 1) - go func() { - data, _ := io.ReadAll(r) - done <- data - }() - - var data []byte - select { - case data = <-done: - case <-time.After(500 * time.Millisecond): - return - } - - if len(data) == 0 || int64(len(data)) > cfg.MaxEntrySize { - return - } - if len(bytes.TrimSpace(data)) == 0 { - return - } - - if !cfg.Disabled && m.db != nil { - m.storeClipboardEntry(data, mimeType) - } - - m.updateState() - m.notifySubscribers() -} - -func (m *Manager) storeClipboardEntry(data []byte, mimeType string) { - if mimeType == "text/uri-list" { - if imgData, imgMime, ok := m.tryReadImageFromURI(data); ok { - data = imgData - mimeType = imgMime - } - } - - entry := Entry{ - Data: data, - MimeType: mimeType, - Size: len(data), - Timestamp: time.Now(), - IsImage: m.isImageMimeType(mimeType), - } - - switch { - case entry.IsImage: - entry.Preview = m.imagePreview(data, mimeType) - case mimeType == "text/uri-list": - entry.Preview, entry.IsImage = m.uriListPreview(data) - default: - entry.Preview = m.textPreview(data) - } - - if err := m.storeEntry(entry); err != nil { - log.Errorf("Failed to store clipboard entry: %v", err) - } -} - -func (m *Manager) storeEntry(entry Entry) error { - if m.db == nil { - return fmt.Errorf("database not available") - } - - entry.Hash = computeHash(entry.Data) - - return m.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - - if err := m.deduplicateInTx(b, entry.Hash); err != nil { - return err - } - - id, err := b.NextSequence() - if err != nil { - return err - } - - entry.ID = id - - encoded, err := encodeEntry(entry) - if err != nil { - return err - } - - if err := b.Put(itob(id), encoded); err != nil { - return err - } - - return m.trimLengthInTx(b) - }) -} - -func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error { - c := b.Cursor() - for k, v := c.Last(); k != nil; k, v = c.Prev() { - if extractHash(v) != hash { - continue - } - entry, err := decodeEntry(v) - if err == nil && entry.Pinned { - continue - } - if err := b.Delete(k); err != nil { - return err - } - } - return nil -} - -func (m *Manager) trimLengthInTx(b *bolt.Bucket) error { - if m.config.MaxHistory < 0 { - return nil - } - c := b.Cursor() - var count int - for k, v := c.Last(); k != nil; k, v = c.Prev() { - entry, err := decodeEntry(v) - if err == nil && entry.Pinned { - continue - } - if count < m.config.MaxHistory { - count++ - continue - } - if err := b.Delete(k); err != nil { - return err - } - } - return nil -} - -func encodeEntry(e Entry) ([]byte, error) { - buf := new(bytes.Buffer) - - binary.Write(buf, binary.BigEndian, e.ID) - binary.Write(buf, binary.BigEndian, uint32(len(e.Data))) - buf.Write(e.Data) - binary.Write(buf, binary.BigEndian, uint32(len(e.MimeType))) - buf.WriteString(e.MimeType) - binary.Write(buf, binary.BigEndian, uint32(len(e.Preview))) - buf.WriteString(e.Preview) - binary.Write(buf, binary.BigEndian, int32(e.Size)) - binary.Write(buf, binary.BigEndian, e.Timestamp.Unix()) - if e.IsImage { - buf.WriteByte(1) - } else { - buf.WriteByte(0) - } - binary.Write(buf, binary.BigEndian, e.Hash) - if e.Pinned { - buf.WriteByte(1) - } else { - buf.WriteByte(0) - } - - return buf.Bytes(), nil -} - -func decodeEntry(data []byte) (Entry, error) { - buf := bytes.NewReader(data) - var e Entry - - binary.Read(buf, binary.BigEndian, &e.ID) - - var dataLen uint32 - binary.Read(buf, binary.BigEndian, &dataLen) - e.Data = make([]byte, dataLen) - buf.Read(e.Data) - - var mimeLen uint32 - binary.Read(buf, binary.BigEndian, &mimeLen) - mimeBytes := make([]byte, mimeLen) - buf.Read(mimeBytes) - e.MimeType = string(mimeBytes) - - var prevLen uint32 - binary.Read(buf, binary.BigEndian, &prevLen) - prevBytes := make([]byte, prevLen) - buf.Read(prevBytes) - e.Preview = string(prevBytes) - - var size int32 - binary.Read(buf, binary.BigEndian, &size) - e.Size = int(size) - - var timestamp int64 - binary.Read(buf, binary.BigEndian, ×tamp) - e.Timestamp = time.Unix(timestamp, 0) - - var isImage byte - binary.Read(buf, binary.BigEndian, &isImage) - e.IsImage = isImage == 1 - - if buf.Len() >= 8 { - binary.Read(buf, binary.BigEndian, &e.Hash) - } - - if buf.Len() >= 1 { - var pinnedByte byte - binary.Read(buf, binary.BigEndian, &pinnedByte) - e.Pinned = pinnedByte == 1 - } - - return e, nil -} - -func itob(v uint64) []byte { - b := make([]byte, 8) - binary.BigEndian.PutUint64(b, v) - return b -} - -func computeHash(data []byte) uint64 { - h := fnv.New64a() - h.Write(data) - return h.Sum64() -} - -func extractHash(data []byte) uint64 { - if len(data) < 9 { - return 0 - } - return binary.BigEndian.Uint64(data[len(data)-9 : len(data)-1]) -} - -func (m *Manager) hasSensitiveMimeType(mimes []string) bool { - return slices.ContainsFunc(mimes, func(mime string) bool { - return slices.Contains(sensitiveMimeTypes, mime) - }) -} - -func (m *Manager) selectMimeType(mimes []string) string { - preferredTypes := []string{ - "text/uri-list", - "text/plain;charset=utf-8", - "text/plain", - "UTF8_STRING", - "STRING", - "TEXT", - "image/png", - "image/jpeg", - "image/gif", - "image/bmp", - "image/tiff", - } - - for _, pref := range preferredTypes { - for _, mime := range mimes { - if mime == pref { - return mime - } - } - } - - // Skip useless MIME types when falling back - for _, mime := range mimes { - switch mime { - case "application/vnd.portal.filetransfer": - continue - default: - return mime - } - } - - return "" -} - -func (m *Manager) isImageMimeType(mime string) bool { - return strings.HasPrefix(mime, "image/") -} - -func (m *Manager) textPreview(data []byte) string { - text := string(data) - text = strings.TrimSpace(text) - text = strings.Join(strings.Fields(text), " ") - - if len(text) > 100 { - return text[:100] + "…" - } - return text -} - -func (m *Manager) imagePreview(data []byte, format string) string { - config, imgFmt, err := image.DecodeConfig(bytes.NewReader(data)) - if err != nil { - return fmt.Sprintf("[[ image %s %s ]]", sizeStr(len(data)), format) - } - return fmt.Sprintf("[[ image %s %s %dx%d ]]", sizeStr(len(data)), imgFmt, config.Width, config.Height) -} - -func (m *Manager) uriListPreview(data []byte) (string, bool) { - text := strings.TrimSpace(string(data)) - uris := strings.Split(text, "\r\n") - if len(uris) == 0 { - uris = strings.Split(text, "\n") - } - - if len(uris) > 1 { - return fmt.Sprintf("[[ %d files ]]", len(uris)), false - } - - if len(uris) == 1 && strings.HasPrefix(uris[0], "file://") { - filePath := strings.TrimPrefix(uris[0], "file://") - info, err := os.Stat(filePath) - if err != nil || info.IsDir() { - return m.textPreview(data), false - } - - cfg := m.getConfig() - if info.Size() <= cfg.MaxEntrySize { - if imgData, err := os.ReadFile(filePath); err == nil { - if config, imgFmt, err := image.DecodeConfig(bytes.NewReader(imgData)); err == nil { - return fmt.Sprintf("[[ file %s %s %dx%d ]]", filepath.Base(filePath), imgFmt, config.Width, config.Height), true - } - } - } - return fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)), false - } - - return m.textPreview(data), false -} - -func (m *Manager) tryReadImageFromURI(data []byte) ([]byte, string, bool) { - text := strings.TrimSpace(string(data)) - uris := strings.Split(text, "\r\n") - if len(uris) == 0 { - uris = strings.Split(text, "\n") - } - - if len(uris) != 1 || !strings.HasPrefix(uris[0], "file://") { - return nil, "", false - } - - filePath := strings.TrimPrefix(uris[0], "file://") - info, err := os.Stat(filePath) - if err != nil || info.IsDir() { - return nil, "", false - } - - cfg := m.getConfig() - if info.Size() > cfg.MaxEntrySize { - return nil, "", false - } - - imgData, err := os.ReadFile(filePath) - if err != nil { - return nil, "", false - } - - _, imgFmt, err := image.DecodeConfig(bytes.NewReader(imgData)) - if err != nil { - return nil, "", false - } - - return imgData, "image/" + imgFmt, true -} - -func sizeStr(size int) string { - units := []string{"B", "KiB", "MiB"} - var i int - fsize := float64(size) - for fsize >= 1024 && i < len(units)-1 { - fsize /= 1024 - i++ - } - return fmt.Sprintf("%.0f %s", fsize, units[i]) -} - -func (m *Manager) updateState() { - history := m.GetHistory() - - for i := range history { - history[i].Data = nil - } - - var current *Entry - if len(history) > 0 { - c := history[0] - c.Data = nil - current = &c - } - - newState := &State{ - Enabled: m.alive, - History: history, - Current: current, - } - - m.stateMutex.Lock() - m.state = newState - m.stateMutex.Unlock() -} - -func (m *Manager) notifier() { - defer m.notifierWg.Done() - - for range m.dirty { - state := m.GetState() - - if m.lastState != nil && stateEqual(m.lastState, &state) { - continue - } - - m.lastState = &state - - m.subMutex.RLock() - subs := make([]chan State, 0, len(m.subscribers)) - for _, ch := range m.subscribers { - subs = append(subs, ch) - } - m.subMutex.RUnlock() - - for _, ch := range subs { - select { - case ch <- state: - default: - } - } - } -} - -func stateEqual(a, b *State) bool { - if a == nil || b == nil { - return false - } - if a.Enabled != b.Enabled { - return false - } - if len(a.History) != len(b.History) { - return false - } - return true -} - -func (m *Manager) GetHistory() []Entry { - if m.db == nil { - return nil - } - - cfg := m.getConfig() - var cutoff time.Time - if cfg.AutoClearDays > 0 { - cutoff = time.Now().AddDate(0, 0, -cfg.AutoClearDays) - } - - var history []Entry - var stale []uint64 - - if err := m.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - c := b.Cursor() - - for k, v := c.Last(); k != nil; k, v = c.Prev() { - entry, err := decodeEntry(v) - if err != nil { - continue - } - if !cutoff.IsZero() && entry.Timestamp.Before(cutoff) { - stale = append(stale, entry.ID) - continue - } - history = append(history, entry) - } - return nil - }); err != nil { - log.Errorf("Failed to read clipboard history: %v", err) - } - - if len(stale) > 0 { - go m.deleteStaleEntries(stale) - } - - return history -} - -func (m *Manager) deleteStaleEntries(ids []uint64) { - if m.db == nil { - return - } - - if err := m.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - for _, id := range ids { - if err := b.Delete(itob(id)); err != nil { - log.Errorf("Failed to delete stale entry %d: %v", id, err) - } - } - return nil - }); err != nil { - log.Errorf("Failed to delete stale entries: %v", err) - } -} - -func (m *Manager) GetEntry(id uint64) (*Entry, error) { - if m.db == nil { - return nil, fmt.Errorf("database not available") - } - - var entry Entry - var found bool - - err := m.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - v := b.Get(itob(id)) - if v == nil { - return nil - } - - var err error - entry, err = decodeEntry(v) - if err != nil { - return err - } - found = true - return nil - }) - - if err != nil { - return nil, err - } - if !found { - return nil, fmt.Errorf("entry not found") - } - - return &entry, nil -} - -func (m *Manager) DeleteEntry(id uint64) error { - if m.db == nil { - return fmt.Errorf("database not available") - } - - err := m.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - return b.Delete(itob(id)) - }) - - if err == nil { - m.updateState() - m.notifySubscribers() - } - - return err -} - -func (m *Manager) TouchEntry(id uint64) error { - if m.db == nil { - return fmt.Errorf("database not available") - } - - entry, err := m.GetEntry(id) - if err != nil { - return err - } - - entry.Timestamp = time.Now() - - if err := m.storeEntry(*entry); err != nil { - return err - } - - m.updateState() - m.notifySubscribers() - - return nil -} - -func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error { - if m.db == nil { - return fmt.Errorf("database not available") - } - - // Create a new unpinned entry with the same data - newEntry := Entry{ - Data: pinnedEntry.Data, - MimeType: pinnedEntry.MimeType, - Size: pinnedEntry.Size, - Timestamp: time.Now(), - IsImage: pinnedEntry.IsImage, - Preview: pinnedEntry.Preview, - Pinned: false, - } - - if err := m.storeEntryWithoutDedup(newEntry); err != nil { - return err - } - - m.updateState() - m.notifySubscribers() - - return nil -} - -func (m *Manager) storeEntryWithoutDedup(entry Entry) error { - if m.db == nil { - return fmt.Errorf("database not available") - } - - entry.Hash = computeHash(entry.Data) - - return m.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - - id, err := b.NextSequence() - if err != nil { - return err - } - - entry.ID = id - - encoded, err := encodeEntry(entry) - if err != nil { - return err - } - - if err := b.Put(itob(id), encoded); err != nil { - return err - } - - return m.trimLengthInTx(b) - }) -} - -func (m *Manager) ClearHistory() { - if m.db == nil { - return - } - - // Delete only non-pinned entries - if err := m.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - if b == nil { - return nil - } - - var toDelete [][]byte - c := b.Cursor() - for k, v := c.First(); k != nil; k, v = c.Next() { - entry, err := decodeEntry(v) - if err != nil || !entry.Pinned { - toDelete = append(toDelete, k) - } - } - - for _, k := range toDelete { - if err := b.Delete(k); err != nil { - return err - } - } - return nil - }); err != nil { - log.Errorf("Failed to clear clipboard history: %v", err) - return - } - - pinnedCount := 0 - if err := m.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - if b != nil { - c := b.Cursor() - for k, v := c.First(); k != nil; k, v = c.Next() { - entry, _ := decodeEntry(v) - if entry.Pinned { - pinnedCount++ - } - } - } - return nil - }); err != nil { - log.Errorf("Failed to count pinned entries: %v", err) - } - - if pinnedCount == 0 { - if err := m.compactDB(); err != nil { - log.Errorf("Failed to compact database: %v", err) - } - } - - m.updateState() - m.notifySubscribers() -} - -func (m *Manager) compactDB() error { - m.db.Close() - - tmpPath := m.dbPath + ".compact" - defer os.Remove(tmpPath) - - srcDB, err := bolt.Open(m.dbPath, 0o644, &bolt.Options{ReadOnly: true, Timeout: time.Second}) - if err != nil { - m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second}) - return fmt.Errorf("open source: %w", err) - } - - dstDB, err := bolt.Open(tmpPath, 0o644, &bolt.Options{Timeout: time.Second}) - if err != nil { - srcDB.Close() - m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second}) - return fmt.Errorf("open destination: %w", err) - } - - if err := bolt.Compact(dstDB, srcDB, 0); err != nil { - srcDB.Close() - dstDB.Close() - m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second}) - return fmt.Errorf("compact: %w", err) - } - - srcDB.Close() - dstDB.Close() - - if err := os.Rename(tmpPath, m.dbPath); err != nil { - m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second}) - return fmt.Errorf("rename: %w", err) - } - - m.db, err = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second}) - if err != nil { - return fmt.Errorf("reopen: %w", err) - } - - return nil -} - -func (m *Manager) SetClipboard(data []byte, mimeType string) error { - if int64(len(data)) > m.config.MaxEntrySize { - return fmt.Errorf("data too large") - } - - dataCopy := make([]byte, len(data)) - copy(dataCopy, data) - - m.post(func() { - if m.dataControlMgr == nil || m.dataDevice == nil { - log.Error("Data control manager or device not initialized") - return - } - - dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1) - - source, err := dataMgr.CreateDataSource() - if err != nil { - log.Errorf("Failed to create data source: %v", err) - return - } - - if err := source.Offer(mimeType); err != nil { - log.Errorf("Failed to offer mime type: %v", err) - return - } - - source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { - fd := e.Fd - defer syscall.Close(fd) - - file := os.NewFile(uintptr(fd), "clipboard-pipe") - defer file.Close() - - if _, err := file.Write(dataCopy); err != nil { - log.Errorf("Failed to write clipboard data: %v", err) - } - }) - - source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) { - m.ownerLock.Lock() - m.isOwner = false - m.ownerLock.Unlock() - }) - - m.currentSource = source - m.sourceMutex.Lock() - m.sourceMimeTypes = []string{mimeType} - m.sourceMutex.Unlock() - - m.ownerLock.Lock() - m.isOwner = true - m.ownerLock.Unlock() - - device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1) - if err := device.SetSelection(source); err != nil { - log.Errorf("Failed to set selection: %v", err) - } - }) - - return nil -} - -func (m *Manager) CopyText(text string) error { - if err := m.SetClipboard([]byte(text), "text/plain;charset=utf-8"); err != nil { - return err - } - - entry := Entry{ - Data: []byte(text), - MimeType: "text/plain;charset=utf-8", - Size: len(text), - Timestamp: time.Now(), - IsImage: false, - Preview: m.textPreview([]byte(text)), - } - - if err := m.storeEntry(entry); err != nil { - log.Errorf("Failed to store clipboard entry: %v", err) - } - - m.updateState() - m.notifySubscribers() - - return nil -} - -func (m *Manager) PasteText() (string, error) { - history := m.GetHistory() - if len(history) == 0 { - return "", fmt.Errorf("no clipboard data available") - } - - entry := history[0] - if entry.IsImage { - return "", fmt.Errorf("clipboard contains image, not text") - } - - fullEntry, err := m.GetEntry(entry.ID) - if err != nil { - return "", err - } - - return string(fullEntry.Data), nil -} - -func (m *Manager) Close() { - if !m.alive { - return - } - - m.alive = false - close(m.stopChan) - - close(m.dirty) - m.notifierWg.Wait() - - m.subMutex.Lock() - for _, ch := range m.subscribers { - close(ch) - } - m.subscribers = make(map[string]chan State) - m.subMutex.Unlock() - - if m.currentSource != nil { - source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1) - source.Destroy() - } - - if m.dataDevice != nil { - device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1) - device.Destroy() - } - - if m.dataControlMgr != nil { - mgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1) - mgr.Destroy() - } - - if m.registry != nil { - m.registry.Destroy() - } - - if m.db != nil { - m.db.Close() - } -} - -func (m *Manager) clearHistoryInternal() error { - return m.db.Update(func(tx *bolt.Tx) error { - if err := tx.DeleteBucket([]byte("clipboard")); err != nil { - return err - } - _, err := tx.CreateBucket([]byte("clipboard")) - return err - }) -} - -func (m *Manager) clearOldEntries(days int) error { - cutoff := time.Now().AddDate(0, 0, -days) - - return m.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - if b == nil { - return nil - } - - var toDelete [][]byte - c := b.Cursor() - for k, v := c.First(); k != nil; k, v = c.Next() { - entry, err := decodeEntry(v) - if err != nil { - continue - } - // Skip pinned entries - if entry.Pinned { - continue - } - if entry.Timestamp.Before(cutoff) { - toDelete = append(toDelete, k) - } - } - - for _, k := range toDelete { - if err := b.Delete(k); err != nil { - return err - } - } - return nil - }) -} - -func (m *Manager) migrateHashes() error { - if m.db == nil { - return nil - } - - var needsMigration bool - if err := m.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - if b == nil { - return nil - } - c := b.Cursor() - for k, v := c.First(); k != nil; k, v = c.Next() { - if extractHash(v) == 0 { - needsMigration = true - return nil - } - } - return nil - }); err != nil { - return err - } - - if !needsMigration { - return nil - } - - log.Info("Migrating clipboard entries to add hashes...") - - return m.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - if b == nil { - return nil - } - - var updates []struct { - key []byte - entry Entry - } - - c := b.Cursor() - for k, v := c.First(); k != nil; k, v = c.Next() { - entry, err := decodeEntry(v) - if err != nil { - continue - } - if entry.Hash != 0 { - continue - } - entry.Hash = computeHash(entry.Data) - keyCopy := make([]byte, len(k)) - copy(keyCopy, k) - updates = append(updates, struct { - key []byte - entry Entry - }{keyCopy, entry}) - } - - for _, u := range updates { - encoded, err := encodeEntry(u.entry) - if err != nil { - continue - } - if err := b.Put(u.key, encoded); err != nil { - return err - } - } - - log.Infof("Migrated %d clipboard entries", len(updates)) - return nil - }) -} - -func (m *Manager) Search(params SearchParams) SearchResult { - if m.db == nil { - return SearchResult{} - } - - if params.Limit <= 0 { - params.Limit = 50 - } - if params.Limit > 500 { - params.Limit = 500 - } - - query := strings.ToLower(params.Query) - mimeFilter := strings.ToLower(params.MimeType) - - var all []Entry - if err := m.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - if b == nil { - return nil - } - - c := b.Cursor() - for k, v := c.Last(); k != nil; k, v = c.Prev() { - entry, err := decodeEntry(v) - if err != nil { - continue - } - - if params.IsImage != nil && entry.IsImage != *params.IsImage { - continue - } - - if mimeFilter != "" && !strings.Contains(strings.ToLower(entry.MimeType), mimeFilter) { - continue - } - - if params.Before != nil && entry.Timestamp.Unix() >= *params.Before { - continue - } - - if params.After != nil && entry.Timestamp.Unix() <= *params.After { - continue - } - - if query != "" && !strings.Contains(strings.ToLower(entry.Preview), query) { - continue - } - - entry.Data = nil - all = append(all, entry) - } - return nil - }); err != nil { - log.Errorf("Search failed: %v", err) - } - - total := len(all) - - start := params.Offset - if start > total { - start = total - } - end := start + params.Limit - if end > total { - end = total - } - - return SearchResult{ - Entries: all[start:end], - Total: total, - HasMore: end < total, - } -} - -func (m *Manager) GetConfig() Config { - return m.config -} - -func (m *Manager) SetConfig(cfg Config) error { - m.configMutex.Lock() - m.config = cfg - m.configMutex.Unlock() - - m.updateState() - m.notifySubscribers() - - return SaveConfig(cfg) -} - -func (m *Manager) getConfig() Config { - m.configMutex.RLock() - defer m.configMutex.RUnlock() - return m.config -} - -func (m *Manager) watchConfig() { - if m.configPath == "" { - return - } - - watcher, err := fsnotify.NewWatcher() - if err != nil { - log.Warnf("Failed to create config watcher: %v", err) - return - } - defer watcher.Close() - - configDir := filepath.Dir(m.configPath) - if err := watcher.Add(configDir); err != nil { - log.Warnf("Failed to watch config directory: %v", err) - return - } - - for { - select { - case <-m.stopChan: - return - case event, ok := <-watcher.Events: - if !ok { - return - } - if event.Name != m.configPath { - continue - } - if event.Op&(fsnotify.Write|fsnotify.Create) == 0 { - continue - } - newCfg := LoadConfig() - m.applyConfigChange(newCfg) - case err, ok := <-watcher.Errors: - if !ok { - return - } - log.Warnf("Config watcher error: %v", err) - } - } -} - -func (m *Manager) applyConfigChange(newCfg Config) { - m.configMutex.Lock() - oldCfg := m.config - m.config = newCfg - m.configMutex.Unlock() - - switch { - case newCfg.Disabled && !oldCfg.Disabled: - log.Info("Clipboard tracking disabled") - case !newCfg.Disabled && oldCfg.Disabled: - log.Info("Clipboard tracking enabled") - } - - m.updateState() - m.notifySubscribers() -} - -func (m *Manager) StoreData(data []byte, mimeType string) error { - cfg := m.getConfig() - - if cfg.Disabled { - return fmt.Errorf("clipboard tracking disabled") - } - - if m.db == nil { - return fmt.Errorf("database not available") - } - - if len(data) == 0 { - return nil - } - - if int64(len(data)) > cfg.MaxEntrySize { - return fmt.Errorf("data too large") - } - - if len(bytes.TrimSpace(data)) == 0 { - return nil - } - - entry := Entry{ - Data: data, - MimeType: mimeType, - Size: len(data), - Timestamp: time.Now(), - IsImage: m.isImageMimeType(mimeType), - } - - switch { - case entry.IsImage: - entry.Preview = m.imagePreview(data, mimeType) - case mimeType == "text/uri-list": - entry.Preview, entry.IsImage = m.uriListPreview(data) - default: - entry.Preview = m.textPreview(data) - } - - if err := m.storeEntry(entry); err != nil { - return err - } - - m.updateState() - m.notifySubscribers() - - return nil -} - -func (m *Manager) PinEntry(id uint64) error { - if m.db == nil { - return fmt.Errorf("database not available") - } - - entryToPin, err := m.GetEntry(id) - if err != nil { - return err - } - - var hashExists bool - if err := m.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - if b == nil { - return nil - } - c := b.Cursor() - for k, v := c.First(); k != nil; k, v = c.Next() { - entry, err := decodeEntry(v) - if err != nil || !entry.Pinned { - continue - } - if entry.Hash == entryToPin.Hash { - hashExists = true - return nil - } - } - return nil - }); err != nil { - return err - } - - if hashExists { - return nil - } - - // Check pinned count - cfg := m.getConfig() - pinnedCount := 0 - if err := m.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - if b == nil { - return nil - } - c := b.Cursor() - for k, v := c.First(); k != nil; k, v = c.Next() { - entry, err := decodeEntry(v) - if err == nil && entry.Pinned { - pinnedCount++ - } - } - return nil - }); err != nil { - log.Errorf("Failed to count pinned entries: %v", err) - } - - if pinnedCount >= cfg.MaxPinned { - return fmt.Errorf("maximum pinned entries reached (%d)", cfg.MaxPinned) - } - - err = m.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - v := b.Get(itob(id)) - if v == nil { - return fmt.Errorf("entry not found") - } - - entry, err := decodeEntry(v) - if err != nil { - return err - } - - entry.Pinned = true - encoded, err := encodeEntry(entry) - if err != nil { - return err - } - - return b.Put(itob(id), encoded) - }) - - if err == nil { - m.updateState() - m.notifySubscribers() - } - - return err -} - -func (m *Manager) UnpinEntry(id uint64) error { - if m.db == nil { - return fmt.Errorf("database not available") - } - - err := m.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - v := b.Get(itob(id)) - if v == nil { - return fmt.Errorf("entry not found") - } - - entry, err := decodeEntry(v) - if err != nil { - return err - } - - entry.Pinned = false - encoded, err := encodeEntry(entry) - if err != nil { - return err - } - - return b.Put(itob(id), encoded) - }) - - if err == nil { - m.updateState() - m.notifySubscribers() - } - - return err -} - -func (m *Manager) GetPinnedEntries() []Entry { - if m.db == nil { - return nil - } - - var pinned []Entry - if err := m.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - if b == nil { - return nil - } - - c := b.Cursor() - for k, v := c.Last(); k != nil; k, v = c.Prev() { - entry, err := decodeEntry(v) - if err != nil { - continue - } - if entry.Pinned { - entry.Data = nil - pinned = append(pinned, entry) - } - } - return nil - }); err != nil { - log.Errorf("Failed to get pinned entries: %v", err) - } - - return pinned -} - -func (m *Manager) GetPinnedCount() int { - if m.db == nil { - return 0 - } - - count := 0 - if err := m.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("clipboard")) - if b == nil { - return nil - } - - c := b.Cursor() - for k, v := c.First(); k != nil; k, v = c.Next() { - entry, err := decodeEntry(v) - if err == nil && entry.Pinned { - count++ - } - } - return nil - }); err != nil { - log.Errorf("Failed to count pinned entries: %v", err) - } - - return count -} - -func (m *Manager) CopyFile(filePath string) error { - fileInfo, err := os.Stat(filePath) - if err != nil { - return fmt.Errorf("file not found: %w", err) - } - - cfg := m.getConfig() - if fileInfo.Size() > cfg.MaxEntrySize { - return fmt.Errorf("file too large: %d > %d", fileInfo.Size(), cfg.MaxEntrySize) - } - - fileData, err := os.ReadFile(filePath) - if err != nil { - return fmt.Errorf("read file: %w", err) - } - - exportedPath, err := m.ExportFileForFlatpak(filePath) - if err != nil { - exportedPath = filePath - } - fileURI := "file://" + exportedPath - - if imgData, imgMime, ok := m.tryReadImageFromURI([]byte("file://" + filePath)); ok { - entry := Entry{ - Data: imgData, - MimeType: imgMime, - Size: len(imgData), - Timestamp: time.Now(), - IsImage: true, - Preview: m.imagePreview(imgData, imgMime), - } - if err := m.storeEntry(entry); err != nil { - log.Errorf("Failed to store file entry: %v", err) - } - } else { - entry := Entry{ - Data: fileData, - MimeType: "text/uri-list", - Size: len(fileData), - Timestamp: time.Now(), - IsImage: false, - Preview: fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)), - } - if err := m.storeEntry(entry); err != nil { - log.Errorf("Failed to store file entry: %v", err) - } - } - - m.updateState() - m.notifySubscribers() - - _, imgMime, imgErr := image.DecodeConfig(bytes.NewReader(fileData)) - - m.post(func() { - if m.dataControlMgr == nil || m.dataDevice == nil { - log.Error("Data control manager or device not initialized") - return - } - - dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1) - source, err := dataMgr.CreateDataSource() - if err != nil { - log.Errorf("Failed to create data source: %v", err) - return - } - - type offer struct { - mime string - data []byte - } - offers := []offer{ - {"x-special/gnome-copied-files", []byte("copy\n" + fileURI)}, - {"text/uri-list", []byte(fileURI + "\r\n")}, - {"text/plain", []byte(filePath)}, - } - - if imgErr == nil { - imgMimeType := "image/" + imgMime - offers = append(offers, offer{imgMimeType, fileData}) - } - - offerData := make(map[string][]byte) - for _, o := range offers { - if err := source.Offer(o.mime); err != nil { - log.Errorf("Failed to offer %s: %v", o.mime, err) - return - } - offerData[o.mime] = o.data - } - - source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { - fd := e.Fd - defer syscall.Close(fd) - file := os.NewFile(uintptr(fd), "clipboard-pipe") - defer file.Close() - if data, ok := offerData[e.MimeType]; ok { - file.Write(data) - } - }) - - source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) { - m.ownerLock.Lock() - m.isOwner = false - m.ownerLock.Unlock() - }) - - m.currentSource = source - - m.ownerLock.Lock() - m.isOwner = true - m.ownerLock.Unlock() - - device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1) - if err := device.SetSelection(source); err != nil { - log.Errorf("Failed to set selection: %v", err) - } - }) - - return nil -} - -func (m *Manager) EntryToFile(entry *Entry) string { - switch { - case entry.MimeType == "text/uri-list": - data := strings.TrimSpace(string(entry.Data)) - lines := strings.Split(data, "\n") - if len(lines) == 0 { - return "" - } - uri := strings.TrimSuffix(strings.TrimSpace(lines[0]), "\r") - if path, ok := strings.CutPrefix(uri, "file://"); ok { - return path - } - case entry.IsImage: - ext := ".png" - if suffix, ok := strings.CutPrefix(entry.MimeType, "image/"); ok { - ext = "." + suffix - } - cacheDir, err := os.UserCacheDir() - if err != nil { - return "" - } - clipDir := filepath.Join(cacheDir, "dms", "clipboard") - if err := os.MkdirAll(clipDir, 0o755); err != nil { - return "" - } - filePath := filepath.Join(clipDir, fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)) - if os.WriteFile(filePath, entry.Data, 0o644) != nil { - return "" - } - return filePath - } - return "" -} - -func (m *Manager) ExportFileForFlatpak(filePath string) (string, error) { - if _, err := os.Stat(filePath); err != nil { - return "", fmt.Errorf("file not found: %w", err) - } - - if m.dbusConn == nil { - conn, err := dbus.ConnectSessionBus() - if err != nil { - return "", fmt.Errorf("connect session bus: %w", err) - } - if !conn.SupportsUnixFDs() { - conn.Close() - return "", fmt.Errorf("D-Bus connection does not support Unix FD passing") - } - m.dbusConn = conn - } - - file, err := os.Open(filePath) - if err != nil { - return "", fmt.Errorf("open file: %w", err) - } - fd := int(file.Fd()) - - portal := m.dbusConn.Object("org.freedesktop.portal.Documents", "/org/freedesktop/portal/documents") - - var docIds []string - var extra map[string]dbus.Variant - flags := uint32(0) - - err = portal.Call( - "org.freedesktop.portal.Documents.AddFull", - 0, - []dbus.UnixFD{dbus.UnixFD(fd)}, - flags, - "", - []string{}, - ).Store(&docIds, &extra) - - file.Close() - - if err != nil { - return "", fmt.Errorf("AddFull: %w", err) - } - - if len(docIds) == 0 { - return "", fmt.Errorf("no doc IDs returned") - } - - docId := docIds[0] - - for _, app := range getInstalledFlatpaks() { - _ = portal.Call( - "org.freedesktop.portal.Documents.GrantPermissions", - 0, - docId, - app, - []string{"read"}, - ).Err - } - - uid := os.Getuid() - basename := filepath.Base(filePath) - exportedPath := fmt.Sprintf("/run/user/%d/doc/%s/%s", uid, docId, basename) - - return exportedPath, nil -} - -func getInstalledFlatpaks() []string { - out, err := exec.Command("flatpak", "list", "--app", "--columns=application").Output() - if err != nil { - return nil - } - - var apps []string - for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { - if app := strings.TrimSpace(line); app != "" { - apps = append(apps, app) - } - } - return apps -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager_test.go deleted file mode 100644 index db45bca..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager_test.go +++ /dev/null @@ -1,531 +0,0 @@ -package clipboard - -import ( - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - - mocks_wlcontext "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlcontext" -) - -func TestEncodeDecodeEntry_Roundtrip(t *testing.T) { - original := Entry{ - ID: 12345, - Data: []byte("hello world"), - MimeType: "text/plain;charset=utf-8", - Preview: "hello world", - Size: 11, - Timestamp: time.Now().Truncate(time.Second), - IsImage: false, - } - - encoded, err := encodeEntry(original) - assert.NoError(t, err) - - decoded, err := decodeEntry(encoded) - assert.NoError(t, err) - - assert.Equal(t, original.ID, decoded.ID) - assert.Equal(t, original.Data, decoded.Data) - assert.Equal(t, original.MimeType, decoded.MimeType) - assert.Equal(t, original.Preview, decoded.Preview) - assert.Equal(t, original.Size, decoded.Size) - assert.Equal(t, original.Timestamp.Unix(), decoded.Timestamp.Unix()) - assert.Equal(t, original.IsImage, decoded.IsImage) -} - -func TestEncodeDecodeEntry_Image(t *testing.T) { - original := Entry{ - ID: 99999, - Data: []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, - MimeType: "image/png", - Preview: "[[ image 8 B png 100x100 ]]", - Size: 8, - Timestamp: time.Now().Truncate(time.Second), - IsImage: true, - } - - encoded, err := encodeEntry(original) - assert.NoError(t, err) - - decoded, err := decodeEntry(encoded) - assert.NoError(t, err) - - assert.Equal(t, original.ID, decoded.ID) - assert.Equal(t, original.Data, decoded.Data) - assert.True(t, decoded.IsImage) - assert.Equal(t, original.Preview, decoded.Preview) -} - -func TestEncodeDecodeEntry_EmptyData(t *testing.T) { - original := Entry{ - ID: 1, - Data: []byte{}, - MimeType: "text/plain", - Preview: "", - Size: 0, - Timestamp: time.Now().Truncate(time.Second), - IsImage: false, - } - - encoded, err := encodeEntry(original) - assert.NoError(t, err) - - decoded, err := decodeEntry(encoded) - assert.NoError(t, err) - - assert.Equal(t, original.ID, decoded.ID) - assert.Empty(t, decoded.Data) -} - -func TestEncodeDecodeEntry_LargeData(t *testing.T) { - largeData := make([]byte, 100000) - for i := range largeData { - largeData[i] = byte(i % 256) - } - - original := Entry{ - ID: 777, - Data: largeData, - MimeType: "application/octet-stream", - Preview: "binary data...", - Size: len(largeData), - Timestamp: time.Now().Truncate(time.Second), - IsImage: false, - } - - encoded, err := encodeEntry(original) - assert.NoError(t, err) - - decoded, err := decodeEntry(encoded) - assert.NoError(t, err) - - assert.Equal(t, original.Data, decoded.Data) - assert.Equal(t, original.Size, decoded.Size) -} - -func TestStateEqual_BothNil(t *testing.T) { - assert.False(t, stateEqual(nil, nil)) -} - -func TestStateEqual_OneNil(t *testing.T) { - s := &State{Enabled: true} - assert.False(t, stateEqual(s, nil)) - assert.False(t, stateEqual(nil, s)) -} - -func TestStateEqual_EnabledDiffers(t *testing.T) { - a := &State{Enabled: true, History: []Entry{}} - b := &State{Enabled: false, History: []Entry{}} - assert.False(t, stateEqual(a, b)) -} - -func TestStateEqual_HistoryLengthDiffers(t *testing.T) { - a := &State{Enabled: true, History: []Entry{{ID: 1}}} - b := &State{Enabled: true, History: []Entry{}} - assert.False(t, stateEqual(a, b)) -} - -func TestStateEqual_BothEqual(t *testing.T) { - a := &State{Enabled: true, History: []Entry{{ID: 1}, {ID: 2}}} - b := &State{Enabled: true, History: []Entry{{ID: 3}, {ID: 4}}} - assert.True(t, stateEqual(a, b)) -} - -func TestManager_ConcurrentSubscriberAccess(t *testing.T) { - m := &Manager{ - subscribers: make(map[string]chan State), - dirty: make(chan struct{}, 1), - } - - var wg sync.WaitGroup - const goroutines = 20 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - subID := string(rune('a' + id)) - ch := m.Subscribe(subID) - assert.NotNil(t, ch) - time.Sleep(time.Millisecond) - m.Unsubscribe(subID) - }(i) - } - - wg.Wait() -} - -func TestManager_ConcurrentGetState(t *testing.T) { - m := &Manager{ - state: &State{ - Enabled: true, - History: []Entry{{ID: 1}, {ID: 2}}, - }, - } - - var wg sync.WaitGroup - const goroutines = 50 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - s := m.GetState() - _ = s.Enabled - _ = len(s.History) - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.stateMutex.Lock() - m.state = &State{ - Enabled: j%2 == 0, - History: []Entry{{ID: uint64(j)}}, - } - m.stateMutex.Unlock() - } - }(i) - } - - wg.Wait() -} - -func TestManager_ConcurrentConfigAccess(t *testing.T) { - m := &Manager{ - config: DefaultConfig(), - } - - var wg sync.WaitGroup - const goroutines = 30 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - cfg := m.getConfig() - _ = cfg.MaxHistory - _ = cfg.MaxEntrySize - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.configMutex.Lock() - m.config.MaxHistory = 50 + j - m.config.MaxEntrySize = int64(1024 * j) - m.configMutex.Unlock() - } - }(i) - } - - wg.Wait() -} - -func TestManager_NotifySubscribersNonBlocking(t *testing.T) { - m := &Manager{ - dirty: make(chan struct{}, 1), - } - - for i := 0; i < 10; i++ { - m.notifySubscribers() - } - - assert.Len(t, m.dirty, 1) -} - -func TestManager_ConcurrentOfferAccess(t *testing.T) { - m := &Manager{ - offerMimeTypes: make(map[any][]string), - offerRegistry: make(map[uint32]any), - } - - var wg sync.WaitGroup - const goroutines = 20 - const iterations = 50 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - key := uint32(id) - - for j := 0; j < iterations; j++ { - m.offerMutex.Lock() - m.offerRegistry[key] = struct{}{} - m.offerMimeTypes[key] = []string{"text/plain"} - m.offerMutex.Unlock() - - m.offerMutex.RLock() - _ = m.offerRegistry[key] - _ = m.offerMimeTypes[key] - m.offerMutex.RUnlock() - - m.offerMutex.Lock() - delete(m.offerRegistry, key) - delete(m.offerMimeTypes, key) - m.offerMutex.Unlock() - } - }(i) - } - - wg.Wait() -} - -func TestManager_ConcurrentPersistAccess(t *testing.T) { - m := &Manager{ - persistData: make(map[string][]byte), - persistMimeTypes: []string{}, - } - - var wg sync.WaitGroup - const goroutines = 20 - const iterations = 50 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.persistMutex.RLock() - _ = m.persistData - _ = m.persistMimeTypes - m.persistMutex.RUnlock() - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.persistMutex.Lock() - m.persistMimeTypes = []string{"text/plain", "text/html"} - m.persistData = map[string][]byte{ - "text/plain": []byte("test"), - } - m.persistMutex.Unlock() - } - }(i) - } - - wg.Wait() -} - -func TestManager_ConcurrentOwnerAccess(t *testing.T) { - m := &Manager{} - - var wg sync.WaitGroup - const goroutines = 30 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.ownerLock.Lock() - _ = m.isOwner - m.ownerLock.Unlock() - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.ownerLock.Lock() - m.isOwner = j%2 == 0 - m.ownerLock.Unlock() - } - }() - } - - wg.Wait() -} - -func TestItob(t *testing.T) { - tests := []struct { - input uint64 - expected []byte - }{ - {0, []byte{0, 0, 0, 0, 0, 0, 0, 0}}, - {1, []byte{0, 0, 0, 0, 0, 0, 0, 1}}, - {256, []byte{0, 0, 0, 0, 0, 0, 1, 0}}, - {0xFFFFFFFFFFFFFFFF, []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}}, - } - - for _, tt := range tests { - result := itob(tt.input) - assert.Equal(t, tt.expected, result) - } -} - -func TestSizeStr(t *testing.T) { - tests := []struct { - input int - expected string - }{ - {0, "0 B"}, - {100, "100 B"}, - {1024, "1 KiB"}, - {2048, "2 KiB"}, - {1048576, "1 MiB"}, - {5242880, "5 MiB"}, - } - - for _, tt := range tests { - result := sizeStr(tt.input) - assert.Equal(t, tt.expected, result) - } -} - -func TestSelectMimeType(t *testing.T) { - m := &Manager{} - - tests := []struct { - mimes []string - expected string - }{ - {[]string{"text/plain;charset=utf-8", "text/html"}, "text/plain;charset=utf-8"}, - {[]string{"text/html", "text/plain"}, "text/plain"}, - {[]string{"text/html", "image/png"}, "image/png"}, - {[]string{"image/png", "image/jpeg"}, "image/png"}, - {[]string{"image/png"}, "image/png"}, - {[]string{"application/octet-stream"}, "application/octet-stream"}, - {[]string{}, ""}, - } - - for _, tt := range tests { - result := m.selectMimeType(tt.mimes) - assert.Equal(t, tt.expected, result) - } -} - -func TestIsImageMimeType(t *testing.T) { - m := &Manager{} - - assert.True(t, m.isImageMimeType("image/png")) - assert.True(t, m.isImageMimeType("image/jpeg")) - assert.True(t, m.isImageMimeType("image/gif")) - assert.False(t, m.isImageMimeType("text/plain")) - assert.False(t, m.isImageMimeType("application/json")) -} - -func TestTextPreview(t *testing.T) { - m := &Manager{} - - short := m.textPreview([]byte("hello world")) - assert.Equal(t, "hello world", short) - - withWhitespace := m.textPreview([]byte(" hello world ")) - assert.Equal(t, "hello world", withWhitespace) - - longText := make([]byte, 200) - for i := range longText { - longText[i] = 'a' - } - preview := m.textPreview(longText) - assert.True(t, len(preview) > 100) - assert.Contains(t, preview, "…") -} - -func TestDefaultConfig(t *testing.T) { - cfg := DefaultConfig() - assert.Equal(t, 100, cfg.MaxHistory) - assert.Equal(t, int64(5*1024*1024), cfg.MaxEntrySize) - assert.Equal(t, 0, cfg.AutoClearDays) - assert.False(t, cfg.ClearAtStartup) - assert.False(t, cfg.Disabled) -} - -func TestManager_PostDelegatesToWlContext(t *testing.T) { - mockCtx := mocks_wlcontext.NewMockWaylandContext(t) - - var called atomic.Bool - mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) { - called.Store(true) - fn() - }).Once() - - m := &Manager{ - wlCtx: mockCtx, - } - - executed := false - m.post(func() { - executed = true - }) - - assert.True(t, called.Load()) - assert.True(t, executed) -} - -func TestManager_PostExecutesFunctionViaContext(t *testing.T) { - mockCtx := mocks_wlcontext.NewMockWaylandContext(t) - - var capturedFn func() - mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) { - capturedFn = fn - }).Times(3) - - m := &Manager{ - wlCtx: mockCtx, - } - - counter := 0 - m.post(func() { counter++ }) - m.post(func() { counter += 10 }) - m.post(func() { counter += 100 }) - - assert.NotNil(t, capturedFn) - capturedFn() - assert.Equal(t, 100, counter) -} - -func TestManager_ConcurrentPostWithMock(t *testing.T) { - mockCtx := mocks_wlcontext.NewMockWaylandContext(t) - - var postCount atomic.Int32 - mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) { - postCount.Add(1) - }).Times(100) - - m := &Manager{ - wlCtx: mockCtx, - } - - var wg sync.WaitGroup - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 10; j++ { - m.post(func() {}) - } - }() - } - - wg.Wait() - assert.Equal(t, int32(100), postCount.Load()) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/types.go deleted file mode 100644 index c754561..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/types.go +++ /dev/null @@ -1,196 +0,0 @@ -package clipboard - -import ( - "encoding/json" - "os" - "path/filepath" - "sync" - "time" - - "github.com/godbus/dbus/v5" - bolt "go.etcd.io/bbolt" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext" - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" -) - -type Config struct { - MaxHistory int `json:"maxHistory"` - MaxEntrySize int64 `json:"maxEntrySize"` - AutoClearDays int `json:"autoClearDays"` - ClearAtStartup bool `json:"clearAtStartup"` - Disabled bool `json:"disabled"` - MaxPinned int `json:"maxPinned"` -} - -func DefaultConfig() Config { - return Config{ - MaxHistory: 100, - MaxEntrySize: 5 * 1024 * 1024, - AutoClearDays: 0, - ClearAtStartup: false, - MaxPinned: 25, - } -} - -func getConfigPath() (string, error) { - configDir, err := os.UserConfigDir() - if err != nil { - return "", err - } - return filepath.Join(configDir, "DankMaterialShell", "clsettings.json"), nil -} - -func LoadConfig() Config { - cfg := DefaultConfig() - - path, err := getConfigPath() - if err != nil { - return cfg - } - - data, err := os.ReadFile(path) - if err != nil { - return cfg - } - - if err := json.Unmarshal(data, &cfg); err != nil { - return DefaultConfig() - } - return cfg -} - -func SaveConfig(cfg Config) error { - path, err := getConfigPath() - if err != nil { - return err - } - - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return err - } - - data, err := json.MarshalIndent(cfg, "", " ") - if err != nil { - return err - } - - return os.WriteFile(path, data, 0o644) -} - -type SearchParams struct { - Query string `json:"query"` - MimeType string `json:"mimeType"` - IsImage *bool `json:"isImage"` - Limit int `json:"limit"` - Offset int `json:"offset"` - Before *int64 `json:"before"` - After *int64 `json:"after"` -} - -type SearchResult struct { - Entries []Entry `json:"entries"` - Total int `json:"total"` - HasMore bool `json:"hasMore"` -} - -type Entry struct { - ID uint64 `json:"id"` - Data []byte `json:"data,omitempty"` - MimeType string `json:"mimeType"` - Preview string `json:"preview"` - Size int `json:"size"` - Timestamp time.Time `json:"timestamp"` - IsImage bool `json:"isImage"` - Hash uint64 `json:"hash,omitempty"` - Pinned bool `json:"pinned"` -} - -type State struct { - Enabled bool `json:"enabled"` - History []Entry `json:"history"` - Current *Entry `json:"current,omitempty"` -} - -type Manager struct { - config Config - configMutex sync.RWMutex - configPath string - - display wlclient.WaylandDisplay - wlCtx wlcontext.WaylandContext - - registry *wlclient.Registry - dataControlMgr any - seat *wlclient.Seat - dataDevice any - currentOffer any - currentSource any - seatName uint32 - mimeTypes []string - offerMimeTypes map[any][]string - offerMutex sync.RWMutex - offerRegistry map[uint32]any - - sourceMimeTypes []string - sourceMutex sync.RWMutex - - persistData map[string][]byte - persistMimeTypes []string - persistMutex sync.RWMutex - - isOwner bool - ownerLock sync.Mutex - - initialized bool - - alive bool - stopChan chan struct{} - - db *bolt.DB - dbPath string - - state *State - stateMutex sync.RWMutex - - subscribers map[string]chan State - subMutex sync.RWMutex - dirty chan struct{} - notifierWg sync.WaitGroup - lastState *State - - dbusConn *dbus.Conn -} - -func (m *Manager) GetState() State { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - if m.state == nil { - return State{} - } - return *m.state -} - -func (m *Manager) Subscribe(id string) chan State { - ch := make(chan State, 64) - m.subMutex.Lock() - m.subscribers[id] = ch - m.subMutex.Unlock() - return ch -} - -func (m *Manager) Unsubscribe(id string) { - m.subMutex.Lock() - if ch, ok := m.subscribers[id]; ok { - close(ch) - delete(m.subscribers, id) - } - m.subMutex.Unlock() -} - -func (m *Manager) notifySubscribers() { - select { - case m.dirty <- struct{}{}: - default: - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/actions.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/actions.go deleted file mode 100644 index 2f9b98b..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/actions.go +++ /dev/null @@ -1,479 +0,0 @@ -package cups - -import ( - "errors" - "fmt" - "net" - "net/url" - "os/exec" - "strings" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/config" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" -) - -func isAuthError(err error) bool { - if err == nil { - return false - } - - var httpErr ipp.HTTPError - if errors.As(err, &httpErr) { - return httpErr.Code == 401 || httpErr.Code == 403 - } - - var ippErr ipp.IPPError - if errors.As(err, &ippErr) { - return ippErr.Status == ipp.StatusErrorForbidden || - ippErr.Status == ipp.StatusErrorNotAuthenticated || - ippErr.Status == ipp.StatusErrorNotAuthorized - } - - return false -} - -func (m *Manager) GetPrinters() ([]Printer, error) { - attributes := []string{ - ipp.AttributePrinterName, - ipp.AttributePrinterUriSupported, - ipp.AttributePrinterState, - ipp.AttributePrinterStateReasons, - ipp.AttributePrinterLocation, - ipp.AttributePrinterInfo, - ipp.AttributePrinterMakeAndModel, - ipp.AttributePrinterIsAcceptingJobs, - } - - printerAttrs, err := m.client.GetPrinters(attributes) - if err != nil { - if isNoPrintersError(err) { - return []Printer{}, nil - } - return nil, err - } - - printers := make([]Printer, 0, len(printerAttrs)) - for _, attrs := range printerAttrs { - printer := Printer{ - Name: getStringAttr(attrs, ipp.AttributePrinterName), - URI: getStringAttr(attrs, ipp.AttributePrinterUriSupported), - State: parsePrinterState(attrs), - StateReason: getStringAttr(attrs, ipp.AttributePrinterStateReasons), - Location: getStringAttr(attrs, ipp.AttributePrinterLocation), - Info: getStringAttr(attrs, ipp.AttributePrinterInfo), - MakeModel: getStringAttr(attrs, ipp.AttributePrinterMakeAndModel), - Accepting: getBoolAttr(attrs, ipp.AttributePrinterIsAcceptingJobs), - } - - if printer.Name != "" { - printers = append(printers, printer) - } - } - - return printers, nil -} - -func (m *Manager) GetJobs(printerName string, whichJobs string) ([]Job, error) { - attributes := []string{ - ipp.AttributeJobID, - ipp.AttributeJobName, - ipp.AttributeJobState, - ipp.AttributeJobPrinterURI, - ipp.AttributeJobOriginatingUserName, - ipp.AttributeJobKilobyteOctets, - "time-at-creation", - } - - jobAttrs, err := m.client.GetJobs(printerName, "", whichJobs, false, 0, 0, attributes) - if err != nil { - return nil, err - } - - jobs := make([]Job, 0, len(jobAttrs)) - for _, attrs := range jobAttrs { - job := Job{ - ID: getIntAttr(attrs, ipp.AttributeJobID), - Name: getStringAttr(attrs, ipp.AttributeJobName), - State: parseJobState(attrs), - User: getStringAttr(attrs, ipp.AttributeJobOriginatingUserName), - Size: getIntAttr(attrs, ipp.AttributeJobKilobyteOctets) * 1024, - } - - if uri := getStringAttr(attrs, ipp.AttributeJobPrinterURI); uri != "" { - parts := strings.Split(uri, "/") - if len(parts) > 0 { - job.Printer = parts[len(parts)-1] - } - } - - if ts := getIntAttr(attrs, "time-at-creation"); ts > 0 { - job.TimeCreated = time.Unix(int64(ts), 0) - } - - if job.ID != 0 { - jobs = append(jobs, job) - } - } - - return jobs, nil -} - -func (m *Manager) CancelJob(jobID int) error { - err := m.client.CancelJob(jobID, false) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.JobCancelPurge(jobID, false) - } - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) PausePrinter(printerName string) error { - err := m.client.PausePrinter(printerName) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.PrinterSetEnabled(printerName, false) - } - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) ResumePrinter(printerName string) error { - err := m.client.ResumePrinter(printerName) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.PrinterSetEnabled(printerName, true) - } - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) PurgeJobs(printerName string) error { - err := m.client.CancelAllJob(printerName, true) - if err == nil { - m.RefreshState() - } - return err -} - -func resolveIPFromURI(uri string) string { - parsed, err := url.Parse(uri) - if err != nil { - return "" - } - host := parsed.Hostname() - if host == "" { - return "" - } - if ip := net.ParseIP(host); ip != nil { - return ip.String() - } - addrs, err := net.LookupIP(host) - if err != nil || len(addrs) == 0 { - return "" - } - for _, addr := range addrs { - if v4 := addr.To4(); v4 != nil { - return v4.String() - } - } - return addrs[0].String() -} - -func (m *Manager) GetDevices() ([]Device, error) { - if m.pkHelper != nil { - devices, err := m.pkHelper.DevicesGet(10, 0, nil, nil) - if err != nil { - return nil, err - } - for i := range devices { - if devices[i].Class == "network" { - devices[i].IP = resolveIPFromURI(devices[i].URI) - } - } - return devices, nil - } - - deviceAttrs, err := m.client.GetDevices() - if err != nil { - return nil, err - } - - devices := make([]Device, 0, len(deviceAttrs)) - for uri, attrs := range deviceAttrs { - device := Device{ - URI: uri, - Class: getStringAttr(attrs, "device-class"), - Info: getStringAttr(attrs, "device-info"), - MakeModel: getStringAttr(attrs, "device-make-and-model"), - ID: getStringAttr(attrs, "device-id"), - Location: getStringAttr(attrs, "device-location"), - } - if device.Class == "network" { - device.IP = resolveIPFromURI(uri) - } - devices = append(devices, device) - } - - return devices, nil -} - -func (m *Manager) GetPPDs() ([]PPD, error) { - ppdAttrs, err := m.client.GetPPDs() - if err != nil { - return nil, err - } - - ppds := make([]PPD, 0, len(ppdAttrs)) - for name, attrs := range ppdAttrs { - ppd := PPD{ - Name: name, - NaturalLanguage: getStringAttr(attrs, "ppd-natural-language"), - MakeModel: getStringAttr(attrs, ipp.AttributePPDMakeAndModel), - DeviceID: getStringAttr(attrs, "ppd-device-id"), - Product: getStringAttr(attrs, "ppd-product"), - PSVersion: getStringAttr(attrs, "ppd-psversion"), - Type: getStringAttr(attrs, "ppd-type"), - } - ppds = append(ppds, ppd) - } - - return ppds, nil -} - -func (m *Manager) GetClasses() ([]PrinterClass, error) { - attributes := []string{ - ipp.AttributePrinterName, - ipp.AttributePrinterUriSupported, - ipp.AttributePrinterState, - ipp.AttributeMemberURIs, - ipp.AttributeMemberNames, - ipp.AttributePrinterLocation, - ipp.AttributePrinterInfo, - } - - classAttrs, err := m.client.GetClasses(attributes) - if err != nil { - return nil, err - } - - classes := make([]PrinterClass, 0, len(classAttrs)) - for _, attrs := range classAttrs { - class := PrinterClass{ - Name: getStringAttr(attrs, ipp.AttributePrinterName), - URI: getStringAttr(attrs, ipp.AttributePrinterUriSupported), - State: parsePrinterState(attrs), - Location: getStringAttr(attrs, ipp.AttributePrinterLocation), - Info: getStringAttr(attrs, ipp.AttributePrinterInfo), - Members: getStringSliceAttr(attrs, ipp.AttributeMemberNames), - } - classes = append(classes, class) - } - - return classes, nil -} - -func createPrinterViaLpadmin(name, deviceURI, ppd, information, location string) error { - args := []string{"-p", name, "-E", "-v", deviceURI, "-m", ppd} - if information != "" { - args = append(args, "-D", information) - } - if location != "" { - args = append(args, "-L", location) - } - out, err := exec.Command("lpadmin", args...).CombinedOutput() - if err != nil { - return fmt.Errorf("lpadmin failed: %s: %w", strings.TrimSpace(string(out)), err) - } - return nil -} - -func deletePrinterViaLpadmin(name string) error { - out, err := exec.Command("lpadmin", "-x", name).CombinedOutput() - if err != nil { - return fmt.Errorf("lpadmin failed: %s: %w", strings.TrimSpace(string(out)), err) - } - return nil -} - -func (m *Manager) CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error { - usedPkHelper := false - - err := m.client.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location) - if isAuthError(err) && m.pkHelper != nil { - if err = m.pkHelper.PrinterAdd(name, deviceURI, ppd, information, location); err != nil { - // pkHelper failed (e.g., no polkit agent), try lpadmin as last resort. - // lpadmin -E enables the printer, so no further setup needed. - if lpadminErr := createPrinterViaLpadmin(name, deviceURI, ppd, information, location); lpadminErr != nil { - return err - } - m.RefreshState() - return nil - } - usedPkHelper = true - } else if err != nil { - return err - } - - if usedPkHelper { - m.pkHelper.PrinterSetEnabled(name, true) //nolint:errcheck - m.pkHelper.PrinterSetAcceptJobs(name, true, "") //nolint:errcheck - } else { - if err := m.client.ResumePrinter(name); isAuthError(err) && m.pkHelper != nil { - m.pkHelper.PrinterSetEnabled(name, true) //nolint:errcheck - } - if err := m.client.AcceptJobs(name); isAuthError(err) && m.pkHelper != nil { - m.pkHelper.PrinterSetAcceptJobs(name, true, "") //nolint:errcheck - } - } - - m.RefreshState() - return nil -} - -func (m *Manager) DeletePrinter(printerName string) error { - err := m.client.DeletePrinter(printerName) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.PrinterDelete(printerName) - if err != nil { - // pkHelper failed, try lpadmin as last resort - if lpadminErr := deletePrinterViaLpadmin(printerName); lpadminErr == nil { - err = nil - } - } - } - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) AcceptJobs(printerName string) error { - err := m.client.AcceptJobs(printerName) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.PrinterSetAcceptJobs(printerName, true, "") - } - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) RejectJobs(printerName string) error { - err := m.client.RejectJobs(printerName) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.PrinterSetAcceptJobs(printerName, false, "") - } - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) SetPrinterShared(printerName string, shared bool) error { - err := m.client.SetPrinterIsShared(printerName, shared) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.PrinterSetShared(printerName, shared) - } - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) SetPrinterLocation(printerName, location string) error { - err := m.client.SetPrinterLocation(printerName, location) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.PrinterSetLocation(printerName, location) - } - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) SetPrinterInfo(printerName, info string) error { - err := m.client.SetPrinterInformation(printerName, info) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.PrinterSetInfo(printerName, info) - } - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) MoveJob(jobID int, destPrinter string) error { - err := m.client.MoveJob(jobID, destPrinter) - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) PrintTestPage(printerName string) (int, error) { - jobID, err := m.client.PrintTestPage(printerName, strings.NewReader(config.TestPage), len(config.TestPage)) - if err == nil { - m.RefreshState() - } - return jobID, err -} - -func (m *Manager) AddPrinterToClass(className, printerName string) error { - err := m.client.AddPrinterToClass(className, printerName) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.ClassAddPrinter(className, printerName) - } - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) RemovePrinterFromClass(className, printerName string) error { - err := m.client.DeletePrinterFromClass(className, printerName) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.ClassDeletePrinter(className, printerName) - } - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) DeleteClass(className string) error { - err := m.client.DeleteClass(className) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.ClassDelete(className) - } - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) RestartJob(jobID int) error { - err := m.client.RestartJob(jobID) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.JobRestart(jobID) - } - if err == nil { - m.RefreshState() - } - return err -} - -func (m *Manager) HoldJob(jobID int, holdUntil string) error { - err := m.client.HoldJobUntil(jobID, holdUntil) - if isAuthError(err) && m.pkHelper != nil { - err = m.pkHelper.JobSetHoldUntil(jobID, holdUntil) - } - if err == nil { - m.RefreshState() - } - return err -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/actions_pkhelper_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/actions_pkhelper_test.go deleted file mode 100644 index ad16a8a..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/actions_pkhelper_test.go +++ /dev/null @@ -1,235 +0,0 @@ -package cups_test - -import ( - "testing" - - mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups" - mocks_pkhelper "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups_pkhelper" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func authErr() error { - return ipp.IPPError{Status: ipp.StatusErrorForbidden} -} - -func TestManager_CancelJob_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().CancelJob(1, false).Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().JobCancelPurge(1, false).Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.CancelJob(1)) -} - -func TestManager_CancelJob_PkHelperError(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().CancelJob(1, false).Return(authErr()) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().JobCancelPurge(1, false).Return(assert.AnError) - - m := cups.NewTestManager(mockClient, mockPk) - assert.Error(t, m.CancelJob(1)) -} - -func TestManager_PausePrinter_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().PausePrinter("printer1").Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().PrinterSetEnabled("printer1", false).Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.PausePrinter("printer1")) -} - -func TestManager_ResumePrinter_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().ResumePrinter("printer1").Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().PrinterSetEnabled("printer1", true).Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.ResumePrinter("printer1")) -} - -func TestManager_GetDevices_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().DevicesGet(10, 0, []string(nil), []string(nil)).Return([]cups.Device{ - {URI: "usb://HP/LaserJet", Class: "direct"}, - }, nil) - - m := cups.NewTestManager(mockClient, mockPk) - got, err := m.GetDevices() - assert.NoError(t, err) - assert.Len(t, got, 1) - assert.Equal(t, "usb://HP/LaserJet", got[0].URI) -} - -func TestManager_GetDevices_PkHelperError(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().DevicesGet(10, 0, []string(nil), []string(nil)).Return(nil, assert.AnError) - - m := cups.NewTestManager(mockClient, mockPk) - _, err := m.GetDevices() - assert.Error(t, err) -} - -func TestManager_CreatePrinter_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().CreatePrinter("newprinter", "usb://HP", "generic.ppd", true, "stop-printer", "info", "location").Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().PrinterAdd("newprinter", "usb://HP", "generic.ppd", "info", "location").Return(nil) - mockPk.EXPECT().PrinterSetEnabled("newprinter", true).Return(nil) - mockPk.EXPECT().PrinterSetAcceptJobs("newprinter", true, "").Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.CreatePrinter("newprinter", "usb://HP", "generic.ppd", true, "stop-printer", "info", "location")) -} - -func TestManager_DeletePrinter_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().DeletePrinter("printer1").Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().PrinterDelete("printer1").Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.DeletePrinter("printer1")) -} - -func TestManager_AcceptJobs_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().AcceptJobs("printer1").Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().PrinterSetAcceptJobs("printer1", true, "").Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.AcceptJobs("printer1")) -} - -func TestManager_RejectJobs_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().RejectJobs("printer1").Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().PrinterSetAcceptJobs("printer1", false, "").Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.RejectJobs("printer1")) -} - -func TestManager_SetPrinterShared_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().SetPrinterIsShared("printer1", true).Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().PrinterSetShared("printer1", true).Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.SetPrinterShared("printer1", true)) -} - -func TestManager_SetPrinterLocation_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().SetPrinterLocation("printer1", "Office").Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().PrinterSetLocation("printer1", "Office").Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.SetPrinterLocation("printer1", "Office")) -} - -func TestManager_SetPrinterInfo_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().SetPrinterInformation("printer1", "Main Printer").Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().PrinterSetInfo("printer1", "Main Printer").Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.SetPrinterInfo("printer1", "Main Printer")) -} - -func TestManager_AddPrinterToClass_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().AddPrinterToClass("office", "printer1").Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().ClassAddPrinter("office", "printer1").Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.AddPrinterToClass("office", "printer1")) -} - -func TestManager_RemovePrinterFromClass_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().DeletePrinterFromClass("office", "printer1").Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().ClassDeletePrinter("office", "printer1").Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.RemovePrinterFromClass("office", "printer1")) -} - -func TestManager_DeleteClass_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().DeleteClass("office").Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().ClassDelete("office").Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.DeleteClass("office")) -} - -func TestManager_RestartJob_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().RestartJob(1).Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().JobRestart(1).Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.RestartJob(1)) -} - -func TestManager_HoldJob_WithPkHelper(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().HoldJobUntil(1, "indefinite").Return(authErr()) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - mockPk := mocks_pkhelper.NewMockPkHelper(t) - mockPk.EXPECT().JobSetHoldUntil(1, "indefinite").Return(nil) - - m := cups.NewTestManager(mockClient, mockPk) - assert.NoError(t, m.HoldJob(1, "indefinite")) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/actions_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/actions_test.go deleted file mode 100644 index 70f4f21..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/actions_test.go +++ /dev/null @@ -1,450 +0,0 @@ -package cups - -import ( - "errors" - "testing" - "time" - - mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestManager_GetPrinters(t *testing.T) { - tests := []struct { - name string - mockRet map[string]ipp.Attributes - mockErr error - want int - wantErr bool - }{ - { - name: "success", - mockRet: map[string]ipp.Attributes{ - "printer1": { - ipp.AttributePrinterName: []ipp.Attribute{{Value: "printer1"}}, - ipp.AttributePrinterUriSupported: []ipp.Attribute{{Value: "ipp://localhost/printers/printer1"}}, - ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}}, - ipp.AttributePrinterStateReasons: []ipp.Attribute{{Value: "none"}}, - ipp.AttributePrinterLocation: []ipp.Attribute{{Value: "Office"}}, - ipp.AttributePrinterInfo: []ipp.Attribute{{Value: "Test Printer"}}, - ipp.AttributePrinterMakeAndModel: []ipp.Attribute{{Value: "Generic"}}, - ipp.AttributePrinterIsAcceptingJobs: []ipp.Attribute{{Value: true}}, - }, - }, - mockErr: nil, - want: 1, - wantErr: false, - }, - { - name: "error", - mockRet: nil, - mockErr: errors.New("test error"), - want: 0, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(tt.mockRet, tt.mockErr) - - m := &Manager{ - client: mockClient, - } - - got, err := m.GetPrinters() - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.want, len(got)) - if len(got) > 0 { - assert.Equal(t, "printer1", got[0].Name) - assert.Equal(t, "idle", got[0].State) - assert.Equal(t, "Office", got[0].Location) - assert.True(t, got[0].Accepting) - } - } - }) - } -} - -func TestManager_GetJobs(t *testing.T) { - tests := []struct { - name string - mockRet map[int]ipp.Attributes - mockErr error - want int - wantErr bool - }{ - { - name: "success", - mockRet: map[int]ipp.Attributes{ - 1: { - ipp.AttributeJobID: []ipp.Attribute{{Value: 1}}, - ipp.AttributeJobName: []ipp.Attribute{{Value: "test-job"}}, - ipp.AttributeJobState: []ipp.Attribute{{Value: 5}}, - ipp.AttributeJobPrinterURI: []ipp.Attribute{{Value: "ipp://localhost/printers/printer1"}}, - ipp.AttributeJobOriginatingUserName: []ipp.Attribute{{Value: "testuser"}}, - ipp.AttributeJobKilobyteOctets: []ipp.Attribute{{Value: 10}}, - "time-at-creation": []ipp.Attribute{{Value: 1609459200}}, - }, - }, - mockErr: nil, - want: 1, - wantErr: false, - }, - { - name: "error", - mockRet: nil, - mockErr: errors.New("test error"), - want: 0, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().GetJobs("printer1", "", "not-completed", false, 0, 0, mock.Anything). - Return(tt.mockRet, tt.mockErr) - - m := &Manager{ - client: mockClient, - } - - got, err := m.GetJobs("printer1", "not-completed") - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.want, len(got)) - if len(got) > 0 { - assert.Equal(t, 1, got[0].ID) - assert.Equal(t, "test-job", got[0].Name) - assert.Equal(t, "processing", got[0].State) - assert.Equal(t, "testuser", got[0].User) - assert.Equal(t, "printer1", got[0].Printer) - assert.Equal(t, 10240, got[0].Size) - assert.Equal(t, time.Unix(1609459200, 0), got[0].TimeCreated) - } - } - }) - } -} - -func TestManager_CancelJob(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().CancelJob(1, false).Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.CancelJob(1)) -} - -func TestManager_PausePrinter(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().PausePrinter("printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.PausePrinter("printer1")) -} - -func TestManager_ResumePrinter(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().ResumePrinter("printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.ResumePrinter("printer1")) -} - -func TestManager_PurgeJobs(t *testing.T) { - tests := []struct { - name string - mockErr error - wantErr bool - }{ - { - name: "success", - mockErr: nil, - wantErr: false, - }, - { - name: "error", - mockErr: errors.New("test error"), - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().CancelAllJob("printer1", true).Return(tt.mockErr) - if !tt.wantErr { - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - } - - m := NewTestManager(mockClient, nil) - - err := m.PurgeJobs("printer1") - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestManager_GetDevices(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().GetDevices().Return(map[string]ipp.Attributes{ - "usb://HP/LaserJet": { - "device-class": []ipp.Attribute{{Value: "direct"}}, - "device-info": []ipp.Attribute{{Value: "HP LaserJet"}}, - "device-make-and-model": []ipp.Attribute{{Value: "HP LaserJet 1020"}}, - }, - }, nil) - - m := &Manager{client: mockClient} - got, err := m.GetDevices() - assert.NoError(t, err) - assert.Len(t, got, 1) - assert.Equal(t, "usb://HP/LaserJet", got[0].URI) - assert.Equal(t, "direct", got[0].Class) -} - -func TestManager_GetPPDs(t *testing.T) { - tests := []struct { - name string - mockRet map[string]ipp.Attributes - mockErr error - want int - wantErr bool - }{ - { - name: "success", - mockRet: map[string]ipp.Attributes{ - "drv:///sample.drv/generic.ppd": { - "ppd-make-and-model": []ipp.Attribute{{Value: "Generic PostScript"}}, - "ppd-type": []ipp.Attribute{{Value: "ppd"}}, - }, - }, - mockErr: nil, - want: 1, - wantErr: false, - }, - { - name: "error", - mockRet: nil, - mockErr: errors.New("test error"), - want: 0, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().GetPPDs().Return(tt.mockRet, tt.mockErr) - - m := &Manager{client: mockClient} - - got, err := m.GetPPDs() - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Equal(t, tt.want, len(got)) - }) - } -} - -func TestManager_GetClasses(t *testing.T) { - tests := []struct { - name string - mockRet map[string]ipp.Attributes - mockErr error - want int - wantErr bool - }{ - { - name: "success", - mockRet: map[string]ipp.Attributes{ - "office": { - ipp.AttributePrinterName: []ipp.Attribute{{Value: "office"}}, - ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}}, - ipp.AttributeMemberNames: []ipp.Attribute{{Value: "printer1"}, {Value: "printer2"}}, - }, - }, - mockErr: nil, - want: 1, - wantErr: false, - }, - { - name: "error", - mockRet: nil, - mockErr: errors.New("test error"), - want: 0, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().GetClasses(mock.Anything).Return(tt.mockRet, tt.mockErr) - - m := &Manager{client: mockClient} - - got, err := m.GetClasses() - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Equal(t, tt.want, len(got)) - if len(got) > 0 { - assert.Equal(t, "office", got[0].Name) - assert.Equal(t, 2, len(got[0].Members)) - } - }) - } -} - -func TestManager_CreatePrinter(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().CreatePrinter("newprinter", "usb://HP", "generic.ppd", true, "stop-printer", "info", "location").Return(nil) - mockClient.EXPECT().ResumePrinter("newprinter").Return(nil) - mockClient.EXPECT().AcceptJobs("newprinter").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.CreatePrinter("newprinter", "usb://HP", "generic.ppd", true, "stop-printer", "info", "location")) -} - -func TestManager_DeletePrinter(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().DeletePrinter("printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.DeletePrinter("printer1")) -} - -func TestManager_AcceptJobs(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().AcceptJobs("printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.AcceptJobs("printer1")) -} - -func TestManager_RejectJobs(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().RejectJobs("printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.RejectJobs("printer1")) -} - -func TestManager_SetPrinterShared(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().SetPrinterIsShared("printer1", true).Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.SetPrinterShared("printer1", true)) -} - -func TestManager_SetPrinterLocation(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().SetPrinterLocation("printer1", "Office").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.SetPrinterLocation("printer1", "Office")) -} - -func TestManager_SetPrinterInfo(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().SetPrinterInformation("printer1", "Main Printer").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.SetPrinterInfo("printer1", "Main Printer")) -} - -func TestManager_MoveJob(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().MoveJob(1, "printer2").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - err := m.MoveJob(1, "printer2") - assert.NoError(t, err) -} - -func TestManager_PrintTestPage(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().PrintTestPage("printer1", mock.Anything, mock.Anything).Return(42, nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - jobID, err := m.PrintTestPage("printer1") - assert.NoError(t, err) - assert.Equal(t, 42, jobID) -} - -func TestManager_AddPrinterToClass(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().AddPrinterToClass("office", "printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.AddPrinterToClass("office", "printer1")) -} - -func TestManager_RemovePrinterFromClass(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().DeletePrinterFromClass("office", "printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.RemovePrinterFromClass("office", "printer1")) -} - -func TestManager_DeleteClass(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().DeleteClass("office").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.DeleteClass("office")) -} - -func TestManager_RestartJob(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().RestartJob(1).Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.RestartJob(1)) -} - -func TestManager_HoldJob(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().HoldJobUntil(1, "indefinite").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - assert.NoError(t, m.HoldJob(1, "indefinite")) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/handlers.go deleted file mode 100644 index 57b45d5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/handlers.go +++ /dev/null @@ -1,487 +0,0 @@ -package cups - -import ( - "encoding/json" - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -type CUPSEvent struct { - Type string `json:"type"` - Data CUPSState `json:"data"` -} - -type TestPageResult struct { - Success bool `json:"success"` - JobID int `json:"jobId"` - Message string `json:"message"` -} - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - switch req.Method { - case "cups.subscribe": - handleSubscribe(conn, req, manager) - case "cups.getPrinters": - handleGetPrinters(conn, req, manager) - case "cups.getJobs": - handleGetJobs(conn, req, manager) - case "cups.pausePrinter": - handlePausePrinter(conn, req, manager) - case "cups.resumePrinter": - handleResumePrinter(conn, req, manager) - case "cups.cancelJob": - handleCancelJob(conn, req, manager) - case "cups.purgeJobs": - handlePurgeJobs(conn, req, manager) - case "cups.getDevices": - handleGetDevices(conn, req, manager) - case "cups.getPPDs": - handleGetPPDs(conn, req, manager) - case "cups.getClasses": - handleGetClasses(conn, req, manager) - case "cups.createPrinter": - handleCreatePrinter(conn, req, manager) - case "cups.deletePrinter": - handleDeletePrinter(conn, req, manager) - case "cups.acceptJobs": - handleAcceptJobs(conn, req, manager) - case "cups.rejectJobs": - handleRejectJobs(conn, req, manager) - case "cups.setPrinterShared": - handleSetPrinterShared(conn, req, manager) - case "cups.setPrinterLocation": - handleSetPrinterLocation(conn, req, manager) - case "cups.setPrinterInfo": - handleSetPrinterInfo(conn, req, manager) - case "cups.moveJob": - handleMoveJob(conn, req, manager) - case "cups.printTestPage": - handlePrintTestPage(conn, req, manager) - case "cups.addPrinterToClass": - handleAddPrinterToClass(conn, req, manager) - case "cups.removePrinterFromClass": - handleRemovePrinterFromClass(conn, req, manager) - case "cups.deleteClass": - handleDeleteClass(conn, req, manager) - case "cups.restartJob": - handleRestartJob(conn, req, manager) - case "cups.holdJob": - handleHoldJob(conn, req, manager) - case "cups.testConnection": - handleTestConnection(conn, req, manager) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleGetPrinters(conn net.Conn, req models.Request, manager *Manager) { - printers, err := manager.GetPrinters() - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, printers) -} - -func handleGetJobs(conn net.Conn, req models.Request, manager *Manager) { - printerName, err := params.String(req.Params, "printerName") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - jobs, err := manager.GetJobs(printerName, "not-completed") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, jobs) -} - -func handlePausePrinter(conn net.Conn, req models.Request, manager *Manager) { - printerName, err := params.String(req.Params, "printerName") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.PausePrinter(printerName); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paused"}) -} - -func handleResumePrinter(conn net.Conn, req models.Request, manager *Manager) { - printerName, err := params.String(req.Params, "printerName") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.ResumePrinter(printerName); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "resumed"}) -} - -func handleCancelJob(conn net.Conn, req models.Request, manager *Manager) { - jobID, err := params.Int(req.Params, "jobID") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.CancelJob(jobID); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job canceled"}) -} - -func handlePurgeJobs(conn net.Conn, req models.Request, manager *Manager) { - printerName, err := params.String(req.Params, "printerName") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.PurgeJobs(printerName); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "jobs canceled"}) -} - -func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) { - clientID := fmt.Sprintf("client-%p", conn) - stateChan := manager.Subscribe(clientID) - defer manager.Unsubscribe(clientID) - - initialState := manager.GetState() - event := CUPSEvent{ - Type: "state_changed", - Data: initialState, - } - - if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{ - ID: req.ID, - Result: &event, - }); err != nil { - return - } - - for state := range stateChan { - event := CUPSEvent{ - Type: "state_changed", - Data: state, - } - if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{ - Result: &event, - }); err != nil { - return - } - } -} - -func handleGetDevices(conn net.Conn, req models.Request, manager *Manager) { - devices, err := manager.GetDevices() - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, devices) -} - -func handleGetPPDs(conn net.Conn, req models.Request, manager *Manager) { - ppds, err := manager.GetPPDs() - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, ppds) -} - -func handleGetClasses(conn net.Conn, req models.Request, manager *Manager) { - classes, err := manager.GetClasses() - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, classes) -} - -func handleCreatePrinter(conn net.Conn, req models.Request, manager *Manager) { - name, err := params.StringNonEmpty(req.Params, "name") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - deviceURI, err := params.StringNonEmpty(req.Params, "deviceURI") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - ppd, err := params.StringNonEmpty(req.Params, "ppd") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - shared := params.BoolOpt(req.Params, "shared", false) - errorPolicy := params.StringOpt(req.Params, "errorPolicy", "") - information := params.StringOpt(req.Params, "information", "") - location := params.StringOpt(req.Params, "location", "") - - if err := manager.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer created"}) -} - -func handleDeletePrinter(conn net.Conn, req models.Request, manager *Manager) { - printerName, err := params.StringNonEmpty(req.Params, "printerName") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.DeletePrinter(printerName); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer deleted"}) -} - -func handleAcceptJobs(conn net.Conn, req models.Request, manager *Manager) { - printerName, err := params.StringNonEmpty(req.Params, "printerName") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.AcceptJobs(printerName); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "accepting jobs"}) -} - -func handleRejectJobs(conn net.Conn, req models.Request, manager *Manager) { - printerName, err := params.StringNonEmpty(req.Params, "printerName") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.RejectJobs(printerName); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "rejecting jobs"}) -} - -func handleSetPrinterShared(conn net.Conn, req models.Request, manager *Manager) { - printerName, err := params.StringNonEmpty(req.Params, "printerName") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - shared, err := params.Bool(req.Params, "shared") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetPrinterShared(printerName, shared); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sharing updated"}) -} - -func handleSetPrinterLocation(conn net.Conn, req models.Request, manager *Manager) { - printerName, err := params.StringNonEmpty(req.Params, "printerName") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - location, err := params.String(req.Params, "location") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetPrinterLocation(printerName, location); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location updated"}) -} - -func handleSetPrinterInfo(conn net.Conn, req models.Request, manager *Manager) { - printerName, err := params.StringNonEmpty(req.Params, "printerName") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - info, err := params.String(req.Params, "info") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetPrinterInfo(printerName, info); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "info updated"}) -} - -func handleMoveJob(conn net.Conn, req models.Request, manager *Manager) { - jobID, err := params.Int(req.Params, "jobID") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - destPrinter, err := params.StringNonEmpty(req.Params, "destPrinter") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.MoveJob(jobID, destPrinter); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job moved"}) -} - -func handlePrintTestPage(conn net.Conn, req models.Request, manager *Manager) { - printerName, err := params.StringNonEmpty(req.Params, "printerName") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - jobID, err := manager.PrintTestPage(printerName) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, TestPageResult{Success: true, JobID: jobID, Message: "test page queued"}) -} - -func handleAddPrinterToClass(conn net.Conn, req models.Request, manager *Manager) { - className, err := params.StringNonEmpty(req.Params, "className") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - printerName, err := params.StringNonEmpty(req.Params, "printerName") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.AddPrinterToClass(className, printerName); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer added to class"}) -} - -func handleRemovePrinterFromClass(conn net.Conn, req models.Request, manager *Manager) { - className, err := params.StringNonEmpty(req.Params, "className") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - printerName, err := params.StringNonEmpty(req.Params, "printerName") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.RemovePrinterFromClass(className, printerName); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer removed from class"}) -} - -func handleDeleteClass(conn net.Conn, req models.Request, manager *Manager) { - className, err := params.StringNonEmpty(req.Params, "className") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.DeleteClass(className); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "class deleted"}) -} - -func handleRestartJob(conn net.Conn, req models.Request, manager *Manager) { - jobID, err := params.Int(req.Params, "jobID") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.RestartJob(jobID); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job restarted"}) -} - -func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) { - jobID, err := params.Int(req.Params, "jobID") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - holdUntil := params.StringOpt(req.Params, "holdUntil", "indefinite") - - if err := manager.HoldJob(jobID, holdUntil); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"}) -} - -func handleTestConnection(conn net.Conn, req models.Request, manager *Manager) { - host, err := params.StringNonEmpty(req.Params, "host") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - port := params.IntOpt(req.Params, "port", 631) - protocol := params.StringOpt(req.Params, "protocol", "ipp") - - result, err := manager.TestRemotePrinter(host, port, protocol) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, result) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/handlers_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/handlers_test.go deleted file mode 100644 index b73931e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/handlers_test.go +++ /dev/null @@ -1,711 +0,0 @@ -package cups - -import ( - "bytes" - "encoding/json" - "errors" - "net" - "testing" - "time" - - mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -type mockConn struct { - *bytes.Buffer -} - -func (m *mockConn) Close() error { return nil } -func (m *mockConn) LocalAddr() net.Addr { return nil } -func (m *mockConn) RemoteAddr() net.Addr { return nil } -func (m *mockConn) SetDeadline(t time.Time) error { return nil } -func (m *mockConn) SetReadDeadline(t time.Time) error { return nil } -func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil } - -func TestHandleGetPrinters(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{ - "printer1": { - ipp.AttributePrinterName: []ipp.Attribute{{Value: "printer1"}}, - ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}}, - ipp.AttributePrinterUriSupported: []ipp.Attribute{{Value: "ipp://localhost/printers/printer1"}}, - }, - }, nil) - - m := &Manager{ - client: mockClient, - } - - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.getPrinters", - } - - handleGetPrinters(conn, req, m) - - var resp models.Response[[]Printer] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.Equal(t, 1, len(*resp.Result)) -} - -func TestHandleGetPrinters_Error(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(nil, errors.New("test error")) - - m := &Manager{ - client: mockClient, - } - - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.getPrinters", - } - - handleGetPrinters(conn, req, m) - - var resp models.Response[any] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.Nil(t, resp.Result) - assert.NotNil(t, resp.Error) -} - -func TestHandleGetJobs(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().GetJobs("printer1", "", "not-completed", false, 0, 0, mock.Anything). - Return(map[int]ipp.Attributes{ - 1: { - ipp.AttributeJobID: []ipp.Attribute{{Value: 1}}, - ipp.AttributeJobName: []ipp.Attribute{{Value: "job1"}}, - ipp.AttributeJobState: []ipp.Attribute{{Value: 5}}, - }, - }, nil) - - m := &Manager{ - client: mockClient, - } - - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.getJobs", - Params: map[string]any{ - "printerName": "printer1", - }, - } - - handleGetJobs(conn, req, m) - - var resp models.Response[[]Job] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.Equal(t, 1, len(*resp.Result)) -} - -func TestHandleGetJobs_MissingParam(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - - m := &Manager{ - client: mockClient, - } - - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.getJobs", - Params: map[string]any{}, - } - - handleGetJobs(conn, req, m) - - var resp models.Response[any] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.Nil(t, resp.Result) - assert.NotNil(t, resp.Error) -} - -func TestHandlePausePrinter(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().PausePrinter("printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.pausePrinter", - Params: map[string]any{ - "printerName": "printer1", - }, - } - - handlePausePrinter(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleResumePrinter(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().ResumePrinter("printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.resumePrinter", - Params: map[string]any{ - "printerName": "printer1", - }, - } - - handleResumePrinter(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleCancelJob(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().CancelJob(1, false).Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.cancelJob", - Params: map[string]any{ - "jobID": float64(1), - }, - } - - handleCancelJob(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandlePurgeJobs(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().CancelAllJob("printer1", true).Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.purgeJobs", - Params: map[string]any{ - "printerName": "printer1", - }, - } - - handlePurgeJobs(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleRequest_UnknownMethod(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - - m := &Manager{ - client: mockClient, - } - - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.unknownMethod", - } - - HandleRequest(conn, req, m) - - var resp models.Response[any] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.Nil(t, resp.Result) - assert.NotNil(t, resp.Error) -} - -func TestHandleGetDevices(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().GetDevices().Return(map[string]ipp.Attributes{ - "usb://HP/LaserJet": { - "device-class": []ipp.Attribute{{Value: "direct"}}, - "device-info": []ipp.Attribute{{Value: "HP LaserJet"}}, - }, - }, nil) - - m := &Manager{client: mockClient} - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ID: 1, Method: "cups.getDevices"} - handleGetDevices(conn, req, m) - - var resp models.Response[[]Device] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.Equal(t, 1, len(*resp.Result)) -} - -func TestHandleGetPPDs(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().GetPPDs().Return(map[string]ipp.Attributes{ - "generic.ppd": { - "ppd-make-and-model": []ipp.Attribute{{Value: "Generic"}}, - }, - }, nil) - - m := &Manager{client: mockClient} - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ID: 1, Method: "cups.getPPDs"} - handleGetPPDs(conn, req, m) - - var resp models.Response[[]PPD] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.Equal(t, 1, len(*resp.Result)) -} - -func TestHandleGetClasses(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().GetClasses(mock.Anything).Return(map[string]ipp.Attributes{ - "office": { - ipp.AttributePrinterName: []ipp.Attribute{{Value: "office"}}, - ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}}, - }, - }, nil) - - m := &Manager{client: mockClient} - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ID: 1, Method: "cups.getClasses"} - handleGetClasses(conn, req, m) - - var resp models.Response[[]PrinterClass] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.Equal(t, 1, len(*resp.Result)) -} - -func TestHandleCreatePrinter(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().CreatePrinter("newprinter", "usb://HP", "generic.ppd", false, "", "", "").Return(nil) - mockClient.EXPECT().ResumePrinter("newprinter").Return(nil) - mockClient.EXPECT().AcceptJobs("newprinter").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.createPrinter", - Params: map[string]any{ - "name": "newprinter", - "deviceURI": "usb://HP", - "ppd": "generic.ppd", - }, - } - handleCreatePrinter(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleCreatePrinter_MissingParams(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - m := &Manager{client: mockClient} - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ID: 1, Method: "cups.createPrinter", Params: map[string]any{}} - handleCreatePrinter(conn, req, m) - - var resp models.Response[any] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.Nil(t, resp.Result) - assert.NotNil(t, resp.Error) -} - -func TestHandleDeletePrinter(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().DeletePrinter("printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.deletePrinter", - Params: map[string]any{"printerName": "printer1"}, - } - handleDeletePrinter(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleAcceptJobs(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().AcceptJobs("printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.acceptJobs", - Params: map[string]any{"printerName": "printer1"}, - } - handleAcceptJobs(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleRejectJobs(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().RejectJobs("printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.rejectJobs", - Params: map[string]any{"printerName": "printer1"}, - } - handleRejectJobs(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleSetPrinterShared(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().SetPrinterIsShared("printer1", true).Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.setPrinterShared", - Params: map[string]any{"printerName": "printer1", "shared": true}, - } - handleSetPrinterShared(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleSetPrinterLocation(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().SetPrinterLocation("printer1", "Office").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.setPrinterLocation", - Params: map[string]any{"printerName": "printer1", "location": "Office"}, - } - handleSetPrinterLocation(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleSetPrinterInfo(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().SetPrinterInformation("printer1", "Main Printer").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.setPrinterInfo", - Params: map[string]any{"printerName": "printer1", "info": "Main Printer"}, - } - handleSetPrinterInfo(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleMoveJob(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().MoveJob(1, "printer2").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.moveJob", - Params: map[string]any{"jobID": float64(1), "destPrinter": "printer2"}, - } - handleMoveJob(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandlePrintTestPage(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().PrintTestPage("printer1", mock.Anything, mock.Anything).Return(42, nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.printTestPage", - Params: map[string]any{"printerName": "printer1"}, - } - handlePrintTestPage(conn, req, m) - - var resp models.Response[TestPageResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, 42, resp.Result.JobID) -} - -func TestHandleAddPrinterToClass(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().AddPrinterToClass("office", "printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.addPrinterToClass", - Params: map[string]any{"className": "office", "printerName": "printer1"}, - } - handleAddPrinterToClass(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleRemovePrinterFromClass(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().DeletePrinterFromClass("office", "printer1").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.removePrinterFromClass", - Params: map[string]any{"className": "office", "printerName": "printer1"}, - } - handleRemovePrinterFromClass(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleDeleteClass(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().DeleteClass("office").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.deleteClass", - Params: map[string]any{"className": "office"}, - } - handleDeleteClass(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleRestartJob(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().RestartJob(1).Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.restartJob", - Params: map[string]any{"jobID": float64(1)}, - } - handleRestartJob(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleHoldJob(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().HoldJobUntil(1, "indefinite").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.holdJob", - Params: map[string]any{"jobID": float64(1)}, - } - handleHoldJob(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} - -func TestHandleHoldJob_WithHoldUntil(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().HoldJobUntil(1, "no-hold").Return(nil) - mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - - m := NewTestManager(mockClient, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.holdJob", - Params: map[string]any{"jobID": float64(1), "holdUntil": "no-hold"}, - } - handleHoldJob(conn, req, m) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/manager.go deleted file mode 100644 index e5b7e8c..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/manager.go +++ /dev/null @@ -1,390 +0,0 @@ -package cups - -import ( - "errors" - "fmt" - "os" - "strconv" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" -) - -func NewManager() (*Manager, error) { - host := os.Getenv("DMS_IPP_HOST") - if host == "" { - host = "localhost" - } - - portStr := os.Getenv("DMS_IPP_PORT") - port := 631 - if portStr != "" { - if p, err := strconv.Atoi(portStr); err == nil { - port = p - } - } - - username := os.Getenv("DMS_IPP_USERNAME") - password := os.Getenv("DMS_IPP_PASSWORD") - - client := ipp.NewCUPSClient(host, port, username, password, false) - baseURL := fmt.Sprintf("http://%s:%d", host, port) - - var pkHelper PkHelper - if isLocalCUPS(host) { - var err error - pkHelper, err = NewPkHelper() - if err != nil { - log.Warnf("[CUPS] Failed to initialize pkhelper: %v", err) - } - } - - m := &Manager{ - state: &CUPSState{ - Printers: make(map[string]*Printer), - }, - client: client, - pkHelper: pkHelper, - baseURL: baseURL, - stateMutex: sync.RWMutex{}, - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - - if err := m.updateState(); err != nil { - return nil, err - } - - if isLocalCUPS(host) { - m.subscription = NewDBusSubscriptionManager(client, baseURL) - log.Infof("[CUPS] Using D-Bus notifications for local CUPS") - } else { - m.subscription = NewSubscriptionManager(client, baseURL) - log.Infof("[CUPS] Using IPPGET notifications for remote CUPS") - } - - m.notifierWg.Add(1) - go m.notifier() - - return m, nil -} - -func isLocalCUPS(host string) bool { - switch host { - case "localhost", "127.0.0.1", "::1", "": - return true - } - return false -} - -func (m *Manager) eventHandler() { - defer m.eventWG.Done() - - if m.subscription == nil { - return - } - - for { - select { - case <-m.stopChan: - return - case event, ok := <-m.subscription.Events(): - if !ok { - return - } - log.Debugf("[CUPS] Received event: %s (printer: %s, job: %d)", - event.EventName, event.PrinterName, event.JobID) - - if err := m.updateState(); err != nil { - log.Warnf("[CUPS] Failed to update state after event: %v", err) - } else { - m.notifySubscribers() - } - } - } -} - -func (m *Manager) updateState() error { - printers, err := m.GetPrinters() - if err != nil { - if isNoPrintersError(err) { - m.stateMutex.Lock() - m.state.Printers = make(map[string]*Printer) - m.stateMutex.Unlock() - return nil - } - return err - } - - printerMap := make(map[string]*Printer, len(printers)) - for _, printer := range printers { - jobs, err := m.GetJobs(printer.Name, "not-completed") - if err != nil { - return err - } - - printer.Jobs = jobs - printerMap[printer.Name] = &printer - } - - m.stateMutex.Lock() - m.state.Printers = printerMap - m.stateMutex.Unlock() - - return nil -} - -func isNoPrintersError(err error) bool { - if err == nil { - return false - } - - var ippErr ipp.IPPError - if errors.As(err, &ippErr) { - return ippErr.Status == 1030 - } - - return false -} - -func (m *Manager) notifier() { - defer m.notifierWg.Done() - const minGap = 100 * time.Millisecond - timer := time.NewTimer(minGap) - timer.Stop() - var pending bool - for { - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.dirty: - if pending { - continue - } - pending = true - timer.Reset(minGap) - case <-timer.C: - if !pending { - continue - } - - currentState := m.snapshotState() - - if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, ¤tState) { - pending = false - continue - } - - m.subscribers.Range(func(key string, ch chan CUPSState) bool { - select { - case ch <- currentState: - default: - } - return true - }) - - stateCopy := currentState - m.lastNotifiedState = &stateCopy - pending = false - } - } -} - -func (m *Manager) notifySubscribers() { - select { - case m.dirty <- struct{}{}: - default: - } -} - -func (m *Manager) RefreshState() { - if err := m.updateState(); err != nil { - log.Warnf("[CUPS] Failed to refresh state: %v", err) - return - } - m.notifySubscribers() -} - -func (m *Manager) GetState() CUPSState { - return m.snapshotState() -} - -func (m *Manager) snapshotState() CUPSState { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - - s := CUPSState{ - Printers: make(map[string]*Printer, len(m.state.Printers)), - } - for name, printer := range m.state.Printers { - printerCopy := *printer - s.Printers[name] = &printerCopy - } - return s -} - -func (m *Manager) Subscribe(id string) chan CUPSState { - ch := make(chan CUPSState, 64) - - wasEmpty := true - m.subscribers.Range(func(key string, ch chan CUPSState) bool { - wasEmpty = false - return false - }) - - m.subscribers.Store(id, ch) - - if wasEmpty && m.subscription != nil { - if err := m.subscription.Start(); err != nil { - log.Warnf("[CUPS] Failed to start subscription manager: %v", err) - } else { - m.eventWG.Add(1) - go m.eventHandler() - } - } - - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - } - - isEmpty := true - m.subscribers.Range(func(key string, ch chan CUPSState) bool { - isEmpty = false - return false - }) - - if isEmpty && m.subscription != nil { - m.subscription.Stop() - m.eventWG.Wait() - } -} - -func (m *Manager) Close() { - close(m.stopChan) - - if m.subscription != nil { - m.subscription.Stop() - } - - m.eventWG.Wait() - m.notifierWg.Wait() - - m.subscribers.Range(func(key string, ch chan CUPSState) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) -} - -func stateChanged(old, new *CUPSState) bool { - if len(old.Printers) != len(new.Printers) { - return true - } - for name, oldPrinter := range old.Printers { - newPrinter, exists := new.Printers[name] - if !exists { - return true - } - if oldPrinter.State != newPrinter.State || - oldPrinter.StateReason != newPrinter.StateReason || - oldPrinter.Accepting != newPrinter.Accepting || - len(oldPrinter.Jobs) != len(newPrinter.Jobs) { - return true - } - } - return false -} - -func parsePrinterState(attrs ipp.Attributes) string { - if stateAttr, ok := attrs[ipp.AttributePrinterState]; ok && len(stateAttr) > 0 { - if state, ok := stateAttr[0].Value.(int); ok { - switch state { - case 3: - return "idle" - case 4: - return "processing" - case 5: - return "stopped" - default: - return fmt.Sprintf("%d", state) - } - } - } - return "unknown" -} - -func parseJobState(attrs ipp.Attributes) string { - if stateAttr, ok := attrs[ipp.AttributeJobState]; ok && len(stateAttr) > 0 { - if state, ok := stateAttr[0].Value.(int); ok { - switch state { - case 3: - return "pending" - case 4: - return "pending-held" - case 5: - return "processing" - case 6: - return "processing-stopped" - case 7: - return "canceled" - case 8: - return "aborted" - case 9: - return "completed" - default: - return fmt.Sprintf("%d", state) - } - } - } - return "unknown" -} - -func getStringAttr(attrs ipp.Attributes, key string) string { - if attr, ok := attrs[key]; ok && len(attr) > 0 { - if val, ok := attr[0].Value.(string); ok { - return val - } - return fmt.Sprintf("%v", attr[0].Value) - } - return "" -} - -func getIntAttr(attrs ipp.Attributes, key string) int { - if attr, ok := attrs[key]; ok && len(attr) > 0 { - if val, ok := attr[0].Value.(int); ok { - return val - } - } - return 0 -} - -func getBoolAttr(attrs ipp.Attributes, key string) bool { - if attr, ok := attrs[key]; ok && len(attr) > 0 { - if val, ok := attr[0].Value.(bool); ok { - return val - } - } - return false -} - -func getStringSliceAttr(attrs ipp.Attributes, key string) []string { - attr, ok := attrs[key] - if !ok { - return nil - } - - result := make([]string, 0, len(attr)) - for _, a := range attr { - if val, ok := a.Value.(string); ok { - result = append(result, val) - } - } - return result -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/manager_test.go deleted file mode 100644 index 7f09e67..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/manager_test.go +++ /dev/null @@ -1,363 +0,0 @@ -package cups - -import ( - "testing" - - mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" - "github.com/stretchr/testify/assert" -) - -func TestNewManager(t *testing.T) { - m := &Manager{ - state: &CUPSState{ - Printers: make(map[string]*Printer), - }, - client: nil, - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - - assert.NotNil(t, m) - assert.NotNil(t, m.state) -} - -func TestManager_GetState(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - - m := &Manager{ - state: &CUPSState{ - Printers: map[string]*Printer{ - "test-printer": { - Name: "test-printer", - State: "idle", - }, - }, - }, - client: mockClient, - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - - state := m.GetState() - assert.Equal(t, 1, len(state.Printers)) - assert.Equal(t, "test-printer", state.Printers["test-printer"].Name) -} - -func TestManager_Subscribe(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - - m := &Manager{ - state: &CUPSState{ - Printers: make(map[string]*Printer), - }, - client: mockClient, - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - - ch := m.Subscribe("test-client") - assert.NotNil(t, ch) - - count := 0 - m.subscribers.Range(func(key string, ch chan CUPSState) bool { - count++ - return true - }) - assert.Equal(t, 1, count) - - m.Unsubscribe("test-client") - count = 0 - m.subscribers.Range(func(key string, ch chan CUPSState) bool { - count++ - return true - }) - assert.Equal(t, 0, count) -} - -func TestManager_Close(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - - m := &Manager{ - state: &CUPSState{ - Printers: make(map[string]*Printer), - }, - client: mockClient, - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - - m.eventWG.Add(1) - go func() { - defer m.eventWG.Done() - <-m.stopChan - }() - - m.notifierWg.Add(1) - go func() { - defer m.notifierWg.Done() - <-m.stopChan - }() - - m.Close() - count := 0 - m.subscribers.Range(func(key string, ch chan CUPSState) bool { - count++ - return true - }) - assert.Equal(t, 0, count) -} - -func TestStateChanged(t *testing.T) { - tests := []struct { - name string - oldState *CUPSState - newState *CUPSState - want bool - }{ - { - name: "no change", - oldState: &CUPSState{ - Printers: map[string]*Printer{ - "p1": {Name: "p1", State: "idle"}, - }, - }, - newState: &CUPSState{ - Printers: map[string]*Printer{ - "p1": {Name: "p1", State: "idle"}, - }, - }, - want: false, - }, - { - name: "state changed", - oldState: &CUPSState{ - Printers: map[string]*Printer{ - "p1": {Name: "p1", State: "idle"}, - }, - }, - newState: &CUPSState{ - Printers: map[string]*Printer{ - "p1": {Name: "p1", State: "processing"}, - }, - }, - want: true, - }, - { - name: "printer added", - oldState: &CUPSState{ - Printers: map[string]*Printer{}, - }, - newState: &CUPSState{ - Printers: map[string]*Printer{ - "p1": {Name: "p1", State: "idle"}, - }, - }, - want: true, - }, - { - name: "printer removed", - oldState: &CUPSState{ - Printers: map[string]*Printer{ - "p1": {Name: "p1", State: "idle"}, - }, - }, - newState: &CUPSState{ - Printers: map[string]*Printer{}, - }, - want: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := stateChanged(tt.oldState, tt.newState) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestParsePrinterState(t *testing.T) { - tests := []struct { - name string - attrs ipp.Attributes - want string - }{ - { - name: "idle", - attrs: ipp.Attributes{ - ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}}, - }, - want: "idle", - }, - { - name: "processing", - attrs: ipp.Attributes{ - ipp.AttributePrinterState: []ipp.Attribute{{Value: 4}}, - }, - want: "processing", - }, - { - name: "stopped", - attrs: ipp.Attributes{ - ipp.AttributePrinterState: []ipp.Attribute{{Value: 5}}, - }, - want: "stopped", - }, - { - name: "unknown", - attrs: ipp.Attributes{}, - want: "unknown", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := parsePrinterState(tt.attrs) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestParseJobState(t *testing.T) { - tests := []struct { - name string - attrs ipp.Attributes - want string - }{ - { - name: "pending", - attrs: ipp.Attributes{ - ipp.AttributeJobState: []ipp.Attribute{{Value: 3}}, - }, - want: "pending", - }, - { - name: "processing", - attrs: ipp.Attributes{ - ipp.AttributeJobState: []ipp.Attribute{{Value: 5}}, - }, - want: "processing", - }, - { - name: "completed", - attrs: ipp.Attributes{ - ipp.AttributeJobState: []ipp.Attribute{{Value: 9}}, - }, - want: "completed", - }, - { - name: "unknown", - attrs: ipp.Attributes{}, - want: "unknown", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := parseJobState(tt.attrs) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestGetStringAttr(t *testing.T) { - tests := []struct { - name string - attrs ipp.Attributes - key string - want string - }{ - { - name: "string value", - attrs: ipp.Attributes{ - "test-key": []ipp.Attribute{{Value: "test-value"}}, - }, - key: "test-key", - want: "test-value", - }, - { - name: "missing key", - attrs: ipp.Attributes{}, - key: "missing", - want: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := getStringAttr(tt.attrs, tt.key) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestGetIntAttr(t *testing.T) { - tests := []struct { - name string - attrs ipp.Attributes - key string - want int - }{ - { - name: "int value", - attrs: ipp.Attributes{ - "test-key": []ipp.Attribute{{Value: 42}}, - }, - key: "test-key", - want: 42, - }, - { - name: "missing key", - attrs: ipp.Attributes{}, - key: "missing", - want: 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := getIntAttr(tt.attrs, tt.key) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestGetBoolAttr(t *testing.T) { - tests := []struct { - name string - attrs ipp.Attributes - key string - want bool - }{ - { - name: "true value", - attrs: ipp.Attributes{ - "test-key": []ipp.Attribute{{Value: true}}, - }, - key: "test-key", - want: true, - }, - { - name: "false value", - attrs: ipp.Attributes{ - "test-key": []ipp.Attribute{{Value: false}}, - }, - key: "test-key", - want: false, - }, - { - name: "missing key", - attrs: ipp.Attributes{}, - key: "missing", - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := getBoolAttr(tt.attrs, tt.key) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/pkhelper.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/pkhelper.go deleted file mode 100644 index 81dd458..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/pkhelper.go +++ /dev/null @@ -1,184 +0,0 @@ -package cups - -import ( - "fmt" - "strings" - - "github.com/godbus/dbus/v5" -) - -const ( - pkHelperDest = "org.opensuse.CupsPkHelper.Mechanism" - pkHelperPath = "/" - pkHelperInterface = "org.opensuse.CupsPkHelper.Mechanism" -) - -type PkHelper interface { - DevicesGet(timeout, limit int, includeSchemes, excludeSchemes []string) ([]Device, error) - PrinterAdd(name, uri, ppd, info, location string) error - PrinterDelete(name string) error - PrinterSetEnabled(name string, enabled bool) error - PrinterSetAcceptJobs(name string, enabled bool, reason string) error - PrinterSetInfo(name, info string) error - PrinterSetLocation(name, location string) error - PrinterSetShared(name string, shared bool) error - ClassAddPrinter(className, printerName string) error - ClassDeletePrinter(className, printerName string) error - ClassDelete(className string) error - JobCancelPurge(jobID int, purge bool) error - JobRestart(jobID int) error - JobSetHoldUntil(jobID int, holdUntil string) error -} - -type DBusPkHelper struct { - conn *dbus.Conn - obj dbus.BusObject -} - -func NewPkHelper() (*DBusPkHelper, error) { - conn, err := dbus.SystemBus() - if err != nil { - return nil, fmt.Errorf("failed to connect to system bus: %w", err) - } - - return &DBusPkHelper{ - conn: conn, - obj: conn.Object(pkHelperDest, pkHelperPath), - }, nil -} - -func (p *DBusPkHelper) DevicesGet(timeout, limit int, includeSchemes, excludeSchemes []string) ([]Device, error) { - if includeSchemes == nil { - includeSchemes = []string{} - } - if excludeSchemes == nil { - excludeSchemes = []string{} - } - - var errStr string - var devicesMap map[string]string - - call := p.obj.Call(pkHelperInterface+".DevicesGet", 0, int32(timeout), int32(limit), includeSchemes, excludeSchemes) - if call.Err != nil { - return nil, call.Err - } - if err := call.Store(&errStr, &devicesMap); err != nil { - return nil, err - } - if errStr != "" { - return nil, fmt.Errorf("%s", errStr) - } - - return parseDevicesMap(devicesMap), nil -} - -func parseDevicesMap(devicesMap map[string]string) []Device { - devicesByIndex := make(map[string]*Device) - - for key, value := range devicesMap { - idx := strings.LastIndex(key, ":") - if idx == -1 { - continue - } - - attr := key[:idx] - index := key[idx+1:] - - dev, ok := devicesByIndex[index] - if !ok { - dev = &Device{} - devicesByIndex[index] = dev - } - - switch attr { - case "device-uri": - dev.URI = value - case "device-class": - dev.Class = value - case "device-info": - dev.Info = value - case "device-make-and-model": - dev.MakeModel = value - case "device-id": - dev.ID = value - case "device-location": - dev.Location = value - } - } - - devices := make([]Device, 0, len(devicesByIndex)) - for _, dev := range devicesByIndex { - if dev.URI != "" { - devices = append(devices, *dev) - } - } - return devices -} - -func (p *DBusPkHelper) PrinterAdd(name, uri, ppd, info, location string) error { - return p.callSimple("PrinterAdd", name, uri, ppd, info, location) -} - -func (p *DBusPkHelper) PrinterDelete(name string) error { - return p.callSimple("PrinterDelete", name) -} - -func (p *DBusPkHelper) PrinterSetEnabled(name string, enabled bool) error { - return p.callSimple("PrinterSetEnabled", name, enabled) -} - -func (p *DBusPkHelper) PrinterSetAcceptJobs(name string, enabled bool, reason string) error { - return p.callSimple("PrinterSetAcceptJobs", name, enabled, reason) -} - -func (p *DBusPkHelper) PrinterSetInfo(name, info string) error { - return p.callSimple("PrinterSetInfo", name, info) -} - -func (p *DBusPkHelper) PrinterSetLocation(name, location string) error { - return p.callSimple("PrinterSetLocation", name, location) -} - -func (p *DBusPkHelper) PrinterSetShared(name string, shared bool) error { - return p.callSimple("PrinterSetShared", name, shared) -} - -func (p *DBusPkHelper) ClassAddPrinter(className, printerName string) error { - return p.callSimple("ClassAddPrinter", className, printerName) -} - -func (p *DBusPkHelper) ClassDeletePrinter(className, printerName string) error { - return p.callSimple("ClassDeletePrinter", className, printerName) -} - -func (p *DBusPkHelper) ClassDelete(className string) error { - return p.callSimple("ClassDelete", className) -} - -func (p *DBusPkHelper) JobCancelPurge(jobID int, purge bool) error { - return p.callSimple("JobCancelPurge", int32(jobID), purge) -} - -func (p *DBusPkHelper) JobRestart(jobID int) error { - return p.callSimple("JobRestart", int32(jobID)) -} - -func (p *DBusPkHelper) JobSetHoldUntil(jobID int, holdUntil string) error { - return p.callSimple("JobSetHoldUntil", int32(jobID), holdUntil) -} - -func (p *DBusPkHelper) callSimple(method string, args ...any) error { - var errStr string - - call := p.obj.Call(pkHelperInterface+"."+method, 0, args...) - if call.Err != nil { - return call.Err - } - if err := call.Store(&errStr); err != nil { - return err - } - if errStr != "" { - return fmt.Errorf("%s", errStr) - } - return nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/pkhelper_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/pkhelper_test.go deleted file mode 100644 index 596f1d0..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/pkhelper_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package cups - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParseDevicesMap(t *testing.T) { - tests := []struct { - name string - input map[string]string - wantLen int - wantURIs []string - }{ - { - name: "empty", - input: map[string]string{}, - wantLen: 0, - wantURIs: nil, - }, - { - name: "single_device", - input: map[string]string{ - "device-uri:0": "usb://HP/LaserJet", - "device-class:0": "direct", - "device-info:0": "HP LaserJet", - "device-make-and-model:0": "HP LaserJet 1020", - "device-id:0": "MFG:HP;MDL:LaserJet", - }, - wantLen: 1, - wantURIs: []string{"usb://HP/LaserJet"}, - }, - { - name: "multiple_devices", - input: map[string]string{ - "device-uri:0": "usb://HP/LaserJet", - "device-class:0": "direct", - "device-info:0": "HP LaserJet", - "device-uri:1": "socket://192.168.1.100", - "device-class:1": "network", - "device-info:1": "Network Printer", - }, - wantLen: 2, - wantURIs: []string{"usb://HP/LaserJet", "socket://192.168.1.100"}, - }, - { - name: "malformed_key", - input: map[string]string{ - "no-colon-here": "value", - }, - wantLen: 0, - wantURIs: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := parseDevicesMap(tt.input) - assert.Len(t, got, tt.wantLen) - - if tt.wantURIs != nil { - gotURIs := make(map[string]bool) - for _, d := range got { - gotURIs[d.URI] = true - } - for _, uri := range tt.wantURIs { - assert.True(t, gotURIs[uri], "expected URI %s not found", uri) - } - } - }) - } -} - -func TestParseDevicesMap_Attributes(t *testing.T) { - input := map[string]string{ - "device-uri:0": "usb://HP/LaserJet", - "device-class:0": "direct", - "device-info:0": "HP LaserJet", - "device-make-and-model:0": "HP LaserJet 1020", - "device-id:0": "MFG:HP;MDL:LaserJet", - "device-location:0": "USB Port", - } - - got := parseDevicesMap(input) - assert.Len(t, got, 1) - - dev := got[0] - assert.Equal(t, "usb://HP/LaserJet", dev.URI) - assert.Equal(t, "direct", dev.Class) - assert.Equal(t, "HP LaserJet", dev.Info) - assert.Equal(t, "HP LaserJet 1020", dev.MakeModel) - assert.Equal(t, "MFG:HP;MDL:LaserJet", dev.ID) - assert.Equal(t, "USB Port", dev.Location) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/subscription.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/subscription.go deleted file mode 100644 index 6b369b4..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/subscription.go +++ /dev/null @@ -1,245 +0,0 @@ -package cups - -import ( - "fmt" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" -) - -type SubscriptionManager struct { - client CUPSClientInterface - subscriptionID int - sequenceNumber int - eventChan chan SubscriptionEvent - stopChan chan struct{} - wg sync.WaitGroup - baseURL string - running bool - mu sync.Mutex -} - -func NewSubscriptionManager(client CUPSClientInterface, baseURL string) *SubscriptionManager { - return &SubscriptionManager{ - client: client, - eventChan: make(chan SubscriptionEvent, 100), - stopChan: make(chan struct{}), - baseURL: baseURL, - } -} - -func (sm *SubscriptionManager) Start() error { - sm.mu.Lock() - if sm.running { - sm.mu.Unlock() - return fmt.Errorf("subscription manager already running") - } - sm.running = true - sm.mu.Unlock() - - subID, err := sm.createSubscription() - if err != nil { - sm.mu.Lock() - sm.running = false - sm.mu.Unlock() - return fmt.Errorf("failed to create subscription: %w", err) - } - - sm.subscriptionID = subID - log.Infof("[CUPS] Created IPP subscription with ID %d", subID) - - sm.wg.Add(1) - go sm.notificationLoop() - - return nil -} - -func (sm *SubscriptionManager) createSubscription() (int, error) { - req := ipp.NewRequest(ipp.OperationCreatePrinterSubscriptions, 1) - req.OperationAttributes[ipp.AttributePrinterURI] = fmt.Sprintf("%s/", sm.baseURL) - req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms" - - // Subscription attributes go in SubscriptionAttributes (subscription-attributes-tag in IPP) - req.SubscriptionAttributes = map[string]any{ - "notify-events": []string{ - "printer-state-changed", - "printer-added", - "printer-deleted", - "job-created", - "job-completed", - "job-state-changed", - }, - "notify-pull-method": "ippget", - "notify-lease-duration": 0, - } - - // Send to root IPP endpoint - resp, err := sm.client.SendRequest(fmt.Sprintf("%s/", sm.baseURL), req, nil) - if err != nil { - return 0, fmt.Errorf("SendRequest failed: %w", err) - } - - // Check for IPP errors - if err := resp.CheckForErrors(); err != nil { - return 0, fmt.Errorf("IPP error: %w", err) - } - - // Subscription ID comes back in SubscriptionAttributes - if len(resp.SubscriptionAttributes) > 0 { - if idAttr, ok := resp.SubscriptionAttributes[0]["notify-subscription-id"]; ok && len(idAttr) > 0 { - if val, ok := idAttr[0].Value.(int); ok { - return val, nil - } - } - } - - return 0, fmt.Errorf("no subscription ID returned") -} - -func (sm *SubscriptionManager) notificationLoop() { - defer sm.wg.Done() - - backoff := 1 * time.Second - - for { - select { - case <-sm.stopChan: - return - default: - } - - gotAny, err := sm.fetchNotificationsWithWait() - if err != nil { - log.Warnf("[CUPS] Error fetching notifications: %v", err) - jitter := time.Duration(50+(time.Now().UnixNano()%200)) * time.Millisecond - sleepTime := backoff + jitter - if sleepTime > 30*time.Second { - sleepTime = 30 * time.Second - } - select { - case <-sm.stopChan: - return - case <-time.After(sleepTime): - } - if backoff < 30*time.Second { - backoff *= 2 - } - continue - } - - backoff = 1 * time.Second - - if gotAny { - continue - } - - select { - case <-sm.stopChan: - return - case <-time.After(2 * time.Second): - } - } -} - -func (sm *SubscriptionManager) fetchNotificationsWithWait() (bool, error) { - req := ipp.NewRequest(ipp.OperationGetNotifications, 1) - req.OperationAttributes[ipp.AttributePrinterURI] = fmt.Sprintf("%s/", sm.baseURL) - req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms" - req.OperationAttributes["notify-subscription-ids"] = sm.subscriptionID - if sm.sequenceNumber > 0 { - req.OperationAttributes["notify-sequence-numbers"] = sm.sequenceNumber - } - - resp, err := sm.client.SendRequest(fmt.Sprintf("%s/", sm.baseURL), req, nil) - if err != nil { - return false, err - } - - gotAny := false - for _, eventGroup := range resp.SubscriptionAttributes { - if seqAttr, ok := eventGroup["notify-sequence-number"]; ok && len(seqAttr) > 0 { - if seqNum, ok := seqAttr[0].Value.(int); ok { - sm.sequenceNumber = seqNum + 1 - } - } - - event := sm.parseEvent(eventGroup) - gotAny = true - select { - case sm.eventChan <- event: - case <-sm.stopChan: - return gotAny, nil - default: - log.Warn("[CUPS] Event channel full, dropping event") - } - } - - return gotAny, nil -} - -func (sm *SubscriptionManager) parseEvent(attrs ipp.Attributes) SubscriptionEvent { - event := SubscriptionEvent{ - SubscribedAt: time.Now(), - } - - if attr, ok := attrs["notify-subscribed-event"]; ok && len(attr) > 0 { - if val, ok := attr[0].Value.(string); ok { - event.EventName = val - } - } - - if attr, ok := attrs["printer-name"]; ok && len(attr) > 0 { - if val, ok := attr[0].Value.(string); ok { - event.PrinterName = val - } - } - - if attr, ok := attrs["notify-job-id"]; ok && len(attr) > 0 { - if val, ok := attr[0].Value.(int); ok { - event.JobID = val - } - } - - return event -} - -func (sm *SubscriptionManager) Events() <-chan SubscriptionEvent { - return sm.eventChan -} - -func (sm *SubscriptionManager) Stop() { - sm.mu.Lock() - if !sm.running { - sm.mu.Unlock() - return - } - sm.running = false - sm.mu.Unlock() - - close(sm.stopChan) - sm.wg.Wait() - - if sm.subscriptionID != 0 { - sm.cancelSubscription() - sm.subscriptionID = 0 - sm.sequenceNumber = 0 - } - - sm.stopChan = make(chan struct{}) -} - -func (sm *SubscriptionManager) cancelSubscription() { - req := ipp.NewRequest(ipp.OperationCancelSubscription, 1) - req.OperationAttributes[ipp.AttributePrinterURI] = fmt.Sprintf("%s/", sm.baseURL) - req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms" - req.OperationAttributes["notify-subscription-id"] = sm.subscriptionID - - _, err := sm.client.SendRequest(fmt.Sprintf("%s/", sm.baseURL), req, nil) - if err != nil { - log.Warnf("[CUPS] Failed to cancel subscription %d: %v", sm.subscriptionID, err) - } else { - log.Infof("[CUPS] Cancelled subscription %d", sm.subscriptionID) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/subscription_dbus.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/subscription_dbus.go deleted file mode 100644 index b205a6b..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/subscription_dbus.go +++ /dev/null @@ -1,295 +0,0 @@ -package cups - -import ( - "fmt" - "strings" - "sync" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" - "github.com/godbus/dbus/v5" -) - -type DBusSubscriptionManager struct { - client CUPSClientInterface - subscriptionID int - eventChan chan SubscriptionEvent - stopChan chan struct{} - wg sync.WaitGroup - baseURL string - running bool - mu sync.Mutex - conn *dbus.Conn -} - -func NewDBusSubscriptionManager(client CUPSClientInterface, baseURL string) *DBusSubscriptionManager { - return &DBusSubscriptionManager{ - client: client, - eventChan: make(chan SubscriptionEvent, 100), - stopChan: make(chan struct{}), - baseURL: baseURL, - } -} - -func (sm *DBusSubscriptionManager) Start() error { - sm.mu.Lock() - if sm.running { - sm.mu.Unlock() - return fmt.Errorf("subscription manager already running") - } - sm.running = true - sm.mu.Unlock() - - conn, err := dbus.ConnectSystemBus() - if err != nil { - sm.mu.Lock() - sm.running = false - sm.mu.Unlock() - return fmt.Errorf("connect to system bus: %w", err) - } - sm.conn = conn - - subID, err := sm.createDBusSubscription() - if err != nil { - sm.conn.Close() - sm.mu.Lock() - sm.running = false - sm.mu.Unlock() - return fmt.Errorf("failed to create D-Bus subscription: %w", err) - } - - sm.subscriptionID = subID - log.Infof("[CUPS] Created D-Bus subscription with ID %d", subID) - - if err := sm.conn.AddMatchSignal( - dbus.WithMatchInterface("org.cups.cupsd.Notifier"), - ); err != nil { - sm.cancelSubscription() - sm.conn.Close() - sm.mu.Lock() - sm.running = false - sm.mu.Unlock() - return fmt.Errorf("failed to add D-Bus match: %w", err) - } - - sm.wg.Add(1) - go sm.dbusListenerLoop() - - return nil -} - -func (sm *DBusSubscriptionManager) createDBusSubscription() (int, error) { - req := ipp.NewRequest(ipp.OperationCreatePrinterSubscriptions, 2) - req.OperationAttributes[ipp.AttributePrinterURI] = fmt.Sprintf("%s/", sm.baseURL) - req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms" - - req.SubscriptionAttributes = map[string]any{ - "notify-events": []string{ - "printer-state-changed", - "printer-added", - "printer-deleted", - "job-created", - "job-completed", - "job-state-changed", - }, - "notify-recipient-uri": "dbus:/", - "notify-lease-duration": 86400, - } - - resp, err := sm.client.SendRequest(fmt.Sprintf("%s/", sm.baseURL), req, nil) - if err != nil { - return 0, fmt.Errorf("SendRequest failed: %w", err) - } - - if err := resp.CheckForErrors(); err != nil { - return 0, fmt.Errorf("IPP error: %w", err) - } - - if len(resp.SubscriptionAttributes) > 0 { - if idAttr, ok := resp.SubscriptionAttributes[0]["notify-subscription-id"]; ok && len(idAttr) > 0 { - if val, ok := idAttr[0].Value.(int); ok { - return val, nil - } - } - } - - return 0, fmt.Errorf("no subscription ID returned") -} - -func (sm *DBusSubscriptionManager) dbusListenerLoop() { - defer sm.wg.Done() - - signalChan := make(chan *dbus.Signal, 10) - sm.conn.Signal(signalChan) - defer sm.conn.RemoveSignal(signalChan) - - for { - select { - case <-sm.stopChan: - return - case sig := <-signalChan: - if sig == nil { - continue - } - - event := sm.parseDBusSignal(sig) - if event.EventName == "" { - continue - } - - select { - case sm.eventChan <- event: - case <-sm.stopChan: - return - default: - log.Warn("[CUPS] Event channel full, dropping event") - } - } - } -} - -func (sm *DBusSubscriptionManager) parseDBusSignal(sig *dbus.Signal) SubscriptionEvent { - event := SubscriptionEvent{} - - switch sig.Name { - case "org.cups.cupsd.Notifier.JobStateChanged": - if len(sig.Body) >= 6 { - if text, ok := sig.Body[0].(string); ok { - event.EventName = "job-state-changed" - parts := strings.Split(text, " ") - if len(parts) >= 2 { - event.PrinterName = parts[0] - } - } - if printerURI, ok := sig.Body[1].(string); ok && event.PrinterName == "" { - if idx := strings.LastIndex(printerURI, "/"); idx != -1 { - event.PrinterName = printerURI[idx+1:] - } - } - if jobID, ok := sig.Body[3].(uint32); ok { - event.JobID = int(jobID) - } - } - - case "org.cups.cupsd.Notifier.JobCreated": - if len(sig.Body) >= 6 { - if text, ok := sig.Body[0].(string); ok { - event.EventName = "job-created" - parts := strings.Split(text, " ") - if len(parts) >= 2 { - event.PrinterName = parts[0] - } - } - if printerURI, ok := sig.Body[1].(string); ok && event.PrinterName == "" { - if idx := strings.LastIndex(printerURI, "/"); idx != -1 { - event.PrinterName = printerURI[idx+1:] - } - } - if jobID, ok := sig.Body[3].(uint32); ok { - event.JobID = int(jobID) - } - } - - case "org.cups.cupsd.Notifier.JobCompleted": - if len(sig.Body) >= 6 { - if text, ok := sig.Body[0].(string); ok { - event.EventName = "job-completed" - parts := strings.Split(text, " ") - if len(parts) >= 2 { - event.PrinterName = parts[0] - } - } - if printerURI, ok := sig.Body[1].(string); ok && event.PrinterName == "" { - if idx := strings.LastIndex(printerURI, "/"); idx != -1 { - event.PrinterName = printerURI[idx+1:] - } - } - if jobID, ok := sig.Body[3].(uint32); ok { - event.JobID = int(jobID) - } - } - - case "org.cups.cupsd.Notifier.PrinterStateChanged": - if len(sig.Body) >= 6 { - if text, ok := sig.Body[0].(string); ok { - event.EventName = "printer-state-changed" - parts := strings.Split(text, " ") - if len(parts) >= 2 { - event.PrinterName = parts[0] - } - } - if printerURI, ok := sig.Body[1].(string); ok && event.PrinterName == "" { - if idx := strings.LastIndex(printerURI, "/"); idx != -1 { - event.PrinterName = printerURI[idx+1:] - } - } - } - - case "org.cups.cupsd.Notifier.PrinterAdded": - if len(sig.Body) >= 6 { - if text, ok := sig.Body[0].(string); ok { - event.EventName = "printer-added" - parts := strings.Split(text, " ") - if len(parts) >= 2 { - event.PrinterName = parts[0] - } - } - } - - case "org.cups.cupsd.Notifier.PrinterDeleted": - if len(sig.Body) >= 6 { - if text, ok := sig.Body[0].(string); ok { - event.EventName = "printer-deleted" - parts := strings.Split(text, " ") - if len(parts) >= 2 { - event.PrinterName = parts[0] - } - } - } - } - - return event -} - -func (sm *DBusSubscriptionManager) Events() <-chan SubscriptionEvent { - return sm.eventChan -} - -func (sm *DBusSubscriptionManager) Stop() { - sm.mu.Lock() - if !sm.running { - sm.mu.Unlock() - return - } - sm.running = false - sm.mu.Unlock() - - close(sm.stopChan) - sm.wg.Wait() - - if sm.subscriptionID != 0 { - sm.cancelSubscription() - sm.subscriptionID = 0 - } - - if sm.conn != nil { - sm.conn.Close() - sm.conn = nil - } - - sm.stopChan = make(chan struct{}) -} - -func (sm *DBusSubscriptionManager) cancelSubscription() { - req := ipp.NewRequest(ipp.OperationCancelSubscription, 1) - req.OperationAttributes[ipp.AttributePrinterURI] = fmt.Sprintf("%s/", sm.baseURL) - req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms" - req.OperationAttributes["notify-subscription-id"] = sm.subscriptionID - - _, err := sm.client.SendRequest(fmt.Sprintf("%s/", sm.baseURL), req, nil) - if err != nil { - log.Warnf("[CUPS] Failed to cancel subscription %d: %v", sm.subscriptionID, err) - } else { - log.Infof("[CUPS] Cancelled subscription %d", sm.subscriptionID) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/test_connection.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/test_connection.go deleted file mode 100644 index d0203e9..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/test_connection.go +++ /dev/null @@ -1,176 +0,0 @@ -package cups - -import ( - "errors" - "fmt" - "net" - "strings" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" -) - -var validProtocols = map[string]bool{ - "ipp": true, - "ipps": true, - "lpd": true, - "socket": true, -} - -func validateTestConnectionParams(host string, port int, protocol string) error { - if host == "" { - return errors.New("host is required") - } - if strings.ContainsAny(host, " \t\n\r/\\") { - return errors.New("host contains invalid characters") - } - if port < 1 || port > 65535 { - return errors.New("port must be between 1 and 65535") - } - if protocol != "" && !validProtocols[protocol] { - return errors.New("protocol must be one of: ipp, ipps, lpd, socket") - } - return nil -} - -const probeTimeout = 10 * time.Second - -func probeRemotePrinter(host string, port int, useTLS bool) (*RemotePrinterInfo, error) { - addr := net.JoinHostPort(host, fmt.Sprintf("%d", port)) - - // Fast fail: TCP reachability check - conn, err := net.DialTimeout("tcp", addr, probeTimeout) - if err != nil { - return &RemotePrinterInfo{ - Reachable: false, - Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()), - }, nil - } - conn.Close() - - // Create a temporary IPP client pointing at the remote host. - // The TCP dial above provides fast-fail for unreachable hosts. - // The IPP adapter's ResponseHeaderTimeout (90s) bounds stalling servers. - client := ipp.NewIPPClient(host, port, "", "", useTLS) - - // Try /ipp/print first (modern driverless printers), then / (legacy) - info, err := probeIPPEndpoint(client, host, port, useTLS, "/ipp/print") - if err != nil { - // If we got an auth error, the printer exists but requires credentials. - // Report it as reachable with the URI that triggered the auth challenge. - if isAuthError(err) { - proto := "ipp" - if useTLS { - proto = "ipps" - } - return &RemotePrinterInfo{ - Reachable: true, - URI: fmt.Sprintf("%s://%s:%d/ipp/print", proto, host, port), - Info: "authentication required", - }, nil - } - info, err = probeIPPEndpoint(client, host, port, useTLS, "/") - } - if err != nil { - if isAuthError(err) { - proto := "ipp" - if useTLS { - proto = "ipps" - } - return &RemotePrinterInfo{ - Reachable: true, - URI: fmt.Sprintf("%s://%s:%d/", proto, host, port), - Info: "authentication required", - }, nil - } - // TCP reachable but not an IPP printer - return &RemotePrinterInfo{ - Reachable: true, - Error: fmt.Sprintf("host is reachable but does not appear to be an IPP printer: %s", err.Error()), - }, nil - } - - return info, nil -} - -func probeIPPEndpoint(client *ipp.IPPClient, host string, port int, useTLS bool, resourcePath string) (*RemotePrinterInfo, error) { - proto := "ipp" - if useTLS { - proto = "ipps" - } - printerURI := fmt.Sprintf("%s://%s:%d%s", proto, host, port, resourcePath) - - httpProto := "http" - if useTLS { - httpProto = "https" - } - httpURL := fmt.Sprintf("%s://%s:%d%s", httpProto, host, port, resourcePath) - - req := ipp.NewRequest(ipp.OperationGetPrinterAttributes, 1) - req.OperationAttributes[ipp.AttributePrinterURI] = printerURI - req.OperationAttributes[ipp.AttributeRequestedAttributes] = []string{ - ipp.AttributePrinterName, - ipp.AttributePrinterMakeAndModel, - ipp.AttributePrinterState, - ipp.AttributePrinterInfo, - ipp.AttributePrinterUriSupported, - } - - resp, err := client.SendRequest(httpURL, req, nil) - if err != nil { - return nil, err - } - - if len(resp.PrinterAttributes) == 0 { - return nil, errors.New("no printer attributes returned") - } - - attrs := resp.PrinterAttributes[0] - - return &RemotePrinterInfo{ - Reachable: true, - MakeModel: getStringAttr(attrs, ipp.AttributePrinterMakeAndModel), - Name: getStringAttr(attrs, ipp.AttributePrinterName), - Info: getStringAttr(attrs, ipp.AttributePrinterInfo), - State: parsePrinterState(attrs), - URI: printerURI, - }, nil -} - -// TestRemotePrinter validates inputs and probes a remote printer via IPP. -// For lpd/socket protocols, only TCP reachability is tested. -func (m *Manager) TestRemotePrinter(host string, port int, protocol string) (*RemotePrinterInfo, error) { - if protocol == "" { - protocol = "ipp" - } - - if err := validateTestConnectionParams(host, port, protocol); err != nil { - return nil, err - } - - // For non-IPP protocols, only check TCP reachability - if protocol == "lpd" || protocol == "socket" { - addr := net.JoinHostPort(host, fmt.Sprintf("%d", port)) - conn, err := net.DialTimeout("tcp", addr, probeTimeout) - if err != nil { - return &RemotePrinterInfo{ - Reachable: false, - Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()), - }, nil - } - conn.Close() - return &RemotePrinterInfo{ - Reachable: true, - URI: fmt.Sprintf("%s://%s:%d", protocol, host, port), - }, nil - } - - useTLS := protocol == "ipps" - - probeFn := m.probeRemoteFn - if probeFn == nil { - probeFn = probeRemotePrinter - } - - return probeFn(host, port, useTLS) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/test_connection_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/test_connection_test.go deleted file mode 100644 index b835028..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/test_connection_test.go +++ /dev/null @@ -1,397 +0,0 @@ -package cups - -import ( - "bytes" - "encoding/json" - "fmt" - "testing" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" - "github.com/stretchr/testify/assert" -) - -func TestValidateTestConnectionParams(t *testing.T) { - tests := []struct { - name string - host string - port int - protocol string - wantErr string - }{ - { - name: "valid ipp", - host: "192.168.0.5", - port: 631, - protocol: "ipp", - wantErr: "", - }, - { - name: "valid ipps", - host: "printer.local", - port: 443, - protocol: "ipps", - wantErr: "", - }, - { - name: "valid lpd", - host: "10.0.0.1", - port: 515, - protocol: "lpd", - wantErr: "", - }, - { - name: "valid socket", - host: "10.0.0.1", - port: 9100, - protocol: "socket", - wantErr: "", - }, - { - name: "empty host", - host: "", - port: 631, - protocol: "ipp", - wantErr: "host is required", - }, - { - name: "port too low", - host: "192.168.0.5", - port: 0, - protocol: "ipp", - wantErr: "port must be between 1 and 65535", - }, - { - name: "port too high", - host: "192.168.0.5", - port: 70000, - protocol: "ipp", - wantErr: "port must be between 1 and 65535", - }, - { - name: "invalid protocol", - host: "192.168.0.5", - port: 631, - protocol: "ftp", - wantErr: "protocol must be one of: ipp, ipps, lpd, socket", - }, - { - name: "empty protocol treated as ipp", - host: "192.168.0.5", - port: 631, - protocol: "", - wantErr: "", - }, - { - name: "host with slash", - host: "192.168.0.5/admin", - port: 631, - protocol: "ipp", - wantErr: "host contains invalid characters", - }, - { - name: "host with space", - host: "192.168.0.5 ", - port: 631, - protocol: "ipp", - wantErr: "host contains invalid characters", - }, - { - name: "host with newline", - host: "192.168.0.5\n", - port: 631, - protocol: "ipp", - wantErr: "host contains invalid characters", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := validateTestConnectionParams(tt.host, tt.port, tt.protocol) - if tt.wantErr == "" { - assert.NoError(t, err) - } else { - assert.EqualError(t, err, tt.wantErr) - } - }) - } -} - -func TestManager_TestRemotePrinter_Validation(t *testing.T) { - m := NewTestManager(nil, nil) - - tests := []struct { - name string - host string - port int - protocol string - wantErr string - }{ - { - name: "empty host returns error", - host: "", - port: 631, - protocol: "ipp", - wantErr: "host is required", - }, - { - name: "invalid port returns error", - host: "192.168.0.5", - port: 0, - protocol: "ipp", - wantErr: "port must be between 1 and 65535", - }, - { - name: "invalid protocol returns error", - host: "192.168.0.5", - port: 631, - protocol: "ftp", - wantErr: "protocol must be one of: ipp, ipps, lpd, socket", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := m.TestRemotePrinter(tt.host, tt.port, tt.protocol) - assert.EqualError(t, err, tt.wantErr) - }) - } -} - -func TestManager_TestRemotePrinter_IPP(t *testing.T) { - tests := []struct { - name string - protocol string - probeRet *RemotePrinterInfo - probeErr error - wantTLS bool - wantReach bool - wantModel string - }{ - { - name: "successful ipp probe", - protocol: "ipp", - probeRet: &RemotePrinterInfo{ - Reachable: true, - MakeModel: "HP OfficeJet 8010", - Name: "OfficeJet", - State: "idle", - URI: "ipp://192.168.0.5:631/ipp/print", - }, - wantTLS: false, - wantReach: true, - wantModel: "HP OfficeJet 8010", - }, - { - name: "successful ipps probe", - protocol: "ipps", - probeRet: &RemotePrinterInfo{ - Reachable: true, - MakeModel: "HP OfficeJet 8010", - URI: "ipps://192.168.0.5:631/ipp/print", - }, - wantTLS: true, - wantReach: true, - wantModel: "HP OfficeJet 8010", - }, - { - name: "unreachable host", - protocol: "ipp", - probeRet: &RemotePrinterInfo{ - Reachable: false, - Error: "cannot reach 192.168.0.5:631: connection refused", - }, - wantReach: false, - }, - { - name: "empty protocol defaults to ipp", - protocol: "", - probeRet: &RemotePrinterInfo{ - Reachable: true, - MakeModel: "Test Printer", - }, - wantTLS: false, - wantReach: true, - wantModel: "Test Printer", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var capturedTLS bool - m := NewTestManager(nil, nil) - m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) { - capturedTLS = useTLS - return tt.probeRet, tt.probeErr - } - - result, err := m.TestRemotePrinter("192.168.0.5", 631, tt.protocol) - if tt.probeErr != nil { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Equal(t, tt.wantReach, result.Reachable) - assert.Equal(t, tt.wantModel, result.MakeModel) - assert.Equal(t, tt.wantTLS, capturedTLS) - }) - } -} - -func TestManager_TestRemotePrinter_AuthRequired(t *testing.T) { - m := NewTestManager(nil, nil) - m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) { - // Simulate what happens when the printer returns HTTP 401 - return probeRemotePrinterWithAuthError(host, port, useTLS) - } - - result, err := m.TestRemotePrinter("192.168.0.107", 631, "ipp") - assert.NoError(t, err) - assert.True(t, result.Reachable) - assert.Equal(t, "authentication required", result.Info) - assert.Contains(t, result.URI, "ipp://192.168.0.107:631") -} - -// probeRemotePrinterWithAuthError simulates a probe where the printer -// returns HTTP 401 on both endpoints. -func probeRemotePrinterWithAuthError(host string, port int, useTLS bool) (*RemotePrinterInfo, error) { - // This simulates what probeRemotePrinter does when both endpoints - // return auth errors. We test the auth detection logic directly. - err := ipp.HTTPError{Code: 401} - if isAuthError(err) { - proto := "ipp" - if useTLS { - proto = "ipps" - } - return &RemotePrinterInfo{ - Reachable: true, - URI: fmt.Sprintf("%s://%s:%d/ipp/print", proto, host, port), - Info: "authentication required", - }, nil - } - return nil, err -} - -func TestManager_TestRemotePrinter_NonIPPProtocol(t *testing.T) { - m := NewTestManager(nil, nil) - probeCalled := false - m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) { - probeCalled = true - return nil, nil - } - - // These will fail at TCP dial (no real server), but the important - // thing is that probeRemoteFn is NOT called for lpd/socket. - m.TestRemotePrinter("192.168.0.5", 9100, "socket") - assert.False(t, probeCalled, "probe function should not be called for socket protocol") - - m.TestRemotePrinter("192.168.0.5", 515, "lpd") - assert.False(t, probeCalled, "probe function should not be called for lpd protocol") -} - -func TestHandleTestConnection_Success(t *testing.T) { - m := NewTestManager(nil, nil) - m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) { - return &RemotePrinterInfo{ - Reachable: true, - MakeModel: "HP OfficeJet 8010", - Name: "OfficeJet", - State: "idle", - URI: "ipp://192.168.0.5:631/ipp/print", - }, nil - } - - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.testConnection", - Params: map[string]any{ - "host": "192.168.0.5", - }, - } - - handleTestConnection(conn, req, m) - - var resp models.Response[RemotePrinterInfo] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Reachable) - assert.Equal(t, "HP OfficeJet 8010", resp.Result.MakeModel) -} - -func TestHandleTestConnection_MissingHost(t *testing.T) { - m := NewTestManager(nil, nil) - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.testConnection", - Params: map[string]any{}, - } - - handleTestConnection(conn, req, m) - - var resp models.Response[any] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.Nil(t, resp.Result) - assert.NotNil(t, resp.Error) -} - -func TestHandleTestConnection_CustomPortAndProtocol(t *testing.T) { - m := NewTestManager(nil, nil) - m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) { - assert.Equal(t, 9631, port) - assert.True(t, useTLS) - return &RemotePrinterInfo{Reachable: true, URI: "ipps://192.168.0.5:9631/ipp/print"}, nil - } - - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.testConnection", - Params: map[string]any{ - "host": "192.168.0.5", - "port": float64(9631), - "protocol": "ipps", - }, - } - - handleTestConnection(conn, req, m) - - var resp models.Response[RemotePrinterInfo] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Reachable) -} - -func TestHandleRequest_TestConnection(t *testing.T) { - m := NewTestManager(nil, nil) - m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) { - return &RemotePrinterInfo{Reachable: true}, nil - } - - buf := &bytes.Buffer{} - conn := &mockConn{Buffer: buf} - - req := models.Request{ - ID: 1, - Method: "cups.testConnection", - Params: map[string]any{"host": "192.168.0.5"}, - } - - HandleRequest(conn, req, m) - - var resp models.Response[RemotePrinterInfo] - err := json.NewDecoder(buf).Decode(&resp) - assert.NoError(t, err) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Reachable) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/testing.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/testing.go deleted file mode 100644 index 4820e34..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/testing.go +++ /dev/null @@ -1,13 +0,0 @@ -package cups - -func NewTestManager(client CUPSClientInterface, pkHelper PkHelper) *Manager { - return &Manager{ - client: client, - pkHelper: pkHelper, - state: &CUPSState{ - Printers: make(map[string]*Printer), - }, - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/types.go deleted file mode 100644 index 52e9d2c..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/cups/types.go +++ /dev/null @@ -1,132 +0,0 @@ -package cups - -import ( - "io" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" -) - -type CUPSState struct { - Printers map[string]*Printer `json:"printers"` -} - -type Printer struct { - Name string `json:"name"` - URI string `json:"uri"` - State string `json:"state"` - StateReason string `json:"stateReason"` - Location string `json:"location"` - Info string `json:"info"` - MakeModel string `json:"makeModel"` - Accepting bool `json:"accepting"` - Jobs []Job `json:"jobs"` -} - -type Job struct { - ID int `json:"id"` - Name string `json:"name"` - State string `json:"state"` - Printer string `json:"printer"` - User string `json:"user"` - Size int `json:"size"` - TimeCreated time.Time `json:"timeCreated"` -} - -type Device struct { - URI string `json:"uri"` - Class string `json:"class"` - Info string `json:"info"` - MakeModel string `json:"makeModel"` - ID string `json:"id"` - Location string `json:"location"` - IP string `json:"ip,omitempty"` -} - -type PPD struct { - Name string `json:"name"` - NaturalLanguage string `json:"naturalLanguage"` - MakeModel string `json:"makeModel"` - DeviceID string `json:"deviceId"` - Product string `json:"product"` - PSVersion string `json:"psVersion"` - Type string `json:"type"` -} - -type RemotePrinterInfo struct { - Reachable bool `json:"reachable"` - MakeModel string `json:"makeModel"` - Name string `json:"name"` - Info string `json:"info"` - State string `json:"state"` - URI string `json:"uri"` - Error string `json:"error,omitempty"` -} - -type PrinterClass struct { - Name string `json:"name"` - URI string `json:"uri"` - State string `json:"state"` - Members []string `json:"members"` - Location string `json:"location"` - Info string `json:"info"` -} - -type Manager struct { - state *CUPSState - client CUPSClientInterface - pkHelper PkHelper - subscription SubscriptionManagerInterface - stateMutex sync.RWMutex - subscribers syncmap.Map[string, chan CUPSState] - stopChan chan struct{} - eventWG sync.WaitGroup - dirty chan struct{} - notifierWg sync.WaitGroup - lastNotifiedState *CUPSState - baseURL string - probeRemoteFn func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) -} - -type SubscriptionManagerInterface interface { - Start() error - Stop() - Events() <-chan SubscriptionEvent -} - -type CUPSClientInterface interface { - GetPrinters(attributes []string) (map[string]ipp.Attributes, error) - GetJobs(printer, class string, whichJobs string, myJobs bool, firstJobId, limit int, attributes []string) (map[int]ipp.Attributes, error) - CancelJob(jobID int, purge bool) error - PausePrinter(printer string) error - ResumePrinter(printer string) error - CancelAllJob(printer string, purge bool) error - SendRequest(url string, req *ipp.Request, additionalResponseData io.Writer) (*ipp.Response, error) - - GetDevices() (map[string]ipp.Attributes, error) - GetPPDs() (map[string]ipp.Attributes, error) - GetClasses(attributes []string) (map[string]ipp.Attributes, error) - CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error - DeletePrinter(printer string) error - AcceptJobs(printer string) error - RejectJobs(printer string) error - SetPrinterIsShared(printer string, shared bool) error - SetPrinterLocation(printer, location string) error - SetPrinterInformation(printer, information string) error - MoveJob(jobID int, destPrinter string) error - PrintTestPage(printer string, testPageData io.Reader, size int) (int, error) - AddPrinterToClass(class, printer string) error - DeletePrinterFromClass(class, printer string) error - DeleteClass(class string) error - RestartJob(jobID int) error - HoldJobUntil(jobID int, holdUntil string) error -} - -type SubscriptionEvent struct { - EventName string - PrinterName string - JobID int - SubscribedAt time.Time -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dbus/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dbus/handlers.go deleted file mode 100644 index e625d79..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dbus/handlers.go +++ /dev/null @@ -1,237 +0,0 @@ -package dbus - -import ( - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -type objectParams struct { - bus string - dest string - path string - iface string -} - -func extractObjectParams(p map[string]any, requirePath bool) (objectParams, error) { - bus, err := params.String(p, "bus") - if err != nil { - return objectParams{}, err - } - dest, err := params.String(p, "dest") - if err != nil { - return objectParams{}, err - } - - var path string - if requirePath { - path, err = params.String(p, "path") - if err != nil { - return objectParams{}, err - } - } else { - path = params.StringOpt(p, "path", "/") - } - - iface, err := params.String(p, "interface") - if err != nil { - return objectParams{}, err - } - - return objectParams{bus: bus, dest: dest, path: path, iface: iface}, nil -} - -func HandleRequest(conn net.Conn, req models.Request, m *Manager, clientID string) { - switch req.Method { - case "dbus.call": - handleCall(conn, req, m) - case "dbus.getProperty": - handleGetProperty(conn, req, m) - case "dbus.setProperty": - handleSetProperty(conn, req, m) - case "dbus.getAllProperties": - handleGetAllProperties(conn, req, m) - case "dbus.introspect": - handleIntrospect(conn, req, m) - case "dbus.listNames": - handleListNames(conn, req, m) - case "dbus.subscribe": - handleSubscribe(conn, req, m, clientID) - case "dbus.unsubscribe": - handleUnsubscribe(conn, req, m) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleCall(conn net.Conn, req models.Request, m *Manager) { - op, err := extractObjectParams(req.Params, true) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - method, err := params.String(req.Params, "method") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - var args []any - if argsRaw, ok := params.Any(req.Params, "args"); ok { - if argsSlice, ok := argsRaw.([]any); ok { - args = argsSlice - } - } - - result, err := m.Call(op.bus, op.dest, op.path, op.iface, method, args) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, result) -} - -func handleGetProperty(conn net.Conn, req models.Request, m *Manager) { - op, err := extractObjectParams(req.Params, true) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - property, err := params.String(req.Params, "property") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - result, err := m.GetProperty(op.bus, op.dest, op.path, op.iface, property) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, result) -} - -func handleSetProperty(conn net.Conn, req models.Request, m *Manager) { - op, err := extractObjectParams(req.Params, true) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - property, err := params.String(req.Params, "property") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - value, ok := params.Any(req.Params, "value") - if !ok { - models.RespondError(conn, req.ID, "missing 'value' parameter") - return - } - - if err := m.SetProperty(op.bus, op.dest, op.path, op.iface, property, value); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true}) -} - -func handleGetAllProperties(conn net.Conn, req models.Request, m *Manager) { - op, err := extractObjectParams(req.Params, true) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - result, err := m.GetAllProperties(op.bus, op.dest, op.path, op.iface) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, result) -} - -func handleIntrospect(conn net.Conn, req models.Request, m *Manager) { - bus, err := params.String(req.Params, "bus") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - dest, err := params.String(req.Params, "dest") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - path := params.StringOpt(req.Params, "path", "/") - - result, err := m.Introspect(bus, dest, path) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, result) -} - -func handleListNames(conn net.Conn, req models.Request, m *Manager) { - bus, err := params.String(req.Params, "bus") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - result, err := m.ListNames(bus) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, result) -} - -func handleSubscribe(conn net.Conn, req models.Request, m *Manager, clientID string) { - bus, err := params.String(req.Params, "bus") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - sender := params.StringOpt(req.Params, "sender", "") - path := params.StringOpt(req.Params, "path", "") - iface := params.StringOpt(req.Params, "interface", "") - member := params.StringOpt(req.Params, "member", "") - - result, err := m.Subscribe(clientID, bus, sender, path, iface, member) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, result) -} - -func handleUnsubscribe(conn net.Conn, req models.Request, m *Manager) { - subID, err := params.String(req.Params, "subscriptionId") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := m.Unsubscribe(subID); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true}) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dbus/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dbus/manager.go deleted file mode 100644 index 86da6bd..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dbus/manager.go +++ /dev/null @@ -1,392 +0,0 @@ -package dbus - -import ( - "crypto/rand" - "encoding/hex" - "fmt" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil" - "github.com/godbus/dbus/v5" -) - -func NewManager() (*Manager, error) { - systemConn, err := dbus.ConnectSystemBus() - if err != nil { - return nil, fmt.Errorf("failed to connect to system bus: %w", err) - } - - sessionConn, err := dbus.ConnectSessionBus() - if err != nil { - systemConn.Close() - return nil, fmt.Errorf("failed to connect to session bus: %w", err) - } - - m := &Manager{ - systemConn: systemConn, - sessionConn: sessionConn, - } - - go m.processSystemSignals() - go m.processSessionSignals() - - return m, nil -} - -func (m *Manager) getConn(bus string) (*dbus.Conn, error) { - switch bus { - case "system": - if m.systemConn == nil { - return nil, fmt.Errorf("system bus not connected") - } - return m.systemConn, nil - case "session": - if m.sessionConn == nil { - return nil, fmt.Errorf("session bus not connected") - } - return m.sessionConn, nil - default: - return nil, fmt.Errorf("invalid bus: %s (must be 'system' or 'session')", bus) - } -} - -func (m *Manager) Call(bus, dest, path, iface, method string, args []any) (*CallResult, error) { - conn, err := m.getConn(bus) - if err != nil { - return nil, err - } - - obj := conn.Object(dest, dbus.ObjectPath(path)) - fullMethod := iface + "." + method - - convertedArgs := convertArgs(args) - call := obj.Call(fullMethod, 0, convertedArgs...) - if call.Err != nil { - return nil, fmt.Errorf("dbus call failed: %w", call.Err) - } - - return &CallResult{Values: dbusutil.NormalizeSlice(call.Body)}, nil -} - -func convertArgs(args []any) []any { - result := make([]any, len(args)) - for i, arg := range args { - result[i] = convertArg(arg) - } - return result -} - -func convertArg(arg any) any { - switch v := arg.(type) { - case float64: - if v == float64(uint32(v)) && v >= 0 && v <= float64(^uint32(0)) { - return uint32(v) - } - if v == float64(int32(v)) { - return int32(v) - } - return v - case []any: - return convertArgs(v) - case map[string]any: - result := make(map[string]any) - for k, val := range v { - result[k] = convertArg(val) - } - return result - default: - return arg - } -} - -func (m *Manager) GetProperty(bus, dest, path, iface, property string) (*PropertyResult, error) { - conn, err := m.getConn(bus) - if err != nil { - return nil, err - } - - obj := conn.Object(dest, dbus.ObjectPath(path)) - - var variant dbus.Variant - err = obj.Call("org.freedesktop.DBus.Properties.Get", 0, iface, property).Store(&variant) - if err != nil { - return nil, fmt.Errorf("failed to get property: %w", err) - } - - return &PropertyResult{Value: dbusutil.Normalize(variant.Value())}, nil -} - -func (m *Manager) SetProperty(bus, dest, path, iface, property string, value any) error { - conn, err := m.getConn(bus) - if err != nil { - return err - } - - obj := conn.Object(dest, dbus.ObjectPath(path)) - - call := obj.Call("org.freedesktop.DBus.Properties.Set", 0, iface, property, dbus.MakeVariant(value)) - if call.Err != nil { - return fmt.Errorf("failed to set property: %w", call.Err) - } - - return nil -} - -func (m *Manager) GetAllProperties(bus, dest, path, iface string) (map[string]any, error) { - conn, err := m.getConn(bus) - if err != nil { - return nil, err - } - - obj := conn.Object(dest, dbus.ObjectPath(path)) - - var props map[string]dbus.Variant - err = obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, iface).Store(&props) - if err != nil { - return nil, fmt.Errorf("failed to get properties: %w", err) - } - - result := make(map[string]any) - for k, v := range props { - result[k] = dbusutil.Normalize(v.Value()) - } - - return result, nil -} - -func (m *Manager) Introspect(bus, dest, path string) (*IntrospectResult, error) { - conn, err := m.getConn(bus) - if err != nil { - return nil, err - } - - obj := conn.Object(dest, dbus.ObjectPath(path)) - - var xml string - err = obj.Call("org.freedesktop.DBus.Introspectable.Introspect", 0).Store(&xml) - if err != nil { - return nil, fmt.Errorf("failed to introspect: %w", err) - } - - return &IntrospectResult{XML: xml}, nil -} - -func (m *Manager) ListNames(bus string) (*ListNamesResult, error) { - conn, err := m.getConn(bus) - if err != nil { - return nil, err - } - - var names []string - err = conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names) - if err != nil { - return nil, fmt.Errorf("failed to list names: %w", err) - } - - return &ListNamesResult{Names: names}, nil -} - -func (m *Manager) Subscribe(clientID, bus, sender, path, iface, member string) (*SubscribeResult, error) { - conn, err := m.getConn(bus) - if err != nil { - return nil, err - } - - subID := generateSubscriptionID() - - parts := []string{"type='signal'"} - if sender != "" { - parts = append(parts, fmt.Sprintf("sender='%s'", sender)) - } - if path != "" { - parts = append(parts, fmt.Sprintf("path='%s'", path)) - } - if iface != "" { - parts = append(parts, fmt.Sprintf("interface='%s'", iface)) - } - if member != "" { - parts = append(parts, fmt.Sprintf("member='%s'", member)) - } - matchRule := strings.Join(parts, ",") - - call := conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule) - if call.Err != nil { - return nil, fmt.Errorf("failed to add match rule: %w", call.Err) - } - - sub := &signalSubscription{ - Bus: bus, - Sender: sender, - Path: path, - Interface: iface, - Member: member, - ClientID: clientID, - } - m.subscriptions.Store(subID, sub) - - log.Debugf("dbus: subscribed %s to %s", subID, matchRule) - - return &SubscribeResult{SubscriptionID: subID}, nil -} - -func (m *Manager) Unsubscribe(subID string) error { - sub, ok := m.subscriptions.LoadAndDelete(subID) - if !ok { - return fmt.Errorf("subscription not found: %s", subID) - } - - conn, err := m.getConn(sub.Bus) - if err != nil { - return err - } - - parts := []string{"type='signal'"} - if sub.Sender != "" { - parts = append(parts, fmt.Sprintf("sender='%s'", sub.Sender)) - } - if sub.Path != "" { - parts = append(parts, fmt.Sprintf("path='%s'", sub.Path)) - } - if sub.Interface != "" { - parts = append(parts, fmt.Sprintf("interface='%s'", sub.Interface)) - } - if sub.Member != "" { - parts = append(parts, fmt.Sprintf("member='%s'", sub.Member)) - } - matchRule := strings.Join(parts, ",") - - call := conn.BusObject().Call("org.freedesktop.DBus.RemoveMatch", 0, matchRule) - if call.Err != nil { - log.Warnf("dbus: failed to remove match rule: %v", call.Err) - } - - log.Debugf("dbus: unsubscribed %s", subID) - - return nil -} - -func (m *Manager) UnsubscribeClient(clientID string) { - var toDelete []string - m.subscriptions.Range(func(subID string, sub *signalSubscription) bool { - if sub.ClientID == clientID { - toDelete = append(toDelete, subID) - } - return true - }) - - for _, subID := range toDelete { - _ = m.Unsubscribe(subID) - } -} - -func (m *Manager) SubscribeSignals(clientID string) chan SignalEvent { - ch := make(chan SignalEvent, 64) - existing, loaded := m.signalSubscribers.LoadOrStore(clientID, ch) - if loaded { - return existing - } - return ch -} - -func (m *Manager) UnsubscribeSignals(clientID string) { - if ch, ok := m.signalSubscribers.LoadAndDelete(clientID); ok { - close(ch) - } - m.UnsubscribeClient(clientID) -} - -func (m *Manager) processSystemSignals() { - if m.systemConn == nil { - return - } - ch := make(chan *dbus.Signal, 256) - m.systemConn.Signal(ch) - - for sig := range ch { - m.dispatchSignal("system", sig) - } -} - -func (m *Manager) processSessionSignals() { - if m.sessionConn == nil { - return - } - ch := make(chan *dbus.Signal, 256) - m.sessionConn.Signal(ch) - - for sig := range ch { - m.dispatchSignal("session", sig) - } -} - -func (m *Manager) dispatchSignal(bus string, sig *dbus.Signal) { - path := string(sig.Path) - iface := "" - member := sig.Name - - if idx := strings.LastIndex(sig.Name, "."); idx != -1 { - iface = sig.Name[:idx] - member = sig.Name[idx+1:] - } - - m.subscriptions.Range(func(subID string, sub *signalSubscription) bool { - if sub.Bus != bus { - return true - } - if sub.Path != "" && sub.Path != path && !strings.HasPrefix(path, sub.Path) { - return true - } - if sub.Interface != "" && sub.Interface != iface { - return true - } - if sub.Member != "" && sub.Member != member { - return true - } - - event := SignalEvent{ - SubscriptionID: subID, - Sender: sig.Sender, - Path: path, - Interface: iface, - Member: member, - Body: dbusutil.NormalizeSlice(sig.Body), - } - - ch, ok := m.signalSubscribers.Load(sub.ClientID) - if !ok { - return true - } - - select { - case ch <- event: - default: - log.Warnf("dbus: channel full for %s, dropping signal", subID) - } - - return true - }) -} - -func (m *Manager) Close() { - m.signalSubscribers.Range(func(clientID string, ch chan SignalEvent) bool { - close(ch) - m.signalSubscribers.Delete(clientID) - return true - }) - - if m.systemConn != nil { - m.systemConn.Close() - } - if m.sessionConn != nil { - m.sessionConn.Close() - } -} - -func generateSubscriptionID() string { - b := make([]byte, 8) - if _, err := rand.Read(b); err != nil { - log.Warnf("dbus: failed to generate random subscription ID: %v", err) - } - return hex.EncodeToString(b) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dbus/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dbus/types.go deleted file mode 100644 index 55c45bf..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dbus/types.go +++ /dev/null @@ -1,52 +0,0 @@ -package dbus - -import ( - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" - "github.com/godbus/dbus/v5" -) - -type Manager struct { - systemConn *dbus.Conn - sessionConn *dbus.Conn - - subscriptions syncmap.Map[string, *signalSubscription] - signalSubscribers syncmap.Map[string, chan SignalEvent] -} - -type signalSubscription struct { - Bus string - Sender string - Path string - Interface string - Member string - ClientID string -} - -type SignalEvent struct { - SubscriptionID string `json:"subscriptionId"` - Sender string `json:"sender"` - Path string `json:"path"` - Interface string `json:"interface"` - Member string `json:"member"` - Body []any `json:"body"` -} - -type CallResult struct { - Values []any `json:"values"` -} - -type PropertyResult struct { - Value any `json:"value"` -} - -type IntrospectResult struct { - XML string `json:"xml"` -} - -type ListNamesResult struct { - Names []string `json:"names"` -} - -type SubscribeResult struct { - SubscriptionID string `json:"subscriptionId"` -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/handlers.go deleted file mode 100644 index e83cc84..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/handlers.go +++ /dev/null @@ -1,138 +0,0 @@ -package dwl - -import ( - "encoding/json" - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -type SuccessResult struct { - Success bool `json:"success"` - Message string `json:"message"` -} - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - if manager == nil { - models.RespondError(conn, req.ID, "dwl manager not initialized") - return - } - - switch req.Method { - case "dwl.getState": - handleGetState(conn, req, manager) - case "dwl.setTags": - handleSetTags(conn, req, manager) - case "dwl.setClientTags": - handleSetClientTags(conn, req, manager) - case "dwl.setLayout": - handleSetLayout(conn, req, manager) - case "dwl.subscribe": - handleSubscribe(conn, req, manager) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleGetState(conn net.Conn, req models.Request, manager *Manager) { - state := manager.GetState() - models.Respond(conn, req.ID, state) -} - -func handleSetTags(conn net.Conn, req models.Request, manager *Manager) { - output, ok := models.Get[string](req, "output") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'output' parameter") - return - } - - tagmask, ok := models.Get[float64](req, "tagmask") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'tagmask' parameter") - return - } - - toggleTagset, ok := models.Get[float64](req, "toggleTagset") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'toggleTagset' parameter") - return - } - - if err := manager.SetTags(output, uint32(tagmask), uint32(toggleTagset)); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "tags set"}) -} - -func handleSetClientTags(conn net.Conn, req models.Request, manager *Manager) { - output, ok := models.Get[string](req, "output") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'output' parameter") - return - } - - andTags, ok := models.Get[float64](req, "andTags") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'andTags' parameter") - return - } - - xorTags, ok := models.Get[float64](req, "xorTags") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'xorTags' parameter") - return - } - - if err := manager.SetClientTags(output, uint32(andTags), uint32(xorTags)); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "client tags set"}) -} - -func handleSetLayout(conn net.Conn, req models.Request, manager *Manager) { - output, ok := models.Get[string](req, "output") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'output' parameter") - return - } - - index, ok := models.Get[float64](req, "index") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'index' parameter") - return - } - - if err := manager.SetLayout(output, uint32(index)); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "layout set"}) -} - -func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) { - clientID := fmt.Sprintf("client-%p", conn) - stateChan := manager.Subscribe(clientID) - defer manager.Unsubscribe(clientID) - - initialState := manager.GetState() - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - ID: req.ID, - Result: &initialState, - }); err != nil { - return - } - - for state := range stateChan { - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - Result: &state, - }); err != nil { - return - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager.go deleted file mode 100644 index 0d21afa..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager.go +++ /dev/null @@ -1,522 +0,0 @@ -package dwl - -import ( - "fmt" - "time" - - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc" -) - -func NewManager(display wlclient.WaylandDisplay) (*Manager, error) { - m := &Manager{ - display: display, - ctx: display.Context(), - cmdq: make(chan cmd, 128), - outputSetupReq: make(chan uint32, 16), - stopChan: make(chan struct{}), - - dirty: make(chan struct{}, 1), - layouts: make([]string, 0), - } - - if err := m.setupRegistry(); err != nil { - return nil, err - } - - m.updateState() - - m.notifierWg.Add(1) - go m.notifier() - - m.wg.Add(1) - go m.waylandActor() - - return m, nil -} - -func (m *Manager) post(fn func()) { - select { - case m.cmdq <- cmd{fn: fn}: - default: - log.Warn("DWL actor command queue full, dropping command") - } -} - -func (m *Manager) waylandActor() { - defer m.wg.Done() - - for { - select { - case <-m.stopChan: - return - case c := <-m.cmdq: - c.fn() - case outputID := <-m.outputSetupReq: - out, exists := m.outputs.Load(outputID) - if !exists { - log.Warnf("DWL: Output %d no longer exists, skipping setup", outputID) - continue - } - - if out.ipcOutput != nil { - continue - } - - mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2) - if !ok || mgr == nil { - log.Errorf("DWL: Manager not available for output %d setup", outputID) - continue - } - - log.Infof("DWL: Setting up ipcOutput for dynamically added output %d", outputID) - if err := m.setupOutput(mgr, out.output); err != nil { - log.Errorf("DWL: Failed to setup output %d: %v", outputID, err) - } else { - m.updateState() - } - } - } -} - -func (m *Manager) setupRegistry() error { - log.Info("DWL: starting registry setup") - - registry, err := m.display.GetRegistry() - if err != nil { - return fmt.Errorf("failed to get registry: %w", err) - } - m.registry = registry - - outputs := make([]*wlclient.Output, 0) - outputRegNames := make(map[uint32]uint32) - var dwlMgr *dwl_ipc.ZdwlIpcManagerV2 - - registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { - switch e.Interface { - case dwl_ipc.ZdwlIpcManagerV2InterfaceName: - log.Infof("DWL: found %s", dwl_ipc.ZdwlIpcManagerV2InterfaceName) - manager := dwl_ipc.NewZdwlIpcManagerV2(m.ctx) - version := e.Version - if version > 2 { - version = 2 - } - if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil { - dwlMgr = manager - log.Info("DWL: manager bound successfully") - - // Set handlers immediately after binding, before roundtrips - manager.SetTagsHandler(func(e dwl_ipc.ZdwlIpcManagerV2TagsEvent) { - log.Infof("DWL: Tags count: %d", e.Amount) - m.tagCount = e.Amount - m.updateState() - }) - - manager.SetLayoutHandler(func(e dwl_ipc.ZdwlIpcManagerV2LayoutEvent) { - log.Infof("DWL: Layout: %s", e.Name) - m.layouts = append(m.layouts, e.Name) - m.updateState() - }) - } else { - log.Errorf("DWL: failed to bind manager: %v", err) - } - case "wl_output": - log.Debugf("DWL: found wl_output (name=%d)", e.Name) - output := wlclient.NewOutput(m.ctx) - - outState := &outputState{ - registryName: e.Name, - output: output, - tags: make([]TagState, 0), - } - - output.SetNameHandler(func(ev wlclient.OutputNameEvent) { - log.Debugf("DWL: Output name: %s (registry=%d)", ev.Name, e.Name) - outState.name = ev.Name - }) - - output.SetDescriptionHandler(func(ev wlclient.OutputDescriptionEvent) { - log.Debugf("DWL: Output description: %s", ev.Description) - }) - - version := e.Version - if version > 4 { - version = 4 - } - if err := registry.Bind(e.Name, e.Interface, version, output); err == nil { - outputID := output.ID() - outState.id = outputID - log.Infof("DWL: Bound wl_output id=%d registry_name=%d", outputID, e.Name) - outputs = append(outputs, output) - outputRegNames[outputID] = e.Name - - m.outputs.Store(outputID, outState) - - if m.manager != nil { - select { - case m.outputSetupReq <- outputID: - log.Debugf("DWL: Queued setup for output %d", outputID) - default: - log.Warnf("DWL: Setup queue full, output %d will not be initialized", outputID) - } - } - } else { - log.Errorf("DWL: Failed to bind wl_output: %v", err) - } - } - }) - - registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) { - m.post(func() { - var outToRelease *outputState - m.outputs.Range(func(id uint32, out *outputState) bool { - if out.registryName == e.Name { - log.Infof("DWL: Output %d removed", id) - outToRelease = out - m.outputs.Delete(id) - return false - } - return true - }) - - if outToRelease != nil { - if ipcOut, ok := outToRelease.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok && ipcOut != nil { - m.wlMutex.Lock() - ipcOut.Release() - m.wlMutex.Unlock() - log.Debugf("DWL: Released ipcOutput for removed output %d", outToRelease.id) - } - m.updateState() - } - }) - }) - - if err := m.display.Roundtrip(); err != nil { - return fmt.Errorf("first roundtrip failed: %w", err) - } - if err := m.display.Roundtrip(); err != nil { - return fmt.Errorf("second roundtrip failed: %w", err) - } - - if dwlMgr == nil { - log.Info("DWL: manager not found in registry") - return fmt.Errorf("dwl_ipc_manager_v2 not available") - } - - m.manager = dwlMgr - - for _, output := range outputs { - if err := m.setupOutput(dwlMgr, output); err != nil { - log.Warnf("DWL: Failed to setup output %d: %v", output.ID(), err) - } - } - - if err := m.display.Roundtrip(); err != nil { - return fmt.Errorf("final roundtrip failed: %w", err) - } - - log.Info("DWL: registry setup complete") - return nil -} - -func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclient.Output) error { - m.wlMutex.Lock() - ipcOutput, err := manager.GetOutput(output) - m.wlMutex.Unlock() - if err != nil { - return fmt.Errorf("failed to get dwl output: %w", err) - } - - outState, exists := m.outputs.Load(output.ID()) - if !exists { - return fmt.Errorf("output state not found for id %d", output.ID()) - } - outState.ipcOutput = ipcOutput - - ipcOutput.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) { - outState.active = e.Active - }) - - ipcOutput.SetTagHandler(func(e dwl_ipc.ZdwlIpcOutputV2TagEvent) { - updated := false - for i, tag := range outState.tags { - if tag.Tag == e.Tag { - outState.tags[i] = TagState{ - Tag: e.Tag, - State: e.State, - Clients: e.Clients, - Focused: e.Focused, - } - updated = true - break - } - } - - if !updated { - outState.tags = append(outState.tags, TagState{ - Tag: e.Tag, - State: e.State, - Clients: e.Clients, - Focused: e.Focused, - }) - } - - m.updateState() - }) - - ipcOutput.SetLayoutHandler(func(e dwl_ipc.ZdwlIpcOutputV2LayoutEvent) { - outState.layout = e.Layout - }) - - ipcOutput.SetTitleHandler(func(e dwl_ipc.ZdwlIpcOutputV2TitleEvent) { - outState.title = e.Title - }) - - ipcOutput.SetAppidHandler(func(e dwl_ipc.ZdwlIpcOutputV2AppidEvent) { - outState.appID = e.Appid - }) - - ipcOutput.SetLayoutSymbolHandler(func(e dwl_ipc.ZdwlIpcOutputV2LayoutSymbolEvent) { - outState.layoutSymbol = e.Layout - }) - - ipcOutput.SetKbLayoutHandler(func(e dwl_ipc.ZdwlIpcOutputV2KbLayoutEvent) { - outState.kbLayout = e.KbLayout - }) - - ipcOutput.SetKeymodeHandler(func(e dwl_ipc.ZdwlIpcOutputV2KeymodeEvent) { - outState.keymode = e.Keymode - }) - - ipcOutput.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) { - m.updateState() - }) - - return nil -} - -func (m *Manager) updateState() { - outputs := make(map[string]*OutputState) - activeOutput := "" - - m.outputs.Range(func(key uint32, out *outputState) bool { - name := out.name - if name == "" { - name = fmt.Sprintf("output-%d", out.id) - } - - tagsCopy := make([]TagState, len(out.tags)) - copy(tagsCopy, out.tags) - - outputs[name] = &OutputState{ - Name: name, - Active: out.active, - Tags: tagsCopy, - Layout: out.layout, - LayoutSymbol: out.layoutSymbol, - Title: out.title, - AppID: out.appID, - KbLayout: out.kbLayout, - Keymode: out.keymode, - } - - if out.active != 0 { - activeOutput = name - } - return true - }) - - newState := State{ - Outputs: outputs, - TagCount: m.tagCount, - Layouts: m.layouts, - ActiveOutput: activeOutput, - } - - m.stateMutex.Lock() - m.state = &newState - m.stateMutex.Unlock() - - m.notifySubscribers() -} - -func (m *Manager) notifier() { - defer m.notifierWg.Done() - const minGap = 100 * time.Millisecond - timer := time.NewTimer(minGap) - timer.Stop() - var pending bool - - for { - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.dirty: - if pending { - continue - } - pending = true - timer.Reset(minGap) - case <-timer.C: - if !pending { - continue - } - - currentState := m.GetState() - - if m.lastNotified != nil && !stateChanged(m.lastNotified, ¤tState) { - pending = false - continue - } - - m.subscribers.Range(func(key string, ch chan State) bool { - select { - case ch <- currentState: - default: - log.Warn("DWL: subscriber channel full, dropping update") - } - return true - }) - - stateCopy := currentState - m.lastNotified = &stateCopy - pending = false - } - } -} - -func (m *Manager) ensureOutputSetup(out *outputState) error { - if out.ipcOutput != nil { - return nil - } - - return fmt.Errorf("output not yet initialized - setup in progress, retry in a moment") -} - -func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32) error { - availableOutputs := make([]string, 0) - var targetOut *outputState - m.outputs.Range(func(key uint32, out *outputState) bool { - name := out.name - if name == "" { - name = fmt.Sprintf("output-%d", out.id) - } - availableOutputs = append(availableOutputs, name) - if name == outputName { - targetOut = out - return false - } - return true - }) - - if targetOut == nil { - return fmt.Errorf("output not found: %s (available: %v)", outputName, availableOutputs) - } - - if err := m.ensureOutputSetup(targetOut); err != nil { - return fmt.Errorf("failed to setup output %s: %w", outputName, err) - } - - ipcOut, ok := targetOut.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2) - if !ok { - return fmt.Errorf("output %s has invalid ipcOutput type", outputName) - } - - m.wlMutex.Lock() - err := ipcOut.SetTags(tagmask, toggleTagset) - m.wlMutex.Unlock() - return err -} - -func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint32) error { - var targetOut *outputState - m.outputs.Range(func(key uint32, out *outputState) bool { - name := out.name - if name == "" { - name = fmt.Sprintf("output-%d", out.id) - } - if name == outputName { - targetOut = out - return false - } - return true - }) - - if targetOut == nil { - return fmt.Errorf("output not found: %s", outputName) - } - - if err := m.ensureOutputSetup(targetOut); err != nil { - return fmt.Errorf("failed to setup output %s: %w", outputName, err) - } - - ipcOut, ok := targetOut.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2) - if !ok { - return fmt.Errorf("output %s has invalid ipcOutput type", outputName) - } - - m.wlMutex.Lock() - err := ipcOut.SetClientTags(andTags, xorTags) - m.wlMutex.Unlock() - return err -} - -func (m *Manager) SetLayout(outputName string, index uint32) error { - var targetOut *outputState - m.outputs.Range(func(key uint32, out *outputState) bool { - name := out.name - if name == "" { - name = fmt.Sprintf("output-%d", out.id) - } - if name == outputName { - targetOut = out - return false - } - return true - }) - - if targetOut == nil { - return fmt.Errorf("output not found: %s", outputName) - } - - if err := m.ensureOutputSetup(targetOut); err != nil { - return fmt.Errorf("failed to setup output %s: %w", outputName, err) - } - - ipcOut, ok := targetOut.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2) - if !ok { - return fmt.Errorf("output %s has invalid ipcOutput type", outputName) - } - - m.wlMutex.Lock() - err := ipcOut.SetLayout(index) - m.wlMutex.Unlock() - return err -} - -func (m *Manager) Close() { - close(m.stopChan) - m.wg.Wait() - m.notifierWg.Wait() - - m.subscribers.Range(func(key string, ch chan State) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - - m.outputs.Range(func(key uint32, out *outputState) bool { - if ipcOut, ok := out.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok { - ipcOut.Release() - } - m.outputs.Delete(key) - return true - }) - - if mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2); ok { - mgr.Release() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager_test.go deleted file mode 100644 index 13af137..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager_test.go +++ /dev/null @@ -1,366 +0,0 @@ -package dwl - -import ( - "errors" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" -) - -func TestStateChanged_BothNil(t *testing.T) { - assert.True(t, stateChanged(nil, nil)) -} - -func TestStateChanged_OneNil(t *testing.T) { - s := &State{TagCount: 9} - assert.True(t, stateChanged(s, nil)) - assert.True(t, stateChanged(nil, s)) -} - -func TestStateChanged_TagCountDiffers(t *testing.T) { - a := &State{TagCount: 9, Outputs: make(map[string]*OutputState), Layouts: []string{}} - b := &State{TagCount: 10, Outputs: make(map[string]*OutputState), Layouts: []string{}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_LayoutLengthDiffers(t *testing.T) { - a := &State{TagCount: 9, Layouts: []string{"tile"}, Outputs: make(map[string]*OutputState)} - b := &State{TagCount: 9, Layouts: []string{"tile", "monocle"}, Outputs: make(map[string]*OutputState)} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_ActiveOutputDiffers(t *testing.T) { - a := &State{TagCount: 9, ActiveOutput: "eDP-1", Outputs: make(map[string]*OutputState), Layouts: []string{}} - b := &State{TagCount: 9, ActiveOutput: "HDMI-A-1", Outputs: make(map[string]*OutputState), Layouts: []string{}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_OutputCountDiffers(t *testing.T) { - a := &State{ - TagCount: 9, - Outputs: map[string]*OutputState{"eDP-1": {}}, - Layouts: []string{}, - } - b := &State{ - TagCount: 9, - Outputs: map[string]*OutputState{}, - Layouts: []string{}, - } - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_OutputFieldsDiffer(t *testing.T) { - a := &State{ - TagCount: 9, - Layouts: []string{}, - Outputs: map[string]*OutputState{ - "eDP-1": {Active: 1, Layout: 0, Title: "Firefox"}, - }, - } - b := &State{ - TagCount: 9, - Layouts: []string{}, - Outputs: map[string]*OutputState{ - "eDP-1": {Active: 0, Layout: 0, Title: "Firefox"}, - }, - } - assert.True(t, stateChanged(a, b)) - - b.Outputs["eDP-1"].Active = 1 - b.Outputs["eDP-1"].Layout = 1 - assert.True(t, stateChanged(a, b)) - - b.Outputs["eDP-1"].Layout = 0 - b.Outputs["eDP-1"].Title = "Code" - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_TagsDiffer(t *testing.T) { - a := &State{ - TagCount: 9, - Layouts: []string{}, - Outputs: map[string]*OutputState{ - "eDP-1": {Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}}}, - }, - } - b := &State{ - TagCount: 9, - Layouts: []string{}, - Outputs: map[string]*OutputState{ - "eDP-1": {Tags: []TagState{{Tag: 1, State: 2, Clients: 2, Focused: 1}}}, - }, - } - assert.True(t, stateChanged(a, b)) - - b.Outputs["eDP-1"].Tags[0].State = 1 - b.Outputs["eDP-1"].Tags[0].Clients = 3 - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_Equal(t *testing.T) { - a := &State{ - TagCount: 9, - ActiveOutput: "eDP-1", - Layouts: []string{"tile", "monocle"}, - Outputs: map[string]*OutputState{ - "eDP-1": { - Name: "eDP-1", - Active: 1, - Layout: 0, - LayoutSymbol: "[]=", - Title: "Firefox", - AppID: "firefox", - KbLayout: "us", - Keymode: "", - Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}}, - }, - }, - } - b := &State{ - TagCount: 9, - ActiveOutput: "eDP-1", - Layouts: []string{"tile", "monocle"}, - Outputs: map[string]*OutputState{ - "eDP-1": { - Name: "eDP-1", - Active: 1, - Layout: 0, - LayoutSymbol: "[]=", - Title: "Firefox", - AppID: "firefox", - KbLayout: "us", - Keymode: "", - Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}}, - }, - }, - } - assert.False(t, stateChanged(a, b)) -} - -func TestManager_ConcurrentGetState(t *testing.T) { - m := &Manager{ - state: &State{ - TagCount: 9, - Layouts: []string{"tile"}, - Outputs: map[string]*OutputState{"eDP-1": {Name: "eDP-1"}}, - }, - } - - var wg sync.WaitGroup - const goroutines = 50 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - s := m.GetState() - _ = s.TagCount - _ = s.Outputs - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.stateMutex.Lock() - m.state = &State{ - TagCount: uint32(j % 10), - Layouts: []string{"tile", "monocle"}, - Outputs: map[string]*OutputState{"eDP-1": {Active: uint32(j % 2)}}, - } - m.stateMutex.Unlock() - } - }(i) - } - - wg.Wait() -} - -func TestManager_ConcurrentSubscriberAccess(t *testing.T) { - m := &Manager{ - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - - var wg sync.WaitGroup - const goroutines = 20 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - subID := string(rune('a' + id)) - ch := m.Subscribe(subID) - assert.NotNil(t, ch) - time.Sleep(time.Millisecond) - m.Unsubscribe(subID) - }(i) - } - - wg.Wait() -} - -func TestManager_SyncmapOutputsConcurrentAccess(t *testing.T) { - m := &Manager{} - - var wg sync.WaitGroup - const goroutines = 30 - const iterations = 50 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - key := uint32(id) - - for j := 0; j < iterations; j++ { - state := &outputState{ - id: key, - name: "test-output", - active: uint32(j % 2), - tags: []TagState{{Tag: uint32(j), State: 1}}, - } - m.outputs.Store(key, state) - - if loaded, ok := m.outputs.Load(key); ok { - assert.Equal(t, key, loaded.id) - } - - m.outputs.Range(func(k uint32, v *outputState) bool { - _ = v.name - _ = v.active - return true - }) - } - - m.outputs.Delete(key) - }(i) - } - - wg.Wait() -} - -func TestManager_NotifySubscribersNonBlocking(t *testing.T) { - m := &Manager{ - dirty: make(chan struct{}, 1), - } - - for i := 0; i < 10; i++ { - m.notifySubscribers() - } - - assert.Len(t, m.dirty, 1) -} - -func TestManager_PostQueueFull(t *testing.T) { - m := &Manager{ - cmdq: make(chan cmd, 2), - stopChan: make(chan struct{}), - } - - m.post(func() {}) - m.post(func() {}) - m.post(func() {}) - m.post(func() {}) - - assert.Len(t, m.cmdq, 2) -} - -func TestManager_GetStateNilState(t *testing.T) { - m := &Manager{} - - s := m.GetState() - assert.NotNil(t, s.Outputs) - assert.NotNil(t, s.Layouts) - assert.Equal(t, uint32(0), s.TagCount) -} - -func TestTagState_Fields(t *testing.T) { - tag := TagState{ - Tag: 1, - State: 2, - Clients: 3, - Focused: 1, - } - - assert.Equal(t, uint32(1), tag.Tag) - assert.Equal(t, uint32(2), tag.State) - assert.Equal(t, uint32(3), tag.Clients) - assert.Equal(t, uint32(1), tag.Focused) -} - -func TestOutputState_Fields(t *testing.T) { - out := OutputState{ - Name: "eDP-1", - Active: 1, - Tags: []TagState{{Tag: 1}}, - Layout: 0, - LayoutSymbol: "[]=", - Title: "Firefox", - AppID: "firefox", - KbLayout: "us", - Keymode: "", - } - - assert.Equal(t, "eDP-1", out.Name) - assert.Equal(t, uint32(1), out.Active) - assert.Len(t, out.Tags, 1) - assert.Equal(t, "[]=", out.LayoutSymbol) -} - -func TestStateChanged_NewOutputAppears(t *testing.T) { - a := &State{ - TagCount: 9, - Layouts: []string{}, - Outputs: map[string]*OutputState{ - "eDP-1": {Name: "eDP-1"}, - }, - } - b := &State{ - TagCount: 9, - Layouts: []string{}, - Outputs: map[string]*OutputState{ - "eDP-1": {Name: "eDP-1"}, - "HDMI-A-1": {Name: "HDMI-A-1"}, - }, - } - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_TagsLengthDiffers(t *testing.T) { - a := &State{ - TagCount: 9, - Layouts: []string{}, - Outputs: map[string]*OutputState{ - "eDP-1": {Tags: []TagState{{Tag: 1}}}, - }, - } - b := &State{ - TagCount: 9, - Layouts: []string{}, - Outputs: map[string]*OutputState{ - "eDP-1": {Tags: []TagState{{Tag: 1}, {Tag: 2}}}, - }, - } - assert.True(t, stateChanged(a, b)) -} - -func TestNewManager_GetRegistryError(t *testing.T) { - mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) - - mockDisplay.EXPECT().Context().Return(nil) - mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry")) - - _, err := NewManager(mockDisplay) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to get registry") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/types.go deleted file mode 100644 index 2b17537..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/types.go +++ /dev/null @@ -1,176 +0,0 @@ -package dwl - -import ( - "sync" - - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" -) - -type TagState struct { - Tag uint32 `json:"tag"` - State uint32 `json:"state"` - Clients uint32 `json:"clients"` - Focused uint32 `json:"focused"` -} - -type OutputState struct { - Name string `json:"name"` - Active uint32 `json:"active"` - Tags []TagState `json:"tags"` - Layout uint32 `json:"layout"` - LayoutSymbol string `json:"layoutSymbol"` - Title string `json:"title"` - AppID string `json:"appId"` - KbLayout string `json:"kbLayout"` - Keymode string `json:"keymode"` -} - -type State struct { - Outputs map[string]*OutputState `json:"outputs"` - TagCount uint32 `json:"tagCount"` - Layouts []string `json:"layouts"` - ActiveOutput string `json:"activeOutput"` -} - -type cmd struct { - fn func() -} - -type Manager struct { - display wlclient.WaylandDisplay - ctx *wlclient.Context - registry *wlclient.Registry - manager any - - outputs syncmap.Map[uint32, *outputState] - - tagCount uint32 - layouts []string - - wlMutex sync.Mutex - cmdq chan cmd - outputSetupReq chan uint32 - stopChan chan struct{} - wg sync.WaitGroup - - subscribers syncmap.Map[string, chan State] - dirty chan struct{} - notifierWg sync.WaitGroup - lastNotified *State - - stateMutex sync.RWMutex - state *State -} - -type outputState struct { - id uint32 - registryName uint32 - output *wlclient.Output - ipcOutput any - name string - active uint32 - tags []TagState - layout uint32 - layoutSymbol string - title string - appID string - kbLayout string - keymode string -} - -func (m *Manager) GetState() State { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - if m.state == nil { - return State{ - Outputs: make(map[string]*OutputState), - Layouts: []string{}, - TagCount: 0, - } - } - stateCopy := *m.state - return stateCopy -} - -func (m *Manager) Subscribe(id string) chan State { - ch := make(chan State, 64) - - m.subscribers.Store(id, ch) - - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) notifySubscribers() { - select { - case m.dirty <- struct{}{}: - default: - } -} - -func stateChanged(old, new *State) bool { - if old == nil || new == nil { - return true - } - if old.TagCount != new.TagCount { - return true - } - if len(old.Layouts) != len(new.Layouts) { - return true - } - if old.ActiveOutput != new.ActiveOutput { - return true - } - if len(old.Outputs) != len(new.Outputs) { - return true - } - - for name, newOut := range new.Outputs { - oldOut, exists := old.Outputs[name] - if !exists { - return true - } - if oldOut.Active != newOut.Active { - return true - } - if oldOut.Layout != newOut.Layout { - return true - } - if oldOut.LayoutSymbol != newOut.LayoutSymbol { - return true - } - if oldOut.Title != newOut.Title { - return true - } - if oldOut.AppID != newOut.AppID { - return true - } - if oldOut.KbLayout != newOut.KbLayout { - return true - } - if oldOut.Keymode != newOut.Keymode { - return true - } - if len(oldOut.Tags) != len(newOut.Tags) { - return true - } - for i, newTag := range newOut.Tags { - if i >= len(oldOut.Tags) { - return true - } - oldTag := oldOut.Tags[i] - if oldTag.Tag != newTag.Tag || oldTag.State != newTag.State || - oldTag.Clients != newTag.Clients || oldTag.Focused != newTag.Focused { - return true - } - } - } - - return false -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers.go deleted file mode 100644 index 54a7bca..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers.go +++ /dev/null @@ -1,20 +0,0 @@ -package evdev - -import ( - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -func HandleRequest(conn net.Conn, req models.Request, m *Manager) { - switch req.Method { - case "evdev.getState": - handleGetState(conn, req, m) - default: - models.RespondError(conn, req.ID, "unknown method: "+req.Method) - } -} - -func handleGetState(conn net.Conn, req models.Request, m *Manager) { - models.Respond(conn, req.ID, m.GetState()) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers_test.go deleted file mode 100644 index f9bd21d..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package evdev - -import ( - "bytes" - "encoding/json" - "errors" - "net" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - mocks "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/evdev" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -type mockNetConn struct { - net.Conn - readBuf *bytes.Buffer - writeBuf *bytes.Buffer - closed bool -} - -func newMockNetConn() *mockNetConn { - return &mockNetConn{ - readBuf: &bytes.Buffer{}, - writeBuf: &bytes.Buffer{}, - } -} - -func (m *mockNetConn) Read(b []byte) (n int, err error) { - return m.readBuf.Read(b) -} - -func (m *mockNetConn) Write(b []byte) (n int, err error) { - return m.writeBuf.Write(b) -} - -func (m *mockNetConn) Close() error { - m.closed = true - return nil -} - -func TestHandleRequest(t *testing.T) { - t.Run("getState request", func(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() - - m := &Manager{ - devices: []EvdevDevice{mockDevice}, - state: State{Available: true, CapsLock: true}, - closeChan: make(chan struct{}), - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "evdev.getState", - Params: map[string]any{}, - } - - HandleRequest(conn, req, m) - - var resp models.Response[State] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Available) - assert.True(t, resp.Result.CapsLock) - }) - - t.Run("unknown method", func(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() - - m := &Manager{ - devices: []EvdevDevice{mockDevice}, - state: State{Available: true, CapsLock: false}, - closeChan: make(chan struct{}), - } - - conn := newMockNetConn() - req := models.Request{ - ID: 456, - Method: "evdev.unknownMethod", - Params: map[string]any{}, - } - - HandleRequest(conn, req, m) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 456, resp.ID) - assert.NotEmpty(t, resp.Error) - assert.Contains(t, resp.Error, "unknown method") - }) -} - -func TestHandleGetState(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() - - m := &Manager{ - devices: []EvdevDevice{mockDevice}, - state: State{Available: true, CapsLock: false}, - closeChan: make(chan struct{}), - } - - conn := newMockNetConn() - req := models.Request{ - ID: 789, - Method: "evdev.getState", - Params: map[string]any{}, - } - - handleGetState(conn, req, m) - - var resp models.Response[State] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 789, resp.ID) - assert.NotNil(t, resp.Result) - assert.True(t, resp.Result.Available) - assert.False(t, resp.Result.CapsLock) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager.go deleted file mode 100644 index b578481..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager.go +++ /dev/null @@ -1,413 +0,0 @@ -package evdev - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" - "github.com/fsnotify/fsnotify" - evdev "github.com/holoplot/go-evdev" -) - -const ( - evKeyType = 0x01 - evLedType = 0x11 - keyCapslockKey = 58 - ledCapslockKey = 1 - keyStateOn = 1 -) - -type EvdevDevice interface { - Name() (string, error) - Path() string - Close() error - ReadOne() (*evdev.InputEvent, error) - State(t evdev.EvType) (evdev.StateMap, error) -} - -type Manager struct { - devices []EvdevDevice - devicesMutex sync.RWMutex - monitoredPaths map[string]bool - state State - stateMutex sync.RWMutex - subscribers syncmap.Map[string, chan State] - closeChan chan struct{} - closeOnce sync.Once - watcher *fsnotify.Watcher -} - -func NewManager() (*Manager, error) { - devices, err := findKeyboards() - if err != nil { - return nil, fmt.Errorf("failed to find keyboards: %w", err) - } - - initialCapsLock := readInitialCapsLockState(devices[0]) - - watcher, err := fsnotify.NewWatcher() - if err != nil { - log.Warnf("Failed to create fsnotify watcher, hotplug detection disabled: %v", err) - watcher = nil - } else if err := watcher.Add("/dev/input"); err != nil { - log.Warnf("Failed to watch /dev/input, hotplug detection disabled: %v", err) - watcher.Close() - watcher = nil - } - - monitoredPaths := make(map[string]bool) - for _, device := range devices { - monitoredPaths[device.Path()] = true - } - - m := &Manager{ - devices: devices, - monitoredPaths: monitoredPaths, - state: State{Available: true, CapsLock: initialCapsLock}, - - closeChan: make(chan struct{}), - watcher: watcher, - } - - for i, device := range devices { - go m.monitorDevice(device, i) - } - - if watcher != nil { - go m.watchForNewKeyboards() - } - - return m, nil -} - -func readInitialCapsLockState(device EvdevDevice) bool { - ledStates, err := device.State(evLedType) - if err != nil { - log.Debugf("Could not read LED state: %v", err) - return false - } - - return ledStates[ledCapslockKey] -} - -func findKeyboards() ([]EvdevDevice, error) { - pattern := "/dev/input/event*" - matches, err := filepath.Glob(pattern) - if err != nil { - return nil, fmt.Errorf("failed to glob input devices: %w", err) - } - - if len(matches) == 0 { - return nil, fmt.Errorf("no input devices found") - } - - var keyboards []EvdevDevice - for _, path := range matches { - device, err := evdev.Open(path) - if err != nil { - continue - } - - if !isKeyboard(device) { - device.Close() - continue - } - - deviceName, _ := device.Name() - log.Debugf("Found keyboard: %s at %s", deviceName, path) - keyboards = append(keyboards, device) - } - - if len(keyboards) == 0 { - return nil, fmt.Errorf("no keyboard device found") - } - - return keyboards, nil -} - -func isKeyboard(device EvdevDevice) bool { - deviceName, err := device.Name() - if err != nil { - return false - } - - name := strings.ToLower(deviceName) - - switch { - case strings.Contains(name, "keyboard"): - return true - case strings.Contains(name, "kbd"): - return true - case strings.Contains(name, "input") && strings.Contains(name, "key"): - return true - } - - keyStates, err := device.State(evKeyType) - if err != nil { - return false - } - - hasKeyA := len(keyStates) > 30 - hasKeyZ := len(keyStates) > 44 - hasEnter := len(keyStates) > 28 - - return hasKeyA && hasKeyZ && hasEnter && len(keyStates) > 100 -} - -func (m *Manager) watchForNewKeyboards() { - defer func() { - if r := recover(); r != nil { - log.Errorf("Panic in keyboard hotplug monitor: %v", r) - } - }() - - for { - select { - case <-m.closeChan: - return - case event, ok := <-m.watcher.Events: - if !ok { - return - } - - if !strings.HasPrefix(filepath.Base(event.Name), "event") { - continue - } - - if event.Op&fsnotify.Create == fsnotify.Create { - time.Sleep(100 * time.Millisecond) - - m.devicesMutex.Lock() - if m.monitoredPaths[event.Name] { - m.devicesMutex.Unlock() - continue - } - - device, err := evdev.Open(event.Name) - if err != nil { - m.devicesMutex.Unlock() - continue - } - - if !isKeyboard(device) { - device.Close() - m.devicesMutex.Unlock() - continue - } - - deviceName, _ := device.Name() - log.Debugf("Hotplugged keyboard: %s at %s", deviceName, event.Name) - - m.devices = append(m.devices, device) - m.monitoredPaths[event.Name] = true - deviceIndex := len(m.devices) - 1 - m.devicesMutex.Unlock() - - go m.monitorDevice(device, deviceIndex) - } else if event.Op&fsnotify.Remove == fsnotify.Remove { - m.devicesMutex.Lock() - if !m.monitoredPaths[event.Name] { - m.devicesMutex.Unlock() - continue - } - - delete(m.monitoredPaths, event.Name) - for i, device := range m.devices { - if device != nil && device.Path() == event.Name { - log.Debugf("Keyboard removed: %s", event.Name) - device.Close() - m.devices[i] = nil - break - } - } - m.devicesMutex.Unlock() - } - - case err, ok := <-m.watcher.Errors: - if !ok { - return - } - log.Warnf("Keyboard hotplug watcher error: %v", err) - } - } -} - -func (m *Manager) monitorDevice(device EvdevDevice, deviceIndex int) { - defer func() { - if r := recover(); r != nil { - log.Errorf("Panic in evdev monitor: %v", r) - } - }() - - for { - select { - case <-m.closeChan: - return - default: - } - - event, err := device.ReadOne() - if err != nil { - if isClosedError(err) { - return - } - log.Warnf("Failed to read evdev event: %v", err) - time.Sleep(100 * time.Millisecond) - continue - } - - if event == nil { - continue - } - - if event.Type == evKeyType && event.Code == keyCapslockKey && event.Value == keyStateOn { - time.Sleep(50 * time.Millisecond) - m.readAndUpdateCapsLockState(deviceIndex) - } else if event.Type == evLedType && event.Code == ledCapslockKey { - capsLockState := event.Value == keyStateOn - m.updateCapsLockStateDirect(capsLockState) - } - } -} - -func isClosedError(err error) bool { - if err == nil { - return false - } - - errStr := err.Error() - switch { - case strings.Contains(errStr, "closed"): - return true - case strings.Contains(errStr, "bad file descriptor"): - return true - default: - return false - } -} - -func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) { - m.devicesMutex.RLock() - if deviceIndex >= len(m.devices) { - m.devicesMutex.RUnlock() - return - } - device := m.devices[deviceIndex] - m.devicesMutex.RUnlock() - - ledStates, err := device.State(evLedType) - if err != nil { - log.Warnf("Failed to read LED state: %v", err) - return - } - - if len(ledStates) == 0 { - log.Debug("No LED state available (empty map)") - - // This means the device either: - // - doesn't support LED reporting at all, or - // - the kernel returned an empty state - return - } - - capsLockState := ledStates[ledCapslockKey] - m.updateCapsLockStateDirect(capsLockState) -} - -func (m *Manager) updateCapsLockStateDirect(capsLockState bool) { - m.stateMutex.Lock() - if m.state.CapsLock == capsLockState { - m.stateMutex.Unlock() - return - } - - m.state.CapsLock = capsLockState - newState := m.state - m.stateMutex.Unlock() - - log.Debugf("Caps lock state: %v", newState.CapsLock) - m.notifySubscribers(newState) -} - -func (m *Manager) GetState() State { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - return m.state -} - -func (m *Manager) Subscribe(id string) chan State { - ch := make(chan State, 16) - m.subscribers.Store(id, ch) - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) notifySubscribers(state State) { - m.subscribers.Range(func(key string, ch chan State) bool { - select { - case ch <- state: - default: - } - return true - }) -} - -func (m *Manager) Close() { - m.closeOnce.Do(func() { - close(m.closeChan) - - if m.watcher != nil { - m.watcher.Close() - } - - m.devicesMutex.Lock() - for _, device := range m.devices { - if device == nil { - continue - } - if err := device.Close(); err != nil && !isClosedError(err) { - log.Warnf("Error closing evdev device: %v", err) - } - } - m.devicesMutex.Unlock() - - m.subscribers.Range(func(key string, ch chan State) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - }) -} - -func InitializeManager() (*Manager, error) { - if os.Getuid() != 0 && !hasInputGroupAccess() { - return nil, fmt.Errorf("insufficient permissions to access input devices") - } - - return NewManager() -} - -func hasInputGroupAccess() bool { - pattern := "/dev/input/event*" - matches, err := filepath.Glob(pattern) - if err != nil || len(matches) == 0 { - return false - } - - testFile, err := os.Open(matches[0]) - if err != nil { - return false - } - testFile.Close() - return true -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager_test.go deleted file mode 100644 index 667c078..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager_test.go +++ /dev/null @@ -1,344 +0,0 @@ -package evdev - -import ( - "errors" - "testing" - - evdev "github.com/holoplot/go-evdev" - "github.com/stretchr/testify/assert" - - mocks "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/evdev" -) - -func TestManager_Creation(t *testing.T) { - t.Run("manager created successfully with caps lock off", func(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() - - m := &Manager{ - devices: []EvdevDevice{mockDevice}, - state: State{Available: true, CapsLock: false}, - closeChan: make(chan struct{}), - } - - assert.NotNil(t, m) - assert.True(t, m.state.Available) - assert.False(t, m.state.CapsLock) - }) - - t.Run("manager created successfully with caps lock on", func(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() - - m := &Manager{ - devices: []EvdevDevice{mockDevice}, - state: State{Available: true, CapsLock: true}, - closeChan: make(chan struct{}), - } - - assert.NotNil(t, m) - assert.True(t, m.state.Available) - assert.True(t, m.state.CapsLock) - }) -} - -func TestManager_GetState(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() - - m := &Manager{ - devices: []EvdevDevice{mockDevice}, - monitoredPaths: make(map[string]bool), - state: State{Available: true, CapsLock: false}, - closeChan: make(chan struct{}), - } - - state := m.GetState() - assert.True(t, state.Available) - assert.False(t, state.CapsLock) -} - -func TestManager_Subscribe(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() - - m := &Manager{ - devices: []EvdevDevice{mockDevice}, - monitoredPaths: make(map[string]bool), - state: State{Available: true, CapsLock: false}, - closeChan: make(chan struct{}), - } - - ch := m.Subscribe("test-client") - assert.NotNil(t, ch) - count := 0 - m.subscribers.Range(func(key string, ch chan State) bool { - count++ - return true - }) - assert.Equal(t, 1, count) -} - -func TestManager_Unsubscribe(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() - - m := &Manager{ - devices: []EvdevDevice{mockDevice}, - monitoredPaths: make(map[string]bool), - state: State{Available: true, CapsLock: false}, - closeChan: make(chan struct{}), - } - - ch := m.Subscribe("test-client") - count := 0 - m.subscribers.Range(func(key string, ch chan State) bool { - count++ - return true - }) - assert.Equal(t, 1, count) - - m.Unsubscribe("test-client") - count = 0 - m.subscribers.Range(func(key string, ch chan State) bool { - count++ - return true - }) - assert.Equal(t, 0, count) - - select { - case _, ok := <-ch: - assert.False(t, ok, "channel should be closed") - default: - t.Error("channel should be closed") - } -} - -func TestManager_UpdateCapsLock(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() - - m := &Manager{ - devices: []EvdevDevice{mockDevice}, - monitoredPaths: make(map[string]bool), - state: State{Available: true, CapsLock: false}, - closeChan: make(chan struct{}), - } - - ch := m.Subscribe("test-client") - - ledStateOn := evdev.StateMap{ledCapslockKey: true} - mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledStateOn, nil).Once() - - go func() { - m.readAndUpdateCapsLockState(0) - }() - - newState := <-ch - assert.True(t, newState.CapsLock) - - ledStateOff := evdev.StateMap{ledCapslockKey: false} - mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledStateOff, nil).Once() - - go func() { - m.readAndUpdateCapsLockState(0) - }() - - newState = <-ch - assert.False(t, newState.CapsLock) -} - -func TestManager_Close(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().Close().Return(nil).Once() - mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() - - m := &Manager{ - devices: []EvdevDevice{mockDevice}, - monitoredPaths: make(map[string]bool), - state: State{Available: true, CapsLock: false}, - closeChan: make(chan struct{}), - } - - ch1 := m.Subscribe("client1") - ch2 := m.Subscribe("client2") - - m.Close() - - select { - case _, ok := <-ch1: - assert.False(t, ok, "channel 1 should be closed") - default: - t.Error("channel 1 should be closed") - } - - select { - case _, ok := <-ch2: - assert.False(t, ok, "channel 2 should be closed") - default: - t.Error("channel 2 should be closed") - } - - count := 0 - m.subscribers.Range(func(key string, ch chan State) bool { - count++ - return true - }) - assert.Equal(t, 0, count) - - m.Close() -} - -func TestIsKeyboard(t *testing.T) { - tests := []struct { - name string - devName string - expected bool - }{ - {"keyboard in name", "AT Translated Set 2 keyboard", true}, - {"kbd in name", "USB kbd", true}, - {"input and key", "input key device", true}, - {"random device", "Mouse", false}, - {"empty name", "", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().Name().Return(tt.devName, nil).Once() - - if !tt.expected { - mockDevice.EXPECT().State(evdev.EvType(evKeyType)).Return(evdev.StateMap{}, nil).Maybe() - } - - result := isKeyboard(mockDevice) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestIsKeyboard_ErrorHandling(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().Name().Return("", errors.New("device error")).Once() - - result := isKeyboard(mockDevice) - assert.False(t, result) -} - -func TestManager_MonitorDevice(t *testing.T) { - t.Run("caps lock key press updates state", func(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - - capsLockEvent := &evdev.InputEvent{ - Type: evKeyType, - Code: keyCapslockKey, - Value: keyStateOn, - } - - ledState := evdev.StateMap{ledCapslockKey: true} - - mockDevice.EXPECT().ReadOne().Return(capsLockEvent, nil).Once() - mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once() - mockDevice.EXPECT().ReadOne().Return(nil, errors.New("stop")).Maybe() - mockDevice.EXPECT().Close().Return(nil).Maybe() - - m := &Manager{ - devices: []EvdevDevice{mockDevice}, - state: State{Available: true, CapsLock: false}, - closeChan: make(chan struct{}), - } - - ch := m.Subscribe("test") - - go m.monitorDevice(mockDevice, 0) - - state := <-ch - assert.True(t, state.CapsLock) - - m.Close() - }) -} - -func TestIsClosedError(t *testing.T) { - tests := []struct { - name string - err error - expected bool - }{ - {"nil error", nil, false}, - {"closed error", errors.New("device closed"), true}, - {"bad file descriptor", errors.New("bad file descriptor"), true}, - {"other error", errors.New("some other error"), false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := isClosedError(tt.err) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestNotifySubscribers(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() - mockDevice.EXPECT().Close().Return(nil).Maybe() - - m := &Manager{ - devices: []EvdevDevice{mockDevice}, - monitoredPaths: make(map[string]bool), - state: State{Available: true, CapsLock: false}, - closeChan: make(chan struct{}), - } - - ch1 := m.Subscribe("client1") - ch2 := m.Subscribe("client2") - - newState := State{Available: true, CapsLock: true} - go m.notifySubscribers(newState) - - state1 := <-ch1 - state2 := <-ch2 - - assert.Equal(t, newState, state1) - assert.Equal(t, newState, state2) - - m.Close() -} - -func TestReadInitialCapsLockState(t *testing.T) { - t.Run("caps lock is on", func(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - ledState := evdev.StateMap{ - ledCapslockKey: true, - } - mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once() - - result := readInitialCapsLockState(mockDevice) - assert.True(t, result) - }) - - t.Run("caps lock is off", func(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - ledState := evdev.StateMap{ - ledCapslockKey: false, - } - mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once() - - result := readInitialCapsLockState(mockDevice) - assert.False(t, result) - }) - - t.Run("error reading LED state", func(t *testing.T) { - mockDevice := mocks.NewMockEvdevDevice(t) - mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(nil, errors.New("read error")).Once() - - result := readInitialCapsLockState(mockDevice) - assert.False(t, result) - }) -} - -func TestHasInputGroupAccess(t *testing.T) { - result := hasInputGroupAccess() - t.Logf("hasInputGroupAccess: %v", result) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/models.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/models.go deleted file mode 100644 index df8ec9e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/models.go +++ /dev/null @@ -1,6 +0,0 @@ -package evdev - -type State struct { - Available bool `json:"available"` - CapsLock bool `json:"capsLock"` -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/handlers.go deleted file mode 100644 index 3f7e17f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/handlers.go +++ /dev/null @@ -1,134 +0,0 @@ -package extworkspace - -import ( - "encoding/json" - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -type SuccessResult struct { - Success bool `json:"success"` - Message string `json:"message"` -} - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - if manager == nil { - models.RespondError(conn, req.ID, "extworkspace manager not initialized") - return - } - - switch req.Method { - case "extworkspace.getState": - handleGetState(conn, req, manager) - case "extworkspace.activateWorkspace": - handleActivateWorkspace(conn, req, manager) - case "extworkspace.deactivateWorkspace": - handleDeactivateWorkspace(conn, req, manager) - case "extworkspace.removeWorkspace": - handleRemoveWorkspace(conn, req, manager) - case "extworkspace.createWorkspace": - handleCreateWorkspace(conn, req, manager) - case "extworkspace.subscribe": - handleSubscribe(conn, req, manager) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleGetState(conn net.Conn, req models.Request, manager *Manager) { - state := manager.GetState() - models.Respond(conn, req.ID, state) -} - -func handleActivateWorkspace(conn net.Conn, req models.Request, manager *Manager) { - groupID := models.GetOr(req, "groupID", "") - workspaceID, ok := models.Get[string](req, "workspaceID") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter") - return - } - - if err := manager.ActivateWorkspace(groupID, workspaceID); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace activated"}) -} - -func handleDeactivateWorkspace(conn net.Conn, req models.Request, manager *Manager) { - groupID := models.GetOr(req, "groupID", "") - workspaceID, ok := models.Get[string](req, "workspaceID") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter") - return - } - - if err := manager.DeactivateWorkspace(groupID, workspaceID); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace deactivated"}) -} - -func handleRemoveWorkspace(conn net.Conn, req models.Request, manager *Manager) { - groupID := models.GetOr(req, "groupID", "") - workspaceID, ok := models.Get[string](req, "workspaceID") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter") - return - } - - if err := manager.RemoveWorkspace(groupID, workspaceID); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace removed"}) -} - -func handleCreateWorkspace(conn net.Conn, req models.Request, manager *Manager) { - groupID, ok := models.Get[string](req, "groupID") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'groupID' parameter") - return - } - - workspaceName, ok := models.Get[string](req, "name") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'name' parameter") - return - } - - if err := manager.CreateWorkspace(groupID, workspaceName); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace create requested"}) -} - -func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) { - clientID := fmt.Sprintf("client-%p", conn) - stateChan := manager.Subscribe(clientID) - defer manager.Unsubscribe(clientID) - - initialState := manager.GetState() - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - ID: req.ID, - Result: &initialState, - }); err != nil { - return - } - - for state := range stateChan { - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - Result: &state, - }); err != nil { - return - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/manager.go deleted file mode 100644 index b791b39..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/manager.go +++ /dev/null @@ -1,598 +0,0 @@ -package extworkspace - -import ( - "fmt" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace" - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" -) - -func CheckCapability() bool { - display, err := wlclient.Connect("") - if err != nil { - return false - } - defer display.Destroy() - - registry, err := display.GetRegistry() - if err != nil { - return false - } - defer registry.Destroy() - - found := false - - registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { - if e.Interface == ext_workspace.ExtWorkspaceManagerV1InterfaceName { - found = true - } - }) - - // Roundtrip to ensure all registry events are processed - if err := display.Roundtrip(); err != nil { - return false - } - - return found -} - -func NewManager(display wlclient.WaylandDisplay) (*Manager, error) { - m := &Manager{ - display: display, - ctx: display.Context(), - cmdq: make(chan cmd, 128), - stopChan: make(chan struct{}), - - dirty: make(chan struct{}, 1), - } - - m.wg.Add(1) - go m.waylandActor() - - if err := m.setupRegistry(); err != nil { - close(m.stopChan) - m.wg.Wait() - return nil, err - } - - m.updateState() - - m.notifierWg.Add(1) - go m.notifier() - - return m, nil -} - -func (m *Manager) post(fn func()) { - select { - case m.cmdq <- cmd{fn: fn}: - default: - log.Warn("ExtWorkspace actor command queue full, dropping command") - } -} - -func (m *Manager) waylandActor() { - defer m.wg.Done() - - for { - select { - case <-m.stopChan: - return - case c := <-m.cmdq: - c.fn() - } - } -} - -func (m *Manager) setupRegistry() error { - log.Info("ExtWorkspace: starting registry setup") - - registry, err := m.display.GetRegistry() - if err != nil { - return fmt.Errorf("failed to get registry: %w", err) - } - m.registry = registry - - registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { - if e.Interface == "wl_output" { - output := wlclient.NewOutput(m.ctx) - if err := registry.Bind(e.Name, e.Interface, 4, output); err == nil { - outputID := output.ID() - - output.SetNameHandler(func(ev wlclient.OutputNameEvent) { - m.outputNames.Store(outputID, ev.Name) - log.Debugf("ExtWorkspace: Output %d (%s) name received", outputID, ev.Name) - m.post(func() { - m.updateState() - }) - }) - } - return - } - - if e.Interface == ext_workspace.ExtWorkspaceManagerV1InterfaceName { - log.Infof("ExtWorkspace: found %s", ext_workspace.ExtWorkspaceManagerV1InterfaceName) - manager := ext_workspace.NewExtWorkspaceManagerV1(m.ctx) - version := e.Version - if version > 1 { - version = 1 - } - - manager.SetWorkspaceGroupHandler(func(e ext_workspace.ExtWorkspaceManagerV1WorkspaceGroupEvent) { - m.handleWorkspaceGroup(e) - }) - - manager.SetWorkspaceHandler(func(e ext_workspace.ExtWorkspaceManagerV1WorkspaceEvent) { - m.handleWorkspace(e) - }) - - manager.SetDoneHandler(func(e ext_workspace.ExtWorkspaceManagerV1DoneEvent) { - log.Debug("ExtWorkspace: done event received") - m.post(func() { - m.updateState() - }) - }) - - manager.SetFinishedHandler(func(e ext_workspace.ExtWorkspaceManagerV1FinishedEvent) { - log.Info("ExtWorkspace: finished event received") - }) - - if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil { - m.manager = manager - log.Info("ExtWorkspace: manager bound successfully") - } else { - log.Errorf("ExtWorkspace: failed to bind manager: %v", err) - } - } - }) - - log.Info("ExtWorkspace: registry setup complete (events will be processed async)") - return nil -} - -func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1WorkspaceGroupEvent) { - handle := e.WorkspaceGroup - groupID := handle.ID() - - log.Debugf("ExtWorkspace: New workspace group (id=%d)", groupID) - - group := &workspaceGroupState{ - id: groupID, - handle: handle, - outputIDs: make(map[uint32]bool), - workspaceIDs: make([]uint32, 0), - } - - m.groups.Store(groupID, group) - - handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1CapabilitiesEvent) { - log.Debugf("ExtWorkspace: Group %d capabilities: %d", groupID, e.Capabilities) - }) - - handle.SetOutputEnterHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1OutputEnterEvent) { - outputID := e.Output.ID() - log.Debugf("ExtWorkspace: Group %d output enter (output=%d)", groupID, outputID) - - m.post(func() { - group.outputIDs[outputID] = true - m.updateState() - }) - }) - - handle.SetOutputLeaveHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1OutputLeaveEvent) { - outputID := e.Output.ID() - log.Debugf("ExtWorkspace: Group %d output leave (output=%d)", groupID, outputID) - m.post(func() { - delete(group.outputIDs, outputID) - m.updateState() - }) - }) - - handle.SetWorkspaceEnterHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1WorkspaceEnterEvent) { - workspaceID := e.Workspace.ID() - log.Debugf("ExtWorkspace: Group %d workspace enter (workspace=%d)", groupID, workspaceID) - - m.post(func() { - if ws, ok := m.workspaces.Load(workspaceID); ok { - ws.groupID = groupID - } - - group.workspaceIDs = append(group.workspaceIDs, workspaceID) - m.updateState() - }) - }) - - handle.SetWorkspaceLeaveHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1WorkspaceLeaveEvent) { - workspaceID := e.Workspace.ID() - log.Debugf("ExtWorkspace: Group %d workspace leave (workspace=%d)", groupID, workspaceID) - - m.post(func() { - if ws, ok := m.workspaces.Load(workspaceID); ok { - ws.groupID = 0 - } - - for i, id := range group.workspaceIDs { - if id == workspaceID { - group.workspaceIDs = append(group.workspaceIDs[:i], group.workspaceIDs[i+1:]...) - break - } - } - m.updateState() - }) - }) - - handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1RemovedEvent) { - log.Debugf("ExtWorkspace: Group %d removed", groupID) - - m.post(func() { - group.removed = true - - m.groups.Delete(groupID) - - m.wlMutex.Lock() - handle.Destroy() - m.wlMutex.Unlock() - - m.updateState() - }) - }) -} - -func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1WorkspaceEvent) { - handle := e.Workspace - workspaceID := handle.ID() - - log.Debugf("ExtWorkspace: New workspace (proxy_id=%d)", workspaceID) - - ws := &workspaceState{ - id: workspaceID, - handle: handle, - coordinates: make([]uint32, 0), - } - - m.workspaces.Store(workspaceID, ws) - - handle.SetIdHandler(func(e ext_workspace.ExtWorkspaceHandleV1IdEvent) { - log.Debugf("ExtWorkspace: Workspace %d id: %s", workspaceID, e.Id) - m.post(func() { - ws.workspaceID = e.Id - m.updateState() - }) - }) - - handle.SetNameHandler(func(e ext_workspace.ExtWorkspaceHandleV1NameEvent) { - log.Debugf("ExtWorkspace: Workspace %d name: %s", workspaceID, e.Name) - m.post(func() { - ws.name = e.Name - m.updateState() - }) - }) - - handle.SetCoordinatesHandler(func(e ext_workspace.ExtWorkspaceHandleV1CoordinatesEvent) { - coords := make([]uint32, 0) - for i := 0; i < len(e.Coordinates); i += 4 { - if i+4 <= len(e.Coordinates) { - val := uint32(e.Coordinates[i]) | - uint32(e.Coordinates[i+1])<<8 | - uint32(e.Coordinates[i+2])<<16 | - uint32(e.Coordinates[i+3])<<24 - coords = append(coords, val) - } - } - log.Debugf("ExtWorkspace: Workspace %d coordinates: %v", workspaceID, coords) - m.post(func() { - ws.coordinates = coords - m.updateState() - }) - }) - - handle.SetStateHandler(func(e ext_workspace.ExtWorkspaceHandleV1StateEvent) { - log.Debugf("ExtWorkspace: Workspace %d state: %d", workspaceID, e.State) - m.post(func() { - ws.state = e.State - m.updateState() - }) - }) - - handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceHandleV1CapabilitiesEvent) { - log.Debugf("ExtWorkspace: Workspace %d capabilities: %d", workspaceID, e.Capabilities) - }) - - handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceHandleV1RemovedEvent) { - log.Debugf("ExtWorkspace: Workspace %d removed", workspaceID) - - m.post(func() { - ws.removed = true - - m.workspaces.Delete(workspaceID) - - m.wlMutex.Lock() - handle.Destroy() - m.wlMutex.Unlock() - - m.updateState() - }) - }) -} - -func (m *Manager) updateState() { - groups := make([]*WorkspaceGroup, 0) - - m.groups.Range(func(key uint32, group *workspaceGroupState) bool { - if group.removed { - return true - } - - outputs := make([]string, 0) - for outputID := range group.outputIDs { - if name, ok := m.outputNames.Load(outputID); ok && name != "" { - outputs = append(outputs, name) - } - } - - workspaces := make([]*Workspace, 0) - for _, wsID := range group.workspaceIDs { - ws, exists := m.workspaces.Load(wsID) - if !exists { - continue - } - if ws.removed { - continue - } - - workspace := &Workspace{ - ID: ws.workspaceID, - Name: ws.name, - Coordinates: ws.coordinates, - State: ws.state, - Active: ws.state&uint32(ext_workspace.ExtWorkspaceHandleV1StateActive) != 0, - Urgent: ws.state&uint32(ext_workspace.ExtWorkspaceHandleV1StateUrgent) != 0, - Hidden: ws.state&uint32(ext_workspace.ExtWorkspaceHandleV1StateHidden) != 0, - } - workspaces = append(workspaces, workspace) - } - - groupState := &WorkspaceGroup{ - ID: fmt.Sprintf("group-%d", group.id), - Outputs: outputs, - Workspaces: workspaces, - } - groups = append(groups, groupState) - return true - }) - - newState := State{ - Groups: groups, - } - - m.stateMutex.Lock() - m.state = &newState - m.stateMutex.Unlock() - - m.notifySubscribers() -} - -func (m *Manager) notifier() { - defer m.notifierWg.Done() - const minGap = 100 * time.Millisecond - timer := time.NewTimer(minGap) - timer.Stop() - var pending bool - - for { - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.dirty: - if pending { - continue - } - pending = true - timer.Reset(minGap) - case <-timer.C: - if !pending { - continue - } - - currentState := m.GetState() - - if m.lastNotified != nil && !stateChanged(m.lastNotified, ¤tState) { - pending = false - continue - } - - m.subscribers.Range(func(key string, ch chan State) bool { - select { - case ch <- currentState: - default: - log.Warn("ExtWorkspace: subscriber channel full, dropping update") - } - return true - }) - - stateCopy := currentState - m.lastNotified = &stateCopy - pending = false - } - } -} - -func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error { - errChan := make(chan error, 1) - - m.post(func() { - var targetGroupID uint32 - if groupID != "" { - var parsedID uint32 - if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil { - targetGroupID = parsedID - } - } - - var found bool - m.workspaces.Range(func(key uint32, ws *workspaceState) bool { - if targetGroupID != 0 && ws.groupID != targetGroupID { - return true - } - if ws.workspaceID == workspaceID || ws.name == workspaceID { - m.wlMutex.Lock() - err := ws.handle.Activate() - if err == nil { - err = m.manager.Commit() - } - m.wlMutex.Unlock() - errChan <- err - found = true - return false - } - return true - }) - - if !found { - errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) - } - }) - - return <-errChan -} - -func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error { - errChan := make(chan error, 1) - - m.post(func() { - var targetGroupID uint32 - if groupID != "" { - var parsedID uint32 - if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil { - targetGroupID = parsedID - } - } - - var found bool - m.workspaces.Range(func(key uint32, ws *workspaceState) bool { - if targetGroupID != 0 && ws.groupID != targetGroupID { - return true - } - if ws.workspaceID == workspaceID || ws.name == workspaceID { - m.wlMutex.Lock() - err := ws.handle.Deactivate() - if err == nil { - err = m.manager.Commit() - } - m.wlMutex.Unlock() - errChan <- err - found = true - return false - } - return true - }) - - if !found { - errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) - } - }) - - return <-errChan -} - -func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error { - errChan := make(chan error, 1) - - m.post(func() { - var targetGroupID uint32 - if groupID != "" { - var parsedID uint32 - if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil { - targetGroupID = parsedID - } - } - - var found bool - m.workspaces.Range(func(key uint32, ws *workspaceState) bool { - if targetGroupID != 0 && ws.groupID != targetGroupID { - return true - } - if ws.workspaceID == workspaceID || ws.name == workspaceID { - m.wlMutex.Lock() - err := ws.handle.Remove() - if err == nil { - err = m.manager.Commit() - } - m.wlMutex.Unlock() - errChan <- err - found = true - return false - } - return true - }) - - if !found { - errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) - } - }) - - return <-errChan -} - -func (m *Manager) CreateWorkspace(groupID, workspaceName string) error { - errChan := make(chan error, 1) - - m.post(func() { - var found bool - m.groups.Range(func(key uint32, group *workspaceGroupState) bool { - if fmt.Sprintf("group-%d", group.id) == groupID { - m.wlMutex.Lock() - err := group.handle.CreateWorkspace(workspaceName) - if err == nil { - err = m.manager.Commit() - } - m.wlMutex.Unlock() - errChan <- err - found = true - return false - } - return true - }) - - if !found { - errChan <- fmt.Errorf("workspace group not found: %s", groupID) - } - }) - - return <-errChan -} - -func (m *Manager) Close() { - close(m.stopChan) - m.wg.Wait() - m.notifierWg.Wait() - - m.subscribers.Range(func(key string, ch chan State) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - - m.workspaces.Range(func(key uint32, ws *workspaceState) bool { - if ws.handle != nil { - ws.handle.Destroy() - } - m.workspaces.Delete(key) - return true - }) - - m.groups.Range(func(key uint32, group *workspaceGroupState) bool { - if group.handle != nil { - group.handle.Destroy() - } - m.groups.Delete(key) - return true - }) - - if m.manager != nil { - m.manager.Stop() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/manager_test.go deleted file mode 100644 index b20d457..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/manager_test.go +++ /dev/null @@ -1,392 +0,0 @@ -package extworkspace - -import ( - "errors" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" -) - -func TestStateChanged_BothNil(t *testing.T) { - assert.True(t, stateChanged(nil, nil)) -} - -func TestStateChanged_OneNil(t *testing.T) { - s := &State{Groups: []*WorkspaceGroup{}} - assert.True(t, stateChanged(s, nil)) - assert.True(t, stateChanged(nil, s)) -} - -func TestStateChanged_GroupCountDiffers(t *testing.T) { - a := &State{Groups: []*WorkspaceGroup{{ID: "group-1"}}} - b := &State{Groups: []*WorkspaceGroup{}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_GroupIDDiffers(t *testing.T) { - a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{}, Workspaces: []*Workspace{}}}} - b := &State{Groups: []*WorkspaceGroup{{ID: "group-2", Outputs: []string{}, Workspaces: []*Workspace{}}}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_OutputCountDiffers(t *testing.T) { - a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}, Workspaces: []*Workspace{}}}} - b := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{}, Workspaces: []*Workspace{}}}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_OutputNameDiffers(t *testing.T) { - a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}, Workspaces: []*Workspace{}}}} - b := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"HDMI-A-1"}, Workspaces: []*Workspace{}}}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_WorkspaceCountDiffers(t *testing.T) { - a := &State{Groups: []*WorkspaceGroup{{ - ID: "group-1", - Outputs: []string{}, - Workspaces: []*Workspace{{ID: "1", Name: "ws1"}}, - }}} - b := &State{Groups: []*WorkspaceGroup{{ - ID: "group-1", - Outputs: []string{}, - Workspaces: []*Workspace{}, - }}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_WorkspaceFieldsDiffer(t *testing.T) { - a := &State{Groups: []*WorkspaceGroup{{ - ID: "group-1", - Outputs: []string{}, - Workspaces: []*Workspace{{ - ID: "1", Name: "ws1", State: 0, Active: false, Urgent: false, Hidden: false, - }}, - }}} - b := &State{Groups: []*WorkspaceGroup{{ - ID: "group-1", - Outputs: []string{}, - Workspaces: []*Workspace{{ - ID: "2", Name: "ws1", State: 0, Active: false, Urgent: false, Hidden: false, - }}, - }}} - assert.True(t, stateChanged(a, b)) - - b.Groups[0].Workspaces[0].ID = "1" - b.Groups[0].Workspaces[0].Name = "ws2" - assert.True(t, stateChanged(a, b)) - - b.Groups[0].Workspaces[0].Name = "ws1" - b.Groups[0].Workspaces[0].State = 1 - assert.True(t, stateChanged(a, b)) - - b.Groups[0].Workspaces[0].State = 0 - b.Groups[0].Workspaces[0].Active = true - assert.True(t, stateChanged(a, b)) - - b.Groups[0].Workspaces[0].Active = false - b.Groups[0].Workspaces[0].Urgent = true - assert.True(t, stateChanged(a, b)) - - b.Groups[0].Workspaces[0].Urgent = false - b.Groups[0].Workspaces[0].Hidden = true - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_WorkspaceCoordinatesDiffer(t *testing.T) { - a := &State{Groups: []*WorkspaceGroup{{ - ID: "group-1", - Outputs: []string{}, - Workspaces: []*Workspace{{ - ID: "1", Name: "ws1", Coordinates: []uint32{0, 0}, - }}, - }}} - b := &State{Groups: []*WorkspaceGroup{{ - ID: "group-1", - Outputs: []string{}, - Workspaces: []*Workspace{{ - ID: "1", Name: "ws1", Coordinates: []uint32{1, 0}, - }}, - }}} - assert.True(t, stateChanged(a, b)) - - b.Groups[0].Workspaces[0].Coordinates = []uint32{0} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_Equal(t *testing.T) { - a := &State{Groups: []*WorkspaceGroup{{ - ID: "group-1", - Outputs: []string{"eDP-1", "HDMI-A-1"}, - Workspaces: []*Workspace{ - {ID: "1", Name: "ws1", Coordinates: []uint32{0, 0}, State: 1, Active: true}, - {ID: "2", Name: "ws2", Coordinates: []uint32{1, 0}, State: 0, Active: false}, - }, - }}} - b := &State{Groups: []*WorkspaceGroup{{ - ID: "group-1", - Outputs: []string{"eDP-1", "HDMI-A-1"}, - Workspaces: []*Workspace{ - {ID: "1", Name: "ws1", Coordinates: []uint32{0, 0}, State: 1, Active: true}, - {ID: "2", Name: "ws2", Coordinates: []uint32{1, 0}, State: 0, Active: false}, - }, - }}} - assert.False(t, stateChanged(a, b)) -} - -func TestManager_ConcurrentGetState(t *testing.T) { - m := &Manager{ - state: &State{ - Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}}}, - }, - } - - var wg sync.WaitGroup - const goroutines = 50 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - s := m.GetState() - _ = s.Groups - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.stateMutex.Lock() - m.state = &State{ - Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}}}, - } - m.stateMutex.Unlock() - } - }(i) - } - - wg.Wait() -} - -func TestManager_ConcurrentSubscriberAccess(t *testing.T) { - m := &Manager{ - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - - var wg sync.WaitGroup - const goroutines = 20 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - subID := string(rune('a' + id)) - ch := m.Subscribe(subID) - assert.NotNil(t, ch) - time.Sleep(time.Millisecond) - m.Unsubscribe(subID) - }(i) - } - - wg.Wait() -} - -func TestManager_SyncmapGroupsConcurrentAccess(t *testing.T) { - m := &Manager{} - - var wg sync.WaitGroup - const goroutines = 30 - const iterations = 50 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - key := uint32(id) - - for j := 0; j < iterations; j++ { - state := &workspaceGroupState{ - id: key, - outputIDs: map[uint32]bool{1: true}, - workspaceIDs: []uint32{uint32(j)}, - } - m.groups.Store(key, state) - - if loaded, ok := m.groups.Load(key); ok { - assert.Equal(t, key, loaded.id) - } - - m.groups.Range(func(k uint32, v *workspaceGroupState) bool { - _ = v.id - _ = v.outputIDs - return true - }) - } - - m.groups.Delete(key) - }(i) - } - - wg.Wait() -} - -func TestManager_SyncmapWorkspacesConcurrentAccess(t *testing.T) { - m := &Manager{} - - var wg sync.WaitGroup - const goroutines = 30 - const iterations = 50 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - key := uint32(id) - - for j := 0; j < iterations; j++ { - state := &workspaceState{ - id: key, - workspaceID: "ws-1", - name: "workspace", - state: uint32(j % 4), - coordinates: []uint32{uint32(j), 0}, - } - m.workspaces.Store(key, state) - - if loaded, ok := m.workspaces.Load(key); ok { - assert.Equal(t, key, loaded.id) - } - - m.workspaces.Range(func(k uint32, v *workspaceState) bool { - _ = v.name - _ = v.state - return true - }) - } - - m.workspaces.Delete(key) - }(i) - } - - wg.Wait() -} - -func TestManager_SyncmapOutputNamesConcurrentAccess(t *testing.T) { - m := &Manager{} - - var wg sync.WaitGroup - const goroutines = 30 - const iterations = 50 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - key := uint32(id) - - for j := 0; j < iterations; j++ { - m.outputNames.Store(key, "eDP-1") - - if loaded, ok := m.outputNames.Load(key); ok { - assert.NotEmpty(t, loaded) - } - - m.outputNames.Range(func(k uint32, v string) bool { - _ = v - return true - }) - } - - m.outputNames.Delete(key) - }(i) - } - - wg.Wait() -} - -func TestManager_NotifySubscribersNonBlocking(t *testing.T) { - m := &Manager{ - dirty: make(chan struct{}, 1), - } - - for i := 0; i < 10; i++ { - m.notifySubscribers() - } - - assert.Len(t, m.dirty, 1) -} - -func TestManager_PostQueueFull(t *testing.T) { - m := &Manager{ - cmdq: make(chan cmd, 2), - stopChan: make(chan struct{}), - } - - m.post(func() {}) - m.post(func() {}) - m.post(func() {}) - m.post(func() {}) - - assert.Len(t, m.cmdq, 2) -} - -func TestManager_GetStateNilState(t *testing.T) { - m := &Manager{} - - s := m.GetState() - assert.NotNil(t, s.Groups) - assert.Empty(t, s.Groups) -} - -func TestWorkspace_Fields(t *testing.T) { - ws := Workspace{ - ID: "ws-1", - Name: "workspace 1", - Coordinates: []uint32{0, 0}, - State: 1, - Active: true, - Urgent: false, - Hidden: false, - } - - assert.Equal(t, "ws-1", ws.ID) - assert.Equal(t, "workspace 1", ws.Name) - assert.True(t, ws.Active) - assert.False(t, ws.Urgent) - assert.False(t, ws.Hidden) -} - -func TestWorkspaceGroup_Fields(t *testing.T) { - group := WorkspaceGroup{ - ID: "group-1", - Outputs: []string{"eDP-1", "HDMI-A-1"}, - Workspaces: []*Workspace{ - {ID: "ws-1", Name: "workspace 1"}, - }, - } - - assert.Equal(t, "group-1", group.ID) - assert.Len(t, group.Outputs, 2) - assert.Len(t, group.Workspaces, 1) -} - -func TestNewManager_GetRegistryError(t *testing.T) { - mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) - - mockDisplay.EXPECT().Context().Return(nil) - mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry")) - - _, err := NewManager(mockDisplay) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to get registry") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/types.go deleted file mode 100644 index 903a1cb..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/extworkspace/types.go +++ /dev/null @@ -1,169 +0,0 @@ -package extworkspace - -import ( - "sync" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace" - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" -) - -type Workspace struct { - ID string `json:"id"` - Name string `json:"name"` - Coordinates []uint32 `json:"coordinates"` - State uint32 `json:"state"` - Active bool `json:"active"` - Urgent bool `json:"urgent"` - Hidden bool `json:"hidden"` -} - -type WorkspaceGroup struct { - ID string `json:"id"` - Outputs []string `json:"outputs"` - Workspaces []*Workspace `json:"workspaces"` -} - -type State struct { - Groups []*WorkspaceGroup `json:"groups"` -} - -type cmd struct { - fn func() -} - -type Manager struct { - display wlclient.WaylandDisplay - ctx *wlclient.Context - registry *wlclient.Registry - manager *ext_workspace.ExtWorkspaceManagerV1 - - outputNames syncmap.Map[uint32, string] - - groups syncmap.Map[uint32, *workspaceGroupState] - - workspaces syncmap.Map[uint32, *workspaceState] - - wlMutex sync.Mutex - cmdq chan cmd - stopChan chan struct{} - wg sync.WaitGroup - - subscribers syncmap.Map[string, chan State] - dirty chan struct{} - notifierWg sync.WaitGroup - lastNotified *State - - stateMutex sync.RWMutex - state *State -} - -type workspaceGroupState struct { - id uint32 - handle *ext_workspace.ExtWorkspaceGroupHandleV1 - outputIDs map[uint32]bool - workspaceIDs []uint32 - removed bool -} - -type workspaceState struct { - id uint32 - handle *ext_workspace.ExtWorkspaceHandleV1 - workspaceID string - name string - coordinates []uint32 - state uint32 - groupID uint32 - removed bool -} - -func (m *Manager) GetState() State { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - if m.state == nil { - return State{ - Groups: []*WorkspaceGroup{}, - } - } - stateCopy := *m.state - return stateCopy -} - -func (m *Manager) Subscribe(id string) chan State { - ch := make(chan State, 64) - - m.subscribers.Store(id, ch) - - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if ch, ok := m.subscribers.LoadAndDelete(id); ok { - close(ch) - } -} - -func (m *Manager) notifySubscribers() { - select { - case m.dirty <- struct{}{}: - default: - } -} - -func stateChanged(old, new *State) bool { - if old == nil || new == nil { - return true - } - if len(old.Groups) != len(new.Groups) { - return true - } - - for i, newGroup := range new.Groups { - if i >= len(old.Groups) { - return true - } - oldGroup := old.Groups[i] - if oldGroup.ID != newGroup.ID { - return true - } - if len(oldGroup.Outputs) != len(newGroup.Outputs) { - return true - } - for j, newOutput := range newGroup.Outputs { - if j >= len(oldGroup.Outputs) { - return true - } - if oldGroup.Outputs[j] != newOutput { - return true - } - } - if len(oldGroup.Workspaces) != len(newGroup.Workspaces) { - return true - } - for j, newWs := range newGroup.Workspaces { - if j >= len(oldGroup.Workspaces) { - return true - } - oldWs := oldGroup.Workspaces[j] - if oldWs.ID != newWs.ID || oldWs.Name != newWs.Name || oldWs.State != newWs.State { - return true - } - if oldWs.Active != newWs.Active || oldWs.Urgent != newWs.Urgent || oldWs.Hidden != newWs.Hidden { - return true - } - if len(oldWs.Coordinates) != len(newWs.Coordinates) { - return true - } - for k, coord := range newWs.Coordinates { - if k >= len(oldWs.Coordinates) { - return true - } - if oldWs.Coordinates[k] != coord { - return true - } - } - } - } - - return false -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions.go deleted file mode 100644 index 52515d1..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions.go +++ /dev/null @@ -1,112 +0,0 @@ -package freedesktop - -import ( - "fmt" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" - "github.com/godbus/dbus/v5" -) - -func (m *Manager) SetIconFile(iconPath string) error { - if !m.state.Accounts.Available || m.accountsObj == nil { - return fmt.Errorf("accounts service not available") - } - - err := m.accountsObj.Call(dbusAccountsUserInterface+".SetIconFile", 0, iconPath).Err - if err != nil { - return fmt.Errorf("failed to set icon file: %w", err) - } - - m.updateAccountsState() - return nil -} - -func (m *Manager) SetRealName(name string) error { - if !m.state.Accounts.Available || m.accountsObj == nil { - return fmt.Errorf("accounts service not available") - } - - err := m.accountsObj.Call(dbusAccountsUserInterface+".SetRealName", 0, name).Err - if err != nil { - return fmt.Errorf("failed to set real name: %w", err) - } - - m.updateAccountsState() - return nil -} - -func (m *Manager) SetEmail(email string) error { - if !m.state.Accounts.Available || m.accountsObj == nil { - return fmt.Errorf("accounts service not available") - } - - err := m.accountsObj.Call(dbusAccountsUserInterface+".SetEmail", 0, email).Err - if err != nil { - return fmt.Errorf("failed to set email: %w", err) - } - - m.updateAccountsState() - return nil -} - -func (m *Manager) SetLanguage(language string) error { - if !m.state.Accounts.Available || m.accountsObj == nil { - return fmt.Errorf("accounts service not available") - } - - err := m.accountsObj.Call(dbusAccountsUserInterface+".SetLanguage", 0, language).Err - if err != nil { - return fmt.Errorf("failed to set language: %w", err) - } - - m.updateAccountsState() - return nil -} - -func (m *Manager) SetLocation(location string) error { - if !m.state.Accounts.Available || m.accountsObj == nil { - return fmt.Errorf("accounts service not available") - } - - err := m.accountsObj.Call(dbusAccountsUserInterface+".SetLocation", 0, location).Err - if err != nil { - return fmt.Errorf("failed to set location: %w", err) - } - - m.updateAccountsState() - return nil -} - -func (m *Manager) GetUserIconFile(username string) (string, error) { - if m.systemConn == nil { - return "", fmt.Errorf("accounts service not available") - } - - accountsManager := m.systemConn.Object(dbusAccountsDest, dbus.ObjectPath(dbusAccountsPath)) - - var userPath dbus.ObjectPath - err := accountsManager.Call(dbusAccountsInterface+".FindUserByName", 0, username).Store(&userPath) - if err != nil { - return "", fmt.Errorf("user not found: %w", err) - } - - userObj := m.systemConn.Object(dbusAccountsDest, userPath) - variant, err := userObj.GetProperty(dbusAccountsUserInterface + ".IconFile") - if err != nil { - return "", err - } - - var iconFile string - if err := variant.Store(&iconFile); err != nil { - return "", err - } - - return iconFile, nil -} - -func (m *Manager) SetIconTheme(iconTheme string) error { - if err := utils.GsettingsSet("org.gnome.desktop.interface", "icon-theme", iconTheme); err != nil { - return fmt.Errorf("failed to set icon theme: %w", err) - } - return nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions_test.go deleted file mode 100644 index e2c21e8..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package freedesktop - -import ( - "sync" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestManager_SetIconFile(t *testing.T) { - t.Run("accounts not available", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: false, - }, - }, - stateMutex: sync.RWMutex{}, - } - - err := manager.SetIconFile("/path/to/icon.png") - assert.Error(t, err) - assert.Contains(t, err.Error(), "accounts service not available") - }) -} - -func TestManager_SetRealName(t *testing.T) { - t.Run("accounts not available", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: false, - }, - }, - stateMutex: sync.RWMutex{}, - } - - err := manager.SetRealName("New Name") - assert.Error(t, err) - assert.Contains(t, err.Error(), "accounts service not available") - }) -} - -func TestManager_SetEmail(t *testing.T) { - t.Run("accounts not available", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: false, - }, - }, - stateMutex: sync.RWMutex{}, - } - - err := manager.SetEmail("test@example.com") - assert.Error(t, err) - assert.Contains(t, err.Error(), "accounts service not available") - }) -} - -func TestManager_SetLanguage(t *testing.T) { - t.Run("accounts not available", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: false, - }, - }, - stateMutex: sync.RWMutex{}, - } - - err := manager.SetLanguage("en_US.UTF-8") - assert.Error(t, err) - assert.Contains(t, err.Error(), "accounts service not available") - }) -} - -func TestManager_SetLocation(t *testing.T) { - t.Run("accounts not available", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: false, - }, - }, - stateMutex: sync.RWMutex{}, - } - - err := manager.SetLocation("Test Location") - assert.Error(t, err) - assert.Contains(t, err.Error(), "accounts service not available") - }) -} - -func TestManager_GetUserIconFile(t *testing.T) { - t.Run("accounts not available", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: false, - }, - }, - stateMutex: sync.RWMutex{}, - } - - iconFile, err := manager.GetUserIconFile("testuser") - assert.Error(t, err) - assert.Contains(t, err.Error(), "accounts service not available") - assert.Empty(t, iconFile) - }) -} - -func TestManager_UpdateAccountsState(t *testing.T) { - t.Run("accounts not available", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: false, - }, - }, - stateMutex: sync.RWMutex{}, - } - - err := manager.updateAccountsState() - assert.Error(t, err) - assert.Contains(t, err.Error(), "accounts service not available") - }) -} - -func TestManager_UpdateSettingsState(t *testing.T) { - t.Run("settings not available", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Settings: SettingsState{ - Available: false, - }, - }, - stateMutex: sync.RWMutex{}, - } - - err := manager.updateSettingsState() - assert.Error(t, err) - assert.Contains(t, err.Error(), "settings portal not available") - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/constants.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/constants.go deleted file mode 100644 index 12db87b..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/constants.go +++ /dev/null @@ -1,23 +0,0 @@ -package freedesktop - -const ( - dbusAccountsDest = "org.freedesktop.Accounts" - dbusAccountsPath = "/org/freedesktop/Accounts" - dbusAccountsInterface = "org.freedesktop.Accounts" - dbusAccountsUserInterface = "org.freedesktop.Accounts.User" - - dbusPortalDest = "org.freedesktop.portal.Desktop" - dbusPortalPath = "/org/freedesktop/portal/desktop" - dbusPortalSettingsInterface = "org.freedesktop.portal.Settings" - - dbusPropsInterface = "org.freedesktop.DBus.Properties" - - dbusScreensaverName = "org.freedesktop.ScreenSaver" - dbusScreensaverPath = "/ScreenSaver" - dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver" - dbusScreensaverInterface = "org.freedesktop.ScreenSaver" - - dbusGnomeScreensaverName = "org.gnome.ScreenSaver" - dbusGnomeScreensaverPath = "/org/gnome/ScreenSaver" - dbusGnomeScreensaverInterface = "org.gnome.ScreenSaver" -) diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers.go deleted file mode 100644 index c3090fa..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers.go +++ /dev/null @@ -1,154 +0,0 @@ -package freedesktop - -import ( - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - switch req.Method { - case "freedesktop.getState": - handleGetState(conn, req, manager) - case "freedesktop.accounts.setIconFile": - handleSetIconFile(conn, req, manager) - case "freedesktop.accounts.setRealName": - handleSetRealName(conn, req, manager) - case "freedesktop.accounts.setEmail": - handleSetEmail(conn, req, manager) - case "freedesktop.accounts.setLanguage": - handleSetLanguage(conn, req, manager) - case "freedesktop.accounts.setLocation": - handleSetLocation(conn, req, manager) - case "freedesktop.accounts.getUserIconFile": - handleGetUserIconFile(conn, req, manager) - case "freedesktop.settings.getColorScheme": - handleGetColorScheme(conn, req, manager) - case "freedesktop.settings.setIconTheme": - handleSetIconTheme(conn, req, manager) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleGetState(conn net.Conn, req models.Request, manager *Manager) { - models.Respond(conn, req.ID, manager.GetState()) -} - -func handleSetIconFile(conn net.Conn, req models.Request, manager *Manager) { - iconPath, err := params.String(req.Params, "path") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetIconFile(iconPath); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "icon file set"}) -} - -func handleSetRealName(conn net.Conn, req models.Request, manager *Manager) { - name, err := params.String(req.Params, "name") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetRealName(name); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "real name set"}) -} - -func handleSetEmail(conn net.Conn, req models.Request, manager *Manager) { - email, err := params.String(req.Params, "email") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetEmail(email); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "email set"}) -} - -func handleSetLanguage(conn net.Conn, req models.Request, manager *Manager) { - language, err := params.String(req.Params, "language") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetLanguage(language); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "language set"}) -} - -func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) { - location, err := params.String(req.Params, "location") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetLocation(location); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location set"}) -} - -func handleGetUserIconFile(conn net.Conn, req models.Request, manager *Manager) { - username, err := params.String(req.Params, "username") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - iconFile, err := manager.GetUserIconFile(username) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Value: iconFile}) -} - -func handleGetColorScheme(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.updateSettingsState(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - state := manager.GetState() - models.Respond(conn, req.ID, map[string]uint32{"colorScheme": state.Settings.ColorScheme}) -} - -func handleSetIconTheme(conn net.Conn, req models.Request, manager *Manager) { - iconTheme, err := params.String(req.Params, "iconTheme") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetIconTheme(iconTheme); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "icon theme set"}) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers_test.go deleted file mode 100644 index f2cb5a2..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers_test.go +++ /dev/null @@ -1,581 +0,0 @@ -package freedesktop - -import ( - "bytes" - "encoding/json" - "net" - "sync" - "testing" - - mockdbus "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/godbus/dbus/v5" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/godbus/dbus/v5" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -type mockNetConn struct { - net.Conn - readBuf *bytes.Buffer - writeBuf *bytes.Buffer - closed bool -} - -func newMockNetConn() *mockNetConn { - return &mockNetConn{ - readBuf: &bytes.Buffer{}, - writeBuf: &bytes.Buffer{}, - } -} - -func (m *mockNetConn) Read(b []byte) (n int, err error) { - return m.readBuf.Read(b) -} - -func (m *mockNetConn) Write(b []byte) (n int, err error) { - return m.writeBuf.Write(b) -} - -func (m *mockNetConn) Close() error { - m.closed = true - return nil -} - -func mockGetAllAccountsProperties() *dbus.Call { - props := map[string]dbus.Variant{ - "IconFile": dbus.MakeVariant("/path/to/icon.png"), - "RealName": dbus.MakeVariant("Test"), - "UserName": dbus.MakeVariant("test"), - "AccountType": dbus.MakeVariant(int32(0)), - "HomeDirectory": dbus.MakeVariant("/home/test"), - "Shell": dbus.MakeVariant("/bin/bash"), - "Email": dbus.MakeVariant(""), - "Language": dbus.MakeVariant(""), - "Location": dbus.MakeVariant(""), - "Locked": dbus.MakeVariant(false), - "PasswordMode": dbus.MakeVariant(int32(1)), - } - return &dbus.Call{Err: nil, Body: []any{props}} -} - -func TestRespondError_Freedesktop(t *testing.T) { - conn := newMockNetConn() - models.RespondError(conn, 123, "test error") - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Equal(t, "test error", resp.Error) - assert.Nil(t, resp.Result) -} - -func TestRespond_Freedesktop(t *testing.T) { - conn := newMockNetConn() - result := models.SuccessResult{Success: true, Message: "test"} - models.Respond(conn, 123, result) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "test", resp.Result.Message) -} - -func TestHandleGetState(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: true, - UserName: "testuser", - RealName: "Test User", - UID: 1000, - }, - Settings: SettingsState{ - Available: true, - ColorScheme: 1, - }, - }, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "freedesktop.getState"} - - handleGetState(conn, req, manager) - - var resp models.Response[FreedeskState] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Accounts.Available) - assert.Equal(t, "testuser", resp.Result.Accounts.UserName) - assert.True(t, resp.Result.Settings.Available) - assert.Equal(t, uint32(1), resp.Result.Settings.ColorScheme) -} - -func TestHandleSetIconFile(t *testing.T) { - t.Run("missing path parameter", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{}, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "freedesktop.accounts.setIconFile", - Params: map[string]any{}, - } - - handleSetIconFile(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "missing or invalid 'path' parameter") - }) - - t.Run("successful set icon file", func(t *testing.T) { - mockAccountsObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockAccountsObj.EXPECT().Call("org.freedesktop.Accounts.User.SetIconFile", dbus.Flags(0), "/path/to/icon.png").Return(mockCall) - mockAccountsObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.Accounts.User").Return(mockGetAllAccountsProperties()) - - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: true, - }, - }, - stateMutex: sync.RWMutex{}, - accountsObj: mockAccountsObj, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "freedesktop.accounts.setIconFile", - Params: map[string]any{ - "path": "/path/to/icon.png", - }, - } - - handleSetIconFile(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "icon file set", resp.Result.Message) - }) - - t.Run("accounts not available", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: false, - }, - }, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "freedesktop.accounts.setIconFile", - Params: map[string]any{ - "path": "/path/to/icon.png", - }, - } - - handleSetIconFile(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "accounts service not available") - }) -} - -func TestHandleSetRealName(t *testing.T) { - t.Run("missing name parameter", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{}, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "freedesktop.accounts.setRealName", - Params: map[string]any{}, - } - - handleSetRealName(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "missing or invalid 'name' parameter") - }) - - t.Run("successful set real name", func(t *testing.T) { - mockAccountsObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockAccountsObj.EXPECT().Call("org.freedesktop.Accounts.User.SetRealName", dbus.Flags(0), "New Name").Return(mockCall) - mockAccountsObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.Accounts.User").Return(mockGetAllAccountsProperties()) - - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: true, - }, - }, - stateMutex: sync.RWMutex{}, - accountsObj: mockAccountsObj, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "freedesktop.accounts.setRealName", - Params: map[string]any{ - "name": "New Name", - }, - } - - handleSetRealName(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "real name set", resp.Result.Message) - }) -} - -func TestHandleSetEmail(t *testing.T) { - t.Run("missing email parameter", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{}, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "freedesktop.accounts.setEmail", - Params: map[string]any{}, - } - - handleSetEmail(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "missing or invalid 'email' parameter") - }) - - t.Run("successful set email", func(t *testing.T) { - mockAccountsObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockAccountsObj.EXPECT().Call("org.freedesktop.Accounts.User.SetEmail", dbus.Flags(0), "test@example.com").Return(mockCall) - mockAccountsObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.Accounts.User").Return(mockGetAllAccountsProperties()) - - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: true, - }, - }, - stateMutex: sync.RWMutex{}, - accountsObj: mockAccountsObj, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "freedesktop.accounts.setEmail", - Params: map[string]any{ - "email": "test@example.com", - }, - } - - handleSetEmail(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "email set", resp.Result.Message) - }) -} - -func TestHandleSetLanguage(t *testing.T) { - t.Run("missing language parameter", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{}, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "freedesktop.accounts.setLanguage", - Params: map[string]any{}, - } - - handleSetLanguage(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "missing or invalid 'language' parameter") - }) -} - -func TestHandleSetLocation(t *testing.T) { - t.Run("missing location parameter", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{}, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "freedesktop.accounts.setLocation", - Params: map[string]any{}, - } - - handleSetLocation(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "missing or invalid 'location' parameter") - }) -} - -func TestHandleGetUserIconFile(t *testing.T) { - t.Run("missing username parameter", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{}, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "freedesktop.accounts.getUserIconFile", - Params: map[string]any{}, - } - - handleGetUserIconFile(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "missing or invalid 'username' parameter") - }) - - t.Run("accounts not available", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: false, - }, - }, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "freedesktop.accounts.getUserIconFile", - Params: map[string]any{ - "username": "testuser", - }, - } - - handleGetUserIconFile(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "accounts service not available") - }) -} - -func TestHandleGetColorScheme(t *testing.T) { - t.Run("settings not available", func(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Settings: SettingsState{ - Available: false, - }, - }, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"} - - handleGetColorScheme(conn, req, manager) - - var resp models.Response[map[string]uint32] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "settings portal not available") - }) - - t.Run("successful get color scheme", func(t *testing.T) { - mockSettingsObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{ - Err: nil, - Body: []any{dbus.MakeVariant(uint32(1))}, - } - mockSettingsObj.EXPECT().Call("org.freedesktop.portal.Settings.ReadOne", dbus.Flags(0), "org.freedesktop.appearance", "color-scheme").Return(mockCall) - - manager := &Manager{ - state: &FreedeskState{ - Settings: SettingsState{ - Available: true, - }, - }, - stateMutex: sync.RWMutex{}, - settingsObj: mockSettingsObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"} - - handleGetColorScheme(conn, req, manager) - - var resp models.Response[map[string]uint32] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.Equal(t, uint32(1), (*resp.Result)["colorScheme"]) - }) -} - -func TestHandleRequest(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: true, - UserName: "testuser", - }, - }, - stateMutex: sync.RWMutex{}, - } - - t.Run("unknown method", func(t *testing.T) { - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "freedesktop.unknown", - } - - HandleRequest(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "unknown method") - }) - - t.Run("valid method - getState", func(t *testing.T) { - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "freedesktop.getState", - } - - HandleRequest(conn, req, manager) - - var resp models.Response[FreedeskState] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - }) - - t.Run("all method routes", func(t *testing.T) { - tests := []string{ - "freedesktop.accounts.setIconFile", - "freedesktop.accounts.setRealName", - "freedesktop.accounts.setEmail", - "freedesktop.accounts.setLanguage", - "freedesktop.accounts.setLocation", - "freedesktop.accounts.getUserIconFile", - "freedesktop.settings.getColorScheme", - } - - for _, method := range tests { - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: method, - Params: map[string]any{}, - } - - HandleRequest(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - // Will have errors due to missing params or service unavailable - // but the method routing should work - } - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager.go deleted file mode 100644 index 054f859..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager.go +++ /dev/null @@ -1,206 +0,0 @@ -package freedesktop - -import ( - "context" - "fmt" - "os" - "sync" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil" - "github.com/godbus/dbus/v5" -) - -func NewManager() (*Manager, error) { - systemConn, err := dbus.ConnectSystemBus() - if err != nil { - return nil, fmt.Errorf("failed to connect to system bus: %w", err) - } - - sessionConn, err := dbus.ConnectSessionBus() - if err != nil { - sessionConn = nil - } - - m := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{}, - Settings: SettingsState{}, - Screensaver: ScreensaverState{}, - }, - stateMutex: sync.RWMutex{}, - systemConn: systemConn, - sessionConn: sessionConn, - currentUID: uint64(os.Getuid()), - } - - m.initializeAccounts() - m.initializeSettings() - m.initializeScreensaver() - - return m, nil -} - -func (m *Manager) initializeAccounts() error { - accountsManager := m.systemConn.Object(dbusAccountsDest, dbus.ObjectPath(dbusAccountsPath)) - - var userPath dbus.ObjectPath - err := accountsManager.Call(dbusAccountsInterface+".FindUserById", 0, int64(m.currentUID)).Store(&userPath) - if err != nil { - m.stateMutex.Lock() - m.state.Accounts.Available = false - m.stateMutex.Unlock() - return err - } - - m.accountsObj = m.systemConn.Object(dbusAccountsDest, userPath) - - m.stateMutex.Lock() - m.state.Accounts.Available = true - m.state.Accounts.UserPath = string(userPath) - m.state.Accounts.UID = m.currentUID - m.stateMutex.Unlock() - - if err := m.updateAccountsState(); err != nil { - return fmt.Errorf("failed to update accounts state: %w", err) - } - - return nil -} - -func (m *Manager) initializeSettings() error { - if m.sessionConn == nil { - m.stateMutex.Lock() - m.state.Settings.Available = false - m.stateMutex.Unlock() - return fmt.Errorf("no session bus connection") - } - - m.settingsObj = m.sessionConn.Object(dbusPortalDest, dbus.ObjectPath(dbusPortalPath)) - - var variant dbus.Variant - err := m.settingsObj.Call(dbusPortalSettingsInterface+".ReadOne", 0, "org.freedesktop.appearance", "color-scheme").Store(&variant) - if err != nil { - m.stateMutex.Lock() - m.state.Settings.Available = false - m.stateMutex.Unlock() - return err - } - - m.stateMutex.Lock() - m.state.Settings.Available = true - m.stateMutex.Unlock() - - if err := m.updateSettingsState(); err != nil { - return fmt.Errorf("failed to update settings state: %w", err) - } - - return nil -} - -func (m *Manager) updateAccountsState() error { - if !m.state.Accounts.Available || m.accountsObj == nil { - return fmt.Errorf("accounts service not available") - } - - ctx := context.Background() - props, err := m.getAccountProperties(ctx) - if err != nil { - return err - } - - m.stateMutex.Lock() - defer m.stateMutex.Unlock() - - m.state.Accounts.IconFile = dbusutil.GetOr(props, "IconFile", "") - m.state.Accounts.RealName = dbusutil.GetOr(props, "RealName", "") - m.state.Accounts.UserName = dbusutil.GetOr(props, "UserName", "") - m.state.Accounts.AccountType = dbusutil.GetOr(props, "AccountType", int32(0)) - m.state.Accounts.HomeDirectory = dbusutil.GetOr(props, "HomeDirectory", "") - m.state.Accounts.Shell = dbusutil.GetOr(props, "Shell", "") - m.state.Accounts.Email = dbusutil.GetOr(props, "Email", "") - m.state.Accounts.Language = dbusutil.GetOr(props, "Language", "") - m.state.Accounts.Location = dbusutil.GetOr(props, "Location", "") - m.state.Accounts.Locked = dbusutil.GetOr(props, "Locked", false) - m.state.Accounts.PasswordMode = dbusutil.GetOr(props, "PasswordMode", int32(0)) - - return nil -} - -func (m *Manager) updateSettingsState() error { - if !m.state.Settings.Available || m.settingsObj == nil { - return fmt.Errorf("settings portal not available") - } - - var variant dbus.Variant - err := m.settingsObj.Call(dbusPortalSettingsInterface+".ReadOne", 0, "org.freedesktop.appearance", "color-scheme").Store(&variant) - if err != nil { - return err - } - - if colorScheme, ok := dbusutil.As[uint32](variant); ok { - m.stateMutex.Lock() - m.state.Settings.ColorScheme = colorScheme - m.stateMutex.Unlock() - } - - return nil -} - -func (m *Manager) getAccountProperties(ctx context.Context) (map[string]dbus.Variant, error) { - var props map[string]dbus.Variant - err := m.accountsObj.CallWithContext(ctx, dbusPropsInterface+".GetAll", 0, dbusAccountsUserInterface).Store(&props) - if err != nil { - return nil, err - } - return props, nil -} - -func (m *Manager) GetState() FreedeskState { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - return *m.state -} - -func (m *Manager) Subscribe(id string) chan FreedeskState { - ch := make(chan FreedeskState, 64) - m.subscribers.Store(id, ch) - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) NotifySubscribers() { - state := m.GetState() - m.subscribers.Range(func(key string, ch chan FreedeskState) bool { - select { - case ch <- state: - default: - } - return true - }) -} - -func (m *Manager) Close() { - m.subscribers.Range(func(key string, ch chan FreedeskState) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - - m.screensaverSubscribers.Range(func(key string, ch chan ScreensaverState) bool { - close(ch) - m.screensaverSubscribers.Delete(key) - return true - }) - - if m.systemConn != nil { - m.systemConn.Close() - } - if m.sessionConn != nil { - m.sessionConn.Close() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager_test.go deleted file mode 100644 index b3617f5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package freedesktop - -import ( - "sync" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestManager_GetState(t *testing.T) { - state := &FreedeskState{ - Accounts: AccountsState{ - Available: true, - UserName: "testuser", - RealName: "Test User", - UID: 1000, - }, - Settings: SettingsState{ - Available: true, - ColorScheme: 1, - }, - } - - manager := &Manager{ - state: state, - stateMutex: sync.RWMutex{}, - } - - result := manager.GetState() - assert.True(t, result.Accounts.Available) - assert.Equal(t, "testuser", result.Accounts.UserName) - assert.Equal(t, "Test User", result.Accounts.RealName) - assert.Equal(t, uint64(1000), result.Accounts.UID) - assert.True(t, result.Settings.Available) - assert.Equal(t, uint32(1), result.Settings.ColorScheme) -} - -func TestManager_GetState_ThreadSafe(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: true, - UserName: "testuser", - }, - Settings: SettingsState{ - Available: true, - ColorScheme: 1, - }, - }, - stateMutex: sync.RWMutex{}, - } - - done := make(chan bool) - for i := 0; i < 10; i++ { - go func() { - state := manager.GetState() - assert.True(t, state.Accounts.Available) - assert.Equal(t, "testuser", state.Accounts.UserName) - done <- true - }() - } - - for i := 0; i < 10; i++ { - <-done - } -} - -func TestManager_Close(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{}, - stateMutex: sync.RWMutex{}, - systemConn: nil, - sessionConn: nil, - } - - assert.NotPanics(t, func() { - manager.Close() - }) -} - -func TestNewManager(t *testing.T) { - t.Run("attempts to create manager", func(t *testing.T) { - manager, err := NewManager() - if err != nil { - assert.Nil(t, manager) - } else { - assert.NotNil(t, manager) - assert.NotNil(t, manager.state) - assert.NotNil(t, manager.systemConn) - - manager.Close() - } - }) -} - -func TestManager_GetState_EmptyState(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{}, - stateMutex: sync.RWMutex{}, - } - - result := manager.GetState() - assert.False(t, result.Accounts.Available) - assert.Empty(t, result.Accounts.UserName) - assert.False(t, result.Settings.Available) - assert.Equal(t, uint32(0), result.Settings.ColorScheme) -} - -func TestManager_AccountsState_Modification(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Accounts: AccountsState{ - Available: true, - UserName: "testuser", - }, - }, - stateMutex: sync.RWMutex{}, - } - - state := manager.GetState() - state.Accounts.UserName = "modifieduser" - - original := manager.GetState() - assert.Equal(t, "testuser", original.Accounts.UserName) -} - -func TestManager_SettingsState_Modification(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Settings: SettingsState{ - Available: true, - ColorScheme: 0, - }, - }, - stateMutex: sync.RWMutex{}, - } - - state := manager.GetState() - state.Settings.ColorScheme = 1 - - original := manager.GetState() - assert.Equal(t, uint32(0), original.Settings.ColorScheme) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver.go deleted file mode 100644 index 095a4c0..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver.go +++ /dev/null @@ -1,349 +0,0 @@ -package freedesktop - -import ( - "fmt" - "path/filepath" - "strings" - "sync/atomic" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/godbus/dbus/v5" - "github.com/godbus/dbus/v5/introspect" -) - -type screensaverHandler struct { - manager *Manager -} - -func screensaverIntrospectIface(ifaceName string) introspect.Interface { - return introspect.Interface{ - Name: ifaceName, - Methods: []introspect.Method{ - { - Name: "Inhibit", - Args: []introspect.Arg{ - {Name: "application_name", Type: "s", Direction: "in"}, - {Name: "reason_for_inhibit", Type: "s", Direction: "in"}, - {Name: "cookie", Type: "u", Direction: "out"}, - }, - }, - { - Name: "UnInhibit", - Args: []introspect.Arg{ - {Name: "cookie", Type: "u", Direction: "in"}, - }, - }, - { - Name: "GetActive", - Args: []introspect.Arg{ - {Name: "active", Type: "b", Direction: "out"}, - }, - }, - { - Name: "SetActive", - Args: []introspect.Arg{ - {Name: "active", Type: "b", Direction: "in"}, - }, - }, - { - Name: "Lock", - }, - }, - Signals: []introspect.Signal{ - { - Name: "ActiveChanged", - Args: []introspect.Arg{ - {Name: "new_value", Type: "b"}, - }, - }, - }, - } -} - -func (m *Manager) initializeScreensaver() error { - if m.sessionConn == nil { - m.stateMutex.Lock() - m.state.Screensaver.Available = false - m.stateMutex.Unlock() - return nil - } - - handler := &screensaverHandler{manager: m} - - m.screensaverFreedesktopClaimed = m.claimScreensaverName(handler, - dbusScreensaverName, dbusScreensaverInterface, dbusScreensaverPath, dbusScreensaverPath2) - m.screensaverGnomeClaimed = m.claimScreensaverName(handler, - dbusGnomeScreensaverName, dbusGnomeScreensaverInterface, dbusGnomeScreensaverPath) - - if !m.screensaverFreedesktopClaimed && !m.screensaverGnomeClaimed { - log.Warn("No screensaver interface could be claimed") - m.stateMutex.Lock() - m.state.Screensaver.Available = false - m.stateMutex.Unlock() - return nil - } - - go m.watchPeerDisconnects() - - m.stateMutex.Lock() - m.state.Screensaver.Available = true - m.state.Screensaver.Active = false - m.state.Screensaver.Inhibited = false - m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{} - m.stateMutex.Unlock() - - log.Info("Screensaver listener initialized") - return nil -} - -func (m *Manager) claimScreensaverName(handler *screensaverHandler, name, iface string, paths ...dbus.ObjectPath) bool { - reply, err := m.sessionConn.RequestName(name, dbus.NameFlagDoNotQueue) - if err != nil { - log.Warnf("Failed to request screensaver name %s: %v", name, err) - return false - } - if reply != dbus.RequestNameReplyPrimaryOwner { - log.Warnf("Screensaver name %s already owned by another process", name) - return false - } - if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil { - log.Warnf("Failed to export screensaver on %s: %v", name, err) - return false - } - log.Infof("Claimed %s on session bus", name) - return true -} - -// exportScreensaverOnPaths exports the handler and introspection on the given -// paths under the specified interface name. -func (m *Manager) exportScreensaverOnPaths(handler *screensaverHandler, ifaceName string, paths ...dbus.ObjectPath) error { - iface := screensaverIntrospectIface(ifaceName) - for _, path := range paths { - if err := m.sessionConn.Export(handler, path, ifaceName); err != nil { - return fmt.Errorf("export handler on %s: %w", path, err) - } - node := &introspect.Node{ - Name: string(path), - Interfaces: []introspect.Interface{ - introspect.IntrospectData, - iface, - }, - } - if err := m.sessionConn.Export(introspect.NewIntrospectable(node), path, "org.freedesktop.DBus.Introspectable"); err != nil { - log.Warnf("Failed to export introspectable on %s: %v", path, err) - } - } - return nil -} - -func (h *screensaverHandler) Inhibit(sender dbus.Sender, appName, reason string) (uint32, *dbus.Error) { - if appName == "" { - return 0, dbus.NewError("org.freedesktop.DBus.Error.InvalidArgs", []any{"application name required"}) - } - - if reason == "" { - return 0, dbus.NewError("org.freedesktop.DBus.Error.InvalidArgs", []any{"reason required"}) - } - - if strings.Contains(strings.ToLower(reason), "audio") && !strings.Contains(strings.ToLower(reason), "video") { - log.Debugf("Ignoring audio-only inhibit from %s: %s", appName, reason) - return 0, nil - } - - if idx := strings.LastIndex(appName, "/"); idx != -1 && idx < len(appName)-1 { - appName = appName[idx+1:] - } - appName = filepath.Base(appName) - - cookie := atomic.AddUint32(&h.manager.screensaverCookieCounter, 1) - - inhibitor := ScreensaverInhibitor{ - Cookie: cookie, - AppName: appName, - Reason: reason, - Peer: string(sender), - StartTime: time.Now().Unix(), - } - - h.manager.stateMutex.Lock() - h.manager.state.Screensaver.Inhibitors = append(h.manager.state.Screensaver.Inhibitors, inhibitor) - h.manager.state.Screensaver.Inhibited = len(h.manager.state.Screensaver.Inhibitors) > 0 - h.manager.stateMutex.Unlock() - - log.Infof("Screensaver inhibited by %s (%s): %s -> cookie %08X", appName, sender, reason, cookie) - - h.manager.NotifyScreensaverSubscribers() - - return cookie, nil -} - -func (h *screensaverHandler) UnInhibit(sender dbus.Sender, cookie uint32) *dbus.Error { - h.manager.stateMutex.Lock() - defer h.manager.stateMutex.Unlock() - - found := false - inhibitors := h.manager.state.Screensaver.Inhibitors - for i, inh := range inhibitors { - if inh.Cookie != cookie { - continue - } - log.Infof("Screensaver uninhibited by %s (%s) cookie %08X", inh.AppName, sender, cookie) - h.manager.state.Screensaver.Inhibitors = append(inhibitors[:i], inhibitors[i+1:]...) - found = true - break - } - - if !found { - log.Debugf("UnInhibit: no match for cookie %08X", cookie) - return nil - } - - h.manager.state.Screensaver.Inhibited = len(h.manager.state.Screensaver.Inhibitors) > 0 - - go h.manager.NotifyScreensaverSubscribers() - - return nil -} - -func (m *Manager) watchPeerDisconnects() { - if m.sessionConn == nil { - return - } - - if err := m.sessionConn.AddMatchSignal( - dbus.WithMatchInterface("org.freedesktop.DBus"), - dbus.WithMatchMember("NameOwnerChanged"), - ); err != nil { - log.Warnf("Failed to watch peer disconnects: %v", err) - return - } - - signals := make(chan *dbus.Signal, 64) - m.sessionConn.Signal(signals) - - for sig := range signals { - if sig.Name != "org.freedesktop.DBus.NameOwnerChanged" { - continue - } - if len(sig.Body) < 3 { - continue - } - - name, ok1 := sig.Body[0].(string) - newOwner, ok2 := sig.Body[2].(string) - if !ok1 || !ok2 { - continue - } - if newOwner != "" { - continue - } - - m.removeInhibitorsByPeer(name) - } -} - -func (m *Manager) removeInhibitorsByPeer(peer string) { - m.stateMutex.Lock() - defer m.stateMutex.Unlock() - - var remaining []ScreensaverInhibitor - var removed []ScreensaverInhibitor - for _, inh := range m.state.Screensaver.Inhibitors { - if inh.Peer == peer { - removed = append(removed, inh) - continue - } - remaining = append(remaining, inh) - } - - if len(removed) == 0 { - return - } - - for _, inh := range removed { - log.Infof("Screensaver: peer %s died, removing inhibitor from %s (cookie %08X)", peer, inh.AppName, inh.Cookie) - } - - m.state.Screensaver.Inhibitors = remaining - m.state.Screensaver.Inhibited = len(remaining) > 0 - - go m.NotifyScreensaverSubscribers() -} - -func (m *Manager) GetScreensaverState() ScreensaverState { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - return m.state.Screensaver -} - -func (m *Manager) SubscribeScreensaver(id string) chan ScreensaverState { - ch := make(chan ScreensaverState, 64) - m.screensaverSubscribers.Store(id, ch) - return ch -} - -func (m *Manager) UnsubscribeScreensaver(id string) { - if val, ok := m.screensaverSubscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) NotifyScreensaverSubscribers() { - state := m.GetScreensaverState() - m.screensaverSubscribers.Range(func(key string, ch chan ScreensaverState) bool { - select { - case ch <- state: - default: - } - return true - }) -} - -func (h *screensaverHandler) GetActive() (bool, *dbus.Error) { - h.manager.stateMutex.RLock() - active := h.manager.state.Screensaver.Active - h.manager.stateMutex.RUnlock() - return active, nil -} - -func (h *screensaverHandler) SetActive(active bool) *dbus.Error { - h.manager.SetScreenLockActive(active) - return nil -} - -func (h *screensaverHandler) Lock() *dbus.Error { - h.manager.SetScreenLockActive(true) - return nil -} - -func (m *Manager) SetScreenLockActive(active bool) { - m.stateMutex.Lock() - changed := m.state.Screensaver.Active != active - m.state.Screensaver.Active = active - m.stateMutex.Unlock() - - if !changed { - return - } - - log.Infof("Screen lock active changed: %v", active) - defer m.NotifyScreensaverSubscribers() - - if m.sessionConn == nil { - return - } - if m.screensaverFreedesktopClaimed { - if err := m.sessionConn.Emit(dbusScreensaverPath, dbusScreensaverInterface+".ActiveChanged", active); err != nil { - log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath, err) - } - if err := m.sessionConn.Emit(dbusScreensaverPath2, dbusScreensaverInterface+".ActiveChanged", active); err != nil { - log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath2, err) - } - } - if m.screensaverGnomeClaimed { - if err := m.sessionConn.Emit(dbusGnomeScreensaverPath, dbusGnomeScreensaverInterface+".ActiveChanged", active); err != nil { - log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusGnomeScreensaverPath, err) - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver_test.go deleted file mode 100644 index 324c17e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package freedesktop - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestSetScreenLockActive_ChangesState(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Screensaver: ScreensaverState{Available: true}, - }, - stateMutex: sync.RWMutex{}, - } - - assert.False(t, manager.GetScreensaverState().Active) - - manager.SetScreenLockActive(true) - assert.True(t, manager.GetScreensaverState().Active) - - manager.SetScreenLockActive(false) - assert.False(t, manager.GetScreensaverState().Active) -} - -func TestSetScreenLockActive_NoChangeNoDuplicate(t *testing.T) { - ch := make(chan ScreensaverState, 64) - manager := &Manager{ - state: &FreedeskState{ - Screensaver: ScreensaverState{Available: true, Active: false}, - }, - stateMutex: sync.RWMutex{}, - } - manager.screensaverSubscribers.Store("test", ch) - defer manager.screensaverSubscribers.Delete("test") - - // Setting to same value should not notify - manager.SetScreenLockActive(false) - - select { - case <-ch: - t.Fatal("should not have received notification for no-change") - case <-time.After(50 * time.Millisecond): - // Expected: no notification - } -} - -func TestSetScreenLockActive_NotifiesSubscribers(t *testing.T) { - ch := make(chan ScreensaverState, 64) - manager := &Manager{ - state: &FreedeskState{ - Screensaver: ScreensaverState{Available: true, Active: false}, - }, - stateMutex: sync.RWMutex{}, - } - manager.screensaverSubscribers.Store("test", ch) - defer manager.screensaverSubscribers.Delete("test") - - manager.SetScreenLockActive(true) - - select { - case state := <-ch: - assert.True(t, state.Active) - case <-time.After(time.Second): - t.Fatal("timeout waiting for subscriber notification") - } -} - -func TestSetScreenLockActive_NilSessionConn(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Screensaver: ScreensaverState{Available: true}, - }, - stateMutex: sync.RWMutex{}, - } - - assert.NotPanics(t, func() { - manager.SetScreenLockActive(true) - }) - assert.True(t, manager.GetScreensaverState().Active) -} - -func TestGetActive_ReturnsCurrentState(t *testing.T) { - manager := &Manager{ - state: &FreedeskState{ - Screensaver: ScreensaverState{Available: true, Active: true}, - }, - stateMutex: sync.RWMutex{}, - } - - handler := &screensaverHandler{manager: manager} - active, dbusErr := handler.GetActive() - assert.Nil(t, dbusErr) - assert.True(t, active) -} - -func TestScreensaverState_ActiveDefaultsFalse(t *testing.T) { - state := ScreensaverState{} - assert.False(t, state.Active) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types.go deleted file mode 100644 index 0a9517a..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types.go +++ /dev/null @@ -1,66 +0,0 @@ -package freedesktop - -import ( - "sync" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" - "github.com/godbus/dbus/v5" -) - -type AccountsState struct { - Available bool `json:"available"` - UserPath string `json:"userPath"` - IconFile string `json:"iconFile"` - RealName string `json:"realName"` - UserName string `json:"userName"` - AccountType int32 `json:"accountType"` - HomeDirectory string `json:"homeDirectory"` - Shell string `json:"shell"` - Email string `json:"email"` - Language string `json:"language"` - Location string `json:"location"` - Locked bool `json:"locked"` - PasswordMode int32 `json:"passwordMode"` - UID uint64 `json:"uid"` -} - -type SettingsState struct { - Available bool `json:"available"` - ColorScheme uint32 `json:"colorScheme"` -} - -type ScreensaverInhibitor struct { - Cookie uint32 `json:"cookie"` - AppName string `json:"appName"` - Reason string `json:"reason"` - Peer string `json:"peer"` - StartTime int64 `json:"startTime"` -} - -type ScreensaverState struct { - Available bool `json:"available"` - Active bool `json:"active"` - Inhibited bool `json:"inhibited"` - Inhibitors []ScreensaverInhibitor `json:"inhibitors"` -} - -type FreedeskState struct { - Accounts AccountsState `json:"accounts"` - Settings SettingsState `json:"settings"` - Screensaver ScreensaverState `json:"screensaver"` -} - -type Manager struct { - state *FreedeskState - stateMutex sync.RWMutex - systemConn *dbus.Conn - sessionConn *dbus.Conn - accountsObj dbus.BusObject - settingsObj dbus.BusObject - currentUID uint64 - subscribers syncmap.Map[string, chan FreedeskState] - screensaverSubscribers syncmap.Map[string, chan ScreensaverState] - screensaverCookieCounter uint32 - screensaverFreedesktopClaimed bool - screensaverGnomeClaimed bool -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types_test.go deleted file mode 100644 index deb0327..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package freedesktop - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAccountsState_Struct(t *testing.T) { - state := AccountsState{ - Available: true, - UserPath: "/org/freedesktop/Accounts/User1000", - RealName: "Test User", - UserName: "testuser", - Locked: false, - UID: 1000, - } - - assert.True(t, state.Available) - assert.Equal(t, "/org/freedesktop/Accounts/User1000", state.UserPath) - assert.Equal(t, "Test User", state.RealName) - assert.Equal(t, "testuser", state.UserName) - assert.Equal(t, uint64(1000), state.UID) - assert.False(t, state.Locked) -} - -func TestSettingsState_Struct(t *testing.T) { - state := SettingsState{ - Available: true, - ColorScheme: 1, // Dark mode - } - - assert.True(t, state.Available) - assert.Equal(t, uint32(1), state.ColorScheme) -} - -func TestFreedeskState_Struct(t *testing.T) { - state := FreedeskState{ - Accounts: AccountsState{ - Available: true, - UserName: "testuser", - UID: 1000, - }, - Settings: SettingsState{ - Available: true, - ColorScheme: 0, // Light mode - }, - } - - assert.True(t, state.Accounts.Available) - assert.Equal(t, "testuser", state.Accounts.UserName) - assert.True(t, state.Settings.Available) - assert.Equal(t, uint32(0), state.Settings.ColorScheme) -} - -func TestAccountsState_DefaultValues(t *testing.T) { - state := AccountsState{} - - assert.False(t, state.Available) - assert.Empty(t, state.UserPath) - assert.Empty(t, state.UserName) - assert.Equal(t, uint64(0), state.UID) -} - -func TestSettingsState_DefaultValues(t *testing.T) { - state := SettingsState{} - - assert.False(t, state.Available) - assert.Equal(t, uint32(0), state.ColorScheme) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/location/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/location/handlers.go deleted file mode 100644 index c9d556c..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/location/handlers.go +++ /dev/null @@ -1,61 +0,0 @@ -package location - -import ( - "encoding/json" - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -type LocationEvent struct { - Type string `json:"type"` - Data State `json:"data"` -} - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - switch req.Method { - case "location.getState": - handleGetState(conn, req, manager) - case "location.subscribe": - handleSubscribe(conn, req, manager) - - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleGetState(conn net.Conn, req models.Request, manager *Manager) { - models.Respond(conn, req.ID, manager.GetState()) -} - -func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) { - clientID := fmt.Sprintf("client-%p", conn) - stateChan := manager.Subscribe(clientID) - defer manager.Unsubscribe(clientID) - - initialState := manager.GetState() - event := LocationEvent{ - Type: "state_changed", - Data: initialState, - } - - if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{ - ID: req.ID, - Result: &event, - }); err != nil { - return - } - - for state := range stateChan { - event := LocationEvent{ - Type: "state_changed", - Data: state, - } - if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{ - Result: &event, - }); err != nil { - return - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/location/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/location/manager.go deleted file mode 100644 index 24bcd95..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/location/manager.go +++ /dev/null @@ -1,175 +0,0 @@ -package location - -import ( - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" -) - -func NewManager(client geolocation.Client) (*Manager, error) { - currLocation, err := client.GetLocation() - if err != nil { - log.Warnf("Failed to get initial location: %v", err) - } - - m := &Manager{ - client: client, - dirty: make(chan struct{}), - stopChan: make(chan struct{}), - - state: &State{ - Latitude: currLocation.Latitude, - Longitude: currLocation.Longitude, - }, - } - - if err := m.startSignalPump(); err != nil { - return nil, err - } - - m.notifierWg.Add(1) - go m.notifier() - - return m, nil -} - -func (m *Manager) Close() { - close(m.stopChan) - m.notifierWg.Wait() - - m.sigWG.Wait() - - m.subscribers.Range(func(key string, ch chan State) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) -} - -func (m *Manager) Subscribe(id string) chan State { - ch := make(chan State, 64) - m.subscribers.Store(id, ch) - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if ch, ok := m.subscribers.LoadAndDelete(id); ok { - close(ch) - } -} - -func (m *Manager) startSignalPump() error { - m.sigWG.Add(1) - go func() { - defer m.sigWG.Done() - - subscription := m.client.Subscribe("locationManager") - defer m.client.Unsubscribe("locationManager") - - for { - select { - case <-m.stopChan: - return - case location, ok := <-subscription: - if !ok { - return - } - - m.handleLocationChange(location) - } - } - }() - - return nil -} - -func (m *Manager) handleLocationChange(location geolocation.Location) { - m.stateMutex.Lock() - defer m.stateMutex.Unlock() - - m.state.Latitude = location.Latitude - m.state.Longitude = location.Longitude - - m.notifySubscribers() -} - -func (m *Manager) notifySubscribers() { - select { - case m.dirty <- struct{}{}: - default: - } -} - -func (m *Manager) GetState() State { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - if m.state == nil { - return State{ - Latitude: 0.0, - Longitude: 0.0, - } - } - stateCopy := *m.state - return stateCopy -} - -func (m *Manager) notifier() { - defer m.notifierWg.Done() - const minGap = 200 * time.Millisecond - timer := time.NewTimer(minGap) - timer.Stop() - var pending bool - - for { - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.dirty: - if pending { - continue - } - pending = true - timer.Reset(minGap) - case <-timer.C: - if !pending { - continue - } - - currentState := m.GetState() - - if m.lastNotified != nil && !stateChanged(m.lastNotified, ¤tState) { - pending = false - continue - } - - m.subscribers.Range(func(key string, ch chan State) bool { - select { - case ch <- currentState: - default: - log.Warn("Location: subscriber channel full, dropping update") - } - return true - }) - - stateCopy := currentState - m.lastNotified = &stateCopy - pending = false - } - } -} - -func stateChanged(old, new *State) bool { - if old == nil || new == nil { - return true - } - if old.Latitude != new.Latitude { - return true - } - if old.Longitude != new.Longitude { - return true - } - - return false -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/location/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/location/types.go deleted file mode 100644 index 1f3d518..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/location/types.go +++ /dev/null @@ -1,28 +0,0 @@ -package location - -import ( - "sync" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" -) - -type State struct { - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` -} - -type Manager struct { - state *State - stateMutex sync.RWMutex - - client geolocation.Client - - stopChan chan struct{} - sigWG sync.WaitGroup - - subscribers syncmap.Map[string, chan State] - dirty chan struct{} - notifierWg sync.WaitGroup - lastNotified *State -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/actions.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/actions.go deleted file mode 100644 index 858248e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/actions.go +++ /dev/null @@ -1,88 +0,0 @@ -package loginctl - -import ( - "fmt" -) - -func (m *Manager) Lock() error { - if m.sessionObj == nil { - return fmt.Errorf("session object not available") - } - err := m.sessionObj.Call(dbusSessionInterface+".Lock", 0).Err - if err != nil { - if refreshErr := m.refreshSessionBinding(); refreshErr == nil { - err = m.sessionObj.Call(dbusSessionInterface+".Lock", 0).Err - } - if err != nil { - return fmt.Errorf("failed to lock session: %w", err) - } - } - return nil -} - -func (m *Manager) Unlock() error { - err := m.sessionObj.Call(dbusSessionInterface+".Unlock", 0).Err - if err != nil { - if refreshErr := m.refreshSessionBinding(); refreshErr == nil { - err = m.sessionObj.Call(dbusSessionInterface+".Unlock", 0).Err - } - if err != nil { - return fmt.Errorf("failed to unlock session: %w", err) - } - } - return nil -} - -func (m *Manager) Activate() error { - err := m.sessionObj.Call(dbusSessionInterface+".Activate", 0).Err - if err != nil { - if refreshErr := m.refreshSessionBinding(); refreshErr == nil { - err = m.sessionObj.Call(dbusSessionInterface+".Activate", 0).Err - } - if err != nil { - return fmt.Errorf("failed to activate session: %w", err) - } - } - return nil -} - -func (m *Manager) SetIdleHint(idle bool) error { - err := m.sessionObj.Call(dbusSessionInterface+".SetIdleHint", 0, idle).Err - if err != nil { - if refreshErr := m.refreshSessionBinding(); refreshErr == nil { - err = m.sessionObj.Call(dbusSessionInterface+".SetIdleHint", 0, idle).Err - } - if err != nil { - return fmt.Errorf("failed to set idle hint: %w", err) - } - } - return nil -} - -func (m *Manager) Terminate() error { - err := m.sessionObj.Call(dbusSessionInterface+".Terminate", 0).Err - if err != nil { - if refreshErr := m.refreshSessionBinding(); refreshErr == nil { - err = m.sessionObj.Call(dbusSessionInterface+".Terminate", 0).Err - } - if err != nil { - return fmt.Errorf("failed to terminate session: %w", err) - } - } - return nil -} - -func (m *Manager) SetLockBeforeSuspend(enabled bool) { - m.lockBeforeSuspend.Store(enabled) -} - -func (m *Manager) SetSleepInhibitorEnabled(enabled bool) { - m.sleepInhibitorEnabled.Store(enabled) - if enabled { - // Re-acquire inhibitor if enabled - m.acquireSleepInhibitor() - } else { - // Release inhibitor if disabled - m.releaseSleepInhibitor() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/constants.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/constants.go deleted file mode 100644 index 1a29588..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/constants.go +++ /dev/null @@ -1,10 +0,0 @@ -package loginctl - -const ( - dbusDest = "org.freedesktop.login1" - dbusPath = "/org/freedesktop/login1" - dbusManagerInterface = "org.freedesktop.login1.Manager" - dbusSessionInterface = "org.freedesktop.login1.Session" - dbusUserInterface = "org.freedesktop.login1.User" - dbusPropsInterface = "org.freedesktop.DBus.Properties" -) diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers.go deleted file mode 100644 index 564e996..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers.go +++ /dev/null @@ -1,156 +0,0 @@ -package loginctl - -import ( - "encoding/json" - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - switch req.Method { - case "loginctl.getState": - handleGetState(conn, req, manager) - case "loginctl.lock": - handleLock(conn, req, manager) - case "loginctl.unlock": - handleUnlock(conn, req, manager) - case "loginctl.activate": - handleActivate(conn, req, manager) - case "loginctl.setIdleHint": - handleSetIdleHint(conn, req, manager) - case "loginctl.setLockBeforeSuspend": - handleSetLockBeforeSuspend(conn, req, manager) - case "loginctl.setSleepInhibitorEnabled": - handleSetSleepInhibitorEnabled(conn, req, manager) - case "loginctl.lockerReady": - handleLockerReady(conn, req, manager) - case "loginctl.terminate": - handleTerminate(conn, req, manager) - case "loginctl.subscribe": - handleSubscribe(conn, req, manager) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleGetState(conn net.Conn, req models.Request, manager *Manager) { - models.Respond(conn, req.ID, manager.GetState()) -} - -func handleLock(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.Lock(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "locked"}) -} - -func handleUnlock(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.Unlock(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "unlocked"}) -} - -func handleActivate(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.Activate(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "activated"}) -} - -func handleSetIdleHint(conn net.Conn, req models.Request, manager *Manager) { - idle, err := params.Bool(req.Params, "idle") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetIdleHint(idle); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "idle hint set"}) -} - -func handleSetLockBeforeSuspend(conn net.Conn, req models.Request, manager *Manager) { - enabled, err := params.Bool(req.Params, "enabled") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - manager.SetLockBeforeSuspend(enabled) - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "lock before suspend set"}) -} - -func handleSetSleepInhibitorEnabled(conn net.Conn, req models.Request, manager *Manager) { - enabled, err := params.Bool(req.Params, "enabled") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - manager.SetSleepInhibitorEnabled(enabled) - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sleep inhibitor setting updated"}) -} - -func handleLockerReady(conn net.Conn, req models.Request, manager *Manager) { - manager.lockTimerMu.Lock() - if manager.lockTimer != nil { - manager.lockTimer.Stop() - manager.lockTimer = nil - } - manager.lockTimerMu.Unlock() - - id := manager.sleepCycleID.Load() - manager.releaseForCycle(id) - - if manager.inSleepCycle.Load() { - manager.signalLockerReady() - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "ok"}) -} - -func handleTerminate(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.Terminate(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "terminated"}) -} - -func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) { - clientID := fmt.Sprintf("client-%p", conn) - stateChan := manager.Subscribe(clientID) - defer manager.Unsubscribe(clientID) - - initialState := manager.GetState() - event := SessionEvent{ - Type: EventStateChanged, - Data: initialState, - } - if err := json.NewEncoder(conn).Encode(models.Response[SessionEvent]{ - ID: req.ID, - Result: &event, - }); err != nil { - return - } - - for state := range stateChan { - event := SessionEvent{ - Type: EventStateChanged, - Data: state, - } - if err := json.NewEncoder(conn).Encode(models.Response[SessionEvent]{ - Result: &event, - }); err != nil { - return - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers_test.go deleted file mode 100644 index d3169e4..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers_test.go +++ /dev/null @@ -1,500 +0,0 @@ -package loginctl - -import ( - "bytes" - "encoding/json" - "net" - "sync" - "testing" - "time" - - mockdbus "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/godbus/dbus/v5" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/godbus/dbus/v5" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -type mockNetConn struct { - net.Conn - readBuf *bytes.Buffer - writeBuf *bytes.Buffer - closed bool -} - -func newMockNetConn() *mockNetConn { - return &mockNetConn{ - readBuf: &bytes.Buffer{}, - writeBuf: &bytes.Buffer{}, - } -} - -func (m *mockNetConn) Read(b []byte) (n int, err error) { - return m.readBuf.Read(b) -} - -func (m *mockNetConn) Write(b []byte) (n int, err error) { - return m.writeBuf.Write(b) -} - -func (m *mockNetConn) Close() error { - m.closed = true - return nil -} - -func TestRespondError_Loginctl(t *testing.T) { - conn := newMockNetConn() - models.RespondError(conn, 123, "test error") - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Equal(t, "test error", resp.Error) - assert.Nil(t, resp.Result) -} - -func TestRespond_Loginctl(t *testing.T) { - conn := newMockNetConn() - result := models.SuccessResult{Success: true, Message: "test"} - models.Respond(conn, 123, result) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "test", resp.Result.Message) -} - -func TestHandleGetState(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - Active: true, - SessionType: "wayland", - SessionClass: "user", - UserName: "testuser", - }, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.getState"} - - handleGetState(conn, req, manager) - - var resp models.Response[SessionState] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.Equal(t, "1", resp.Result.SessionID) - assert.False(t, resp.Result.Locked) - assert.True(t, resp.Result.Active) -} - -func TestHandleLock(t *testing.T) { - t.Run("successful lock", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Lock", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.lock"} - handleLock(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "locked", resp.Result.Message) - }) - - t.Run("lock fails", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: assert.AnError} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Lock", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.lock"} - handleLock(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "failed to lock session") - }) -} - -func TestHandleUnlock(t *testing.T) { - t.Run("successful unlock", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Unlock", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.unlock"} - handleUnlock(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "unlocked", resp.Result.Message) - }) - - t.Run("unlock fails", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: assert.AnError} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Unlock", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.unlock"} - handleUnlock(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "failed to unlock session") - }) -} - -func TestHandleActivate(t *testing.T) { - t.Run("successful activate", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Activate", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.activate"} - handleActivate(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "activated", resp.Result.Message) - }) - - t.Run("activate fails", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: assert.AnError} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Activate", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.activate"} - handleActivate(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "failed to activate session") - }) -} - -func TestHandleSetIdleHint(t *testing.T) { - t.Run("missing idle parameter", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "loginctl.setIdleHint", - Params: map[string]any{}, - } - - handleSetIdleHint(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "missing or invalid 'idle' parameter") - }) - - t.Run("successful set idle hint true", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.SetIdleHint", dbus.Flags(0), true).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "loginctl.setIdleHint", - Params: map[string]any{ - "idle": true, - }, - } - - handleSetIdleHint(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "idle hint set", resp.Result.Message) - }) - - t.Run("set idle hint fails", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: assert.AnError} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.SetIdleHint", dbus.Flags(0), false).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "loginctl.setIdleHint", - Params: map[string]any{ - "idle": false, - }, - } - - handleSetIdleHint(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "failed to set idle hint") - }) -} - -func TestHandleTerminate(t *testing.T) { - t.Run("successful terminate", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Terminate", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.terminate"} - handleTerminate(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "terminated", resp.Result.Message) - }) - - t.Run("terminate fails", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: assert.AnError} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Terminate", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.terminate"} - handleTerminate(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "failed to terminate session") - }) -} - -func TestHandleRequest(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - }, - stateMutex: sync.RWMutex{}, - } - - t.Run("unknown method", func(t *testing.T) { - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "loginctl.unknown", - } - - HandleRequest(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "unknown method") - }) - - t.Run("valid method - getState", func(t *testing.T) { - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "loginctl.getState", - } - - HandleRequest(conn, req, manager) - - var resp models.Response[SessionState] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - }) - - t.Run("lock method", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Lock", mock.Anything).Return(mockCall) - - manager.sessionObj = mockSessionObj - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "loginctl.lock", - } - - HandleRequest(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - }) -} - -func TestHandleSubscribe(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - }, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.subscribe"} - - done := make(chan bool) - go func() { - handleSubscribe(conn, req, manager) - done <- true - }() - - time.Sleep(50 * time.Millisecond) - - conn.Close() - - if conn.writeBuf.Len() > 0 { - var resp models.Response[SessionEvent] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - if err == nil { - assert.Equal(t, 123, resp.ID) - require.NotNil(t, resp.Result) - assert.Equal(t, EventStateChanged, resp.Result.Type) - assert.Equal(t, "1", resp.Result.Data.SessionID) - } - } - - select { - case <-done: - case <-time.After(100 * time.Millisecond): - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager.go deleted file mode 100644 index 0c51e64..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager.go +++ /dev/null @@ -1,723 +0,0 @@ -package loginctl - -import ( - "context" - "fmt" - "os" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil" - "github.com/godbus/dbus/v5" -) - -func NewManager() (*Manager, error) { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return nil, fmt.Errorf("failed to connect to system bus: %w", err) - } - - m := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - - stopChan: make(chan struct{}), - conn: conn, - dirty: make(chan struct{}, 1), - signals: make(chan *dbus.Signal, 256), - } - m.sleepInhibitorEnabled.Store(true) - - if err := m.initialize(); err != nil { - conn.Close() - return nil, err - } - - if err := m.acquireSleepInhibitor(); err != nil { - fmt.Fprintf(os.Stderr, "sleep inhibitor unavailable: %v\n", err) - } - - m.notifierWg.Add(1) - go m.notifier() - - if err := m.startSignalPump(); err != nil { - m.Close() - return nil, err - } - - return m, nil -} - -func (m *Manager) initialize() error { - m.managerObj = m.conn.Object(dbusDest, dbus.ObjectPath(dbusPath)) - - m.initializeFallbackDelay() - - sessionID, sessionPath, err := m.discoverSession() - if err != nil { - return fmt.Errorf("failed to get session path: %w", err) - } - - m.stateMutex.Lock() - m.state.SessionID = sessionID - m.state.SessionPath = string(sessionPath) - m.sessionPath = sessionPath - m.stateMutex.Unlock() - - m.sessionObj = m.conn.Object(dbusDest, sessionPath) - - if err := m.updateSessionState(); err != nil { - return err - } - - return nil -} - -func (m *Manager) discoverSession() (string, dbus.ObjectPath, error) { - // 1. Explicit XDG_SESSION_ID - if id := os.Getenv("XDG_SESSION_ID"); id != "" { - if path, err := m.getSession(id); err == nil { - fmt.Fprintf(os.Stderr, "loginctl: using XDG_SESSION_ID=%s\n", id) - return id, path, nil - } - } - - // 2. PID-based lookup (works when caller is inside a session cgroup) - if id, path, err := m.getSessionByPID(uint32(os.Getpid())); err == nil { - fmt.Fprintf(os.Stderr, "loginctl: found session %s via PID\n", id) - return id, path, nil - } - - // 3. User's primary display session (handles UWSM and similar) - if id, path, err := m.getUserDisplaySession(); err == nil { - fmt.Fprintf(os.Stderr, "loginctl: found session %s via User.Display\n", id) - return id, path, nil - } - - // 4. Score all sessions for current UID - if id, path, err := m.findBestSession(); err == nil { - fmt.Fprintf(os.Stderr, "loginctl: found session %s via ListSessions scoring\n", id) - return id, path, nil - } - - // 5. Last resort: "self" - path, err := m.getSession("self") - if err != nil { - return "", "", fmt.Errorf("%w", err) - } - return "self", path, nil -} - -func (m *Manager) getSession(id string) (dbus.ObjectPath, error) { - var out dbus.ObjectPath - err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out) - if err != nil { - return "", err - } - return out, nil -} - -func (m *Manager) getSessionByPID(pid uint32) (string, dbus.ObjectPath, error) { - var path dbus.ObjectPath - if err := m.managerObj.Call(dbusManagerInterface+".GetSessionByPID", 0, pid).Store(&path); err != nil { - return "", "", err - } - - sessionObj := m.conn.Object(dbusDest, path) - var id dbus.Variant - if err := sessionObj.Call(dbusPropsInterface+".Get", 0, dbusSessionInterface, "Id").Store(&id); err != nil { - return "", "", err - } - return id.Value().(string), path, nil -} - -func (m *Manager) getUserDisplaySession() (string, dbus.ObjectPath, error) { - uid := uint32(os.Getuid()) - - var userPath dbus.ObjectPath - if err := m.managerObj.Call(dbusManagerInterface+".GetUser", 0, uid).Store(&userPath); err != nil { - return "", "", err - } - - userObj := m.conn.Object(dbusDest, userPath) - var display dbus.Variant - if err := userObj.Call(dbusPropsInterface+".Get", 0, dbusUserInterface, "Display").Store(&display); err != nil { - return "", "", err - } - - pair, ok := display.Value().([]any) - if !ok || len(pair) < 2 { - return "", "", fmt.Errorf("unexpected Display format") - } - - sessionID, _ := pair[0].(string) - sessionPath, _ := pair[1].(dbus.ObjectPath) - if sessionID == "" || sessionPath == "" { - return "", "", fmt.Errorf("empty Display session") - } - - return sessionID, sessionPath, nil -} - -type sessionCandidate struct { - id string - path dbus.ObjectPath -} - -func (m *Manager) findBestSession() (string, dbus.ObjectPath, error) { - // ListSessions returns a(susso): [][]any where each entry is [id, uid, name, seat, path] - var raw [][]any - if err := m.managerObj.Call(dbusManagerInterface+".ListSessions", 0).Store(&raw); err != nil { - return "", "", err - } - - uid := uint32(os.Getuid()) - var candidates []sessionCandidate - for _, entry := range raw { - if len(entry) < 5 { - continue - } - entryUID, _ := entry[1].(uint32) - if entryUID != uid { - continue - } - id, _ := entry[0].(string) - path, _ := entry[4].(dbus.ObjectPath) - if id != "" && path != "" { - candidates = append(candidates, sessionCandidate{id: id, path: path}) - } - } - if len(candidates) == 0 { - return "", "", fmt.Errorf("no sessions for uid %d", uid) - } - - bestScore := -1 - var best sessionCandidate - for _, c := range candidates { - score := m.scoreSession(c.path) - if score > bestScore { - bestScore = score - best = c - } - } - if bestScore < 0 { - return "", "", fmt.Errorf("no viable session found") - } - return best.id, best.path, nil -} - -func (m *Manager) scoreSession(path dbus.ObjectPath) int { - obj := m.conn.Object(dbusDest, path) - var props map[string]dbus.Variant - if err := obj.Call(dbusPropsInterface+".GetAll", 0, dbusSessionInterface).Store(&props); err != nil { - return -1 - } - - getStr := func(key string) string { - if v, ok := props[key]; ok { - if s, ok := v.Value().(string); ok { - return s - } - } - return "" - } - getBool := func(key string) bool { - if v, ok := props[key]; ok { - if b, ok := v.Value().(bool); ok { - return b - } - } - return false - } - getUint32 := func(key string) uint32 { - if v, ok := props[key]; ok { - if u, ok := v.Value().(uint32); ok { - return u - } - } - return 0 - } - - class := getStr("Class") - if class != "user" { - return -1 - } - if getBool("Remote") { - return -1 - } - - score := 0 - - if getBool("Active") { - score += 100 - } - - switch getStr("Type") { - case "wayland", "x11": - score += 80 - case "tty": - score += 10 - } - - if v, ok := props["Seat"]; ok { - if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 { - if seat, ok := seatArr[0].(string); ok && seat != "" { - score += 40 - if seat == "seat0" { - score += 10 - } - } - } - } - - if getUint32("VTNr") > 0 { - score += 20 - } - - return score -} - -func (m *Manager) refreshSessionBinding() error { - if m.managerObj == nil || m.conn == nil { - return fmt.Errorf("manager not fully initialized") - } - - sessionPath, err := m.getSession(m.state.SessionID) - if err != nil { - return fmt.Errorf("failed to get session path: %w", err) - } - - m.stateMutex.RLock() - currentPath := m.sessionPath - m.stateMutex.RUnlock() - - if sessionPath == currentPath { - return nil - } - - m.stopSignalPump() - - m.stateMutex.Lock() - m.state.SessionPath = string(sessionPath) - m.sessionPath = sessionPath - m.stateMutex.Unlock() - - m.sessionObj = m.conn.Object(dbusDest, sessionPath) - - if err := m.updateSessionState(); err != nil { - return err - } - - m.signals = make(chan *dbus.Signal, 256) - return m.startSignalPump() -} - -func (m *Manager) updateSessionState() error { - ctx := context.Background() - props, err := m.getSessionProperties(ctx) - if err != nil { - return err - } - - m.stateMutex.Lock() - defer m.stateMutex.Unlock() - - m.state.Active = dbusutil.GetOr(props, "Active", m.state.Active) - m.state.IdleHint = dbusutil.GetOr(props, "IdleHint", m.state.IdleHint) - m.state.IdleSinceHint = dbusutil.GetOr(props, "IdleSinceHint", m.state.IdleSinceHint) - if lockedHint, ok := dbusutil.Get[bool](props, "LockedHint"); ok { - m.state.LockedHint = lockedHint - m.state.Locked = lockedHint - } - m.state.SessionType = dbusutil.GetOr(props, "Type", m.state.SessionType) - m.state.SessionClass = dbusutil.GetOr(props, "Class", m.state.SessionClass) - if v, ok := props["User"]; ok { - if userArr, ok := v.Value().([]any); ok && len(userArr) >= 1 { - if uid, ok := userArr[0].(uint32); ok { - m.state.User = uid - } - } - } - m.state.UserName = dbusutil.GetOr(props, "Name", m.state.UserName) - m.state.RemoteHost = dbusutil.GetOr(props, "RemoteHost", m.state.RemoteHost) - m.state.Service = dbusutil.GetOr(props, "Service", m.state.Service) - m.state.TTY = dbusutil.GetOr(props, "TTY", m.state.TTY) - m.state.Display = dbusutil.GetOr(props, "Display", m.state.Display) - m.state.Remote = dbusutil.GetOr(props, "Remote", m.state.Remote) - if v, ok := props["Seat"]; ok { - if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 { - if seatID, ok := seatArr[0].(string); ok { - m.state.Seat = seatID - } - } - } - m.state.VTNr = dbusutil.GetOr(props, "VTNr", m.state.VTNr) - - return nil -} - -func (m *Manager) getSessionProperties(ctx context.Context) (map[string]dbus.Variant, error) { - var props map[string]dbus.Variant - err := m.sessionObj.CallWithContext(ctx, dbusPropsInterface+".GetAll", 0, dbusSessionInterface).Store(&props) - if err != nil { - return nil, err - } - return props, nil -} - -func (m *Manager) acquireSleepInhibitor() error { - if !m.sleepInhibitorEnabled.Load() { - return nil - } - - m.inhibitMu.Lock() - defer m.inhibitMu.Unlock() - - if m.inhibitFile != nil { - return nil - } - - if m.managerObj == nil { - return fmt.Errorf("manager object not available") - } - - file, err := m.inhibit("sleep", "DankMaterialShell", "Lock before suspend", "delay") - if err != nil { - return err - } - - m.inhibitFile = file - return nil -} - -func (m *Manager) inhibit(what, who, why, mode string) (*os.File, error) { - var fd dbus.UnixFD - err := m.managerObj.Call(dbusManagerInterface+".Inhibit", 0, what, who, why, mode).Store(&fd) - if err != nil { - return nil, err - } - return os.NewFile(uintptr(fd), "inhibit"), nil -} - -func (m *Manager) releaseSleepInhibitor() { - m.inhibitMu.Lock() - f := m.inhibitFile - m.inhibitFile = nil - m.inhibitMu.Unlock() - if f != nil { - f.Close() - } -} - -func (m *Manager) releaseForCycle(id uint64) { - if !m.inSleepCycle.Load() || m.sleepCycleID.Load() != id { - return - } - m.releaseSleepInhibitor() -} - -func (m *Manager) initializeFallbackDelay() { - var maxDelayUSec uint64 - err := m.managerObj.Call( - dbusPropsInterface+".Get", - 0, - dbusManagerInterface, - "InhibitDelayMaxUSec", - ).Store(&maxDelayUSec) - - if err != nil { - m.fallbackDelay = 2 * time.Second - return - } - - maxDelay := time.Duration(maxDelayUSec) * time.Microsecond - computed := (maxDelay * 8) / 10 - - if computed < 2*time.Second { - m.fallbackDelay = 2 * time.Second - } else if computed > 4*time.Second { - m.fallbackDelay = 4 * time.Second - } else { - m.fallbackDelay = computed - } -} - -func (m *Manager) newLockerReadyCh() chan struct{} { - m.lockerReadyChMu.Lock() - defer m.lockerReadyChMu.Unlock() - m.lockerReadyCh = make(chan struct{}) - return m.lockerReadyCh -} - -func (m *Manager) signalLockerReady() { - m.lockerReadyChMu.Lock() - ch := m.lockerReadyCh - if ch != nil { - close(ch) - m.lockerReadyCh = nil - } - m.lockerReadyChMu.Unlock() -} - -func (m *Manager) snapshotState() SessionState { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - return *m.state -} - -func stateChangedMeaningfully(old, new *SessionState) bool { - if old.Locked != new.Locked { - return true - } - if old.LockedHint != new.LockedHint { - return true - } - if old.Active != new.Active { - return true - } - if old.IdleHint != new.IdleHint { - return true - } - if old.PreparingForSleep != new.PreparingForSleep { - return true - } - return false -} - -func (m *Manager) GetState() SessionState { - return m.snapshotState() -} - -func (m *Manager) Subscribe(id string) chan SessionState { - ch := make(chan SessionState, 64) - m.subscribers.Store(id, ch) - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) notifier() { - defer m.notifierWg.Done() - const minGap = 100 * time.Millisecond - timer := time.NewTimer(minGap) - timer.Stop() - var pending bool - for { - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.dirty: - if pending { - continue - } - pending = true - timer.Reset(minGap) - case <-timer.C: - if !pending { - continue - } - - currentState := m.snapshotState() - - if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, ¤tState) { - pending = false - continue - } - - m.subscribers.Range(func(key string, ch chan SessionState) bool { - select { - case ch <- currentState: - default: - } - return true - }) - - stateCopy := currentState - m.lastNotifiedState = &stateCopy - pending = false - } - } -} - -func (m *Manager) notifySubscribers() { - select { - case m.dirty <- struct{}{}: - default: - } -} - -func (m *Manager) startSignalPump() error { - m.conn.Signal(m.signals) - - if err := m.conn.AddMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ); err != nil { - m.conn.RemoveSignal(m.signals) - return err - } - if err := m.conn.AddMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Lock"), - ); err != nil { - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - m.conn.RemoveSignal(m.signals) - return err - } - if err := m.conn.AddMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Unlock"), - ); err != nil { - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Lock"), - ) - m.conn.RemoveSignal(m.signals) - return err - } - if err := m.conn.AddMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(dbusPath)), - dbus.WithMatchInterface(dbusManagerInterface), - dbus.WithMatchMember("PrepareForSleep"), - ); err != nil { - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Lock"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Unlock"), - ) - m.conn.RemoveSignal(m.signals) - return err - } - - if err := m.conn.AddMatchSignal( - dbus.WithMatchObjectPath("/org/freedesktop/DBus"), - dbus.WithMatchInterface("org.freedesktop.DBus"), - dbus.WithMatchMember("NameOwnerChanged"), - ); err != nil { - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Lock"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Unlock"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(dbusPath)), - dbus.WithMatchInterface(dbusManagerInterface), - dbus.WithMatchMember("PrepareForSleep"), - ) - m.conn.RemoveSignal(m.signals) - return err - } - - m.sigWG.Add(1) - go func() { - defer m.sigWG.Done() - for { - select { - case <-m.stopChan: - return - case sig, ok := <-m.signals: - if !ok { - return - } - if sig == nil { - continue - } - m.handleDBusSignal(sig) - } - } - }() - return nil -} - -func (m *Manager) stopSignalPump() { - if m.conn == nil { - return - } - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Lock"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Unlock"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(dbusPath)), - dbus.WithMatchInterface(dbusManagerInterface), - dbus.WithMatchMember("PrepareForSleep"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath("/org/freedesktop/DBus"), - dbus.WithMatchInterface("org.freedesktop.DBus"), - dbus.WithMatchMember("NameOwnerChanged"), - ) - - m.conn.RemoveSignal(m.signals) - close(m.signals) - - m.sigWG.Wait() -} - -func (m *Manager) Close() { - close(m.stopChan) - m.notifierWg.Wait() - - m.stopSignalPump() - - m.releaseSleepInhibitor() - - m.subscribers.Range(func(key string, ch chan SessionState) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - - if m.conn != nil { - m.conn.Close() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager_test.go deleted file mode 100644 index 148ee60..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager_test.go +++ /dev/null @@ -1,296 +0,0 @@ -package loginctl - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestManager_GetState(t *testing.T) { - state := &SessionState{ - SessionID: "1", - Locked: false, - Active: true, - IdleHint: false, - SessionType: "wayland", - SessionClass: "user", - UserName: "testuser", - } - - manager := &Manager{ - state: state, - stateMutex: sync.RWMutex{}, - } - - result := manager.GetState() - assert.Equal(t, "1", result.SessionID) - assert.False(t, result.Locked) - assert.True(t, result.Active) - assert.Equal(t, "wayland", result.SessionType) - assert.Equal(t, "testuser", result.UserName) -} - -func TestManager_Subscribe(t *testing.T) { - manager := &Manager{ - state: &SessionState{}, - } - - ch := manager.Subscribe("test-client") - assert.NotNil(t, ch) - assert.Equal(t, 64, cap(ch)) - - _, exists := manager.subscribers.Load("test-client") - assert.True(t, exists) -} - -func TestManager_Unsubscribe(t *testing.T) { - manager := &Manager{ - state: &SessionState{}, - } - - ch := manager.Subscribe("test-client") - - manager.Unsubscribe("test-client") - - _, ok := <-ch - assert.False(t, ok) - - _, exists := manager.subscribers.Load("test-client") - assert.False(t, exists) -} - -func TestManager_Unsubscribe_NonExistent(t *testing.T) { - manager := &Manager{ - state: &SessionState{}, - } - - // Unsubscribe a non-existent client should not panic - assert.NotPanics(t, func() { - manager.Unsubscribe("non-existent") - }) -} - -func TestManager_NotifySubscribers(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - }, - stateMutex: sync.RWMutex{}, - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - manager.notifierWg.Add(1) - go manager.notifier() - - ch := make(chan SessionState, 10) - manager.subscribers.Store("test-client", ch) - - manager.notifySubscribers() - - select { - case state := <-ch: - assert.Equal(t, "1", state.SessionID) - assert.False(t, state.Locked) - case <-time.After(200 * time.Millisecond): - t.Fatal("did not receive state update") - } - - close(manager.stopChan) - manager.notifierWg.Wait() -} - -func TestManager_NotifySubscribers_Debounce(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - }, - stateMutex: sync.RWMutex{}, - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - manager.notifierWg.Add(1) - go manager.notifier() - - ch := make(chan SessionState, 10) - manager.subscribers.Store("test-client", ch) - - manager.notifySubscribers() - manager.notifySubscribers() - manager.notifySubscribers() - - receivedCount := 0 - timeout := time.After(200 * time.Millisecond) - for { - select { - case <-ch: - receivedCount++ - case <-timeout: - assert.Equal(t, 1, receivedCount, "should receive exactly one debounced update") - close(manager.stopChan) - manager.notifierWg.Wait() - return - } - } -} - -func TestManager_Close(t *testing.T) { - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - stopChan: make(chan struct{}), - } - - ch1 := make(chan SessionState, 1) - ch2 := make(chan SessionState, 1) - manager.subscribers.Store("client1", ch1) - manager.subscribers.Store("client2", ch2) - - manager.Close() - - select { - case <-manager.stopChan: - case <-time.After(100 * time.Millisecond): - t.Fatal("stopChan not closed") - } - - _, ok1 := <-ch1 - _, ok2 := <-ch2 - assert.False(t, ok1, "ch1 should be closed") - assert.False(t, ok2, "ch2 should be closed") - - count := 0 - manager.subscribers.Range(func(key string, ch chan SessionState) bool { - count++ - return true - }) - assert.Equal(t, 0, count) -} - -func TestManager_GetState_ThreadSafe(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - Active: true, - }, - stateMutex: sync.RWMutex{}, - } - - done := make(chan bool) - for i := 0; i < 10; i++ { - go func() { - state := manager.GetState() - assert.Equal(t, "1", state.SessionID) - assert.True(t, state.Active) - done <- true - }() - } - - for i := 0; i < 10; i++ { - select { - case <-done: - case <-time.After(1 * time.Second): - t.Fatal("timeout waiting for goroutines") - } - } -} - -func TestStateChangedMeaningfully(t *testing.T) { - tests := []struct { - name string - old *SessionState - new *SessionState - expected bool - }{ - { - name: "no change", - old: &SessionState{Locked: false, Active: true, IdleHint: false}, - new: &SessionState{Locked: false, Active: true, IdleHint: false}, - expected: false, - }, - { - name: "locked changed", - old: &SessionState{Locked: false, Active: true, IdleHint: false}, - new: &SessionState{Locked: true, Active: true, IdleHint: false}, - expected: true, - }, - { - name: "active changed", - old: &SessionState{Locked: false, Active: true, IdleHint: false}, - new: &SessionState{Locked: false, Active: false, IdleHint: false}, - expected: true, - }, - { - name: "idle hint changed", - old: &SessionState{Locked: false, Active: true, IdleHint: false}, - new: &SessionState{Locked: false, Active: true, IdleHint: true}, - expected: true, - }, - { - name: "locked hint changed", - old: &SessionState{Locked: false, Active: true, LockedHint: false}, - new: &SessionState{Locked: false, Active: true, LockedHint: true}, - expected: true, - }, - { - name: "preparing for sleep changed", - old: &SessionState{Locked: false, Active: true, PreparingForSleep: false}, - new: &SessionState{Locked: false, Active: true, PreparingForSleep: true}, - expected: true, - }, - { - name: "non-meaningful change (username)", - old: &SessionState{Locked: false, Active: true, UserName: "user1"}, - new: &SessionState{Locked: false, Active: true, UserName: "user2"}, - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := stateChangedMeaningfully(tt.old, tt.new) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestManager_SnapshotState(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - Active: true, - UserName: "testuser", - }, - stateMutex: sync.RWMutex{}, - } - - snapshot := manager.snapshotState() - assert.Equal(t, "1", snapshot.SessionID) - assert.False(t, snapshot.Locked) - assert.True(t, snapshot.Active) - assert.Equal(t, "testuser", snapshot.UserName) - - snapshot.Locked = true - assert.False(t, manager.state.Locked) -} - -func TestNewManager(t *testing.T) { - t.Run("attempts to create manager", func(t *testing.T) { - manager, err := NewManager() - if err != nil { - assert.Nil(t, manager) - } else { - assert.NotNil(t, manager) - assert.NotNil(t, manager.state) - assert.NotNil(t, &manager.subscribers) - assert.NotNil(t, manager.stopChan) - - manager.Close() - } - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor.go deleted file mode 100644 index 4c0605a..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor.go +++ /dev/null @@ -1,155 +0,0 @@ -package loginctl - -import ( - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil" - "github.com/godbus/dbus/v5" -) - -func (m *Manager) handleDBusSignal(sig *dbus.Signal) { - switch sig.Name { - case dbusSessionInterface + ".Lock": - m.stateMutex.Lock() - m.state.Locked = true - m.state.LockedHint = true - m.stateMutex.Unlock() - m.notifySubscribers() - - if m.sleepInhibitorEnabled.Load() && m.inSleepCycle.Load() { - id := m.sleepCycleID.Load() - m.lockTimerMu.Lock() - if m.lockTimer != nil { - m.lockTimer.Stop() - } - m.lockTimer = time.AfterFunc(m.fallbackDelay, func() { - m.releaseForCycle(id) - }) - m.lockTimerMu.Unlock() - } - - case dbusSessionInterface + ".Unlock": - m.stateMutex.Lock() - m.state.Locked = false - m.state.LockedHint = false - m.stateMutex.Unlock() - m.notifySubscribers() - - // Cancel the lock timer if it's still running - m.lockTimerMu.Lock() - if m.lockTimer != nil { - m.lockTimer.Stop() - m.lockTimer = nil - } - m.lockTimerMu.Unlock() - - // Re-acquire the sleep inhibitor (acquireSleepInhibitor checks the enabled flag) - m.acquireSleepInhibitor() - - case dbusManagerInterface + ".PrepareForSleep": - if len(sig.Body) == 0 { - return - } - preparing, _ := sig.Body[0].(bool) - - if preparing { - cycleID := m.sleepCycleID.Add(1) - m.inSleepCycle.Store(true) - - if m.lockBeforeSuspend.Load() { - m.Lock() - } - - readyCh := m.newLockerReadyCh() - go func(id uint64, ch <-chan struct{}) { - <-ch - if m.inSleepCycle.Load() && m.sleepCycleID.Load() == id { - m.releaseSleepInhibitor() - } - }(cycleID, readyCh) - } else { - m.inSleepCycle.Store(false) - m.signalLockerReady() - m.refreshSessionBinding() - m.acquireSleepInhibitor() - } - - m.stateMutex.Lock() - m.state.PreparingForSleep = preparing - m.stateMutex.Unlock() - m.notifySubscribers() - - case dbusPropsInterface + ".PropertiesChanged": - m.handlePropertiesChanged(sig) - - case "org.freedesktop.DBus.NameOwnerChanged": - if len(sig.Body) == 3 { - name, _ := sig.Body[0].(string) - oldOwner, _ := sig.Body[1].(string) - newOwner, _ := sig.Body[2].(string) - if name == dbusDest && oldOwner != "" && newOwner != "" { - m.updateSessionState() - if !m.inSleepCycle.Load() { - m.acquireSleepInhibitor() - } - m.notifySubscribers() - } - } - } -} - -func (m *Manager) handlePropertiesChanged(sig *dbus.Signal) { - if len(sig.Body) < 2 { - return - } - - iface, ok := sig.Body[0].(string) - if !ok || iface != dbusSessionInterface { - return - } - - changes, ok := sig.Body[1].(map[string]dbus.Variant) - if !ok { - return - } - - var needsUpdate bool - - for key, variant := range changes { - switch key { - case "Active": - if val, ok := dbusutil.As[bool](variant); ok { - m.stateMutex.Lock() - m.state.Active = val - m.stateMutex.Unlock() - needsUpdate = true - } - case "IdleHint": - if val, ok := dbusutil.As[bool](variant); ok { - m.stateMutex.Lock() - m.state.IdleHint = val - m.stateMutex.Unlock() - needsUpdate = true - } - case "IdleSinceHint": - if val, ok := dbusutil.As[uint64](variant); ok { - m.stateMutex.Lock() - m.state.IdleSinceHint = val - m.stateMutex.Unlock() - needsUpdate = true - } - case "LockedHint": - if val, ok := dbusutil.As[bool](variant); ok { - m.stateMutex.Lock() - m.state.LockedHint = val - m.state.Locked = val - m.stateMutex.Unlock() - needsUpdate = true - } - } - } - - if needsUpdate { - m.notifySubscribers() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor_test.go deleted file mode 100644 index ecbb57f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor_test.go +++ /dev/null @@ -1,298 +0,0 @@ -package loginctl - -import ( - "sync" - "testing" - - "github.com/godbus/dbus/v5" - "github.com/stretchr/testify/assert" -) - -func TestManager_HandleDBusSignal_Lock(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - Locked: false, - LockedHint: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.login1.Session.Lock", - } - - manager.handleDBusSignal(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.True(t, manager.state.Locked) - assert.True(t, manager.state.LockedHint) -} - -func TestManager_HandleDBusSignal_Unlock(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - Locked: true, - LockedHint: true, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.login1.Session.Unlock", - } - - manager.handleDBusSignal(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.False(t, manager.state.Locked) - assert.False(t, manager.state.LockedHint) -} - -func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) { - t.Run("preparing for sleep - true", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - PreparingForSleep: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.login1.Manager.PrepareForSleep", - Body: []any{true}, - } - - manager.handleDBusSignal(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.True(t, manager.state.PreparingForSleep) - }) - - t.Run("preparing for sleep - false", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - PreparingForSleep: true, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.login1.Manager.PrepareForSleep", - Body: []any{false}, - } - - manager.handleDBusSignal(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.False(t, manager.state.PreparingForSleep) - }) - - t.Run("empty body", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - PreparingForSleep: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.login1.Manager.PrepareForSleep", - Body: []any{}, - } - - manager.handleDBusSignal(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.False(t, manager.state.PreparingForSleep) - }) -} - -func TestManager_HandlePropertiesChanged(t *testing.T) { - t.Run("active property changed", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - Active: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{ - "org.freedesktop.login1.Session", - map[string]dbus.Variant{ - "Active": dbus.MakeVariant(true), - }, - }, - } - - manager.handlePropertiesChanged(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.True(t, manager.state.Active) - }) - - t.Run("idle hint property changed", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - IdleHint: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{ - "org.freedesktop.login1.Session", - map[string]dbus.Variant{ - "IdleHint": dbus.MakeVariant(true), - }, - }, - } - - manager.handlePropertiesChanged(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.True(t, manager.state.IdleHint) - }) - - t.Run("idle since hint property changed", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - IdleSinceHint: 0, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{ - "org.freedesktop.login1.Session", - map[string]dbus.Variant{ - "IdleSinceHint": dbus.MakeVariant(uint64(123456789)), - }, - }, - } - - manager.handlePropertiesChanged(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.Equal(t, uint64(123456789), manager.state.IdleSinceHint) - }) - - t.Run("locked hint property changed", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - LockedHint: false, - Locked: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{ - "org.freedesktop.login1.Session", - map[string]dbus.Variant{ - "LockedHint": dbus.MakeVariant(true), - }, - }, - } - - manager.handlePropertiesChanged(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.True(t, manager.state.LockedHint) - assert.True(t, manager.state.Locked) - }) - - t.Run("wrong interface", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - Active: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{ - "org.freedesktop.SomeOtherInterface", - map[string]dbus.Variant{ - "Active": dbus.MakeVariant(true), - }, - }, - } - - manager.handlePropertiesChanged(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.False(t, manager.state.Active) - }) - - t.Run("empty body", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{}, - } - - assert.NotPanics(t, func() { - manager.handlePropertiesChanged(sig) - }) - }) - - t.Run("multiple properties changed", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - Active: false, - IdleHint: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{ - "org.freedesktop.login1.Session", - map[string]dbus.Variant{ - "Active": dbus.MakeVariant(true), - "IdleHint": dbus.MakeVariant(true), - }, - }, - } - - manager.handlePropertiesChanged(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.True(t, manager.state.Active) - assert.True(t, manager.state.IdleHint) - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types.go deleted file mode 100644 index 76c7414..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types.go +++ /dev/null @@ -1,76 +0,0 @@ -package loginctl - -import ( - "os" - "sync" - "sync/atomic" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" - "github.com/godbus/dbus/v5" -) - -type SessionState struct { - SessionID string `json:"sessionId"` - SessionPath string `json:"sessionPath"` - Locked bool `json:"locked"` - Active bool `json:"active"` - IdleHint bool `json:"idleHint"` - IdleSinceHint uint64 `json:"idleSinceHint"` - LockedHint bool `json:"lockedHint"` - SessionType string `json:"sessionType"` - SessionClass string `json:"sessionClass"` - User uint32 `json:"user"` - UserName string `json:"userName"` - RemoteHost string `json:"remoteHost"` - Service string `json:"service"` - TTY string `json:"tty"` - Display string `json:"display"` - Remote bool `json:"remote"` - Seat string `json:"seat"` - VTNr uint32 `json:"vtnr"` - PreparingForSleep bool `json:"preparingForSleep"` -} - -type EventType string - -const ( - EventStateChanged EventType = "state_changed" - EventLock EventType = "lock" - EventUnlock EventType = "unlock" - EventPrepareForSleep EventType = "prepare_for_sleep" - EventIdleHintChanged EventType = "idle_hint_changed" - EventLockedHintChanged EventType = "locked_hint_changed" -) - -type SessionEvent struct { - Type EventType `json:"type"` - Data SessionState `json:"data"` -} - -type Manager struct { - state *SessionState - stateMutex sync.RWMutex - subscribers syncmap.Map[string, chan SessionState] - stopChan chan struct{} - conn *dbus.Conn - sessionPath dbus.ObjectPath - managerObj dbus.BusObject - sessionObj dbus.BusObject - dirty chan struct{} - notifierWg sync.WaitGroup - lastNotifiedState *SessionState - signals chan *dbus.Signal - sigWG sync.WaitGroup - inhibitMu sync.Mutex - inhibitFile *os.File - lockBeforeSuspend atomic.Bool - inSleepCycle atomic.Bool - sleepCycleID atomic.Uint64 - lockerReadyChMu sync.Mutex - lockerReadyCh chan struct{} - lockTimerMu sync.Mutex - lockTimer *time.Timer - sleepInhibitorEnabled atomic.Bool - fallbackDelay time.Duration -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types_test.go deleted file mode 100644 index 383300f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package loginctl - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestEventType_Constants(t *testing.T) { - assert.Equal(t, EventType("state_changed"), EventStateChanged) - assert.Equal(t, EventType("lock"), EventLock) - assert.Equal(t, EventType("unlock"), EventUnlock) - assert.Equal(t, EventType("prepare_for_sleep"), EventPrepareForSleep) - assert.Equal(t, EventType("idle_hint_changed"), EventIdleHintChanged) - assert.Equal(t, EventType("locked_hint_changed"), EventLockedHintChanged) -} - -func TestSessionState_Struct(t *testing.T) { - state := SessionState{ - SessionID: "1", - Locked: false, - Active: true, - SessionType: "wayland", - User: 1000, - UserName: "testuser", - } - - assert.Equal(t, "1", state.SessionID) - assert.True(t, state.Active) - assert.False(t, state.Locked) - assert.Equal(t, "wayland", state.SessionType) - assert.Equal(t, uint32(1000), state.User) - assert.Equal(t, "testuser", state.UserName) -} - -func TestSessionEvent_Struct(t *testing.T) { - state := SessionState{ - SessionID: "1", - Locked: true, - } - - event := SessionEvent{ - Type: EventLock, - Data: state, - } - - assert.Equal(t, EventLock, event.Type) - assert.Equal(t, "1", event.Data.SessionID) - assert.True(t, event.Data.Locked) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/matugen_handler.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/matugen_handler.go deleted file mode 100644 index 73c6ad6..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/matugen_handler.go +++ /dev/null @@ -1,79 +0,0 @@ -package server - -import ( - "context" - "net" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/matugen" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -type MatugenQueueResult struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` -} - -func handleMatugenQueue(conn net.Conn, req models.Request) { - opts := matugen.Options{ - StateDir: models.GetOr(req, "stateDir", ""), - ShellDir: models.GetOr(req, "shellDir", ""), - ConfigDir: models.GetOr(req, "configDir", ""), - Kind: models.GetOr(req, "kind", ""), - Value: models.GetOr(req, "value", ""), - Mode: matugen.ColorMode(models.GetOr(req, "mode", "")), - IconTheme: models.GetOr(req, "iconTheme", ""), - MatugenType: models.GetOr(req, "matugenType", ""), - RunUserTemplates: models.GetOr(req, "runUserTemplates", true), - StockColors: models.GetOr(req, "stockColors", ""), - SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false), - TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false), - SkipTemplates: models.GetOr(req, "skipTemplates", ""), - Contrast: models.GetOr(req, "contrast", 0.0), - } - - wait := models.GetOr(req, "wait", true) - - queue := matugen.GetQueue() - resultCh := queue.Submit(opts) - - if !wait { - models.Respond(conn, req.ID, MatugenQueueResult{ - Success: true, - Message: "queued", - }) - return - } - - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - defer cancel() - - select { - case result := <-resultCh: - if result.Error != nil { - if result.Error == context.Canceled { - models.Respond(conn, req.ID, MatugenQueueResult{ - Success: false, - Message: "cancelled", - }) - return - } - models.RespondError(conn, req.ID, result.Error.Error()) - return - } - models.Respond(conn, req.ID, MatugenQueueResult{ - Success: true, - Message: "completed", - }) - case <-ctx.Done(): - models.RespondError(conn, req.ID, "timeout waiting for theme generation") - } -} - -func handleMatugenStatus(conn net.Conn, req models.Request) { - queue := matugen.GetQueue() - models.Respond(conn, req.ID, map[string]bool{ - "running": queue.IsRunning(), - "pending": queue.HasPending(), - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/models/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/models/types.go deleted file mode 100644 index 2933fb5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/models/types.go +++ /dev/null @@ -1,47 +0,0 @@ -package models - -import ( - "encoding/json" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -type Request struct { - ID int `json:"id,omitempty"` - Method string `json:"method"` - Params map[string]any `json:"params,omitempty"` -} - -func Get[T any](r Request, key string) (T, bool) { - v, err := params.Get[T](r.Params, key) - return v, err == nil -} - -func GetOr[T any](r Request, key string, def T) T { - return params.GetOpt(r.Params, key, def) -} - -type Response[T any] struct { - ID int `json:"id,omitempty"` - Result *T `json:"result,omitempty"` - Error string `json:"error,omitempty"` -} - -func RespondError(conn net.Conn, id int, errMsg string) { - log.Errorf("DMS API Error: id=%d error=%s", id, errMsg) - resp := Response[any]{ID: id, Error: errMsg} - json.NewEncoder(conn).Encode(resp) -} - -func Respond[T any](conn net.Conn, id int, result T) { - resp := Response[T]{ID: id, Result: &result} - json.NewEncoder(conn).Encode(resp) -} - -type SuccessResult struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Value string `json:"value,omitempty"` -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/models/types_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/models/types_test.go deleted file mode 100644 index 98f9528..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/models/types_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package models - -import "testing" - -func TestGet(t *testing.T) { - req := Request{Params: map[string]any{"name": "test", "count": 42, "enabled": true}} - - name, ok := Get[string](req, "name") - if !ok || name != "test" { - t.Errorf("Get[string] = %q, %v; want 'test', true", name, ok) - } - - count, ok := Get[int](req, "count") - if !ok || count != 42 { - t.Errorf("Get[int] = %d, %v; want 42, true", count, ok) - } - - enabled, ok := Get[bool](req, "enabled") - if !ok || !enabled { - t.Errorf("Get[bool] = %v, %v; want true, true", enabled, ok) - } - - _, ok = Get[string](req, "missing") - if ok { - t.Error("Get missing key should return false") - } - - _, ok = Get[int](req, "name") - if ok { - t.Error("Get wrong type should return false") - } -} - -func TestGetOr(t *testing.T) { - req := Request{Params: map[string]any{"name": "test", "enabled": true}} - - if v := GetOr(req, "name", "default"); v != "test" { - t.Errorf("GetOr existing = %q; want 'test'", v) - } - - if v := GetOr(req, "missing", "default"); v != "default" { - t.Errorf("GetOr missing = %q; want 'default'", v) - } - - if v := GetOr(req, "enabled", false); !v { - t.Errorf("GetOr bool = %v; want true", v) - } - - if v := GetOr(req, "name", 0); v != 0 { - t.Errorf("GetOr wrong type = %d; want 0 (default)", v) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/API.md b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/API.md deleted file mode 100644 index 361ebac..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/API.md +++ /dev/null @@ -1,552 +0,0 @@ -# NetworkManager API Documentation - -## Overview - -The network manager API provides methods for managing WiFi connections, monitoring network state, and handling credential prompts through NetworkManager or iwd (and systemd-networkd for ethernet only). Communication occurs over a message-based protocol (websocket, IPC, etc.) with event subscriptions for state updates. - -## API Methods - -### network.wifi.connect - -Initiate a WiFi connection. - -**Request:** -```json -{ - "method": "network.wifi.connect", - "params": { - "ssid": "NetworkName", - "password": "optional-password", - "interactive": true - } -} -``` - -**Parameters:** -- `ssid` (string, required): Network SSID -- `password` (string, optional): Pre-shared key for WPA/WPA2/WPA3 networks -- `interactive` (boolean, optional): Enable credential prompting if authentication fails or password is missing. Automatically set to `true` when connecting to secured networks without providing a password. - -**Response:** -```json -{ - "success": true, - "message": "connecting" -} -``` - -**Behavior:** -- Returns immediately; connection happens asynchronously -- State updates delivered via `network` service subscription -- Credential prompts delivered via `network.credentials` service subscription - -### network.credentials.submit - -Submit credentials in response to a prompt. - -**Request:** -```json -{ - "method": "network.credentials.submit", - "params": { - "token": "correlation-token", - "secrets": { - "psk": "password" - }, - "save": true - } -} -``` - -**Parameters:** -- `token` (string, required): Token from credential prompt -- `secrets` (object, required): Key-value map of credential fields -- `save` (boolean, optional): Whether to persist credentials (default: false) - -**Common secret fields:** -- `psk`: Pre-shared key for WPA2/WPA3 personal networks -- `identity`: Username for 802.1X enterprise networks -- `password`: Password for 802.1X enterprise networks - -### network.credentials.cancel - -Cancel a credential prompt. - -**Request:** -```json -{ - "method": "network.credentials.cancel", - "params": { - "token": "correlation-token" - } -} -``` - -## Event Subscriptions - -### Subscribing to Events - -Subscribe to receive network state updates and credential prompts: - -```json -{ - "method": "subscribe", - "params": { - "services": ["network", "network.credentials"] - } -} -``` - -Both services are required for full connection handling. Missing `network.credentials` means credential prompts won't be received. - -### network Service Events - -State updates are sent whenever network configuration changes: - -```json -{ - "service": "network", - "data": { - "networkStatus": "wifi", - "isConnecting": false, - "connectingSSID": "", - "wifiConnected": true, - "wifiSSID": "MyNetwork", - "wifiIP": "192.168.1.100", - "lastError": "" - } -} -``` - -**State fields:** -- `networkStatus`: Current connection type (`wifi`, `ethernet`, `disconnected`) -- `isConnecting`: Whether a connection attempt is in progress -- `connectingSSID`: SSID being connected to (empty when idle) -- `wifiConnected`: Whether associated with an access point -- `wifiSSID`: Currently connected network name -- `wifiIP`: Assigned IP address (empty until DHCP completes) -- `lastError`: Error message from last failed connection attempt - -### network.credentials Service Events - -Credential prompts are sent when authentication is required: - -```json -{ - "service": "network.credentials", - "data": { - "token": "unique-prompt-id", - "ssid": "NetworkName", - "setting": "802-11-wireless-security", - "fields": ["psk"], - "hints": ["wpa3", "sae"], - "reason": "Credentials required" - } -} -``` - -**Prompt fields:** -- `token`: Unique identifier for this prompt (use in submit/cancel) -- `ssid`: Network requesting credentials -- `setting`: Authentication type (`802-11-wireless-security` for personal WiFi, `802-1x` for enterprise) -- `fields`: Array of required credential field names -- `hints`: Additional context about the network type -- `reason`: Human-readable explanation (e.g., "Previous password was incorrect") - -## Connection Flow - -### Typical Timeline - -``` -T+0ms Call network.wifi.connect -T+10ms Receive {"success": true, "message": "connecting"} -T+100ms State update: isConnecting=true, connectingSSID="Network" -T+500ms Credential prompt (if needed) -T+1000ms Submit credentials -T+3000ms State update: wifiConnected=true, wifiIP="192.168.x.x" -``` - -### State Machine - -``` -IDLE - | - | network.wifi.connect - v -CONNECTING (isConnecting=true, connectingSSID set) - | - +-- Needs credentials - | | - | v - | PROMPTING (credential prompt event) - | | - | | network.credentials.submit - | v - | back to CONNECTING - | - +-- Success - | | - | v - | CONNECTED (wifiConnected=true, wifiIP set, isConnecting=false) - | - +-- Failure - | - v - ERROR (isConnecting=false, !wifiConnected, lastError set) -``` - -## Connection Success Detection - -A connection is successful when all of the following are true: - -1. `wifiConnected` is `true` -2. `wifiIP` is set and non-empty -3. `wifiSSID` matches the target network -4. `isConnecting` is `false` - -Do not rely on `wifiConnected` alone - the device may be associated with an access point but not have an IP address yet. - -**Example:** -```javascript -function isConnectionComplete(state, targetSSID) { - return state.wifiConnected && - state.wifiIP && - state.wifiIP !== "" && - state.wifiSSID === targetSSID && - !state.isConnecting; -} -``` - -## Error Handling - -### Error Detection - -Errors occur when a connection attempt stops without success: - -```javascript -function checkForFailure(state, wasConnecting, targetSSID) { - // Was connecting, now idle, but not connected - if (wasConnecting && - !state.isConnecting && - state.connectingSSID === "" && - !state.wifiConnected) { - return state.lastError || "Connection failed"; - } - return null; -} -``` - -### Common Error Scenarios - -#### Wrong Password - -**Detection methods:** - -1. Quick failure (< 3 seconds from start) -2. `lastError` contains "password", "auth", or "secrets" -3. Second credential prompt with `reason: "Previous password was incorrect"` - -**Handling:** -```javascript -if (prompt.reason === "Previous password was incorrect") { - // Show error, clear password field, re-focus input -} -``` - -#### Network Out of Range - -**Detection:** -- `lastError` contains "not-found" or "connection-attempt-failed" - -#### Connection Timeout - -**Detection:** -- `isConnecting` remains true for > 30 seconds - -**Implementation:** -```javascript -let timeout = setTimeout(() => { - if (currentState.isConnecting) { - handleTimeout(); - } -}, 30000); -``` - -#### DHCP Failure - -**Detection:** -- `wifiConnected` is true -- `wifiIP` is empty after 15+ seconds - -### Error Message Translation - -Map technical errors to user-friendly messages: - -| lastError value | Meaning | User message | -|----------------|---------|--------------| -| `secrets-required` | Password needed | "Please enter password" | -| `authentication-failed` | Wrong password | "Incorrect password" | -| `connection-removed` | Profile deleted | "Network configuration removed" | -| `connection-attempt-failed` | Generic failure | "Failed to connect" | -| `network-not-found` | Out of range | "Network not found" | -| `(timeout)` | Timeout | "Connection timed out" | - -## Credential Handling - -### Secret Agent Architecture - -The credential system uses a broker pattern: - -``` -NetworkManager -> SecretAgent -> PromptBroker -> UI -> User - ^ - | - User Response - | -NetworkManager <- SecretAgent <- PromptBroker <- UI -``` - -### Implementing a Broker - -```go -type CustomBroker struct { - ui UIInterface - pending map[string]chan network.PromptReply -} - -func (b *CustomBroker) Ask(ctx context.Context, req network.PromptRequest) (string, error) { - token := generateToken() - b.pending[token] = make(chan network.PromptReply, 1) - - // Send to UI - b.ui.ShowCredentialPrompt(token, req) - - return token, nil -} - -func (b *CustomBroker) Wait(ctx context.Context, token string) (network.PromptReply, error) { - select { - case <-ctx.Done(): - return network.PromptReply{}, errors.New("timeout") - case reply := <-b.pending[token]: - return reply, nil - } -} - -func (b *CustomBroker) Resolve(token string, reply network.PromptReply) error { - if ch, ok := b.pending[token]; ok { - ch <- reply - close(ch) - delete(b.pending, token) - } - return nil -} -``` - -### Credential Field Types - -**Personal WiFi (802-11-wireless-security):** -- Fields: `["psk"]` -- UI: Single password input - -**Enterprise WiFi (802-1x):** -- Fields: `["identity", "password"]` -- UI: Username and password inputs - -### Building Secrets Object - -```javascript -function buildSecrets(setting, fields, formData) { - let secrets = {}; - - if (setting === "802-11-wireless-security") { - secrets.psk = formData.password; - } else if (setting === "802-1x") { - secrets.identity = formData.username; - secrets.password = formData.password; - } - - return secrets; -} -``` - -## Best Practices - -### Track Target Network - -Always store which network you're connecting to: - -```javascript -let targetSSID = null; - -function connect(ssid) { - targetSSID = ssid; - // send request -} - -function onStateUpdate(state) { - if (!targetSSID) return; - - if (state.wifiSSID === targetSSID && state.wifiConnected && state.wifiIP) { - // Success for the network we care about - targetSSID = null; - } -} -``` - -### Implement Timeouts - -Never wait indefinitely for a connection: - -```javascript -const CONNECTION_TIMEOUT = 30000; // 30 seconds -const DHCP_TIMEOUT = 15000; // 15 seconds - -let timer = setTimeout(() => { - if (stillConnecting) { - handleTimeout(); - } -}, CONNECTION_TIMEOUT); -``` - -### Handle Credential Re-prompts - -Wrong passwords trigger a second prompt: - -```javascript -function onCredentialPrompt(prompt) { - if (prompt.reason.includes("incorrect")) { - // Show error, but keep dialog open - showError("Wrong password"); - clearPasswordField(); - } else { - // First time prompt - showDialog(prompt); - } -} -``` - -### Clean Up State - -Reset tracking variables on success, failure, or cancellation: - -```javascript -function cleanup() { - clearTimeout(timer); - targetSSID = null; - closeDialogs(); -} -``` - -### Subscribe to Both Services - -Missing `network.credentials` means prompts won't arrive: - -```javascript -// Correct -services: ["network", "network.credentials"] - -// Wrong - will miss credential prompts -services: ["network"] -``` - -## Testing - -### Connection Test Checklist - -- [ ] Connect to open network -- [ ] Connect to WPA2 network with password provided -- [ ] Connect to WPA2 network without password (triggers prompt) -- [ ] Enter wrong password (verify error and re-prompt) -- [ ] Cancel credential prompt -- [ ] Connection timeout after 30 seconds -- [ ] DHCP timeout detection -- [ ] Network out of range -- [ ] Reconnect to already-configured network - -### Verifying Secret Agent Setup - -Check connection profile flags: -```bash -nmcli connection show "NetworkName" | grep flags -# Should show: 802-11-wireless-security.psk-flags: 1 (agent-owned) -``` - -Check agent registration in logs: -``` -INFO: Registered with NetworkManager as secret agent -``` - -## Security - -- Never log credential values (passwords, PSKs) -- Clear password fields when dialogs close -- Implement prompt timeouts (default: 2 minutes) -- Validate user input before submission -- Use secure channels for credential transmission - -## Troubleshooting - -### Credential prompt doesn't appear - -**Check:** -- Subscribed to both `network` and `network.credentials` -- Connection has `interactive: true` -- Secret flags set to AGENT_OWNED (value: 1) -- Broker registered successfully - -### Connection succeeds without prompting - -**Cause:** NetworkManager found saved credentials - -**Solution:** Delete existing connection first, or use different credentials - -### State updates seem delayed - -**Expected behavior:** State changes occur in rapid succession during connection - -**Solution:** Debounce UI updates; only act on final state - -### Multiple rapid credential prompts - -**Cause:** Connection profile has incorrect flags or conflicting agents - -**Solution:** -- Check only one agent is running -- Verify psk-flags value -- Check NetworkManager logs for agent conflicts - -## Data Structures Reference - -### PromptRequest -```go -type PromptRequest struct { - SSID string `json:"ssid"` - SettingName string `json:"setting"` - Fields []string `json:"fields"` - Hints []string `json:"hints"` - Reason string `json:"reason"` -} -``` - -### PromptReply -```go -type PromptReply struct { - Secrets map[string]string `json:"secrets"` - Save bool `json:"save"` - Cancel bool `json:"cancel"` -} -``` - -### NetworkState -```go -type NetworkState struct { - NetworkStatus string `json:"networkStatus"` - IsConnecting bool `json:"isConnecting"` - ConnectingSSID string `json:"connectingSSID"` - WifiConnected bool `json:"wifiConnected"` - WifiSSID string `json:"wifiSSID"` - WifiIP string `json:"wifiIP"` - LastError string `json:"lastError"` -} -``` diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/agent_iwd.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/agent_iwd.go deleted file mode 100644 index 5d49f04..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/agent_iwd.go +++ /dev/null @@ -1,306 +0,0 @@ -package network - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - "github.com/godbus/dbus/v5" -) - -const ( - iwdAgentManagerPath = "/net/connman/iwd" - iwdAgentManagerIface = "net.connman.iwd.AgentManager" - iwdAgentInterface = "net.connman.iwd.Agent" - iwdAgentObjectPath = "/com/danklinux/iwdagent" -) - -type ConnectionStateChecker interface { - IsConnectingTo(ssid string) bool -} - -type IWDAgent struct { - conn *dbus.Conn - objPath dbus.ObjectPath - prompts PromptBroker - onUserCanceled func() - onPromptRetry func(ssid string) - lastRequestSSID string - stateChecker ConnectionStateChecker -} - -const iwdAgentIntrospectXML = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - -` - -func NewIWDAgent(conn *dbus.Conn, prompts PromptBroker) (*IWDAgent, error) { - if conn == nil { - return nil, fmt.Errorf("dbus connection is nil") - } - - agent := &IWDAgent{ - conn: conn, - objPath: dbus.ObjectPath(iwdAgentObjectPath), - prompts: prompts, - } - - if err := conn.Export(agent, agent.objPath, iwdAgentInterface); err != nil { - return nil, fmt.Errorf("failed to export IWD agent: %w", err) - } - - if err := conn.Export(agent, agent.objPath, "org.freedesktop.DBus.Introspectable"); err != nil { - return nil, fmt.Errorf("failed to export introspection: %w", err) - } - - mgr := conn.Object("net.connman.iwd", dbus.ObjectPath(iwdAgentManagerPath)) - call := mgr.Call(iwdAgentManagerIface+".RegisterAgent", 0, agent.objPath) - if call.Err != nil { - return nil, fmt.Errorf("failed to register agent with iwd: %w", call.Err) - } - - return agent, nil -} - -func (a *IWDAgent) Close() { - if a.conn != nil { - mgr := a.conn.Object("net.connman.iwd", dbus.ObjectPath(iwdAgentManagerPath)) - mgr.Call(iwdAgentManagerIface+".UnregisterAgent", 0, a.objPath) - } -} - -func (a *IWDAgent) SetStateChecker(checker ConnectionStateChecker) { - a.stateChecker = checker -} - -func (a *IWDAgent) getNetworkName(networkPath dbus.ObjectPath) string { - netObj := a.conn.Object("net.connman.iwd", networkPath) - nameVar, err := netObj.GetProperty("net.connman.iwd.Network.Name") - if err == nil { - if name, ok := nameVar.Value().(string); ok { - return name - } - } - return string(networkPath) -} - -func (a *IWDAgent) RequestPassphrase(network dbus.ObjectPath) (string, *dbus.Error) { - ssid := a.getNetworkName(network) - - if a.stateChecker != nil && !a.stateChecker.IsConnectingTo(ssid) { - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - if a.prompts == nil { - if a.onUserCanceled != nil { - a.onUserCanceled() - } - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - if a.lastRequestSSID == ssid { - if a.onPromptRetry != nil { - a.onPromptRetry(ssid) - } - } - a.lastRequestSSID = ssid - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - token, err := a.prompts.Ask(ctx, PromptRequest{ - SSID: ssid, - Fields: []string{"psk"}, - }) - if err != nil { - if a.onUserCanceled != nil { - a.onUserCanceled() - } - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - reply, err := a.prompts.Wait(ctx, token) - if err != nil { - if reply.Cancel || errors.Is(err, errdefs.ErrSecretPromptCancelled) { - if a.onUserCanceled != nil { - a.onUserCanceled() - } - } - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - if passphrase, ok := reply.Secrets["psk"]; ok { - return passphrase, nil - } - - if a.onUserCanceled != nil { - a.onUserCanceled() - } - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) -} - -func (a *IWDAgent) RequestPrivateKeyPassphrase(network dbus.ObjectPath) (string, *dbus.Error) { - ssid := a.getNetworkName(network) - - if a.stateChecker != nil && !a.stateChecker.IsConnectingTo(ssid) { - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - if a.prompts == nil { - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - if a.lastRequestSSID == ssid { - if a.onPromptRetry != nil { - a.onPromptRetry(ssid) - } - } - a.lastRequestSSID = ssid - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - token, err := a.prompts.Ask(ctx, PromptRequest{ - SSID: ssid, - Fields: []string{"private-key-password"}, - }) - if err != nil { - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - reply, err := a.prompts.Wait(ctx, token) - if err != nil || reply.Cancel { - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - if passphrase, ok := reply.Secrets["private-key-password"]; ok { - return passphrase, nil - } - - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) -} - -func (a *IWDAgent) RequestUserNameAndPassword(network dbus.ObjectPath) (string, string, *dbus.Error) { - ssid := a.getNetworkName(network) - - if a.stateChecker != nil && !a.stateChecker.IsConnectingTo(ssid) { - return "", "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - if a.prompts == nil { - return "", "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - if a.lastRequestSSID == ssid { - if a.onPromptRetry != nil { - a.onPromptRetry(ssid) - } - } - a.lastRequestSSID = ssid - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - token, err := a.prompts.Ask(ctx, PromptRequest{ - SSID: ssid, - Fields: []string{"identity", "password"}, - }) - if err != nil { - return "", "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - reply, err := a.prompts.Wait(ctx, token) - if err != nil || reply.Cancel { - return "", "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - username, hasUser := reply.Secrets["identity"] - password, hasPass := reply.Secrets["password"] - - if hasUser && hasPass { - return username, password, nil - } - - return "", "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) -} - -func (a *IWDAgent) RequestUserPassword(network dbus.ObjectPath, user string) (string, *dbus.Error) { - ssid := a.getNetworkName(network) - - if a.stateChecker != nil && !a.stateChecker.IsConnectingTo(ssid) { - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - if a.prompts == nil { - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - if a.lastRequestSSID == ssid { - if a.onPromptRetry != nil { - a.onPromptRetry(ssid) - } - } - a.lastRequestSSID = ssid - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - token, err := a.prompts.Ask(ctx, PromptRequest{ - SSID: ssid, - Fields: []string{"password"}, - }) - if err != nil { - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - reply, err := a.prompts.Wait(ctx, token) - if err != nil || reply.Cancel { - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) - } - - if password, ok := reply.Secrets["password"]; ok { - return password, nil - } - - return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) -} - -func (a *IWDAgent) Cancel(reason string) *dbus.Error { - return nil -} - -func (a *IWDAgent) Release() *dbus.Error { - return nil -} - -func (a *IWDAgent) Introspect() (string, *dbus.Error) { - return iwdAgentIntrospectXML, nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/agent_networkmanager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/agent_networkmanager.go deleted file mode 100644 index 088b7fa..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/agent_networkmanager.go +++ /dev/null @@ -1,897 +0,0 @@ -package network - -import ( - "context" - "errors" - "fmt" - "strconv" - "strings" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/godbus/dbus/v5" - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - -const ( - nmAgentManagerPath = "/org/freedesktop/NetworkManager/AgentManager" - nmAgentManagerIface = "org.freedesktop.NetworkManager.AgentManager" - nmSecretAgentIface = "org.freedesktop.NetworkManager.SecretAgent" - agentObjectPath = "/org/freedesktop/NetworkManager/SecretAgent" - agentIdentifier = "com.danklinux.NMAgent" -) - -type SecretAgent struct { - conn *dbus.Conn - objPath dbus.ObjectPath - id string - prompts PromptBroker - manager *Manager - backend *NetworkManagerBackend -} - -type ( - nmVariantMap map[string]dbus.Variant - nmSettingMap map[string]nmVariantMap -) - -const introspectXML = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - -` - -func NewSecretAgent(prompts PromptBroker, manager *Manager, backend *NetworkManagerBackend) (*SecretAgent, error) { - c, err := dbus.ConnectSystemBus() - if err != nil { - return nil, fmt.Errorf("failed to connect to system bus: %w", err) - } - - sa := &SecretAgent{ - conn: c, - objPath: dbus.ObjectPath(agentObjectPath), - id: agentIdentifier, - prompts: prompts, - manager: manager, - backend: backend, - } - - if err := c.Export(sa, sa.objPath, nmSecretAgentIface); err != nil { - c.Close() - return nil, fmt.Errorf("failed to export secret agent: %w", err) - } - - if err := c.Export(sa, sa.objPath, "org.freedesktop.DBus.Introspectable"); err != nil { - c.Close() - return nil, fmt.Errorf("failed to export introspection: %w", err) - } - - mgr := c.Object("org.freedesktop.NetworkManager", dbus.ObjectPath(nmAgentManagerPath)) - call := mgr.Call(nmAgentManagerIface+".Register", 0, sa.id) - if call.Err != nil { - c.Close() - return nil, fmt.Errorf("failed to register agent with NetworkManager: %w", call.Err) - } - - log.Infof("[SecretAgent] Registered with NetworkManager (id=%s, unique name=%s, fixed path=%s)", sa.id, c.Names()[0], sa.objPath) - return sa, nil -} - -func (a *SecretAgent) Close() { - if a.conn != nil { - mgr := a.conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath(nmAgentManagerPath)) - mgr.Call(nmAgentManagerIface+".Unregister", 0, a.id) - a.conn.Close() - } -} - -func (a *SecretAgent) GetSecrets( - conn map[string]nmVariantMap, - path dbus.ObjectPath, - settingName string, - hints []string, - flags uint32, -) (nmSettingMap, *dbus.Error) { - log.Infof("[SecretAgent] GetSecrets called: path=%s, setting=%s, hints=%v, flags=%d", - path, settingName, hints, flags) - - connType, displayName, vpnSvc := readConnTypeAndName(conn) - ssid := readSSID(conn) - fields := fieldsNeeded(settingName, hints, conn) - vpnPasswordFlags := readVPNPasswordFlags(conn, settingName) - - log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d, vpnPasswordFlags=%d", connType, displayName, vpnSvc, fields, flags, vpnPasswordFlags) - - if a.backend != nil { - a.backend.stateMutex.RLock() - isConnecting := a.backend.state.IsConnecting - connectingSSID := a.backend.state.ConnectingSSID - isConnectingVPN := a.backend.state.IsConnectingVPN - connectingVPNUUID := a.backend.state.ConnectingVPNUUID - a.backend.stateMutex.RUnlock() - - switch connType { - case "802-11-wireless": - // If we're connecting to a WiFi network, only respond if it's the one we're connecting to - if isConnecting && connectingSSID != ssid { - log.Infof("[SecretAgent] Ignoring WiFi request for SSID '%s' - we're connecting to '%s'", ssid, connectingSSID) - return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil) - } - case "vpn", "wireguard": - var connUuid string - if c, ok := conn["connection"]; ok { - if v, ok := c["uuid"]; ok { - if s, ok2 := v.Value().(string); ok2 { - connUuid = s - } - } - } - - // If we're connecting to a VPN, only respond if it's the one we're connecting to - // This prevents interfering with nmcli/other tools when our app isn't connecting - if isConnectingVPN && connUuid != connectingVPNUUID { - log.Infof("[SecretAgent] Ignoring VPN request for UUID '%s' - we're connecting to '%s'", connUuid, connectingVPNUUID) - return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil) - } - } - } - - if len(fields) == 0 { - if settingName == "vpn" { - if a.backend != nil { - a.backend.stateMutex.RLock() - isConnectingVPN := a.backend.state.IsConnectingVPN - a.backend.stateMutex.RUnlock() - - if !isConnectingVPN { - log.Infof("[SecretAgent] VPN with empty hints - deferring to other agents for %s", vpnSvc) - return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil) - } - - fields = inferVPNFields(conn, vpnSvc) - log.Infof("[SecretAgent] VPN with empty hints but we're connecting - inferred fields: %v", fields) - } else { - log.Infof("[SecretAgent] VPN with empty hints - deferring to other agents for %s", vpnSvc) - return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil) - } - } - - if len(fields) == 0 { - const ( - NM_SETTING_SECRET_FLAG_NONE = 0 - NM_SETTING_SECRET_FLAG_AGENT_OWNED = 1 - NM_SETTING_SECRET_FLAG_NOT_SAVED = 2 - NM_SETTING_SECRET_FLAG_NOT_REQUIRED = 4 - ) - - var passwordFlags uint32 = 0xFFFF - switch settingName { - case "802-11-wireless-security": - if wifiSecSettings, ok := conn["802-11-wireless-security"]; ok { - if flagsVariant, ok := wifiSecSettings["psk-flags"]; ok { - if pwdFlags, ok := flagsVariant.Value().(uint32); ok { - passwordFlags = pwdFlags - } - } - } - case "802-1x": - if dot1xSettings, ok := conn["802-1x"]; ok { - if flagsVariant, ok := dot1xSettings["password-flags"]; ok { - if pwdFlags, ok := flagsVariant.Value().(uint32); ok { - passwordFlags = pwdFlags - } - } - } - } - - if passwordFlags == 0xFFFF { - log.Warnf("[SecretAgent] Could not determine password-flags for empty hints - returning NoSecrets error") - return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil) - } else if passwordFlags&NM_SETTING_SECRET_FLAG_NOT_REQUIRED != 0 { - log.Infof("[SecretAgent] Secrets not required (flags=%d)", passwordFlags) - out := nmSettingMap{} - out[settingName] = nmVariantMap{} - return out, nil - } else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 { - switch settingName { - case "802-11-wireless-security": - fields = []string{"psk"} - case "802-1x": - fields = infer8021xFields(conn) - default: - log.Warnf("[SecretAgent] Agent-owned secrets for unhandled setting %s (flags=%d)", settingName, passwordFlags) - return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil) - } - log.Infof("[SecretAgent] Agent-owned secrets, inferred fields: %v", fields) - } else { - log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags) - out := nmSettingMap{} - out[settingName] = nmVariantMap{} - return out, nil - } - } - } - - reason := reasonFromFlags(flags) - if a.manager != nil && connType == "802-11-wireless" && a.manager.WasRecentlyFailed(ssid) { - reason = "wrong-password" - } - if settingName == "vpn" && isPKCS11Auth(conn, vpnSvc) { - reason = "pkcs11" - } - - var connId, connUuid string - if c, ok := conn["connection"]; ok { - if v, ok := c["id"]; ok { - if s, ok2 := v.Value().(string); ok2 { - connId = s - } - } - if v, ok := c["uuid"]; ok { - if s, ok2 := v.Value().(string); ok2 { - connUuid = s - } - } - } - - if settingName == "vpn" && a.backend != nil { - // Check for cached PKCS11 PIN first - isPKCS11Request := len(fields) == 1 && fields[0] == "key_pass" - if isPKCS11Request { - a.backend.cachedPKCS11Mu.Lock() - cached := a.backend.cachedPKCS11PIN - if cached != nil && cached.ConnectionUUID == connUuid { - a.backend.cachedPKCS11PIN = nil - a.backend.cachedPKCS11Mu.Unlock() - - log.Infof("[SecretAgent] Using cached PKCS11 PIN") - - out := nmSettingMap{} - vpnSec := nmVariantMap{} - vpnSec["secrets"] = dbus.MakeVariant(map[string]string{"key_pass": cached.PIN}) - out[settingName] = vpnSec - - return out, nil - } - a.backend.cachedPKCS11Mu.Unlock() - } - - // Check for cached VPN password - a.backend.cachedVPNCredsMu.Lock() - cached := a.backend.cachedVPNCreds - if cached != nil && cached.ConnectionUUID == connUuid { - a.backend.cachedVPNCreds = nil - a.backend.cachedVPNCredsMu.Unlock() - - log.Infof("[SecretAgent] Using cached password from pre-activation prompt") - - out := nmSettingMap{} - vpnSec := nmVariantMap{} - vpnSec["secrets"] = dbus.MakeVariant(map[string]string{"password": cached.Password}) - out[settingName] = vpnSec - - if cached.SavePassword { - a.backend.pendingVPNSaveMu.Lock() - a.backend.pendingVPNSave = &pendingVPNCredentials{ - ConnectionPath: string(path), - Password: cached.Password, - SavePassword: true, - } - a.backend.pendingVPNSaveMu.Unlock() - } - - return out, nil - } - a.backend.cachedVPNCredsMu.Unlock() - - a.backend.cachedGPSamlMu.Lock() - cachedGPSaml := a.backend.cachedGPSamlCookie - if cachedGPSaml != nil && cachedGPSaml.ConnectionUUID == connUuid { - a.backend.cachedGPSamlMu.Unlock() - - log.Infof("[SecretAgent] Using cached GlobalProtect SAML cookie for %s", connUuid) - - return buildGPSamlSecretsResponse(settingName, cachedGPSaml.Cookie, cachedGPSaml.Host, cachedGPSaml.Fingerprint), nil - } - a.backend.cachedGPSamlMu.Unlock() - - if len(fields) == 1 && fields[0] == "gp-saml" { - gateway := "" - protocol := "" - if vpnSettings, ok := conn["vpn"]; ok { - if dataVariant, ok := vpnSettings["data"]; ok { - if dataMap, ok := dataVariant.Value().(map[string]string); ok { - if gw, ok := dataMap["gateway"]; ok { - gateway = gw - } - if proto, ok := dataMap["protocol"]; ok && proto != "" { - protocol = proto - } - } - } - } - - if protocol != "gp" { - return nil, dbus.MakeFailedError(fmt.Errorf("gp-saml auth only supported for GlobalProtect (protocol=gp), got: %s", protocol)) - } - - log.Infof("[SecretAgent] Starting GlobalProtect SAML authentication for gateway=%s", gateway) - - samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer samlCancel() - - authResult, err := a.backend.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol) - if err != nil { - log.Warnf("[SecretAgent] GlobalProtect SAML authentication failed: %v", err) - return nil, dbus.MakeFailedError(fmt.Errorf("GlobalProtect SAML authentication failed: %w", err)) - } - - log.Infof("[SecretAgent] GlobalProtect SAML authentication successful, returning cookie to NetworkManager") - - a.backend.cachedGPSamlMu.Lock() - a.backend.cachedGPSamlCookie = &cachedGPSamlCookie{ - ConnectionUUID: connUuid, - Cookie: authResult.Cookie, - Host: authResult.Host, - User: authResult.User, - Fingerprint: authResult.Fingerprint, - } - a.backend.cachedGPSamlMu.Unlock() - - return buildGPSamlSecretsResponse(settingName, authResult.Cookie, authResult.Host, authResult.Fingerprint), nil - } - } - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - - token, err := a.prompts.Ask(ctx, PromptRequest{ - Name: displayName, - SSID: ssid, - ConnType: connType, - VpnService: vpnSvc, - SettingName: settingName, - Fields: fields, - FieldsInfo: buildFieldsInfo(settingName, fields, vpnSvc), - Hints: hints, - Reason: reason, - ConnectionId: connId, - ConnectionUuid: connUuid, - ConnectionPath: string(path), - }) - if err != nil { - log.Warnf("[SecretAgent] Failed to create prompt: %v", err) - return nil, dbus.MakeFailedError(err) - } - - log.Infof("[SecretAgent] Waiting for user input (token=%s)", token) - reply, err := a.prompts.Wait(ctx, token) - if err != nil { - log.Warnf("[SecretAgent] Prompt failed or cancelled: %v", err) - - // Clear connecting state immediately on cancellation - if a.backend != nil { - a.backend.stateMutex.Lock() - wasConnecting := a.backend.state.IsConnecting - wasConnectingVPN := a.backend.state.IsConnectingVPN - cancelledSSID := a.backend.state.ConnectingSSID - cancelledVPNUUID := a.backend.state.ConnectingVPNUUID - if wasConnecting || wasConnectingVPN { - log.Infof("[SecretAgent] Clearing connecting state due to cancelled prompt") - a.backend.state.IsConnecting = false - a.backend.state.ConnectingSSID = "" - a.backend.state.IsConnectingVPN = false - a.backend.state.ConnectingVPNUUID = "" - } - a.backend.stateMutex.Unlock() - - // If this was a WiFi connection that was just cancelled, remove the connection profile - // (it was created with AddConnection but activation was cancelled) - if wasConnecting && cancelledSSID != "" && connType == "802-11-wireless" { - log.Infof("[SecretAgent] Removing connection profile for cancelled WiFi connection: %s", cancelledSSID) - if err := a.backend.ForgetWiFiNetwork(cancelledSSID); err != nil { - log.Warnf("[SecretAgent] Failed to remove cancelled connection profile: %v", err) - } - } - - // If this was a VPN connection that was cancelled, deactivate it - if wasConnectingVPN && cancelledVPNUUID != "" { - log.Infof("[SecretAgent] Deactivating cancelled VPN connection: %s", cancelledVPNUUID) - if err := a.backend.DisconnectVPN(cancelledVPNUUID); err != nil { - log.Warnf("[SecretAgent] Failed to deactivate cancelled VPN: %v", err) - } - } - - if (wasConnecting || wasConnectingVPN) && a.backend.onStateChange != nil { - a.backend.onStateChange() - } - } - - if reply.Cancel || errors.Is(err, errdefs.ErrSecretPromptCancelled) { - return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.UserCanceled", nil) - } - - if errors.Is(err, errdefs.ErrSecretPromptTimeout) { - return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.Failed", nil) - } - return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.Failed", nil) - } - - log.Infof("[SecretAgent] User provided secrets, save=%v", reply.Save) - - out := nmSettingMap{} - sec := nmVariantMap{} - - var vpnUsername string - for k, v := range reply.Secrets { - if settingName == "vpn" && k == "username" { - vpnUsername = v - } - sec[k] = dbus.MakeVariant(v) - } - - // Check if this is PKCS11 auth (key_pass) - pin, isPKCS11 := reply.Secrets["key_pass"] - - switch settingName { - case "vpn": - // VPN secrets must be wrapped in a "secrets" key per NM spec - secretsDict := make(map[string]string) - for k, v := range reply.Secrets { - if k != "username" { - secretsDict[k] = v - } - } - vpnSec := nmVariantMap{} - vpnSec["secrets"] = dbus.MakeVariant(secretsDict) - out[settingName] = vpnSec - log.Infof("[SecretAgent] Returning VPN secrets with %d fields for %s", len(secretsDict), vpnSvc) - - // Cache PKCS11 PIN in case GetSecrets is called again during activation - if isPKCS11 && a.backend != nil { - a.backend.cachedPKCS11Mu.Lock() - a.backend.cachedPKCS11PIN = &cachedPKCS11PIN{ - ConnectionUUID: connUuid, - PIN: pin, - } - a.backend.cachedPKCS11Mu.Unlock() - log.Infof("[SecretAgent] Cached PKCS11 PIN for potential re-request") - } - case "802-1x": - secretsOnly := nmVariantMap{} - for k, v := range reply.Secrets { - switch k { - case "password", "private-key-password", "phase2-private-key-password", "pin": - secretsOnly[k] = dbus.MakeVariant(v) - } - } - out[settingName] = secretsOnly - - if identity, ok := reply.Secrets["identity"]; ok && identity != "" { - a.save8021xIdentity(path, identity) - } - log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(secretsOnly)) - default: - out[settingName] = sec - } - if settingName == "vpn" && a.backend != nil && !isPKCS11 && (vpnUsername != "" || reply.Save) { - pw := reply.Secrets["password"] - a.backend.pendingVPNSaveMu.Lock() - a.backend.pendingVPNSave = &pendingVPNCredentials{ - ConnectionPath: string(path), - Username: vpnUsername, - Password: pw, - SavePassword: reply.Save, - } - a.backend.pendingVPNSaveMu.Unlock() - log.Infof("[SecretAgent] Queued credentials persist for after connection succeeds") - } - - return out, nil -} - -func (a *SecretAgent) DeleteSecrets(conn map[string]nmVariantMap, path dbus.ObjectPath) *dbus.Error { - ssid := readSSID(conn) - log.Infof("[SecretAgent] DeleteSecrets called: path=%s, SSID=%s", path, ssid) - return nil -} - -func (a *SecretAgent) DeleteSecrets2(path dbus.ObjectPath, setting string) *dbus.Error { - log.Infof("[SecretAgent] DeleteSecrets2 (alternate) called: path=%s, setting=%s", path, setting) - return nil -} - -func (a *SecretAgent) CancelGetSecrets(path dbus.ObjectPath, settingName string) *dbus.Error { - log.Infof("[SecretAgent] CancelGetSecrets called: path=%s, setting=%s", path, settingName) - - if a.prompts != nil { - if err := a.prompts.Cancel(string(path), settingName); err != nil { - log.Warnf("[SecretAgent] Failed to cancel prompt: %v", err) - } - } - - return nil -} - -func (a *SecretAgent) Introspect() (string, *dbus.Error) { - return introspectXML, nil -} - -func (a *SecretAgent) save8021xIdentity(path dbus.ObjectPath, identity string) { - connObj := a.conn.Object("org.freedesktop.NetworkManager", path) - var existing map[string]map[string]dbus.Variant - if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existing); err != nil { - log.Warnf("[SecretAgent] Failed to get settings for identity save: %v", err) - return - } - - settings := make(map[string]map[string]dbus.Variant) - if connSection, ok := existing["connection"]; ok { - settings["connection"] = connSection - } - - dot1x, ok := existing["802-1x"] - if !ok { - dot1x = make(map[string]dbus.Variant) - } - dot1x["identity"] = dbus.MakeVariant(identity) - settings["802-1x"] = dot1x - - var result map[string]dbus.Variant - if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0, - settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result); err != nil { - log.Warnf("[SecretAgent] Failed to save 802.1x identity: %v", err) - return - } - log.Infof("[SecretAgent] Saved 802.1x identity to connection profile") -} - -func readSSID(conn map[string]nmVariantMap) string { - if w, ok := conn["802-11-wireless"]; ok { - if v, ok := w["ssid"]; ok { - if b, ok := v.Value().([]byte); ok { - return string(b) - } - if s, ok := v.Value().(string); ok { - return s - } - } - } - return "" -} - -func readConnTypeAndName(conn map[string]nmVariantMap) (string, string, string) { - var connType, name, svc string - if c, ok := conn["connection"]; ok { - if v, ok := c["type"]; ok { - if s, ok2 := v.Value().(string); ok2 { - connType = s - } - } - if v, ok := c["id"]; ok { - if s, ok2 := v.Value().(string); ok2 { - name = s - } - } - } - if vpn, ok := conn["vpn"]; ok { - if v, ok := vpn["service-type"]; ok { - if s, ok2 := v.Value().(string); ok2 { - svc = s - } - } - } - if name == "" && connType == "802-11-wireless" { - name = readSSID(conn) - } - return connType, name, svc -} - -func fieldsNeeded(setting string, hints []string, conn map[string]nmVariantMap) []string { - switch setting { - case "802-11-wireless-security": - return []string{"psk"} - case "802-1x": - if len(hints) > 0 { - return hints - } - return infer8021xFields(conn) - case "vpn": - return hints - default: - return []string{} - } -} - -func infer8021xFields(conn map[string]nmVariantMap) []string { - dot1x, ok := conn["802-1x"] - if !ok { - return []string{"identity", "password"} - } - - var fields []string - - if v, ok := dot1x["identity"]; ok { - if id, ok := v.Value().(string); ok && id != "" { - // identity already stored, don't ask again - } else { - fields = append(fields, "identity") - } - } else { - fields = append(fields, "identity") - } - - var eapMethods []string - if v, ok := dot1x["eap"]; ok { - if methods, ok := v.Value().([]string); ok { - eapMethods = methods - } - } - - switch { - case len(eapMethods) > 0 && eapMethods[0] == "tls": - fields = append(fields, "private-key-password") - default: - fields = append(fields, "password") - } - - return fields -} - -func buildFieldsInfo(setting string, fields []string, vpnService string) []FieldInfo { - result := make([]FieldInfo, 0, len(fields)) - for _, f := range fields { - info := FieldInfo{Name: f} - switch setting { - case "802-11-wireless-security": - info.Label = "Password" - info.IsSecret = true - case "802-1x": - switch f { - case "identity": - info.Label = "Username" - info.IsSecret = false - case "password": - info.Label = "Password" - info.IsSecret = true - default: - info.Label = f - info.IsSecret = true - } - case "vpn": - info.Label, info.IsSecret = vpnFieldMeta(f, vpnService) - default: - info.Label = f - info.IsSecret = true - } - result = append(result, info) - } - return result -} - -func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string { - fields := []string{"password"} - - vpnSettings, ok := conn["vpn"] - if !ok { - return fields - } - - dataVariant, ok := vpnSettings["data"] - if !ok { - return fields - } - - dataMap, ok := dataVariant.Value().(map[string]string) - if !ok { - return fields - } - - connType := dataMap["connection-type"] - - switch { - case strings.Contains(vpnService, "openconnect"): - protocol := dataMap["protocol"] - authType := dataMap["authtype"] - username := dataMap["username"] - - if authType == "cert" && strings.HasPrefix(dataMap["usercert"], "pkcs11:") { - return []string{"key_pass"} - } - - if needsExternalBrowserAuth(protocol, authType, username, dataMap) { - switch protocol { - case "gp": - log.Infof("[SecretAgent] GlobalProtect SAML auth detected") - return []string{"gp-saml"} - default: - log.Infof("[SecretAgent] External browser auth detected for protocol '%s' but only GlobalProtect (gp) SAML is currently supported, falling back to credentials", protocol) - } - } - - if username == "" { - fields = []string{"username", "password"} - } - case strings.Contains(vpnService, "openvpn"): - if connType == "password" || connType == "password-tls" { - if dataMap["username"] == "" { - fields = []string{"username", "password"} - } - } - case strings.Contains(vpnService, "vpnc"), strings.Contains(vpnService, "l2tp"), - strings.Contains(vpnService, "pptp"): - if dataMap["username"] == "" { - fields = []string{"username", "password"} - } - } - - return fields -} - -func needsExternalBrowserAuth(protocol, authType, username string, data map[string]string) bool { - if method, ok := data["saml-auth-method"]; ok { - if method == "REDIRECT" || method == "POST" { - return true - } - } - - if authType != "" && authType != "password" && authType != "cert" { - return true - } - - switch protocol { - case "gp": - if authType == "" && username == "" { - return true - } - } - - return false -} - -func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) { - switch field { - case "gp-saml": - return "GlobalProtect SAML/SSO", false - case "key_pass": - return "PIN", true - case "password": - return "Password", true - case "Xauth password": - return "IPSec Password", true - case "IPSec secret": - return "IPSec Pre-Shared Key", true - case "cert-pass": - return "Certificate Password", true - case "http-proxy-password": - return "HTTP Proxy Password", true - case "username": - return "Username", false - case "Xauth username": - return "IPSec Username", false - case "proxy-password": - return "Proxy Password", true - case "private-key-password": - return "Private Key Password", true - } - titleCaser := cases.Title(language.English) - if strings.HasSuffix(field, "password") || strings.HasSuffix(field, "secret") || - strings.HasSuffix(field, "pass") || strings.HasSuffix(field, "psk") { - return titleCaser.String(strings.ReplaceAll(field, "-", " ")), true - } - return titleCaser.String(strings.ReplaceAll(field, "-", " ")), false -} - -func isPKCS11Auth(conn map[string]nmVariantMap, vpnService string) bool { - if !strings.Contains(vpnService, "openconnect") { - return false - } - vpnSettings, ok := conn["vpn"] - if !ok { - return false - } - dataVariant, ok := vpnSettings["data"] - if !ok { - return false - } - dataMap, ok := dataVariant.Value().(map[string]string) - if !ok { - return false - } - return dataMap["authtype"] == "cert" && strings.HasPrefix(dataMap["usercert"], "pkcs11:") -} - -func readVPNPasswordFlags(conn map[string]nmVariantMap, settingName string) uint32 { - if settingName != "vpn" { - return 0xFFFF - } - - vpnSettings, ok := conn["vpn"] - if !ok { - return 0xFFFF - } - - dataVariant, ok := vpnSettings["data"] - if !ok { - return 0xFFFF - } - - dataMap, ok := dataVariant.Value().(map[string]string) - if !ok { - return 0xFFFF - } - - flagsStr, ok := dataMap["password-flags"] - if !ok { - return 0xFFFF - } - - flags64, err := strconv.ParseUint(flagsStr, 10, 32) - if err != nil { - return 0xFFFF - } - - return uint32(flags64) -} - -func reasonFromFlags(flags uint32) string { - const ( - NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE = 0x0 - NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION = 0x1 - NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW = 0x2 - NM_SECRET_AGENT_GET_SECRETS_FLAG_USER_REQUESTED = 0x4 - NM_SECRET_AGENT_GET_SECRETS_FLAG_WPS_PBC_ACTIVE = 0x8 - NM_SECRET_AGENT_GET_SECRETS_FLAG_ONLY_SYSTEM = 0x80000000 - NM_SECRET_AGENT_GET_SECRETS_FLAG_NO_ERRORS = 0x40000000 - ) - - if flags&NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW != 0 { - return "wrong-password" - } - if flags&NM_SECRET_AGENT_GET_SECRETS_FLAG_USER_REQUESTED != 0 { - return "user-requested" - } - return "required" -} - -func buildGPSamlSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap { - out := nmSettingMap{} - vpnSec := nmVariantMap{} - - secrets := map[string]string{ - "cookie": cookie, - "gateway": host, - "gwcert": fingerprint, - } - vpnSec["secrets"] = dbus.MakeVariant(secrets) - - out[settingName] = vpnSec - return out -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/agent_networkmanager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/agent_networkmanager_test.go deleted file mode 100644 index 2040986..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/agent_networkmanager_test.go +++ /dev/null @@ -1,355 +0,0 @@ -package network - -import ( - "testing" - - "github.com/godbus/dbus/v5" - "github.com/stretchr/testify/assert" -) - -func TestNeedsExternalBrowserAuth(t *testing.T) { - tests := []struct { - name string - protocol string - authType string - username string - data map[string]string - expected bool - }{ - { - name: "GP with saml-auth-method REDIRECT", - protocol: "gp", - authType: "password", - username: "user", - data: map[string]string{"saml-auth-method": "REDIRECT"}, - expected: true, - }, - { - name: "GP with saml-auth-method POST", - protocol: "gp", - authType: "password", - username: "user", - data: map[string]string{"saml-auth-method": "POST"}, - expected: true, - }, - { - name: "GP with no authtype and no username", - protocol: "gp", - authType: "", - username: "", - data: map[string]string{}, - expected: true, - }, - { - name: "GP with username and password authtype", - protocol: "gp", - authType: "password", - username: "john", - data: map[string]string{}, - expected: false, - }, - { - name: "GP with username but no authtype", - protocol: "gp", - authType: "", - username: "john", - data: map[string]string{}, - expected: false, - }, - { - name: "GP with authtype but no username - should detect SAML", - protocol: "gp", - authType: "", - username: "", - data: map[string]string{}, - expected: true, - }, - { - name: "pulse with SAML", - protocol: "pulse", - authType: "", - username: "", - data: map[string]string{"saml-auth-method": "REDIRECT"}, - expected: true, - }, - { - name: "fortinet with non-password authtype", - protocol: "fortinet", - authType: "saml", - username: "", - data: map[string]string{}, - expected: true, - }, - { - name: "anyconnect with cert", - protocol: "anyconnect", - authType: "cert", - username: "", - data: map[string]string{}, - expected: false, - }, - { - name: "anyconnect with password", - protocol: "anyconnect", - authType: "password", - username: "user", - data: map[string]string{}, - expected: false, - }, - { - name: "empty protocol", - protocol: "", - authType: "", - username: "", - data: map[string]string{}, - expected: false, - }, - { - name: "GP with cert authtype", - protocol: "gp", - authType: "cert", - username: "", - data: map[string]string{}, - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := needsExternalBrowserAuth(tt.protocol, tt.authType, tt.username, tt.data) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestBuildGPSamlSecretsResponse(t *testing.T) { - tests := []struct { - name string - settingName string - cookie string - host string - fingerprint string - }{ - { - name: "all fields populated", - settingName: "vpn", - cookie: "authcookie=abc123&portal=GATE", - host: "vpn.example.com", - fingerprint: "pin-sha256:ABCD1234", - }, - { - name: "empty fingerprint", - settingName: "vpn", - cookie: "authcookie=xyz", - host: "10.0.0.1", - fingerprint: "", - }, - { - name: "complex cookie with special chars", - settingName: "vpn", - cookie: "authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100", - host: "connect.seclore.com", - fingerprint: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := buildGPSamlSecretsResponse(tt.settingName, tt.cookie, tt.host, tt.fingerprint) - - assert.NotNil(t, result) - assert.Contains(t, result, tt.settingName) - - vpnSec := result[tt.settingName] - assert.NotNil(t, vpnSec) - - secretsVariant, ok := vpnSec["secrets"] - assert.True(t, ok, "secrets key should exist") - - secrets, ok := secretsVariant.Value().(map[string]string) - assert.True(t, ok, "secrets should be map[string]string") - - assert.Equal(t, tt.cookie, secrets["cookie"]) - assert.Equal(t, tt.host, secrets["gateway"]) - assert.Equal(t, tt.fingerprint, secrets["gwcert"]) - }) - } -} - -func TestVpnFieldMeta_GPSaml(t *testing.T) { - label, isSecret := vpnFieldMeta("gp-saml", "org.freedesktop.NetworkManager.openconnect") - - assert.Equal(t, "GlobalProtect SAML/SSO", label) - assert.False(t, isSecret, "gp-saml should not be marked as secret") -} - -func TestVpnFieldMeta_StandardFields(t *testing.T) { - tests := []struct { - field string - vpnService string - expectedLabel string - expectedSecret bool - }{ - { - field: "username", - vpnService: "org.freedesktop.NetworkManager.openconnect", - expectedLabel: "Username", - expectedSecret: false, - }, - { - field: "password", - vpnService: "org.freedesktop.NetworkManager.openconnect", - expectedLabel: "Password", - expectedSecret: true, - }, - { - field: "key_pass", - vpnService: "org.freedesktop.NetworkManager.openconnect", - expectedLabel: "PIN", - expectedSecret: true, - }, - } - - for _, tt := range tests { - t.Run(tt.field, func(t *testing.T) { - label, isSecret := vpnFieldMeta(tt.field, tt.vpnService) - assert.Equal(t, tt.expectedLabel, label) - assert.Equal(t, tt.expectedSecret, isSecret) - }) - } -} - -func TestInferVPNFields_GPSaml(t *testing.T) { - tests := []struct { - name string - vpnService string - dataMap map[string]string - expectedLen int - shouldHave []string - }{ - { - name: "GP with no authtype and no username - should require SAML", - vpnService: "org.freedesktop.NetworkManager.openconnect", - dataMap: map[string]string{ - "protocol": "gp", - "gateway": "vpn.example.com", - }, - expectedLen: 1, - shouldHave: []string{"gp-saml"}, - }, - { - name: "GP with saml-auth-method REDIRECT", - vpnService: "org.freedesktop.NetworkManager.openconnect", - dataMap: map[string]string{ - "protocol": "gp", - "gateway": "vpn.example.com", - "saml-auth-method": "REDIRECT", - "username": "john", - }, - expectedLen: 1, - shouldHave: []string{"gp-saml"}, - }, - { - name: "GP with saml-auth-method POST", - vpnService: "org.freedesktop.NetworkManager.openconnect", - dataMap: map[string]string{ - "protocol": "gp", - "gateway": "vpn.example.com", - "saml-auth-method": "POST", - }, - expectedLen: 1, - shouldHave: []string{"gp-saml"}, - }, - { - name: "GP with username and password authtype - should use credentials", - vpnService: "org.freedesktop.NetworkManager.openconnect", - dataMap: map[string]string{ - "protocol": "gp", - "gateway": "vpn.example.com", - "authtype": "password", - "username": "john", - }, - expectedLen: 1, - shouldHave: []string{"password"}, - }, - { - name: "GP with username but no authtype - password only", - vpnService: "org.freedesktop.NetworkManager.openconnect", - dataMap: map[string]string{ - "protocol": "gp", - "gateway": "vpn.example.com", - "username": "john", - }, - expectedLen: 1, - shouldHave: []string{"password"}, - }, - { - name: "GP with PKCS11 cert", - vpnService: "org.freedesktop.NetworkManager.openconnect", - dataMap: map[string]string{ - "protocol": "gp", - "gateway": "vpn.example.com", - "authtype": "cert", - "usercert": "pkcs11:model=PKCS%2315%20emulated;manufacturer=piv_II", - }, - expectedLen: 1, - shouldHave: []string{"key_pass"}, - }, - { - name: "non-GP protocol (anyconnect)", - vpnService: "org.freedesktop.NetworkManager.openconnect", - dataMap: map[string]string{ - "protocol": "anyconnect", - "gateway": "vpn.example.com", - }, - expectedLen: 2, - shouldHave: []string{"username", "password"}, - }, - { - name: "OpenVPN with username", - vpnService: "org.freedesktop.NetworkManager.openvpn", - dataMap: map[string]string{ - "connection-type": "password", - "username": "john", - }, - expectedLen: 1, - shouldHave: []string{"password"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Convert dataMap to nmVariantMap - vpnSettings := make(nmVariantMap) - vpnSettings["data"] = dbus.MakeVariant(tt.dataMap) - vpnSettings["service-type"] = dbus.MakeVariant(tt.vpnService) - - conn := make(map[string]nmVariantMap) - conn["vpn"] = vpnSettings - - fields := inferVPNFields(conn, tt.vpnService) - - assert.Len(t, fields, tt.expectedLen, "unexpected number of fields") - if len(tt.shouldHave) > 0 { - for _, expected := range tt.shouldHave { - assert.Contains(t, fields, expected, "should contain field: %s", expected) - } - } - }) - } -} - -func TestNmVariantMap(t *testing.T) { - // Test that nmVariantMap and nmSettingMap work correctly - settingMap := make(nmSettingMap) - variantMap := make(nmVariantMap) - - variantMap["test-key"] = dbus.MakeVariant("test-value") - settingMap["test-setting"] = variantMap - - assert.Contains(t, settingMap, "test-setting") - assert.Contains(t, settingMap["test-setting"], "test-key") - - value := settingMap["test-setting"]["test-key"].Value() - assert.Equal(t, "test-value", value) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend.go deleted file mode 100644 index f1bb04d..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend.go +++ /dev/null @@ -1,80 +0,0 @@ -package network - -type Backend interface { - Initialize() error - Close() - - GetWiFiEnabled() (bool, error) - SetWiFiEnabled(enabled bool) error - - ScanWiFi() error - ScanWiFiDevice(device string) error - GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) - GetWiFiQRCodeContent(ssid string) (string, error) - GetWiFiDevices() []WiFiDevice - - ConnectWiFi(req ConnectionRequest) error - DisconnectWiFi() error - DisconnectWiFiDevice(device string) error - ForgetWiFiNetwork(ssid string) error - SetWiFiAutoconnect(ssid string, autoconnect bool) error - - GetEthernetDevices() []EthernetDevice - GetWiredConnections() ([]WiredConnection, error) - GetWiredNetworkDetails(uuid string) (*WiredNetworkInfoResponse, error) - ConnectEthernet() error - DisconnectEthernet() error - DisconnectEthernetDevice(device string) error - ActivateWiredConnection(uuid string) error - - ListVPNProfiles() ([]VPNProfile, error) - ListActiveVPN() ([]VPNActive, error) - ConnectVPN(uuidOrName string, singleActive bool) error - DisconnectVPN(uuidOrName string) error - DisconnectAllVPN() error - ClearVPNCredentials(uuidOrName string) error - ListVPNPlugins() ([]VPNPlugin, error) - ImportVPN(filePath string, name string) (*VPNImportResult, error) - GetVPNConfig(uuidOrName string) (*VPNConfig, error) - UpdateVPNConfig(uuid string, updates map[string]any) error - SetVPNCredentials(uuid string, username string, password string, save bool) error - DeleteVPN(uuidOrName string) error - - GetCurrentState() (*BackendState, error) - - StartMonitoring(onStateChange func()) error - StopMonitoring() - - GetPromptBroker() PromptBroker - SetPromptBroker(broker PromptBroker) error - SubmitCredentials(token string, secrets map[string]string, save bool) error - CancelCredentials(token string) error -} - -type BackendState struct { - Backend string - NetworkStatus NetworkStatus - EthernetIP string - EthernetDevice string - EthernetConnected bool - EthernetConnectionUuid string - EthernetDevices []EthernetDevice - WiFiIP string - WiFiDevice string - WiFiConnected bool - WiFiEnabled bool - WiFiSSID string - WiFiBSSID string - WiFiSignal uint8 - WiFiNetworks []WiFiNetwork - WiFiDevices []WiFiDevice - WiredConnections []WiredConnection - VPNProfiles []VPNProfile - VPNActive []VPNActive - IsConnecting bool - ConnectingSSID string - ConnectingDevice string - IsConnectingVPN bool - ConnectingVPNUUID string - LastError string -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_hybrid_iwd_networkd.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_hybrid_iwd_networkd.go deleted file mode 100644 index ce883ba..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_hybrid_iwd_networkd.go +++ /dev/null @@ -1,245 +0,0 @@ -package network - -import ( - "fmt" -) - -type HybridIwdNetworkdBackend struct { - wifi *IWDBackend - l3 *SystemdNetworkdBackend - onStateChange func() -} - -func NewHybridIwdNetworkdBackend(w *IWDBackend, n *SystemdNetworkdBackend) (*HybridIwdNetworkdBackend, error) { - return &HybridIwdNetworkdBackend{ - wifi: w, - l3: n, - }, nil -} - -func (b *HybridIwdNetworkdBackend) Initialize() error { - if err := b.wifi.Initialize(); err != nil { - return fmt.Errorf("iwd init: %w", err) - } - if err := b.l3.Initialize(); err != nil { - return fmt.Errorf("networkd init: %w", err) - } - return nil -} - -func (b *HybridIwdNetworkdBackend) Close() { - b.wifi.Close() - b.l3.Close() -} - -func (b *HybridIwdNetworkdBackend) StartMonitoring(onStateChange func()) error { - b.onStateChange = onStateChange - - mergedCallback := func() { - ws, _ := b.wifi.GetCurrentState() - ls, _ := b.l3.GetCurrentState() - - if ws != nil && ls != nil && ws.WiFiDevice != "" && ls.WiFiIP != "" { - b.wifi.MarkIPConfigSeen() - } - - if b.onStateChange != nil { - b.onStateChange() - } - } - - if err := b.wifi.StartMonitoring(mergedCallback); err != nil { - return fmt.Errorf("wifi monitoring: %w", err) - } - if err := b.l3.StartMonitoring(mergedCallback); err != nil { - return fmt.Errorf("l3 monitoring: %w", err) - } - - return nil -} - -func (b *HybridIwdNetworkdBackend) StopMonitoring() { - b.wifi.StopMonitoring() - b.l3.StopMonitoring() -} - -func (b *HybridIwdNetworkdBackend) GetCurrentState() (*BackendState, error) { - ws, err := b.wifi.GetCurrentState() - if err != nil { - return nil, err - } - ls, err := b.l3.GetCurrentState() - if err != nil { - return nil, err - } - - merged := *ws - merged.Backend = "iwd+networkd" - - merged.WiFiIP = ls.WiFiIP - merged.EthernetConnected = ls.EthernetConnected - merged.EthernetIP = ls.EthernetIP - merged.EthernetDevice = ls.EthernetDevice - merged.EthernetConnectionUuid = ls.EthernetConnectionUuid - merged.WiredConnections = ls.WiredConnections - merged.EthernetDevices = ls.EthernetDevices - - if ls.EthernetConnected && ls.EthernetIP != "" { - merged.NetworkStatus = StatusEthernet - } else if ws.WiFiConnected && ls.WiFiIP != "" { - merged.NetworkStatus = StatusWiFi - } else { - merged.NetworkStatus = StatusDisconnected - } - - return &merged, nil -} - -func (b *HybridIwdNetworkdBackend) GetWiFiEnabled() (bool, error) { - return b.wifi.GetWiFiEnabled() -} - -func (b *HybridIwdNetworkdBackend) SetWiFiEnabled(enabled bool) error { - return b.wifi.SetWiFiEnabled(enabled) -} - -func (b *HybridIwdNetworkdBackend) ScanWiFi() error { - return b.wifi.ScanWiFi() -} - -func (b *HybridIwdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) { - return b.wifi.GetWiFiNetworkDetails(ssid) -} - -func (b *HybridIwdNetworkdBackend) GetWiFiQRCodeContent(ssid string) (string, error) { - return b.wifi.GetWiFiQRCodeContent(ssid) -} - -func (b *HybridIwdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error { - if err := b.wifi.ConnectWiFi(req); err != nil { - return err - } - - ws, err := b.wifi.GetCurrentState() - if err == nil && ws.WiFiDevice != "" { - b.l3.EnsureDhcpUp(ws.WiFiDevice) //nolint:errcheck - } - - return nil -} - -func (b *HybridIwdNetworkdBackend) DisconnectWiFi() error { - return b.wifi.DisconnectWiFi() -} - -func (b *HybridIwdNetworkdBackend) ForgetWiFiNetwork(ssid string) error { - return b.wifi.ForgetWiFiNetwork(ssid) -} - -func (b *HybridIwdNetworkdBackend) GetWiredConnections() ([]WiredConnection, error) { - return b.l3.GetWiredConnections() -} - -func (b *HybridIwdNetworkdBackend) GetWiredNetworkDetails(uuid string) (*WiredNetworkInfoResponse, error) { - return b.l3.GetWiredNetworkDetails(uuid) -} - -func (b *HybridIwdNetworkdBackend) ConnectEthernet() error { - return b.l3.ConnectEthernet() -} - -func (b *HybridIwdNetworkdBackend) DisconnectEthernet() error { - return b.l3.DisconnectEthernet() -} - -func (b *HybridIwdNetworkdBackend) DisconnectEthernetDevice(device string) error { - return b.l3.DisconnectEthernetDevice(device) -} - -func (b *HybridIwdNetworkdBackend) GetEthernetDevices() []EthernetDevice { - return b.l3.GetEthernetDevices() -} - -func (b *HybridIwdNetworkdBackend) ActivateWiredConnection(uuid string) error { - return b.l3.ActivateWiredConnection(uuid) -} - -func (b *HybridIwdNetworkdBackend) ListVPNProfiles() ([]VPNProfile, error) { - return []VPNProfile{}, nil -} - -func (b *HybridIwdNetworkdBackend) ListActiveVPN() ([]VPNActive, error) { - return []VPNActive{}, nil -} - -func (b *HybridIwdNetworkdBackend) ConnectVPN(uuidOrName string, singleActive bool) error { - return fmt.Errorf("VPN not supported in hybrid mode") -} - -func (b *HybridIwdNetworkdBackend) DisconnectVPN(uuidOrName string) error { - return fmt.Errorf("VPN not supported in hybrid mode") -} - -func (b *HybridIwdNetworkdBackend) DisconnectAllVPN() error { - return fmt.Errorf("VPN not supported in hybrid mode") -} - -func (b *HybridIwdNetworkdBackend) ClearVPNCredentials(uuidOrName string) error { - return fmt.Errorf("VPN not supported in hybrid mode") -} - -func (b *HybridIwdNetworkdBackend) ListVPNPlugins() ([]VPNPlugin, error) { - return []VPNPlugin{}, nil -} - -func (b *HybridIwdNetworkdBackend) ImportVPN(filePath string, name string) (*VPNImportResult, error) { - return nil, fmt.Errorf("VPN not supported in hybrid mode") -} - -func (b *HybridIwdNetworkdBackend) GetVPNConfig(uuidOrName string) (*VPNConfig, error) { - return nil, fmt.Errorf("VPN not supported in hybrid mode") -} - -func (b *HybridIwdNetworkdBackend) UpdateVPNConfig(uuid string, updates map[string]any) error { - return fmt.Errorf("VPN not supported in hybrid mode") -} - -func (b *HybridIwdNetworkdBackend) DeleteVPN(uuidOrName string) error { - return fmt.Errorf("VPN not supported in hybrid mode") -} - -func (b *HybridIwdNetworkdBackend) GetPromptBroker() PromptBroker { - return b.wifi.GetPromptBroker() -} - -func (b *HybridIwdNetworkdBackend) SetPromptBroker(broker PromptBroker) error { - return b.wifi.SetPromptBroker(broker) -} - -func (b *HybridIwdNetworkdBackend) SubmitCredentials(token string, secrets map[string]string, save bool) error { - return b.wifi.SubmitCredentials(token, secrets, save) -} - -func (b *HybridIwdNetworkdBackend) CancelCredentials(token string) error { - return b.wifi.CancelCredentials(token) -} - -func (b *HybridIwdNetworkdBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error { - return b.wifi.SetWiFiAutoconnect(ssid, autoconnect) -} - -func (b *HybridIwdNetworkdBackend) ScanWiFiDevice(device string) error { - return b.wifi.ScanWiFiDevice(device) -} - -func (b *HybridIwdNetworkdBackend) DisconnectWiFiDevice(device string) error { - return b.wifi.DisconnectWiFiDevice(device) -} - -func (b *HybridIwdNetworkdBackend) GetWiFiDevices() []WiFiDevice { - return b.wifi.GetWiFiDevices() -} - -func (b *HybridIwdNetworkdBackend) SetVPNCredentials(uuid, username, password string, save bool) error { - return fmt.Errorf("VPN not supported in hybrid mode") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_hybrid_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_hybrid_test.go deleted file mode 100644 index cd061ed..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_hybrid_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package network - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestHybridIwdNetworkdBackend_New(t *testing.T) { - wifi, _ := NewIWDBackend() - l3, _ := NewSystemdNetworkdBackend() - - hybrid, err := NewHybridIwdNetworkdBackend(wifi, l3) - assert.NoError(t, err) - assert.NotNil(t, hybrid) - assert.NotNil(t, hybrid.wifi) - assert.NotNil(t, hybrid.l3) -} - -func TestHybridIwdNetworkdBackend_GetCurrentState_MergesState(t *testing.T) { - wifi, _ := NewIWDBackend() - l3, _ := NewSystemdNetworkdBackend() - hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3) - - wifi.state.WiFiConnected = true - wifi.state.WiFiSSID = "TestNetwork" - wifi.state.WiFiBSSID = "00:11:22:33:44:55" - wifi.state.WiFiSignal = 75 - wifi.state.WiFiDevice = "wlan0" - - l3.state.WiFiIP = "192.168.1.100" - l3.state.EthernetConnected = false - - state, err := hybrid.GetCurrentState() - assert.NoError(t, err) - assert.NotNil(t, state) - assert.Equal(t, "iwd+networkd", state.Backend) - assert.Equal(t, "TestNetwork", state.WiFiSSID) - assert.Equal(t, "00:11:22:33:44:55", state.WiFiBSSID) - assert.Equal(t, uint8(75), state.WiFiSignal) - assert.Equal(t, "192.168.1.100", state.WiFiIP) - assert.True(t, state.WiFiConnected) - assert.False(t, state.EthernetConnected) - assert.Equal(t, StatusWiFi, state.NetworkStatus) -} - -func TestHybridIwdNetworkdBackend_GetCurrentState_EthernetPriority(t *testing.T) { - wifi, _ := NewIWDBackend() - l3, _ := NewSystemdNetworkdBackend() - hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3) - - wifi.state.WiFiConnected = true - wifi.state.WiFiSSID = "TestNetwork" - - l3.state.WiFiIP = "192.168.1.100" - l3.state.EthernetConnected = true - l3.state.EthernetIP = "192.168.1.50" - l3.state.EthernetDevice = "eth0" - - state, err := hybrid.GetCurrentState() - assert.NoError(t, err) - assert.Equal(t, StatusEthernet, state.NetworkStatus) - assert.Equal(t, "192.168.1.50", state.EthernetIP) - assert.Equal(t, "eth0", state.EthernetDevice) -} - -func TestHybridIwdNetworkdBackend_GetCurrentState_WiFiNoIP(t *testing.T) { - wifi, _ := NewIWDBackend() - l3, _ := NewSystemdNetworkdBackend() - hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3) - - wifi.state.WiFiConnected = true - wifi.state.WiFiSSID = "TestNetwork" - - l3.state.WiFiIP = "" - l3.state.EthernetConnected = false - - state, err := hybrid.GetCurrentState() - assert.NoError(t, err) - assert.Equal(t, StatusDisconnected, state.NetworkStatus) - assert.True(t, state.WiFiConnected) - assert.Empty(t, state.WiFiIP) -} - -func TestHybridIwdNetworkdBackend_WiFiDelegation(t *testing.T) { - wifi, _ := NewIWDBackend() - l3, _ := NewSystemdNetworkdBackend() - hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3) - - enabled, err := hybrid.GetWiFiEnabled() - assert.NoError(t, err) - assert.True(t, enabled) - - state, err := hybrid.GetCurrentState() - assert.NoError(t, err) - assert.NotNil(t, state) - assert.Equal(t, "iwd+networkd", state.Backend) -} - -func TestHybridIwdNetworkdBackend_WiredDelegation(t *testing.T) { - wifi, _ := NewIWDBackend() - l3, _ := NewSystemdNetworkdBackend() - hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3) - - conns, err := hybrid.GetWiredConnections() - assert.NoError(t, err) - assert.Empty(t, conns) -} - -func TestHybridIwdNetworkdBackend_VPNNotSupported(t *testing.T) { - wifi, _ := NewIWDBackend() - l3, _ := NewSystemdNetworkdBackend() - hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3) - - profiles, err := hybrid.ListVPNProfiles() - assert.NoError(t, err) - assert.Empty(t, profiles) - - active, err := hybrid.ListActiveVPN() - assert.NoError(t, err) - assert.Empty(t, active) - - err = hybrid.ConnectVPN("test", false) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not supported") -} - -func TestHybridIwdNetworkdBackend_PromptBrokerDelegation(t *testing.T) { - wifi, _ := NewIWDBackend() - l3, _ := NewSystemdNetworkdBackend() - hybrid, _ := NewHybridIwdNetworkdBackend(wifi, l3) - - broker := hybrid.GetPromptBroker() - assert.Nil(t, broker) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd.go deleted file mode 100644 index f73aaf4..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd.go +++ /dev/null @@ -1,238 +0,0 @@ -package network - -import ( - "fmt" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/godbus/dbus/v5" -) - -const ( - iwdBusName = "net.connman.iwd" - iwdObjectPath = "/" - iwdAdapterInterface = "net.connman.iwd.Adapter" - iwdDeviceInterface = "net.connman.iwd.Device" - iwdStationInterface = "net.connman.iwd.Station" - iwdNetworkInterface = "net.connman.iwd.Network" - iwdKnownNetworkInterface = "net.connman.iwd.KnownNetwork" - dbusObjectManager = "org.freedesktop.DBus.ObjectManager" - dbusPropertiesInterface = "org.freedesktop.DBus.Properties" -) - -type connectAttempt struct { - ssid string - netPath dbus.ObjectPath - start time.Time - deadline time.Time - sawAuthish bool - connectedAt time.Time - sawIPConfig bool - sawPromptRetry bool - finalized bool - mu sync.Mutex -} - -type IWDBackend struct { - conn *dbus.Conn - state *BackendState - stateMutex sync.RWMutex - promptBroker PromptBroker - onStateChange func() - - devicePath dbus.ObjectPath - stationPath dbus.ObjectPath - adapterPath dbus.ObjectPath - - iwdAgent *IWDAgent - - stopChan chan struct{} - sigWG sync.WaitGroup - curAttempt *connectAttempt - attemptMutex sync.RWMutex - recentScans map[string]time.Time - recentScansMu sync.Mutex -} - -func NewIWDBackend() (*IWDBackend, error) { - backend := &IWDBackend{ - state: &BackendState{ - Backend: "iwd", - WiFiEnabled: true, - }, - stopChan: make(chan struct{}), - recentScans: make(map[string]time.Time), - } - - return backend, nil -} - -func (b *IWDBackend) Initialize() error { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return fmt.Errorf("failed to connect to system bus: %w", err) - } - b.conn = conn - - if err := b.discoverDevices(); err != nil { - conn.Close() - return fmt.Errorf("failed to discover iwd devices: %w", err) - } - - if err := b.updateState(); err != nil { - conn.Close() - return fmt.Errorf("failed to get initial state: %w", err) - } - - return nil -} - -func (b *IWDBackend) Close() { - close(b.stopChan) - b.sigWG.Wait() - - if b.iwdAgent != nil { - b.iwdAgent.Close() - } - - if b.conn != nil { - b.conn.Close() - } -} - -func (b *IWDBackend) discoverDevices() error { - obj := b.conn.Object(iwdBusName, iwdObjectPath) - - var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant - err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects) - if err != nil { - return fmt.Errorf("failed to get managed objects: %w", err) - } - - for path, interfaces := range objects { - if _, hasStation := interfaces[iwdStationInterface]; hasStation { - b.stationPath = path - } - if _, hasDevice := interfaces[iwdDeviceInterface]; hasDevice { - b.devicePath = path - - if devProps, ok := interfaces[iwdDeviceInterface]; ok { - if nameVar, ok := devProps["Name"]; ok { - if name, ok := nameVar.Value().(string); ok { - b.stateMutex.Lock() - b.state.WiFiDevice = name - b.stateMutex.Unlock() - } - } - } - } - if _, hasAdapter := interfaces[iwdAdapterInterface]; hasAdapter { - b.adapterPath = path - } - } - - if b.stationPath == "" || b.devicePath == "" { - return fmt.Errorf("no WiFi device found") - } - - return nil -} - -func (b *IWDBackend) GetCurrentState() (*BackendState, error) { - b.stateMutex.RLock() - defer b.stateMutex.RUnlock() - - state := *b.state - state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...) - state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...) - state.WiFiDevices = b.getWiFiDevicesLocked() - - return &state, nil -} - -func (b *IWDBackend) OnUserCanceledPrompt() { - b.stateMutex.RLock() - cancelledSSID := b.state.ConnectingSSID - b.stateMutex.RUnlock() - - b.setConnectError("user-canceled") - - if cancelledSSID != "" { - if err := b.ForgetWiFiNetwork(cancelledSSID); err != nil { - log.Warnf("failed to forget cancelled WiFi network %s: %v", cancelledSSID, err) - } - } - - if b.onStateChange != nil { - b.onStateChange() - } -} - -func (b *IWDBackend) OnPromptRetry(ssid string) { - b.attemptMutex.RLock() - att := b.curAttempt - b.attemptMutex.RUnlock() - - if att != nil && att.ssid == ssid { - att.mu.Lock() - att.sawPromptRetry = true - att.mu.Unlock() - } -} - -func (b *IWDBackend) MarkIPConfigSeen() { - b.attemptMutex.RLock() - att := b.curAttempt - b.attemptMutex.RUnlock() - if att != nil { - att.mu.Lock() - att.sawIPConfig = true - att.mu.Unlock() - } -} - -func (b *IWDBackend) GetPromptBroker() PromptBroker { - return b.promptBroker -} - -func (b *IWDBackend) SetPromptBroker(broker PromptBroker) error { - if broker == nil { - return fmt.Errorf("broker cannot be nil") - } - - b.promptBroker = broker - return nil -} - -func (b *IWDBackend) SubmitCredentials(token string, secrets map[string]string, save bool) error { - if b.promptBroker == nil { - return fmt.Errorf("prompt broker not initialized") - } - - return b.promptBroker.Resolve(token, PromptReply{ - Secrets: secrets, - Save: save, - Cancel: false, - }) -} - -func (b *IWDBackend) CancelCredentials(token string) error { - if b.promptBroker == nil { - return fmt.Errorf("prompt broker not initialized") - } - - return b.promptBroker.Resolve(token, PromptReply{ - Cancel: true, - }) -} - -func (b *IWDBackend) StopMonitoring() { - select { - case <-b.stopChan: - return - default: - close(b.stopChan) - } - b.sigWG.Wait() -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_signals.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_signals.go deleted file mode 100644 index bde4753..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_signals.go +++ /dev/null @@ -1,355 +0,0 @@ -package network - -import ( - "fmt" - "time" - - "github.com/godbus/dbus/v5" -) - -func (b *IWDBackend) StartMonitoring(onStateChange func()) error { - b.onStateChange = onStateChange - - if b.promptBroker != nil { - agent, err := NewIWDAgent(b.conn, b.promptBroker) - if err != nil { - return fmt.Errorf("failed to start IWD agent: %w", err) - } - agent.onUserCanceled = b.OnUserCanceledPrompt - agent.onPromptRetry = b.OnPromptRetry - b.iwdAgent = agent - } - - sigChan := make(chan *dbus.Signal, 100) - b.conn.Signal(sigChan) - - if b.devicePath != "" { - err := b.conn.AddMatchSignal( - dbus.WithMatchObjectPath(b.devicePath), - dbus.WithMatchInterface(dbusPropertiesInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - if err != nil { - return fmt.Errorf("failed to add device signal match: %w", err) - } - } - - if b.stationPath != "" { - err := b.conn.AddMatchSignal( - dbus.WithMatchObjectPath(b.stationPath), - dbus.WithMatchInterface(dbusPropertiesInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - if err != nil { - return fmt.Errorf("failed to add station signal match: %w", err) - } - } - - b.sigWG.Add(1) - go b.signalHandler(sigChan) - - return nil -} - -func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) { - defer b.sigWG.Done() - - for { - select { - case <-b.stopChan: - b.conn.RemoveSignal(sigChan) - close(sigChan) - return - - case sig := <-sigChan: - if sig == nil { - return - } - - if sig.Name != dbusPropertiesInterface+".PropertiesChanged" { - continue - } - - if len(sig.Body) < 2 { - continue - } - - iface, ok := sig.Body[0].(string) - if !ok { - continue - } - - changed, ok := sig.Body[1].(map[string]dbus.Variant) - if !ok { - continue - } - - stateChanged := false - - switch iface { - case iwdDeviceInterface: - if sig.Path == b.devicePath { - if poweredVar, ok := changed["Powered"]; ok { - if powered, ok := poweredVar.Value().(bool); ok { - b.stateMutex.Lock() - if b.state.WiFiEnabled != powered { - b.state.WiFiEnabled = powered - stateChanged = true - } - b.stateMutex.Unlock() - } - } - } - - case iwdStationInterface: - if sig.Path == b.stationPath { - if scanningVar, ok := changed["Scanning"]; ok { - if scanning, ok := scanningVar.Value().(bool); ok && !scanning { - networks, err := b.updateWiFiNetworks() - if err == nil { - b.stateMutex.Lock() - b.state.WiFiNetworks = networks - b.stateMutex.Unlock() - stateChanged = true - } - - b.stateMutex.RLock() - wifiConnected := b.state.WiFiConnected - b.stateMutex.RUnlock() - - if wifiConnected { - stationObj := b.conn.Object(iwdBusName, b.stationPath) - connNetVar, err := stationObj.GetProperty(iwdStationInterface + ".ConnectedNetwork") - if err == nil && connNetVar.Value() != nil { - if netPath, ok := connNetVar.Value().(dbus.ObjectPath); ok && netPath != "/" { - var orderedNetworks [][]dbus.Variant - err = stationObj.Call(iwdStationInterface+".GetOrderedNetworks", 0).Store(&orderedNetworks) - if err == nil { - for _, netData := range orderedNetworks { - if len(netData) < 2 { - continue - } - currentNetPath, ok := netData[0].Value().(dbus.ObjectPath) - if !ok || currentNetPath != netPath { - continue - } - signalStrength, ok := netData[1].Value().(int16) - if !ok { - continue - } - signalDbm := signalStrength / 100 - signal := uint8(signalDbm + 100) - if signalDbm > 0 { - signal = 100 - } else if signalDbm < -100 { - signal = 0 - } - b.stateMutex.Lock() - if b.state.WiFiSignal != signal { - b.state.WiFiSignal = signal - stateChanged = true - } - b.stateMutex.Unlock() - break - } - } - } - } - } - } - } - - if stateVar, ok := changed["State"]; ok { - if state, ok := stateVar.Value().(string); ok { - b.attemptMutex.RLock() - att := b.curAttempt - b.attemptMutex.RUnlock() - - var connPath dbus.ObjectPath - if v, ok := changed["ConnectedNetwork"]; ok { - if v.Value() != nil { - if p, ok := v.Value().(dbus.ObjectPath); ok { - connPath = p - } - } - } - if connPath == "" { - station := b.conn.Object(iwdBusName, b.stationPath) - if cnVar, err := station.GetProperty(iwdStationInterface + ".ConnectedNetwork"); err == nil && cnVar.Value() != nil { - cnVar.Store(&connPath) - } - } - - b.stateMutex.RLock() - prevConnected := b.state.WiFiConnected - prevSSID := b.state.WiFiSSID - b.stateMutex.RUnlock() - - targetPath := dbus.ObjectPath("") - if att != nil { - targetPath = att.netPath - } - - isTarget := att != nil && targetPath != "" && connPath == targetPath - - if att != nil { - switch state { - case "authenticating", "associating", "associated", "roaming": - att.mu.Lock() - att.sawAuthish = true - att.mu.Unlock() - } - } - - if att != nil && state == "connected" && isTarget { - att.mu.Lock() - if att.connectedAt.IsZero() { - att.connectedAt = time.Now() - } - att.mu.Unlock() - } - - if att != nil && state == "configuring" { - att.mu.Lock() - att.sawIPConfig = true - att.mu.Unlock() - } - - switch state { - case "connected": - b.stateMutex.Lock() - b.state.WiFiConnected = true - b.state.NetworkStatus = StatusWiFi - b.state.IsConnecting = false - b.state.ConnectingSSID = "" - b.state.LastError = "" - b.stateMutex.Unlock() - - if connPath != "" && connPath != "/" { - netObj := b.conn.Object(iwdBusName, connPath) - if nameVar, err := netObj.GetProperty(iwdNetworkInterface + ".Name"); err == nil { - if name, ok := nameVar.Value().(string); ok { - b.stateMutex.Lock() - b.state.WiFiSSID = name - b.stateMutex.Unlock() - } - } - } - - stateChanged = true - - if att != nil && isTarget { - go func(attLocal *connectAttempt, tgt dbus.ObjectPath) { - time.Sleep(3 * time.Second) - station := b.conn.Object(iwdBusName, b.stationPath) - var nowState string - if stVar, err := station.GetProperty(iwdStationInterface + ".State"); err == nil { - stVar.Store(&nowState) - } - var nowConn dbus.ObjectPath - if cnVar, err := station.GetProperty(iwdStationInterface + ".ConnectedNetwork"); err == nil && cnVar.Value() != nil { - cnVar.Store(&nowConn) - } - - if nowState == "connected" && nowConn == tgt { - b.finalizeAttempt(attLocal, "") - b.attemptMutex.Lock() - if b.curAttempt == attLocal { - b.curAttempt = nil - } - b.attemptMutex.Unlock() - } - }(att, targetPath) - } - - case "disconnecting", "disconnected": - if att != nil { - wasConnectedToTarget := prevConnected && prevSSID == att.ssid - if wasConnectedToTarget || isTarget { - code := b.classifyAttempt(att) - b.finalizeAttempt(att, code) - b.attemptMutex.Lock() - if b.curAttempt == att { - b.curAttempt = nil - } - b.attemptMutex.Unlock() - } - } - - b.stateMutex.Lock() - b.state.WiFiConnected = false - if state == "disconnected" { - b.state.NetworkStatus = StatusDisconnected - } - b.stateMutex.Unlock() - stateChanged = true - } - } - } - - if connNetVar, ok := changed["ConnectedNetwork"]; ok { - if netPath, ok := connNetVar.Value().(dbus.ObjectPath); ok && netPath != "/" { - netObj := b.conn.Object(iwdBusName, netPath) - nameVar, err := netObj.GetProperty(iwdNetworkInterface + ".Name") - if err == nil { - if name, ok := nameVar.Value().(string); ok { - b.stateMutex.Lock() - if b.state.WiFiSSID != name { - b.state.WiFiSSID = name - stateChanged = true - } - b.stateMutex.Unlock() - } - } - - stationObj := b.conn.Object(iwdBusName, b.stationPath) - var orderedNetworks [][]dbus.Variant - err = stationObj.Call(iwdStationInterface+".GetOrderedNetworks", 0).Store(&orderedNetworks) - if err == nil { - for _, netData := range orderedNetworks { - if len(netData) < 2 { - continue - } - currentNetPath, ok := netData[0].Value().(dbus.ObjectPath) - if !ok || currentNetPath != netPath { - continue - } - signalStrength, ok := netData[1].Value().(int16) - if !ok { - continue - } - signalDbm := signalStrength / 100 - signal := uint8(signalDbm + 100) - if signalDbm > 0 { - signal = 100 - } else if signalDbm < -100 { - signal = 0 - } - b.stateMutex.Lock() - if b.state.WiFiSignal != signal { - b.state.WiFiSignal = signal - stateChanged = true - } - b.stateMutex.Unlock() - break - } - } - } else { - b.stateMutex.Lock() - if b.state.WiFiSSID != "" { - b.state.WiFiSSID = "" - b.state.WiFiSignal = 0 - stateChanged = true - } - b.stateMutex.Unlock() - } - } - } - } - - if stateChanged && b.onStateChange != nil { - b.onStateChange() - } - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_test.go deleted file mode 100644 index 829e413..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_test.go +++ /dev/null @@ -1,212 +0,0 @@ -package network - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestIWDBackend_MarkIPConfigSeen(t *testing.T) { - backend, _ := NewIWDBackend() - - att := &connectAttempt{ - ssid: "TestNetwork", - netPath: "/net/connman/iwd/0/1/test", - start: time.Now(), - deadline: time.Now().Add(15 * time.Second), - } - - backend.attemptMutex.Lock() - backend.curAttempt = att - backend.attemptMutex.Unlock() - - backend.MarkIPConfigSeen() - - att.mu.Lock() - assert.True(t, att.sawIPConfig, "sawIPConfig should be true after MarkIPConfigSeen") - att.mu.Unlock() -} - -func TestIWDBackend_MarkIPConfigSeen_NoAttempt(t *testing.T) { - backend, _ := NewIWDBackend() - - backend.attemptMutex.Lock() - backend.curAttempt = nil - backend.attemptMutex.Unlock() - - backend.MarkIPConfigSeen() -} - -func TestIWDBackend_OnPromptRetry(t *testing.T) { - backend, _ := NewIWDBackend() - - att := &connectAttempt{ - ssid: "TestNetwork", - netPath: "/net/connman/iwd/0/1/test", - start: time.Now(), - deadline: time.Now().Add(15 * time.Second), - } - - backend.attemptMutex.Lock() - backend.curAttempt = att - backend.attemptMutex.Unlock() - - backend.OnPromptRetry("TestNetwork") - - att.mu.Lock() - assert.True(t, att.sawPromptRetry, "sawPromptRetry should be true after OnPromptRetry") - att.mu.Unlock() -} - -func TestIWDBackend_OnPromptRetry_WrongSSID(t *testing.T) { - backend, _ := NewIWDBackend() - - att := &connectAttempt{ - ssid: "TestNetwork", - netPath: "/net/connman/iwd/0/1/test", - start: time.Now(), - deadline: time.Now().Add(15 * time.Second), - } - - backend.attemptMutex.Lock() - backend.curAttempt = att - backend.attemptMutex.Unlock() - - backend.OnPromptRetry("DifferentNetwork") - - att.mu.Lock() - assert.False(t, att.sawPromptRetry, "sawPromptRetry should remain false for different SSID") - att.mu.Unlock() -} - -func TestIWDBackend_ClassifyAttempt_BadCredentials_PromptRetry(t *testing.T) { - backend, _ := NewIWDBackend() - - att := &connectAttempt{ - ssid: "TestNetwork", - netPath: "/test", - start: time.Now().Add(-5 * time.Second), - deadline: time.Now().Add(10 * time.Second), - sawPromptRetry: true, - } - - code := backend.classifyAttempt(att) - assert.Equal(t, "bad-credentials", code) -} - -func TestIWDBackend_ClassifyAttempt_DhcpTimeout(t *testing.T) { - backend, _ := NewIWDBackend() - - att := &connectAttempt{ - ssid: "TestNetwork", - netPath: "/test", - start: time.Now().Add(-13 * time.Second), - deadline: time.Now().Add(2 * time.Second), - sawAuthish: true, - sawIPConfig: false, - } - - code := backend.classifyAttempt(att) - assert.Equal(t, "dhcp-timeout", code) -} - -func TestIWDBackend_ClassifyAttempt_AssocTimeout(t *testing.T) { - backend, _ := NewIWDBackend() - - att := &connectAttempt{ - ssid: "TestNetwork", - netPath: "/test", - start: time.Now().Add(-5 * time.Second), - deadline: time.Now().Add(10 * time.Second), - } - - backend.recentScansMu.Lock() - backend.recentScans["TestNetwork"] = time.Now() - backend.recentScansMu.Unlock() - - code := backend.classifyAttempt(att) - assert.Equal(t, "assoc-timeout", code) -} - -func TestIWDBackend_ClassifyAttempt_NoSuchSSID(t *testing.T) { - backend, _ := NewIWDBackend() - - att := &connectAttempt{ - ssid: "TestNetwork", - netPath: "/test", - start: time.Now().Add(-5 * time.Second), - deadline: time.Now().Add(10 * time.Second), - } - - code := backend.classifyAttempt(att) - assert.Equal(t, "no-such-ssid", code) -} - -func TestIWDBackend_MapIwdDBusError(t *testing.T) { - backend, _ := NewIWDBackend() - - testCases := []struct { - name string - expected string - }{ - {"net.connman.iwd.Error.AlreadyConnected", "already-connected"}, - {"net.connman.iwd.Error.AuthenticationFailed", "bad-credentials"}, - {"net.connman.iwd.Error.InvalidKey", "bad-credentials"}, - {"net.connman.iwd.Error.IncorrectPassphrase", "bad-credentials"}, - {"net.connman.iwd.Error.NotFound", "no-such-ssid"}, - {"net.connman.iwd.Error.NotSupported", "connection-failed"}, - {"net.connman.iwd.Agent.Error.Canceled", "user-canceled"}, - {"net.connman.iwd.Error.Unknown", "connection-failed"}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - code := backend.mapIwdDBusError(tc.name) - assert.Equal(t, tc.expected, code) - }) - } -} - -func TestConnectAttempt_Finalization(t *testing.T) { - backend, _ := NewIWDBackend() - backend.state = &BackendState{} - - att := &connectAttempt{ - ssid: "TestNetwork", - netPath: "/test", - start: time.Now(), - deadline: time.Now().Add(15 * time.Second), - } - - backend.finalizeAttempt(att, "bad-credentials") - - att.mu.Lock() - assert.True(t, att.finalized) - att.mu.Unlock() - - backend.stateMutex.RLock() - assert.False(t, backend.state.IsConnecting) - assert.Empty(t, backend.state.ConnectingSSID) - assert.Equal(t, "bad-credentials", backend.state.LastError) - backend.stateMutex.RUnlock() -} - -func TestConnectAttempt_DoubleFinalization(t *testing.T) { - backend, _ := NewIWDBackend() - backend.state = &BackendState{} - - att := &connectAttempt{ - ssid: "TestNetwork", - netPath: "/test", - start: time.Now(), - deadline: time.Now().Add(15 * time.Second), - } - - backend.finalizeAttempt(att, "bad-credentials") - backend.finalizeAttempt(att, "dhcp-timeout") - - backend.stateMutex.RLock() - assert.Equal(t, "bad-credentials", backend.state.LastError) - backend.stateMutex.RUnlock() -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_unimplemented.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_unimplemented.go deleted file mode 100644 index f5a8b03..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_unimplemented.go +++ /dev/null @@ -1,133 +0,0 @@ -package network - -import ( - "fmt" - "os" -) - -func (b *IWDBackend) GetWiredConnections() ([]WiredConnection, error) { - return nil, fmt.Errorf("wired connections not supported by iwd") -} - -func (b *IWDBackend) GetWiredNetworkDetails(uuid string) (*WiredNetworkInfoResponse, error) { - return nil, fmt.Errorf("wired connections not supported by iwd") -} - -func (b *IWDBackend) ConnectEthernet() error { - return fmt.Errorf("wired connections not supported by iwd") -} - -func (b *IWDBackend) DisconnectEthernet() error { - return fmt.Errorf("wired connections not supported by iwd") -} - -func (b *IWDBackend) DisconnectEthernetDevice(device string) error { - return fmt.Errorf("wired connections not supported by iwd") -} - -func (b *IWDBackend) GetEthernetDevices() []EthernetDevice { - return []EthernetDevice{} -} - -func (b *IWDBackend) ActivateWiredConnection(uuid string) error { - return fmt.Errorf("wired connections not supported by iwd") -} - -func (b *IWDBackend) ListVPNProfiles() ([]VPNProfile, error) { - return nil, fmt.Errorf("VPN not supported by iwd backend") -} - -func (b *IWDBackend) ListActiveVPN() ([]VPNActive, error) { - return nil, fmt.Errorf("VPN not supported by iwd backend") -} - -func (b *IWDBackend) ConnectVPN(uuidOrName string, singleActive bool) error { - return fmt.Errorf("VPN not supported by iwd backend") -} - -func (b *IWDBackend) DisconnectVPN(uuidOrName string) error { - return fmt.Errorf("VPN not supported by iwd backend") -} - -func (b *IWDBackend) DisconnectAllVPN() error { - return fmt.Errorf("VPN not supported by iwd backend") -} - -func (b *IWDBackend) ClearVPNCredentials(uuidOrName string) error { - return fmt.Errorf("VPN not supported by iwd backend") -} - -func (b *IWDBackend) ListVPNPlugins() ([]VPNPlugin, error) { - return nil, fmt.Errorf("VPN not supported by iwd backend") -} - -func (b *IWDBackend) ImportVPN(filePath string, name string) (*VPNImportResult, error) { - return nil, fmt.Errorf("VPN not supported by iwd backend") -} - -func (b *IWDBackend) GetVPNConfig(uuidOrName string) (*VPNConfig, error) { - return nil, fmt.Errorf("VPN not supported by iwd backend") -} - -func (b *IWDBackend) UpdateVPNConfig(uuid string, updates map[string]any) error { - return fmt.Errorf("VPN not supported by iwd backend") -} - -func (b *IWDBackend) DeleteVPN(uuidOrName string) error { - return fmt.Errorf("VPN not supported by iwd backend") -} - -func (b *IWDBackend) SetVPNCredentials(uuid, username, password string, save bool) error { - return fmt.Errorf("VPN not supported by iwd backend") -} - -func (b *IWDBackend) ScanWiFiDevice(device string) error { - return b.ScanWiFi() -} - -func (b *IWDBackend) DisconnectWiFiDevice(device string) error { - return b.DisconnectWiFi() -} - -func (b *IWDBackend) GetWiFiDevices() []WiFiDevice { - b.stateMutex.RLock() - defer b.stateMutex.RUnlock() - return b.getWiFiDevicesLocked() -} - -func (b *IWDBackend) getWiFiDevicesLocked() []WiFiDevice { - if b.state.WiFiDevice == "" { - return nil - } - - stateStr := "disconnected" - if b.state.WiFiConnected { - stateStr = "connected" - } - - return []WiFiDevice{{ - Name: b.state.WiFiDevice, - State: stateStr, - Connected: b.state.WiFiConnected, - SSID: b.state.WiFiSSID, - Signal: b.state.WiFiSignal, - IP: b.state.WiFiIP, - Networks: b.state.WiFiNetworks, - }} -} - -func (b *IWDBackend) GetWiFiQRCodeContent(ssid string) (string, error) { - path := iwdConfigPath(ssid) - - data, err := os.ReadFile(path) - if err != nil { - return "", fmt.Errorf("no saved iwd config for `%s`: %w", ssid, err) - } - - passphrase, err := parseIWDPassphrase(string(data)) - if err != nil { - return "", fmt.Errorf("failed to read passphrase for `%s`: %w", ssid, err) - } - - return FormatWiFiQRString("WPA", ssid, passphrase), nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_wifi.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_wifi.go deleted file mode 100644 index b3bea13..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_iwd_wifi.go +++ /dev/null @@ -1,662 +0,0 @@ -package network - -import ( - "fmt" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - "github.com/godbus/dbus/v5" -) - -func (b *IWDBackend) updateState() error { - if b.devicePath == "" { - return nil - } - - obj := b.conn.Object(iwdBusName, b.devicePath) - - poweredVar, err := obj.GetProperty(iwdDeviceInterface + ".Powered") - if err == nil { - if powered, ok := poweredVar.Value().(bool); ok { - b.stateMutex.Lock() - b.state.WiFiEnabled = powered - b.stateMutex.Unlock() - } - } - - if b.stationPath == "" { - return nil - } - - stationObj := b.conn.Object(iwdBusName, b.stationPath) - - stateVar, err := stationObj.GetProperty(iwdStationInterface + ".State") - if err == nil { - if state, ok := stateVar.Value().(string); ok { - b.stateMutex.Lock() - b.state.WiFiConnected = (state == "connected") - if state == "connected" { - b.state.NetworkStatus = StatusWiFi - } else { - b.state.NetworkStatus = StatusDisconnected - } - b.stateMutex.Unlock() - } - } - - connNetVar, err := stationObj.GetProperty(iwdStationInterface + ".ConnectedNetwork") - if err == nil && connNetVar.Value() != nil { - if netPath, ok := connNetVar.Value().(dbus.ObjectPath); ok && netPath != "/" { - netObj := b.conn.Object(iwdBusName, netPath) - - nameVar, err := netObj.GetProperty(iwdNetworkInterface + ".Name") - if err == nil { - if name, ok := nameVar.Value().(string); ok { - b.stateMutex.Lock() - b.state.WiFiSSID = name - b.stateMutex.Unlock() - } - } - - var orderedNetworks [][]dbus.Variant - err = stationObj.Call(iwdStationInterface+".GetOrderedNetworks", 0).Store(&orderedNetworks) - if err == nil { - for _, netData := range orderedNetworks { - if len(netData) < 2 { - continue - } - currentNetPath, ok := netData[0].Value().(dbus.ObjectPath) - if !ok || currentNetPath != netPath { - continue - } - signalStrength, ok := netData[1].Value().(int16) - if !ok { - continue - } - signalDbm := signalStrength / 100 - signal := uint8(signalDbm + 100) - if signalDbm > 0 { - signal = 100 - } else if signalDbm < -100 { - signal = 0 - } - b.stateMutex.Lock() - b.state.WiFiSignal = signal - b.stateMutex.Unlock() - break - } - } - } - } - - networks, err := b.updateWiFiNetworks() - if err == nil { - b.stateMutex.Lock() - b.state.WiFiNetworks = networks - b.stateMutex.Unlock() - } - - return nil -} - -func (b *IWDBackend) GetWiFiEnabled() (bool, error) { - b.stateMutex.RLock() - defer b.stateMutex.RUnlock() - return b.state.WiFiEnabled, nil -} - -func (b *IWDBackend) SetWiFiEnabled(enabled bool) error { - if b.devicePath == "" { - return fmt.Errorf("no WiFi device available") - } - - obj := b.conn.Object(iwdBusName, b.devicePath) - call := obj.Call(dbusPropertiesInterface+".Set", 0, iwdDeviceInterface, "Powered", dbus.MakeVariant(enabled)) - if call.Err != nil { - return fmt.Errorf("failed to set WiFi enabled: %w", call.Err) - } - - b.stateMutex.Lock() - b.state.WiFiEnabled = enabled - b.stateMutex.Unlock() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil -} - -func (b *IWDBackend) ScanWiFi() error { - if b.stationPath == "" { - return fmt.Errorf("no WiFi device available") - } - - obj := b.conn.Object(iwdBusName, b.stationPath) - - scanningVar, err := obj.GetProperty(iwdStationInterface + ".Scanning") - if err != nil { - return fmt.Errorf("failed to check scanning state: %w", err) - } - - if scanning, ok := scanningVar.Value().(bool); ok && scanning { - return fmt.Errorf("scan already in progress") - } - - call := obj.Call(iwdStationInterface+".Scan", 0) - if call.Err != nil { - return fmt.Errorf("scan request failed: %w", call.Err) - } - - return nil -} - -func (b *IWDBackend) updateWiFiNetworks() ([]WiFiNetwork, error) { - if b.stationPath == "" { - return nil, fmt.Errorf("no WiFi device available") - } - - obj := b.conn.Object(iwdBusName, b.stationPath) - - var orderedNetworks [][]dbus.Variant - err := obj.Call(iwdStationInterface+".GetOrderedNetworks", 0).Store(&orderedNetworks) - if err != nil { - return nil, fmt.Errorf("failed to get networks: %w", err) - } - - knownNetworks, err := b.getKnownNetworks() - if err != nil { - knownNetworks = make(map[string]bool) - } - - autoconnectMap, err := b.getAutoconnectSettings() - if err != nil { - autoconnectMap = make(map[string]bool) - } - - b.stateMutex.RLock() - currentSSID := b.state.WiFiSSID - wifiConnected := b.state.WiFiConnected - b.stateMutex.RUnlock() - - networks := make([]WiFiNetwork, 0, len(orderedNetworks)) - for _, netData := range orderedNetworks { - if len(netData) < 2 { - continue - } - - networkPath, ok := netData[0].Value().(dbus.ObjectPath) - if !ok { - continue - } - - signalStrength, ok := netData[1].Value().(int16) - if !ok { - continue - } - - netObj := b.conn.Object(iwdBusName, networkPath) - - nameVar, err := netObj.GetProperty(iwdNetworkInterface + ".Name") - if err != nil { - continue - } - name, ok := nameVar.Value().(string) - if !ok { - continue - } - - typeVar, err := netObj.GetProperty(iwdNetworkInterface + ".Type") - if err != nil { - continue - } - netType, ok := typeVar.Value().(string) - if !ok { - continue - } - - signalDbm := signalStrength / 100 - signal := uint8(signalDbm + 100) - if signalDbm > 0 { - signal = 100 - } else if signalDbm < -100 { - signal = 0 - } - - secured := netType != "open" - - network := WiFiNetwork{ - SSID: name, - Signal: signal, - Secured: secured, - Connected: wifiConnected && name == currentSSID, - Saved: knownNetworks[name], - Autoconnect: autoconnectMap[name], - Enterprise: netType == "8021x", - } - - networks = append(networks, network) - } - - sortWiFiNetworks(networks) - - b.stateMutex.Lock() - b.state.WiFiNetworks = networks - b.stateMutex.Unlock() - - now := time.Now() - b.recentScansMu.Lock() - for _, net := range networks { - b.recentScans[net.SSID] = now - } - b.recentScansMu.Unlock() - - return networks, nil -} - -func (b *IWDBackend) getKnownNetworks() (map[string]bool, error) { - obj := b.conn.Object(iwdBusName, iwdObjectPath) - - var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant - err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects) - if err != nil { - return nil, err - } - - known := make(map[string]bool) - for _, interfaces := range objects { - if knownProps, ok := interfaces[iwdKnownNetworkInterface]; ok { - if nameVar, ok := knownProps["Name"]; ok { - if name, ok := nameVar.Value().(string); ok { - known[name] = true - } - } - } - } - - return known, nil -} - -func (b *IWDBackend) getAutoconnectSettings() (map[string]bool, error) { - obj := b.conn.Object(iwdBusName, iwdObjectPath) - - var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant - err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects) - if err != nil { - return nil, err - } - - autoconnectMap := make(map[string]bool) - for _, interfaces := range objects { - if knownProps, ok := interfaces[iwdKnownNetworkInterface]; ok { - if nameVar, ok := knownProps["Name"]; ok { - if name, ok := nameVar.Value().(string); ok { - autoconnect := true - if acVar, ok := knownProps["AutoConnect"]; ok { - if ac, ok := acVar.Value().(bool); ok { - autoconnect = ac - } - } - autoconnectMap[name] = autoconnect - } - } - } - } - - return autoconnectMap, nil -} - -func (b *IWDBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) { - b.stateMutex.RLock() - networks := b.state.WiFiNetworks - b.stateMutex.RUnlock() - - var found *WiFiNetwork - for i := range networks { - if networks[i].SSID == ssid { - found = &networks[i] - break - } - } - - if found == nil { - return nil, fmt.Errorf("network not found: %s", ssid) - } - - return &NetworkInfoResponse{ - SSID: ssid, - Bands: []WiFiNetwork{*found}, - }, nil -} - -func (b *IWDBackend) setConnectError(code string) { - b.stateMutex.Lock() - b.state.IsConnecting = false - b.state.ConnectingSSID = "" - b.state.LastError = code - b.stateMutex.Unlock() -} - -func (b *IWDBackend) seenInRecentScan(ssid string) bool { - b.recentScansMu.Lock() - defer b.recentScansMu.Unlock() - lastSeen, ok := b.recentScans[ssid] - return ok && time.Since(lastSeen) < 30*time.Second -} - -func (b *IWDBackend) classifyAttempt(att *connectAttempt) string { - att.mu.Lock() - defer att.mu.Unlock() - - if att.sawPromptRetry { - return errdefs.ErrBadCredentials - } - - if !att.connectedAt.IsZero() && !att.sawIPConfig { - connDuration := time.Since(att.connectedAt) - if connDuration > 500*time.Millisecond && connDuration < 3*time.Second { - return errdefs.ErrBadCredentials - } - } - - if (att.sawAuthish || !att.connectedAt.IsZero()) && !att.sawIPConfig { - if time.Since(att.start) > 12*time.Second { - return errdefs.ErrDhcpTimeout - } - } - - if !att.sawAuthish && att.connectedAt.IsZero() { - if !b.seenInRecentScan(att.ssid) { - return errdefs.ErrNoSuchSSID - } - return errdefs.ErrAssocTimeout - } - - return errdefs.ErrAssocTimeout -} - -func (b *IWDBackend) finalizeAttempt(att *connectAttempt, code string) { - att.mu.Lock() - if att.finalized { - att.mu.Unlock() - return - } - att.finalized = true - att.mu.Unlock() - - b.stateMutex.Lock() - b.state.IsConnecting = false - b.state.ConnectingSSID = "" - b.state.LastError = code - b.stateMutex.Unlock() - - b.updateState() - - if b.onStateChange != nil { - b.onStateChange() - } -} - -func (b *IWDBackend) startAttemptWatchdog(att *connectAttempt) { - b.sigWG.Add(1) - go func() { - defer b.sigWG.Done() - - ticker := time.NewTicker(250 * time.Millisecond) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - att.mu.Lock() - finalized := att.finalized - att.mu.Unlock() - - if finalized || time.Now().After(att.deadline) { - if !finalized { - b.finalizeAttempt(att, b.classifyAttempt(att)) - } - return - } - - station := b.conn.Object(iwdBusName, b.stationPath) - stVar, err := station.GetProperty(iwdStationInterface + ".State") - if err != nil { - continue - } - state, _ := stVar.Value().(string) - - cnVar, err := station.GetProperty(iwdStationInterface + ".ConnectedNetwork") - if err != nil { - continue - } - var connPath dbus.ObjectPath - if cnVar.Value() != nil { - connPath, _ = cnVar.Value().(dbus.ObjectPath) - } - - att.mu.Lock() - if connPath == att.netPath && state == "connected" && att.connectedAt.IsZero() { - att.connectedAt = time.Now() - } - if state == "configuring" { - att.sawIPConfig = true - } - att.mu.Unlock() - - case <-b.stopChan: - return - } - } - }() -} - -func (b *IWDBackend) mapIwdDBusError(name string) string { - switch name { - case "net.connman.iwd.Error.AlreadyConnected": - return errdefs.ErrAlreadyConnected - case "net.connman.iwd.Error.AuthenticationFailed", - "net.connman.iwd.Error.InvalidKey", - "net.connman.iwd.Error.IncorrectPassphrase": - return errdefs.ErrBadCredentials - case "net.connman.iwd.Error.NotFound": - return errdefs.ErrNoSuchSSID - case "net.connman.iwd.Error.NotSupported": - return errdefs.ErrConnectionFailed - case "net.connman.iwd.Agent.Error.Canceled": - return errdefs.ErrUserCanceled - default: - return errdefs.ErrConnectionFailed - } -} - -func (b *IWDBackend) ConnectWiFi(req ConnectionRequest) error { - if b.stationPath == "" { - b.setConnectError(errdefs.ErrWifiDisabled) - if b.onStateChange != nil { - b.onStateChange() - } - return fmt.Errorf("no WiFi device available") - } - - networkPath, err := b.findNetworkPath(req.SSID) - if err != nil { - b.setConnectError(errdefs.ErrNoSuchSSID) - if b.onStateChange != nil { - b.onStateChange() - } - return fmt.Errorf("network not found: %w", err) - } - - att := &connectAttempt{ - ssid: req.SSID, - netPath: networkPath, - start: time.Now(), - deadline: time.Now().Add(15 * time.Second), - } - - b.attemptMutex.Lock() - b.curAttempt = att - b.attemptMutex.Unlock() - - b.stateMutex.Lock() - b.state.IsConnecting = true - b.state.ConnectingSSID = req.SSID - b.state.LastError = "" - b.stateMutex.Unlock() - - if b.onStateChange != nil { - b.onStateChange() - } - - netObj := b.conn.Object(iwdBusName, networkPath) - go func() { - call := netObj.Call(iwdNetworkInterface+".Connect", 0) - if call.Err != nil { - var code string - if dbusErr, ok := call.Err.(dbus.Error); ok { - code = b.mapIwdDBusError(dbusErr.Name) - } else if dbusErrPtr, ok := call.Err.(*dbus.Error); ok { - code = b.mapIwdDBusError(dbusErrPtr.Name) - } else { - code = errdefs.ErrConnectionFailed - } - - att.mu.Lock() - if att.sawPromptRetry { - code = errdefs.ErrBadCredentials - } - att.mu.Unlock() - - b.finalizeAttempt(att, code) - return - } - - b.startAttemptWatchdog(att) - }() - - return nil -} - -func (b *IWDBackend) findNetworkPath(ssid string) (dbus.ObjectPath, error) { - obj := b.conn.Object(iwdBusName, iwdObjectPath) - - var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant - err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects) - if err != nil { - return "", err - } - - for path, interfaces := range objects { - if netProps, ok := interfaces[iwdNetworkInterface]; ok { - if nameVar, ok := netProps["Name"]; ok { - if name, ok := nameVar.Value().(string); ok && name == ssid { - return path, nil - } - } - } - } - - return "", fmt.Errorf("network not found") -} - -func (b *IWDBackend) DisconnectWiFi() error { - if b.stationPath == "" { - return fmt.Errorf("no WiFi device available") - } - - obj := b.conn.Object(iwdBusName, b.stationPath) - call := obj.Call(iwdStationInterface+".Disconnect", 0) - if call.Err != nil { - return fmt.Errorf("failed to disconnect: %w", call.Err) - } - - b.updateState() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil -} - -func (b *IWDBackend) ForgetWiFiNetwork(ssid string) error { - b.stateMutex.RLock() - currentSSID := b.state.WiFiSSID - isConnected := b.state.WiFiConnected - b.stateMutex.RUnlock() - - obj := b.conn.Object(iwdBusName, iwdObjectPath) - - var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant - err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects) - if err != nil { - return err - } - - for path, interfaces := range objects { - if knownProps, ok := interfaces[iwdKnownNetworkInterface]; ok { - if nameVar, ok := knownProps["Name"]; ok { - if name, ok := nameVar.Value().(string); ok && name == ssid { - knownObj := b.conn.Object(iwdBusName, path) - call := knownObj.Call(iwdKnownNetworkInterface+".Forget", 0) - if call.Err != nil { - return fmt.Errorf("failed to forget network: %w", call.Err) - } - - if isConnected && currentSSID == ssid { - b.stateMutex.Lock() - b.state.WiFiConnected = false - b.state.WiFiSSID = "" - b.state.WiFiSignal = 0 - b.state.WiFiIP = "" - b.state.NetworkStatus = StatusDisconnected - b.stateMutex.Unlock() - } - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil - } - } - } - } - - return fmt.Errorf("network not found") -} - -func (b *IWDBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error { - obj := b.conn.Object(iwdBusName, iwdObjectPath) - - var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant - err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects) - if err != nil { - return err - } - - for path, interfaces := range objects { - if knownProps, ok := interfaces[iwdKnownNetworkInterface]; ok { - if nameVar, ok := knownProps["Name"]; ok { - if name, ok := nameVar.Value().(string); ok && name == ssid { - knownObj := b.conn.Object(iwdBusName, path) - call := knownObj.Call(dbusPropertiesInterface+".Set", 0, iwdKnownNetworkInterface, "AutoConnect", dbus.MakeVariant(autoconnect)) - if call.Err != nil { - return fmt.Errorf("failed to set autoconnect: %w", call.Err) - } - - b.updateState() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil - } - } - } - } - - return fmt.Errorf("network not found") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd.go deleted file mode 100644 index 802cbf0..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd.go +++ /dev/null @@ -1,301 +0,0 @@ -package network - -import ( - "fmt" - "net" - "strings" - "sync" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/godbus/dbus/v5" -) - -const ( - networkdBusName = "org.freedesktop.network1" - networkdManagerPath = "/org/freedesktop/network1" - networkdManagerIface = "org.freedesktop.network1.Manager" - networkdLinkIface = "org.freedesktop.network1.Link" -) - -type linkInfo struct { - ifindex int32 - name string - path dbus.ObjectPath - opState string -} - -type SystemdNetworkdBackend struct { - conn *dbus.Conn - managerPath dbus.ObjectPath - links map[string]*linkInfo - linksMutex sync.RWMutex - state *BackendState - stateMutex sync.RWMutex - onStateChange func() - stopChan chan struct{} - signals chan *dbus.Signal - sigWG sync.WaitGroup -} - -func NewSystemdNetworkdBackend() (*SystemdNetworkdBackend, error) { - return &SystemdNetworkdBackend{ - managerPath: networkdManagerPath, - links: make(map[string]*linkInfo), - state: &BackendState{ - Backend: "networkd", - WiFiNetworks: []WiFiNetwork{}, - }, - stopChan: make(chan struct{}), - }, nil -} - -func (b *SystemdNetworkdBackend) Initialize() error { - c, err := dbus.ConnectSystemBus() - if err != nil { - return fmt.Errorf("connect bus: %w", err) - } - b.conn = c - - if err := b.enumerateLinks(); err != nil { - c.Close() - return fmt.Errorf("enumerate links: %w", err) - } - - if err := b.updateState(); err != nil { - c.Close() - return fmt.Errorf("update initial state: %w", err) - } - - return nil -} - -func (b *SystemdNetworkdBackend) Close() { - close(b.stopChan) - b.StopMonitoring() - - if b.conn != nil { - b.conn.Close() - } -} - -func (b *SystemdNetworkdBackend) enumerateLinks() error { - obj := b.conn.Object(networkdBusName, b.managerPath) - - var links []struct { - Ifindex int32 - Name string - Path dbus.ObjectPath - } - err := obj.Call(networkdManagerIface+".ListLinks", 0).Store(&links) - if err != nil { - return fmt.Errorf("ListLinks: %w", err) - } - - b.linksMutex.Lock() - defer b.linksMutex.Unlock() - - for _, l := range links { - b.links[l.Name] = &linkInfo{ - ifindex: l.Ifindex, - name: l.Name, - path: l.Path, - } - log.Debugf("networkd: enumerated link %s (ifindex=%d, path=%s)", l.Name, l.Ifindex, l.Path) - } - - return nil -} - -func (b *SystemdNetworkdBackend) updateState() error { - b.linksMutex.RLock() - defer b.linksMutex.RUnlock() - - var wiredIface *linkInfo - var wifiIface *linkInfo - - for name, link := range b.links { - if b.isVirtualInterface(name) { - continue - } - - linkObj := b.conn.Object(networkdBusName, link.path) - opStateVar, err := linkObj.GetProperty(networkdLinkIface + ".OperationalState") - if err == nil { - if opState, ok := opStateVar.Value().(string); ok { - link.opState = opState - } - } - - if strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") { - if wifiIface == nil || link.opState == "routable" || link.opState == "carrier" { - wifiIface = link - } - } else if !b.isVirtualInterface(name) { - if wiredIface == nil || link.opState == "routable" || link.opState == "carrier" { - wiredIface = link - } - } - } - - var wiredConns []WiredConnection - var ethernetDevices []EthernetDevice - for name, link := range b.links { - if b.isVirtualInterface(name) || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") { - continue - } - - active := link.opState == "routable" || link.opState == "carrier" - wiredConns = append(wiredConns, WiredConnection{ - Path: link.path, - ID: name, - UUID: "wired:" + name, - Type: "ethernet", - IsActive: active, - }) - - var ip string - var hwAddr string - if iface, err := net.InterfaceByName(name); err == nil { - hwAddr = iface.HardwareAddr.String() - if addrs := b.getAddresses(name); len(addrs) > 0 { - ip = addrs[0] - } - } - - stateStr := "disconnected" - switch link.opState { - case "routable": - stateStr = "routable" - case "carrier": - stateStr = "carrier" - case "degraded": - stateStr = "degraded" - case "no-carrier": - stateStr = "no-carrier" - case "off": - stateStr = "off" - } - - ethernetDevices = append(ethernetDevices, EthernetDevice{ - Name: name, - HwAddress: hwAddr, - State: stateStr, - Connected: active, - IP: ip, - }) - } - - b.stateMutex.Lock() - defer b.stateMutex.Unlock() - - b.state.NetworkStatus = StatusDisconnected - b.state.EthernetConnected = false - b.state.EthernetIP = "" - b.state.WiFiConnected = false - b.state.WiFiIP = "" - b.state.WiredConnections = wiredConns - b.state.EthernetDevices = ethernetDevices - - if wiredIface != nil { - b.state.EthernetDevice = wiredIface.name - log.Debugf("networkd: wired interface %s opState=%s", wiredIface.name, wiredIface.opState) - if wiredIface.opState == "routable" || wiredIface.opState == "carrier" { - b.state.EthernetConnected = true - b.state.NetworkStatus = StatusEthernet - - if addrs := b.getAddresses(wiredIface.name); len(addrs) > 0 { - b.state.EthernetIP = addrs[0] - log.Debugf("networkd: ethernet IP %s on %s", addrs[0], wiredIface.name) - } - } - } - - if wifiIface != nil { - b.state.WiFiDevice = wifiIface.name - log.Debugf("networkd: wifi interface %s opState=%s", wifiIface.name, wifiIface.opState) - if wifiIface.opState == "routable" || wifiIface.opState == "carrier" { - b.state.WiFiConnected = true - - if addrs := b.getAddresses(wifiIface.name); len(addrs) > 0 { - b.state.WiFiIP = addrs[0] - log.Debugf("networkd: wifi IP %s on %s", addrs[0], wifiIface.name) - if b.state.NetworkStatus == StatusDisconnected { - b.state.NetworkStatus = StatusWiFi - } - } - } - } - - return nil -} - -func (b *SystemdNetworkdBackend) isVirtualInterface(name string) bool { - virtualPrefixes := []string{ - "lo", "docker", "veth", "virbr", "br-", "vnet", "tun", "tap", - "vboxnet", "vmnet", "kube", "cni", "flannel", "cali", - } - for _, prefix := range virtualPrefixes { - if strings.HasPrefix(name, prefix) { - return true - } - } - return false -} - -func (b *SystemdNetworkdBackend) getAddresses(ifname string) []string { - iface, err := net.InterfaceByName(ifname) - if err != nil { - return nil - } - - addrs, err := iface.Addrs() - if err != nil { - return nil - } - - var result []string - for _, addr := range addrs { - if ipnet, ok := addr.(*net.IPNet); ok { - if ipv4 := ipnet.IP.To4(); ipv4 != nil { - result = append(result, ipv4.String()) - } - } - } - return result -} - -func (b *SystemdNetworkdBackend) GetCurrentState() (*BackendState, error) { - b.stateMutex.RLock() - defer b.stateMutex.RUnlock() - s := *b.state - return &s, nil -} - -func (b *SystemdNetworkdBackend) GetPromptBroker() PromptBroker { - return nil -} - -func (b *SystemdNetworkdBackend) SetPromptBroker(broker PromptBroker) error { - return nil -} - -func (b *SystemdNetworkdBackend) SubmitCredentials(token string, secrets map[string]string, save bool) error { - return fmt.Errorf("credentials not needed by networkd backend") -} - -func (b *SystemdNetworkdBackend) CancelCredentials(token string) error { - return fmt.Errorf("credentials not needed by networkd backend") -} - -func (b *SystemdNetworkdBackend) EnsureDhcpUp(ifname string) error { - b.linksMutex.RLock() - link, exists := b.links[ifname] - b.linksMutex.RUnlock() - - if !exists { - return fmt.Errorf("interface %s not found", ifname) - } - - linkObj := b.conn.Object(networkdBusName, link.path) - return linkObj.Call(networkdLinkIface+".Reconfigure", 0).Err -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_ethernet.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_ethernet.go deleted file mode 100644 index 8f9b225..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_ethernet.go +++ /dev/null @@ -1,120 +0,0 @@ -package network - -import ( - "fmt" - "net" - "strings" -) - -func (b *SystemdNetworkdBackend) GetWiredConnections() ([]WiredConnection, error) { - b.linksMutex.RLock() - defer b.linksMutex.RUnlock() - - var conns []WiredConnection - for name, link := range b.links { - if b.isVirtualInterface(name) || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") { - continue - } - - active := link.opState == "routable" || link.opState == "carrier" - conns = append(conns, WiredConnection{ - Path: link.path, - ID: name, - UUID: "wired:" + name, - Type: "ethernet", - IsActive: active, - }) - } - - return conns, nil -} - -func (b *SystemdNetworkdBackend) GetWiredNetworkDetails(id string) (*WiredNetworkInfoResponse, error) { - ifname := strings.TrimPrefix(id, "wired:") - - b.linksMutex.RLock() - _, exists := b.links[ifname] - b.linksMutex.RUnlock() - - if !exists { - return nil, fmt.Errorf("interface %s not found", ifname) - } - - iface, err := net.InterfaceByName(ifname) - if err != nil { - return nil, fmt.Errorf("get interface: %w", err) - } - - addrs, _ := iface.Addrs() - var ipv4s, ipv6s []string - for _, addr := range addrs { - if ipnet, ok := addr.(*net.IPNet); ok { - if ipv4 := ipnet.IP.To4(); ipv4 != nil { - ipv4s = append(ipv4s, ipnet.String()) - } else if ipv6 := ipnet.IP.To16(); ipv6 != nil { - ipv6s = append(ipv6s, ipnet.String()) - } - } - } - - return &WiredNetworkInfoResponse{ - UUID: id, - IFace: ifname, - HwAddr: iface.HardwareAddr.String(), - IPv4: WiredIPConfig{ - IPs: ipv4s, - }, - IPv6: WiredIPConfig{ - IPs: ipv6s, - }, - }, nil -} - -func (b *SystemdNetworkdBackend) ConnectEthernet() error { - b.linksMutex.RLock() - var primaryWired *linkInfo - for name, l := range b.links { - if strings.HasPrefix(name, "lo") || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") { - continue - } - primaryWired = l - break - } - b.linksMutex.RUnlock() - - if primaryWired == nil { - return fmt.Errorf("no wired interface found") - } - - linkObj := b.conn.Object(networkdBusName, primaryWired.path) - return linkObj.Call(networkdLinkIface+".Reconfigure", 0).Err -} - -func (b *SystemdNetworkdBackend) DisconnectEthernet() error { - return fmt.Errorf("not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) ActivateWiredConnection(id string) error { - ifname := strings.TrimPrefix(id, "wired:") - - b.linksMutex.RLock() - link, exists := b.links[ifname] - b.linksMutex.RUnlock() - - if !exists { - return fmt.Errorf("interface %s not found", ifname) - } - - linkObj := b.conn.Object(networkdBusName, link.path) - return linkObj.Call(networkdLinkIface+".Reconfigure", 0).Err -} - -func (b *SystemdNetworkdBackend) GetEthernetDevices() []EthernetDevice { - b.stateMutex.RLock() - defer b.stateMutex.RUnlock() - return append([]EthernetDevice(nil), b.state.EthernetDevices...) -} - -func (b *SystemdNetworkdBackend) DisconnectEthernetDevice(device string) error { - return fmt.Errorf("not supported by networkd backend") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_signals.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_signals.go deleted file mode 100644 index dc642ed..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_signals.go +++ /dev/null @@ -1,68 +0,0 @@ -package network - -import ( - "fmt" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/godbus/dbus/v5" -) - -func (b *SystemdNetworkdBackend) StartMonitoring(onStateChange func()) error { - b.onStateChange = onStateChange - - b.signals = make(chan *dbus.Signal, 64) - b.conn.Signal(b.signals) - - matchRules := []string{ - "type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path_namespace='/org/freedesktop/network1'", - "type='signal',interface='org.freedesktop.network1.Manager'", - } - - for _, rule := range matchRules { - if err := b.conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, rule).Err; err != nil { - return fmt.Errorf("add match %q: %w", rule, err) - } - } - - b.sigWG.Add(1) - go b.signalLoop() - - return nil -} - -func (b *SystemdNetworkdBackend) StopMonitoring() { - b.sigWG.Wait() -} - -func (b *SystemdNetworkdBackend) signalLoop() { - defer b.sigWG.Done() - - for { - select { - case <-b.stopChan: - return - case sig := <-b.signals: - if sig == nil { - continue - } - - if sig.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" { - if len(sig.Body) < 2 { - continue - } - iface, ok := sig.Body[0].(string) - if !ok || iface != networkdLinkIface { - continue - } - - b.enumerateLinks() - if err := b.updateState(); err != nil { - log.Warnf("networkd state update failed: %v", err) - } - if b.onStateChange != nil { - b.onStateChange() - } - } - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_test.go deleted file mode 100644 index 18c5281..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package network - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSystemdNetworkdBackend_New(t *testing.T) { - backend, err := NewSystemdNetworkdBackend() - assert.NoError(t, err) - assert.NotNil(t, backend) - assert.Equal(t, "networkd", backend.state.Backend) - assert.NotNil(t, backend.links) - assert.NotNil(t, backend.stopChan) -} - -func TestSystemdNetworkdBackend_GetCurrentState(t *testing.T) { - backend, _ := NewSystemdNetworkdBackend() - backend.state.NetworkStatus = StatusEthernet - backend.state.EthernetConnected = true - backend.state.EthernetIP = "192.168.1.100" - - state, err := backend.GetCurrentState() - assert.NoError(t, err) - assert.NotNil(t, state) - assert.Equal(t, StatusEthernet, state.NetworkStatus) - assert.True(t, state.EthernetConnected) - assert.Equal(t, "192.168.1.100", state.EthernetIP) -} - -func TestSystemdNetworkdBackend_WiFiNotSupported(t *testing.T) { - backend, _ := NewSystemdNetworkdBackend() - - err := backend.ScanWiFi() - assert.Error(t, err) - assert.Contains(t, err.Error(), "not supported") - - req := ConnectionRequest{SSID: "test"} - err = backend.ConnectWiFi(req) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not supported") - - err = backend.DisconnectWiFi() - assert.Error(t, err) - assert.Contains(t, err.Error(), "not supported") - - err = backend.ForgetWiFiNetwork("test") - assert.Error(t, err) - assert.Contains(t, err.Error(), "not supported") - - _, err = backend.GetWiFiNetworkDetails("test") - assert.Error(t, err) - assert.Contains(t, err.Error(), "not supported") -} - -func TestSystemdNetworkdBackend_VPNNotSupported(t *testing.T) { - backend, _ := NewSystemdNetworkdBackend() - - profiles, err := backend.ListVPNProfiles() - assert.NoError(t, err) - assert.Empty(t, profiles) - - active, err := backend.ListActiveVPN() - assert.NoError(t, err) - assert.Empty(t, active) - - err = backend.ConnectVPN("test", false) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not supported") - - err = backend.DisconnectVPN("test") - assert.Error(t, err) - assert.Contains(t, err.Error(), "not supported") - - err = backend.DisconnectAllVPN() - assert.Error(t, err) - assert.Contains(t, err.Error(), "not supported") - - err = backend.ClearVPNCredentials("test") - assert.Error(t, err) - assert.Contains(t, err.Error(), "not supported") -} - -func TestSystemdNetworkdBackend_PromptBroker(t *testing.T) { - backend, _ := NewSystemdNetworkdBackend() - - broker := backend.GetPromptBroker() - assert.Nil(t, broker) - - err := backend.SetPromptBroker(nil) - assert.NoError(t, err) - - err = backend.SubmitCredentials("token", nil, false) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not needed") - - err = backend.CancelCredentials("token") - assert.Error(t, err) - assert.Contains(t, err.Error(), "not needed") -} - -func TestSystemdNetworkdBackend_GetWiFiEnabled(t *testing.T) { - backend, _ := NewSystemdNetworkdBackend() - - enabled, err := backend.GetWiFiEnabled() - assert.NoError(t, err) - assert.True(t, enabled) -} - -func TestSystemdNetworkdBackend_SetWiFiEnabled(t *testing.T) { - backend, _ := NewSystemdNetworkdBackend() - - err := backend.SetWiFiEnabled(false) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not supported") -} - -func TestSystemdNetworkdBackend_DisconnectEthernet(t *testing.T) { - backend, _ := NewSystemdNetworkdBackend() - - err := backend.DisconnectEthernet() - assert.Error(t, err) - assert.Contains(t, err.Error(), "not supported") -} - -func TestSystemdNetworkdBackend_GetEthernetDevices(t *testing.T) { - backend, _ := NewSystemdNetworkdBackend() - - backend.state.EthernetDevices = []EthernetDevice{ - {Name: "enp0s3", State: "routable", Connected: true}, - {Name: "enp0s8", State: "no-carrier", Connected: false}, - } - - devices := backend.GetEthernetDevices() - assert.Len(t, devices, 2) - assert.Equal(t, "enp0s3", devices[0].Name) - assert.True(t, devices[0].Connected) -} - -func TestSystemdNetworkdBackend_DisconnectEthernetDevice(t *testing.T) { - backend, _ := NewSystemdNetworkdBackend() - - err := backend.DisconnectEthernetDevice("enp0s3") - assert.Error(t, err) - assert.Contains(t, err.Error(), "not supported") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_unimplemented.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_unimplemented.go deleted file mode 100644 index 695c3c5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkd_unimplemented.go +++ /dev/null @@ -1,99 +0,0 @@ -package network - -import "fmt" - -func (b *SystemdNetworkdBackend) GetWiFiEnabled() (bool, error) { - return true, nil -} - -func (b *SystemdNetworkdBackend) SetWiFiEnabled(enabled bool) error { - return fmt.Errorf("WiFi control not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) ScanWiFi() error { - return fmt.Errorf("WiFi scan not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) { - return nil, fmt.Errorf("WiFi details not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) GetWiFiQRCodeContent(ssid string) (string, error) { - return "", fmt.Errorf("WiFi QR Code not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error { - return fmt.Errorf("WiFi connect not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) DisconnectWiFi() error { - return fmt.Errorf("WiFi disconnect not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) ForgetWiFiNetwork(ssid string) error { - return fmt.Errorf("WiFi forget not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) ListVPNProfiles() ([]VPNProfile, error) { - return []VPNProfile{}, nil -} - -func (b *SystemdNetworkdBackend) ListActiveVPN() ([]VPNActive, error) { - return []VPNActive{}, nil -} - -func (b *SystemdNetworkdBackend) ConnectVPN(uuidOrName string, singleActive bool) error { - return fmt.Errorf("VPN not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) DisconnectVPN(uuidOrName string) error { - return fmt.Errorf("VPN not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) DisconnectAllVPN() error { - return fmt.Errorf("VPN not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) ClearVPNCredentials(uuidOrName string) error { - return fmt.Errorf("VPN not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) ListVPNPlugins() ([]VPNPlugin, error) { - return []VPNPlugin{}, nil -} - -func (b *SystemdNetworkdBackend) ImportVPN(filePath string, name string) (*VPNImportResult, error) { - return nil, fmt.Errorf("VPN not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) GetVPNConfig(uuidOrName string) (*VPNConfig, error) { - return nil, fmt.Errorf("VPN not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) UpdateVPNConfig(uuid string, updates map[string]any) error { - return fmt.Errorf("VPN not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) DeleteVPN(uuidOrName string) error { - return fmt.Errorf("VPN not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) SetVPNCredentials(uuid, username, password string, save bool) error { - return fmt.Errorf("VPN not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error { - return fmt.Errorf("WiFi autoconnect not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) ScanWiFiDevice(device string) error { - return fmt.Errorf("WiFi scan not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) DisconnectWiFiDevice(device string) error { - return fmt.Errorf("WiFi disconnect not supported by networkd backend") -} - -func (b *SystemdNetworkdBackend) GetWiFiDevices() []WiFiDevice { - return nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager.go deleted file mode 100644 index 2233975..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager.go +++ /dev/null @@ -1,405 +0,0 @@ -package network - -import ( - "fmt" - "sync" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/Wifx/gonetworkmanager/v2" - "github.com/godbus/dbus/v5" -) - -const ( - dbusNMPath = "/org/freedesktop/NetworkManager" - dbusNMInterface = "org.freedesktop.NetworkManager" - dbusNMDeviceInterface = "org.freedesktop.NetworkManager.Device" - dbusNMWiredInterface = "org.freedesktop.NetworkManager.Device.Wired" - dbusNMWirelessInterface = "org.freedesktop.NetworkManager.Device.Wireless" - dbusNMAccessPointInterface = "org.freedesktop.NetworkManager.AccessPoint" - dbusPropsInterface = "org.freedesktop.DBus.Properties" - - NmDeviceStateReasonWrongPassword = 8 - NmDeviceStateReasonSupplicantTimeout = 24 - NmDeviceStateReasonSupplicantFailed = 25 - NmDeviceStateReasonSecretsRequired = 7 - NmDeviceStateReasonNoSecrets = 6 - NmDeviceStateReasonNoSsid = 10 - NmDeviceStateReasonDhcpClientFailed = 14 - NmDeviceStateReasonIpConfigUnavailable = 18 - NmDeviceStateReasonSupplicantDisconnect = 23 - NmDeviceStateReasonCarrier = 40 - NmDeviceStateReasonNewActivation = 60 -) - -type wifiDeviceInfo struct { - device gonetworkmanager.Device - wireless gonetworkmanager.DeviceWireless - name string - hwAddress string -} - -type ethernetDeviceInfo struct { - device gonetworkmanager.Device - wired gonetworkmanager.DeviceWired - name string - hwAddress string -} - -type NetworkManagerBackend struct { - nmConn any - ethernetDevice any - ethernetDevices map[string]*ethernetDeviceInfo - wifiDevice any - settings any - wifiDev any - wifiDevices map[string]*wifiDeviceInfo - - dbusConn *dbus.Conn - signals chan *dbus.Signal - sigWG sync.WaitGroup - stopChan chan struct{} - - secretAgent *SecretAgent - promptBroker PromptBroker - - state *BackendState - stateMutex sync.RWMutex - - lastFailedSSID string - lastFailedTime int64 - failedMutex sync.RWMutex - - pendingVPNSave *pendingVPNCredentials - pendingVPNSaveMu sync.Mutex - cachedVPNCreds *cachedVPNCredentials - cachedVPNCredsMu sync.Mutex - cachedPKCS11PIN *cachedPKCS11PIN - cachedPKCS11Mu sync.Mutex - cachedGPSamlCookie *cachedGPSamlCookie - cachedGPSamlMu sync.Mutex - - onStateChange func() -} - -type pendingVPNCredentials struct { - ConnectionPath string - Username string - Password string - SavePassword bool -} - -type cachedVPNCredentials struct { - ConnectionUUID string - Password string - SavePassword bool -} - -type cachedPKCS11PIN struct { - ConnectionUUID string - PIN string -} - -type cachedGPSamlCookie struct { - ConnectionUUID string - Cookie string - Host string - User string - Fingerprint string -} - -func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*NetworkManagerBackend, error) { - var nm gonetworkmanager.NetworkManager - var err error - - if len(nmConn) > 0 && nmConn[0] != nil { - // Use injected connection (for testing) - nm = nmConn[0] - } else { - // Create real connection - nm, err = gonetworkmanager.NewNetworkManager() - if err != nil { - return nil, fmt.Errorf("failed to connect to NetworkManager: %w", err) - } - } - - backend := &NetworkManagerBackend{ - nmConn: nm, - stopChan: make(chan struct{}), - ethernetDevices: make(map[string]*ethernetDeviceInfo), - wifiDevices: make(map[string]*wifiDeviceInfo), - state: &BackendState{ - Backend: "networkmanager", - }, - } - - return backend, nil -} - -func (b *NetworkManagerBackend) Initialize() error { - nm := b.nmConn.(gonetworkmanager.NetworkManager) - - if s, err := gonetworkmanager.NewSettings(); err == nil { - b.settings = s - } - - devices, err := nm.GetDevices() - if err != nil { - return fmt.Errorf("failed to get devices: %w", err) - } - - for _, dev := range devices { - devType, err := dev.GetPropertyDeviceType() - if err != nil { - continue - } - - switch devType { - case gonetworkmanager.NmDeviceTypeEthernet: - if managed, _ := dev.GetPropertyManaged(); !managed { - continue - } - iface, err := dev.GetPropertyInterface() - if err != nil { - continue - } - w, err := gonetworkmanager.NewDeviceWired(dev.GetPath()) - if err != nil { - continue - } - hwAddr, _ := w.GetPropertyHwAddress() - - b.ethernetDevices[iface] = ðernetDeviceInfo{ - device: dev, - wired: w, - name: iface, - hwAddress: hwAddr, - } - - if b.ethernetDevice == nil { - b.ethernetDevice = dev - } - if err := b.updateEthernetState(); err != nil { - continue - } - _, err = b.listEthernetConnections() - if err != nil { - return fmt.Errorf("failed to get wired configurations: %w", err) - } - - case gonetworkmanager.NmDeviceTypeWifi: - iface, err := dev.GetPropertyInterface() - if err != nil { - continue - } - w, err := gonetworkmanager.NewDeviceWireless(dev.GetPath()) - if err != nil { - continue - } - hwAddr, _ := w.GetPropertyHwAddress() - - b.wifiDevices[iface] = &wifiDeviceInfo{ - device: dev, - wireless: w, - name: iface, - hwAddress: hwAddr, - } - - if b.wifiDevice == nil { - b.wifiDevice = dev - b.wifiDev = w - } - } - } - - wifiEnabled, err := nm.GetPropertyWirelessEnabled() - if err == nil { - b.stateMutex.Lock() - b.state.WiFiEnabled = wifiEnabled - b.stateMutex.Unlock() - } - - if err := b.updateWiFiState(); err != nil { - log.Warnf("Failed to update WiFi state: %v", err) - } - - if wifiEnabled { - if _, err := b.updateWiFiNetworks(); err != nil { - log.Warnf("Failed to get initial networks: %v", err) - } - b.updateAllWiFiDevices() - } - - b.updateAllEthernetDevices() - - if err := b.updatePrimaryConnection(); err != nil { - return err - } - - if _, err := b.ListVPNProfiles(); err != nil { - log.Warnf("Failed to get initial VPN profiles: %v", err) - } - - if _, err := b.ListActiveVPN(); err != nil { - log.Warnf("Failed to get initial active VPNs: %v", err) - } - - return nil -} - -func (b *NetworkManagerBackend) Close() { - close(b.stopChan) - b.StopMonitoring() - - if b.secretAgent != nil { - b.secretAgent.Close() - } -} - -func (b *NetworkManagerBackend) GetCurrentState() (*BackendState, error) { - b.stateMutex.RLock() - defer b.stateMutex.RUnlock() - - state := *b.state - state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...) - state.WiFiDevices = append([]WiFiDevice(nil), b.state.WiFiDevices...) - state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...) - state.EthernetDevices = append([]EthernetDevice(nil), b.state.EthernetDevices...) - state.VPNProfiles = append([]VPNProfile(nil), b.state.VPNProfiles...) - state.VPNActive = append([]VPNActive(nil), b.state.VPNActive...) - - return &state, nil -} - -func (b *NetworkManagerBackend) StartMonitoring(onStateChange func()) error { - b.onStateChange = onStateChange - - if err := b.startSecretAgent(); err != nil { - return fmt.Errorf("failed to start secret agent: %w", err) - } - - if err := b.startSignalPump(); err != nil { - return err - } - - return nil -} - -func (b *NetworkManagerBackend) StopMonitoring() { - b.stopSignalPump() -} - -func (b *NetworkManagerBackend) GetPromptBroker() PromptBroker { - return b.promptBroker -} - -func (b *NetworkManagerBackend) SetPromptBroker(broker PromptBroker) error { - if broker == nil { - return fmt.Errorf("broker cannot be nil") - } - - hadAgent := b.secretAgent != nil - - b.promptBroker = broker - - if b.secretAgent != nil { - b.secretAgent.Close() - b.secretAgent = nil - } - - if hadAgent { - return b.startSecretAgent() - } - - return nil -} - -func (b *NetworkManagerBackend) SubmitCredentials(token string, secrets map[string]string, save bool) error { - if b.promptBroker == nil { - return fmt.Errorf("prompt broker not initialized") - } - - return b.promptBroker.Resolve(token, PromptReply{ - Secrets: secrets, - Save: save, - Cancel: false, - }) -} - -func (b *NetworkManagerBackend) CancelCredentials(token string) error { - if b.promptBroker == nil { - return fmt.Errorf("prompt broker not initialized") - } - - return b.promptBroker.Resolve(token, PromptReply{ - Cancel: true, - }) -} - -func (b *NetworkManagerBackend) ensureWiFiDevice() error { - if b.wifiDev != nil { - return nil - } - - if b.wifiDevice == nil { - return fmt.Errorf("no WiFi device available") - } - - dev := b.wifiDevice.(gonetworkmanager.Device) - wifiDev, err := gonetworkmanager.NewDeviceWireless(dev.GetPath()) - if err != nil { - return fmt.Errorf("failed to get wireless device: %w", err) - } - b.wifiDev = wifiDev - return nil -} - -func (b *NetworkManagerBackend) startSecretAgent() error { - if b.promptBroker == nil { - return fmt.Errorf("prompt broker not set") - } - - agent, err := NewSecretAgent(b.promptBroker, nil, b) - if err != nil { - return err - } - - b.secretAgent = agent - return nil -} - -func (b *NetworkManagerBackend) getActiveConnections() (map[string]bool, error) { - nm := b.nmConn.(gonetworkmanager.NetworkManager) - - activeUUIDs := make(map[string]bool) - - activeConns, err := nm.GetPropertyActiveConnections() - if err != nil { - return activeUUIDs, fmt.Errorf("failed to get active connections: %w", err) - } - - for _, activeConn := range activeConns { - connType, err := activeConn.GetPropertyType() - if err != nil { - continue - } - - if connType != "802-3-ethernet" { - continue - } - - state, err := activeConn.GetPropertyState() - if err != nil { - continue - } - if state < 1 || state > 2 { - continue - } - - uuid, err := activeConn.GetPropertyUUID() - if err != nil { - continue - } - activeUUIDs[uuid] = true - } - return activeUUIDs, nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_ethernet.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_ethernet.go deleted file mode 100644 index 47f785b..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_ethernet.go +++ /dev/null @@ -1,402 +0,0 @@ -package network - -import ( - "fmt" - "net" - "strconv" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/Wifx/gonetworkmanager/v2" -) - -func (b *NetworkManagerBackend) GetWiredConnections() ([]WiredConnection, error) { - return b.listEthernetConnections() -} - -func (b *NetworkManagerBackend) GetWiredNetworkDetails(uuid string) (*WiredNetworkInfoResponse, error) { - if b.ethernetDevice == nil { - return nil, fmt.Errorf("no ethernet device available") - } - - dev := b.ethernetDevice.(gonetworkmanager.Device) - - iface, _ := dev.GetPropertyInterface() - driver, _ := dev.GetPropertyDriver() - - hwAddr := "Not available" - var speed uint32 = 0 - wiredDevice, err := gonetworkmanager.NewDeviceWired(dev.GetPath()) - if err == nil { - hwAddr, _ = wiredDevice.GetPropertyHwAddress() - speed, _ = wiredDevice.GetPropertySpeed() - } - var ipv4Config WiredIPConfig - var ipv6Config WiredIPConfig - - activeConn, err := dev.GetPropertyActiveConnection() - if err == nil && activeConn != nil { - ip4Config, err := activeConn.GetPropertyIP4Config() - if err == nil && ip4Config != nil { - var ips []string - addresses, err := ip4Config.GetPropertyAddressData() - if err == nil && len(addresses) > 0 { - for _, addr := range addresses { - ips = append(ips, fmt.Sprintf("%s/%s", addr.Address, strconv.Itoa(int(addr.Prefix)))) - } - } - - gateway, _ := ip4Config.GetPropertyGateway() - dnsAddrs := "" - dns, err := ip4Config.GetPropertyNameserverData() - if err == nil && len(dns) > 0 { - for _, d := range dns { - if len(dnsAddrs) > 0 { - dnsAddrs = strings.Join([]string{dnsAddrs, d.Address}, "; ") - } else { - dnsAddrs = d.Address - } - } - } - - ipv4Config = WiredIPConfig{ - IPs: ips, - Gateway: gateway, - DNS: dnsAddrs, - } - } - - ip6Config, err := activeConn.GetPropertyIP6Config() - if err == nil && ip6Config != nil { - var ips []string - addresses, err := ip6Config.GetPropertyAddressData() - if err == nil && len(addresses) > 0 { - for _, addr := range addresses { - ips = append(ips, fmt.Sprintf("%s/%s", addr.Address, strconv.Itoa(int(addr.Prefix)))) - } - } - - gateway, _ := ip6Config.GetPropertyGateway() - dnsAddrs := "" - dns, err := ip6Config.GetPropertyNameservers() - if err == nil && len(dns) > 0 { - for _, d := range dns { - if len(d) == 16 { - ip := net.IP(d) - if len(dnsAddrs) > 0 { - dnsAddrs = strings.Join([]string{dnsAddrs, ip.String()}, "; ") - } else { - dnsAddrs = ip.String() - } - } - } - } - - ipv6Config = WiredIPConfig{ - IPs: ips, - Gateway: gateway, - DNS: dnsAddrs, - } - } - } - - return &WiredNetworkInfoResponse{ - UUID: uuid, - IFace: iface, - Driver: driver, - HwAddr: hwAddr, - Speed: strconv.Itoa(int(speed)), - IPv4: ipv4Config, - IPv6: ipv6Config, - }, nil -} - -func (b *NetworkManagerBackend) ConnectEthernet() error { - if b.ethernetDevice == nil { - return fmt.Errorf("no ethernet device available") - } - - nm := b.nmConn.(gonetworkmanager.NetworkManager) - dev := b.ethernetDevice.(gonetworkmanager.Device) - - settingsMgr, err := gonetworkmanager.NewSettings() - if err != nil { - return fmt.Errorf("failed to get settings: %w", err) - } - - connections, err := settingsMgr.ListConnections() - if err != nil { - return fmt.Errorf("failed to get connections: %w", err) - } - - for _, conn := range connections { - connSettings, err := conn.GetSettings() - if err != nil { - continue - } - - if connMeta, ok := connSettings["connection"]; ok { - if connType, ok := connMeta["type"].(string); ok && connType == "802-3-ethernet" { - _, err := nm.ActivateConnection(conn, dev, nil) - if err != nil { - return fmt.Errorf("failed to activate ethernet: %w", err) - } - - b.updateEthernetState() - b.listEthernetConnections() - b.updatePrimaryConnection() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil - } - } - } - - settings := make(map[string]map[string]any) - settings["connection"] = map[string]any{ - "id": "Wired connection", - "type": "802-3-ethernet", - } - - _, err = nm.AddAndActivateConnection(settings, dev) - if err != nil { - return fmt.Errorf("failed to create and activate ethernet: %w", err) - } - - b.updateEthernetState() - b.listEthernetConnections() - b.updatePrimaryConnection() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil -} - -func (b *NetworkManagerBackend) DisconnectEthernet() error { - if b.ethernetDevice == nil { - return fmt.Errorf("no ethernet device available") - } - - dev := b.ethernetDevice.(gonetworkmanager.Device) - - err := dev.Disconnect() - if err != nil { - return fmt.Errorf("failed to disconnect: %w", err) - } - - b.updateEthernetState() - b.listEthernetConnections() - b.updatePrimaryConnection() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil -} - -func (b *NetworkManagerBackend) ActivateWiredConnection(uuid string) error { - if b.ethernetDevice == nil { - return fmt.Errorf("no ethernet device available") - } - - nm := b.nmConn.(gonetworkmanager.NetworkManager) - dev := b.ethernetDevice.(gonetworkmanager.Device) - - settingsMgr, err := gonetworkmanager.NewSettings() - if err != nil { - return fmt.Errorf("failed to get settings: %w", err) - } - - connections, err := settingsMgr.ListConnections() - if err != nil { - return fmt.Errorf("failed to get connections: %w", err) - } - - var targetConnection gonetworkmanager.Connection - for _, conn := range connections { - settings, err := conn.GetSettings() - if err != nil { - continue - } - - if connectionSettings, ok := settings["connection"]; ok { - if connUUID, ok := connectionSettings["uuid"].(string); ok && connUUID == uuid { - targetConnection = conn - break - } - } - } - - if targetConnection == nil { - return fmt.Errorf("connection with UUID %s not found", uuid) - } - - _, err = nm.ActivateConnection(targetConnection, dev, nil) - if err != nil { - return fmt.Errorf("error activation connection: %w", err) - } - - b.updateEthernetState() - b.listEthernetConnections() - b.updatePrimaryConnection() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil -} - -func (b *NetworkManagerBackend) listEthernetConnections() ([]WiredConnection, error) { - if b.ethernetDevice == nil { - return nil, fmt.Errorf("no ethernet device available") - } - - s := b.settings - if s == nil { - s, err := gonetworkmanager.NewSettings() - if err != nil { - return nil, fmt.Errorf("failed to get settings: %w", err) - } - b.settings = s - } - - settingsMgr := s.(gonetworkmanager.Settings) - connections, err := settingsMgr.ListConnections() - if err != nil { - return nil, fmt.Errorf("failed to get connections: %w", err) - } - - wiredConfigs := make([]WiredConnection, 0) - activeUUIDs, err := b.getActiveConnections() - - if err != nil { - return nil, fmt.Errorf("failed to get active wired connections: %w", err) - } - - currentUuid := "" - for _, connection := range connections { - path := connection.GetPath() - settings, err := connection.GetSettings() - if err != nil { - log.Errorf("unable to get settings for %s: %v", path, err) - continue - } - - connectionSettings := settings["connection"] - connType, _ := connectionSettings["type"].(string) - connID, _ := connectionSettings["id"].(string) - connUUID, _ := connectionSettings["uuid"].(string) - - if connType == "802-3-ethernet" { - wiredConfigs = append(wiredConfigs, WiredConnection{ - Path: path, - ID: connID, - UUID: connUUID, - Type: connType, - IsActive: activeUUIDs[connUUID], - }) - if activeUUIDs[connUUID] { - currentUuid = connUUID - } - } - } - - b.stateMutex.Lock() - b.state.EthernetConnectionUuid = currentUuid - b.state.WiredConnections = wiredConfigs - b.stateMutex.Unlock() - - return wiredConfigs, nil -} - -func (b *NetworkManagerBackend) GetEthernetDevices() []EthernetDevice { - b.stateMutex.RLock() - defer b.stateMutex.RUnlock() - return append([]EthernetDevice(nil), b.state.EthernetDevices...) -} - -func (b *NetworkManagerBackend) DisconnectEthernetDevice(device string) error { - info, ok := b.ethernetDevices[device] - if !ok { - return fmt.Errorf("ethernet device %s not found", device) - } - - if err := info.device.Disconnect(); err != nil { - return fmt.Errorf("failed to disconnect %s: %w", device, err) - } - - b.updateAllEthernetDevices() - b.updateEthernetState() - b.listEthernetConnections() - b.updatePrimaryConnection() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil -} - -func (b *NetworkManagerBackend) updateAllEthernetDevices() { - devices := make([]EthernetDevice, 0, len(b.ethernetDevices)) - - for name, info := range b.ethernetDevices { - state, _ := info.device.GetPropertyState() - connected := state == gonetworkmanager.NmDeviceStateActivated - driver, _ := info.device.GetPropertyDriver() - - var ip string - var speed uint32 = 0 - if connected { - ip = b.getDeviceIP(info.device) - } - if info.wired != nil { - speed, _ = info.wired.GetPropertySpeed() - } - - stateStr := "disconnected" - switch state { - case gonetworkmanager.NmDeviceStateActivated: - stateStr = "activated" - case gonetworkmanager.NmDeviceStatePrepare: - stateStr = "preparing" - case gonetworkmanager.NmDeviceStateConfig: - stateStr = "configuring" - case gonetworkmanager.NmDeviceStateIpConfig: - stateStr = "ip-config" - case gonetworkmanager.NmDeviceStateIpCheck: - stateStr = "ip-check" - case gonetworkmanager.NmDeviceStateSecondaries: - stateStr = "secondaries" - case gonetworkmanager.NmDeviceStateDeactivating: - stateStr = "deactivating" - case gonetworkmanager.NmDeviceStateFailed: - stateStr = "failed" - case gonetworkmanager.NmDeviceStateUnavailable: - stateStr = "unavailable" - case gonetworkmanager.NmDeviceStateUnmanaged: - stateStr = "unmanaged" - } - - devices = append(devices, EthernetDevice{ - Name: name, - HwAddress: info.hwAddress, - State: stateStr, - Connected: connected, - IP: ip, - Speed: speed, - Driver: driver, - }) - } - - b.stateMutex.Lock() - b.state.EthernetDevices = devices - b.stateMutex.Unlock() -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_ethernet_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_ethernet_test.go deleted file mode 100644 index 0901913..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_ethernet_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package network - -import ( - "testing" - - mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2" - "github.com/stretchr/testify/assert" -) - -func TestNetworkManagerBackend_GetWiredConnections_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.ethernetDevice = nil - _, err = backend.GetWiredConnections() - assert.Error(t, err) - assert.Contains(t, err.Error(), "no ethernet device available") -} - -func TestNetworkManagerBackend_GetWiredNetworkDetails_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.ethernetDevice = nil - _, err = backend.GetWiredNetworkDetails("test-uuid") - assert.Error(t, err) - assert.Contains(t, err.Error(), "no ethernet device available") -} - -func TestNetworkManagerBackend_ConnectEthernet_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.ethernetDevice = nil - err = backend.ConnectEthernet() - assert.Error(t, err) - assert.Contains(t, err.Error(), "no ethernet device available") -} - -func TestNetworkManagerBackend_DisconnectEthernet_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.ethernetDevice = nil - err = backend.DisconnectEthernet() - assert.Error(t, err) - assert.Contains(t, err.Error(), "no ethernet device available") -} - -func TestNetworkManagerBackend_ActivateWiredConnection_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.ethernetDevice = nil - err = backend.ActivateWiredConnection("test-uuid") - assert.Error(t, err) - assert.Contains(t, err.Error(), "no ethernet device available") -} - -func TestNetworkManagerBackend_ActivateWiredConnection_NotFound(t *testing.T) { - t.Skip("ActivateWiredConnection creates a new Settings instance internally, cannot be fully mocked") -} - -func TestNetworkManagerBackend_ListEthernetConnections_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.ethernetDevice = nil - _, err = backend.listEthernetConnections() - assert.Error(t, err) - assert.Contains(t, err.Error(), "no ethernet device available") -} - -func TestNetworkManagerBackend_GetEthernetDevices_Empty(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - devices := backend.GetEthernetDevices() - assert.Empty(t, devices) -} - -func TestNetworkManagerBackend_GetEthernetDevices_WithState(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.state.EthernetDevices = []EthernetDevice{ - {Name: "enp0s3", HwAddress: "00:11:22:33:44:55", State: "activated", Connected: true, IP: "192.168.1.100"}, - {Name: "enp0s8", HwAddress: "00:11:22:33:44:66", State: "disconnected", Connected: false}, - } - - devices := backend.GetEthernetDevices() - assert.Len(t, devices, 2) - assert.Equal(t, "enp0s3", devices[0].Name) - assert.True(t, devices[0].Connected) - assert.Equal(t, "enp0s8", devices[1].Name) - assert.False(t, devices[1].Connected) -} - -func TestNetworkManagerBackend_DisconnectEthernetDevice_NotFound(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - err = backend.DisconnectEthernetDevice("nonexistent") - assert.Error(t, err) - assert.Contains(t, err.Error(), "not found") -} - -func TestNetworkManagerBackend_UpdateAllEthernetDevices_Empty(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.updateAllEthernetDevices() - assert.Empty(t, backend.state.EthernetDevices) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_gp_saml.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_gp_saml.go deleted file mode 100644 index b1fa7fb..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_gp_saml.go +++ /dev/null @@ -1,203 +0,0 @@ -package network - -import ( - "bufio" - "context" - "fmt" - "os/exec" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" -) - -type gpSamlAuthResult struct { - Cookie string - Host string - User string - Fingerprint string -} - -// runGlobalProtectSAMLAuth handles GlobalProtect SAML/SSO authentication using gp-saml-gui. -// Only supports protocol=gp. Other protocols need their own implementations. -func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, gateway, protocol string) (*gpSamlAuthResult, error) { - if gateway == "" { - return nil, fmt.Errorf("GP SAML auth: gateway is empty") - } - if protocol != "gp" { - return nil, fmt.Errorf("only GlobalProtect (protocol=gp) SAML is supported, got: %s", protocol) - } - - log.Infof("[GP-SAML] Starting GlobalProtect SAML authentication with gp-saml-gui for gateway=%s", gateway) - - gpSamlPath, err := exec.LookPath("gp-saml-gui") - if err != nil { - return nil, fmt.Errorf("GlobalProtect SAML requires gp-saml-gui (install: pip install gp-saml-gui): %w", err) - } - - args := []string{ - "--gateway", - "--allow-insecure-crypto", - gateway, - } - - cmd := exec.CommandContext(ctx, gpSamlPath, args...) - - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, fmt.Errorf("GP SAML auth: failed to create stdout pipe: %w", err) - } - - stderr, err := cmd.StderrPipe() - if err != nil { - return nil, fmt.Errorf("GP SAML auth: failed to create stderr pipe: %w", err) - } - - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("GP SAML auth: failed to start gp-saml-gui: %w", err) - } - - go func() { - scanner := bufio.NewScanner(stderr) - for scanner.Scan() { - log.Debugf("[GP-SAML] gp-saml-gui: %s", scanner.Text()) - } - }() - - result := &gpSamlAuthResult{Host: gateway} - var allOutput []string - - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - line := scanner.Text() - allOutput = append(allOutput, line) - log.Infof("[GP-SAML] stdout: %s", line) - - switch { - case strings.HasPrefix(line, "COOKIE="): - result.Cookie = unshellQuote(strings.TrimPrefix(line, "COOKIE=")) - case strings.HasPrefix(line, "HOST="): - result.Host = unshellQuote(strings.TrimPrefix(line, "HOST=")) - case strings.HasPrefix(line, "USER="): - result.User = unshellQuote(strings.TrimPrefix(line, "USER=")) - case strings.HasPrefix(line, "FINGERPRINT="): - result.Fingerprint = unshellQuote(strings.TrimPrefix(line, "FINGERPRINT=")) - default: - parseGPSamlFromCommandLine(line, result) - } - } - - if err := cmd.Wait(); err != nil { - if ctx.Err() != nil { - return nil, fmt.Errorf("GP SAML auth timed out or was cancelled: %w", ctx.Err()) - } - if result.Cookie == "" { - return nil, fmt.Errorf("GP SAML auth failed: %w (output: %s)", err, strings.Join(allOutput, "\n")) - } - log.Warnf("[GP-SAML] gp-saml-gui exited with error but cookie was captured: %v", err) - } - - if result.Cookie == "" { - return nil, fmt.Errorf("GP SAML auth: no cookie in gp-saml-gui output") - } - - log.Infof("[GP-SAML] Got prelogin-cookie from gp-saml-gui, converting to openconnect cookie via --authenticate") - - // Convert prelogin-cookie to full openconnect cookie format - ocResult, err := convertGPPreloginCookie(ctx, gateway, result.Cookie, result.User) - if err != nil { - return nil, fmt.Errorf("GP SAML auth: failed to convert prelogin-cookie: %w", err) - } - - result.Cookie = ocResult.Cookie - result.Host = ocResult.Host - result.Fingerprint = ocResult.Fingerprint - - log.Infof("[GP-SAML] Authentication successful: user=%s, host=%s, cookie_len=%d, has_fingerprint=%v", - result.User, result.Host, len(result.Cookie), result.Fingerprint != "") - return result, nil -} - -func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user string) (*gpSamlAuthResult, error) { - ocPath, err := exec.LookPath("openconnect") - if err != nil { - return nil, fmt.Errorf("openconnect not found: %w", err) - } - - args := []string{ - "--protocol=gp", - "--usergroup=gateway:prelogin-cookie", - "--user=" + user, - "--passwd-on-stdin", - "--allow-insecure-crypto", - "--authenticate", - gateway, - } - - cmd := exec.CommandContext(ctx, ocPath, args...) - cmd.Stdin = strings.NewReader(preloginCookie) - - output, err := cmd.CombinedOutput() - if err != nil { - return nil, fmt.Errorf("openconnect --authenticate failed: %w\noutput: %s", err, string(output)) - } - - result := &gpSamlAuthResult{} - for _, line := range strings.Split(string(output), "\n") { - line = strings.TrimSpace(line) - switch { - case strings.HasPrefix(line, "COOKIE="): - result.Cookie = unshellQuote(strings.TrimPrefix(line, "COOKIE=")) - case strings.HasPrefix(line, "HOST="): - result.Host = unshellQuote(strings.TrimPrefix(line, "HOST=")) - case strings.HasPrefix(line, "FINGERPRINT="): - result.Fingerprint = unshellQuote(strings.TrimPrefix(line, "FINGERPRINT=")) - case strings.HasPrefix(line, "CONNECT_URL="): - connectURL := unshellQuote(strings.TrimPrefix(line, "CONNECT_URL=")) - if connectURL != "" && result.Host == "" { - result.Host = connectURL - } - } - } - - if result.Cookie == "" { - return nil, fmt.Errorf("no COOKIE in openconnect --authenticate output: %s", string(output)) - } - - log.Infof("[GP-SAML] openconnect --authenticate: cookie_len=%d, host=%s, fingerprint=%s", - len(result.Cookie), result.Host, result.Fingerprint) - - return result, nil -} - -func unshellQuote(s string) string { - if len(s) >= 2 { - if (s[0] == '\'' && s[len(s)-1] == '\'') || - (s[0] == '"' && s[len(s)-1] == '"') { - return s[1 : len(s)-1] - } - } - return s -} - -func parseGPSamlFromCommandLine(line string, result *gpSamlAuthResult) { - if !strings.Contains(line, "openconnect") { - return - } - - for _, part := range strings.Fields(line) { - switch { - case strings.HasPrefix(part, "--cookie="): - if result.Cookie == "" { - result.Cookie = strings.TrimPrefix(part, "--cookie=") - } - case strings.HasPrefix(part, "--servercert="): - if result.Fingerprint == "" { - result.Fingerprint = strings.TrimPrefix(part, "--servercert=") - } - case strings.HasPrefix(part, "--user="): - if result.User == "" { - result.User = strings.TrimPrefix(part, "--user=") - } - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_gp_saml_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_gp_saml_test.go deleted file mode 100644 index 5655815..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_gp_saml_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package network - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestUnshellQuote(t *testing.T) { - tests := []struct { - name string - input string - expected string - }{ - { - name: "single quoted", - input: "'hello world'", - expected: "hello world", - }, - { - name: "double quoted", - input: `"hello world"`, - expected: "hello world", - }, - { - name: "unquoted", - input: "hello", - expected: "hello", - }, - { - name: "empty single quotes", - input: "''", - expected: "", - }, - { - name: "empty double quotes", - input: `""`, - expected: "", - }, - { - name: "single quote only", - input: "'", - expected: "'", - }, - { - name: "mismatched quotes", - input: "'hello\"", - expected: "'hello\"", - }, - { - name: "with special chars", - input: "'cookie=abc123&user=john'", - expected: "cookie=abc123&user=john", - }, - { - name: "complex cookie", - input: `'authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100'`, - expected: "authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := unshellQuote(tt.input) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestParseGPSamlFromCommandLine(t *testing.T) { - tests := []struct { - name string - line string - initialResult *gpSamlAuthResult - expectedCookie string - expectedUser string - expectedFP string - }{ - { - name: "full openconnect command", - line: "openconnect --protocol=gp --cookie=AUTH123 --servercert=pin-sha256:ABC --user=john", - initialResult: &gpSamlAuthResult{}, - expectedCookie: "AUTH123", - expectedUser: "john", - expectedFP: "pin-sha256:ABC", - }, - { - name: "with equals signs in cookie", - line: "openconnect --cookie=authcookie=xyz123&portal=GATE --user=jane", - initialResult: &gpSamlAuthResult{}, - expectedCookie: "authcookie=xyz123&portal=GATE", - expectedUser: "jane", - expectedFP: "", - }, - { - name: "non-openconnect line", - line: "some other output", - initialResult: &gpSamlAuthResult{}, - expectedCookie: "", - expectedUser: "", - expectedFP: "", - }, - { - name: "preserves existing values", - line: "openconnect --user=newuser", - initialResult: &gpSamlAuthResult{Cookie: "existing", Fingerprint: "existing-fp"}, - expectedCookie: "existing", - expectedUser: "newuser", - expectedFP: "existing-fp", - }, - { - name: "only updates empty fields", - line: "openconnect --cookie=NEW --user=NEW", - initialResult: &gpSamlAuthResult{Cookie: "OLD"}, - expectedCookie: "OLD", - expectedUser: "NEW", - expectedFP: "", - }, - { - name: "real gp-saml-gui output", - line: "openconnect --protocol=gp --user=john.doe@example.com --os=linux-64 --usergroup=gateway:prelogin-cookie --passwd-on-stdin", - initialResult: &gpSamlAuthResult{}, - expectedCookie: "", - expectedUser: "john.doe@example.com", - expectedFP: "", - }, - { - name: "with server cert flag", - line: "openconnect --servercert=pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE= vpn.example.com", - initialResult: &gpSamlAuthResult{}, - expectedCookie: "", - expectedUser: "", - expectedFP: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := tt.initialResult - parseGPSamlFromCommandLine(tt.line, result) - - assert.Equal(t, tt.expectedCookie, result.Cookie, "cookie mismatch") - assert.Equal(t, tt.expectedUser, result.User, "user mismatch") - assert.Equal(t, tt.expectedFP, result.Fingerprint, "fingerprint mismatch") - }) - } -} - -func TestParseGPSamlFromCommandLine_MultipleLines(t *testing.T) { - // Simulate gp-saml-gui output with command line suggestion - lines := []string{ - "", - "SAML REDIRECT", - "Got SAML Login URL", - "POST to ACS endpoint...", - "Got 'prelogin-cookie': 'FAKE_cookie_12345'", - "openconnect --protocol=gp --user=john.doe@example.com --usergroup=gateway:prelogin-cookie --passwd-on-stdin vpn.example.com", - "", - } - - result := &gpSamlAuthResult{} - for _, line := range lines { - parseGPSamlFromCommandLine(line, result) - } - - assert.Equal(t, "john.doe@example.com", result.User) - assert.Empty(t, result.Cookie, "cookie should not be parsed from command line") - assert.Empty(t, result.Fingerprint) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_signals.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_signals.go deleted file mode 100644 index 5274ed0..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_signals.go +++ /dev/null @@ -1,530 +0,0 @@ -package network - -import ( - "github.com/Wifx/gonetworkmanager/v2" - "github.com/godbus/dbus/v5" -) - -func (b *NetworkManagerBackend) startSignalPump() error { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return err - } - b.dbusConn = conn - - signals := make(chan *dbus.Signal, 256) - b.signals = signals - conn.Signal(signals) - - if err := conn.AddMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ); err != nil { - conn.RemoveSignal(signals) - conn.Close() - return err - } - - if err := conn.AddMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")), - dbus.WithMatchInterface("org.freedesktop.NetworkManager.Settings"), - dbus.WithMatchMember("NewConnection"), - ); err != nil { - conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - conn.RemoveSignal(signals) - conn.Close() - return err - } - - if err := conn.AddMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")), - dbus.WithMatchInterface("org.freedesktop.NetworkManager.Settings"), - dbus.WithMatchMember("ConnectionRemoved"), - ); err != nil { - conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")), - dbus.WithMatchInterface("org.freedesktop.NetworkManager.Settings"), - dbus.WithMatchMember("NewConnection"), - ) - conn.RemoveSignal(signals) - conn.Close() - return err - } - - if err := conn.AddMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)), - dbus.WithMatchInterface(dbusNMInterface), - dbus.WithMatchMember("DeviceAdded"), - ); err != nil { - conn.RemoveSignal(signals) - conn.Close() - return err - } - - if err := conn.AddMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)), - dbus.WithMatchInterface(dbusNMInterface), - dbus.WithMatchMember("DeviceRemoved"), - ); err != nil { - conn.RemoveSignal(signals) - conn.Close() - return err - } - - for _, info := range b.wifiDevices { - if err := conn.AddMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ); err != nil { - conn.RemoveSignal(signals) - conn.Close() - return err - } - } - - for _, info := range b.ethernetDevices { - if err := conn.AddMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ); err != nil { - conn.RemoveSignal(signals) - conn.Close() - return err - } - } - - b.sigWG.Add(1) - go func() { - defer b.sigWG.Done() - for { - select { - case <-b.stopChan: - return - case sig, ok := <-signals: - if !ok { - return - } - if sig == nil { - continue - } - b.handleDBusSignal(sig) - } - } - }() - return nil -} - -func (b *NetworkManagerBackend) stopSignalPump() { - if b.dbusConn == nil { - return - } - - b.dbusConn.RemoveMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - - for _, info := range b.wifiDevices { - b.dbusConn.RemoveMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - } - - for _, info := range b.ethernetDevices { - b.dbusConn.RemoveMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - } - - if b.signals != nil { - b.dbusConn.RemoveSignal(b.signals) - close(b.signals) - } - - b.sigWG.Wait() - - b.dbusConn.Close() -} - -func (b *NetworkManagerBackend) handleDBusSignal(sig *dbus.Signal) { - if sig.Name == "org.freedesktop.NetworkManager.Settings.NewConnection" || - sig.Name == "org.freedesktop.NetworkManager.Settings.ConnectionRemoved" { - b.ListVPNProfiles() - if b.onStateChange != nil { - b.onStateChange() - } - return - } - - if sig.Name == "org.freedesktop.NetworkManager.DeviceAdded" { - if len(sig.Body) >= 1 { - if devicePath, ok := sig.Body[0].(dbus.ObjectPath); ok { - b.handleDeviceAdded(devicePath) - } - } - return - } - - if sig.Name == "org.freedesktop.NetworkManager.DeviceRemoved" { - if len(sig.Body) >= 1 { - if devicePath, ok := sig.Body[0].(dbus.ObjectPath); ok { - b.handleDeviceRemoved(devicePath) - } - } - return - } - - if len(sig.Body) < 2 { - return - } - - iface, ok := sig.Body[0].(string) - if !ok { - return - } - - changes, ok := sig.Body[1].(map[string]dbus.Variant) - if !ok { - return - } - - switch iface { - case dbusNMInterface: - b.handleNetworkManagerChange(changes) - - case dbusNMDeviceInterface: - b.handleDeviceChange(sig.Path, changes) - - case dbusNMWiredInterface: - b.handleWiredChange(changes) - - case dbusNMWirelessInterface: - b.handleWiFiChange(changes) - - case dbusNMAccessPointInterface: - b.handleAccessPointChange(changes) - } -} - -func (b *NetworkManagerBackend) handleNetworkManagerChange(changes map[string]dbus.Variant) { - var needsUpdate bool - - for key := range changes { - switch key { - case "PrimaryConnection", "State", "ActiveConnections": - needsUpdate = true - case "WirelessEnabled": - nm := b.nmConn.(gonetworkmanager.NetworkManager) - if enabled, err := nm.GetPropertyWirelessEnabled(); err == nil { - b.stateMutex.Lock() - b.state.WiFiEnabled = enabled - b.stateMutex.Unlock() - needsUpdate = true - } - default: - continue - } - } - - if needsUpdate { - b.updatePrimaryConnection() - if _, exists := changes["State"]; exists { - b.updateEthernetState() - b.updateWiFiState() - } - if _, exists := changes["ActiveConnections"]; exists { - b.updateVPNConnectionState() - b.ListActiveVPN() - } - if b.onStateChange != nil { - b.onStateChange() - } - } -} - -func (b *NetworkManagerBackend) handleDeviceChange(devicePath dbus.ObjectPath, changes map[string]dbus.Variant) { - var needsUpdate bool - var stateChanged bool - var managedChanged bool - - for key := range changes { - switch key { - case "State": - stateChanged = true - needsUpdate = true - case "Ip4Config": - needsUpdate = true - case "Managed": - managedChanged = true - default: - continue - } - } - - if managedChanged { - if managedVariant, ok := changes["Managed"]; ok { - if managed, ok := managedVariant.Value().(bool); ok && managed { - b.handleDeviceAdded(devicePath) - return - } - } - } - - if !needsUpdate { - return - } - - b.updateAllEthernetDevices() - b.updateEthernetState() - b.updateAllWiFiDevices() - b.updateWiFiState() - if stateChanged { - b.listEthernetConnections() - b.updatePrimaryConnection() - } - if b.onStateChange != nil { - b.onStateChange() - } -} - -func (b *NetworkManagerBackend) handleWiredChange(changes map[string]dbus.Variant) { - var needsUpdate bool - - for key := range changes { - switch key { - case "Carrier", "Speed", "HwAddress": - needsUpdate = true - default: - continue - } - } - - if !needsUpdate { - return - } - - b.updateAllEthernetDevices() - b.updateEthernetState() - b.updatePrimaryConnection() - if b.onStateChange != nil { - b.onStateChange() - } -} - -func (b *NetworkManagerBackend) handleWiFiChange(changes map[string]dbus.Variant) { - var needsStateUpdate bool - var needsNetworkUpdate bool - - for key := range changes { - switch key { - case "ActiveAccessPoint": - needsStateUpdate = true - needsNetworkUpdate = true - case "AccessPoints": - needsNetworkUpdate = true - default: - continue - } - } - - if needsStateUpdate { - b.updateWiFiState() - } - if needsNetworkUpdate { - b.updateWiFiNetworks() - } - if needsStateUpdate || needsNetworkUpdate { - if b.onStateChange != nil { - b.onStateChange() - } - } -} - -func (b *NetworkManagerBackend) handleAccessPointChange(changes map[string]dbus.Variant) { - _, hasStrength := changes["Strength"] - if !hasStrength { - return - } - - b.stateMutex.RLock() - oldSignal := b.state.WiFiSignal - b.stateMutex.RUnlock() - - b.updateWiFiState() - - b.stateMutex.RLock() - newSignal := b.state.WiFiSignal - b.stateMutex.RUnlock() - - if signalChangeSignificant(oldSignal, newSignal) { - if b.onStateChange != nil { - b.onStateChange() - } - } -} - -func (b *NetworkManagerBackend) handleDeviceAdded(devicePath dbus.ObjectPath) { - dev, err := gonetworkmanager.NewDevice(devicePath) - if err != nil { - return - } - - devType, err := dev.GetPropertyDeviceType() - if err != nil { - return - } - - if devType != gonetworkmanager.NmDeviceTypeEthernet && devType != gonetworkmanager.NmDeviceTypeWifi { - return - } - - if b.dbusConn != nil { - b.dbusConn.AddMatchSignal( - dbus.WithMatchObjectPath(devicePath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - } - - managed, _ := dev.GetPropertyManaged() - if !managed { - return - } - - iface, err := dev.GetPropertyInterface() - if err != nil { - return - } - - switch devType { - case gonetworkmanager.NmDeviceTypeEthernet: - w, err := gonetworkmanager.NewDeviceWired(devicePath) - if err != nil { - return - } - hwAddr, _ := w.GetPropertyHwAddress() - - b.ethernetDevices[iface] = ðernetDeviceInfo{ - device: dev, - wired: w, - name: iface, - hwAddress: hwAddr, - } - - if b.ethernetDevice == nil { - b.ethernetDevice = dev - } - - b.updateAllEthernetDevices() - b.updateEthernetState() - b.listEthernetConnections() - b.updatePrimaryConnection() - - case gonetworkmanager.NmDeviceTypeWifi: - w, err := gonetworkmanager.NewDeviceWireless(devicePath) - if err != nil { - return - } - hwAddr, _ := w.GetPropertyHwAddress() - - b.wifiDevices[iface] = &wifiDeviceInfo{ - device: dev, - wireless: w, - name: iface, - hwAddress: hwAddr, - } - - if b.wifiDevice == nil { - b.wifiDevice = dev - b.wifiDev = w - } - - b.updateAllWiFiDevices() - b.updateWiFiState() - } - - if b.onStateChange != nil { - b.onStateChange() - } -} - -func (b *NetworkManagerBackend) handleDeviceRemoved(devicePath dbus.ObjectPath) { - if b.dbusConn != nil { - b.dbusConn.RemoveMatchSignal( - dbus.WithMatchObjectPath(devicePath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - } - - for iface, info := range b.ethernetDevices { - if info.device.GetPath() == devicePath { - delete(b.ethernetDevices, iface) - - if b.ethernetDevice != nil { - dev := b.ethernetDevice.(gonetworkmanager.Device) - if dev.GetPath() == devicePath { - b.ethernetDevice = nil - for _, remaining := range b.ethernetDevices { - b.ethernetDevice = remaining.device - break - } - } - } - - b.updateAllEthernetDevices() - b.updateEthernetState() - b.listEthernetConnections() - b.updatePrimaryConnection() - - if b.onStateChange != nil { - b.onStateChange() - } - return - } - } - - for iface, info := range b.wifiDevices { - if info.device.GetPath() == devicePath { - delete(b.wifiDevices, iface) - - if b.wifiDevice != nil { - dev := b.wifiDevice.(gonetworkmanager.Device) - if dev.GetPath() == devicePath { - b.wifiDevice = nil - b.wifiDev = nil - for _, remaining := range b.wifiDevices { - b.wifiDevice = remaining.device - b.wifiDev = remaining.wireless - break - } - } - } - - b.updateAllWiFiDevices() - b.updateWiFiState() - - if b.onStateChange != nil { - b.onStateChange() - } - return - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_signals_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_signals_test.go deleted file mode 100644 index 6b58684..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_signals_test.go +++ /dev/null @@ -1,255 +0,0 @@ -package network - -import ( - "testing" - - mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2" - "github.com/Wifx/gonetworkmanager/v2" - "github.com/godbus/dbus/v5" - "github.com/stretchr/testify/assert" -) - -func TestNetworkManagerBackend_HandleDBusSignal_NewConnection(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - sig := &dbus.Signal{ - Name: "org.freedesktop.NetworkManager.Settings.NewConnection", - Body: []any{"/org/freedesktop/NetworkManager/Settings/1"}, - } - - assert.NotPanics(t, func() { - backend.handleDBusSignal(sig) - }) -} - -func TestNetworkManagerBackend_HandleDBusSignal_ConnectionRemoved(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - sig := &dbus.Signal{ - Name: "org.freedesktop.NetworkManager.Settings.ConnectionRemoved", - Body: []any{"/org/freedesktop/NetworkManager/Settings/1"}, - } - - assert.NotPanics(t, func() { - backend.handleDBusSignal(sig) - }) -} - -func TestNetworkManagerBackend_HandleDBusSignal_InvalidBody(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{"only-one-element"}, - } - - assert.NotPanics(t, func() { - backend.handleDBusSignal(sig) - }) -} - -func TestNetworkManagerBackend_HandleDBusSignal_InvalidInterface(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{123, map[string]dbus.Variant{}}, - } - - assert.NotPanics(t, func() { - backend.handleDBusSignal(sig) - }) -} - -func TestNetworkManagerBackend_HandleDBusSignal_InvalidChanges(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{dbusNMInterface, "not-a-map"}, - } - - assert.NotPanics(t, func() { - backend.handleDBusSignal(sig) - }) -} - -func TestNetworkManagerBackend_HandleNetworkManagerChange(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil).Maybe() - mockNM.EXPECT().GetPropertyPrimaryConnection().Return(nil, nil).Maybe() - - changes := map[string]dbus.Variant{ - "PrimaryConnection": dbus.MakeVariant("/"), - "State": dbus.MakeVariant(uint32(70)), - } - - assert.NotPanics(t, func() { - backend.handleNetworkManagerChange(changes) - }) -} - -func TestNetworkManagerBackend_HandleNetworkManagerChange_WirelessEnabled(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - mockNM.EXPECT().GetPropertyWirelessEnabled().Return(true, nil) - mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil).Maybe() - mockNM.EXPECT().GetPropertyPrimaryConnection().Return(nil, nil).Maybe() - - changes := map[string]dbus.Variant{ - "WirelessEnabled": dbus.MakeVariant(true), - } - - assert.NotPanics(t, func() { - backend.handleNetworkManagerChange(changes) - }) -} - -func TestNetworkManagerBackend_HandleNetworkManagerChange_ActiveConnections(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil) - mockNM.EXPECT().GetPropertyPrimaryConnection().Return(nil, nil).Maybe() - - changes := map[string]dbus.Variant{ - "ActiveConnections": dbus.MakeVariant([]any{}), - } - - assert.NotPanics(t, func() { - backend.handleNetworkManagerChange(changes) - }) -} - -func TestNetworkManagerBackend_HandleDeviceChange(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil).Maybe() - mockNM.EXPECT().GetPropertyPrimaryConnection().Return(nil, nil).Maybe() - - changes := map[string]dbus.Variant{ - "State": dbus.MakeVariant(uint32(100)), - } - - assert.NotPanics(t, func() { - backend.handleDeviceChange("/org/freedesktop/NetworkManager/Devices/1", changes) - }) -} - -func TestNetworkManagerBackend_HandleDeviceChange_Ip4Config(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - changes := map[string]dbus.Variant{ - "Ip4Config": dbus.MakeVariant("/"), - } - - assert.NotPanics(t, func() { - backend.handleDeviceChange("/org/freedesktop/NetworkManager/Devices/1", changes) - }) -} - -func TestNetworkManagerBackend_HandleWiFiChange_ActiveAccessPoint(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - changes := map[string]dbus.Variant{ - "ActiveAccessPoint": dbus.MakeVariant("/"), - } - - assert.NotPanics(t, func() { - backend.handleWiFiChange(changes) - }) -} - -func TestNetworkManagerBackend_HandleWiFiChange_AccessPoints(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - changes := map[string]dbus.Variant{ - "AccessPoints": dbus.MakeVariant([]any{}), - } - - assert.NotPanics(t, func() { - backend.handleWiFiChange(changes) - }) -} - -func TestNetworkManagerBackend_HandleAccessPointChange_NoStrength(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - changes := map[string]dbus.Variant{ - "SomeOtherProperty": dbus.MakeVariant("value"), - } - - assert.NotPanics(t, func() { - backend.handleAccessPointChange(changes) - }) -} - -func TestNetworkManagerBackend_HandleAccessPointChange_WithStrength(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.stateMutex.Lock() - backend.state.WiFiSignal = 50 - backend.stateMutex.Unlock() - - changes := map[string]dbus.Variant{ - "Strength": dbus.MakeVariant(uint8(80)), - } - - assert.NotPanics(t, func() { - backend.handleAccessPointChange(changes) - }) -} - -func TestNetworkManagerBackend_StopSignalPump_NoConnection(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.dbusConn = nil - assert.NotPanics(t, func() { - backend.stopSignalPump() - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_state.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_state.go deleted file mode 100644 index ba77f6d..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_state.go +++ /dev/null @@ -1,277 +0,0 @@ -package network - -import ( - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/Wifx/gonetworkmanager/v2" -) - -func (b *NetworkManagerBackend) updatePrimaryConnection() error { - nm := b.nmConn.(gonetworkmanager.NetworkManager) - - activeConns, err := nm.GetPropertyActiveConnections() - if err != nil { - return err - } - - hasActiveVPN := false - for _, activeConn := range activeConns { - connType, err := activeConn.GetPropertyType() - if err != nil { - continue - } - if connType == "vpn" || connType == "wireguard" { - state, _ := activeConn.GetPropertyState() - if state == 2 { - hasActiveVPN = true - break - } - } - } - - if hasActiveVPN { - b.stateMutex.Lock() - b.state.NetworkStatus = StatusVPN - b.stateMutex.Unlock() - return nil - } - - primaryConn, err := nm.GetPropertyPrimaryConnection() - if err != nil { - return err - } - - if primaryConn == nil || primaryConn.GetPath() == "/" { - b.stateMutex.Lock() - b.state.NetworkStatus = StatusDisconnected - b.stateMutex.Unlock() - return nil - } - - connType, err := primaryConn.GetPropertyType() - if err != nil { - return err - } - - b.stateMutex.Lock() - switch connType { - case "802-3-ethernet": - b.state.NetworkStatus = StatusEthernet - case "802-11-wireless": - b.state.NetworkStatus = StatusWiFi - case "vpn", "wireguard": - b.state.NetworkStatus = StatusVPN - default: - b.state.NetworkStatus = StatusDisconnected - } - b.stateMutex.Unlock() - - return nil -} - -func (b *NetworkManagerBackend) updateEthernetState() error { - var connectedDevice string - var connectedIP string - var anyConnected bool - - for name, info := range b.ethernetDevices { - state, err := info.device.GetPropertyState() - if err != nil { - continue - } - - if state == gonetworkmanager.NmDeviceStateActivated { - anyConnected = true - connectedDevice = name - connectedIP = b.getDeviceIP(info.device) - break - } - } - - if !anyConnected && b.ethernetDevice != nil { - dev := b.ethernetDevice.(gonetworkmanager.Device) - iface, _ := dev.GetPropertyInterface() - connectedDevice = iface - } - - b.stateMutex.Lock() - b.state.EthernetDevice = connectedDevice - b.state.EthernetConnected = anyConnected - b.state.EthernetIP = connectedIP - b.stateMutex.Unlock() - - return nil -} - -func (b *NetworkManagerBackend) getDeviceStateReason(dev gonetworkmanager.Device) uint32 { - path := dev.GetPath() - obj := b.dbusConn.Object("org.freedesktop.NetworkManager", path) - - variant, err := obj.GetProperty(dbusNMDeviceInterface + ".StateReason") - if err != nil { - return 0 - } - - if stateReasonStruct, ok := variant.Value().([]any); ok && len(stateReasonStruct) >= 2 { - if reason, ok := stateReasonStruct[1].(uint32); ok { - return reason - } - } - - return 0 -} - -func (b *NetworkManagerBackend) classifyNMStateReason(reason uint32) string { - switch reason { - case NmDeviceStateReasonWrongPassword, - NmDeviceStateReasonSupplicantTimeout, - NmDeviceStateReasonSupplicantFailed, - NmDeviceStateReasonSecretsRequired: - return errdefs.ErrBadCredentials - case NmDeviceStateReasonNoSecrets: - return errdefs.ErrUserCanceled - case NmDeviceStateReasonNoSsid: - return errdefs.ErrNoSuchSSID - case NmDeviceStateReasonDhcpClientFailed, - NmDeviceStateReasonIpConfigUnavailable: - return errdefs.ErrDhcpTimeout - case NmDeviceStateReasonSupplicantDisconnect, - NmDeviceStateReasonCarrier: - return errdefs.ErrAssocTimeout - default: - return errdefs.ErrConnectionFailed - } -} - -func (b *NetworkManagerBackend) updateWiFiState() error { - if b.wifiDevice == nil { - return nil - } - - dev := b.wifiDevice.(gonetworkmanager.Device) - - iface, err := dev.GetPropertyInterface() - if err != nil { - return err - } - - state, err := dev.GetPropertyState() - if err != nil { - return err - } - - connected := state == gonetworkmanager.NmDeviceStateActivated - failed := state == gonetworkmanager.NmDeviceStateFailed - disconnected := state == gonetworkmanager.NmDeviceStateDisconnected - - var ip, ssid, bssid string - var signal uint8 - - if connected { - if err := b.ensureWiFiDevice(); err == nil && b.wifiDev != nil { - w := b.wifiDev.(gonetworkmanager.DeviceWireless) - activeAP, err := w.GetPropertyActiveAccessPoint() - if err == nil && activeAP != nil && activeAP.GetPath() != "/" { - ssid, _ = activeAP.GetPropertySSID() - signal, _ = activeAP.GetPropertyStrength() - bssid, _ = activeAP.GetPropertyHWAddress() - } - } - - ip = b.getDeviceIP(dev) - } - - b.stateMutex.RLock() - wasConnecting := b.state.IsConnecting - connectingSSID := b.state.ConnectingSSID - b.stateMutex.RUnlock() - - var reasonCode string - if wasConnecting && connectingSSID != "" && (failed || (disconnected && !connected)) { - reason := b.getDeviceStateReason(dev) - - if reason == NmDeviceStateReasonNewActivation || reason == 0 { - return nil - } - - log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d, reason=%d", connectingSSID, state, reason) - - reasonCode = b.classifyNMStateReason(reason) - - if reasonCode == errdefs.ErrConnectionFailed { - b.failedMutex.RLock() - if b.lastFailedSSID == connectingSSID { - elapsed := time.Now().Unix() - b.lastFailedTime - if elapsed < 5 { - reasonCode = errdefs.ErrBadCredentials - } - } - b.failedMutex.RUnlock() - } - } - - var forgetSSID string - - b.stateMutex.Lock() - - wasConnecting = b.state.IsConnecting - connectingSSID = b.state.ConnectingSSID - - if wasConnecting && connectingSSID != "" { - switch { - case connected && ssid == connectingSSID: - log.Infof("[updateWiFiState] Connection successful: %s", ssid) - b.state.IsConnecting = false - b.state.ConnectingSSID = "" - b.state.LastError = "" - case failed || (disconnected && !connected): - log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d", connectingSSID, state) - b.state.IsConnecting = false - b.state.ConnectingSSID = "" - b.state.LastError = reasonCode - - if reasonCode == errdefs.ErrUserCanceled { - forgetSSID = connectingSSID - } - - b.failedMutex.Lock() - b.lastFailedSSID = connectingSSID - b.lastFailedTime = time.Now().Unix() - b.failedMutex.Unlock() - } - } - - b.state.WiFiDevice = iface - b.state.WiFiConnected = connected - b.state.WiFiIP = ip - b.state.WiFiSSID = ssid - b.state.WiFiBSSID = bssid - b.state.WiFiSignal = signal - - b.stateMutex.Unlock() - - if forgetSSID != "" { - log.Infof("[updateWiFiState] User cancelled authentication, removing connection profile for %s", forgetSSID) - if err := b.ForgetWiFiNetwork(forgetSSID); err != nil { - log.Warnf("[updateWiFiState] Failed to remove cancelled connection: %v", err) - } - } - - return nil -} - -func (b *NetworkManagerBackend) getDeviceIP(dev gonetworkmanager.Device) string { - ip4Config, err := dev.GetPropertyIP4Config() - if err != nil || ip4Config == nil { - return "" - } - - addresses, err := ip4Config.GetPropertyAddressData() - if err != nil || len(addresses) == 0 { - return "" - } - - return addresses[0].Address -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_state_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_state_test.go deleted file mode 100644 index 86aa48f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_state_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package network - -import ( - "testing" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2" - "github.com/Wifx/gonetworkmanager/v2" - "github.com/stretchr/testify/assert" -) - -func TestNetworkManagerBackend_UpdatePrimaryConnection(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil) - mockNM.EXPECT().GetPropertyPrimaryConnection().Return(nil, nil) - - err = backend.updatePrimaryConnection() - assert.NoError(t, err) -} - -func TestNetworkManagerBackend_UpdateEthernetState_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.ethernetDevice = nil - err = backend.updateEthernetState() - assert.NoError(t, err) -} - -func TestNetworkManagerBackend_UpdateWiFiState_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.wifiDevice = nil - err = backend.updateWiFiState() - assert.NoError(t, err) -} - -func TestNetworkManagerBackend_ClassifyNMStateReason(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - testCases := []struct { - reason uint32 - expected string - }{ - {NmDeviceStateReasonWrongPassword, errdefs.ErrBadCredentials}, - {NmDeviceStateReasonNoSecrets, errdefs.ErrUserCanceled}, - {NmDeviceStateReasonSupplicantTimeout, errdefs.ErrBadCredentials}, - {NmDeviceStateReasonDhcpClientFailed, errdefs.ErrDhcpTimeout}, - {NmDeviceStateReasonNoSsid, errdefs.ErrNoSuchSSID}, - {999, errdefs.ErrConnectionFailed}, - } - - for _, tc := range testCases { - result := backend.classifyNMStateReason(tc.reason) - assert.Equal(t, tc.expected, result) - } -} - -func TestNetworkManagerBackend_GetDeviceIP_NoConfig(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - mockDevice := mock_gonetworkmanager.NewMockDevice(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - mockDevice.EXPECT().GetPropertyIP4Config().Return(nil, nil) - - ip := backend.getDeviceIP(mockDevice) - assert.Empty(t, ip) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_test.go deleted file mode 100644 index 1391e4a..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package network - -import ( - "testing" - - mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2" - "github.com/stretchr/testify/assert" -) - -func TestNetworkManagerBackend_New(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - assert.NotNil(t, backend) - assert.Equal(t, "networkmanager", backend.state.Backend) - assert.NotNil(t, backend.stopChan) - assert.NotNil(t, backend.state) -} - -func TestNetworkManagerBackend_GetCurrentState(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.state.NetworkStatus = StatusWiFi - backend.state.WiFiConnected = true - backend.state.WiFiSSID = "TestNetwork" - backend.state.WiFiIP = "192.168.1.100" - backend.state.WiFiNetworks = []WiFiNetwork{ - {SSID: "TestNetwork", Signal: 80, Connected: true}, - } - backend.state.WiredConnections = []WiredConnection{ - {ID: "Wired connection 1", UUID: "test-uuid"}, - } - - state, err := backend.GetCurrentState() - assert.NoError(t, err) - assert.NotNil(t, state) - assert.Equal(t, StatusWiFi, state.NetworkStatus) - assert.True(t, state.WiFiConnected) - assert.Equal(t, "TestNetwork", state.WiFiSSID) - assert.Len(t, state.WiFiNetworks, 1) - assert.Len(t, state.WiredConnections, 1) - - assert.NotSame(t, &backend.state.WiFiNetworks, &state.WiFiNetworks) - assert.NotSame(t, &backend.state.WiredConnections, &state.WiredConnections) -} - -func TestNetworkManagerBackend_SetPromptBroker_Nil(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - err = backend.SetPromptBroker(nil) - assert.Error(t, err) - assert.Contains(t, err.Error(), "cannot be nil") -} - -func TestNetworkManagerBackend_SubmitCredentials_NoBroker(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.promptBroker = nil - err = backend.SubmitCredentials("token", map[string]string{"password": "test"}, false) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not initialized") -} - -func TestNetworkManagerBackend_CancelCredentials_NoBroker(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.promptBroker = nil - err = backend.CancelCredentials("token") - assert.Error(t, err) - assert.Contains(t, err.Error(), "not initialized") -} - -func TestNetworkManagerBackend_EnsureWiFiDevice_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.wifiDevice = nil - backend.wifiDev = nil - - err = backend.ensureWiFiDevice() - assert.Error(t, err) - assert.Contains(t, err.Error(), "no WiFi device available") -} - -func TestNetworkManagerBackend_EnsureWiFiDevice_AlreadySet(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.wifiDev = "dummy-device" - - err = backend.ensureWiFiDevice() - assert.NoError(t, err) -} - -func TestNetworkManagerBackend_StartSecretAgent_NoBroker(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.promptBroker = nil - err = backend.startSecretAgent() - assert.Error(t, err) - assert.Contains(t, err.Error(), "prompt broker not set") -} - -func TestNetworkManagerBackend_Close(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - assert.NotPanics(t, func() { - backend.Close() - }) -} - -func TestNetworkManagerBackend_GetPromptBroker(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - broker := backend.GetPromptBroker() - assert.Nil(t, broker) -} - -func TestNetworkManagerBackend_StopMonitoring_NoSignals(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - assert.NotPanics(t, func() { - backend.StopMonitoring() - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_vpn.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_vpn.go deleted file mode 100644 index 261fcef..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_vpn.go +++ /dev/null @@ -1,1394 +0,0 @@ -package network - -import ( - "bufio" - "context" - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/Wifx/gonetworkmanager/v2" - "github.com/godbus/dbus/v5" -) - -func (b *NetworkManagerBackend) ListVPNProfiles() ([]VPNProfile, error) { - s := b.settings - if s == nil { - var err error - s, err = gonetworkmanager.NewSettings() - if err != nil { - return nil, fmt.Errorf("failed to get settings: %w", err) - } - b.settings = s - } - - settingsMgr := s.(gonetworkmanager.Settings) - connections, err := settingsMgr.ListConnections() - if err != nil { - return nil, fmt.Errorf("failed to get connections: %w", err) - } - - profiles := []VPNProfile{} - for _, conn := range connections { - settings, err := conn.GetSettings() - if err != nil { - continue - } - - connMeta, ok := settings["connection"] - if !ok { - continue - } - - connType, _ := connMeta["type"].(string) - if connType != "vpn" && connType != "wireguard" { - continue - } - - connID, _ := connMeta["id"].(string) - connUUID, _ := connMeta["uuid"].(string) - autoconnect, _ := connMeta["autoconnect"].(bool) - - profile := VPNProfile{ - Name: connID, - UUID: connUUID, - Type: connType, - Autoconnect: autoconnect, - } - - if connType == "vpn" { - if vpnSettings, ok := settings["vpn"]; ok { - if svcType, ok := vpnSettings["service-type"].(string); ok { - profile.ServiceType = svcType - } - // Get full data map - if data, ok := vpnSettings["data"].(map[string]string); ok { - profile.Data = data - if remote, ok := data["remote"]; ok { - profile.RemoteHost = remote - } - if username, ok := data["username"]; ok { - profile.Username = username - } - } - } - } - - profiles = append(profiles, profile) - } - - sort.Slice(profiles, func(i, j int) bool { - return strings.ToLower(profiles[i].Name) < strings.ToLower(profiles[j].Name) - }) - - b.stateMutex.Lock() - b.state.VPNProfiles = profiles - b.stateMutex.Unlock() - - return profiles, nil -} - -func (b *NetworkManagerBackend) ListActiveVPN() ([]VPNActive, error) { - nm := b.nmConn.(gonetworkmanager.NetworkManager) - - activeConns, err := nm.GetPropertyActiveConnections() - if err != nil { - return nil, fmt.Errorf("failed to get active connections: %w", err) - } - - active := []VPNActive{} - for _, activeConn := range activeConns { - connType, err := activeConn.GetPropertyType() - if err != nil { - continue - } - - if connType != "vpn" && connType != "wireguard" { - continue - } - - uuid, _ := activeConn.GetPropertyUUID() - id, _ := activeConn.GetPropertyID() - state, _ := activeConn.GetPropertyState() - - var stateStr string - switch state { - case 0: - stateStr = "unknown" - case 1: - stateStr = "activating" - case 2: - stateStr = "activated" - case 3: - stateStr = "deactivating" - case 4: - stateStr = "deactivated" - } - - vpnActive := VPNActive{ - Name: id, - UUID: uuid, - State: stateStr, - Type: connType, - Plugin: "", - } - - // Get VPN device - devices, _ := activeConn.GetPropertyDevices() - if len(devices) > 0 { - if iface, err := devices[0].GetPropertyInterface(); err == nil { - vpnActive.Device = iface - } - } - - // Get VPN IP from IP4Config - if ip4Config, err := activeConn.GetPropertyIP4Config(); err == nil && ip4Config != nil { - if addrData, err := ip4Config.GetPropertyAddressData(); err == nil && len(addrData) > 0 { - vpnActive.IP = addrData[0].Address - } - if gw, err := ip4Config.GetPropertyGateway(); err == nil { - vpnActive.Gateway = gw - } - } - - // Get MTU from device - if len(devices) > 0 { - if mtu, err := devices[0].GetPropertyMtu(); err == nil { - vpnActive.MTU = mtu - } - } - - if connType == "vpn" { - conn, _ := activeConn.GetPropertyConnection() - if conn != nil { - connSettings, err := conn.GetSettings() - if err == nil { - if vpnSettings, ok := connSettings["vpn"]; ok { - if svcType, ok := vpnSettings["service-type"].(string); ok { - vpnActive.Plugin = svcType - } - // Get full data map - if data, ok := vpnSettings["data"].(map[string]string); ok { - vpnActive.Data = data - if remote, ok := data["remote"]; ok { - vpnActive.RemoteHost = remote - } - if username, ok := data["username"]; ok { - vpnActive.Username = username - } - } - } - } - } - } - - active = append(active, vpnActive) - } - - b.stateMutex.Lock() - b.state.VPNActive = active - b.stateMutex.Unlock() - - return active, nil -} - -func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool) error { - if singleActive { - active, err := b.ListActiveVPN() - if err == nil && len(active) > 0 { - alreadyConnected := false - for _, vpn := range active { - if vpn.UUID == uuidOrName || vpn.Name == uuidOrName { - alreadyConnected = true - break - } - } - - if !alreadyConnected { - if err := b.DisconnectAllVPN(); err != nil { - log.Warnf("Failed to disconnect existing VPNs: %v", err) - } - time.Sleep(500 * time.Millisecond) - } else { - return nil - } - } - } - - s := b.settings - if s == nil { - var err error - s, err = gonetworkmanager.NewSettings() - if err != nil { - return fmt.Errorf("failed to get settings: %w", err) - } - b.settings = s - } - - settingsMgr := s.(gonetworkmanager.Settings) - connections, err := settingsMgr.ListConnections() - if err != nil { - return fmt.Errorf("failed to get connections: %w", err) - } - - var targetConn gonetworkmanager.Connection - for _, conn := range connections { - settings, err := conn.GetSettings() - if err != nil { - continue - } - - connMeta, ok := settings["connection"] - if !ok { - continue - } - - connType, _ := connMeta["type"].(string) - if connType != "vpn" && connType != "wireguard" { - continue - } - - connID, _ := connMeta["id"].(string) - connUUID, _ := connMeta["uuid"].(string) - - if connUUID == uuidOrName || connID == uuidOrName { - targetConn = conn - break - } - } - - if targetConn == nil { - return fmt.Errorf("VPN connection not found: %s", uuidOrName) - } - - targetSettings, err := targetConn.GetSettings() - if err != nil { - return fmt.Errorf("failed to get connection settings: %w", err) - } - - var targetUUID string - var connName string - if connMeta, ok := targetSettings["connection"]; ok { - if uuid, ok := connMeta["uuid"].(string); ok { - targetUUID = uuid - } - if id, ok := connMeta["id"].(string); ok { - connName = id - } - } - - var vpnServiceType string - var vpnData map[string]string - if vpnSettings, ok := targetSettings["vpn"]; ok { - if svc, ok := vpnSettings["service-type"].(string); ok { - vpnServiceType = svc - } - if data, ok := vpnSettings["data"].(map[string]string); ok { - vpnData = data - } - } - - authAction := detectVPNAuthAction(vpnServiceType, vpnData) - - switch authAction { - case "openvpn_username": - if b.promptBroker == nil { - return fmt.Errorf("OpenVPN password authentication requires interactive prompt") - } - if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil { - return err - } - case "gp_saml": - gateway := vpnData["gateway"] - protocol := vpnData["protocol"] - if protocol != "gp" { - return fmt.Errorf("GlobalProtect SAML authentication only supported for protocol=gp, got: %s", protocol) - } - - log.Infof("[ConnectVPN] GlobalProtect SAML/SSO authentication required for %s (gateway=%s)", connName, gateway) - - samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute) - authResult, err := b.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol) - samlCancel() - if err != nil { - errMsg := err.Error() - switch { - case strings.Contains(errMsg, "not installed"): - return fmt.Errorf("gp-saml-gui is not installed (required for GlobalProtect SAML/SSO VPN)") - case strings.Contains(errMsg, "timed out") || strings.Contains(errMsg, "cancelled"): - return fmt.Errorf("GlobalProtect SAML authentication timed out — please try again") - case strings.Contains(errMsg, "no cookie"): - return fmt.Errorf("GlobalProtect SAML login did not complete — browser was closed before authentication finished") - case strings.Contains(errMsg, "convert prelogin-cookie"): - return fmt.Errorf("GlobalProtect VPN authentication succeeded but cookie exchange failed: %w", err) - default: - return fmt.Errorf("GlobalProtect SAML authentication failed: %w", err) - } - } - - b.cachedGPSamlMu.Lock() - b.cachedGPSamlCookie = &cachedGPSamlCookie{ - ConnectionUUID: targetUUID, - Cookie: authResult.Cookie, - Host: authResult.Host, - User: authResult.User, - Fingerprint: authResult.Fingerprint, - } - b.cachedGPSamlMu.Unlock() - - if err := targetConn.ClearSecrets(); err != nil { - log.Warnf("[ConnectVPN] ClearSecrets failed (non-fatal): %v", err) - } else { - log.Infof("[ConnectVPN] Cleared stale stored secrets for %s", connName) - } - - log.Infof("[ConnectVPN] GlobalProtect SAML cookie cached for %s, proceeding with activation", connName) - } - - b.stateMutex.Lock() - b.state.IsConnectingVPN = true - b.state.ConnectingVPNUUID = targetUUID - b.stateMutex.Unlock() - - if b.onStateChange != nil { - b.onStateChange() - } - - nm := b.nmConn.(gonetworkmanager.NetworkManager) - _, err = nm.ActivateConnection(targetConn, nil, nil) - if err != nil { - b.stateMutex.Lock() - b.state.IsConnectingVPN = false - b.state.ConnectingVPNUUID = "" - b.stateMutex.Unlock() - - if b.onStateChange != nil { - b.onStateChange() - } - - return fmt.Errorf("failed to activate VPN: %w", err) - } - - return nil -} - -func detectVPNAuthAction(serviceType string, data map[string]string) string { - if data == nil { - return "" - } - - switch { - case strings.Contains(serviceType, "openconnect"): - protocol := data["protocol"] - if needsExternalBrowserAuth(protocol, data["authtype"], data["username"], data) { - switch protocol { - case "gp": - return "gp_saml" - default: - log.Infof("[VPN] External browser auth detected for protocol '%s' but only GlobalProtect (gp) is currently supported", protocol) - } - } - case strings.Contains(serviceType, "openvpn"): - connType := data["connection-type"] - username := data["username"] - if (connType == "password" || connType == "password-tls") && username == "" { - return "openvpn_username" - } - } - return "" -} - -func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkmanager.Connection, connName, targetUUID, vpnServiceType string) error { - log.Infof("[ConnectVPN] OpenVPN requires username in vpn.data - prompting before activation") - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - - token, err := b.promptBroker.Ask(ctx, PromptRequest{ - Name: connName, - ConnType: "vpn", - VpnService: vpnServiceType, - SettingName: "vpn", - Fields: []string{"username", "password"}, - FieldsInfo: []FieldInfo{{Name: "username", Label: "Username", IsSecret: false}, {Name: "password", Label: "Password", IsSecret: true}}, - Reason: "required", - ConnectionId: connName, - ConnectionUuid: targetUUID, - ConnectionPath: string(targetConn.GetPath()), - }) - if err != nil { - return fmt.Errorf("failed to request credentials: %w", err) - } - - reply, err := b.promptBroker.Wait(ctx, token) - if err != nil { - return fmt.Errorf("credentials prompt failed: %w", err) - } - - if reply.Cancel { - return fmt.Errorf("user cancelled authentication") - } - - username := reply.Secrets["username"] - password := reply.Secrets["password"] - if username == "" { - return nil - } - - connObj := b.dbusConn.Object("org.freedesktop.NetworkManager", targetConn.GetPath()) - var existingSettings map[string]map[string]dbus.Variant - if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existingSettings); err != nil { - return fmt.Errorf("failed to get settings for username save: %w", err) - } - - settings := make(map[string]map[string]dbus.Variant) - if connSection, ok := existingSettings["connection"]; ok { - settings["connection"] = connSection - } - vpn := existingSettings["vpn"] - var data map[string]string - if dataVariant, ok := vpn["data"]; ok { - if dm, ok := dataVariant.Value().(map[string]string); ok { - data = make(map[string]string) - for k, v := range dm { - data[k] = v - } - } else { - data = make(map[string]string) - } - } else { - data = make(map[string]string) - } - data["username"] = username - - vpn["data"] = dbus.MakeVariant(data) - settings["vpn"] = vpn - - var result map[string]dbus.Variant - if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0, - settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result); err != nil { - return fmt.Errorf("failed to save username: %w", err) - } - log.Infof("[ConnectVPN] Username saved to connection") - - if password != "" { - b.cachedVPNCredsMu.Lock() - b.cachedVPNCreds = &cachedVPNCredentials{ - ConnectionUUID: targetUUID, - Password: password, - SavePassword: reply.Save, - } - b.cachedVPNCredsMu.Unlock() - log.Infof("[ConnectVPN] Cached password for GetSecrets") - } - - return nil -} - -func (b *NetworkManagerBackend) DisconnectVPN(uuidOrName string) error { - nm := b.nmConn.(gonetworkmanager.NetworkManager) - - activeConns, err := nm.GetPropertyActiveConnections() - if err != nil { - return fmt.Errorf("failed to get active connections: %w", err) - } - - log.Debugf("[DisconnectVPN] Looking for VPN: %s", uuidOrName) - - for _, activeConn := range activeConns { - connType, err := activeConn.GetPropertyType() - if err != nil { - continue - } - - if connType != "vpn" && connType != "wireguard" { - continue - } - - uuid, _ := activeConn.GetPropertyUUID() - id, _ := activeConn.GetPropertyID() - state, _ := activeConn.GetPropertyState() - - log.Debugf("[DisconnectVPN] Found active VPN: uuid=%s id=%s state=%d", uuid, id, state) - - if uuid == uuidOrName || id == uuidOrName { - log.Infof("[DisconnectVPN] Deactivating VPN: %s (state=%d)", id, state) - if err := nm.DeactivateConnection(activeConn); err != nil { - return fmt.Errorf("failed to deactivate VPN: %w", err) - } - b.ListActiveVPN() - if b.onStateChange != nil { - b.onStateChange() - } - return nil - } - } - - log.Warnf("[DisconnectVPN] VPN not found in active connections: %s", uuidOrName) - - s := b.settings - if s == nil { - var err error - s, err = gonetworkmanager.NewSettings() - if err != nil { - return fmt.Errorf("VPN connection not active and cannot access settings: %w", err) - } - b.settings = s - } - - settingsMgr := s.(gonetworkmanager.Settings) - connections, err := settingsMgr.ListConnections() - if err != nil { - return fmt.Errorf("VPN connection not active: %s", uuidOrName) - } - - for _, conn := range connections { - settings, err := conn.GetSettings() - if err != nil { - continue - } - - connMeta, ok := settings["connection"] - if !ok { - continue - } - - connType, _ := connMeta["type"].(string) - if connType != "vpn" && connType != "wireguard" { - continue - } - - connID, _ := connMeta["id"].(string) - connUUID, _ := connMeta["uuid"].(string) - - if connUUID == uuidOrName || connID == uuidOrName { - log.Infof("[DisconnectVPN] VPN connection exists but not active: %s", connID) - return nil - } - } - - return fmt.Errorf("VPN connection not found: %s", uuidOrName) -} - -func (b *NetworkManagerBackend) DisconnectAllVPN() error { - nm := b.nmConn.(gonetworkmanager.NetworkManager) - - activeConns, err := nm.GetPropertyActiveConnections() - if err != nil { - return fmt.Errorf("failed to get active connections: %w", err) - } - - var lastErr error - var disconnected bool - for _, activeConn := range activeConns { - connType, err := activeConn.GetPropertyType() - if err != nil { - continue - } - - if connType != "vpn" && connType != "wireguard" { - continue - } - - if err := nm.DeactivateConnection(activeConn); err != nil { - lastErr = err - log.Warnf("Failed to deactivate VPN connection: %v", err) - } else { - disconnected = true - } - } - - if disconnected { - b.ListActiveVPN() - if b.onStateChange != nil { - b.onStateChange() - } - } - - return lastErr -} - -func (b *NetworkManagerBackend) ClearVPNCredentials(uuidOrName string) error { - s := b.settings - if s == nil { - var err error - s, err = gonetworkmanager.NewSettings() - if err != nil { - return fmt.Errorf("failed to get settings: %w", err) - } - b.settings = s - } - - settingsMgr := s.(gonetworkmanager.Settings) - connections, err := settingsMgr.ListConnections() - if err != nil { - return fmt.Errorf("failed to get connections: %w", err) - } - - for _, conn := range connections { - settings, err := conn.GetSettings() - if err != nil { - continue - } - - connMeta, ok := settings["connection"] - if !ok { - continue - } - - connType, _ := connMeta["type"].(string) - if connType != "vpn" && connType != "wireguard" { - continue - } - - connID, _ := connMeta["id"].(string) - connUUID, _ := connMeta["uuid"].(string) - - if connUUID == uuidOrName || connID == uuidOrName { - if connType == "vpn" { - if vpnSettings, ok := settings["vpn"]; ok { - delete(vpnSettings, "secrets") - - if dataMap, ok := vpnSettings["data"].(map[string]string); ok { - dataMap["password-flags"] = "1" - vpnSettings["data"] = dataMap - } - } - } - - if err := conn.Update(settings); err != nil { - return fmt.Errorf("failed to update connection: %w", err) - } - - if err := conn.ClearSecrets(); err != nil { - log.Warnf("ClearSecrets call failed (may not be critical): %v", err) - } - - log.Infof("Cleared credentials for VPN: %s", connID) - return nil - } - } - - return fmt.Errorf("VPN connection not found: %s", uuidOrName) -} - -func (b *NetworkManagerBackend) updateVPNConnectionState() { - b.stateMutex.RLock() - isConnectingVPN := b.state.IsConnectingVPN - connectingVPNUUID := b.state.ConnectingVPNUUID - b.stateMutex.RUnlock() - - if !isConnectingVPN || connectingVPNUUID == "" { - return - } - - nm := b.nmConn.(gonetworkmanager.NetworkManager) - activeConns, err := nm.GetPropertyActiveConnections() - if err != nil { - return - } - - foundConnection := false - for _, activeConn := range activeConns { - connType, err := activeConn.GetPropertyType() - if err != nil { - continue - } - - if connType != "vpn" && connType != "wireguard" { - continue - } - - connUUID, err := activeConn.GetPropertyUUID() - if err != nil { - continue - } - - state, _ := activeConn.GetPropertyState() - stateReason, _ := activeConn.GetPropertyStateFlags() - - if connUUID == connectingVPNUUID { - foundConnection = true - - switch state { - case 2: - log.Infof("[updateVPNConnectionState] VPN connection successful: %s", connUUID) - b.stateMutex.Lock() - b.state.IsConnectingVPN = false - b.state.ConnectingVPNUUID = "" - b.state.LastError = "" - b.stateMutex.Unlock() - - // Clear cached PKCS11 PIN and SAML cookie on success - b.cachedPKCS11Mu.Lock() - b.cachedPKCS11PIN = nil - b.cachedPKCS11Mu.Unlock() - b.cachedGPSamlMu.Lock() - b.cachedGPSamlCookie = nil - b.cachedGPSamlMu.Unlock() - - b.pendingVPNSaveMu.Lock() - pending := b.pendingVPNSave - b.pendingVPNSave = nil - b.pendingVPNSaveMu.Unlock() - - if pending != nil { - go b.saveVPNCredentials(pending) - } - return - case 4: - log.Warnf("[updateVPNConnectionState] VPN connection failed/deactivated: %s (state=%d, flags=%d)", connUUID, state, stateReason) - b.stateMutex.Lock() - b.state.IsConnectingVPN = false - b.state.ConnectingVPNUUID = "" - b.state.LastError = "VPN connection failed" - b.stateMutex.Unlock() - - // Clear cached PKCS11 PIN and SAML cookie on failure - b.cachedPKCS11Mu.Lock() - b.cachedPKCS11PIN = nil - b.cachedPKCS11Mu.Unlock() - b.cachedGPSamlMu.Lock() - b.cachedGPSamlCookie = nil - b.cachedGPSamlMu.Unlock() - return - } - } - } - - if !foundConnection { - log.Warnf("[updateVPNConnectionState] VPN connection no longer exists: %s", connectingVPNUUID) - b.stateMutex.Lock() - b.state.IsConnectingVPN = false - b.state.ConnectingVPNUUID = "" - b.state.LastError = "VPN connection failed" - b.stateMutex.Unlock() - - // Clear cached PKCS11 PIN and SAML cookie - b.cachedPKCS11Mu.Lock() - b.cachedPKCS11PIN = nil - b.cachedPKCS11Mu.Unlock() - b.cachedGPSamlMu.Lock() - b.cachedGPSamlCookie = nil - b.cachedGPSamlMu.Unlock() - } -} - -func (b *NetworkManagerBackend) saveVPNCredentials(creds *pendingVPNCredentials) { - log.Infof("[saveVPNCredentials] Saving credentials for %s (username=%v, savePassword=%v)", - creds.ConnectionPath, creds.Username != "", creds.SavePassword) - - connObj := b.dbusConn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath(creds.ConnectionPath)) - var existingSettings map[string]map[string]dbus.Variant - if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existingSettings); err != nil { - log.Warnf("[saveVPNCredentials] GetSettings failed: %v", err) - return - } - - settings := make(map[string]map[string]dbus.Variant) - if connSection, ok := existingSettings["connection"]; ok { - settings["connection"] = connSection - } - - vpn, ok := existingSettings["vpn"] - if !ok { - vpn = make(map[string]dbus.Variant) - } - - // Get existing data map - var data map[string]string - if dataVariant, ok := vpn["data"]; ok { - if dm, ok := dataVariant.Value().(map[string]string); ok { - data = make(map[string]string) - for k, v := range dm { - data[k] = v - } - } else { - data = make(map[string]string) - } - } else { - data = make(map[string]string) - } - - // Always save username if provided - if creds.Username != "" { - data["username"] = creds.Username - log.Infof("[saveVPNCredentials] Saving username") - } - - // Save password if requested - if creds.SavePassword { - data["password-flags"] = "0" - secs := make(map[string]string) - secs["password"] = creds.Password - vpn["secrets"] = dbus.MakeVariant(secs) - log.Infof("[saveVPNCredentials] Saving password with password-flags=0") - } - - vpn["data"] = dbus.MakeVariant(data) - settings["vpn"] = vpn - - var result map[string]dbus.Variant - if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0, - settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result); err != nil { - log.Warnf("[saveVPNCredentials] Update2 failed: %v", err) - } else { - log.Infof("[saveVPNCredentials] Successfully saved credentials") - } -} - -func (b *NetworkManagerBackend) ListVPNPlugins() ([]VPNPlugin, error) { - plugins := []VPNPlugin{} - pluginDirs := []string{ - "/usr/lib/NetworkManager/VPN", - "/usr/lib64/NetworkManager/VPN", - "/etc/NetworkManager/VPN", - } - - seen := make(map[string]bool) - - for _, dir := range pluginDirs { - entries, err := os.ReadDir(dir) - if err != nil { - continue - } - - for _, entry := range entries { - if !strings.HasSuffix(entry.Name(), ".name") { - continue - } - - filePath := filepath.Join(dir, entry.Name()) - plugin, err := parseVPNPluginFile(filePath) - if err != nil { - log.Debugf("Failed to parse VPN plugin file %s: %v", filePath, err) - continue - } - - if seen[plugin.ServiceType] { - continue - } - seen[plugin.ServiceType] = true - plugins = append(plugins, *plugin) - } - } - - sort.Slice(plugins, func(i, j int) bool { - return strings.ToLower(plugins[i].Name) < strings.ToLower(plugins[j].Name) - }) - - return plugins, nil -} - -func parseVPNPluginFile(path string) (*VPNPlugin, error) { - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - - plugin := &VPNPlugin{} - scanner := bufio.NewScanner(file) - - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "[") { - continue - } - - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue - } - - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - - switch key { - case "name": - plugin.Name = value - case "service": - plugin.ServiceType = value - case "program": - plugin.Program = value - case "supports": - plugin.Supports = strings.Split(value, ",") - for i := range plugin.Supports { - plugin.Supports[i] = strings.TrimSpace(plugin.Supports[i]) - } - } - } - - if plugin.ServiceType == "" { - return nil, fmt.Errorf("plugin file missing service type") - } - - plugin.FileExtensions = getVPNFileExtensions(plugin.ServiceType) - - return plugin, nil -} - -func getVPNFileExtensions(serviceType string) []string { - switch { - case strings.Contains(serviceType, "openvpn"): - return []string{".ovpn", ".conf"} - case strings.Contains(serviceType, "wireguard"): - return []string{".conf"} - case strings.Contains(serviceType, "vpnc"), strings.Contains(serviceType, "cisco"): - return []string{".pcf", ".conf"} - case strings.Contains(serviceType, "openconnect"): - return []string{".conf"} - case strings.Contains(serviceType, "pptp"): - return []string{".conf"} - case strings.Contains(serviceType, "l2tp"): - return []string{".conf"} - case strings.Contains(serviceType, "strongswan"), strings.Contains(serviceType, "ipsec"): - return []string{".conf", ".sswan"} - default: - return []string{".conf"} - } -} - -func (b *NetworkManagerBackend) ImportVPN(filePath string, name string) (*VPNImportResult, error) { - if _, err := os.Stat(filePath); os.IsNotExist(err) { - return &VPNImportResult{ - Success: false, - Error: fmt.Sprintf("file not found: %s", filePath), - }, nil - } - - ext := strings.ToLower(filepath.Ext(filePath)) - - switch ext { - case ".ovpn", ".conf": - return b.importVPNWithNmcli(filePath, name) - default: - return b.importVPNWithNmcli(filePath, name) - } -} - -func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string) (*VPNImportResult, error) { - vpnTypes := []string{"openvpn", "wireguard", "vpnc", "pptp", "l2tp", "openconnect", "strongswan"} - - var allErrors []error - var outputStr string - for _, vpnType := range vpnTypes { - cmd := exec.Command("nmcli", "connection", "import", "type", vpnType, "file", filePath) - output, err := cmd.CombinedOutput() - if err == nil { - outputStr = string(output) - break - } - allErrors = append(allErrors, fmt.Errorf("%s: %s", vpnType, strings.TrimSpace(string(output)))) - } - - if len(allErrors) == len(vpnTypes) { - return &VPNImportResult{ - Success: false, - Error: errors.Join(allErrors...).Error(), - }, nil - } - var connUUID, connName string - - lines := strings.Split(outputStr, "\n") - for _, line := range lines { - if strings.Contains(line, "successfully added") { - parts := strings.Fields(line) - for i, part := range parts { - if part == "(" && i+1 < len(parts) { - connUUID = strings.TrimSuffix(parts[i+1], ")") - break - } - } - } - } - - if name != "" && connUUID != "" { - renameCmd := exec.Command("nmcli", "connection", "modify", connUUID, "connection.id", name) - if err := renameCmd.Run(); err != nil { - log.Warnf("Failed to rename imported VPN: %v", err) - } else { - connName = name - } - } - - if connUUID == "" { - s := b.settings - if s == nil { - var settingsErr error - s, settingsErr = gonetworkmanager.NewSettings() - if settingsErr == nil { - b.settings = s - } - } - - if s != nil { - settingsMgr := s.(gonetworkmanager.Settings) - connections, _ := settingsMgr.ListConnections() - baseName := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) - - for _, conn := range connections { - settings, err := conn.GetSettings() - if err != nil { - continue - } - connMeta, ok := settings["connection"] - if !ok { - continue - } - connType, _ := connMeta["type"].(string) - if connType != "vpn" && connType != "wireguard" { - continue - } - connID, _ := connMeta["id"].(string) - if strings.Contains(connID, baseName) || (name != "" && connID == name) { - connUUID, _ = connMeta["uuid"].(string) - connName = connID - break - } - } - } - } - - b.ListVPNProfiles() - - if b.onStateChange != nil { - b.onStateChange() - } - - return &VPNImportResult{ - Success: true, - UUID: connUUID, - Name: connName, - }, nil -} - -func (b *NetworkManagerBackend) GetVPNConfig(uuidOrName string) (*VPNConfig, error) { - s := b.settings - if s == nil { - var err error - s, err = gonetworkmanager.NewSettings() - if err != nil { - return nil, fmt.Errorf("failed to get settings: %w", err) - } - b.settings = s - } - - settingsMgr := s.(gonetworkmanager.Settings) - connections, err := settingsMgr.ListConnections() - if err != nil { - return nil, fmt.Errorf("failed to get connections: %w", err) - } - - for _, conn := range connections { - settings, err := conn.GetSettings() - if err != nil { - continue - } - - connMeta, ok := settings["connection"] - if !ok { - continue - } - - connType, _ := connMeta["type"].(string) - if connType != "vpn" && connType != "wireguard" { - continue - } - - connID, _ := connMeta["id"].(string) - connUUID, _ := connMeta["uuid"].(string) - - if connUUID != uuidOrName && connID != uuidOrName { - continue - } - - autoconnect := true - if ac, ok := connMeta["autoconnect"].(bool); ok { - autoconnect = ac - } - - config := &VPNConfig{ - UUID: connUUID, - Name: connID, - Type: connType, - Autoconnect: autoconnect, - Data: make(map[string]string), - } - - if connType == "vpn" { - if vpnSettings, ok := settings["vpn"]; ok { - if svcType, ok := vpnSettings["service-type"].(string); ok { - config.ServiceType = svcType - } - if dataMap, ok := vpnSettings["data"].(map[string]string); ok { - for k, v := range dataMap { - if !strings.Contains(strings.ToLower(k), "password") && - !strings.Contains(strings.ToLower(k), "secret") && - !strings.Contains(strings.ToLower(k), "key") { - config.Data[k] = v - } - } - } - } - } - - return config, nil - } - - return nil, fmt.Errorf("VPN connection not found: %s", uuidOrName) -} - -func (b *NetworkManagerBackend) UpdateVPNConfig(connUUID string, updates map[string]any) error { - s := b.settings - if s == nil { - var err error - s, err = gonetworkmanager.NewSettings() - if err != nil { - return fmt.Errorf("failed to get settings: %w", err) - } - b.settings = s - } - - settingsMgr := s.(gonetworkmanager.Settings) - connections, err := settingsMgr.ListConnections() - if err != nil { - return fmt.Errorf("failed to get connections: %w", err) - } - - for _, conn := range connections { - settings, err := conn.GetSettings() - if err != nil { - continue - } - - connMeta, ok := settings["connection"] - if !ok { - continue - } - - connType, _ := connMeta["type"].(string) - if connType != "vpn" && connType != "wireguard" { - continue - } - - existingUUID, _ := connMeta["uuid"].(string) - if existingUUID != connUUID { - continue - } - - if name, ok := updates["name"].(string); ok && name != "" { - connMeta["id"] = name - } - - if autoconnect, ok := updates["autoconnect"].(bool); ok { - connMeta["autoconnect"] = autoconnect - } - - if data, ok := updates["data"].(map[string]any); ok { - if vpnSettings, ok := settings["vpn"]; ok { - existingData, _ := vpnSettings["data"].(map[string]string) - if existingData == nil { - existingData = make(map[string]string) - } - for k, v := range data { - if strVal, ok := v.(string); ok { - existingData[k] = strVal - } - } - vpnSettings["data"] = existingData - } - } - - if ipv4, ok := settings["ipv4"]; ok { - delete(ipv4, "addresses") - delete(ipv4, "routes") - delete(ipv4, "dns") - } - if ipv6, ok := settings["ipv6"]; ok { - delete(ipv6, "addresses") - delete(ipv6, "routes") - delete(ipv6, "dns") - } - - if err := conn.Update(settings); err != nil { - return fmt.Errorf("failed to update connection: %w", err) - } - - b.ListVPNProfiles() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil - } - - return fmt.Errorf("VPN connection not found: %s", connUUID) -} - -func (b *NetworkManagerBackend) SetVPNCredentials(connUUID string, username string, password string, saveToKeyring bool) error { - s := b.settings - if s == nil { - var err error - s, err = gonetworkmanager.NewSettings() - if err != nil { - return fmt.Errorf("failed to get settings: %w", err) - } - b.settings = s - } - - settingsMgr := s.(gonetworkmanager.Settings) - connections, err := settingsMgr.ListConnections() - if err != nil { - return fmt.Errorf("failed to get connections: %w", err) - } - - for _, conn := range connections { - settings, err := conn.GetSettings() - if err != nil { - continue - } - - connMeta, ok := settings["connection"] - if !ok { - continue - } - - connType, _ := connMeta["type"].(string) - if connType != "vpn" && connType != "wireguard" { - continue - } - - existingUUID, _ := connMeta["uuid"].(string) - if existingUUID != connUUID { - continue - } - - vpnSettings, ok := settings["vpn"] - if !ok { - vpnSettings = make(map[string]any) - settings["vpn"] = vpnSettings - } - - existingData, _ := vpnSettings["data"].(map[string]string) - if existingData == nil { - existingData = make(map[string]string) - } - - if username != "" { - existingData["username"] = username - } - - if saveToKeyring { - existingData["password-flags"] = "0" - } else { - existingData["password-flags"] = "2" - } - - vpnSettings["data"] = existingData - - if password != "" { - secrets := make(map[string]string) - secrets["password"] = password - vpnSettings["secrets"] = secrets - } - - if ipv4, ok := settings["ipv4"]; ok { - delete(ipv4, "addresses") - delete(ipv4, "routes") - delete(ipv4, "dns") - } - if ipv6, ok := settings["ipv6"]; ok { - delete(ipv6, "addresses") - delete(ipv6, "routes") - delete(ipv6, "dns") - } - - if err := conn.Update(settings); err != nil { - return fmt.Errorf("failed to update connection: %w", err) - } - - log.Infof("Updated VPN credentials for %s (save=%v)", connUUID, saveToKeyring) - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil - } - - return fmt.Errorf("VPN connection not found: %s", connUUID) -} - -func (b *NetworkManagerBackend) DeleteVPN(uuidOrName string) error { - active, _ := b.ListActiveVPN() - for _, vpn := range active { - if vpn.UUID == uuidOrName || vpn.Name == uuidOrName { - if err := b.DisconnectVPN(uuidOrName); err != nil { - log.Warnf("Failed to disconnect VPN before deletion: %v", err) - } - time.Sleep(200 * time.Millisecond) - break - } - } - - s := b.settings - if s == nil { - var err error - s, err = gonetworkmanager.NewSettings() - if err != nil { - return fmt.Errorf("failed to get settings: %w", err) - } - b.settings = s - } - - settingsMgr := s.(gonetworkmanager.Settings) - connections, err := settingsMgr.ListConnections() - if err != nil { - return fmt.Errorf("failed to get connections: %w", err) - } - - for _, conn := range connections { - settings, err := conn.GetSettings() - if err != nil { - continue - } - - connMeta, ok := settings["connection"] - if !ok { - continue - } - - connType, _ := connMeta["type"].(string) - if connType != "vpn" && connType != "wireguard" { - continue - } - - connID, _ := connMeta["id"].(string) - connUUID, _ := connMeta["uuid"].(string) - - if connUUID == uuidOrName || connID == uuidOrName { - if err := conn.Delete(); err != nil { - return fmt.Errorf("failed to delete VPN: %w", err) - } - - b.ListVPNProfiles() - - if b.onStateChange != nil { - b.onStateChange() - } - - log.Infof("Deleted VPN connection: %s (%s)", connID, connUUID) - return nil - } - } - - return fmt.Errorf("VPN connection not found: %s", uuidOrName) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_vpn_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_vpn_test.go deleted file mode 100644 index eebd433..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_vpn_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package network - -import ( - "testing" - - mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2" - "github.com/Wifx/gonetworkmanager/v2" - "github.com/stretchr/testify/assert" -) - -func TestNetworkManagerBackend_ListVPNProfiles(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - mockSettings := mock_gonetworkmanager.NewMockSettings(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - backend.settings = mockSettings - - mockSettings.EXPECT().ListConnections().Return([]gonetworkmanager.Connection{}, nil) - - profiles, err := backend.ListVPNProfiles() - assert.NoError(t, err) - assert.Empty(t, profiles) -} - -func TestNetworkManagerBackend_ListActiveVPN(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil) - - active, err := backend.ListActiveVPN() - assert.NoError(t, err) - assert.Empty(t, active) -} - -func TestNetworkManagerBackend_ConnectVPN_NotFound(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - mockSettings := mock_gonetworkmanager.NewMockSettings(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - backend.settings = mockSettings - - mockSettings.EXPECT().ListConnections().Return([]gonetworkmanager.Connection{}, nil) - - err = backend.ConnectVPN("non-existent-vpn-12345", false) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not found") -} - -func TestNetworkManagerBackend_ConnectVPN_SingleActive_NoActiveVPN(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - mockSettings := mock_gonetworkmanager.NewMockSettings(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - backend.settings = mockSettings - - mockSettings.EXPECT().ListConnections().Return([]gonetworkmanager.Connection{}, nil) - mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil) - - err = backend.ConnectVPN("non-existent-vpn-12345", true) - assert.Error(t, err) -} - -func TestNetworkManagerBackend_DisconnectVPN_NotActive(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil) - - err = backend.DisconnectVPN("non-existent-vpn-12345") - assert.Error(t, err) -} - -func TestNetworkManagerBackend_DisconnectAllVPN(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil) - - err = backend.DisconnectAllVPN() - assert.NoError(t, err) -} - -func TestNetworkManagerBackend_ClearVPNCredentials_NotFound(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - mockSettings := mock_gonetworkmanager.NewMockSettings(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - backend.settings = mockSettings - - mockSettings.EXPECT().ListConnections().Return([]gonetworkmanager.Connection{}, nil) - - err = backend.ClearVPNCredentials("non-existent-vpn-12345") - assert.Error(t, err) - assert.Contains(t, err.Error(), "not found") -} - -func TestNetworkManagerBackend_UpdateVPNConnectionState_NotConnecting(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.stateMutex.Lock() - backend.state.IsConnectingVPN = false - backend.state.ConnectingVPNUUID = "" - backend.stateMutex.Unlock() - - assert.NotPanics(t, func() { - backend.updateVPNConnectionState() - }) -} - -func TestNetworkManagerBackend_UpdateVPNConnectionState_EmptyUUID(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.stateMutex.Lock() - backend.state.IsConnectingVPN = true - backend.state.ConnectingVPNUUID = "" - backend.stateMutex.Unlock() - - assert.NotPanics(t, func() { - backend.updateVPNConnectionState() - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_wifi.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_wifi.go deleted file mode 100644 index 6f593b1..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_wifi.go +++ /dev/null @@ -1,1173 +0,0 @@ -package network - -import ( - "bytes" - "fmt" - "sort" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/Wifx/gonetworkmanager/v2" -) - -func (b *NetworkManagerBackend) GetWiFiEnabled() (bool, error) { - nm := b.nmConn.(gonetworkmanager.NetworkManager) - return nm.GetPropertyWirelessEnabled() -} - -func (b *NetworkManagerBackend) SetWiFiEnabled(enabled bool) error { - nm := b.nmConn.(gonetworkmanager.NetworkManager) - err := nm.SetPropertyWirelessEnabled(enabled) - if err != nil { - return fmt.Errorf("failed to set WiFi enabled: %w", err) - } - - b.stateMutex.Lock() - b.state.WiFiEnabled = enabled - b.stateMutex.Unlock() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil -} - -func (b *NetworkManagerBackend) ScanWiFi() error { - if b.wifiDevice == nil { - return fmt.Errorf("no WiFi device available") - } - - b.stateMutex.RLock() - enabled := b.state.WiFiEnabled - b.stateMutex.RUnlock() - - if !enabled { - return fmt.Errorf("WiFi is disabled") - } - - if err := b.ensureWiFiDevice(); err != nil { - return err - } - - w := b.wifiDev.(gonetworkmanager.DeviceWireless) - err := w.RequestScan() - if err != nil { - return fmt.Errorf("scan request failed: %w", err) - } - - _, err = b.updateWiFiNetworks() - return err -} - -func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) { - if b.wifiDevice == nil { - return nil, fmt.Errorf("no WiFi device available") - } - - if err := b.ensureWiFiDevice(); err != nil { - return nil, err - } - wifiDev := b.wifiDev - - w := wifiDev.(gonetworkmanager.DeviceWireless) - apPaths, err := w.GetAccessPoints() - if err != nil { - return nil, fmt.Errorf("failed to get access points: %w", err) - } - - s := b.settings - if s == nil { - s, err = gonetworkmanager.NewSettings() - if err != nil { - return nil, fmt.Errorf("failed to get settings: %w", err) - } - b.settings = s - } - - settingsMgr := s.(gonetworkmanager.Settings) - connections, err := settingsMgr.ListConnections() - if err != nil { - return nil, fmt.Errorf("failed to get connections: %w", err) - } - - savedSSIDs := make(map[string]bool) - autoconnectMap := make(map[string]bool) - for _, conn := range connections { - connSettings, err := conn.GetSettings() - if err != nil { - continue - } - - if connMeta, ok := connSettings["connection"]; ok { - if connType, ok := connMeta["type"].(string); ok && connType == "802-11-wireless" { - if wifiSettings, ok := connSettings["802-11-wireless"]; ok { - if ssidBytes, ok := wifiSettings["ssid"].([]byte); ok { - savedSSID := string(ssidBytes) - savedSSIDs[savedSSID] = true - autoconnect := true - if ac, ok := connMeta["autoconnect"].(bool); ok { - autoconnect = ac - } - autoconnectMap[savedSSID] = autoconnect - } - } - } - } - } - - b.stateMutex.RLock() - currentSSID := b.state.WiFiSSID - currentBSSID := b.state.WiFiBSSID - b.stateMutex.RUnlock() - - var bands []WiFiNetwork - - for _, ap := range apPaths { - apSSID, err := ap.GetPropertySSID() - if err != nil || apSSID != ssid { - continue - } - - strength, _ := ap.GetPropertyStrength() - flags, _ := ap.GetPropertyFlags() - wpaFlags, _ := ap.GetPropertyWPAFlags() - rsnFlags, _ := ap.GetPropertyRSNFlags() - freq, _ := ap.GetPropertyFrequency() - maxBitrate, _ := ap.GetPropertyMaxBitrate() - bssid, _ := ap.GetPropertyHWAddress() - mode, _ := ap.GetPropertyMode() - - secured := flags != uint32(gonetworkmanager.Nm80211APFlagsNone) || - wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) || - rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone) - - enterprise := (rsnFlags&uint32(gonetworkmanager.Nm80211APSecKeyMgmt8021X) != 0) || - (wpaFlags&uint32(gonetworkmanager.Nm80211APSecKeyMgmt8021X) != 0) - - var modeStr string - switch mode { - case gonetworkmanager.Nm80211ModeAdhoc: - modeStr = "adhoc" - case gonetworkmanager.Nm80211ModeInfra: - modeStr = "infrastructure" - case gonetworkmanager.Nm80211ModeAp: - modeStr = "ap" - default: - modeStr = "unknown" - } - - channel := frequencyToChannel(freq) - - isConnected := ssid == currentSSID && bssid == currentBSSID - rate := maxBitrate / 1000 - if isConnected { - if devBitrate, err := w.GetPropertyBitrate(); err == nil && devBitrate > 0 { - rate = devBitrate / 1000 - } - } - - network := WiFiNetwork{ - SSID: ssid, - BSSID: bssid, - Signal: strength, - Secured: secured, - Enterprise: enterprise, - Connected: isConnected, - Saved: savedSSIDs[ssid], - Autoconnect: autoconnectMap[ssid], - Frequency: freq, - Mode: modeStr, - Rate: rate, - Channel: channel, - } - - bands = append(bands, network) - } - - if len(bands) == 0 { - return nil, fmt.Errorf("network not found: %s", ssid) - } - - sort.Slice(bands, func(i, j int) bool { - if bands[i].Connected && !bands[j].Connected { - return true - } - if !bands[i].Connected && bands[j].Connected { - return false - } - return bands[i].Signal > bands[j].Signal - }) - - return &NetworkInfoResponse{ - SSID: ssid, - Bands: bands, - }, nil -} - -func (b *NetworkManagerBackend) GetWiFiQRCodeContent(ssid string) (string, error) { - conn, err := b.findConnection(ssid) - if err != nil { - return "", fmt.Errorf("no saved connection for `%s`: %w", ssid, err) - } - - connSettings, err := conn.GetSettings() - if err != nil { - return "", fmt.Errorf("failed to get settings for `%s`: %w", ssid, err) - } - - secSettings, ok := connSettings["802-11-wireless-security"] - if !ok { - return "", fmt.Errorf("network `%s` has no security settings", ssid) - } - - keyMgmt, ok := secSettings["key-mgmt"].(string) - if !ok { - return "", fmt.Errorf("failed to identify security type of network `%s`", ssid) - } - - var securityType string - switch keyMgmt { - case "none": - authAlg, _ := secSettings["auth-alg"].(string) - switch authAlg { - case "open": - securityType = "nopass" - default: - securityType = "WEP" - } - case "ieee8021x": - securityType = "WEP" - default: - securityType = "WPA" - } - - if securityType != "WPA" { - return "", fmt.Errorf("QR code generation only supports WPA connections, `%s` uses %s", ssid, securityType) - } - - secrets, err := conn.GetSecrets("802-11-wireless-security") - if err != nil { - return "", fmt.Errorf("failed to retrieve connection secrets for `%s`: %w", ssid, err) - } - - secSecrets, ok := secrets["802-11-wireless-security"] - if !ok { - return "", fmt.Errorf("failed to retrieve password for `%s`", ssid) - } - - psk, ok := secSecrets["psk"].(string) - if !ok { - return "", fmt.Errorf("failed to retrieve password for `%s`", ssid) - } - - return FormatWiFiQRString(securityType, ssid, psk), nil -} - -func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error { - devInfo, err := b.getWifiDeviceForConnection(req.Device) - if err != nil { - return err - } - - b.stateMutex.RLock() - alreadyConnected := b.state.WiFiConnected && b.state.WiFiSSID == req.SSID - b.stateMutex.RUnlock() - - if alreadyConnected && !req.Interactive && req.Device == "" { - return nil - } - - b.stateMutex.Lock() - b.state.IsConnecting = true - b.state.ConnectingSSID = req.SSID - b.state.ConnectingDevice = req.Device - b.state.LastError = "" - b.stateMutex.Unlock() - - if b.onStateChange != nil { - b.onStateChange() - } - - nm := b.nmConn.(gonetworkmanager.NetworkManager) - - existingConn, err := b.findConnection(req.SSID) - if err == nil && existingConn != nil { - _, err := nm.ActivateConnection(existingConn, devInfo.device, nil) - if err != nil { - log.Warnf("[ConnectWiFi] Failed to activate existing connection: %v", err) - b.stateMutex.Lock() - b.state.IsConnecting = false - b.state.ConnectingSSID = "" - b.state.ConnectingDevice = "" - b.state.LastError = fmt.Sprintf("failed to activate connection: %v", err) - b.stateMutex.Unlock() - if b.onStateChange != nil { - b.onStateChange() - } - return fmt.Errorf("failed to activate connection: %w", err) - } - - return nil - } - - if err := b.createAndConnectWiFiOnDevice(req, devInfo); err != nil { - log.Warnf("[ConnectWiFi] Failed to create and connect: %v", err) - b.stateMutex.Lock() - b.state.IsConnecting = false - b.state.ConnectingSSID = "" - b.state.ConnectingDevice = "" - b.state.LastError = err.Error() - b.stateMutex.Unlock() - if b.onStateChange != nil { - b.onStateChange() - } - return err - } - - return nil -} - -func (b *NetworkManagerBackend) DisconnectWiFi() error { - if b.wifiDevice == nil { - return fmt.Errorf("no WiFi device available") - } - - dev := b.wifiDevice.(gonetworkmanager.Device) - - err := dev.Disconnect() - if err != nil { - return fmt.Errorf("failed to disconnect: %w", err) - } - - b.updateWiFiState() - b.updatePrimaryConnection() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil -} - -func (b *NetworkManagerBackend) ForgetWiFiNetwork(ssid string) error { - conn, err := b.findConnection(ssid) - if err != nil { - return fmt.Errorf("connection not found: %w", err) - } - - b.stateMutex.RLock() - currentSSID := b.state.WiFiSSID - isConnected := b.state.WiFiConnected - b.stateMutex.RUnlock() - - err = conn.Delete() - if err != nil { - return fmt.Errorf("failed to delete connection: %w", err) - } - - if isConnected && currentSSID == ssid { - b.stateMutex.Lock() - b.state.WiFiConnected = false - b.state.WiFiSSID = "" - b.state.WiFiBSSID = "" - b.state.WiFiSignal = 0 - b.state.WiFiIP = "" - b.state.NetworkStatus = StatusDisconnected - b.stateMutex.Unlock() - } - - b.updateWiFiNetworks() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil -} - -func (b *NetworkManagerBackend) IsConnectingTo(ssid string) bool { - b.stateMutex.RLock() - defer b.stateMutex.RUnlock() - return b.state.IsConnecting && b.state.ConnectingSSID == ssid -} - -func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) { - if b.wifiDevice == nil { - return nil, fmt.Errorf("no WiFi device available") - } - - if err := b.ensureWiFiDevice(); err != nil { - return nil, err - } - wifiDev := b.wifiDev - - w := wifiDev.(gonetworkmanager.DeviceWireless) - apPaths, err := w.GetAccessPoints() - if err != nil { - return nil, fmt.Errorf("failed to get access points: %w", err) - } - - s := b.settings - if s == nil { - s, err = gonetworkmanager.NewSettings() - if err != nil { - return nil, fmt.Errorf("failed to get settings: %w", err) - } - b.settings = s - } - - settingsMgr := s.(gonetworkmanager.Settings) - connections, err := settingsMgr.ListConnections() - if err != nil { - return nil, fmt.Errorf("failed to get connections: %w", err) - } - - savedSSIDs := make(map[string]bool) - autoconnectMap := make(map[string]bool) - hiddenSSIDs := make(map[string]bool) - for _, conn := range connections { - connSettings, err := conn.GetSettings() - if err != nil { - continue - } - - connMeta, ok := connSettings["connection"] - if !ok { - continue - } - - connType, ok := connMeta["type"].(string) - if !ok || connType != "802-11-wireless" { - continue - } - - wifiSettings, ok := connSettings["802-11-wireless"] - if !ok { - continue - } - - ssidBytes, ok := wifiSettings["ssid"].([]byte) - if !ok { - continue - } - - ssid := string(ssidBytes) - savedSSIDs[ssid] = true - autoconnect := true - if ac, ok := connMeta["autoconnect"].(bool); ok { - autoconnect = ac - } - autoconnectMap[ssid] = autoconnect - - if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden { - hiddenSSIDs[ssid] = true - } - } - - b.stateMutex.RLock() - currentSSID := b.state.WiFiSSID - wifiConnected := b.state.WiFiConnected - wifiSignal := b.state.WiFiSignal - wifiBSSID := b.state.WiFiBSSID - b.stateMutex.RUnlock() - - seenSSIDs := make(map[string]*WiFiNetwork) - networks := []WiFiNetwork{} - - for _, ap := range apPaths { - ssid, err := ap.GetPropertySSID() - if err != nil || ssid == "" { - continue - } - - if existing, exists := seenSSIDs[ssid]; exists { - strength, _ := ap.GetPropertyStrength() - if strength > existing.Signal { - existing.Signal = strength - freq, _ := ap.GetPropertyFrequency() - existing.Frequency = freq - bssid, _ := ap.GetPropertyHWAddress() - existing.BSSID = bssid - } - continue - } - - strength, _ := ap.GetPropertyStrength() - flags, _ := ap.GetPropertyFlags() - wpaFlags, _ := ap.GetPropertyWPAFlags() - rsnFlags, _ := ap.GetPropertyRSNFlags() - freq, _ := ap.GetPropertyFrequency() - maxBitrate, _ := ap.GetPropertyMaxBitrate() - bssid, _ := ap.GetPropertyHWAddress() - mode, _ := ap.GetPropertyMode() - - secured := flags != uint32(gonetworkmanager.Nm80211APFlagsNone) || - wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) || - rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone) - - enterprise := (rsnFlags&uint32(gonetworkmanager.Nm80211APSecKeyMgmt8021X) != 0) || - (wpaFlags&uint32(gonetworkmanager.Nm80211APSecKeyMgmt8021X) != 0) - - var modeStr string - switch mode { - case gonetworkmanager.Nm80211ModeAdhoc: - modeStr = "adhoc" - case gonetworkmanager.Nm80211ModeInfra: - modeStr = "infrastructure" - case gonetworkmanager.Nm80211ModeAp: - modeStr = "ap" - default: - modeStr = "unknown" - } - - channel := frequencyToChannel(freq) - - isConnected := ssid == currentSSID - rate := maxBitrate / 1000 - if isConnected { - if devBitrate, err := w.GetPropertyBitrate(); err == nil && devBitrate > 0 { - rate = devBitrate / 1000 - } - } - - network := WiFiNetwork{ - SSID: ssid, - BSSID: bssid, - Signal: strength, - Secured: secured, - Enterprise: enterprise, - Connected: isConnected, - Saved: savedSSIDs[ssid], - Autoconnect: autoconnectMap[ssid], - Hidden: hiddenSSIDs[ssid], - Frequency: freq, - Mode: modeStr, - Rate: rate, - Channel: channel, - } - - seenSSIDs[ssid] = &network - networks = append(networks, network) - } - - if wifiConnected && currentSSID != "" { - if _, exists := seenSSIDs[currentSSID]; !exists { - hiddenNetwork := WiFiNetwork{ - SSID: currentSSID, - BSSID: wifiBSSID, - Signal: wifiSignal, - Secured: true, - Connected: true, - Saved: savedSSIDs[currentSSID], - Autoconnect: autoconnectMap[currentSSID], - Hidden: true, - Mode: "infrastructure", - } - networks = append(networks, hiddenNetwork) - } - } - - sortWiFiNetworks(networks) - - b.stateMutex.Lock() - b.state.WiFiNetworks = networks - b.stateMutex.Unlock() - - return networks, nil -} - -func (b *NetworkManagerBackend) findConnection(ssid string) (gonetworkmanager.Connection, error) { - s := b.settings - if s == nil { - var err error - s, err = gonetworkmanager.NewSettings() - if err != nil { - return nil, err - } - b.settings = s - } - - settings := s.(gonetworkmanager.Settings) - connections, err := settings.ListConnections() - if err != nil { - return nil, err - } - - ssidBytes := []byte(ssid) - for _, conn := range connections { - connSettings, err := conn.GetSettings() - if err != nil { - continue - } - - if connMeta, ok := connSettings["connection"]; ok { - if connType, ok := connMeta["type"].(string); ok && connType == "802-11-wireless" { - if wifiSettings, ok := connSettings["802-11-wireless"]; ok { - if candidateSSID, ok := wifiSettings["ssid"].([]byte); ok { - if bytes.Equal(candidateSSID, ssidBytes) { - return conn, nil - } - } - } - } - } - } - - return nil, fmt.Errorf("connection not found") -} - -func (b *NetworkManagerBackend) createAndConnectWiFi(req ConnectionRequest) error { - devInfo, err := b.getWifiDeviceForConnection(req.Device) - if err != nil { - return err - } - return b.createAndConnectWiFiOnDevice(req, devInfo) -} - -func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionRequest, devInfo *wifiDeviceInfo) error { - nm := b.nmConn.(gonetworkmanager.NetworkManager) - dev := devInfo.device - w := devInfo.wireless - - var targetAP gonetworkmanager.AccessPoint - var flags, wpaFlags, rsnFlags uint32 - - if !req.Hidden { - apPaths, err := w.GetAccessPoints() - if err != nil { - return fmt.Errorf("failed to get access points: %w", err) - } - - for _, ap := range apPaths { - ssid, err := ap.GetPropertySSID() - if err != nil || ssid != req.SSID { - continue - } - targetAP = ap - break - } - - if targetAP == nil { - return fmt.Errorf("access point not found: %s", req.SSID) - } - - flags, _ = targetAP.GetPropertyFlags() - wpaFlags, _ = targetAP.GetPropertyWPAFlags() - rsnFlags, _ = targetAP.GetPropertyRSNFlags() - } - - const KeyMgmt8021x = uint32(512) - const KeyMgmtPsk = uint32(256) - const KeyMgmtSae = uint32(1024) - - var isEnterprise, isPsk, isSae, secured bool - - switch { - case req.Hidden: - secured = req.Password != "" || req.Username != "" - isEnterprise = req.Username != "" - isPsk = req.Password != "" && !isEnterprise - default: - isEnterprise = (wpaFlags&KeyMgmt8021x) != 0 || (rsnFlags&KeyMgmt8021x) != 0 - isPsk = (wpaFlags&KeyMgmtPsk) != 0 || (rsnFlags&KeyMgmtPsk) != 0 - isSae = (wpaFlags&KeyMgmtSae) != 0 || (rsnFlags&KeyMgmtSae) != 0 - secured = flags != uint32(gonetworkmanager.Nm80211APFlagsNone) || - wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) || - rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone) - } - - if isEnterprise { - log.Infof("[createAndConnectWiFi] Enterprise network detected (802.1x) - SSID: %s, interactive: %v", - req.SSID, req.Interactive) - } - - settings := make(map[string]map[string]any) - - settings["connection"] = map[string]any{ - "id": req.SSID, - "type": "802-11-wireless", - "autoconnect": true, - } - - settings["ipv4"] = map[string]any{"method": "auto"} - settings["ipv6"] = map[string]any{"method": "auto"} - - if secured { - wifiSettings := map[string]any{ - "ssid": []byte(req.SSID), - "mode": "infrastructure", - "security": "802-11-wireless-security", - } - if req.Hidden { - wifiSettings["hidden"] = true - } - settings["802-11-wireless"] = wifiSettings - - switch { - case isEnterprise || req.Username != "": - settings["802-11-wireless-security"] = map[string]any{ - "key-mgmt": "wpa-eap", - } - - eapMethod := "peap" - if req.EAPMethod != "" { - eapMethod = req.EAPMethod - } - - phase2Auth := "mschapv2" - if req.Phase2Auth != "" { - phase2Auth = req.Phase2Auth - } - - useSystemCACerts := false - if req.UseSystemCACerts != nil { - useSystemCACerts = *req.UseSystemCACerts - } - - x := map[string]any{ - "eap": []string{eapMethod}, - "system-ca-certs": useSystemCACerts, - "password-flags": uint32(0), - } - - switch eapMethod { - case "peap", "ttls": - x["phase2-auth"] = phase2Auth - case "tls": - if req.ClientCertPath != "" { - x["client-cert"] = []byte("file://" + req.ClientCertPath) - } - if req.PrivateKeyPath != "" { - x["private-key"] = []byte("file://" + req.PrivateKeyPath) - } - } - - if req.Username != "" { - x["identity"] = req.Username - } - if req.Password != "" { - x["password"] = req.Password - } - if req.AnonymousIdentity != "" { - x["anonymous-identity"] = req.AnonymousIdentity - } - if req.DomainSuffixMatch != "" { - x["domain-suffix-match"] = req.DomainSuffixMatch - } - if req.CACertPath != "" { - x["ca-cert"] = []byte("file://" + req.CACertPath) - } - - settings["802-1x"] = x - - log.Infof("[createAndConnectWiFi] WPA-EAP settings: eap=%s, phase2-auth=%s, identity=%s, interactive=%v, system-ca-certs=%v, domain-suffix-match=%q", - eapMethod, phase2Auth, req.Username, req.Interactive, useSystemCACerts, req.DomainSuffixMatch) - - case isPsk: - sec := map[string]any{ - "key-mgmt": "wpa-psk", - "psk-flags": uint32(0), - } - if !req.Interactive { - sec["psk"] = req.Password - } - settings["802-11-wireless-security"] = sec - - case isSae: - sec := map[string]any{ - "key-mgmt": "sae", - "pmf": int32(3), - "psk-flags": uint32(0), - } - if !req.Interactive { - sec["psk"] = req.Password - } - settings["802-11-wireless-security"] = sec - - default: - return fmt.Errorf("secured network but not SAE/PSK/802.1X (rsn=0x%x wpa=0x%x)", rsnFlags, wpaFlags) - } - } else { - wifiSettings := map[string]any{ - "ssid": []byte(req.SSID), - "mode": "infrastructure", - } - if req.Hidden { - wifiSettings["hidden"] = true - } - settings["802-11-wireless"] = wifiSettings - } - - if req.Interactive { - s := b.settings - if s == nil { - var settingsErr error - s, settingsErr = gonetworkmanager.NewSettings() - if settingsErr != nil { - return fmt.Errorf("failed to get settings manager: %w", settingsErr) - } - b.settings = s - } - - settingsMgr := s.(gonetworkmanager.Settings) - conn, err := settingsMgr.AddConnection(settings) - if err != nil { - return fmt.Errorf("failed to add connection: %w", err) - } - - if isEnterprise { - log.Infof("[createAndConnectWiFi] Enterprise connection added, activating (secret agent will be called)") - } - - if req.Hidden { - _, err = nm.ActivateConnection(conn, dev, nil) - } else { - _, err = nm.ActivateWirelessConnection(conn, dev, targetAP) - } - if err != nil { - return fmt.Errorf("failed to activate connection: %w", err) - } - - log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...") - } else { - var err error - if req.Hidden { - _, err = nm.AddAndActivateConnection(settings, dev) - } else { - _, err = nm.AddAndActivateWirelessConnection(settings, dev, targetAP) - } - if err != nil { - return fmt.Errorf("failed to connect: %w", err) - } - log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...") - } - - return nil -} - -func (b *NetworkManagerBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error { - conn, err := b.findConnection(ssid) - if err != nil { - return fmt.Errorf("connection not found: %w", err) - } - - settings, err := conn.GetSettings() - if err != nil { - return fmt.Errorf("failed to get connection settings: %w", err) - } - - if connMeta, ok := settings["connection"]; ok { - connMeta["autoconnect"] = autoconnect - } else { - return fmt.Errorf("connection metadata not found") - } - - if ipv4, ok := settings["ipv4"]; ok { - delete(ipv4, "addresses") - delete(ipv4, "routes") - delete(ipv4, "dns") - } - - if ipv6, ok := settings["ipv6"]; ok { - delete(ipv6, "addresses") - delete(ipv6, "routes") - delete(ipv6, "dns") - } - - err = conn.Update(settings) - if err != nil { - return fmt.Errorf("failed to update connection: %w", err) - } - - b.updateWiFiNetworks() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil -} - -func (b *NetworkManagerBackend) ScanWiFiDevice(device string) error { - devInfo, ok := b.wifiDevices[device] - if !ok { - return fmt.Errorf("WiFi device not found: %s", device) - } - - b.stateMutex.RLock() - enabled := b.state.WiFiEnabled - b.stateMutex.RUnlock() - - if !enabled { - return fmt.Errorf("WiFi is disabled") - } - - if err := devInfo.wireless.RequestScan(); err != nil { - return fmt.Errorf("scan request failed: %w", err) - } - - b.updateAllWiFiDevices() - return nil -} - -func (b *NetworkManagerBackend) DisconnectWiFiDevice(device string) error { - devInfo, ok := b.wifiDevices[device] - if !ok { - return fmt.Errorf("WiFi device not found: %s", device) - } - - if err := devInfo.device.Disconnect(); err != nil { - return fmt.Errorf("failed to disconnect: %w", err) - } - - b.updateWiFiState() - b.updateAllWiFiDevices() - b.updatePrimaryConnection() - - if b.onStateChange != nil { - b.onStateChange() - } - - return nil -} - -func (b *NetworkManagerBackend) GetWiFiDevices() []WiFiDevice { - b.stateMutex.RLock() - defer b.stateMutex.RUnlock() - return append([]WiFiDevice(nil), b.state.WiFiDevices...) -} - -func (b *NetworkManagerBackend) updateAllWiFiDevices() { - s := b.settings - if s == nil { - var err error - s, err = gonetworkmanager.NewSettings() - if err != nil { - return - } - b.settings = s - } - - settingsMgr := s.(gonetworkmanager.Settings) - connections, err := settingsMgr.ListConnections() - if err != nil { - return - } - - savedSSIDs := make(map[string]bool) - autoconnectMap := make(map[string]bool) - hiddenSSIDs := make(map[string]bool) - for _, conn := range connections { - connSettings, err := conn.GetSettings() - if err != nil { - continue - } - - connMeta, ok := connSettings["connection"] - if !ok { - continue - } - - connType, ok := connMeta["type"].(string) - if !ok || connType != "802-11-wireless" { - continue - } - - wifiSettings, ok := connSettings["802-11-wireless"] - if !ok { - continue - } - - ssidBytes, ok := wifiSettings["ssid"].([]byte) - if !ok { - continue - } - - ssid := string(ssidBytes) - savedSSIDs[ssid] = true - autoconnect := true - if ac, ok := connMeta["autoconnect"].(bool); ok { - autoconnect = ac - } - autoconnectMap[ssid] = autoconnect - - if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden { - hiddenSSIDs[ssid] = true - } - } - - var devices []WiFiDevice - - for name, devInfo := range b.wifiDevices { - state, _ := devInfo.device.GetPropertyState() - connected := state == gonetworkmanager.NmDeviceStateActivated - - var ssid, bssid, ip string - var signal uint8 - - if connected { - if activeAP, err := devInfo.wireless.GetPropertyActiveAccessPoint(); err == nil && activeAP != nil && activeAP.GetPath() != "/" { - ssid, _ = activeAP.GetPropertySSID() - signal, _ = activeAP.GetPropertyStrength() - bssid, _ = activeAP.GetPropertyHWAddress() - } - ip = b.getDeviceIP(devInfo.device) - } - - stateStr := "disconnected" - switch state { - case gonetworkmanager.NmDeviceStateActivated: - stateStr = "connected" - case gonetworkmanager.NmDeviceStateConfig, gonetworkmanager.NmDeviceStateIpConfig: - stateStr = "connecting" - case gonetworkmanager.NmDeviceStatePrepare: - stateStr = "preparing" - case gonetworkmanager.NmDeviceStateDeactivating: - stateStr = "disconnecting" - } - - apPaths, err := devInfo.wireless.GetAccessPoints() - var networks []WiFiNetwork - if err == nil { - seenSSIDs := make(map[string]*WiFiNetwork) - for _, ap := range apPaths { - apSSID, err := ap.GetPropertySSID() - if err != nil || apSSID == "" { - continue - } - - if existing, exists := seenSSIDs[apSSID]; exists { - strength, _ := ap.GetPropertyStrength() - if strength > existing.Signal { - existing.Signal = strength - freq, _ := ap.GetPropertyFrequency() - existing.Frequency = freq - apBSSID, _ := ap.GetPropertyHWAddress() - existing.BSSID = apBSSID - } - continue - } - - strength, _ := ap.GetPropertyStrength() - flags, _ := ap.GetPropertyFlags() - wpaFlags, _ := ap.GetPropertyWPAFlags() - rsnFlags, _ := ap.GetPropertyRSNFlags() - freq, _ := ap.GetPropertyFrequency() - maxBitrate, _ := ap.GetPropertyMaxBitrate() - apBSSID, _ := ap.GetPropertyHWAddress() - mode, _ := ap.GetPropertyMode() - - secured := flags != uint32(gonetworkmanager.Nm80211APFlagsNone) || - wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) || - rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone) - - enterprise := (rsnFlags&uint32(gonetworkmanager.Nm80211APSecKeyMgmt8021X) != 0) || - (wpaFlags&uint32(gonetworkmanager.Nm80211APSecKeyMgmt8021X) != 0) - - var modeStr string - switch mode { - case gonetworkmanager.Nm80211ModeAdhoc: - modeStr = "adhoc" - case gonetworkmanager.Nm80211ModeInfra: - modeStr = "infrastructure" - case gonetworkmanager.Nm80211ModeAp: - modeStr = "ap" - default: - modeStr = "unknown" - } - - channel := frequencyToChannel(freq) - - isConnected := connected && apSSID == ssid - rate := maxBitrate / 1000 - if isConnected { - if devBitrate, err := devInfo.wireless.GetPropertyBitrate(); err == nil && devBitrate > 0 { - rate = devBitrate / 1000 - } - } - - network := WiFiNetwork{ - SSID: apSSID, - BSSID: apBSSID, - Signal: strength, - Secured: secured, - Enterprise: enterprise, - Connected: isConnected, - Saved: savedSSIDs[apSSID], - Autoconnect: autoconnectMap[apSSID], - Hidden: hiddenSSIDs[apSSID], - Frequency: freq, - Mode: modeStr, - Rate: rate, - Channel: channel, - Device: name, - } - - seenSSIDs[apSSID] = &network - networks = append(networks, network) - } - - if connected && ssid != "" { - if _, exists := seenSSIDs[ssid]; !exists { - hiddenNetwork := WiFiNetwork{ - SSID: ssid, - BSSID: bssid, - Signal: signal, - Secured: true, - Connected: true, - Saved: savedSSIDs[ssid], - Autoconnect: autoconnectMap[ssid], - Hidden: true, - Mode: "infrastructure", - Device: name, - } - networks = append(networks, hiddenNetwork) - } - } - - sortWiFiNetworks(networks) - } - - devices = append(devices, WiFiDevice{ - Name: name, - HwAddress: devInfo.hwAddress, - State: stateStr, - Connected: connected, - SSID: ssid, - BSSID: bssid, - Signal: signal, - IP: ip, - Networks: networks, - }) - } - - sort.Slice(devices, func(i, j int) bool { - return devices[i].Name < devices[j].Name - }) - - b.stateMutex.Lock() - b.state.WiFiDevices = devices - b.stateMutex.Unlock() -} - -func (b *NetworkManagerBackend) getWifiDeviceForConnection(deviceName string) (*wifiDeviceInfo, error) { - if deviceName != "" { - devInfo, ok := b.wifiDevices[deviceName] - if !ok { - return nil, fmt.Errorf("WiFi device not found: %s", deviceName) - } - return devInfo, nil - } - - if b.wifiDevice == nil { - return nil, fmt.Errorf("no WiFi device available") - } - - dev := b.wifiDevice.(gonetworkmanager.Device) - iface, _ := dev.GetPropertyInterface() - if devInfo, ok := b.wifiDevices[iface]; ok { - return devInfo, nil - } - - return nil, fmt.Errorf("no WiFi device available") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_wifi_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_wifi_test.go deleted file mode 100644 index 79c2c5b..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/backend_networkmanager_wifi_test.go +++ /dev/null @@ -1,202 +0,0 @@ -package network - -import ( - "testing" - - mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2" - "github.com/stretchr/testify/assert" -) - -func TestNetworkManagerBackend_GetWiFiEnabled(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - mockNM.EXPECT().GetPropertyWirelessEnabled().Return(true, nil) - - enabled, err := backend.GetWiFiEnabled() - assert.NoError(t, err) - assert.True(t, enabled) -} - -func TestNetworkManagerBackend_SetWiFiEnabled(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - mockNM.EXPECT().SetPropertyWirelessEnabled(true).Return(nil) - - err = backend.SetWiFiEnabled(true) - assert.NoError(t, err) - - backend.stateMutex.RLock() - assert.True(t, backend.state.WiFiEnabled) - backend.stateMutex.RUnlock() -} - -func TestNetworkManagerBackend_ScanWiFi_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.wifiDevice = nil - err = backend.ScanWiFi() - assert.Error(t, err) - assert.Contains(t, err.Error(), "no WiFi device available") -} - -func TestNetworkManagerBackend_ScanWiFi_Disabled(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - mockDeviceWireless := mock_gonetworkmanager.NewMockDeviceWireless(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.wifiDevice = mockDeviceWireless - backend.wifiDev = mockDeviceWireless - - backend.stateMutex.Lock() - backend.state.WiFiEnabled = false - backend.stateMutex.Unlock() - - err = backend.ScanWiFi() - assert.Error(t, err) - assert.Contains(t, err.Error(), "WiFi is disabled") -} - -func TestNetworkManagerBackend_GetWiFiNetworkDetails_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.wifiDevice = nil - _, err = backend.GetWiFiNetworkDetails("TestNetwork") - assert.Error(t, err) - assert.Contains(t, err.Error(), "no WiFi device available") -} - -func TestNetworkManagerBackend_ConnectWiFi_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.wifiDevice = nil - req := ConnectionRequest{SSID: "TestNetwork", Password: "password"} - err = backend.ConnectWiFi(req) - assert.Error(t, err) - assert.Contains(t, err.Error(), "no WiFi device available") -} - -func TestNetworkManagerBackend_ConnectWiFi_AlreadyConnected(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - mockDeviceWireless := mock_gonetworkmanager.NewMockDeviceWireless(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.wifiDevice = mockDeviceWireless - backend.wifiDev = mockDeviceWireless - backend.wifiDevices = map[string]*wifiDeviceInfo{ - "wlan0": { - device: nil, - wireless: mockDeviceWireless, - name: "wlan0", - hwAddress: "00:11:22:33:44:55", - }, - } - - mockDeviceWireless.EXPECT().GetPropertyInterface().Return("wlan0", nil) - - backend.stateMutex.Lock() - backend.state.WiFiConnected = true - backend.state.WiFiSSID = "TestNetwork" - backend.state.WiFiDevice = "wlan0" - backend.stateMutex.Unlock() - - req := ConnectionRequest{SSID: "TestNetwork", Password: "password"} - err = backend.ConnectWiFi(req) - assert.NoError(t, err) -} - -func TestNetworkManagerBackend_DisconnectWiFi_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.wifiDevice = nil - err = backend.DisconnectWiFi() - assert.Error(t, err) - assert.Contains(t, err.Error(), "no WiFi device available") -} - -func TestNetworkManagerBackend_IsConnectingTo(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.stateMutex.Lock() - backend.state.IsConnecting = true - backend.state.ConnectingSSID = "TestNetwork" - backend.stateMutex.Unlock() - - assert.True(t, backend.IsConnectingTo("TestNetwork")) - assert.False(t, backend.IsConnectingTo("OtherNetwork")) -} - -func TestNetworkManagerBackend_IsConnectingTo_NotConnecting(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.stateMutex.Lock() - backend.state.IsConnecting = false - backend.state.ConnectingSSID = "" - backend.stateMutex.Unlock() - - assert.False(t, backend.IsConnectingTo("TestNetwork")) -} - -func TestNetworkManagerBackend_UpdateWiFiNetworks_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.wifiDevice = nil - _, err = backend.updateWiFiNetworks() - assert.Error(t, err) - assert.Contains(t, err.Error(), "no WiFi device available") -} - -func TestNetworkManagerBackend_FindConnection_NoSettings(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.settings = nil - _, err = backend.findConnection("NonExistentNetwork") - assert.Error(t, err) -} - -func TestNetworkManagerBackend_CreateAndConnectWiFi_NoDevice(t *testing.T) { - mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) - - backend, err := NewNetworkManagerBackend(mockNM) - assert.NoError(t, err) - - backend.wifiDevice = nil - backend.wifiDev = nil - req := ConnectionRequest{SSID: "TestNetwork", Password: "password"} - err = backend.createAndConnectWiFi(req) - assert.Error(t, err) - assert.Contains(t, err.Error(), "no WiFi device available") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/broker.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/broker.go deleted file mode 100644 index 5290fb5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/broker.go +++ /dev/null @@ -1,22 +0,0 @@ -package network - -import ( - "context" - "crypto/rand" - "encoding/hex" -) - -type PromptBroker interface { - Ask(ctx context.Context, req PromptRequest) (token string, err error) - Wait(ctx context.Context, token string) (PromptReply, error) - Resolve(token string, reply PromptReply) error - Cancel(path string, setting string) error -} - -func generateToken() (string, error) { - bytes := make([]byte, 16) - if _, err := rand.Read(bytes); err != nil { - return "", err - } - return hex.EncodeToString(bytes), nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/connection_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/connection_test.go deleted file mode 100644 index 7b37731..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/connection_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package network_test - -import ( - "errors" - "testing" - - mocks_network "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/network" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" - "github.com/stretchr/testify/assert" -) - -func TestConnectionRequest_Validation(t *testing.T) { - t.Run("basic WiFi connection", func(t *testing.T) { - req := network.ConnectionRequest{ - SSID: "TestNetwork", - Password: "testpass123", - } - - assert.NotEmpty(t, req.SSID) - assert.NotEmpty(t, req.Password) - assert.Empty(t, req.Username) - }) - - t.Run("enterprise WiFi connection", func(t *testing.T) { - req := network.ConnectionRequest{ - SSID: "EnterpriseNetwork", - Password: "testpass123", - Username: "testuser", - } - - assert.NotEmpty(t, req.SSID) - assert.NotEmpty(t, req.Password) - assert.NotEmpty(t, req.Username) - }) - - t.Run("open WiFi connection", func(t *testing.T) { - req := network.ConnectionRequest{ - SSID: "OpenNetwork", - } - - assert.NotEmpty(t, req.SSID) - assert.Empty(t, req.Password) - assert.Empty(t, req.Username) - }) -} - -func TestManager_ConnectWiFi_NoDevice(t *testing.T) { - backend := mocks_network.NewMockBackend(t) - req := network.ConnectionRequest{ - SSID: "TestNetwork", - Password: "testpass123", - } - backend.EXPECT().ConnectWiFi(req).Return(errors.New("no WiFi device available")) - - manager := network.NewTestManager(backend, &network.NetworkState{}) - - err := manager.ConnectWiFi(req) - assert.Error(t, err) - assert.Contains(t, err.Error(), "no WiFi device available") -} - -func TestManager_DisconnectWiFi_NoDevice(t *testing.T) { - backend := mocks_network.NewMockBackend(t) - backend.EXPECT().DisconnectWiFi().Return(errors.New("no WiFi device available")) - - manager := network.NewTestManager(backend, &network.NetworkState{}) - - err := manager.DisconnectWiFi() - assert.Error(t, err) - assert.Contains(t, err.Error(), "no WiFi device available") -} - -func TestManager_ForgetWiFiNetwork_NotFound(t *testing.T) { - backend := mocks_network.NewMockBackend(t) - backend.EXPECT().ForgetWiFiNetwork("NonExistentNetwork").Return(errors.New("connection not found")) - - manager := network.NewTestManager(backend, &network.NetworkState{}) - - err := manager.ForgetWiFiNetwork("NonExistentNetwork") - assert.Error(t, err) - assert.Contains(t, err.Error(), "connection not found") -} - -func TestManager_ConnectEthernet_NoDevice(t *testing.T) { - backend := mocks_network.NewMockBackend(t) - backend.EXPECT().ConnectEthernet().Return(errors.New("no ethernet device available")) - - manager := network.NewTestManager(backend, &network.NetworkState{}) - - err := manager.ConnectEthernet() - assert.Error(t, err) - assert.Contains(t, err.Error(), "no ethernet device available") -} - -func TestManager_DisconnectEthernet_NoDevice(t *testing.T) { - backend := mocks_network.NewMockBackend(t) - backend.EXPECT().DisconnectEthernet().Return(errors.New("no ethernet device available")) - - manager := network.NewTestManager(backend, &network.NetworkState{}) - - err := manager.DisconnectEthernet() - assert.Error(t, err) - assert.Contains(t, err.Error(), "no ethernet device available") -} - -// Note: More comprehensive tests for connection operations would require -// mocking the NetworkManager D-Bus interfaces, which is beyond the scope -// of these unit tests. The tests above cover the basic error cases and -// validation logic. Integration tests would be needed for full coverage. diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/detect.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/detect.go deleted file mode 100644 index 6031e42..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/detect.go +++ /dev/null @@ -1,89 +0,0 @@ -package network - -import ( - "fmt" - - "github.com/godbus/dbus/v5" -) - -type BackendType int - -const ( - BackendNone BackendType = iota - BackendNetworkManager - BackendIwd - BackendConnMan - BackendNetworkd -) - -func nameHasOwner(bus *dbus.Conn, name string) (bool, error) { - obj := bus.Object("org.freedesktop.DBus", "/org/freedesktop/DBus") - var owned bool - if err := obj.Call("org.freedesktop.DBus.NameHasOwner", 0, name).Store(&owned); err != nil { - return false, err - } - return owned, nil -} - -type DetectResult struct { - Backend BackendType - HasNM bool - HasIwd bool - HasConnMan bool - HasWpaSupp bool - HasNetworkd bool - ChosenReason string -} - -func DetectNetworkStack() (*DetectResult, error) { - bus, err := dbus.ConnectSystemBus() - if err != nil { - return nil, fmt.Errorf("connect system bus: %w", err) - } - defer bus.Close() - - hasNM, _ := nameHasOwner(bus, "org.freedesktop.NetworkManager") - hasIwd, _ := nameHasOwner(bus, "net.connman.iwd") - hasConn, _ := nameHasOwner(bus, "net.connman") - hasWpa, _ := nameHasOwner(bus, "fi.w1.wpa_supplicant1") - hasNetworkd, _ := nameHasOwner(bus, "org.freedesktop.network1") - - res := &DetectResult{ - HasNM: hasNM, - HasIwd: hasIwd, - HasConnMan: hasConn, - HasWpaSupp: hasWpa, - HasNetworkd: hasNetworkd, - } - - switch { - case hasNM: - res.Backend = BackendNetworkManager - if hasIwd { - res.ChosenReason = "NetworkManager present; iwd also running (likely NM's Wi-Fi backend). Using NM API." - } else { - res.ChosenReason = "NetworkManager present. Using NM API." - } - case hasConn && hasIwd: - res.Backend = BackendConnMan - res.ChosenReason = "ConnMan + iwd detected. Use ConnMan API (iwd is its Wi-Fi daemon)." - case hasIwd && hasNetworkd: - res.Backend = BackendNetworkd - res.ChosenReason = "iwd + systemd-networkd detected. Using iwd for Wi-Fi association and networkd for IP/DHCP." - case hasIwd: - res.Backend = BackendIwd - res.ChosenReason = "iwd detected without NM/ConnMan. Using iwd API." - case hasNetworkd: - res.Backend = BackendNetworkd - res.ChosenReason = "systemd-networkd detected (no NM/ConnMan). Using networkd for L3 and wired." - default: - res.Backend = BackendNone - if hasWpa { - res.ChosenReason = "No NM/ConnMan/iwd; wpa_supplicant present. Consider a wpa_supplicant path." - } else { - res.ChosenReason = "No known network manager bus names found." - } - } - - return res, nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/detect_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/detect_test.go deleted file mode 100644 index 87e7635..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/detect_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package network - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBackendType_Constants(t *testing.T) { - assert.Equal(t, BackendType(0), BackendNone) - assert.Equal(t, BackendType(1), BackendNetworkManager) - assert.Equal(t, BackendType(2), BackendIwd) - assert.Equal(t, BackendType(3), BackendConnMan) - assert.Equal(t, BackendType(4), BackendNetworkd) -} - -func TestDetectResult_HasNetworkdField(t *testing.T) { - result := &DetectResult{ - Backend: BackendNetworkd, - HasNetworkd: true, - HasIwd: true, - } - - assert.True(t, result.HasNetworkd) - assert.True(t, result.HasIwd) - assert.Equal(t, BackendNetworkd, result.Backend) -} - -func TestDetectNetworkStack_Integration(t *testing.T) { - result, err := DetectNetworkStack() - - if err != nil && strings.Contains(err.Error(), "connect system bus") { - t.Skipf("system D-Bus unavailable: %v", err) - } - - assert.NoError(t, err) - if assert.NotNil(t, result) { - assert.NotEmpty(t, result.ChosenReason) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/handlers.go deleted file mode 100644 index 2b3e75e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/handlers.go +++ /dev/null @@ -1,630 +0,0 @@ -package network - -import ( - "encoding/json" - "fmt" - "net" - "os" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - switch req.Method { - case "network.getState": - handleGetState(conn, req, manager) - case "network.wifi.scan": - handleScanWiFi(conn, req, manager) - case "network.wifi.networks": - handleGetWiFiNetworks(conn, req, manager) - case "network.wifi.connect": - handleConnectWiFi(conn, req, manager) - case "network.wifi.disconnect": - handleDisconnectWiFi(conn, req, manager) - case "network.wifi.forget": - handleForgetWiFi(conn, req, manager) - case "network.wifi.toggle": - handleToggleWiFi(conn, req, manager) - case "network.wifi.enable": - handleEnableWiFi(conn, req, manager) - case "network.wifi.disable": - handleDisableWiFi(conn, req, manager) - case "network.ethernet.connect.config": - handleConnectEthernetSpecificConfig(conn, req, manager) - case "network.ethernet.connect": - handleConnectEthernet(conn, req, manager) - case "network.ethernet.disconnect": - handleDisconnectEthernet(conn, req, manager) - case "network.preference.set": - handleSetPreference(conn, req, manager) - case "network.info": - handleGetNetworkInfo(conn, req, manager) - case "network.qrcode": - handleGetNetworkQRCode(conn, req, manager) - case "network.delete-qrcode": - handleDeleteQRCode(conn, req, manager) - case "network.ethernet.info": - handleGetWiredNetworkInfo(conn, req, manager) - case "network.subscribe": - handleSubscribe(conn, req, manager) - case "network.credentials.submit": - handleCredentialsSubmit(conn, req, manager) - case "network.credentials.cancel": - handleCredentialsCancel(conn, req, manager) - case "network.vpn.profiles": - handleListVPNProfiles(conn, req, manager) - case "network.vpn.active": - handleListActiveVPN(conn, req, manager) - case "network.vpn.connect": - handleConnectVPN(conn, req, manager) - case "network.vpn.disconnect": - handleDisconnectVPN(conn, req, manager) - case "network.vpn.disconnectAll": - handleDisconnectAllVPN(conn, req, manager) - case "network.vpn.clearCredentials": - handleClearVPNCredentials(conn, req, manager) - case "network.vpn.plugins": - handleListVPNPlugins(conn, req, manager) - case "network.vpn.import": - handleImportVPN(conn, req, manager) - case "network.vpn.getConfig": - handleGetVPNConfig(conn, req, manager) - case "network.vpn.updateConfig": - handleUpdateVPNConfig(conn, req, manager) - case "network.vpn.delete": - handleDeleteVPN(conn, req, manager) - case "network.vpn.setCredentials": - handleSetVPNCredentials(conn, req, manager) - case "network.wifi.setAutoconnect": - handleSetWiFiAutoconnect(conn, req, manager) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleCredentialsSubmit(conn net.Conn, req models.Request, manager *Manager) { - token, err := params.String(req.Params, "token") - if err != nil { - log.Warnf("handleCredentialsSubmit: missing or invalid token parameter") - models.RespondError(conn, req.ID, err.Error()) - return - } - - secrets, err := params.StringMap(req.Params, "secrets") - if err != nil { - log.Warnf("handleCredentialsSubmit: missing or invalid secrets parameter") - models.RespondError(conn, req.ID, err.Error()) - return - } - - save := params.BoolOpt(req.Params, "save", true) - - if err := manager.SubmitCredentials(token, secrets, save); err != nil { - log.Warnf("handleCredentialsSubmit: failed to submit credentials: %v", err) - models.RespondError(conn, req.ID, err.Error()) - return - } - - log.Infof("handleCredentialsSubmit: credentials submitted successfully") - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "credentials submitted"}) -} - -func handleCredentialsCancel(conn net.Conn, req models.Request, manager *Manager) { - token, err := params.String(req.Params, "token") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.CancelCredentials(token); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "credentials cancelled"}) -} - -func handleGetState(conn net.Conn, req models.Request, manager *Manager) { - models.Respond(conn, req.ID, manager.GetState()) -} - -func handleScanWiFi(conn net.Conn, req models.Request, manager *Manager) { - device := params.StringOpt(req.Params, "device", "") - var err error - if device != "" { - err = manager.ScanWiFiDevice(device) - } else { - err = manager.ScanWiFi() - } - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "scanning"}) -} - -func handleGetWiFiNetworks(conn net.Conn, req models.Request, manager *Manager) { - models.Respond(conn, req.ID, manager.GetWiFiNetworks()) -} - -func handleConnectWiFi(conn net.Conn, req models.Request, manager *Manager) { - ssid, err := params.String(req.Params, "ssid") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - var connReq ConnectionRequest - connReq.SSID = ssid - connReq.Password = params.StringOpt(req.Params, "password", "") - connReq.Username = params.StringOpt(req.Params, "username", "") - connReq.Device = params.StringOpt(req.Params, "device", "") - - if interactive, ok := models.Get[bool](req, "interactive"); ok { - connReq.Interactive = interactive - } else { - state := manager.GetState() - alreadyConnected := state.WiFiConnected && state.WiFiSSID == ssid - - if alreadyConnected && connReq.Device == "" { - connReq.Interactive = false - } else { - networkInfo, err := manager.GetNetworkInfo(ssid) - isSaved := err == nil && networkInfo.Saved - - if isSaved { - connReq.Interactive = false - } else if err == nil && networkInfo.Secured && connReq.Password == "" && connReq.Username == "" { - connReq.Interactive = true - } - } - } - - connReq.AnonymousIdentity = params.StringOpt(req.Params, "anonymousIdentity", "") - connReq.DomainSuffixMatch = params.StringOpt(req.Params, "domainSuffixMatch", "") - connReq.EAPMethod = params.StringOpt(req.Params, "eapMethod", "") - connReq.Phase2Auth = params.StringOpt(req.Params, "phase2Auth", "") - connReq.CACertPath = params.StringOpt(req.Params, "caCertPath", "") - connReq.ClientCertPath = params.StringOpt(req.Params, "clientCertPath", "") - connReq.PrivateKeyPath = params.StringOpt(req.Params, "privateKeyPath", "") - - if useSystemCACerts, ok := models.Get[bool](req, "useSystemCACerts"); ok { - connReq.UseSystemCACerts = &useSystemCACerts - } - - if err := manager.ConnectWiFi(connReq); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"}) -} - -func handleDisconnectWiFi(conn net.Conn, req models.Request, manager *Manager) { - device := params.StringOpt(req.Params, "device", "") - var err error - if device != "" { - err = manager.DisconnectWiFiDevice(device) - } else { - err = manager.DisconnectWiFi() - } - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"}) -} - -func handleForgetWiFi(conn net.Conn, req models.Request, manager *Manager) { - ssid, err := params.String(req.Params, "ssid") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.ForgetWiFiNetwork(ssid); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "forgotten"}) -} - -func handleToggleWiFi(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.ToggleWiFi(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - state := manager.GetState() - models.Respond(conn, req.ID, map[string]bool{"enabled": state.WiFiEnabled}) -} - -func handleEnableWiFi(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.EnableWiFi(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, map[string]bool{"enabled": true}) -} - -func handleDisableWiFi(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.DisableWiFi(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, map[string]bool{"enabled": false}) -} - -func handleConnectEthernetSpecificConfig(conn net.Conn, req models.Request, manager *Manager) { - uuid, err := params.String(req.Params, "uuid") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - if err := manager.activateConnection(uuid); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"}) -} - -func handleConnectEthernet(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.ConnectEthernet(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"}) -} - -func handleDisconnectEthernet(conn net.Conn, req models.Request, manager *Manager) { - device := params.StringOpt(req.Params, "device", "") - var err error - if device != "" { - err = manager.DisconnectEthernetDevice(device) - } else { - err = manager.DisconnectEthernet() - } - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"}) -} - -func handleSetPreference(conn net.Conn, req models.Request, manager *Manager) { - preference, err := params.String(req.Params, "preference") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetConnectionPreference(ConnectionPreference(preference)); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, map[string]string{"preference": preference}) -} - -func handleGetNetworkInfo(conn net.Conn, req models.Request, manager *Manager) { - ssid, err := params.String(req.Params, "ssid") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - network, err := manager.GetNetworkInfoDetailed(ssid) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, network) -} - -func handleGetNetworkQRCode(conn net.Conn, req models.Request, manager *Manager) { - ssid, err := params.String(req.Params, "ssid") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - content, err := manager.GetNetworkQRCode(ssid) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, content) -} - -func handleDeleteQRCode(conn net.Conn, req models.Request, _ *Manager) { - path, err := params.String(req.Params, "path") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if !isValidQRCodePath(path) { - models.RespondError(conn, req.ID, "invalid QR code path") - return - } - - if err := os.Remove(path); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "QR code file deleted"}) -} - -func handleGetWiredNetworkInfo(conn net.Conn, req models.Request, manager *Manager) { - uuid, err := params.String(req.Params, "uuid") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - network, err := manager.GetWiredNetworkInfoDetailed(uuid) - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, network) -} - -func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) { - clientID := fmt.Sprintf("client-%p", conn) - stateChan := manager.Subscribe(clientID) - defer manager.Unsubscribe(clientID) - - initialState := manager.GetState() - event := NetworkEvent{ - Type: EventStateChanged, - Data: initialState, - } - if err := json.NewEncoder(conn).Encode(models.Response[NetworkEvent]{ - ID: req.ID, - Result: &event, - }); err != nil { - return - } - - for state := range stateChan { - event := NetworkEvent{ - Type: EventStateChanged, - Data: state, - } - if err := json.NewEncoder(conn).Encode(models.Response[NetworkEvent]{ - Result: &event, - }); err != nil { - return - } - } -} - -func handleListVPNProfiles(conn net.Conn, req models.Request, manager *Manager) { - profiles, err := manager.ListVPNProfiles() - if err != nil { - log.Warnf("handleListVPNProfiles: failed to list profiles: %v", err) - models.RespondError(conn, req.ID, fmt.Sprintf("failed to list VPN profiles: %v", err)) - return - } - - models.Respond(conn, req.ID, profiles) -} - -func handleListActiveVPN(conn net.Conn, req models.Request, manager *Manager) { - active, err := manager.ListActiveVPN() - if err != nil { - log.Warnf("handleListActiveVPN: failed to list active VPNs: %v", err) - models.RespondError(conn, req.ID, fmt.Sprintf("failed to list active VPNs: %v", err)) - return - } - - models.Respond(conn, req.ID, active) -} - -func handleConnectVPN(conn net.Conn, req models.Request, manager *Manager) { - uuidOrName, ok := params.StringAlt(req.Params, "uuidOrName", "name", "uuid") - if !ok { - log.Warnf("handleConnectVPN: missing uuidOrName/name/uuid parameter") - models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter") - return - } - - singleActive := params.BoolOpt(req.Params, "singleActive", true) - - if err := manager.ConnectVPN(uuidOrName, singleActive); err != nil { - log.Warnf("handleConnectVPN: failed to connect: %v", err) - models.RespondError(conn, req.ID, fmt.Sprintf("failed to connect VPN: %v", err)) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN connection initiated"}) -} - -func handleDisconnectVPN(conn net.Conn, req models.Request, manager *Manager) { - uuidOrName, ok := params.StringAlt(req.Params, "uuidOrName", "name", "uuid") - if !ok { - log.Warnf("handleDisconnectVPN: missing uuidOrName/name/uuid parameter") - models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter") - return - } - - if err := manager.DisconnectVPN(uuidOrName); err != nil { - log.Warnf("handleDisconnectVPN: failed to disconnect: %v", err) - models.RespondError(conn, req.ID, fmt.Sprintf("failed to disconnect VPN: %v", err)) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN disconnected"}) -} - -func handleDisconnectAllVPN(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.DisconnectAllVPN(); err != nil { - log.Warnf("handleDisconnectAllVPN: failed: %v", err) - models.RespondError(conn, req.ID, fmt.Sprintf("failed to disconnect all VPNs: %v", err)) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "All VPNs disconnected"}) -} - -func handleClearVPNCredentials(conn net.Conn, req models.Request, manager *Manager) { - uuidOrName, ok := params.StringAlt(req.Params, "uuid", "name", "uuidOrName") - if !ok { - log.Warnf("handleClearVPNCredentials: missing uuidOrName/name/uuid parameter") - models.RespondError(conn, req.ID, "missing uuidOrName/name/uuid parameter") - return - } - - if err := manager.ClearVPNCredentials(uuidOrName); err != nil { - log.Warnf("handleClearVPNCredentials: failed: %v", err) - models.RespondError(conn, req.ID, fmt.Sprintf("failed to clear VPN credentials: %v", err)) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN credentials cleared"}) -} - -func handleSetWiFiAutoconnect(conn net.Conn, req models.Request, manager *Manager) { - ssid, err := params.String(req.Params, "ssid") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - autoconnect, err := params.Bool(req.Params, "autoconnect") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetWiFiAutoconnect(ssid, autoconnect); err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to set autoconnect: %v", err)) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "autoconnect updated"}) -} - -func handleListVPNPlugins(conn net.Conn, req models.Request, manager *Manager) { - plugins, err := manager.ListVPNPlugins() - if err != nil { - log.Warnf("handleListVPNPlugins: failed to list plugins: %v", err) - models.RespondError(conn, req.ID, fmt.Sprintf("failed to list VPN plugins: %v", err)) - return - } - - models.Respond(conn, req.ID, plugins) -} - -func handleImportVPN(conn net.Conn, req models.Request, manager *Manager) { - filePath, ok := params.StringAlt(req.Params, "file", "path") - if !ok { - models.RespondError(conn, req.ID, "missing 'file' or 'path' parameter") - return - } - - name := params.StringOpt(req.Params, "name", "") - - result, err := manager.ImportVPN(filePath, name) - if err != nil { - log.Warnf("handleImportVPN: failed to import: %v", err) - models.RespondError(conn, req.ID, fmt.Sprintf("failed to import VPN: %v", err)) - return - } - - models.Respond(conn, req.ID, result) -} - -func handleGetVPNConfig(conn net.Conn, req models.Request, manager *Manager) { - uuidOrName, ok := params.StringAlt(req.Params, "uuid", "name", "uuidOrName") - if !ok { - models.RespondError(conn, req.ID, "missing 'uuid', 'name', or 'uuidOrName' parameter") - return - } - - config, err := manager.GetVPNConfig(uuidOrName) - if err != nil { - log.Warnf("handleGetVPNConfig: failed to get config: %v", err) - models.RespondError(conn, req.ID, fmt.Sprintf("failed to get VPN config: %v", err)) - return - } - - models.Respond(conn, req.ID, config) -} - -func handleUpdateVPNConfig(conn net.Conn, req models.Request, manager *Manager) { - connUUID, err := params.String(req.Params, "uuid") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - updates := make(map[string]any) - - if name, ok := models.Get[string](req, "name"); ok { - updates["name"] = name - } - if autoconnect, ok := models.Get[bool](req, "autoconnect"); ok { - updates["autoconnect"] = autoconnect - } - if data, ok := models.Get[map[string]any](req, "data"); ok { - updates["data"] = data - } - - if len(updates) == 0 { - models.RespondError(conn, req.ID, "no updates provided") - return - } - - if err := manager.UpdateVPNConfig(connUUID, updates); err != nil { - log.Warnf("handleUpdateVPNConfig: failed to update: %v", err) - models.RespondError(conn, req.ID, fmt.Sprintf("failed to update VPN config: %v", err)) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN config updated"}) -} - -func handleDeleteVPN(conn net.Conn, req models.Request, manager *Manager) { - uuidOrName, ok := params.StringAlt(req.Params, "uuid", "name", "uuidOrName") - if !ok { - models.RespondError(conn, req.ID, "missing 'uuid', 'name', or 'uuidOrName' parameter") - return - } - - if err := manager.DeleteVPN(uuidOrName); err != nil { - log.Warnf("handleDeleteVPN: failed to delete: %v", err) - models.RespondError(conn, req.ID, fmt.Sprintf("failed to delete VPN: %v", err)) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN deleted"}) -} - -func handleSetVPNCredentials(conn net.Conn, req models.Request, manager *Manager) { - connUUID, err := params.String(req.Params, "uuid") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - username := params.StringOpt(req.Params, "username", "") - password := params.StringOpt(req.Params, "password", "") - save := params.BoolOpt(req.Params, "save", true) - - if err := manager.SetVPNCredentials(connUUID, username, password, save); err != nil { - log.Warnf("handleSetVPNCredentials: failed to set credentials: %v", err) - models.RespondError(conn, req.ID, fmt.Sprintf("failed to set VPN credentials: %v", err)) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN credentials set"}) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/handlers_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/handlers_test.go deleted file mode 100644 index ea8ae33..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/handlers_test.go +++ /dev/null @@ -1,269 +0,0 @@ -package network - -import ( - "bytes" - "encoding/json" - "net" - "testing" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type mockNetConn struct { - net.Conn - readBuf *bytes.Buffer - writeBuf *bytes.Buffer - closed bool -} - -func newMockNetConn() *mockNetConn { - return &mockNetConn{ - readBuf: &bytes.Buffer{}, - writeBuf: &bytes.Buffer{}, - } -} - -func (m *mockNetConn) Read(b []byte) (n int, err error) { - return m.readBuf.Read(b) -} - -func (m *mockNetConn) Write(b []byte) (n int, err error) { - return m.writeBuf.Write(b) -} - -func (m *mockNetConn) Close() error { - m.closed = true - return nil -} - -func TestRespondError_Network(t *testing.T) { - conn := newMockNetConn() - models.RespondError(conn, 123, "test error") - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Equal(t, "test error", resp.Error) - assert.Nil(t, resp.Result) -} - -func TestRespond_Network(t *testing.T) { - conn := newMockNetConn() - result := models.SuccessResult{Success: true, Message: "test"} - models.Respond(conn, 123, result) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "test", resp.Result.Message) -} - -func TestHandleGetState(t *testing.T) { - manager := &Manager{ - state: &NetworkState{ - NetworkStatus: StatusWiFi, - WiFiSSID: "TestNetwork", - WiFiConnected: true, - }, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "network.getState"} - - handleGetState(conn, req, manager) - - var resp models.Response[NetworkState] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.Equal(t, StatusWiFi, resp.Result.NetworkStatus) - assert.Equal(t, "TestNetwork", resp.Result.WiFiSSID) -} - -func TestHandleGetWiFiNetworks(t *testing.T) { - manager := &Manager{ - state: &NetworkState{ - WiFiNetworks: []WiFiNetwork{ - {SSID: "Network1", Signal: 90}, - {SSID: "Network2", Signal: 80}, - }, - }, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "network.wifi.networks"} - - handleGetWiFiNetworks(conn, req, manager) - - var resp models.Response[[]WiFiNetwork] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.Len(t, *resp.Result, 2) - assert.Equal(t, "Network1", (*resp.Result)[0].SSID) -} - -func TestHandleConnectWiFi(t *testing.T) { - t.Run("missing ssid parameter", func(t *testing.T) { - manager := &Manager{ - state: &NetworkState{}, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "network.wifi.connect", - Params: map[string]any{}, - } - - handleConnectWiFi(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "missing or invalid 'ssid' parameter") - }) -} - -func TestHandleSetPreference(t *testing.T) { - t.Run("missing preference parameter", func(t *testing.T) { - manager := &Manager{ - state: &NetworkState{}, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "network.preference.set", - Params: map[string]any{}, - } - - handleSetPreference(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "missing or invalid 'preference' parameter") - }) -} - -func TestHandleGetNetworkInfo(t *testing.T) { - t.Run("missing ssid parameter", func(t *testing.T) { - manager := &Manager{ - state: &NetworkState{}, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "network.info", - Params: map[string]any{}, - } - - handleGetNetworkInfo(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "missing or invalid 'ssid' parameter") - }) -} - -func TestHandleRequest(t *testing.T) { - manager := &Manager{ - state: &NetworkState{ - NetworkStatus: StatusWiFi, - }, - } - - t.Run("unknown method", func(t *testing.T) { - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "network.unknown", - } - - HandleRequest(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "unknown method") - }) - - t.Run("valid method - getState", func(t *testing.T) { - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "network.getState", - } - - HandleRequest(conn, req, manager) - - var resp models.Response[NetworkState] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - }) -} - -func TestHandleSubscribe(t *testing.T) { - // This test is complex due to the streaming nature of subscriptions - // Better suited as an integration test - t.Skip("Subscription test requires connection lifecycle management - integration test needed") -} - -func TestManager_Subscribe_Unsubscribe(t *testing.T) { - manager := &Manager{ - state: &NetworkState{}, - } - - t.Run("subscribe creates channel", func(t *testing.T) { - ch := manager.Subscribe("client1") - assert.NotNil(t, ch) - count := 0 - manager.subscribers.Range(func(key string, ch chan NetworkState) bool { - count++ - return true - }) - assert.Equal(t, 1, count) - }) - - t.Run("unsubscribe removes channel", func(t *testing.T) { - manager.Unsubscribe("client1") - count := 0 - manager.subscribers.Range(func(key string, ch chan NetworkState) bool { count++; return true }) - assert.Equal(t, 0, count) - }) - - t.Run("unsubscribe non-existent client is safe", func(t *testing.T) { - assert.NotPanics(t, func() { - manager.Unsubscribe("non-existent") - }) - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/helpers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/helpers.go deleted file mode 100644 index 4669f52..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/helpers.go +++ /dev/null @@ -1,53 +0,0 @@ -package network - -import "sort" - -func frequencyToChannel(freq uint32) uint32 { - if freq >= 2412 && freq <= 2484 { - if freq == 2484 { - return 14 - } - return (freq-2412)/5 + 1 - } - - if freq >= 5170 && freq <= 5825 { - return (freq-5170)/5 + 34 - } - - if freq >= 5955 && freq <= 7115 { - return (freq-5955)/5 + 1 - } - - return 0 -} - -func sortWiFiNetworks(networks []WiFiNetwork) { - sort.Slice(networks, func(i, j int) bool { - if networks[i].Connected && !networks[j].Connected { - return true - } - if !networks[i].Connected && networks[j].Connected { - return false - } - - if networks[i].Saved && !networks[j].Saved { - return true - } - if !networks[i].Saved && networks[j].Saved { - return false - } - - if !networks[i].Secured && networks[j].Secured { - if networks[i].Signal >= 50 { - return true - } - } - if networks[i].Secured && !networks[j].Secured { - if networks[j].Signal >= 50 { - return false - } - } - - return networks[i].Signal > networks[j].Signal - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/manager.go deleted file mode 100644 index 5c6657e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/manager.go +++ /dev/null @@ -1,626 +0,0 @@ -package network - -import ( - "fmt" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/yeqown/go-qrcode/v2" - "github.com/yeqown/go-qrcode/writer/standard" -) - -func NewManager() (*Manager, error) { - detection, err := DetectNetworkStack() - if err != nil { - return nil, fmt.Errorf("failed to detect network stack: %w", err) - } - - log.Infof("Network backend detection: %s", detection.ChosenReason) - - var backend Backend - switch detection.Backend { - case BackendNetworkManager: - nm, err := NewNetworkManagerBackend() - if err != nil { - return nil, fmt.Errorf("failed to create NetworkManager backend: %w", err) - } - backend = nm - - case BackendIwd: - iwd, err := NewIWDBackend() - if err != nil { - return nil, fmt.Errorf("failed to create iwd backend: %w", err) - } - backend = iwd - - case BackendNetworkd: - if detection.HasIwd && !detection.HasNM { - wifi, err := NewIWDBackend() - if err != nil { - return nil, fmt.Errorf("failed to create iwd backend: %w", err) - } - l3, err := NewSystemdNetworkdBackend() - if err != nil { - return nil, fmt.Errorf("failed to create networkd backend: %w", err) - } - hybrid, err := NewHybridIwdNetworkdBackend(wifi, l3) - if err != nil { - return nil, fmt.Errorf("failed to create hybrid backend: %w", err) - } - backend = hybrid - } else { - nd, err := NewSystemdNetworkdBackend() - if err != nil { - return nil, fmt.Errorf("failed to create networkd backend: %w", err) - } - backend = nd - } - - default: - return nil, fmt.Errorf("no supported network backend found: %s", detection.ChosenReason) - } - - m := &Manager{ - backend: backend, - state: &NetworkState{ - NetworkStatus: StatusDisconnected, - Preference: PreferenceAuto, - WiFiNetworks: []WiFiNetwork{}, - }, - stateMutex: sync.RWMutex{}, - - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - - broker := NewSubscriptionBroker(m.broadcastCredentialPrompt) - if err := backend.SetPromptBroker(broker); err != nil { - return nil, fmt.Errorf("failed to set prompt broker: %w", err) - } - - if err := backend.Initialize(); err != nil { - return nil, fmt.Errorf("failed to initialize backend: %w", err) - } - - if err := m.syncStateFromBackend(); err != nil { - return nil, fmt.Errorf("failed to sync initial state: %w", err) - } - - m.notifierWg.Add(1) - go m.notifier() - - if err := backend.StartMonitoring(m.onBackendStateChange); err != nil { - m.Close() - return nil, fmt.Errorf("failed to start monitoring: %w", err) - } - - return m, nil -} - -func (m *Manager) syncStateFromBackend() error { - backendState, err := m.backend.GetCurrentState() - if err != nil { - return err - } - - m.stateMutex.Lock() - m.state.Backend = backendState.Backend - m.state.NetworkStatus = backendState.NetworkStatus - m.state.EthernetIP = backendState.EthernetIP - m.state.EthernetDevice = backendState.EthernetDevice - m.state.EthernetConnected = backendState.EthernetConnected - m.state.EthernetConnectionUuid = backendState.EthernetConnectionUuid - m.state.EthernetDevices = backendState.EthernetDevices - m.state.WiFiIP = backendState.WiFiIP - m.state.WiFiDevice = backendState.WiFiDevice - m.state.WiFiConnected = backendState.WiFiConnected - m.state.WiFiEnabled = backendState.WiFiEnabled - m.state.WiFiSSID = backendState.WiFiSSID - m.state.WiFiBSSID = backendState.WiFiBSSID - m.state.WiFiSignal = backendState.WiFiSignal - m.state.WiFiNetworks = backendState.WiFiNetworks - m.state.WiFiDevices = backendState.WiFiDevices - m.state.WiredConnections = backendState.WiredConnections - m.state.VPNProfiles = backendState.VPNProfiles - m.state.VPNActive = backendState.VPNActive - m.state.IsConnecting = backendState.IsConnecting - m.state.ConnectingSSID = backendState.ConnectingSSID - m.state.ConnectingDevice = backendState.ConnectingDevice - m.state.LastError = backendState.LastError - m.stateMutex.Unlock() - - return nil -} - -func (m *Manager) onBackendStateChange() { - if err := m.syncStateFromBackend(); err != nil { - log.Errorf("failed to sync state from backend: %v", err) - } - m.notifySubscribers() -} - -func signalChangeSignificant(old, new uint8) bool { - if old == 0 || new == 0 { - return true - } - diff := int(new) - int(old) - if diff < 0 { - diff = -diff - } - return diff >= 5 -} - -func (m *Manager) snapshotState() NetworkState { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - s := *m.state - s.WiFiNetworks = append([]WiFiNetwork(nil), m.state.WiFiNetworks...) - s.WiFiDevices = append([]WiFiDevice(nil), m.state.WiFiDevices...) - s.WiredConnections = append([]WiredConnection(nil), m.state.WiredConnections...) - s.EthernetDevices = append([]EthernetDevice(nil), m.state.EthernetDevices...) - s.VPNProfiles = append([]VPNProfile(nil), m.state.VPNProfiles...) - s.VPNActive = append([]VPNActive(nil), m.state.VPNActive...) - return s -} - -func stateChangedMeaningfully(old, new *NetworkState) bool { - if old.NetworkStatus != new.NetworkStatus { - return true - } - if old.Preference != new.Preference { - return true - } - if old.EthernetConnected != new.EthernetConnected { - return true - } - if old.EthernetIP != new.EthernetIP { - return true - } - if old.WiFiConnected != new.WiFiConnected { - return true - } - if old.WiFiEnabled != new.WiFiEnabled { - return true - } - if old.WiFiSSID != new.WiFiSSID { - return true - } - if old.WiFiBSSID != new.WiFiBSSID { - return true - } - if old.WiFiIP != new.WiFiIP { - return true - } - if !signalChangeSignificant(old.WiFiSignal, new.WiFiSignal) { - if old.WiFiSignal != new.WiFiSignal { - return false - } - } else if old.WiFiSignal != new.WiFiSignal { - return true - } - if old.IsConnecting != new.IsConnecting { - return true - } - if old.ConnectingSSID != new.ConnectingSSID { - return true - } - if old.LastError != new.LastError { - return true - } - if len(old.WiFiNetworks) != len(new.WiFiNetworks) { - return true - } - if len(old.WiFiDevices) != len(new.WiFiDevices) { - return true - } - if len(old.WiredConnections) != len(new.WiredConnections) { - return true - } - if len(old.EthernetDevices) != len(new.EthernetDevices) { - return true - } - - for i := range old.WiFiNetworks { - oldNet := &old.WiFiNetworks[i] - newNet := &new.WiFiNetworks[i] - if oldNet.SSID != newNet.SSID { - return true - } - if oldNet.Connected != newNet.Connected { - return true - } - if oldNet.Saved != newNet.Saved { - return true - } - if oldNet.Autoconnect != newNet.Autoconnect { - return true - } - } - - for i := range old.WiredConnections { - oldNet := &old.WiredConnections[i] - newNet := &new.WiredConnections[i] - if oldNet.ID != newNet.ID { - return true - } - if oldNet.IsActive != newNet.IsActive { - return true - } - } - - for i := range old.EthernetDevices { - oldDev := &old.EthernetDevices[i] - newDev := &new.EthernetDevices[i] - if oldDev.Name != newDev.Name { - return true - } - if oldDev.Connected != newDev.Connected { - return true - } - if oldDev.State != newDev.State { - return true - } - if oldDev.IP != newDev.IP { - return true - } - } - - // Check VPN profiles count - if len(old.VPNProfiles) != len(new.VPNProfiles) { - return true - } - - // Check active VPN connections count or state - if len(old.VPNActive) != len(new.VPNActive) { - return true - } - - // Check if any active VPN changed - for i := range old.VPNActive { - oldVPN := &old.VPNActive[i] - newVPN := &new.VPNActive[i] - if oldVPN.UUID != newVPN.UUID { - return true - } - if oldVPN.State != newVPN.State { - return true - } - } - - return false -} - -func (m *Manager) GetState() NetworkState { - return m.snapshotState() -} - -func (m *Manager) Subscribe(id string) chan NetworkState { - ch := make(chan NetworkState, 64) - m.subscribers.Store(id, ch) - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) SubscribeCredentials(id string) chan CredentialPrompt { - ch := make(chan CredentialPrompt, 16) - m.credentialSubscribers.Store(id, ch) - return ch -} - -func (m *Manager) UnsubscribeCredentials(id string) { - if ch, ok := m.credentialSubscribers.LoadAndDelete(id); ok { - close(ch) - } -} - -func (m *Manager) broadcastCredentialPrompt(prompt CredentialPrompt) { - m.credentialSubscribers.Range(func(key string, ch chan CredentialPrompt) bool { - select { - case ch <- prompt: - default: - } - return true - }) -} - -func (m *Manager) notifier() { - defer m.notifierWg.Done() - const minGap = 100 * time.Millisecond - timer := time.NewTimer(minGap) - timer.Stop() - var pending bool - for { - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.dirty: - if pending { - continue - } - pending = true - timer.Reset(minGap) - case <-timer.C: - if !pending { - continue - } - - currentState := m.snapshotState() - - if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, ¤tState) { - pending = false - continue - } - - m.subscribers.Range(func(key string, ch chan NetworkState) bool { - select { - case ch <- currentState: - default: - } - return true - }) - - stateCopy := currentState - m.lastNotifiedState = &stateCopy - pending = false - } - } -} - -func (m *Manager) notifySubscribers() { - select { - case m.dirty <- struct{}{}: - default: - } -} - -func (m *Manager) SetPromptBroker(broker PromptBroker) error { - return m.backend.SetPromptBroker(broker) -} - -func (m *Manager) SubmitCredentials(token string, secrets map[string]string, save bool) error { - return m.backend.SubmitCredentials(token, secrets, save) -} - -func (m *Manager) CancelCredentials(token string) error { - return m.backend.CancelCredentials(token) -} - -func (m *Manager) GetPromptBroker() PromptBroker { - return m.backend.GetPromptBroker() -} - -func (m *Manager) Close() { - close(m.stopChan) - m.notifierWg.Wait() - - if m.backend != nil { - m.backend.Close() - } - - m.subscribers.Range(func(key string, ch chan NetworkState) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) -} - -func (m *Manager) ScanWiFi() error { - return m.backend.ScanWiFi() -} - -func (m *Manager) GetWiFiNetworks() []WiFiNetwork { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - networks := make([]WiFiNetwork, len(m.state.WiFiNetworks)) - copy(networks, m.state.WiFiNetworks) - return networks -} - -func (m *Manager) GetNetworkInfo(ssid string) (*WiFiNetwork, error) { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - - for _, network := range m.state.WiFiNetworks { - if network.SSID == ssid { - return &network, nil - } - } - - return nil, fmt.Errorf("network not found: %s", ssid) -} - -func (m *Manager) GetNetworkInfoDetailed(ssid string) (*NetworkInfoResponse, error) { - return m.backend.GetWiFiNetworkDetails(ssid) -} - -func (m *Manager) GetNetworkQRCode(ssid string) ([2]string, error) { - content, err := m.backend.GetWiFiQRCodeContent(ssid) - if err != nil { - return [2]string{}, err - } - - qrc, err := qrcode.New(content) - if err != nil { - return [2]string{}, fmt.Errorf("failed to create QR code for `%s`: %w", ssid, err) - } - - pathThemed, pathNormal := qrCodePaths(ssid) - - wThemed, err := standard.New( - pathThemed, - standard.WithBuiltinImageEncoder(standard.PNG_FORMAT), - standard.WithBgTransparent(), - standard.WithFgColorRGBHex("#ffffff"), - ) - if err != nil { - return [2]string{}, fmt.Errorf("failed to create QR code writer: %w", err) - } - if err := qrc.Save(wThemed); err != nil { - return [2]string{}, fmt.Errorf("failed to save QR code for `%s`: %w", ssid, err) - } - - wNormal, err := standard.New(pathNormal, standard.WithBuiltinImageEncoder(standard.PNG_FORMAT)) - if err != nil { - return [2]string{}, fmt.Errorf("failed to create QR code writer: %w", err) - } - if err := qrc.Save(wNormal); err != nil { - return [2]string{}, fmt.Errorf("failed to save QR code for `%s`: %w", ssid, err) - } - - return [2]string{pathThemed, pathNormal}, nil -} - -func (m *Manager) ToggleWiFi() error { - enabled, err := m.backend.GetWiFiEnabled() - if err != nil { - return fmt.Errorf("failed to get WiFi state: %w", err) - } - - err = m.backend.SetWiFiEnabled(!enabled) - if err != nil { - return fmt.Errorf("failed to toggle WiFi: %w", err) - } - - return nil -} - -func (m *Manager) EnableWiFi() error { - err := m.backend.SetWiFiEnabled(true) - if err != nil { - return fmt.Errorf("failed to enable WiFi: %w", err) - } - - return nil -} - -func (m *Manager) DisableWiFi() error { - err := m.backend.SetWiFiEnabled(false) - if err != nil { - return fmt.Errorf("failed to disable WiFi: %w", err) - } - - return nil -} - -func (m *Manager) ConnectWiFi(req ConnectionRequest) error { - return m.backend.ConnectWiFi(req) -} - -func (m *Manager) DisconnectWiFi() error { - return m.backend.DisconnectWiFi() -} - -func (m *Manager) ForgetWiFiNetwork(ssid string) error { - return m.backend.ForgetWiFiNetwork(ssid) -} - -func (m *Manager) GetWiredConfigs() []WiredConnection { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - configs := make([]WiredConnection, len(m.state.WiredConnections)) - copy(configs, m.state.WiredConnections) - return configs -} - -func (m *Manager) GetWiredNetworkInfoDetailed(uuid string) (*WiredNetworkInfoResponse, error) { - return m.backend.GetWiredNetworkDetails(uuid) -} - -func (m *Manager) ConnectEthernet() error { - return m.backend.ConnectEthernet() -} - -func (m *Manager) DisconnectEthernet() error { - return m.backend.DisconnectEthernet() -} - -func (m *Manager) DisconnectEthernetDevice(device string) error { - return m.backend.DisconnectEthernetDevice(device) -} - -func (m *Manager) GetEthernetDevices() []EthernetDevice { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - devices := make([]EthernetDevice, len(m.state.EthernetDevices)) - copy(devices, m.state.EthernetDevices) - return devices -} - -func (m *Manager) activateConnection(uuid string) error { - return m.backend.ActivateWiredConnection(uuid) -} - -func (m *Manager) ListVPNProfiles() ([]VPNProfile, error) { - return m.backend.ListVPNProfiles() -} - -func (m *Manager) ListActiveVPN() ([]VPNActive, error) { - return m.backend.ListActiveVPN() -} - -func (m *Manager) ConnectVPN(uuidOrName string, singleActive bool) error { - return m.backend.ConnectVPN(uuidOrName, singleActive) -} - -func (m *Manager) DisconnectVPN(uuidOrName string) error { - return m.backend.DisconnectVPN(uuidOrName) -} - -func (m *Manager) DisconnectAllVPN() error { - return m.backend.DisconnectAllVPN() -} - -func (m *Manager) ClearVPNCredentials(uuidOrName string) error { - return m.backend.ClearVPNCredentials(uuidOrName) -} - -func (m *Manager) ListVPNPlugins() ([]VPNPlugin, error) { - return m.backend.ListVPNPlugins() -} - -func (m *Manager) ImportVPN(filePath string, name string) (*VPNImportResult, error) { - return m.backend.ImportVPN(filePath, name) -} - -func (m *Manager) GetVPNConfig(uuidOrName string) (*VPNConfig, error) { - return m.backend.GetVPNConfig(uuidOrName) -} - -func (m *Manager) UpdateVPNConfig(uuid string, updates map[string]any) error { - return m.backend.UpdateVPNConfig(uuid, updates) -} - -func (m *Manager) DeleteVPN(uuidOrName string) error { - return m.backend.DeleteVPN(uuidOrName) -} - -func (m *Manager) SetVPNCredentials(uuid, username, password string, save bool) error { - return m.backend.SetVPNCredentials(uuid, username, password, save) -} - -func (m *Manager) SetWiFiAutoconnect(ssid string, autoconnect bool) error { - return m.backend.SetWiFiAutoconnect(ssid, autoconnect) -} - -func (m *Manager) GetWiFiDevices() []WiFiDevice { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - devices := make([]WiFiDevice, len(m.state.WiFiDevices)) - copy(devices, m.state.WiFiDevices) - return devices -} - -func (m *Manager) ScanWiFiDevice(device string) error { - return m.backend.ScanWiFiDevice(device) -} - -func (m *Manager) DisconnectWiFiDevice(device string) error { - return m.backend.DisconnectWiFiDevice(device) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/manager_test.go deleted file mode 100644 index 79545c5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/manager_test.go +++ /dev/null @@ -1,191 +0,0 @@ -package network - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestManager_GetState(t *testing.T) { - state := &NetworkState{ - NetworkStatus: StatusWiFi, - WiFiSSID: "TestNetwork", - WiFiConnected: true, - } - - manager := &Manager{ - state: state, - stateMutex: sync.RWMutex{}, - } - - result := manager.GetState() - assert.Equal(t, StatusWiFi, result.NetworkStatus) - assert.Equal(t, "TestNetwork", result.WiFiSSID) - assert.True(t, result.WiFiConnected) -} - -func TestManager_NotifySubscribers(t *testing.T) { - manager := &Manager{ - state: &NetworkState{ - NetworkStatus: StatusWiFi, - }, - stateMutex: sync.RWMutex{}, - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - manager.notifierWg.Add(1) - go manager.notifier() - - ch := make(chan NetworkState, 10) - manager.subscribers.Store("test-client", ch) - - manager.notifySubscribers() - - select { - case state := <-ch: - assert.Equal(t, StatusWiFi, state.NetworkStatus) - case <-time.After(200 * time.Millisecond): - t.Fatal("did not receive state update") - } - - close(manager.stopChan) - manager.notifierWg.Wait() -} - -func TestManager_NotifySubscribers_Debounce(t *testing.T) { - manager := &Manager{ - state: &NetworkState{ - NetworkStatus: StatusWiFi, - }, - stateMutex: sync.RWMutex{}, - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - manager.notifierWg.Add(1) - go manager.notifier() - - ch := make(chan NetworkState, 10) - manager.subscribers.Store("test-client", ch) - - manager.notifySubscribers() - manager.notifySubscribers() - manager.notifySubscribers() - - receivedCount := 0 - timeout := time.After(200 * time.Millisecond) - for { - select { - case <-ch: - receivedCount++ - case <-timeout: - assert.Equal(t, 1, receivedCount, "should receive exactly one debounced update") - close(manager.stopChan) - manager.notifierWg.Wait() - return - } - } -} - -func TestManager_Close(t *testing.T) { - manager := &Manager{ - state: &NetworkState{}, - stateMutex: sync.RWMutex{}, - stopChan: make(chan struct{}), - } - - ch1 := make(chan NetworkState, 1) - ch2 := make(chan NetworkState, 1) - manager.subscribers.Store("client1", ch1) - manager.subscribers.Store("client2", ch2) - - manager.Close() - - select { - case <-manager.stopChan: - case <-time.After(100 * time.Millisecond): - t.Fatal("stopChan not closed") - } - - _, ok1 := <-ch1 - _, ok2 := <-ch2 - assert.False(t, ok1, "ch1 should be closed") - assert.False(t, ok2, "ch2 should be closed") - - count := 0 - manager.subscribers.Range(func(key string, ch chan NetworkState) bool { count++; return true }) - assert.Equal(t, 0, count) -} - -func TestManager_Subscribe(t *testing.T) { - manager := &Manager{ - state: &NetworkState{}, - } - - ch := manager.Subscribe("test-client") - assert.NotNil(t, ch) - assert.Equal(t, 64, cap(ch)) - - _, exists := manager.subscribers.Load("test-client") - assert.True(t, exists) -} - -func TestManager_Unsubscribe(t *testing.T) { - manager := &Manager{ - state: &NetworkState{}, - } - - ch := manager.Subscribe("test-client") - - manager.Unsubscribe("test-client") - - _, ok := <-ch - assert.False(t, ok) - - _, exists := manager.subscribers.Load("test-client") - assert.False(t, exists) -} - -func TestNewManager(t *testing.T) { - t.Run("attempts to create manager", func(t *testing.T) { - manager, err := NewManager() - if err != nil { - assert.Nil(t, manager) - } else { - assert.NotNil(t, manager) - assert.NotNil(t, manager.state) - assert.NotNil(t, manager.subscribers) - assert.NotNil(t, manager.stopChan) - - manager.Close() - } - }) -} - -func TestManager_GetState_ThreadSafe(t *testing.T) { - manager := &Manager{ - state: &NetworkState{ - NetworkStatus: StatusWiFi, - WiFiSSID: "TestNetwork", - }, - stateMutex: sync.RWMutex{}, - } - - done := make(chan bool) - for i := 0; i < 10; i++ { - go func() { - state := manager.GetState() - assert.Equal(t, StatusWiFi, state.NetworkStatus) - done <- true - }() - } - - for i := 0; i < 10; i++ { - select { - case <-done: - case <-time.After(1 * time.Second): - t.Fatal("timeout waiting for goroutines") - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/monitor.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/monitor.go deleted file mode 100644 index 1ae2e9d..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/monitor.go +++ /dev/null @@ -1 +0,0 @@ -package network diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/priority.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/priority.go deleted file mode 100644 index 8ee7fdf..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/priority.go +++ /dev/null @@ -1,187 +0,0 @@ -package network - -import ( - "fmt" - "os/exec" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/godbus/dbus/v5" -) - -const ( - priorityHigh = int32(100) - priorityLow = int32(10) - priorityDefault = int32(0) - - metricPreferred = int64(100) - metricNonPreferred = int64(300) - metricDefault = int64(100) -) - -func (m *Manager) SetConnectionPreference(pref ConnectionPreference) error { - switch pref { - case PreferenceWiFi, PreferenceEthernet, PreferenceAuto: - default: - return fmt.Errorf("invalid preference: %s", pref) - } - - m.stateMutex.Lock() - m.state.Preference = pref - m.stateMutex.Unlock() - - if _, ok := m.backend.(*NetworkManagerBackend); !ok { - m.notifySubscribers() - return nil - } - - switch pref { - case PreferenceWiFi: - return m.prioritizeWiFi() - case PreferenceEthernet: - return m.prioritizeEthernet() - case PreferenceAuto: - return m.balancePriorities() - } - - return nil -} - -func (m *Manager) prioritizeWiFi() error { - if err := m.setConnectionPriority("802-11-wireless", priorityHigh, metricPreferred); err != nil { - log.Warnf("Failed to set WiFi priority: %v", err) - } - - if err := m.setConnectionPriority("802-3-ethernet", priorityLow, metricNonPreferred); err != nil { - log.Warnf("Failed to set Ethernet priority: %v", err) - } - - m.reapplyActiveConnections() - m.notifySubscribers() - return nil -} - -func (m *Manager) prioritizeEthernet() error { - if err := m.setConnectionPriority("802-3-ethernet", priorityHigh, metricPreferred); err != nil { - log.Warnf("Failed to set Ethernet priority: %v", err) - } - - if err := m.setConnectionPriority("802-11-wireless", priorityLow, metricNonPreferred); err != nil { - log.Warnf("Failed to set WiFi priority: %v", err) - } - - m.reapplyActiveConnections() - m.notifySubscribers() - return nil -} - -func (m *Manager) balancePriorities() error { - if err := m.setConnectionPriority("802-3-ethernet", priorityDefault, metricDefault); err != nil { - log.Warnf("Failed to reset Ethernet priority: %v", err) - } - - if err := m.setConnectionPriority("802-11-wireless", priorityDefault, metricDefault); err != nil { - log.Warnf("Failed to reset WiFi priority: %v", err) - } - - m.reapplyActiveConnections() - m.notifySubscribers() - return nil -} - -func (m *Manager) reapplyActiveConnections() { - m.stateMutex.RLock() - ethDev := m.state.EthernetDevice - wifiDev := m.state.WiFiDevice - m.stateMutex.RUnlock() - - if ethDev != "" { - exec.Command("nmcli", "dev", "reapply", ethDev).Run() - } - if wifiDev != "" { - exec.Command("nmcli", "dev", "reapply", wifiDev).Run() - } -} - -func (m *Manager) setConnectionPriority(connType string, autoconnectPriority int32, routeMetric int64) error { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return fmt.Errorf("failed to connect to system bus: %w", err) - } - defer conn.Close() - - settingsObj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/Settings") - - var connPaths []dbus.ObjectPath - if err := settingsObj.Call("org.freedesktop.NetworkManager.Settings.ListConnections", 0).Store(&connPaths); err != nil { - return fmt.Errorf("failed to list connections: %w", err) - } - - for _, connPath := range connPaths { - connObj := conn.Object("org.freedesktop.NetworkManager", connPath) - - var settings map[string]map[string]dbus.Variant - if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&settings); err != nil { - continue - } - - connSection, ok := settings["connection"] - if !ok { - continue - } - - typeVariant, ok := connSection["type"] - if !ok { - continue - } - - cType, ok := typeVariant.Value().(string) - if !ok || cType != connType { - continue - } - - connName := "" - if idVariant, ok := connSection["id"]; ok { - connName, _ = idVariant.Value().(string) - } - - if connName == "" { - continue - } - - if err := exec.Command("nmcli", "con", "mod", connName, - "connection.autoconnect-priority", fmt.Sprintf("%d", autoconnectPriority), - "ipv4.route-metric", fmt.Sprintf("%d", routeMetric), - "ipv6.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil { - log.Warnf("Failed to set priority for %s: %v", connName, err) - continue - } - - log.Infof("Updated %v: autoconnect-priority=%d, route-metric=%d", connName, autoconnectPriority, routeMetric) - } - - return nil -} - -func (m *Manager) GetConnectionPreference() ConnectionPreference { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - return m.state.Preference -} - -func (m *Manager) WasRecentlyFailed(ssid string) bool { - nm, ok := m.backend.(*NetworkManagerBackend) - if !ok { - return false - } - - nm.failedMutex.RLock() - defer nm.failedMutex.RUnlock() - - if nm.lastFailedSSID != ssid { - return false - } - - elapsed := time.Now().Unix() - nm.lastFailedTime - return elapsed < 10 -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/priority_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/priority_test.go deleted file mode 100644 index c0c65e3..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/priority_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package network - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestManager_SetConnectionPreference(t *testing.T) { - t.Run("invalid preference", func(t *testing.T) { - manager := &Manager{ - state: &NetworkState{ - Preference: PreferenceAuto, - }, - } - - err := manager.SetConnectionPreference(ConnectionPreference("invalid")) - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid preference") - }) -} - -func TestManager_GetConnectionPreference(t *testing.T) { - tests := []struct { - name string - preference ConnectionPreference - }{ - {"auto", PreferenceAuto}, - {"wifi", PreferenceWiFi}, - {"ethernet", PreferenceEthernet}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - manager := &Manager{ - state: &NetworkState{ - Preference: tt.preference, - }, - } - - result := manager.GetConnectionPreference() - assert.Equal(t, tt.preference, result) - }) - } -} - -// Note: Full testing of priority operations would require mocking NetworkManager -// D-Bus interfaces. The tests above cover the basic logic and error handling. -// Integration tests would be needed for complete coverage of network connection -// priority updates and reactivation. diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/subscription_broker.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/subscription_broker.go deleted file mode 100644 index 1b150a2..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/subscription_broker.go +++ /dev/null @@ -1,125 +0,0 @@ -package network - -import ( - "context" - "fmt" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" -) - -type SubscriptionBroker struct { - pending syncmap.Map[string, chan PromptReply] - requests syncmap.Map[string, PromptRequest] - pathSettingToToken syncmap.Map[string, string] - broadcastPrompt func(CredentialPrompt) -} - -func NewSubscriptionBroker(broadcastPrompt func(CredentialPrompt)) PromptBroker { - return &SubscriptionBroker{ - broadcastPrompt: broadcastPrompt, - } -} - -func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string, error) { - pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName) - - if existingToken, alreadyPending := b.pathSettingToToken.Load(pathSettingKey); alreadyPending { - log.Infof("[SubscriptionBroker] Duplicate prompt for %s, returning existing token", pathSettingKey) - return existingToken, nil - } - - token, err := generateToken() - if err != nil { - return "", err - } - - replyChan := make(chan PromptReply, 1) - b.pending.Store(token, replyChan) - b.requests.Store(token, req) - b.pathSettingToToken.Store(pathSettingKey, token) - - if b.broadcastPrompt != nil { - prompt := CredentialPrompt{ - Token: token, - Name: req.Name, - SSID: req.SSID, - ConnType: req.ConnType, - VpnService: req.VpnService, - Setting: req.SettingName, - Fields: req.Fields, - FieldsInfo: req.FieldsInfo, - Hints: req.Hints, - Reason: req.Reason, - ConnectionId: req.ConnectionId, - ConnectionUuid: req.ConnectionUuid, - } - b.broadcastPrompt(prompt) - } - - return token, nil -} - -func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) { - replyChan, exists := b.pending.Load(token) - if !exists { - return PromptReply{}, fmt.Errorf("unknown token: %s", token) - } - - select { - case <-ctx.Done(): - b.cleanup(token) - return PromptReply{}, errdefs.ErrSecretPromptTimeout - case reply := <-replyChan: - b.cleanup(token) - if reply.Cancel { - return reply, errdefs.ErrSecretPromptCancelled - } - return reply, nil - } -} - -func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error { - replyChan, exists := b.pending.Load(token) - if !exists { - log.Warnf("[SubscriptionBroker] Resolve: unknown or expired token: %s", token) - return fmt.Errorf("unknown or expired token: %s", token) - } - - select { - case replyChan <- reply: - return nil - default: - log.Warnf("[SubscriptionBroker] Resolve: failed to deliver reply for token %s (channel full or closed)", token) - return fmt.Errorf("failed to deliver reply for token: %s", token) - } -} - -func (b *SubscriptionBroker) cleanup(token string) { - if req, exists := b.requests.Load(token); exists { - pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName) - b.pathSettingToToken.Delete(pathSettingKey) - } - - b.pending.Delete(token) - b.requests.Delete(token) -} - -func (b *SubscriptionBroker) Cancel(path string, setting string) error { - pathSettingKey := fmt.Sprintf("%s:%s", path, setting) - - token, exists := b.pathSettingToToken.Load(pathSettingKey) - if !exists { - log.Infof("[SubscriptionBroker] Cancel: no pending prompt for %s", pathSettingKey) - return nil - } - - log.Infof("[SubscriptionBroker] Cancelling prompt for %s (token=%s)", pathSettingKey, token) - - reply := PromptReply{ - Cancel: true, - } - - return b.Resolve(token, reply) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/testing.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/testing.go deleted file mode 100644 index a6c4a00..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/testing.go +++ /dev/null @@ -1,14 +0,0 @@ -package network - -// NewTestManager creates a Manager for testing with a provided backend -func NewTestManager(backend Backend, state *NetworkState) *Manager { - if state == nil { - state = &NetworkState{} - } - return &Manager{ - backend: backend, - state: state, - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/types.go deleted file mode 100644 index 93448cf..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/types.go +++ /dev/null @@ -1,267 +0,0 @@ -package network - -import ( - "sync" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" - "github.com/godbus/dbus/v5" -) - -type NetworkStatus string - -const ( - StatusDisconnected NetworkStatus = "disconnected" - StatusEthernet NetworkStatus = "ethernet" - StatusWiFi NetworkStatus = "wifi" - StatusVPN NetworkStatus = "vpn" -) - -type ConnectionPreference string - -const ( - PreferenceAuto ConnectionPreference = "auto" - PreferenceWiFi ConnectionPreference = "wifi" - PreferenceEthernet ConnectionPreference = "ethernet" -) - -type WiFiNetwork struct { - SSID string `json:"ssid"` - BSSID string `json:"bssid"` - Signal uint8 `json:"signal"` - Secured bool `json:"secured"` - Enterprise bool `json:"enterprise"` - Connected bool `json:"connected"` - Saved bool `json:"saved"` - Autoconnect bool `json:"autoconnect"` - Hidden bool `json:"hidden"` - Frequency uint32 `json:"frequency"` - Mode string `json:"mode"` - Rate uint32 `json:"rate"` - Channel uint32 `json:"channel"` - Device string `json:"device,omitempty"` -} - -type WiFiDevice struct { - Name string `json:"name"` - HwAddress string `json:"hwAddress"` - State string `json:"state"` - Connected bool `json:"connected"` - SSID string `json:"ssid,omitempty"` - BSSID string `json:"bssid,omitempty"` - Signal uint8 `json:"signal,omitempty"` - IP string `json:"ip,omitempty"` - Networks []WiFiNetwork `json:"networks"` -} - -type EthernetDevice struct { - Name string `json:"name"` - HwAddress string `json:"hwAddress"` - State string `json:"state"` - Connected bool `json:"connected"` - IP string `json:"ip,omitempty"` - Speed uint32 `json:"speed,omitempty"` - Driver string `json:"driver,omitempty"` -} - -type VPNProfile struct { - Name string `json:"name"` - UUID string `json:"uuid"` - Type string `json:"type"` - ServiceType string `json:"serviceType"` - RemoteHost string `json:"remoteHost,omitempty"` - Username string `json:"username,omitempty"` - Autoconnect bool `json:"autoconnect"` - Data map[string]string `json:"data,omitempty"` -} - -type VPNActive struct { - Name string `json:"name"` - UUID string `json:"uuid"` - Device string `json:"device,omitempty"` - State string `json:"state,omitempty"` - Type string `json:"type"` - Plugin string `json:"serviceType"` - IP string `json:"ip,omitempty"` - Gateway string `json:"gateway,omitempty"` - RemoteHost string `json:"remoteHost,omitempty"` - Username string `json:"username,omitempty"` - MTU uint32 `json:"mtu,omitempty"` - Data map[string]string `json:"data,omitempty"` -} - -type VPNState struct { - Profiles []VPNProfile `json:"profiles"` - Active []VPNActive `json:"activeConnections"` -} - -type NetworkState struct { - Backend string `json:"backend"` - NetworkStatus NetworkStatus `json:"networkStatus"` - Preference ConnectionPreference `json:"preference"` - EthernetIP string `json:"ethernetIP"` - EthernetDevice string `json:"ethernetDevice"` - EthernetConnected bool `json:"ethernetConnected"` - EthernetConnectionUuid string `json:"ethernetConnectionUuid"` - EthernetDevices []EthernetDevice `json:"ethernetDevices"` - WiFiIP string `json:"wifiIP"` - WiFiDevice string `json:"wifiDevice"` - WiFiConnected bool `json:"wifiConnected"` - WiFiEnabled bool `json:"wifiEnabled"` - WiFiSSID string `json:"wifiSSID"` - WiFiBSSID string `json:"wifiBSSID"` - WiFiSignal uint8 `json:"wifiSignal"` - WiFiNetworks []WiFiNetwork `json:"wifiNetworks"` - WiFiDevices []WiFiDevice `json:"wifiDevices"` - WiredConnections []WiredConnection `json:"wiredConnections"` - VPNProfiles []VPNProfile `json:"vpnProfiles"` - VPNActive []VPNActive `json:"vpnActive"` - IsConnecting bool `json:"isConnecting"` - ConnectingSSID string `json:"connectingSSID"` - ConnectingDevice string `json:"connectingDevice,omitempty"` - LastError string `json:"lastError"` -} - -type ConnectionRequest struct { - SSID string `json:"ssid"` - Password string `json:"password,omitempty"` - Username string `json:"username,omitempty"` - AnonymousIdentity string `json:"anonymousIdentity,omitempty"` - DomainSuffixMatch string `json:"domainSuffixMatch,omitempty"` - Interactive bool `json:"interactive,omitempty"` - Hidden bool `json:"hidden,omitempty"` - Device string `json:"device,omitempty"` - EAPMethod string `json:"eapMethod,omitempty"` - Phase2Auth string `json:"phase2Auth,omitempty"` - CACertPath string `json:"caCertPath,omitempty"` - ClientCertPath string `json:"clientCertPath,omitempty"` - PrivateKeyPath string `json:"privateKeyPath,omitempty"` - UseSystemCACerts *bool `json:"useSystemCACerts,omitempty"` -} - -type WiredConnection struct { - Path dbus.ObjectPath `json:"path"` - ID string `json:"id"` - UUID string `json:"uuid"` - Type string `json:"type"` - IsActive bool `json:"isActive"` -} - -type PriorityUpdate struct { - Preference ConnectionPreference `json:"preference"` -} - -type Manager struct { - backend Backend - state *NetworkState - stateMutex sync.RWMutex - subscribers syncmap.Map[string, chan NetworkState] - stopChan chan struct{} - dirty chan struct{} - notifierWg sync.WaitGroup - lastNotifiedState *NetworkState - credentialSubscribers syncmap.Map[string, chan CredentialPrompt] -} - -type EventType string - -const ( - EventStateChanged EventType = "state_changed" - EventNetworksUpdated EventType = "networks_updated" - EventConnecting EventType = "connecting" - EventConnected EventType = "connected" - EventDisconnected EventType = "disconnected" - EventError EventType = "error" -) - -type NetworkEvent struct { - Type EventType `json:"type"` - Data NetworkState `json:"data"` -} - -type PromptRequest struct { - Name string `json:"name"` - SSID string `json:"ssid"` - ConnType string `json:"connType"` - VpnService string `json:"vpnService"` - SettingName string `json:"setting"` - Fields []string `json:"fields"` - FieldsInfo []FieldInfo `json:"fieldsInfo"` - Hints []string `json:"hints"` - Reason string `json:"reason"` - ConnectionId string `json:"connectionId"` - ConnectionUuid string `json:"connectionUuid"` - ConnectionPath string `json:"connectionPath"` -} - -type PromptReply struct { - Secrets map[string]string `json:"secrets"` - Save bool `json:"save"` - Cancel bool `json:"cancel"` -} - -type FieldInfo struct { - Name string `json:"name"` - Label string `json:"label"` - IsSecret bool `json:"isSecret"` -} - -type CredentialPrompt struct { - Token string `json:"token"` - Name string `json:"name"` - SSID string `json:"ssid"` - ConnType string `json:"connType"` - VpnService string `json:"vpnService"` - Setting string `json:"setting"` - Fields []string `json:"fields"` - FieldsInfo []FieldInfo `json:"fieldsInfo"` - Hints []string `json:"hints"` - Reason string `json:"reason"` - ConnectionId string `json:"connectionId"` - ConnectionUuid string `json:"connectionUuid"` -} - -type NetworkInfoResponse struct { - SSID string `json:"ssid"` - Bands []WiFiNetwork `json:"bands"` -} - -type WiredNetworkInfoResponse struct { - UUID string `json:"uuid"` - IFace string `json:"iface"` - Driver string `json:"driver"` - HwAddr string `json:"hwAddr"` - Speed string `json:"speed"` - IPv4 WiredIPConfig `json:"IPv4s"` - IPv6 WiredIPConfig `json:"IPv6s"` -} - -type WiredIPConfig struct { - IPs []string `json:"ips"` - Gateway string `json:"gateway"` - DNS string `json:"dns"` -} - -type VPNPlugin struct { - Name string `json:"name"` - ServiceType string `json:"serviceType"` - Program string `json:"program,omitempty"` - Supports []string `json:"supports,omitempty"` - FileExtensions []string `json:"fileExtensions"` -} - -type VPNConfig struct { - UUID string `json:"uuid"` - Name string `json:"name"` - Type string `json:"type"` - ServiceType string `json:"serviceType,omitempty"` - Autoconnect bool `json:"autoconnect"` - Data map[string]string `json:"data,omitempty"` -} - -type VPNImportResult struct { - Success bool `json:"success"` - UUID string `json:"uuid,omitempty"` - Name string `json:"name,omitempty"` - ServiceType string `json:"serviceType,omitempty"` - Error string `json:"error,omitempty"` -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/types_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/types_test.go deleted file mode 100644 index 1c3e0bf..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/types_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package network - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNetworkStatus_Constants(t *testing.T) { - assert.Equal(t, NetworkStatus("disconnected"), StatusDisconnected) - assert.Equal(t, NetworkStatus("ethernet"), StatusEthernet) - assert.Equal(t, NetworkStatus("wifi"), StatusWiFi) -} - -func TestConnectionPreference_Constants(t *testing.T) { - assert.Equal(t, ConnectionPreference("auto"), PreferenceAuto) - assert.Equal(t, ConnectionPreference("wifi"), PreferenceWiFi) - assert.Equal(t, ConnectionPreference("ethernet"), PreferenceEthernet) -} - -func TestEventType_Constants(t *testing.T) { - assert.Equal(t, EventType("state_changed"), EventStateChanged) - assert.Equal(t, EventType("networks_updated"), EventNetworksUpdated) - assert.Equal(t, EventType("connecting"), EventConnecting) - assert.Equal(t, EventType("connected"), EventConnected) - assert.Equal(t, EventType("disconnected"), EventDisconnected) - assert.Equal(t, EventType("error"), EventError) -} - -func TestWiFiNetwork_JSON(t *testing.T) { - network := WiFiNetwork{ - SSID: "TestNetwork", - BSSID: "00:11:22:33:44:55", - Signal: 85, - Secured: true, - Enterprise: false, - Connected: true, - Saved: true, - Frequency: 2437, - Mode: "infrastructure", - Rate: 300, - Channel: 6, - } - - data, err := json.Marshal(network) - require.NoError(t, err) - - var decoded WiFiNetwork - err = json.Unmarshal(data, &decoded) - require.NoError(t, err) - - assert.Equal(t, network.SSID, decoded.SSID) - assert.Equal(t, network.BSSID, decoded.BSSID) - assert.Equal(t, network.Signal, decoded.Signal) - assert.Equal(t, network.Secured, decoded.Secured) - assert.Equal(t, network.Enterprise, decoded.Enterprise) - assert.Equal(t, network.Connected, decoded.Connected) - assert.Equal(t, network.Saved, decoded.Saved) - assert.Equal(t, network.Frequency, decoded.Frequency) - assert.Equal(t, network.Mode, decoded.Mode) - assert.Equal(t, network.Rate, decoded.Rate) - assert.Equal(t, network.Channel, decoded.Channel) -} - -func TestNetworkState_JSON(t *testing.T) { - state := NetworkState{ - NetworkStatus: StatusWiFi, - Preference: PreferenceAuto, - EthernetIP: "192.168.1.100", - EthernetDevice: "eth0", - EthernetConnected: false, - WiFiIP: "192.168.1.101", - WiFiDevice: "wlan0", - WiFiConnected: true, - WiFiEnabled: true, - WiFiSSID: "TestNetwork", - WiFiBSSID: "00:11:22:33:44:55", - WiFiSignal: 85, - WiFiNetworks: []WiFiNetwork{ - {SSID: "Network1", Signal: 90}, - {SSID: "Network2", Signal: 60}, - }, - IsConnecting: false, - ConnectingSSID: "", - LastError: "", - } - - data, err := json.Marshal(state) - require.NoError(t, err) - - var decoded NetworkState - err = json.Unmarshal(data, &decoded) - require.NoError(t, err) - - assert.Equal(t, state.NetworkStatus, decoded.NetworkStatus) - assert.Equal(t, state.Preference, decoded.Preference) - assert.Equal(t, state.WiFiIP, decoded.WiFiIP) - assert.Equal(t, state.WiFiSSID, decoded.WiFiSSID) - assert.Equal(t, len(state.WiFiNetworks), len(decoded.WiFiNetworks)) -} - -func TestConnectionRequest_JSON(t *testing.T) { - t.Run("with password", func(t *testing.T) { - req := ConnectionRequest{ - SSID: "TestNetwork", - Password: "testpass123", - } - - data, err := json.Marshal(req) - require.NoError(t, err) - - var decoded ConnectionRequest - err = json.Unmarshal(data, &decoded) - require.NoError(t, err) - - assert.Equal(t, req.SSID, decoded.SSID) - assert.Equal(t, req.Password, decoded.Password) - assert.Empty(t, decoded.Username) - }) - - t.Run("with username and password (enterprise)", func(t *testing.T) { - req := ConnectionRequest{ - SSID: "EnterpriseNetwork", - Password: "testpass123", - Username: "testuser", - } - - data, err := json.Marshal(req) - require.NoError(t, err) - - var decoded ConnectionRequest - err = json.Unmarshal(data, &decoded) - require.NoError(t, err) - - assert.Equal(t, req.SSID, decoded.SSID) - assert.Equal(t, req.Password, decoded.Password) - assert.Equal(t, req.Username, decoded.Username) - }) -} - -func TestPriorityUpdate_JSON(t *testing.T) { - update := PriorityUpdate{ - Preference: PreferenceWiFi, - } - - data, err := json.Marshal(update) - require.NoError(t, err) - - var decoded PriorityUpdate - err = json.Unmarshal(data, &decoded) - require.NoError(t, err) - - assert.Equal(t, update.Preference, decoded.Preference) -} - -func TestNetworkEvent_JSON(t *testing.T) { - event := NetworkEvent{ - Type: EventStateChanged, - Data: NetworkState{ - NetworkStatus: StatusWiFi, - WiFiSSID: "TestNetwork", - WiFiConnected: true, - }, - } - - data, err := json.Marshal(event) - require.NoError(t, err) - - var decoded NetworkEvent - err = json.Unmarshal(data, &decoded) - require.NoError(t, err) - - assert.Equal(t, event.Type, decoded.Type) - assert.Equal(t, event.Data.NetworkStatus, decoded.Data.NetworkStatus) - assert.Equal(t, event.Data.WiFiSSID, decoded.Data.WiFiSSID) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/wifi_qrcode.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/wifi_qrcode.go deleted file mode 100644 index 680b6d1..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/wifi_qrcode.go +++ /dev/null @@ -1,59 +0,0 @@ -package network - -import ( - "fmt" - "path/filepath" - "regexp" - "strings" -) - -const qrCodeTmpPrefix = "/tmp/dank-wifi-qrcode-" - -func FormatWiFiQRString(securityType, ssid, password string) string { - return fmt.Sprintf("WIFI:T:%s;S:%s;P:%s;;", securityType, ssid, password) -} - -func qrCodePaths(ssid string) (themed, normal string) { - safe := sanitizeSSIDForPath(ssid) - themed = fmt.Sprintf("%s%s-themed.png", qrCodeTmpPrefix, safe) - normal = fmt.Sprintf("%s%s-normal.png", qrCodeTmpPrefix, safe) - return -} - -func isValidQRCodePath(path string) bool { - clean := filepath.Clean(path) - return strings.HasPrefix(clean, qrCodeTmpPrefix) && strings.HasSuffix(clean, ".png") -} - -var safePathChar = regexp.MustCompile(`[^a-zA-Z0-9_-]`) - -func sanitizeSSIDForPath(ssid string) string { - return safePathChar.ReplaceAllString(ssid, "_") -} - -var iwdVerbatimSSID = regexp.MustCompile(`^[a-zA-Z0-9 _-]+$`) - -func iwdConfigPath(ssid string) string { - switch { - case iwdVerbatimSSID.MatchString(ssid): - return fmt.Sprintf("/var/lib/iwd/%s.psk", ssid) - default: - return fmt.Sprintf("/var/lib/iwd/=%x.psk", []byte(ssid)) - } -} - -func parseIWDPassphrase(data string) (string, error) { - inSecurity := false - for _, line := range strings.Split(data, "\n") { - line = strings.TrimSpace(line) - switch { - case line == "[Security]": - inSecurity = true - case strings.HasPrefix(line, "["): - inSecurity = false - case inSecurity && strings.HasPrefix(line, "Passphrase="): - return strings.TrimPrefix(line, "Passphrase="), nil - } - } - return "", fmt.Errorf("no passphrase found in iwd config") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/wifi_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/wifi_test.go deleted file mode 100644 index 2256b60..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/wifi_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package network - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestFrequencyToChannel(t *testing.T) { - tests := []struct { - name string - frequency uint32 - channel uint32 - }{ - {"2.4 GHz channel 1", 2412, 1}, - {"2.4 GHz channel 6", 2437, 6}, - {"2.4 GHz channel 11", 2462, 11}, - {"2.4 GHz channel 14", 2484, 14}, - {"5 GHz channel 36", 5180, 36}, - {"5 GHz channel 40", 5200, 40}, - {"5 GHz channel 165", 5825, 165}, - {"6 GHz channel 1", 5955, 1}, - {"6 GHz channel 233", 7115, 233}, - {"Unknown frequency", 1000, 0}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := frequencyToChannel(tt.frequency) - assert.Equal(t, tt.channel, result) - }) - } -} - -func TestSortWiFiNetworks(t *testing.T) { - t.Run("connected network comes first", func(t *testing.T) { - networks := []WiFiNetwork{ - {SSID: "Network1", Signal: 90, Connected: false}, - {SSID: "Network2", Signal: 80, Connected: true}, - {SSID: "Network3", Signal: 70, Connected: false}, - } - - sortWiFiNetworks(networks) - - assert.Equal(t, "Network2", networks[0].SSID) - assert.True(t, networks[0].Connected) - }) - - t.Run("sorts by signal strength", func(t *testing.T) { - networks := []WiFiNetwork{ - {SSID: "Weak", Signal: 40, Secured: true}, - {SSID: "Strong", Signal: 90, Secured: true}, - {SSID: "Medium", Signal: 60, Secured: true}, - } - - sortWiFiNetworks(networks) - - assert.Equal(t, "Strong", networks[0].SSID) - assert.Equal(t, "Medium", networks[1].SSID) - assert.Equal(t, "Weak", networks[2].SSID) - }) - - t.Run("prioritizes open networks with good signal", func(t *testing.T) { - networks := []WiFiNetwork{ - {SSID: "SecureWeak", Signal: 40, Secured: true}, - {SSID: "OpenStrong", Signal: 60, Secured: false}, - {SSID: "SecureStrong", Signal: 90, Secured: true}, - } - - sortWiFiNetworks(networks) - - assert.Equal(t, "OpenStrong", networks[0].SSID) - - openIdx := -1 - weakSecureIdx := -1 - for i, n := range networks { - if n.SSID == "OpenStrong" { - openIdx = i - } - if n.SSID == "SecureWeak" { - weakSecureIdx = i - } - } - assert.Less(t, openIdx, weakSecureIdx, "OpenStrong should come before SecureWeak") - }) - - t.Run("prioritizes saved networks after connected", func(t *testing.T) { - networks := []WiFiNetwork{ - {SSID: "UnsavedStrong", Signal: 95, Saved: false}, - {SSID: "SavedMedium", Signal: 60, Saved: true}, - {SSID: "SavedWeak", Signal: 50, Saved: true}, - {SSID: "UnsavedMedium", Signal: 70, Saved: false}, - } - - sortWiFiNetworks(networks) - - assert.Equal(t, "SavedMedium", networks[0].SSID) - assert.Equal(t, "SavedWeak", networks[1].SSID) - assert.Equal(t, "UnsavedStrong", networks[2].SSID) - assert.Equal(t, "UnsavedMedium", networks[3].SSID) - }) -} - -func TestManager_GetWiFiNetworks(t *testing.T) { - manager := &Manager{ - state: &NetworkState{ - WiFiNetworks: []WiFiNetwork{ - {SSID: "Network1", Signal: 90}, - {SSID: "Network2", Signal: 80}, - }, - }, - } - - networks := manager.GetWiFiNetworks() - - assert.Len(t, networks, 2) - assert.Equal(t, "Network1", networks[0].SSID) - assert.Equal(t, "Network2", networks[1].SSID) - - networks[0].SSID = "Modified" - assert.Equal(t, "Network1", manager.state.WiFiNetworks[0].SSID) -} - -func TestManager_GetNetworkInfo(t *testing.T) { - manager := &Manager{ - state: &NetworkState{ - WiFiNetworks: []WiFiNetwork{ - {SSID: "Network1", Signal: 90, BSSID: "00:11:22:33:44:55"}, - {SSID: "Network2", Signal: 80, BSSID: "AA:BB:CC:DD:EE:FF"}, - }, - }, - } - - t.Run("finds existing network", func(t *testing.T) { - network, err := manager.GetNetworkInfo("Network1") - assert.NoError(t, err) - assert.NotNil(t, network) - assert.Equal(t, "Network1", network.SSID) - assert.Equal(t, uint8(90), network.Signal) - }) - - t.Run("returns error for non-existent network", func(t *testing.T) { - network, err := manager.GetNetworkInfo("NonExistent") - assert.Error(t, err) - assert.Nil(t, network) - assert.Contains(t, err.Error(), "network not found") - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/wired_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/wired_test.go deleted file mode 100644 index 5c346e7..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/network/wired_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package network - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestManager_GetWiredConfigs(t *testing.T) { - manager := &Manager{ - state: &NetworkState{ - EthernetConnected: true, - WiredConnections: []WiredConnection{ - {ID: "Test", IsActive: true}, - }, - }, - } - - configs := manager.GetWiredConfigs() - - assert.Len(t, configs, 1) - assert.Equal(t, "Test", configs[0].ID) -} - -func TestManager_GetEthernetDevices(t *testing.T) { - manager := &Manager{ - state: &NetworkState{ - EthernetDevices: []EthernetDevice{ - {Name: "enp0s3", Connected: true, IP: "192.168.1.100"}, - {Name: "enp0s8", Connected: false}, - }, - }, - } - - devices := manager.GetEthernetDevices() - - assert.Len(t, devices, 2) - assert.Equal(t, "enp0s3", devices[0].Name) - assert.True(t, devices[0].Connected) - assert.Equal(t, "enp0s8", devices[1].Name) - assert.False(t, devices[1].Connected) -} - -func TestManager_GetEthernetDevices_Empty(t *testing.T) { - manager := &Manager{ - state: &NetworkState{}, - } - - devices := manager.GetEthernetDevices() - assert.Empty(t, devices) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/params/params.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/params/params.go deleted file mode 100644 index 1f592c6..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/params/params.go +++ /dev/null @@ -1,113 +0,0 @@ -package params - -import "fmt" - -func Get[T any](params map[string]any, key string) (T, error) { - val, ok := params[key].(T) - if !ok { - var zero T - return zero, fmt.Errorf("missing or invalid '%s' parameter", key) - } - return val, nil -} - -func GetOpt[T any](params map[string]any, key string, def T) T { - if val, ok := params[key].(T); ok { - return val - } - return def -} - -func String(params map[string]any, key string) (string, error) { - return Get[string](params, key) -} - -func StringNonEmpty(params map[string]any, key string) (string, error) { - val, err := Get[string](params, key) - if err != nil || val == "" { - return "", fmt.Errorf("missing or invalid '%s' parameter", key) - } - return val, nil -} - -func StringOpt(params map[string]any, key string, def string) string { - return GetOpt(params, key, def) -} - -func Int(params map[string]any, key string) (int, error) { - val, err := Get[float64](params, key) - if err != nil { - return 0, err - } - return int(val), nil -} - -func IntOpt(params map[string]any, key string, def int) int { - if val, ok := params[key].(float64); ok { - return int(val) - } - return def -} - -func Float(params map[string]any, key string) (float64, error) { - return Get[float64](params, key) -} - -func FloatOpt(params map[string]any, key string, def float64) float64 { - return GetOpt(params, key, def) -} - -func Bool(params map[string]any, key string) (bool, error) { - return Get[bool](params, key) -} - -func BoolOpt(params map[string]any, key string, def bool) bool { - return GetOpt(params, key, def) -} - -func StringMap(params map[string]any, key string) (map[string]string, error) { - rawMap, err := Get[map[string]any](params, key) - if err != nil { - return nil, err - } - result := make(map[string]string) - for k, v := range rawMap { - if str, ok := v.(string); ok { - result[k] = str - } - } - return result, nil -} - -func StringMapOpt(params map[string]any, key string) map[string]string { - rawMap, ok := params[key].(map[string]any) - if !ok { - return nil - } - result := make(map[string]string) - for k, v := range rawMap { - if str, ok := v.(string); ok { - result[k] = str - } - } - return result -} - -func Any(params map[string]any, key string) (any, bool) { - val, ok := params[key] - return val, ok -} - -func AnyMap(params map[string]any, key string) (map[string]any, bool) { - val, ok := params[key].(map[string]any) - return val, ok -} - -func StringAlt(params map[string]any, keys ...string) (string, bool) { - for _, key := range keys { - if val, ok := params[key].(string); ok { - return val, true - } - } - return "", false -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/params/params_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/params/params_test.go deleted file mode 100644 index 99847ff..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/params/params_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package params - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGet(t *testing.T) { - p := map[string]any{"key": "value"} - val, err := Get[string](p, "key") - assert.NoError(t, err) - assert.Equal(t, "value", val) - - _, err = Get[string](p, "missing") - assert.Error(t, err) - - _, err = Get[int](p, "key") - assert.Error(t, err) -} - -func TestGetOpt(t *testing.T) { - p := map[string]any{"key": "value"} - assert.Equal(t, "value", GetOpt(p, "key", "default")) - assert.Equal(t, "default", GetOpt(p, "missing", "default")) -} - -func TestString(t *testing.T) { - p := map[string]any{"s": "hello", "n": 123} - val, err := String(p, "s") - assert.NoError(t, err) - assert.Equal(t, "hello", val) - - _, err = String(p, "n") - assert.Error(t, err) -} - -func TestStringNonEmpty(t *testing.T) { - p := map[string]any{"s": "hello", "empty": ""} - val, err := StringNonEmpty(p, "s") - assert.NoError(t, err) - assert.Equal(t, "hello", val) - - _, err = StringNonEmpty(p, "empty") - assert.Error(t, err) - - _, err = StringNonEmpty(p, "missing") - assert.Error(t, err) -} - -func TestStringOpt(t *testing.T) { - p := map[string]any{"s": "hello"} - assert.Equal(t, "hello", StringOpt(p, "s", "default")) - assert.Equal(t, "default", StringOpt(p, "missing", "default")) -} - -func TestInt(t *testing.T) { - p := map[string]any{"n": float64(42), "s": "str"} - val, err := Int(p, "n") - assert.NoError(t, err) - assert.Equal(t, 42, val) - - _, err = Int(p, "s") - assert.Error(t, err) -} - -func TestIntOpt(t *testing.T) { - p := map[string]any{"n": float64(42)} - assert.Equal(t, 42, IntOpt(p, "n", 0)) - assert.Equal(t, 99, IntOpt(p, "missing", 99)) -} - -func TestFloat(t *testing.T) { - p := map[string]any{"f": 3.14, "s": "str"} - val, err := Float(p, "f") - assert.NoError(t, err) - assert.Equal(t, 3.14, val) - - _, err = Float(p, "s") - assert.Error(t, err) -} - -func TestFloatOpt(t *testing.T) { - p := map[string]any{"f": 3.14} - assert.Equal(t, 3.14, FloatOpt(p, "f", 0)) - assert.Equal(t, 1.0, FloatOpt(p, "missing", 1.0)) -} - -func TestBool(t *testing.T) { - p := map[string]any{"b": true, "s": "str"} - val, err := Bool(p, "b") - assert.NoError(t, err) - assert.True(t, val) - - _, err = Bool(p, "s") - assert.Error(t, err) -} - -func TestBoolOpt(t *testing.T) { - p := map[string]any{"b": true} - assert.True(t, BoolOpt(p, "b", false)) - assert.True(t, BoolOpt(p, "missing", true)) -} - -func TestStringMap(t *testing.T) { - p := map[string]any{ - "m": map[string]any{"a": "1", "b": "2", "c": 3}, - } - val, err := StringMap(p, "m") - assert.NoError(t, err) - assert.Equal(t, map[string]string{"a": "1", "b": "2"}, val) - - _, err = StringMap(p, "missing") - assert.Error(t, err) -} - -func TestStringMapOpt(t *testing.T) { - p := map[string]any{ - "m": map[string]any{"a": "1"}, - } - assert.Equal(t, map[string]string{"a": "1"}, StringMapOpt(p, "m")) - assert.Nil(t, StringMapOpt(p, "missing")) -} - -func TestAny(t *testing.T) { - p := map[string]any{"k": 123} - val, ok := Any(p, "k") - assert.True(t, ok) - assert.Equal(t, 123, val) - - _, ok = Any(p, "missing") - assert.False(t, ok) -} - -func TestAnyMap(t *testing.T) { - inner := map[string]any{"nested": true} - p := map[string]any{"m": inner} - val, ok := AnyMap(p, "m") - assert.True(t, ok) - assert.Equal(t, inner, val) - - _, ok = AnyMap(p, "missing") - assert.False(t, ok) -} - -func TestStringAlt(t *testing.T) { - p := map[string]any{"b": "found"} - val, ok := StringAlt(p, "a", "b", "c") - assert.True(t, ok) - assert.Equal(t, "found", val) - - _, ok = StringAlt(p, "x", "y") - assert.False(t, ok) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/handlers.go deleted file mode 100644 index 33f6564..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/handlers.go +++ /dev/null @@ -1,27 +0,0 @@ -package plugins - -import ( - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -func HandleRequest(conn net.Conn, req models.Request) { - switch req.Method { - case "plugins.list": - HandleList(conn, req) - case "plugins.listInstalled": - HandleListInstalled(conn, req) - case "plugins.install": - HandleInstall(conn, req) - case "plugins.uninstall": - HandleUninstall(conn, req) - case "plugins.update": - HandleUpdate(conn, req) - case "plugins.search": - HandleSearch(conn, req) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/handlers_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/handlers_test.go deleted file mode 100644 index 70202e3..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/handlers_test.go +++ /dev/null @@ -1,196 +0,0 @@ -package plugins - -import ( - "encoding/json" - "testing" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/net" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestHandleList(t *testing.T) { - conn := net.NewMockConn(t) - conn.EXPECT().Write(mock.Anything).Return(0, nil).Maybe() - - req := models.Request{ - ID: 123, - Method: "plugins.list", - Params: map[string]any{}, - } - - HandleList(conn, req) -} - -func TestHandleListInstalled(t *testing.T) { - conn := net.NewMockConn(t) - conn.EXPECT().Write(mock.Anything).Return(0, nil).Maybe() - - req := models.Request{ - ID: 123, - Method: "plugins.listInstalled", - Params: map[string]any{}, - } - - HandleListInstalled(conn, req) -} - -func TestHandleInstallMissingName(t *testing.T) { - conn := net.NewMockConn(t) - var written []byte - conn.EXPECT().Write(mock.Anything).RunAndReturn(func(b []byte) (int, error) { - written = b - return len(b), nil - }).Maybe() - - req := models.Request{ - ID: 123, - Method: "plugins.install", - Params: map[string]any{}, - } - - HandleInstall(conn, req) - - var resp models.Response[SuccessResult] - err := json.Unmarshal(written, &resp) - assert.NoError(t, err) - assert.NotEmpty(t, resp.Error) - assert.Contains(t, resp.Error, "missing or invalid 'name' parameter") -} - -func TestHandleInstallInvalidName(t *testing.T) { - conn := net.NewMockConn(t) - var written []byte - conn.EXPECT().Write(mock.Anything).RunAndReturn(func(b []byte) (int, error) { - written = b - return len(b), nil - }).Maybe() - - req := models.Request{ - ID: 123, - Method: "plugins.install", - Params: map[string]any{ - "name": 123, - }, - } - - HandleInstall(conn, req) - - var resp models.Response[SuccessResult] - err := json.Unmarshal(written, &resp) - assert.NoError(t, err) - assert.NotEmpty(t, resp.Error) -} - -func TestHandleUninstallMissingName(t *testing.T) { - conn := net.NewMockConn(t) - var written []byte - conn.EXPECT().Write(mock.Anything).RunAndReturn(func(b []byte) (int, error) { - written = b - return len(b), nil - }).Maybe() - - req := models.Request{ - ID: 123, - Method: "plugins.uninstall", - Params: map[string]any{}, - } - - HandleUninstall(conn, req) - - var resp models.Response[SuccessResult] - err := json.Unmarshal(written, &resp) - assert.NoError(t, err) - assert.NotEmpty(t, resp.Error) -} - -func TestHandleUpdateMissingName(t *testing.T) { - conn := net.NewMockConn(t) - var written []byte - conn.EXPECT().Write(mock.Anything).RunAndReturn(func(b []byte) (int, error) { - written = b - return len(b), nil - }).Maybe() - - req := models.Request{ - ID: 123, - Method: "plugins.update", - Params: map[string]any{}, - } - - HandleUpdate(conn, req) - - var resp models.Response[SuccessResult] - err := json.Unmarshal(written, &resp) - assert.NoError(t, err) - assert.NotEmpty(t, resp.Error) -} - -func TestHandleSearchMissingQuery(t *testing.T) { - conn := net.NewMockConn(t) - var written []byte - conn.EXPECT().Write(mock.Anything).RunAndReturn(func(b []byte) (int, error) { - written = b - return len(b), nil - }).Maybe() - - req := models.Request{ - ID: 123, - Method: "plugins.search", - Params: map[string]any{}, - } - - HandleSearch(conn, req) - - var resp models.Response[[]PluginInfo] - err := json.Unmarshal(written, &resp) - assert.NoError(t, err) - assert.NotEmpty(t, resp.Error) -} - -func TestSortPluginInfoByFirstParty(t *testing.T) { - plugins := []PluginInfo{ - {Name: "third-party", Repo: "https://github.com/other/test"}, - {Name: "first-party", Repo: "https://github.com/AvengeMedia/test"}, - } - - SortPluginInfoByFirstParty(plugins) - - assert.Equal(t, "first-party", plugins[0].Name) - assert.Equal(t, "third-party", plugins[1].Name) -} - -func TestPluginInfoJSON(t *testing.T) { - info := PluginInfo{ - Name: "test", - Description: "test description", - Installed: true, - FirstParty: true, - } - - data, err := json.Marshal(info) - assert.NoError(t, err) - - var unmarshaled PluginInfo - err = json.Unmarshal(data, &unmarshaled) - assert.NoError(t, err) - assert.Equal(t, info.Name, unmarshaled.Name) - assert.Equal(t, info.Installed, unmarshaled.Installed) -} - -func TestSuccessResult(t *testing.T) { - result := SuccessResult{ - Success: true, - Message: "test message", - } - - data, err := json.Marshal(result) - assert.NoError(t, err) - - var unmarshaled SuccessResult - err = json.Unmarshal(data, &unmarshaled) - assert.NoError(t, err) - assert.True(t, unmarshaled.Success) - assert.Equal(t, "test message", unmarshaled.Message) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/install.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/install.go deleted file mode 100644 index 2c13a0e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/install.go +++ /dev/null @@ -1,69 +0,0 @@ -package plugins - -import ( - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/plugins" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -func HandleInstall(conn net.Conn, req models.Request) { - idOrName, ok := models.Get[string](req, "name") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'name' parameter") - return - } - - registry, err := plugins.NewRegistry() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err)) - return - } - - pluginList, err := registry.List() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to list plugins: %v", err)) - return - } - - // First, try to find by ID (preferred method) - var plugin *plugins.Plugin - for _, p := range pluginList { - if p.ID == idOrName { - plugin = &p - break - } - } - - // Fallback to name for backward compatibility - if plugin == nil { - for _, p := range pluginList { - if p.Name == idOrName { - plugin = &p - break - } - } - } - - if plugin == nil { - models.RespondError(conn, req.ID, fmt.Sprintf("plugin not found: %s", idOrName)) - return - } - - manager, err := plugins.NewManager() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) - return - } - - if err := manager.Install(*plugin); err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to install plugin: %v", err)) - return - } - - models.Respond(conn, req.ID, SuccessResult{ - Success: true, - Message: fmt.Sprintf("plugin installed: %s", plugin.Name), - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/list.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/list.go deleted file mode 100644 index e44d034..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/list.go +++ /dev/null @@ -1,53 +0,0 @@ -package plugins - -import ( - "fmt" - "net" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/plugins" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -func HandleList(conn net.Conn, req models.Request) { - registry, err := plugins.NewRegistry() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err)) - return - } - - pluginList, err := registry.List() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to list plugins: %v", err)) - return - } - - manager, err := plugins.NewManager() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) - return - } - - result := make([]PluginInfo, len(pluginList)) - for i, p := range pluginList { - installed, _ := manager.IsInstalled(p) - result[i] = PluginInfo{ - ID: p.ID, - Name: p.Name, - Category: p.Category, - Author: p.Author, - Description: p.Description, - Repo: p.Repo, - Path: p.Path, - Capabilities: p.Capabilities, - Compositors: p.Compositors, - Dependencies: p.Dependencies, - Installed: installed, - FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"), - Featured: p.Featured, - RequiresDMS: p.RequiresDMS, - } - } - - models.Respond(conn, req.ID, result) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/list_installed.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/list_installed.go deleted file mode 100644 index b771b0e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/list_installed.go +++ /dev/null @@ -1,77 +0,0 @@ -package plugins - -import ( - "fmt" - "net" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/plugins" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -func HandleListInstalled(conn net.Conn, req models.Request) { - manager, err := plugins.NewManager() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) - return - } - - installedNames, err := manager.ListInstalled() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to list installed plugins: %v", err)) - return - } - - registry, err := plugins.NewRegistry() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err)) - return - } - - allPlugins, err := registry.List() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to list plugins: %v", err)) - return - } - - pluginMap := make(map[string]plugins.Plugin) - for _, p := range allPlugins { - pluginMap[p.ID] = p - } - - result := make([]PluginInfo, 0, len(installedNames)) - for _, id := range installedNames { - if plugin, ok := pluginMap[id]; ok { - hasUpdate := false - if hasUpdates, err := manager.HasUpdates(id, plugin); err == nil { - hasUpdate = hasUpdates - } - - result = append(result, PluginInfo{ - ID: plugin.ID, - Name: plugin.Name, - Category: plugin.Category, - Author: plugin.Author, - Description: plugin.Description, - Repo: plugin.Repo, - Path: plugin.Path, - Capabilities: plugin.Capabilities, - Compositors: plugin.Compositors, - Dependencies: plugin.Dependencies, - FirstParty: strings.HasPrefix(plugin.Repo, "https://github.com/AvengeMedia"), - HasUpdate: hasUpdate, - RequiresDMS: plugin.RequiresDMS, - }) - } else { - result = append(result, PluginInfo{ - ID: id, - Name: id, - Note: "not in registry", - }) - } - } - - SortPluginInfoByFirstParty(result) - - models.Respond(conn, req.ID, result) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/search.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/search.go deleted file mode 100644 index 9dbea6d..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/search.go +++ /dev/null @@ -1,74 +0,0 @@ -package plugins - -import ( - "fmt" - "net" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/plugins" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -func HandleSearch(conn net.Conn, req models.Request) { - query, ok := models.Get[string](req, "query") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'query' parameter") - return - } - - registry, err := plugins.NewRegistry() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err)) - return - } - - pluginList, err := registry.List() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to list plugins: %v", err)) - return - } - - searchResults := plugins.FuzzySearch(query, pluginList) - - if category := models.GetOr(req, "category", ""); category != "" { - searchResults = plugins.FilterByCategory(category, searchResults) - } - - if compositor := models.GetOr(req, "compositor", ""); compositor != "" { - searchResults = plugins.FilterByCompositor(compositor, searchResults) - } - - if capability := models.GetOr(req, "capability", ""); capability != "" { - searchResults = plugins.FilterByCapability(capability, searchResults) - } - - searchResults = plugins.SortByFirstParty(searchResults) - - manager, err := plugins.NewManager() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) - return - } - - result := make([]PluginInfo, len(searchResults)) - for i, p := range searchResults { - installed, _ := manager.IsInstalled(p) - result[i] = PluginInfo{ - ID: p.ID, - Name: p.Name, - Category: p.Category, - Author: p.Author, - Description: p.Description, - Repo: p.Repo, - Path: p.Path, - Capabilities: p.Capabilities, - Compositors: p.Compositors, - Dependencies: p.Dependencies, - Installed: installed, - FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"), - RequiresDMS: p.RequiresDMS, - } - } - - models.Respond(conn, req.ID, result) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/types.go deleted file mode 100644 index 8290f72..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/types.go +++ /dev/null @@ -1,25 +0,0 @@ -package plugins - -type PluginInfo struct { - ID string `json:"id"` - Name string `json:"name"` - Category string `json:"category,omitempty"` - Author string `json:"author,omitempty"` - Description string `json:"description,omitempty"` - Repo string `json:"repo,omitempty"` - Path string `json:"path,omitempty"` - Capabilities []string `json:"capabilities,omitempty"` - Compositors []string `json:"compositors,omitempty"` - Dependencies []string `json:"dependencies,omitempty"` - Installed bool `json:"installed,omitempty"` - FirstParty bool `json:"firstParty,omitempty"` - Featured bool `json:"featured,omitempty"` - Note string `json:"note,omitempty"` - HasUpdate bool `json:"hasUpdate,omitempty"` - RequiresDMS string `json:"requires_dms,omitempty"` -} - -type SuccessResult struct { - Success bool `json:"success"` - Message string `json:"message"` -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/uninstall.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/uninstall.go deleted file mode 100644 index 7a3d698..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/uninstall.go +++ /dev/null @@ -1,66 +0,0 @@ -package plugins - -import ( - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/plugins" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -func HandleUninstall(conn net.Conn, req models.Request) { - name, ok := models.Get[string](req, "name") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'name' parameter") - return - } - - manager, err := plugins.NewManager() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) - return - } - - // First try to find in registry (by name or ID) - registry, err := plugins.NewRegistry() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err)) - return - } - - pluginList, _ := registry.List() - plugin := plugins.FindByIDOrName(name, pluginList) - - // If found in registry, use that - if plugin != nil { - installed, err := manager.IsInstalled(*plugin) - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if plugin is installed: %v", err)) - return - } - if !installed { - models.RespondError(conn, req.ID, fmt.Sprintf("plugin not installed: %s", name)) - return - } - if err := manager.Uninstall(*plugin); err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to uninstall plugin: %v", err)) - return - } - models.Respond(conn, req.ID, SuccessResult{ - Success: true, - Message: fmt.Sprintf("plugin uninstalled: %s", plugin.Name), - }) - return - } - - // Not in registry - try to find and uninstall from installed plugins directly - if err := manager.UninstallByIDOrName(name); err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("plugin not found: %s", name)) - return - } - - models.Respond(conn, req.ID, SuccessResult{ - Success: true, - Message: fmt.Sprintf("plugin uninstalled: %s", name), - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/update.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/update.go deleted file mode 100644 index 4ea762a..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/update.go +++ /dev/null @@ -1,64 +0,0 @@ -package plugins - -import ( - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/plugins" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -func HandleUpdate(conn net.Conn, req models.Request) { - name, ok := models.Get[string](req, "name") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'name' parameter") - return - } - - manager, err := plugins.NewManager() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) - return - } - - registry, err := plugins.NewRegistry() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err)) - return - } - - pluginList, _ := registry.List() - plugin := plugins.FindByIDOrName(name, pluginList) - - if plugin != nil { - installed, err := manager.IsInstalled(*plugin) - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if plugin is installed: %v", err)) - return - } - if !installed { - models.RespondError(conn, req.ID, fmt.Sprintf("plugin not installed: %s", name)) - return - } - if err := manager.Update(*plugin); err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to update plugin: %v", err)) - return - } - models.Respond(conn, req.ID, SuccessResult{ - Success: true, - Message: fmt.Sprintf("plugin updated: %s", plugin.Name), - }) - return - } - - // Not in registry - try to update from installed plugins directly - if err := manager.UpdateByIDOrName(name); err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("plugin not found: %s", name)) - return - } - - models.Respond(conn, req.ID, SuccessResult{ - Success: true, - Message: fmt.Sprintf("plugin updated: %s", name), - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/utils.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/utils.go deleted file mode 100644 index 8b89f44..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/plugins/utils.go +++ /dev/null @@ -1,17 +0,0 @@ -package plugins - -import ( - "sort" - "strings" -) - -func SortPluginInfoByFirstParty(pluginInfos []PluginInfo) { - sort.SliceStable(pluginInfos, func(i, j int) bool { - isFirstPartyI := strings.HasPrefix(pluginInfos[i].Repo, "https://github.com/AvengeMedia") - isFirstPartyJ := strings.HasPrefix(pluginInfos[j].Repo, "https://github.com/AvengeMedia") - if isFirstPartyI != isFirstPartyJ { - return isFirstPartyI - } - return false - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/router.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/router.go deleted file mode 100644 index 6b8c10b..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/router.go +++ /dev/null @@ -1,250 +0,0 @@ -package server - -import ( - "fmt" - "net" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups" - serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/location" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" - serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode" - serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput" -) - -func RouteRequest(conn net.Conn, req models.Request) { - if strings.HasPrefix(req.Method, "network.") { - if networkManager == nil { - models.RespondError(conn, req.ID, "network manager not initialized") - return - } - network.HandleRequest(conn, req, networkManager) - return - } - - if strings.HasPrefix(req.Method, "plugins.") { - serverPlugins.HandleRequest(conn, req) - return - } - - if strings.HasPrefix(req.Method, "themes.") { - serverThemes.HandleRequest(conn, req) - return - } - - if strings.HasPrefix(req.Method, "theme.auto.") { - if themeModeManager == nil { - models.RespondError(conn, req.ID, "theme mode manager not initialized") - return - } - thememode.HandleRequest(conn, req, themeModeManager) - return - } - - if strings.HasPrefix(req.Method, "loginctl.") { - if loginctlManager == nil { - models.RespondError(conn, req.ID, "loginctl manager not initialized") - return - } - loginctl.HandleRequest(conn, req, loginctlManager) - return - } - - if strings.HasPrefix(req.Method, "freedesktop.") { - if freedesktopManager == nil { - models.RespondError(conn, req.ID, "freedesktop manager not initialized") - return - } - freedesktop.HandleRequest(conn, req, freedesktopManager) - return - } - - if strings.HasPrefix(req.Method, "wayland.") { - if waylandManager == nil { - models.RespondError(conn, req.ID, "wayland manager not initialized") - return - } - wayland.HandleRequest(conn, req, waylandManager) - return - } - - if strings.HasPrefix(req.Method, "bluetooth.") { - if bluezManager == nil { - models.RespondError(conn, req.ID, "bluetooth manager not initialized") - return - } - bluez.HandleRequest(conn, req, bluezManager) - return - } - - if strings.HasPrefix(req.Method, "browser.") || strings.HasPrefix(req.Method, "apppicker.") { - if appPickerManager == nil { - models.RespondError(conn, req.ID, "apppicker manager not initialized") - return - } - apppicker.HandleRequest(conn, req, appPickerManager) - return - } - - if strings.HasPrefix(req.Method, "cups.") { - if cupsManager == nil { - models.RespondError(conn, req.ID, "CUPS manager not initialized") - return - } - cups.HandleRequest(conn, req, cupsManager) - return - } - - if strings.HasPrefix(req.Method, "dwl.") { - if dwlManager == nil { - models.RespondError(conn, req.ID, "dwl manager not initialized") - return - } - dwl.HandleRequest(conn, req, dwlManager) - return - } - - if strings.HasPrefix(req.Method, "brightness.") { - if brightnessManager == nil { - models.RespondError(conn, req.ID, "brightness manager not initialized") - return - } - brightness.HandleRequest(conn, req, brightnessManager) - return - } - - if strings.HasPrefix(req.Method, "extworkspace.") { - if extWorkspaceManager == nil { - if extWorkspaceAvailable.Load() { - extWorkspaceInitMutex.Lock() - if extWorkspaceManager == nil { - if err := InitializeExtWorkspaceManager(); err != nil { - extWorkspaceInitMutex.Unlock() - models.RespondError(conn, req.ID, "extworkspace manager not available") - return - } - } - extWorkspaceInitMutex.Unlock() - } else { - models.RespondError(conn, req.ID, "extworkspace manager not initialized") - return - } - } - extworkspace.HandleRequest(conn, req, extWorkspaceManager) - return - } - - if strings.HasPrefix(req.Method, "wlroutput.") { - if wlrOutputManager == nil { - models.RespondError(conn, req.ID, "wlroutput manager not initialized") - return - } - wlroutput.HandleRequest(conn, req, wlrOutputManager) - return - } - - if strings.HasPrefix(req.Method, "evdev.") { - if evdevManager == nil { - models.RespondError(conn, req.ID, "evdev manager not initialized") - return - } - evdev.HandleRequest(conn, req, evdevManager) - return - } - - if strings.HasPrefix(req.Method, "dbus.") { - if dbusManager == nil { - models.RespondError(conn, req.ID, "dbus manager not initialized") - return - } - serverDbus.HandleRequest(conn, req, dbusManager, dbusClientID) - return - } - - if strings.HasPrefix(req.Method, "clipboard.") { - switch req.Method { - case "clipboard.getConfig": - cfg := clipboard.LoadConfig() - models.Respond(conn, req.ID, cfg) - return - case "clipboard.setConfig": - handleClipboardSetConfig(conn, req) - return - } - if clipboardManager == nil { - models.RespondError(conn, req.ID, "clipboard manager not initialized") - return - } - clipboard.HandleRequest(conn, req, clipboardManager) - return - } - - if strings.HasPrefix(req.Method, "location.") { - if locationManager == nil { - models.RespondError(conn, req.ID, "location manager not initialized") - return - } - location.HandleRequest(conn, req, locationManager) - return - } - - switch req.Method { - case "ping": - models.Respond(conn, req.ID, "pong") - case "getServerInfo": - info := getServerInfo() - models.Respond(conn, req.ID, info) - case "subscribe": - handleSubscribe(conn, req) - case "matugen.queue": - handleMatugenQueue(conn, req) - case "matugen.status": - handleMatugenStatus(conn, req) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleClipboardSetConfig(conn net.Conn, req models.Request) { - cfg := clipboard.LoadConfig() - - if v, ok := models.Get[float64](req, "maxHistory"); ok { - cfg.MaxHistory = int(v) - } - if v, ok := models.Get[float64](req, "maxEntrySize"); ok { - cfg.MaxEntrySize = int64(v) - } - if v, ok := models.Get[float64](req, "autoClearDays"); ok { - cfg.AutoClearDays = int(v) - } - if v, ok := models.Get[bool](req, "clearAtStartup"); ok { - cfg.ClearAtStartup = v - } - if v, ok := models.Get[bool](req, "disabled"); ok { - cfg.Disabled = v - } - if v, ok := models.Get[float64](req, "maxPinned"); ok { - cfg.MaxPinned = int(v) - } - - if err := clipboard.SaveConfig(cfg); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "config updated"}) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/server.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/server.go deleted file mode 100644 index 58acb62..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/server.go +++ /dev/null @@ -1,1758 +0,0 @@ -package server - -import ( - "bufio" - "encoding/json" - "fmt" - "net" - "os" - "path/filepath" - "strconv" - "strings" - "sync" - "sync/atomic" - "syscall" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups" - serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/location" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" -) - -const APIVersion = 24 - -var CLIVersion = "dev" - -type Capabilities struct { - Capabilities []string `json:"capabilities"` -} - -type ServerInfo struct { - APIVersion int `json:"apiVersion"` - CLIVersion string `json:"cliVersion,omitempty"` - Capabilities []string `json:"capabilities"` -} - -type ServiceEvent struct { - Service string `json:"service"` - Data any `json:"data"` -} - -var networkManager *network.Manager -var loginctlManager *loginctl.Manager -var freedesktopManager *freedesktop.Manager -var waylandManager *wayland.Manager -var bluezManager *bluez.Manager -var appPickerManager *apppicker.Manager -var cupsManager *cups.Manager -var dwlManager *dwl.Manager -var extWorkspaceManager *extworkspace.Manager -var brightnessManager *brightness.Manager -var wlrOutputManager *wlroutput.Manager -var evdevManager *evdev.Manager -var clipboardManager *clipboard.Manager -var dbusManager *serverDbus.Manager -var wlContext *wlcontext.SharedContext -var themeModeManager *thememode.Manager -var trayRecoveryManager *trayrecovery.Manager -var locationManager *location.Manager -var geoClientInstance geolocation.Client - -const dbusClientID = "dms-dbus-client" - -var capabilitySubscribers syncmap.Map[string, chan ServerInfo] -var cupsSubscribers syncmap.Map[string, bool] -var cupsSubscriberCount atomic.Int32 -var extWorkspaceAvailable atomic.Bool -var extWorkspaceInitMutex sync.Mutex - -func getSocketDir() string { - if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" { - return runtime - } - - if os.Getuid() == 0 { - if _, err := os.Stat("/run"); err == nil { - return "/run/dankdots" - } - return "/var/run/dankdots" - } - - return os.TempDir() -} - -func GetSocketPath() string { - return filepath.Join(getSocketDir(), fmt.Sprintf("danklinux-%d.sock", os.Getpid())) -} - -func FindSocket() (string, error) { - dir := getSocketDir() - entries, err := os.ReadDir(dir) - if err != nil { - return "", err - } - - for _, entry := range entries { - if strings.HasPrefix(entry.Name(), "danklinux-") && strings.HasSuffix(entry.Name(), ".sock") { - return filepath.Join(dir, entry.Name()), nil - } - } - return "", fmt.Errorf("no dms socket found") -} - -func cleanupStaleSockets() { - dir := getSocketDir() - entries, err := os.ReadDir(dir) - if err != nil { - return - } - - for _, entry := range entries { - if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".sock") { - continue - } - - pidStr := strings.TrimPrefix(entry.Name(), "danklinux-") - pidStr = strings.TrimSuffix(pidStr, ".sock") - pid, err := strconv.Atoi(pidStr) - if err != nil { - continue - } - - process, err := os.FindProcess(pid) - if err != nil { - socketPath := filepath.Join(dir, entry.Name()) - os.Remove(socketPath) - log.Debugf("Removed stale socket: %s", socketPath) - continue - } - - err = process.Signal(syscall.Signal(0)) - if err != nil { - socketPath := filepath.Join(dir, entry.Name()) - os.Remove(socketPath) - log.Debugf("Removed stale socket: %s", socketPath) - } - } -} - -func InitializeNetworkManager() error { - manager, err := network.NewManager() - if err != nil { - log.Warnf("Failed to initialize network manager: %v", err) - return err - } - - networkManager = manager - - log.Info("Network manager initialized") - return nil -} - -func InitializeLoginctlManager() error { - manager, err := loginctl.NewManager() - if err != nil { - log.Warnf("Failed to initialize loginctl manager: %v", err) - return err - } - - loginctlManager = manager - - log.Info("Loginctl manager initialized") - return nil -} - -func InitializeFreedeskManager() error { - manager, err := freedesktop.NewManager() - if err != nil { - log.Warnf("Failed to initialize freedesktop manager: %v", err) - return err - } - - freedesktopManager = manager - - log.Info("Freedesktop manager initialized") - return nil -} - -func InitializeWaylandManager() error { - log.Info("Attempting to initialize Wayland gamma control...") - - if wlContext == nil { - ctx, err := wlcontext.New() - if err != nil { - log.Errorf("Failed to create shared Wayland context: %v", err) - return err - } - wlContext = ctx - } - - config := wayland.DefaultConfig() - manager, err := wayland.NewManager(wlContext.Display(), config) - if err != nil { - log.Errorf("Failed to initialize wayland manager: %v", err) - return err - } - - waylandManager = manager - - log.Info("Wayland gamma control initialized successfully") - return nil -} - -func InitializeBluezManager() error { - manager, err := bluez.NewManager() - if err != nil { - log.Warnf("Failed to initialize bluez manager: %v", err) - return err - } - - bluezManager = manager - - log.Info("Bluez manager initialized") - return nil -} - -func InitializeAppPickerManager() error { - manager := apppicker.NewManager() - appPickerManager = manager - log.Info("AppPicker manager initialized") - return nil -} - -func InitializeCupsManager() error { - manager, err := cups.NewManager() - if err != nil { - log.Warnf("Failed to initialize cups manager: %v", err) - return err - } - - cupsManager = manager - - log.Info("CUPS manager initialized") - return nil -} - -func InitializeDwlManager() error { - log.Info("Attempting to initialize DWL IPC...") - - if wlContext == nil { - ctx, err := wlcontext.New() - if err != nil { - log.Errorf("Failed to create shared Wayland context: %v", err) - return err - } - wlContext = ctx - } - - manager, err := dwl.NewManager(wlContext.Display()) - if err != nil { - log.Debug("Failed to initialize dwl manager: %v", err) - return err - } - - dwlManager = manager - - log.Info("DWL IPC initialized successfully") - return nil -} - -func InitializeBrightnessManager() error { - manager, err := brightness.NewManager() - if err != nil { - log.Warnf("Failed to initialize brightness manager: %v", err) - return err - } - - brightnessManager = manager - - log.Info("Brightness manager initialized") - return nil -} - -func InitializeExtWorkspaceManager() error { - log.Info("Attempting to initialize ExtWorkspace...") - - if wlContext == nil { - ctx, err := wlcontext.New() - if err != nil { - log.Errorf("Failed to create shared Wayland context: %v", err) - return err - } - wlContext = ctx - } - - manager, err := extworkspace.NewManager(wlContext.Display()) - if err != nil { - log.Debug("Failed to initialize extworkspace manager: %v", err) - return err - } - - extWorkspaceManager = manager - - log.Info("ExtWorkspace initialized successfully") - return nil -} - -func InitializeWlrOutputManager() error { - log.Info("Attempting to initialize WlrOutput management...") - - if wlContext == nil { - ctx, err := wlcontext.New() - if err != nil { - log.Errorf("Failed to create shared Wayland context: %v", err) - return err - } - wlContext = ctx - } - - manager, err := wlroutput.NewManager(wlContext.Display()) - if err != nil { - log.Debug("Failed to initialize wlroutput manager: %v", err) - return err - } - - wlrOutputManager = manager - - log.Info("WlrOutput management initialized successfully") - return nil -} - -func InitializeEvdevManager() error { - manager, err := evdev.InitializeManager() - if err != nil { - log.Warnf("Failed to initialize evdev manager: %v", err) - return err - } - - evdevManager = manager - - log.Info("Evdev manager initialized") - return nil -} - -func InitializeClipboardManager() error { - log.Info("Attempting to initialize clipboard manager...") - - if wlContext == nil { - ctx, err := wlcontext.New() - if err != nil { - log.Errorf("Failed to create shared Wayland context: %v", err) - return err - } - wlContext = ctx - } - - config := clipboard.LoadConfig() - manager, err := clipboard.NewManager(wlContext, config) - if err != nil { - log.Errorf("Failed to initialize clipboard manager: %v", err) - return err - } - - clipboardManager = manager - - log.Info("Clipboard manager initialized successfully") - return nil -} - -func InitializeDbusManager() error { - manager, err := serverDbus.NewManager() - if err != nil { - log.Warnf("Failed to initialize dbus manager: %v", err) - return err - } - - dbusManager = manager - - log.Info("DBus manager initialized") - return nil -} - -func InitializeThemeModeManager() error { - manager := thememode.NewManager() - themeModeManager = manager - - log.Info("Theme mode automation manager initialized") - return nil -} - -func InitializeTrayRecoveryManager() error { - manager, err := trayrecovery.NewManager() - if err != nil { - return err - } - - trayRecoveryManager = manager - - log.Info("TrayRecovery manager initialized") - return nil -} - -func InitializeLocationManager(geoClient geolocation.Client) error { - manager, err := location.NewManager(geoClient) - if err != nil { - log.Warnf("Failed to initialize location manager: %v", err) - return err - } - - locationManager = manager - - log.Info("Location manager initialized") - return nil -} - -func handleConnection(conn net.Conn) { - defer conn.Close() - - caps := getCapabilities() - capsData, _ := json.Marshal(caps) - conn.Write(capsData) - conn.Write([]byte("\n")) - scanner := bufio.NewScanner(conn) - for scanner.Scan() { - line := scanner.Bytes() - - var req models.Request - if err := json.Unmarshal(line, &req); err != nil { - log.Warnf("handleConnection: Failed to unmarshal JSON: %v, line: %s", err, string(line)) - models.RespondError(conn, 0, "invalid json") - continue - } - - go RouteRequest(conn, req) - } -} - -func getCapabilities() Capabilities { - caps := []string{"plugins"} - - if networkManager != nil { - caps = append(caps, "network") - } - - if loginctlManager != nil { - caps = append(caps, "loginctl") - } - - if freedesktopManager != nil { - caps = append(caps, "freedesktop") - } - - if waylandManager != nil { - caps = append(caps, "gamma") - } - - if bluezManager != nil { - caps = append(caps, "bluetooth") - } - - if appPickerManager != nil { - caps = append(caps, "browser") - } - - if cupsManager != nil { - caps = append(caps, "cups") - } - - if dwlManager != nil { - caps = append(caps, "dwl") - } - - if extWorkspaceAvailable.Load() { - caps = append(caps, "extworkspace") - } - - if brightnessManager != nil { - caps = append(caps, "brightness") - } - - if wlrOutputManager != nil { - caps = append(caps, "wlroutput") - } - - if evdevManager != nil { - caps = append(caps, "evdev") - } - - if clipboardManager != nil { - caps = append(caps, "clipboard") - } - - if themeModeManager != nil { - caps = append(caps, "theme.auto") - } - - if dbusManager != nil { - caps = append(caps, "dbus") - } - - return Capabilities{Capabilities: caps} -} - -func getServerInfo() ServerInfo { - caps := []string{"plugins"} - - if networkManager != nil { - caps = append(caps, "network") - } - - if loginctlManager != nil { - caps = append(caps, "loginctl") - } - - if freedesktopManager != nil { - caps = append(caps, "freedesktop") - } - - if waylandManager != nil { - caps = append(caps, "gamma") - } - - if bluezManager != nil { - caps = append(caps, "bluetooth") - } - - if appPickerManager != nil { - caps = append(caps, "browser") - } - - if cupsManager != nil { - caps = append(caps, "cups") - } - - if dwlManager != nil { - caps = append(caps, "dwl") - } - - if extWorkspaceAvailable.Load() { - caps = append(caps, "extworkspace") - } - - if brightnessManager != nil { - caps = append(caps, "brightness") - } - - if wlrOutputManager != nil { - caps = append(caps, "wlroutput") - } - - if evdevManager != nil { - caps = append(caps, "evdev") - } - - if clipboardManager != nil { - caps = append(caps, "clipboard") - } - - if themeModeManager != nil { - caps = append(caps, "theme.auto") - } - - if locationManager != nil { - caps = append(caps, "location") - } - - if dbusManager != nil { - caps = append(caps, "dbus") - } - - return ServerInfo{ - APIVersion: APIVersion, - CLIVersion: CLIVersion, - Capabilities: caps, - } -} - -func notifyCapabilityChange() { - info := getServerInfo() - capabilitySubscribers.Range(func(key string, ch chan ServerInfo) bool { - select { - case ch <- info: - default: - } - return true - }) -} - -func handleSubscribe(conn net.Conn, req models.Request) { - clientID := fmt.Sprintf("meta-client-%p", conn) - - var services []string - if servicesParam, ok := models.Get[[]any](req, "services"); ok { - for _, s := range servicesParam { - if str, ok := s.(string); ok { - services = append(services, str) - } - } - } - - if len(services) == 0 { - services = []string{"all"} - } - - subscribeAll := false - for _, s := range services { - if s == "all" { - subscribeAll = true - break - } - } - - var wg sync.WaitGroup - eventChan := make(chan ServiceEvent, 256) - stopChan := make(chan struct{}) - - capChan := make(chan ServerInfo, 64) - capabilitySubscribers.Store(clientID+"-capabilities", capChan) - - wg.Add(1) - go func() { - defer wg.Done() - defer capabilitySubscribers.Delete(clientID + "-capabilities") - - for { - select { - case info, ok := <-capChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "server", Data: info}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - - shouldSubscribe := func(service string) bool { - if subscribeAll { - return true - } - for _, s := range services { - if s == service { - return true - } - } - return false - } - - if shouldSubscribe("network") && networkManager != nil { - wg.Add(1) - netChan := networkManager.Subscribe(clientID + "-network") - go func() { - defer wg.Done() - defer networkManager.Unsubscribe(clientID + "-network") - - initialState := networkManager.GetState() - select { - case eventChan <- ServiceEvent{Service: "network", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-netChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "network", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("network.credentials") && networkManager != nil { - wg.Add(1) - credChan := networkManager.SubscribeCredentials(clientID + "-credentials") - go func() { - defer wg.Done() - defer networkManager.UnsubscribeCredentials(clientID + "-credentials") - - for { - select { - case prompt, ok := <-credChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "network.credentials", Data: prompt}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("loginctl") && loginctlManager != nil { - wg.Add(1) - loginChan := loginctlManager.Subscribe(clientID + "-loginctl") - go func() { - defer wg.Done() - defer loginctlManager.Unsubscribe(clientID + "-loginctl") - - initialState := loginctlManager.GetState() - select { - case eventChan <- ServiceEvent{Service: "loginctl", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-loginChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "loginctl", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("freedesktop") && freedesktopManager != nil { - wg.Add(1) - freedesktopChan := freedesktopManager.Subscribe(clientID + "-freedesktop") - go func() { - defer wg.Done() - defer freedesktopManager.Unsubscribe(clientID + "-freedesktop") - - initialState := freedesktopManager.GetState() - select { - case eventChan <- ServiceEvent{Service: "freedesktop", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-freedesktopChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "freedesktop", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("freedesktop.screensaver") && freedesktopManager != nil { - wg.Add(1) - screensaverChan := freedesktopManager.SubscribeScreensaver(clientID + "-screensaver") - go func() { - defer wg.Done() - defer freedesktopManager.UnsubscribeScreensaver(clientID + "-screensaver") - - initialState := freedesktopManager.GetScreensaverState() - select { - case eventChan <- ServiceEvent{Service: "freedesktop.screensaver", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-screensaverChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "freedesktop.screensaver", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("gamma") && waylandManager != nil { - wg.Add(1) - waylandChan := waylandManager.Subscribe(clientID + "-gamma") - go func() { - defer wg.Done() - defer waylandManager.Unsubscribe(clientID + "-gamma") - - initialState := waylandManager.GetState() - select { - case eventChan <- ServiceEvent{Service: "gamma", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-waylandChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "gamma", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("theme.auto") && themeModeManager != nil { - wg.Add(1) - themeAutoChan := themeModeManager.Subscribe(clientID + "-theme-auto") - go func() { - defer wg.Done() - defer themeModeManager.Unsubscribe(clientID + "-theme-auto") - - initialState := themeModeManager.GetState() - select { - case eventChan <- ServiceEvent{Service: "theme.auto", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-themeAutoChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "theme.auto", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("bluetooth") && bluezManager != nil { - wg.Add(1) - bluezChan := bluezManager.Subscribe(clientID + "-bluetooth") - go func() { - defer wg.Done() - defer bluezManager.Unsubscribe(clientID + "-bluetooth") - - initialState := bluezManager.GetState() - select { - case eventChan <- ServiceEvent{Service: "bluetooth", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-bluezChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "bluetooth", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("bluetooth.pairing") && bluezManager != nil { - wg.Add(1) - pairingChan := bluezManager.SubscribePairing(clientID + "-pairing") - go func() { - defer wg.Done() - defer bluezManager.UnsubscribePairing(clientID + "-pairing") - - for { - select { - case prompt, ok := <-pairingChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "bluetooth.pairing", Data: prompt}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("browser") && appPickerManager != nil { - wg.Add(1) - appPickerChan := appPickerManager.Subscribe(clientID + "-browser") - go func() { - defer wg.Done() - defer appPickerManager.Unsubscribe(clientID + "-browser") - - for { - select { - case event, ok := <-appPickerChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "browser.open_requested", Data: event}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("cups") { - cupsSubscribers.Store(clientID+"-cups", true) - count := cupsSubscriberCount.Add(1) - - if count == 1 { - if err := InitializeCupsManager(); err != nil { - log.Warnf("Failed to initialize CUPS manager for subscription: %v", err) - } else { - notifyCapabilityChange() - } - } - - if cupsManager != nil { - wg.Add(1) - cupsChan := cupsManager.Subscribe(clientID + "-cups") - go func() { - defer wg.Done() - defer func() { - cupsManager.Unsubscribe(clientID + "-cups") - cupsSubscribers.Delete(clientID + "-cups") - count := cupsSubscriberCount.Add(-1) - - if count == 0 { - log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager") - if cupsManager != nil { - cupsManager.Close() - cupsManager = nil - notifyCapabilityChange() - } - } - }() - - initialState := cupsManager.GetState() - select { - case eventChan <- ServiceEvent{Service: "cups", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-cupsChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "cups", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - } - - if shouldSubscribe("dwl") && dwlManager != nil { - wg.Add(1) - dwlChan := dwlManager.Subscribe(clientID + "-dwl") - go func() { - defer wg.Done() - defer dwlManager.Unsubscribe(clientID + "-dwl") - - initialState := dwlManager.GetState() - select { - case eventChan <- ServiceEvent{Service: "dwl", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-dwlChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "dwl", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("extworkspace") { - if extWorkspaceManager == nil && extWorkspaceAvailable.Load() { - extWorkspaceInitMutex.Lock() - if extWorkspaceManager == nil { - if err := InitializeExtWorkspaceManager(); err != nil { - log.Warnf("Failed to initialize ExtWorkspace manager for subscription: %v", err) - } - } - extWorkspaceInitMutex.Unlock() - } - - if extWorkspaceManager != nil { - wg.Add(1) - extWorkspaceChan := extWorkspaceManager.Subscribe(clientID + "-extworkspace") - go func() { - defer wg.Done() - defer extWorkspaceManager.Unsubscribe(clientID + "-extworkspace") - - initialState := extWorkspaceManager.GetState() - select { - case eventChan <- ServiceEvent{Service: "extworkspace", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-extWorkspaceChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "extworkspace", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - } - - if shouldSubscribe("brightness") && brightnessManager != nil { - wg.Add(2) - brightnessStateChan := brightnessManager.Subscribe(clientID + "-brightness-state") - brightnessUpdateChan := brightnessManager.SubscribeUpdates(clientID + "-brightness-updates") - - go func() { - defer wg.Done() - defer brightnessManager.Unsubscribe(clientID + "-brightness-state") - - initialState := brightnessManager.GetState() - select { - case eventChan <- ServiceEvent{Service: "brightness", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-brightnessStateChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "brightness", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - - go func() { - defer wg.Done() - defer brightnessManager.UnsubscribeUpdates(clientID + "-brightness-updates") - - for { - select { - case update, ok := <-brightnessUpdateChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "brightness.update", Data: update}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("wlroutput") && wlrOutputManager != nil { - wg.Add(1) - wlrOutputChan := wlrOutputManager.Subscribe(clientID + "-wlroutput") - go func() { - defer wg.Done() - defer wlrOutputManager.Unsubscribe(clientID + "-wlroutput") - - initialState := wlrOutputManager.GetState() - select { - case eventChan <- ServiceEvent{Service: "wlroutput", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-wlrOutputChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "wlroutput", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("evdev") && evdevManager != nil { - wg.Add(1) - evdevChan := evdevManager.Subscribe(clientID + "-evdev") - go func() { - defer wg.Done() - defer evdevManager.Unsubscribe(clientID + "-evdev") - - initialState := evdevManager.GetState() - select { - case eventChan <- ServiceEvent{Service: "evdev", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-evdevChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "evdev", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("clipboard") && clipboardManager != nil { - wg.Add(1) - clipboardChan := clipboardManager.Subscribe(clientID + "-clipboard") - go func() { - defer wg.Done() - defer clipboardManager.Unsubscribe(clientID + "-clipboard") - - initialState := clipboardManager.GetState() - select { - case eventChan <- ServiceEvent{Service: "clipboard", Data: initialState}: - case <-stopChan: - return - } - - for { - select { - case state, ok := <-clipboardChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "clipboard", Data: state}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - if shouldSubscribe("dbus") && dbusManager != nil { - wg.Add(1) - dbusChan := dbusManager.SubscribeSignals(dbusClientID) - go func() { - defer wg.Done() - defer dbusManager.UnsubscribeSignals(dbusClientID) - - for { - select { - case event, ok := <-dbusChan: - if !ok { - return - } - select { - case eventChan <- ServiceEvent{Service: "dbus", Data: event}: - case <-stopChan: - return - } - case <-stopChan: - return - } - } - }() - } - - go func() { - wg.Wait() - close(eventChan) - }() - - info := getServerInfo() - if err := json.NewEncoder(conn).Encode(models.Response[ServiceEvent]{ - ID: req.ID, - Result: &ServiceEvent{Service: "server", Data: info}, - }); err != nil { - close(stopChan) - return - } - - for event := range eventChan { - if err := json.NewEncoder(conn).Encode(models.Response[ServiceEvent]{ - ID: req.ID, - Result: &event, - }); err != nil { - close(stopChan) - return - } - } -} - -func cleanupManagers() { - if networkManager != nil { - networkManager.Close() - } - if loginctlManager != nil { - loginctlManager.Close() - } - if freedesktopManager != nil { - freedesktopManager.Close() - } - if waylandManager != nil { - waylandManager.Close() - } - if bluezManager != nil { - bluezManager.Close() - } - if appPickerManager != nil { - appPickerManager.Close() - } - if cupsManager != nil { - cupsManager.Close() - } - if dwlManager != nil { - dwlManager.Close() - } - if extWorkspaceManager != nil { - extWorkspaceManager.Close() - } - if brightnessManager != nil { - brightnessManager.Close() - } - if wlrOutputManager != nil { - wlrOutputManager.Close() - } - if evdevManager != nil { - evdevManager.Close() - } - if clipboardManager != nil { - clipboardManager.Close() - } - if dbusManager != nil { - dbusManager.Close() - } - if themeModeManager != nil { - themeModeManager.Close() - } - if trayRecoveryManager != nil { - trayRecoveryManager.Close() - } - if wlContext != nil { - wlContext.Close() - } - if locationManager != nil { - locationManager.Close() - } - if geoClientInstance != nil { - geoClientInstance.Close() - } -} - -func Start(printDocs bool) error { - cleanupStaleSockets() - - socketPath := GetSocketPath() - os.Remove(socketPath) - - listener, err := net.Listen("unix", socketPath) - if err != nil { - return err - } - defer listener.Close() - defer cleanupManagers() - - log.Infof("DMS API Server listening on: %s", socketPath) - log.Infof("API Version: %d", APIVersion) - log.Info("Protocol: JSON over Unix socket") - log.Info("Request format: {\"id\": , \"method\": \"...\", \"params\": {...}}") - log.Info("Response format: {\"id\": , \"result\": {...}} or {\"id\": , \"error\": \"...\"}") - log.Info("") - if printDocs { - log.Info("Available methods:") - log.Info(" ping - Test connection") - log.Info(" getServerInfo - Get server info (API version and capabilities)") - log.Info(" subscribe - Subscribe to multiple services (params: services [default: all])") - log.Info("Plugins:") - log.Info(" plugins.list - List all plugins") - log.Info(" plugins.listInstalled - List installed plugins") - log.Info(" plugins.install - Install plugin (params: name)") - log.Info(" plugins.uninstall - Uninstall plugin (params: name)") - log.Info(" plugins.update - Update plugin (params: name)") - log.Info(" plugins.search - Search plugins (params: query, category?, compositor?, capability?)") - log.Info("Network:") - log.Info(" network.getState - Get current network state") - log.Info(" network.wifi.scan - Scan for WiFi networks (params: device?)") - log.Info(" network.wifi.networks - Get WiFi network list") - log.Info(" network.wifi.connect - Connect to WiFi (params: ssid, password?, username?, device?, eapMethod?, phase2Auth?, caCertPath?, clientCertPath?, privateKeyPath?, useSystemCACerts?)") - log.Info(" network.wifi.disconnect - Disconnect WiFi (params: device?)") - log.Info(" network.wifi.forget - Forget network (params: ssid)") - log.Info(" network.wifi.toggle - Toggle WiFi radio") - log.Info(" network.wifi.enable - Enable WiFi") - log.Info(" network.wifi.disable - Disable WiFi") - log.Info(" network.wifi.setAutoconnect - Set network autoconnect (params: ssid, autoconnect)") - log.Info(" network.ethernet.connect - Connect Ethernet") - log.Info(" network.ethernet.connect.config - Connect Ethernet to a specific configuration") - log.Info(" network.ethernet.disconnect - Disconnect Ethernet") - log.Info(" network.vpn.profiles - List VPN profiles") - log.Info(" network.vpn.active - List active VPN connections") - log.Info(" network.vpn.connect - Connect VPN (params: uuidOrName|name|uuid, singleActive?)") - log.Info(" network.vpn.disconnect - Disconnect VPN (params: uuidOrName|name|uuid)") - log.Info(" network.vpn.disconnectAll - Disconnect all VPNs") - log.Info(" network.vpn.clearCredentials - Clear saved VPN credentials (params: uuidOrName|name|uuid)") - log.Info(" network.vpn.plugins - List available VPN plugins") - log.Info(" network.vpn.import - Import VPN from file (params: file|path, name?)") - log.Info(" network.vpn.getConfig - Get VPN configuration (params: uuid|name|uuidOrName)") - log.Info(" network.vpn.updateConfig - Update VPN configuration (params: uuid, name?, autoconnect?, data?)") - log.Info(" network.vpn.delete - Delete VPN connection (params: uuid|name|uuidOrName)") - log.Info(" network.preference.set - Set preference (params: preference [auto|wifi|ethernet])") - log.Info(" network.info - Get network info (params: ssid)") - log.Info(" network.credentials.submit - Submit credentials for prompt (params: token, secrets, save?)") - log.Info(" network.credentials.cancel - Cancel credential prompt (params: token)") - log.Info(" network.subscribe - Subscribe to network state changes (streaming)") - log.Info("Loginctl:") - log.Info(" loginctl.getState - Get current session state") - log.Info(" loginctl.lock - Lock session") - log.Info(" loginctl.unlock - Unlock session") - log.Info(" loginctl.activate - Activate session") - log.Info(" loginctl.setIdleHint - Set idle hint (params: idle)") - log.Info(" loginctl.setLockBeforeSuspend - Set lock before suspend (params: enabled)") - log.Info(" loginctl.setSleepInhibitorEnabled - Enable/disable sleep inhibitor (params: enabled)") - log.Info(" loginctl.lockerReady - Signal locker UI is ready (releases sleep inhibitor)") - log.Info(" loginctl.terminate - Terminate session") - log.Info(" loginctl.subscribe - Subscribe to session state changes (streaming)") - log.Info("Freedesktop:") - log.Info(" freedesktop.getState - Get accounts & settings state") - log.Info(" freedesktop.accounts.setIconFile - Set profile icon (params: path)") - log.Info(" freedesktop.accounts.setRealName - Set real name (params: name)") - log.Info(" freedesktop.accounts.setEmail - Set email (params: email)") - log.Info(" freedesktop.accounts.setLanguage - Set language (params: language)") - log.Info(" freedesktop.accounts.setLocation - Set location (params: location)") - log.Info(" freedesktop.accounts.getUserIconFile - Get user icon (params: username)") - log.Info(" freedesktop.settings.getColorScheme - Get color scheme") - log.Info(" freedesktop.settings.setIconTheme - Set icon theme (params: iconTheme)") - log.Info("Wayland:") - log.Info(" wayland.gamma.getState - Get current gamma control state") - log.Info(" wayland.gamma.setTemperature - Set temperature range (params: low, high)") - log.Info(" wayland.gamma.setLocation - Set location (params: latitude, longitude)") - log.Info(" wayland.gamma.setManualTimes - Set manual times (params: sunrise, sunset)") - log.Info(" wayland.gamma.setGamma - Set gamma value (params: gamma)") - log.Info(" wayland.gamma.setEnabled - Enable/disable gamma control (params: enabled)") - log.Info(" wayland.gamma.subscribe - Subscribe to gamma state changes (streaming)") - log.Info("Theme automation:") - log.Info(" theme.auto.getState - Get current theme automation state") - log.Info(" theme.auto.setEnabled - Enable/disable theme automation (params: enabled)") - log.Info(" theme.auto.setMode - Set automation mode (params: mode [time|location])") - log.Info(" theme.auto.setSchedule - Set time schedule (params: startHour, startMinute, endHour, endMinute)") - log.Info(" theme.auto.setLocation - Set location (params: latitude, longitude)") - log.Info(" theme.auto.setUseIPLocation - Use IP location (params: use)") - log.Info(" theme.auto.trigger - Trigger immediate re-evaluation") - log.Info(" theme.auto.subscribe - Subscribe to theme automation state changes (streaming)") - log.Info("Bluetooth:") - log.Info(" bluetooth.getState - Get current bluetooth state") - log.Info(" bluetooth.startDiscovery - Start device discovery") - log.Info(" bluetooth.stopDiscovery - Stop device discovery") - log.Info(" bluetooth.setPowered - Set adapter power state (params: powered)") - log.Info(" bluetooth.pair - Pair with device (params: device)") - log.Info(" bluetooth.connect - Connect to device (params: device)") - log.Info(" bluetooth.disconnect - Disconnect from device (params: device)") - log.Info(" bluetooth.remove - Remove/unpair device (params: device)") - log.Info(" bluetooth.trust - Trust device (params: device)") - log.Info(" bluetooth.untrust - Untrust device (params: device)") - log.Info(" bluetooth.pairing.submit - Submit pairing response (params: token, secrets?, accept?)") - log.Info(" bluetooth.pairing.cancel - Cancel pairing prompt (params: token)") - log.Info(" bluetooth.subscribe - Subscribe to bluetooth state changes (streaming)") - log.Info("CUPS:") - log.Info(" cups.getPrinters - Get printers list") - log.Info(" cups.getJobs - Get non-completed jobs list (params: printerName)") - log.Info(" cups.pausePrinter - Pause printer (params: printerName)") - log.Info(" cups.resumePrinter - Resume printer (params: printerName)") - log.Info(" cups.cancelJob - Cancel job (params: printerName, jobID)") - log.Info(" cups.purgeJobs - Cancel all jobs (params: printerName)") - log.Info("DWL:") - log.Info(" dwl.getState - Get current dwl state (tags, windows, layouts, keyboard)") - log.Info(" dwl.setTags - Set active tags (params: output, tagmask, toggleTagset)") - log.Info(" dwl.setClientTags - Set focused client tags (params: output, andTags, xorTags)") - log.Info(" dwl.setLayout - Set layout (params: output, index)") - log.Info(" dwl.subscribe - Subscribe to dwl state changes (streaming)") - log.Info(" Output state includes:") - log.Info(" - tags : Tag states (active, clients, focused)") - log.Info(" - layoutSymbol : Current layout name") - log.Info(" - title : Focused window title") - log.Info(" - appId : Focused window app ID") - log.Info(" - kbLayout : Current keyboard layout") - log.Info(" - keymode : Current keybind mode") - log.Info("ExtWorkspace:") - log.Info(" extworkspace.getState - Get current workspace state (groups, workspaces)") - log.Info(" extworkspace.activateWorkspace - Activate workspace (params: groupID, workspaceID)") - log.Info(" extworkspace.deactivateWorkspace - Deactivate workspace (params: groupID, workspaceID)") - log.Info(" extworkspace.removeWorkspace - Remove workspace (params: groupID, workspaceID)") - log.Info(" extworkspace.createWorkspace - Create workspace (params: groupID, name)") - log.Info(" extworkspace.subscribe - Subscribe to workspace state changes (streaming)") - log.Info("Brightness:") - log.Info(" brightness.getState - Get current brightness state for all devices") - log.Info(" brightness.setBrightness - Set device brightness (params: device, percent)") - log.Info(" brightness.increment - Increment device brightness (params: device, step?)") - log.Info(" brightness.decrement - Decrement device brightness (params: device, step?)") - log.Info(" brightness.rescan - Rescan for brightness devices (e.g., after plugging in monitor)") - log.Info(" brightness.subscribe - Subscribe to brightness state changes (streaming)") - log.Info(" Subscription events:") - log.Info(" - brightness : Full device list (on rescan, DDC discovery, device changes)") - log.Info(" - brightness.update: Single device update (on brightness change for efficiency)") - log.Info("WlrOutput:") - log.Info(" wlroutput.getState - Get current output configuration state") - log.Info(" wlroutput.applyConfiguration - Apply output configuration (params: heads)") - log.Info(" wlroutput.testConfiguration - Test output configuration without applying (params: heads)") - log.Info(" wlroutput.subscribe - Subscribe to output state changes (streaming)") - log.Info(" Head configuration params:") - log.Info(" - name : Output name (required)") - log.Info(" - enabled : Enable/disable output (required)") - log.Info(" - modeId : Mode ID from available modes (optional)") - log.Info(" - customMode : Custom mode {width, height, refresh} (optional)") - log.Info(" - position : Position {x, y} (optional)") - log.Info(" - transform : Transform value (optional)") - log.Info(" - scale : Scale value (optional)") - log.Info(" - adaptiveSync : Adaptive sync state (optional)") - log.Info("Evdev:") - log.Info(" evdev.getState - Get current evdev state (caps lock)") - log.Info(" evdev.subscribe - Subscribe to evdev state changes (streaming)") - log.Info("Clipboard:") - log.Info(" clipboard.getState - Get clipboard state (enabled, history, current)") - log.Info(" clipboard.getHistory - Get clipboard history with previews") - log.Info(" clipboard.getEntry - Get full entry by ID (params: id)") - log.Info(" clipboard.deleteEntry - Delete entry by ID (params: id)") - log.Info(" clipboard.clearHistory - Clear all clipboard history") - log.Info(" clipboard.copy - Copy text to clipboard (params: text)") - log.Info(" clipboard.paste - Get current clipboard text") - log.Info(" clipboard.search - Search history (params: query?, mimeType?, isImage?, limit?, offset?, before?, after?)") - log.Info(" clipboard.getConfig - Get clipboard configuration") - log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)") - log.Info(" clipboard.subscribe - Subscribe to clipboard state changes (streaming)") - log.Info("Location:") - log.Info(" location.getState - Get current location state") - log.Info(" location.subscribe - Subscribe to location changes (streaming)") - log.Info("") - } - log.Info("Initializing managers...") - log.Info("") - - go func() { - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() - - if err := InitializeNetworkManager(); err != nil { - log.Warnf("Network manager unavailable: %v", err) - } else { - notifyCapabilityChange() - return - } - - for range ticker.C { - if networkManager != nil { - return - } - if err := InitializeNetworkManager(); err == nil { - log.Info("Network manager initialized") - notifyCapabilityChange() - return - } - } - }() - - loginctlReady := make(chan struct{}) - freedesktopReady := make(chan struct{}) - - go func() { - defer close(loginctlReady) - if err := InitializeLoginctlManager(); err != nil { - log.Warnf("Loginctl manager unavailable: %v", err) - } else { - notifyCapabilityChange() - } - }() - - go func() { - defer close(freedesktopReady) - if err := InitializeFreedeskManager(); err != nil { - log.Warnf("Freedesktop manager unavailable: %v", err) - } else if freedesktopManager != nil { - freedesktopManager.NotifySubscribers() - notifyCapabilityChange() - } - }() - - // Bridge loginctl lock state to the freedesktop/gnome screensaver - // ActiveChanged signal so apps like Bitwarden can detect screen lock. - go func() { - <-loginctlReady - <-freedesktopReady - - if loginctlManager == nil || freedesktopManager == nil { - return - } - - ch := loginctlManager.Subscribe("dms-lock-bridge") - defer loginctlManager.Unsubscribe("dms-lock-bridge") - - initial := loginctlManager.GetState() - lastLocked := initial.Locked - freedesktopManager.SetScreenLockActive(lastLocked) - - for state := range ch { - if state.Locked != lastLocked { - lastLocked = state.Locked - freedesktopManager.SetScreenLockActive(lastLocked) - } - } - }() - - if err := InitializeWaylandManager(); err != nil { - log.Warnf("Wayland manager unavailable: %v", err) - } - - if err := InitializeThemeModeManager(); err != nil { - log.Warnf("Theme mode manager unavailable: %v", err) - } else { - notifyCapabilityChange() - go func() { - <-loginctlReady - if loginctlManager == nil { - return - } - themeModeManager.WatchLoginctl(loginctlManager) - }() - } - - go func() { - <-loginctlReady - if loginctlManager == nil { - return - } - if err := InitializeTrayRecoveryManager(); err != nil { - log.Warnf("TrayRecovery manager unavailable: %v", err) - } else { - trayRecoveryManager.WatchLoginctl(loginctlManager) - } - }() - - go func() { - geoClient := geolocation.NewClient() - geoClientInstance = geoClient - - if waylandManager != nil { - waylandManager.SetGeoClient(geoClient) - } - if themeModeManager != nil { - themeModeManager.SetGeoClient(geoClient) - } - - if err := InitializeLocationManager(geoClient); err != nil { - log.Warnf("Location manager unavailable: %v", err) - } else { - notifyCapabilityChange() - } - }() - - go func() { - if err := InitializeBluezManager(); err != nil { - log.Warnf("Bluez manager unavailable: %v", err) - } else { - notifyCapabilityChange() - } - }() - - if err := InitializeAppPickerManager(); err != nil { - log.Debugf("AppPicker manager unavailable: %v", err) - } - - if err := InitializeDwlManager(); err != nil { - log.Debugf("DWL manager unavailable: %v", err) - } - - if extworkspace.CheckCapability() { - extWorkspaceAvailable.Store(true) - log.Info("ExtWorkspace capability detected and will be available on subscription") - } else { - log.Debug("ExtWorkspace capability not available") - extWorkspaceAvailable.Store(false) - } - - if err := InitializeWlrOutputManager(); err != nil { - log.Debugf("WlrOutput manager unavailable: %v", err) - } - - fatalErrChan := make(chan error, 1) - if wlrOutputManager != nil { - go func() { - err := <-wlrOutputManager.FatalError() - fatalErrChan <- fmt.Errorf("WlrOutput fatal error: %w", err) - }() - } - if wlContext != nil { - go func() { - err := <-wlContext.FatalError() - fatalErrChan <- fmt.Errorf("wayland context fatal error: %w", err) - }() - } - - go func() { - if err := InitializeBrightnessManager(); err != nil { - log.Warnf("Brightness manager unavailable: %v", err) - } else { - notifyCapabilityChange() - } - }() - - go func() { - if err := InitializeEvdevManager(); err != nil { - log.Debugf("Evdev manager unavailable: %v", err) - } else { - notifyCapabilityChange() - } - }() - - go func() { - if err := InitializeClipboardManager(); err != nil { - log.Warnf("Clipboard manager unavailable: %v", err) - } - if wlContext != nil { - wlContext.Start() - log.Info("Wayland event dispatcher started") - } - }() - - go func() { - if err := InitializeDbusManager(); err != nil { - log.Warnf("DBus manager unavailable: %v", err) - } else { - notifyCapabilityChange() - } - }() - - log.Info("") - log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities) - - listenerErrChan := make(chan error, 1) - - go func() { - for { - conn, err := listener.Accept() - if err != nil { - listenerErrChan <- err - return - } - go handleConnection(conn) - } - }() - - select { - case err := <-listenerErrChan: - return err - case err := <-fatalErrChan: - return err - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/server_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/server_test.go deleted file mode 100644 index 404f624..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/server_test.go +++ /dev/null @@ -1,180 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "net" - "os" - "path/filepath" - "testing" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetSocketDir(t *testing.T) { - tests := []struct { - name string - xdgRuntimeDir string - uid int - expectedSubstr string - }{ - { - name: "uses XDG_RUNTIME_DIR when set", - xdgRuntimeDir: "/run/user/1000", - expectedSubstr: "/run/user/1000", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.xdgRuntimeDir != "" { - t.Setenv("XDG_RUNTIME_DIR", tt.xdgRuntimeDir) - } - - result := getSocketDir() - assert.Contains(t, result, tt.expectedSubstr) - }) - } -} - -func TestGetSocketPath(t *testing.T) { - path := GetSocketPath() - assert.Contains(t, path, "danklinux-") - assert.Contains(t, path, ".sock") - assert.Contains(t, path, fmt.Sprintf("%d", os.Getpid())) -} - -func TestGetCapabilities(t *testing.T) { - originalNetworkManager := networkManager - defer func() { networkManager = originalNetworkManager }() - - t.Run("capabilities without network manager", func(t *testing.T) { - networkManager = nil - caps := getCapabilities() - assert.Contains(t, caps.Capabilities, "plugins") - assert.NotContains(t, caps.Capabilities, "network") - }) - - t.Run("capabilities with network manager", func(t *testing.T) { - networkManager = &network.Manager{} - caps := getCapabilities() - assert.Contains(t, caps.Capabilities, "plugins") - assert.Contains(t, caps.Capabilities, "network") - }) -} - -type mockConn struct { - net.Conn - written []byte -} - -func (m *mockConn) Write(b []byte) (n int, err error) { - m.written = append(m.written, b...) - return len(b), nil -} - -func (m *mockConn) Close() error { - return nil -} - -func TestRespondError(t *testing.T) { - conn := &mockConn{} - models.RespondError(conn, 123, "test error") - - var resp models.Response[any] - err := json.Unmarshal(conn.written, &resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Equal(t, "test error", resp.Error) - assert.Nil(t, resp.Result) -} - -func TestRespond(t *testing.T) { - conn := &mockConn{} - result := map[string]string{"foo": "bar"} - models.Respond(conn, 123, result) - - var resp models.Response[map[string]string] - err := json.Unmarshal(conn.written, &resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.Equal(t, "bar", (*resp.Result)["foo"]) -} - -func TestRequest_JSON(t *testing.T) { - jsonStr := `{"id":123,"method":"test.method","params":{"key":"value"}}` - var req models.Request - err := json.Unmarshal([]byte(jsonStr), &req) - require.NoError(t, err) - - assert.Equal(t, 123, req.ID) - assert.Equal(t, "test.method", req.Method) - assert.Equal(t, "value", req.Params["key"]) -} - -func TestResponse_JSON(t *testing.T) { - t.Run("success response", func(t *testing.T) { - result := "success" - resp := models.Response[string]{ - ID: 123, - Result: &result, - } - - data, err := json.Marshal(resp) - require.NoError(t, err) - - var decoded models.Response[string] - err = json.Unmarshal(data, &decoded) - require.NoError(t, err) - - assert.Equal(t, 123, decoded.ID) - assert.Equal(t, "success", *decoded.Result) - assert.Empty(t, decoded.Error) - }) - - t.Run("error response", func(t *testing.T) { - resp := models.Response[any]{ - ID: 123, - Error: "test error", - } - - data, err := json.Marshal(resp) - require.NoError(t, err) - - var decoded models.Response[any] - err = json.Unmarshal(data, &decoded) - require.NoError(t, err) - - assert.Equal(t, 123, decoded.ID) - assert.Equal(t, "test error", decoded.Error) - assert.Nil(t, decoded.Result) - }) -} - -func TestCleanupStaleSockets(t *testing.T) { - tempDir := t.TempDir() - t.Setenv("XDG_RUNTIME_DIR", tempDir) - - staleSocket := filepath.Join(tempDir, "danklinux-4194305.sock") - err := os.WriteFile(staleSocket, []byte{}, 0o600) - require.NoError(t, err) - - activeSocket := filepath.Join(tempDir, fmt.Sprintf("danklinux-%d.sock", os.Getpid())) - err = os.WriteFile(activeSocket, []byte{}, 0o600) - require.NoError(t, err) - - cleanupStaleSockets() - - _, err = os.Stat(staleSocket) - assert.True(t, os.IsNotExist(err)) - - _, err = os.Stat(activeSocket) - assert.NoError(t, err) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/handlers.go deleted file mode 100644 index 6891875..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/handlers.go +++ /dev/null @@ -1,154 +0,0 @@ -package thememode - -import ( - "encoding/json" - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - if manager == nil { - models.RespondError(conn, req.ID, "theme mode manager not initialized") - return - } - - switch req.Method { - case "theme.auto.getState": - handleGetState(conn, req, manager) - case "theme.auto.setEnabled": - handleSetEnabled(conn, req, manager) - case "theme.auto.setMode": - handleSetMode(conn, req, manager) - case "theme.auto.setSchedule": - handleSetSchedule(conn, req, manager) - case "theme.auto.setLocation": - handleSetLocation(conn, req, manager) - case "theme.auto.setUseIPLocation": - handleSetUseIPLocation(conn, req, manager) - case "theme.auto.trigger": - handleTrigger(conn, req, manager) - case "theme.auto.subscribe": - handleSubscribe(conn, req, manager) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleGetState(conn net.Conn, req models.Request, manager *Manager) { - models.Respond(conn, req.ID, manager.GetState()) -} - -func handleSetEnabled(conn net.Conn, req models.Request, manager *Manager) { - enabled, err := params.Bool(req.Params, "enabled") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - manager.SetEnabled(enabled) - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto enabled set"}) -} - -func handleSetMode(conn net.Conn, req models.Request, manager *Manager) { - mode, err := params.String(req.Params, "mode") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if mode != "time" && mode != "location" { - models.RespondError(conn, req.ID, "invalid mode") - return - } - - manager.SetMode(mode) - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto mode set"}) -} - -func handleSetSchedule(conn net.Conn, req models.Request, manager *Manager) { - startHour, err := params.Int(req.Params, "startHour") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - startMinute, err := params.Int(req.Params, "startMinute") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - endHour, err := params.Int(req.Params, "endHour") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - endMinute, err := params.Int(req.Params, "endMinute") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.ValidateSchedule(startHour, startMinute, endHour, endMinute); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - manager.SetSchedule(startHour, startMinute, endHour, endMinute) - models.Respond(conn, req.ID, manager.GetState()) -} - -func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) { - lat, err := params.Float(req.Params, "latitude") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - lon, err := params.Float(req.Params, "longitude") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - manager.SetLocation(lat, lon) - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto location set"}) -} - -func handleSetUseIPLocation(conn net.Conn, req models.Request, manager *Manager) { - use, err := params.Bool(req.Params, "use") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - manager.SetUseIPLocation(use) - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto IP location set"}) -} - -func handleTrigger(conn net.Conn, req models.Request, manager *Manager) { - manager.TriggerUpdate() - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto update triggered"}) -} - -func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) { - clientID := fmt.Sprintf("client-%p", conn) - stateChan := manager.Subscribe(clientID) - defer manager.Unsubscribe(clientID) - - initialState := manager.GetState() - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - ID: req.ID, - Result: &initialState, - }); err != nil { - return - } - - for state := range stateChan { - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - Result: &state, - }); err != nil { - return - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/manager.go deleted file mode 100644 index f7644ad..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/manager.go +++ /dev/null @@ -1,462 +0,0 @@ -package thememode - -import ( - "errors" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" -) - -const ( - defaultStartHour = 18 - defaultStartMinute = 0 - defaultEndHour = 6 - defaultEndMinute = 0 - defaultElevationTwilight = -6.0 - defaultElevationDaylight = 3.0 -) - -type Manager struct { - config Config - configMutex sync.RWMutex - - state *State - stateMutex sync.RWMutex - - subscribers syncmap.Map[string, chan State] - - locationMutex sync.RWMutex - cachedIPLat *float64 - cachedIPLon *float64 - - geoClient geolocation.Client - - stopChan chan struct{} - updateTrigger chan struct{} - wg sync.WaitGroup -} - -func NewManager() *Manager { - m := &Manager{ - config: Config{ - Enabled: false, - Mode: "time", - StartHour: defaultStartHour, - StartMinute: defaultStartMinute, - EndHour: defaultEndHour, - EndMinute: defaultEndMinute, - ElevationTwilight: defaultElevationTwilight, - ElevationDaylight: defaultElevationDaylight, - }, - stopChan: make(chan struct{}), - updateTrigger: make(chan struct{}, 1), - } - - m.updateState(time.Now()) - - m.wg.Add(1) - go m.schedulerLoop() - - return m -} - -func (m *Manager) GetState() State { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - if m.state == nil { - return State{Config: m.getConfig()} - } - stateCopy := *m.state - return stateCopy -} - -func (m *Manager) Subscribe(id string) chan State { - ch := make(chan State, 64) - m.subscribers.Store(id, ch) - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) SetEnabled(enabled bool) { - m.configMutex.Lock() - if m.config.Enabled == enabled { - m.configMutex.Unlock() - return - } - m.config.Enabled = enabled - m.configMutex.Unlock() - m.TriggerUpdate() -} - -func (m *Manager) SetMode(mode string) { - m.configMutex.Lock() - if m.config.Mode == mode { - m.configMutex.Unlock() - return - } - m.config.Mode = mode - m.configMutex.Unlock() - m.TriggerUpdate() -} - -func (m *Manager) SetSchedule(startHour, startMinute, endHour, endMinute int) { - m.configMutex.Lock() - changed := m.config.StartHour != startHour || - m.config.StartMinute != startMinute || - m.config.EndHour != endHour || - m.config.EndMinute != endMinute - if !changed { - m.configMutex.Unlock() - return - } - m.config.StartHour = startHour - m.config.StartMinute = startMinute - m.config.EndHour = endHour - m.config.EndMinute = endMinute - m.configMutex.Unlock() - m.TriggerUpdate() -} - -func (m *Manager) SetLocation(lat, lon float64) { - m.configMutex.Lock() - if m.config.Latitude != nil && m.config.Longitude != nil && - *m.config.Latitude == lat && *m.config.Longitude == lon && !m.config.UseIPLocation { - m.configMutex.Unlock() - return - } - m.config.Latitude = &lat - m.config.Longitude = &lon - m.config.UseIPLocation = false - m.configMutex.Unlock() - - m.locationMutex.Lock() - m.cachedIPLat = nil - m.cachedIPLon = nil - m.locationMutex.Unlock() - - m.TriggerUpdate() -} - -func (m *Manager) SetUseIPLocation(use bool) { - m.configMutex.Lock() - if m.config.UseIPLocation == use { - m.configMutex.Unlock() - return - } - m.config.UseIPLocation = use - if use { - m.config.Latitude = nil - m.config.Longitude = nil - } - m.configMutex.Unlock() - - if use { - m.locationMutex.Lock() - m.cachedIPLat = nil - m.cachedIPLon = nil - m.locationMutex.Unlock() - } - - m.TriggerUpdate() -} - -func (m *Manager) TriggerUpdate() { - select { - case m.updateTrigger <- struct{}{}: - default: - } -} - -func (m *Manager) Close() { - select { - case <-m.stopChan: - return - default: - close(m.stopChan) - } - m.wg.Wait() - m.subscribers.Range(func(key string, ch chan State) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) -} - -func (m *Manager) WatchLoginctl(lm *loginctl.Manager) { - ch := lm.Subscribe("thememode") - m.wg.Add(1) - go func() { - defer m.wg.Done() - defer lm.Unsubscribe("thememode") - for { - select { - case <-m.stopChan: - return - case state, ok := <-ch: - if !ok { - return - } - if state.PreparingForSleep { - continue - } - m.TriggerUpdate() - } - } - }() -} - -func (m *Manager) schedulerLoop() { - defer m.wg.Done() - - var timer *time.Timer - for { - config := m.getConfig() - now := time.Now() - var isLight bool - var next time.Time - if config.Enabled { - isLight, next = m.computeSchedule(now, config) - } else { - m.stateMutex.RLock() - if m.state != nil { - isLight = m.state.IsLight - } - m.stateMutex.RUnlock() - next = now.Add(24 * time.Hour) - } - - m.updateStateWithValues(config, isLight, next) - - waitDur := time.Until(next) - if !config.Enabled { - waitDur = 24 * time.Hour - } - if waitDur < time.Second { - waitDur = time.Second - } - - if timer != nil { - timer.Stop() - } - timer = time.NewTimer(waitDur) - - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.updateTrigger: - timer.Stop() - continue - case <-timer.C: - continue - } - } -} - -func (m *Manager) updateState(now time.Time) { - config := m.getConfig() - var isLight bool - var next time.Time - if config.Enabled { - isLight, next = m.computeSchedule(now, config) - } else { - m.stateMutex.RLock() - if m.state != nil { - isLight = m.state.IsLight - } - m.stateMutex.RUnlock() - next = now.Add(24 * time.Hour) - } - m.updateStateWithValues(config, isLight, next) -} - -func (m *Manager) updateStateWithValues(config Config, isLight bool, next time.Time) { - newState := State{ - Config: config, - IsLight: isLight, - NextTransition: next, - } - - m.stateMutex.Lock() - if m.state != nil && statesEqual(m.state, &newState) { - m.stateMutex.Unlock() - return - } - m.state = &newState - m.stateMutex.Unlock() - - m.notifySubscribers() -} - -func (m *Manager) notifySubscribers() { - state := m.GetState() - m.subscribers.Range(func(key string, ch chan State) bool { - select { - case ch <- state: - default: - } - return true - }) -} - -func (m *Manager) getConfig() Config { - m.configMutex.RLock() - defer m.configMutex.RUnlock() - return m.config -} - -func (m *Manager) SetGeoClient(client geolocation.Client) { - m.geoClient = client -} - -func (m *Manager) getLocation(config Config) (*float64, *float64) { - if config.Latitude != nil && config.Longitude != nil { - return config.Latitude, config.Longitude - } - if !config.UseIPLocation { - return nil, nil - } - if m.geoClient == nil { - return nil, nil - } - - m.locationMutex.RLock() - if m.cachedIPLat != nil && m.cachedIPLon != nil { - lat, lon := m.cachedIPLat, m.cachedIPLon - m.locationMutex.RUnlock() - return lat, lon - } - m.locationMutex.RUnlock() - - location, err := m.geoClient.GetLocation() - if err != nil { - return nil, nil - } - - m.locationMutex.Lock() - m.cachedIPLat = &location.Latitude - m.cachedIPLon = &location.Longitude - m.locationMutex.Unlock() - - return m.cachedIPLat, m.cachedIPLon -} - -func statesEqual(a, b *State) bool { - if a == nil || b == nil { - return a == b - } - if a.IsLight != b.IsLight || !a.NextTransition.Equal(b.NextTransition) { - return false - } - return a.Config == b.Config -} - -func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) { - switch config.Mode { - case "location": - return m.computeLocationSchedule(now, config) - default: - return computeTimeSchedule(now, config) - } -} - -func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) { - startMinutes := config.StartHour*60 + config.StartMinute - endMinutes := config.EndHour*60 + config.EndMinute - currentMinutes := now.Hour()*60 + now.Minute() - - startTime := time.Date(now.Year(), now.Month(), now.Day(), config.StartHour, config.StartMinute, 0, 0, now.Location()) - endTime := time.Date(now.Year(), now.Month(), now.Day(), config.EndHour, config.EndMinute, 0, 0, now.Location()) - - if startMinutes == endMinutes { - next := startTime - if !next.After(now) { - next = next.Add(24 * time.Hour) - } - return true, next - } - - if startMinutes < endMinutes { - if currentMinutes < startMinutes { - return true, startTime - } - if currentMinutes >= endMinutes { - return true, startTime.Add(24 * time.Hour) - } - return false, endTime - } - - if currentMinutes >= startMinutes { - return false, endTime.Add(24 * time.Hour) - } - if currentMinutes < endMinutes { - return false, endTime - } - return true, startTime -} - -func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, time.Time) { - lat, lon := m.getLocation(config) - if lat == nil || lon == nil { - currentIsLight := false - m.stateMutex.RLock() - if m.state != nil { - currentIsLight = m.state.IsLight - } - m.stateMutex.RUnlock() - return currentIsLight, now.Add(10 * time.Minute) - } - - times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight) - switch cond { - case wayland.SunMidnightSun: - return true, startOfNextDay(now) - case wayland.SunPolarNight: - return false, startOfNextDay(now) - } - - if now.Before(times.Sunrise) { - return false, times.Sunrise - } - if now.Before(times.Sunset) { - return true, times.Sunset - } - - nextDay := startOfNextDay(now) - nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight) - switch nextCond { - case wayland.SunMidnightSun: - return true, startOfNextDay(nextDay) - case wayland.SunPolarNight: - return false, startOfNextDay(nextDay) - } - - return false, nextTimes.Sunrise -} - -func startOfNextDay(t time.Time) time.Time { - next := t.Add(24 * time.Hour) - return time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, next.Location()) -} - -func validateHourMinute(hour, minute int) bool { - return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59 -} - -func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error { - if !validateHourMinute(startHour, startMinute) || !validateHourMinute(endHour, endMinute) { - return errInvalidTime - } - return nil -} - -var errInvalidTime = errors.New("invalid schedule time") diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/types.go deleted file mode 100644 index 72e6468..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/types.go +++ /dev/null @@ -1,23 +0,0 @@ -package thememode - -import "time" - -type Config struct { - Enabled bool `json:"enabled"` - Mode string `json:"mode"` - StartHour int `json:"startHour"` - StartMinute int `json:"startMinute"` - EndHour int `json:"endHour"` - EndMinute int `json:"endMinute"` - Latitude *float64 `json:"latitude,omitempty"` - Longitude *float64 `json:"longitude,omitempty"` - UseIPLocation bool `json:"useIPLocation"` - ElevationTwilight float64 `json:"elevationTwilight"` - ElevationDaylight float64 `json:"elevationDaylight"` -} - -type State struct { - Config Config `json:"config"` - IsLight bool `json:"isLight"` - NextTransition time.Time `json:"nextTransition"` -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/handlers.go deleted file mode 100644 index b3b308a..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/handlers.go +++ /dev/null @@ -1,27 +0,0 @@ -package themes - -import ( - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -func HandleRequest(conn net.Conn, req models.Request) { - switch req.Method { - case "themes.list": - HandleList(conn, req) - case "themes.listInstalled": - HandleListInstalled(conn, req) - case "themes.install": - HandleInstall(conn, req) - case "themes.uninstall": - HandleUninstall(conn, req) - case "themes.update": - HandleUpdate(conn, req) - case "themes.search": - HandleSearch(conn, req) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/install.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/install.go deleted file mode 100644 index 26d22ba..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/install.go +++ /dev/null @@ -1,52 +0,0 @@ -package themes - -import ( - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/themes" -) - -func HandleInstall(conn net.Conn, req models.Request) { - idOrName, ok := models.Get[string](req, "name") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'name' parameter") - return - } - - registry, err := themes.NewRegistry() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err)) - return - } - - themeList, err := registry.List() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err)) - return - } - - theme := themes.FindByIDOrName(idOrName, themeList) - if theme == nil { - models.RespondError(conn, req.ID, fmt.Sprintf("theme not found: %s", idOrName)) - return - } - - manager, err := themes.NewManager() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) - return - } - - registryThemeDir := registry.GetThemeDir(theme.SourceDir) - if err := manager.Install(*theme, registryThemeDir); err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to install theme: %v", err)) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{ - Success: true, - Message: fmt.Sprintf("theme installed: %s", theme.Name), - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/list.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/list.go deleted file mode 100644 index 9564be1..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/list.go +++ /dev/null @@ -1,54 +0,0 @@ -package themes - -import ( - "fmt" - "net" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/themes" -) - -func HandleList(conn net.Conn, req models.Request) { - registry, err := themes.NewRegistry() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err)) - return - } - - themeList, err := registry.List() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err)) - return - } - - manager, err := themes.NewManager() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) - return - } - - result := make([]ThemeInfo, len(themeList)) - for i, t := range themeList { - installed, _ := manager.IsInstalled(t) - info := ThemeInfo{ - ID: t.ID, - Name: t.Name, - Version: t.Version, - Author: t.Author, - Description: t.Description, - PreviewPath: t.PreviewPath, - SourceDir: t.SourceDir, - Installed: installed, - FirstParty: isFirstParty(t.Author), - } - addVariantsInfo(&info, t.Variants) - result[i] = info - } - - models.Respond(conn, req.ID, result) -} - -func isFirstParty(author string) bool { - return strings.EqualFold(author, "Avenge Media") || strings.EqualFold(author, "AvengeMedia") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/list_installed.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/list_installed.go deleted file mode 100644 index 49be81b..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/list_installed.go +++ /dev/null @@ -1,149 +0,0 @@ -package themes - -import ( - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/themes" -) - -func addVariantsInfo(info *ThemeInfo, variants *themes.ThemeVariants) { - if variants == nil { - return - } - - if variants.Type == "multi" { - if len(variants.Flavors) == 0 && len(variants.Accents) == 0 { - return - } - info.HasVariants = true - info.Variants = &VariantsInfo{ - Type: "multi", - Flavors: make([]FlavorInfo, len(variants.Flavors)), - Accents: make([]AccentInfo, len(variants.Accents)), - } - if variants.Defaults != nil { - info.Variants.Defaults = &MultiDefaults{ - Dark: variants.Defaults.Dark, - Light: variants.Defaults.Light, - } - } - for i, f := range variants.Flavors { - mode := "" - switch { - case f.Dark.Primary != "" && f.Light.Primary != "": - mode = "both" - case f.Dark.Primary != "": - mode = "dark" - case f.Light.Primary != "": - mode = "light" - default: - if f.Dark.Surface != "" { - mode = "dark" - } else if f.Light.Surface != "" { - mode = "light" - } - } - info.Variants.Flavors[i] = FlavorInfo{ID: f.ID, Name: f.Name, Mode: mode} - } - for i, a := range variants.Accents { - color := "" - if colors, ok := a.FlavorColors["mocha"]; ok && colors.Primary != "" { - color = colors.Primary - } else if colors, ok := a.FlavorColors["latte"]; ok && colors.Primary != "" { - color = colors.Primary - } else { - for _, c := range a.FlavorColors { - if c.Primary != "" { - color = c.Primary - break - } - } - } - info.Variants.Accents[i] = AccentInfo{ID: a.ID, Name: a.Name, Color: color} - } - return - } - - if len(variants.Options) == 0 { - return - } - info.HasVariants = true - info.Variants = &VariantsInfo{ - Default: variants.Default, - Options: make([]VariantInfo, len(variants.Options)), - } - for i, v := range variants.Options { - info.Variants.Options[i] = VariantInfo{ID: v.ID, Name: v.Name} - } -} - -func HandleListInstalled(conn net.Conn, req models.Request) { - manager, err := themes.NewManager() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) - return - } - - installedIDs, err := manager.ListInstalled() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to list installed themes: %v", err)) - return - } - - themeMap := make(map[string]themes.Theme) - if registry, err := themes.NewRegistry(); err == nil { - if allThemes, err := registry.List(); err == nil { - for _, t := range allThemes { - themeMap[t.ID] = t - } - } - } - - result := make([]ThemeInfo, 0, len(installedIDs)) - for _, id := range installedIDs { - if theme, ok := themeMap[id]; ok { - hasUpdate := false - if hasUpdates, err := manager.HasUpdates(id, theme); err == nil { - hasUpdate = hasUpdates - } - - info := ThemeInfo{ - ID: theme.ID, - Name: theme.Name, - Version: theme.Version, - Author: theme.Author, - Description: theme.Description, - SourceDir: id, - FirstParty: isFirstParty(theme.Author), - HasUpdate: hasUpdate, - } - addVariantsInfo(&info, theme.Variants) - result = append(result, info) - } else { - installed, err := manager.GetInstalledTheme(id) - if err != nil { - result = append(result, ThemeInfo{ - ID: id, - Name: id, - SourceDir: id, - }) - continue - } - info := ThemeInfo{ - ID: installed.ID, - Name: installed.Name, - Version: installed.Version, - Author: installed.Author, - Description: installed.Description, - SourceDir: id, - FirstParty: isFirstParty(installed.Author), - } - addVariantsInfo(&info, installed.Variants) - result = append(result, info) - } - } - - models.Respond(conn, req.ID, result) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/search.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/search.go deleted file mode 100644 index 2823fe0..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/search.go +++ /dev/null @@ -1,53 +0,0 @@ -package themes - -import ( - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/themes" -) - -func HandleSearch(conn net.Conn, req models.Request) { - query, ok := models.Get[string](req, "query") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'query' parameter") - return - } - - registry, err := themes.NewRegistry() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err)) - return - } - - themeList, err := registry.List() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err)) - return - } - - searchResults := themes.FuzzySearch(query, themeList) - - manager, err := themes.NewManager() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) - return - } - - result := make([]ThemeInfo, len(searchResults)) - for i, t := range searchResults { - installed, _ := manager.IsInstalled(t) - result[i] = ThemeInfo{ - ID: t.ID, - Name: t.Name, - Version: t.Version, - Author: t.Author, - Description: t.Description, - Installed: installed, - FirstParty: isFirstParty(t.Author), - } - } - - models.Respond(conn, req.ID, result) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/types.go deleted file mode 100644 index 0ad19b4..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/types.go +++ /dev/null @@ -1,47 +0,0 @@ -package themes - -type VariantInfo struct { - ID string `json:"id"` - Name string `json:"name"` -} - -type FlavorInfo struct { - ID string `json:"id"` - Name string `json:"name"` - Mode string `json:"mode,omitempty"` -} - -type AccentInfo struct { - ID string `json:"id"` - Name string `json:"name"` - Color string `json:"color,omitempty"` -} - -type MultiDefaults struct { - Dark map[string]string `json:"dark,omitempty"` - Light map[string]string `json:"light,omitempty"` -} - -type VariantsInfo struct { - Type string `json:"type,omitempty"` - Default string `json:"default,omitempty"` - Defaults *MultiDefaults `json:"defaults,omitempty"` - Options []VariantInfo `json:"options,omitempty"` - Flavors []FlavorInfo `json:"flavors,omitempty"` - Accents []AccentInfo `json:"accents,omitempty"` -} - -type ThemeInfo struct { - ID string `json:"id"` - Name string `json:"name"` - Version string `json:"version"` - Author string `json:"author,omitempty"` - Description string `json:"description,omitempty"` - PreviewPath string `json:"previewPath,omitempty"` - SourceDir string `json:"sourceDir,omitempty"` - Installed bool `json:"installed,omitempty"` - FirstParty bool `json:"firstParty,omitempty"` - HasUpdate bool `json:"hasUpdate,omitempty"` - HasVariants bool `json:"hasVariants,omitempty"` - Variants *VariantsInfo `json:"variants,omitempty"` -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/uninstall.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/uninstall.go deleted file mode 100644 index 9b453e4..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/uninstall.go +++ /dev/null @@ -1,63 +0,0 @@ -package themes - -import ( - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/themes" -) - -func HandleUninstall(conn net.Conn, req models.Request) { - idOrName, ok := models.Get[string](req, "name") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'name' parameter") - return - } - - manager, err := themes.NewManager() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) - return - } - - registry, err := themes.NewRegistry() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err)) - return - } - - themeList, _ := registry.List() - theme := themes.FindByIDOrName(idOrName, themeList) - - if theme != nil { - installed, err := manager.IsInstalled(*theme) - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if theme is installed: %v", err)) - return - } - if !installed { - models.RespondError(conn, req.ID, fmt.Sprintf("theme not installed: %s", idOrName)) - return - } - if err := manager.Uninstall(*theme); err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to uninstall theme: %v", err)) - return - } - models.Respond(conn, req.ID, models.SuccessResult{ - Success: true, - Message: fmt.Sprintf("theme uninstalled: %s", theme.Name), - }) - return - } - - if err := manager.UninstallByID(idOrName); err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("theme not found: %s", idOrName)) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{ - Success: true, - Message: fmt.Sprintf("theme uninstalled: %s", idOrName), - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/update.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/update.go deleted file mode 100644 index fb9f81f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/themes/update.go +++ /dev/null @@ -1,57 +0,0 @@ -package themes - -import ( - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/themes" -) - -func HandleUpdate(conn net.Conn, req models.Request) { - idOrName, ok := models.Get[string](req, "name") - if !ok { - models.RespondError(conn, req.ID, "missing or invalid 'name' parameter") - return - } - - manager, err := themes.NewManager() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) - return - } - - registry, err := themes.NewRegistry() - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err)) - return - } - - themeList, _ := registry.List() - theme := themes.FindByIDOrName(idOrName, themeList) - - if theme == nil { - models.RespondError(conn, req.ID, fmt.Sprintf("theme not found in registry: %s", idOrName)) - return - } - - installed, err := manager.IsInstalled(*theme) - if err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if theme is installed: %v", err)) - return - } - if !installed { - models.RespondError(conn, req.ID, fmt.Sprintf("theme not installed: %s", idOrName)) - return - } - - if err := manager.Update(*theme); err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("failed to update theme: %v", err)) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{ - Success: true, - Message: fmt.Sprintf("theme updated: %s", theme.Name), - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/trayrecovery/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/trayrecovery/manager.go deleted file mode 100644 index 3610f1b..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/trayrecovery/manager.go +++ /dev/null @@ -1,93 +0,0 @@ -package trayrecovery - -import ( - "fmt" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl" - "github.com/godbus/dbus/v5" -) - -const resumeDelay = 3 * time.Second - -type Manager struct { - conn *dbus.Conn - stopChan chan struct{} - wg sync.WaitGroup -} - -func NewManager() (*Manager, error) { - conn, err := dbus.ConnectSessionBus() - if err != nil { - return nil, fmt.Errorf("failed to connect to session bus: %w", err) - } - - m := &Manager{ - conn: conn, - stopChan: make(chan struct{}), - } - - // Run a startup scan after a delay — covers the case where the process - // was killed during suspend and restarted by systemd (Type=dbus). - // The fresh process never sees the PrepareForSleep true→false transition, - // so the loginctl watcher alone is not enough. - go m.scheduleRecovery() - - return m, nil -} - -// WatchLoginctl subscribes to loginctl session state changes and triggers -// tray recovery after resume from suspend (PrepareForSleep false transition). -// This handles the case where the process survives suspend. -func (m *Manager) WatchLoginctl(lm *loginctl.Manager) { - ch := lm.Subscribe("tray-recovery") - m.wg.Add(1) - go func() { - defer m.wg.Done() - defer lm.Unsubscribe("tray-recovery") - - wasSleeping := false - for { - select { - case <-m.stopChan: - return - case state, ok := <-ch: - if !ok { - return - } - if state.PreparingForSleep { - wasSleeping = true - continue - } - if wasSleeping { - wasSleeping = false - go m.scheduleRecovery() - } - } - } - }() -} - -func (m *Manager) scheduleRecovery() { - select { - case <-time.After(resumeDelay): - m.recoverTrayItems() - case <-m.stopChan: - } -} - -func (m *Manager) Close() { - select { - case <-m.stopChan: - return - default: - close(m.stopChan) - } - m.wg.Wait() - if m.conn != nil { - m.conn.Close() - } - log.Info("TrayRecovery manager closed") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/trayrecovery/recovery.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/trayrecovery/recovery.go deleted file mode 100644 index c7b98fb..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/trayrecovery/recovery.go +++ /dev/null @@ -1,262 +0,0 @@ -package trayrecovery - -import ( - "context" - "strings" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/godbus/dbus/v5" -) - -const ( - sniWatcherDest = "org.kde.StatusNotifierWatcher" - sniWatcherPath = "/StatusNotifierWatcher" - sniWatcherIface = "org.kde.StatusNotifierWatcher" - sniItemIface = "org.kde.StatusNotifierItem" - dbusIface = "org.freedesktop.DBus" - propsIface = "org.freedesktop.DBus.Properties" - probeTimeout = 300 * time.Millisecond - connProbeTimeout = 150 * time.Millisecond - batchSize = 30 -) - -var excludedPrefixes = []string{ - "org.freedesktop.", - "org.gnome.", - "org.kde.StatusNotifier", - "com.canonical.AppMenu", - "org.mpris.", - "org.pipewire.", - "org.pulseaudio", - "fi.epitaph", - "quickshell", - "org.kde.quickshell", -} - -func (m *Manager) recoverTrayItems() { - registeredItems := m.getRegisteredItems() - allNames := m.getDBusNames() - if allNames == nil { - return - } - - registeredConnIDs := m.buildRegisteredConnIDs(registeredItems) - - count := len(registeredItems) - log.Infof("TrayRecoveryService: scanning DBus for unregistered SNI items (%d already registered)...", count) - - m.scanWellKnownNames(allNames, registeredItems, registeredConnIDs) - m.scanConnectionIDs(allNames, registeredItems, registeredConnIDs) -} - -func (m *Manager) getRegisteredItems() []string { - obj := m.conn.Object(sniWatcherDest, sniWatcherPath) - variant, err := obj.GetProperty(sniWatcherIface + ".RegisteredStatusNotifierItems") - if err != nil { - log.Warnf("TrayRecoveryService: failed to get registered items: %v", err) - return nil - } - - switch v := variant.Value().(type) { - case []string: - return v - case []any: - items := make([]string, 0, len(v)) - for _, elem := range v { - if s, ok := elem.(string); ok { - items = append(items, s) - } - } - return items - } - return nil -} - -func (m *Manager) getDBusNames() []string { - var names []string - err := m.conn.BusObject().Call(dbusIface+".ListNames", 0).Store(&names) - if err != nil { - log.Warnf("TrayRecoveryService: failed to list bus names: %v", err) - return nil - } - return names -} - -func (m *Manager) getNameOwner(name string) string { - var owner string - err := m.conn.BusObject().Call(dbusIface+".GetNameOwner", 0, name).Store(&owner) - if err != nil { - return "" - } - return owner -} - -// buildRegisteredConnIDs resolves every registered SNI item (well-known name -// or :1.xxx connection ID) to a canonical connection ID. This prevents -// duplicates in both directions. -func (m *Manager) buildRegisteredConnIDs(registeredItems []string) map[string]bool { - connIDs := make(map[string]bool, len(registeredItems)) - for _, item := range registeredItems { - name := extractName(item) - if strings.HasPrefix(name, ":1.") { - connIDs[name] = true - } else { - owner := m.getNameOwner(name) - if owner != "" { - connIDs[owner] = true - } - } - } - return connIDs -} - -// scanWellKnownNames probes well-known names (e.g. DinoX, nm-applet) for -// unregistered SNI items and re-registers them. -func (m *Manager) scanWellKnownNames(allNames []string, registeredItems []string, registeredConnIDs map[string]bool) { - registeredRaw := strings.Join(registeredItems, "\n") - - for _, name := range allNames { - if strings.HasPrefix(name, ":") { - continue - } - - if strings.Contains(registeredRaw, name) { - continue - } - - // Skip if this name's connection ID is already in the registered set - // (handles the case where the app registered via connection ID instead) - connForName := m.getNameOwner(name) - if connForName != "" && registeredConnIDs[connForName] { - continue - } - - if isExcludedName(name) { - continue - } - - short := shortName(name) - objectPaths := []string{ - "/StatusNotifierItem", - "/org/ayatana/NotificationItem/" + short, - } - - for _, objPath := range objectPaths { - if m.probeSNI(name, objPath, probeTimeout) { - m.registerSNI(name) - // Update set so the connection-ID section won't double-register this app - if connForName != "" { - registeredConnIDs[connForName] = true - } - break - } - } - } -} - -// scanConnectionIDs probes all :1.xxx connections in parallel for unregistered -// SNI items (e.g. Vesktop, Electron apps). Most non-SNI connections return an -// error instantly, so this is fast. -func (m *Manager) scanConnectionIDs(allNames []string, registeredItems []string, registeredConnIDs map[string]bool) { - registeredRaw := strings.Join(registeredItems, "\n") - registeredLower := strings.ToLower(registeredRaw) - - var wg sync.WaitGroup - sem := make(chan struct{}, batchSize) - - for _, name := range allNames { - if !strings.HasPrefix(name, ":1.") { - continue - } - if registeredConnIDs[name] { - continue - } - - sem <- struct{}{} - wg.Add(1) - go func(conn string) { - defer wg.Done() - defer func() { <-sem }() - - sniID := m.getSNIId(conn, connProbeTimeout) - if sniID == "" { - return - } - - // Skip if an item with the same Id is already registered (case-insensitive) - if strings.Contains(registeredLower, strings.ToLower(sniID)) { - return - } - - m.registerSNI(conn) - log.Infof("TrayRecovery: re-registered %s (Id: %s)", conn, sniID) - }(name) - } - wg.Wait() -} - -func (m *Manager) probeSNI(dest, path string, timeout time.Duration) bool { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - obj := m.conn.Object(dest, dbus.ObjectPath(path)) - var props map[string]dbus.Variant - err := obj.CallWithContext(ctx, propsIface+".GetAll", 0, sniItemIface).Store(&props) - if err != nil { - return false - } - - _, hasID := props["Id"] - return hasID -} - -func (m *Manager) getSNIId(dest string, timeout time.Duration) string { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - obj := m.conn.Object(dest, "/StatusNotifierItem") - var variant dbus.Variant - err := obj.CallWithContext(ctx, propsIface+".Get", 0, sniItemIface, "Id").Store(&variant) - if err != nil { - return "" - } - - id, _ := variant.Value().(string) - return id -} - -func (m *Manager) registerSNI(name string) { - obj := m.conn.Object(sniWatcherDest, sniWatcherPath) - call := obj.Call(sniWatcherIface+".RegisterStatusNotifierItem", 0, name) - if call.Err != nil { - log.Warnf("TrayRecovery: failed to register %s: %v", name, call.Err) - return - } - log.Infof("TrayRecovery: re-registered %s", name) -} - -func extractName(item string) string { - if idx := strings.IndexByte(item, '/'); idx != -1 { - return item[:idx] - } - return item -} - -func shortName(name string) string { - parts := strings.Split(name, ".") - if len(parts) > 0 { - return parts[len(parts)-1] - } - return name -} - -func isExcludedName(name string) bool { - for _, prefix := range excludedPrefixes { - if strings.HasPrefix(name, prefix) { - return true - } - } - return false -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma.go deleted file mode 100644 index 1fff03f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma.go +++ /dev/null @@ -1,167 +0,0 @@ -package wayland - -import ( - "math" -) - -type GammaRamp struct { - Red []uint16 - Green []uint16 - Blue []uint16 -} - -type rgb struct { - r, g, b float64 -} - -type xyz struct { - x, y, z float64 -} - -func illuminantD(temp int) (float64, float64, bool) { - var x float64 - switch { - case temp >= 2500 && temp <= 7000: - t := float64(temp) - x = 0.244063 + 0.09911e3/t + 2.9678e6/(t*t) - 4.6070e9/(t*t*t) - case temp > 7000 && temp <= 25000: - t := float64(temp) - x = 0.237040 + 0.24748e3/t + 1.9018e6/(t*t) - 2.0064e9/(t*t*t) - default: - return 0, 0, false - } - y := -3*(x*x) + 2.870*x - 0.275 - return x, y, true -} - -func planckianLocus(temp int) (float64, float64, bool) { - var x, y float64 - switch { - case temp >= 1667 && temp <= 4000: - t := float64(temp) - x = -0.2661239e9/(t*t*t) - 0.2343589e6/(t*t) + 0.8776956e3/t + 0.179910 - if temp <= 2222 { - y = -1.1064814*(x*x*x) - 1.34811020*(x*x) + 2.18555832*x - 0.20219683 - } else { - y = -0.9549476*(x*x*x) - 1.37418593*(x*x) + 2.09137015*x - 0.16748867 - } - case temp > 4000 && temp < 25000: - t := float64(temp) - x = -3.0258469e9/(t*t*t) + 2.1070379e6/(t*t) + 0.2226347e3/t + 0.240390 - y = 3.0817580*(x*x*x) - 5.87338670*(x*x) + 3.75112997*x - 0.37001483 - default: - return 0, 0, false - } - return x, y, true -} - -func srgbGamma(value, gamma float64) float64 { - if value <= 0.0031308 { - return 12.92 * value - } - return math.Pow(1.055*value, 1.0/gamma) - 0.055 -} - -func clamp01(v float64) float64 { - switch { - case v > 1.0: - return 1.0 - case v < 0.0: - return 0.0 - default: - return v - } -} - -func xyzToSRGB(c xyz) rgb { - return rgb{ - r: srgbGamma(clamp01(3.2404542*c.x-1.5371385*c.y-0.4985314*c.z), 2.2), - g: srgbGamma(clamp01(-0.9692660*c.x+1.8760108*c.y+0.0415560*c.z), 2.2), - b: srgbGamma(clamp01(0.0556434*c.x-0.2040259*c.y+1.0572252*c.z), 2.2), - } -} - -func normalizeRGB(c *rgb) { - maxw := math.Max(c.r, math.Max(c.g, c.b)) - if maxw > 0 { - c.r /= maxw - c.g /= maxw - c.b /= maxw - } -} - -func calcWhitepoint(temp int) rgb { - if temp == 6500 { - return rgb{r: 1.0, g: 1.0, b: 1.0} - } - - var wp xyz - - switch { - case temp >= 25000: - x, y, _ := illuminantD(25000) - wp.x = x - wp.y = y - case temp >= 4000: - x, y, _ := illuminantD(temp) - wp.x = x - wp.y = y - case temp >= 2500: - x1, y1, _ := illuminantD(temp) - x2, y2, _ := planckianLocus(temp) - factor := float64(4000-temp) / 1500.0 - sineFactor := (math.Cos(math.Pi*factor) + 1.0) / 2.0 - wp.x = x1*sineFactor + x2*(1.0-sineFactor) - wp.y = y1*sineFactor + y2*(1.0-sineFactor) - default: - t := temp - if t < 1667 { - t = 1667 - } - x, y, _ := planckianLocus(t) - wp.x = x - wp.y = y - } - - wp.z = 1.0 - wp.x - wp.y - - wpRGB := xyzToSRGB(wp) - normalizeRGB(&wpRGB) - return wpRGB -} - -func GenerateGammaRamp(size uint32, temp int, gamma float64) GammaRamp { - ramp := GammaRamp{ - Red: make([]uint16, size), - Green: make([]uint16, size), - Blue: make([]uint16, size), - } - - wp := calcWhitepoint(temp) - - for i := uint32(0); i < size; i++ { - val := float64(i) / float64(size-1) - ramp.Red[i] = uint16(clamp01(math.Pow(val*wp.r, 1.0/gamma)) * 65535.0) - ramp.Green[i] = uint16(clamp01(math.Pow(val*wp.g, 1.0/gamma)) * 65535.0) - ramp.Blue[i] = uint16(clamp01(math.Pow(val*wp.b, 1.0/gamma)) * 65535.0) - } - - return ramp -} - -func GenerateIdentityRamp(size uint32) GammaRamp { - ramp := GammaRamp{ - Red: make([]uint16, size), - Green: make([]uint16, size), - Blue: make([]uint16, size), - } - - for i := uint32(0); i < size; i++ { - val := uint16((float64(i) / float64(size-1)) * 65535.0) - ramp.Red[i] = val - ramp.Green[i] = val - ramp.Blue[i] = val - } - - return ramp -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma_test.go deleted file mode 100644 index 0bf8e71..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package wayland - -import ( - "testing" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" -) - -func TestGenerateGammaRamp(t *testing.T) { - tests := []struct { - name string - size uint32 - temp int - gamma float64 - }{ - {"small_warm", 16, 6500, 1.0}, - {"small_cool", 16, 4000, 1.0}, - {"large_warm", 256, 6500, 1.0}, - {"large_cool", 256, 4000, 1.0}, - {"custom_gamma", 64, 5500, 1.2}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ramp := GenerateGammaRamp(tt.size, tt.temp, tt.gamma) - - if len(ramp.Red) != int(tt.size) { - t.Errorf("expected %d red values, got %d", tt.size, len(ramp.Red)) - } - if len(ramp.Green) != int(tt.size) { - t.Errorf("expected %d green values, got %d", tt.size, len(ramp.Green)) - } - if len(ramp.Blue) != int(tt.size) { - t.Errorf("expected %d blue values, got %d", tt.size, len(ramp.Blue)) - } - - if ramp.Red[0] != 0 || ramp.Green[0] != 0 || ramp.Blue[0] != 0 { - t.Errorf("first values should be 0, got R:%d G:%d B:%d", - ramp.Red[0], ramp.Green[0], ramp.Blue[0]) - } - - lastIdx := tt.size - 1 - if ramp.Red[lastIdx] == 0 || ramp.Green[lastIdx] == 0 || ramp.Blue[lastIdx] == 0 { - t.Errorf("last values should be non-zero, got R:%d G:%d B:%d", - ramp.Red[lastIdx], ramp.Green[lastIdx], ramp.Blue[lastIdx]) - } - - for i := uint32(1); i < tt.size; i++ { - if ramp.Red[i] < ramp.Red[i-1] { - t.Errorf("red ramp not monotonic at index %d", i) - } - } - }) - } -} - -func TestCalcWhitepoint(t *testing.T) { - tests := []struct { - name string - temp int - }{ - {"very_warm", 6500}, - {"neutral", 5500}, - {"cool", 4000}, - {"very_cool", 3000}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - wp := calcWhitepoint(tt.temp) - - if wp.r < 0 || wp.r > 1 { - t.Errorf("red out of range: %f", wp.r) - } - if wp.g < 0 || wp.g > 1 { - t.Errorf("green out of range: %f", wp.g) - } - if wp.b < 0 || wp.b > 1 { - t.Errorf("blue out of range: %f", wp.b) - } - }) - } -} - -func TestWhitepointProgression(t *testing.T) { - temps := []int{3000, 4000, 5000, 6000, 6500} - - var prevBlue float64 - for i, temp := range temps { - wp := calcWhitepoint(temp) - if i > 0 && wp.b < prevBlue { - t.Errorf("blue should increase with temperature, %d->%d: %f->%f", - temps[i-1], temp, prevBlue, wp.b) - } - prevBlue = wp.b - } -} - -func TestClamp(t *testing.T) { - tests := []struct { - val float64 - min float64 - max float64 - expected float64 - }{ - {5, 0, 10, 5}, - {-5, 0, 10, 0}, - {15, 0, 10, 10}, - {0, 0, 10, 0}, - {10, 0, 10, 10}, - } - - for _, tt := range tests { - result := utils.Clamp(tt.val, tt.min, tt.max) - if result != tt.expected { - t.Errorf("clamp(%f, %f, %f) = %f, want %f", - tt.val, tt.min, tt.max, result, tt.expected) - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/geolocation.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/geolocation.go deleted file mode 100644 index ba99b5f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/geolocation.go +++ /dev/null @@ -1,50 +0,0 @@ -package wayland - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" -) - -type ipAPIResponse struct { - Lat float64 `json:"lat"` - Lon float64 `json:"lon"` - City string `json:"city"` -} - -func FetchIPLocation() (*float64, *float64, error) { - client := &http.Client{ - Timeout: 10 * time.Second, - } - - resp, err := client.Get("http://ip-api.com/json/") - if err != nil { - return nil, nil, fmt.Errorf("failed to fetch IP location: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, nil, fmt.Errorf("ip-api.com returned status %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response: %w", err) - } - - var data ipAPIResponse - if err := json.Unmarshal(body, &data); err != nil { - return nil, nil, fmt.Errorf("failed to parse response: %w", err) - } - - if data.Lat == 0 && data.Lon == 0 { - return nil, nil, fmt.Errorf("missing location data in response") - } - - log.Infof("Fetched IP-based location: %s (%.4f, %.4f)", data.City, data.Lat, data.Lon) - return &data.Lat, &data.Lon, nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/handlers.go deleted file mode 100644 index 76b08b5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/handlers.go +++ /dev/null @@ -1,182 +0,0 @@ -package wayland - -import ( - "encoding/json" - "fmt" - "net" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - if manager == nil { - models.RespondError(conn, req.ID, "wayland manager not initialized") - return - } - - switch req.Method { - case "wayland.gamma.getState": - handleGetState(conn, req, manager) - case "wayland.gamma.setTemperature": - handleSetTemperature(conn, req, manager) - case "wayland.gamma.setLocation": - handleSetLocation(conn, req, manager) - case "wayland.gamma.setManualTimes": - handleSetManualTimes(conn, req, manager) - case "wayland.gamma.setUseIPLocation": - handleSetUseIPLocation(conn, req, manager) - case "wayland.gamma.setGamma": - handleSetGamma(conn, req, manager) - case "wayland.gamma.setEnabled": - handleSetEnabled(conn, req, manager) - case "wayland.gamma.subscribe": - handleSubscribe(conn, req, manager) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleGetState(conn net.Conn, req models.Request, manager *Manager) { - models.Respond(conn, req.ID, manager.GetState()) -} - -func handleSetTemperature(conn net.Conn, req models.Request, manager *Manager) { - var lowTemp, highTemp int - - if temp, ok := models.Get[float64](req, "temp"); ok { - lowTemp = int(temp) - highTemp = int(temp) - } else { - low, err := params.Float(req.Params, "low") - if err != nil { - models.RespondError(conn, req.ID, "missing temperature parameters (provide 'temp' or both 'low' and 'high')") - return - } - high, err := params.Float(req.Params, "high") - if err != nil { - models.RespondError(conn, req.ID, "missing temperature parameters (provide 'temp' or both 'low' and 'high')") - return - } - lowTemp = int(low) - highTemp = int(high) - } - - if err := manager.SetTemperature(lowTemp, highTemp); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "temperature set"}) -} - -func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) { - lat, err := params.Float(req.Params, "latitude") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - lon, err := params.Float(req.Params, "longitude") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetLocation(lat, lon); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location set"}) -} - -func handleSetManualTimes(conn net.Conn, req models.Request, manager *Manager) { - sunriseStr, sunriseOK := models.Get[string](req, "sunrise") - sunsetStr, sunsetOK := models.Get[string](req, "sunset") - - if !sunriseOK || !sunsetOK || sunriseStr == "" || sunsetStr == "" { - manager.ClearManualTimes() - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"}) - return - } - - sunrise, err := time.Parse("15:04", sunriseStr) - if err != nil { - models.RespondError(conn, req.ID, "invalid sunrise format (use HH:MM)") - return - } - - sunset, err := time.Parse("15:04", sunsetStr) - if err != nil { - models.RespondError(conn, req.ID, "invalid sunset format (use HH:MM)") - return - } - - if err := manager.SetManualTimes(sunrise, sunset); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times set"}) -} - -func handleSetUseIPLocation(conn net.Conn, req models.Request, manager *Manager) { - use, err := params.Bool(req.Params, "use") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - manager.SetUseIPLocation(use) - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "IP location preference set"}) -} - -func handleSetGamma(conn net.Conn, req models.Request, manager *Manager) { - gamma, err := params.Float(req.Params, "gamma") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetGamma(gamma); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "gamma set"}) -} - -func handleSetEnabled(conn net.Conn, req models.Request, manager *Manager) { - enabled, err := params.Bool(req.Params, "enabled") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - manager.SetEnabled(enabled) - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "enabled state set"}) -} - -func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) { - clientID := fmt.Sprintf("client-%p", conn) - stateChan := manager.Subscribe(clientID) - defer manager.Unsubscribe(clientID) - - initialState := manager.GetState() - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - ID: req.ID, - Result: &initialState, - }); err != nil { - return - } - - for state := range stateChan { - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - Result: &state, - }); err != nil { - return - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager.go deleted file mode 100644 index 3540e3d..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager.go +++ /dev/null @@ -1,1256 +0,0 @@ -package wayland - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "io" - "os" - "slices" - "syscall" - "time" - - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" - "github.com/godbus/dbus/v5" - "golang.org/x/sys/unix" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control" -) - -const animKelvinStep = 25 - -func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) { - if err := config.Validate(); err != nil { - return nil, err - } - - if config.ElevationTwilight == 0 { - config.ElevationTwilight = -6.0 - } - if config.ElevationDaylight == 0 { - config.ElevationDaylight = 3.0 - } - - m := &Manager{ - config: config, - display: display, - ctx: display.Context(), - cmdq: make(chan cmd, 128), - stopChan: make(chan struct{}), - updateTrigger: make(chan struct{}, 1), - dirty: make(chan struct{}, 1), - dbusSignal: make(chan *dbus.Signal, 16), - } - - if err := m.setupRegistry(); err != nil { - return nil, err - } - - if err := m.setupDBusMonitor(); err != nil { - log.Warnf("Failed to setup D-Bus monitoring: %v", err) - } - - m.alive = true - m.recalcSchedule(time.Now()) - m.updateStateFromSchedule() - - m.notifierWg.Add(1) - go m.notifier() - - m.wg.Add(1) - go m.schedulerLoop() - - if m.dbusConn != nil { - m.wg.Add(1) - go m.dbusMonitor() - } - - m.wg.Add(1) - go m.waylandActor() - - if config.Enabled { - m.post(func() { - log.Info("Gamma control enabled at startup") - gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) - m.availOutputsMu.RLock() - outs := slices.Clone(m.availableOutputs) - m.availOutputsMu.RUnlock() - if err := m.setupOutputControls(outs, gammaMgr); err != nil { - log.Errorf("Failed to initialize gamma controls: %v", err) - return - } - m.controlsInitialized = true - }) - } - - return m, nil -} - -func (m *Manager) post(fn func()) { - select { - case m.cmdq <- cmd{fn: fn}: - default: - log.Warn("Actor command queue full") - } -} - -func (m *Manager) waylandActor() { - defer m.wg.Done() - for { - select { - case <-m.stopChan: - return - case c := <-m.cmdq: - c.fn() - } - } -} - -func (m *Manager) anyOutputReady() bool { - anyReady := false - m.outputs.Range(func(_ uint32, out *outputState) bool { - if out.rampSize > 0 && !out.failed { - anyReady = true - return false // stop iteration - } - return true - }) - return anyReady -} - -func (m *Manager) setupDBusMonitor() error { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return fmt.Errorf("system bus: %w", err) - } - - matchRule := "type='signal',interface='org.freedesktop.login1.Manager',member='PrepareForSleep',path='/org/freedesktop/login1'" - if err := conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule).Err; err != nil { - conn.Close() - return fmt.Errorf("add match: %w", err) - } - - conn.Signal(m.dbusSignal) - m.dbusConn = conn - return nil -} - -func (m *Manager) setupRegistry() error { - registry, err := m.display.GetRegistry() - if err != nil { - return fmt.Errorf("get registry: %w", err) - } - m.registry = registry - - outputs := make([]*wlclient.Output, 0) - outputNames := make(map[uint32]string) - var gammaMgr *wlr_gamma_control.ZwlrGammaControlManagerV1 - - registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { - switch e.Interface { - case wlr_gamma_control.ZwlrGammaControlManagerV1InterfaceName: - manager := wlr_gamma_control.NewZwlrGammaControlManagerV1(m.ctx) - version := e.Version - if version > 1 { - version = 1 - } - if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil { - gammaMgr = manager - } - case "wl_output": - output := wlclient.NewOutput(m.ctx) - version := e.Version - if version > 4 { - version = 4 - } - if err := registry.Bind(e.Name, e.Interface, version, output); err != nil { - return - } - outputID := output.ID() - output.SetNameHandler(func(ev wlclient.OutputNameEvent) { - outputNames[outputID] = ev.Name - }) - if gammaMgr != nil { - outputs = append(outputs, output) - m.addAvailableOutput(output) - } - m.outputRegNames.Store(outputID, e.Name) - - m.configMutex.RLock() - enabled := m.config.Enabled - m.configMutex.RUnlock() - - if enabled && m.controlsInitialized { - m.post(func() { - if err := m.addOutputControl(output); err != nil { - log.Warnf("Failed to add output control: %v", err) - } - }) - } - } - }) - - registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) { - m.post(func() { - var foundID uint32 - var foundOut *outputState - m.outputs.Range(func(id uint32, out *outputState) bool { - if out.registryName == e.Name { - foundID = id - foundOut = out - return false - } - return true - }) - if foundOut == nil { - return - } - if foundOut.gammaControl != nil { - foundOut.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy() - foundOut.gammaControl = nil - } - m.removeAvailableOutput(foundOut.output) - if foundOut.output != nil && !foundOut.output.IsZombie() { - _ = foundOut.output.Release() - } - m.outputs.Delete(foundID) - - hasOutputs := false - m.outputs.Range(func(_ uint32, _ *outputState) bool { - hasOutputs = true - return false - }) - if !hasOutputs { - m.controlsInitialized = false - } - }) - }) - - if err := m.display.Roundtrip(); err != nil { - return fmt.Errorf("roundtrip 1: %w", err) - } - if err := m.display.Roundtrip(); err != nil { - return fmt.Errorf("roundtrip 2: %w", err) - } - - if gammaMgr == nil { - return errdefs.ErrNoGammaControl - } - if len(outputs) == 0 { - return fmt.Errorf("no outputs") - } - - physicalOutputs := make([]*wlclient.Output, 0, len(outputs)) - for _, output := range outputs { - name := outputNames[output.ID()] - if len(name) >= 9 && name[:9] == "HEADLESS-" { - continue - } - physicalOutputs = append(physicalOutputs, output) - } - - m.gammaControl = gammaMgr - m.availableOutputs = physicalOutputs - return nil -} - -func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_gamma_control.ZwlrGammaControlManagerV1) error { - for _, output := range outputs { - control, err := manager.GetGammaControl(output) - if err != nil { - continue - } - outputID := output.ID() - registryName, _ := m.outputRegNames.Load(outputID) - outState := &outputState{ - id: outputID, - registryName: registryName, - output: output, - gammaControl: control, - } - m.setupControlHandlers(outState, control) - m.outputs.Store(outputID, outState) - } - return nil -} - -func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_control.ZwlrGammaControlV1) { - outputID := state.id - - control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { - size := e.Size - m.post(func() { - if out, ok := m.outputs.Load(outputID); ok { - out.rampSize = size - out.failed = false - out.retryCount = 0 - } - m.lastAppliedTemp = 0 - m.applyCurrentTemp("gamma_size") - }) - }) - - control.SetFailedHandler(func(_ wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { - m.post(func() { - out, ok := m.outputs.Load(outputID) - if !ok { - return - } - if ctrl, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok && ctrl != nil && !ctrl.IsZombie() { - ctrl.Destroy() - } - out.gammaControl = nil - out.failed = true - out.rampSize = 0 - out.retryCount++ - out.lastFailTime = time.Now() - - if !m.outputStillValid(out) { - return - } - - backoff := time.Duration(300<= 10: - return nil - case !m.outputStillValid(out): - return nil - } - if _, ok := m.outputs.Load(out.id); !ok { - return nil - } - - gammaMgr, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) - if !ok { - return fmt.Errorf("no gamma manager") - } - - if existing, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok && existing != nil && !existing.IsZombie() { - existing.Destroy() - out.gammaControl = nil - } - - control, err := gammaMgr.GetGammaControl(out.output) - if err != nil { - if isConnectionDeadErr(err) { - m.markConnectionDead(err) - } - return err - } - - m.setupControlHandlers(out, control) - out.gammaControl = control - out.failed = false - return nil -} - -func (m *Manager) markConnectionDead(err error) { - if m.connectionDead.Swap(true) { - return - } - log.Errorf("gamma: wayland connection appears dead (%v); pausing gamma operations", err) -} - -func (m *Manager) recalcSchedule(now time.Time) { - m.configMutex.RLock() - config := m.config - m.configMutex.RUnlock() - - m.scheduleMutex.Lock() - defer m.scheduleMutex.Unlock() - - dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) - alreadyValid := !m.schedule.times.Sunrise.IsZero() - if m.schedule.calcDay.Equal(dayStart) && alreadyValid { - return - } - - var times SunTimes - var cond SunCondition - - if config.ManualSunrise != nil && config.ManualSunset != nil { - dur := time.Hour - if config.ManualDuration != nil { - dur = *config.ManualDuration - } - sunrise := time.Date(now.Year(), now.Month(), now.Day(), - config.ManualSunrise.Hour(), config.ManualSunrise.Minute(), config.ManualSunrise.Second(), 0, now.Location()) - sunset := time.Date(now.Year(), now.Month(), now.Day(), - config.ManualSunset.Hour(), config.ManualSunset.Minute(), config.ManualSunset.Second(), 0, now.Location()) - times = SunTimes{ - Dawn: sunrise.Add(-dur), - Sunrise: sunrise, - Sunset: sunset, - Night: sunset.Add(dur), - } - cond = SunNormal - } else { - lat, lon := m.getLocation() - if lat == nil || lon == nil { - m.gammaState = StateStatic - return - } - times, cond = CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight) - } - - m.schedule.calcDay = dayStart - m.schedule.times = times - m.schedule.condition = cond - - switch cond { - case SunNormal: - m.gammaState = StateNormal - tempDiff := config.HighTemp - config.LowTemp - if tempDiff > 0 { - dawnDur := times.Sunrise.Sub(times.Dawn) - nightDur := times.Night.Sub(times.Sunset) - m.schedule.dawnStepTime = time.Duration(max(1, int(dawnDur.Seconds())*animKelvinStep/tempDiff)) * time.Second - m.schedule.nightStepTime = time.Duration(max(1, int(nightDur.Seconds())*animKelvinStep/tempDiff)) * time.Second - } - case SunMidnightSun: - m.gammaState = StateStatic - case SunPolarNight: - m.gammaState = StateStatic - } -} - -func (m *Manager) SetGeoClient(client geolocation.Client) { - m.geoClient = client -} - -func (m *Manager) getLocation() (*float64, *float64) { - m.configMutex.RLock() - config := m.config - m.configMutex.RUnlock() - - if config.Latitude != nil && config.Longitude != nil { - return config.Latitude, config.Longitude - } - if !config.UseIPLocation { - return nil, nil - } - if m.geoClient == nil { - return nil, nil - } - - m.locationMutex.RLock() - if m.cachedIPLat != nil && m.cachedIPLon != nil { - lat, lon := m.cachedIPLat, m.cachedIPLon - m.locationMutex.RUnlock() - return lat, lon - } - m.locationMutex.RUnlock() - - location, err := m.geoClient.GetLocation() - if err != nil { - return nil, nil - } - - m.locationMutex.Lock() - m.cachedIPLat = &location.Latitude - m.cachedIPLon = &location.Longitude - m.locationMutex.Unlock() - return m.cachedIPLat, m.cachedIPLon -} - -func (m *Manager) hasValidSchedule() bool { - m.scheduleMutex.RLock() - defer m.scheduleMutex.RUnlock() - return !m.schedule.times.Sunrise.IsZero() -} - -func (m *Manager) getSunPosition(now time.Time) float64 { - m.scheduleMutex.RLock() - sched := m.schedule - state := m.gammaState - m.scheduleMutex.RUnlock() - - if sched.times.Sunrise.IsZero() { - return 1.0 - } - - switch state { - case StateStatic: - if sched.condition == SunMidnightSun { - return 1.0 - } - return 0.0 - case StateNormal: - return m.getSunPositionNormal(now, sched.times) - } - return 1.0 -} - -func (m *Manager) getSunPositionNormal(now time.Time, times SunTimes) float64 { - if now.Before(times.Dawn) { - return 0.0 - } - if now.Before(times.Sunrise) { - return interpolate(now, times.Dawn, times.Sunrise) - } - if now.Before(times.Sunset) { - return 1.0 - } - if now.Before(times.Night) { - return interpolate(now, times.Night, times.Sunset) - } - return 0.0 -} - -func interpolate(now time.Time, start, stop time.Time) float64 { - if start.Equal(stop) { - return 1.0 - } - pos := float64(now.Sub(start)) / float64(stop.Sub(start)) - switch { - case pos > 1.0: - return 1.0 - case pos < 0.0: - return 0.0 - default: - return pos - } -} - -func (m *Manager) getTempFromPosition(pos float64) int { - m.configMutex.RLock() - low, high := m.config.LowTemp, m.config.HighTemp - m.configMutex.RUnlock() - return low + int(float64(high-low)*pos) -} - -func (m *Manager) getNextDeadline(now time.Time) time.Time { - m.scheduleMutex.RLock() - sched := m.schedule - state := m.gammaState - m.scheduleMutex.RUnlock() - - switch state { - case StateStatic: - return m.tomorrow(now) - case StateNormal: - return m.getDeadlineNormal(now, sched) - default: - return m.tomorrow(now) - } -} - -func (m *Manager) getDeadlineNormal(now time.Time, sched sunSchedule) time.Time { - times := sched.times - switch { - case now.Before(times.Dawn): - return times.Dawn - case now.Before(times.Sunrise): - return now.Add(sched.dawnStepTime) - case now.Before(times.Sunset): - return times.Sunset - case now.Before(times.Night): - return now.Add(sched.nightStepTime) - default: - return m.tomorrowDawn(now) - } -} - -func (m *Manager) tomorrowDawn(now time.Time) time.Time { - tomorrow := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location()) - - m.configMutex.RLock() - config := m.config - m.configMutex.RUnlock() - - if config.ManualSunrise != nil { - dur := time.Hour - if config.ManualDuration != nil { - dur = *config.ManualDuration - } - return time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), - config.ManualSunrise.Hour(), config.ManualSunrise.Minute(), config.ManualSunrise.Second(), 0, tomorrow.Location()).Add(-dur) - } - - lat, lon := m.getLocation() - if lat == nil || lon == nil { - return tomorrow - } - - times, cond := CalculateSunTimesWithTwilight(*lat, *lon, tomorrow, config.ElevationTwilight, config.ElevationDaylight) - if cond != SunNormal { - return tomorrow - } - return times.Dawn -} - -func (m *Manager) tomorrow(now time.Time) time.Time { - return time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location()) -} - -func (m *Manager) schedulerLoop() { - defer m.wg.Done() - - m.configMutex.RLock() - enabled := m.config.Enabled - m.configMutex.RUnlock() - - if enabled { - m.post(func() { m.applyCurrentTemp("startup") }) - } - - var timer *time.Timer - for { - m.configMutex.RLock() - enabled := m.config.Enabled - m.configMutex.RUnlock() - - now := time.Now() - m.recalcSchedule(now) - - var waitDur time.Duration - if enabled { - deadline := m.getNextDeadline(now) - waitDur = time.Until(deadline) - if waitDur < time.Second { - waitDur = time.Second - } - } else { - waitDur = 24 * time.Hour - } - - if timer != nil { - timer.Stop() - } - timer = time.NewTimer(waitDur) - - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.updateTrigger: - timer.Stop() - m.scheduleMutex.Lock() - m.schedule.calcDay = time.Time{} - m.scheduleMutex.Unlock() - m.recalcSchedule(time.Now()) - m.updateStateFromSchedule() - m.configMutex.RLock() - enabled := m.config.Enabled - m.configMutex.RUnlock() - if enabled { - m.post(func() { m.applyCurrentTemp("updateTrigger") }) - } - case <-timer.C: - m.configMutex.RLock() - enabled := m.config.Enabled - m.configMutex.RUnlock() - if enabled { - m.post(func() { m.applyCurrentTemp("timer") }) - } - } - } -} - -func (m *Manager) applyCurrentTemp(_ string) { - if !m.controlsInitialized || !m.anyOutputReady() { - return - } - - // Ensure schedule is up-to-date (handles display wake after overnight sleep) - m.recalcSchedule(time.Now()) - - m.configMutex.RLock() - low, high := m.config.LowTemp, m.config.HighTemp - m.configMutex.RUnlock() - - if low == high { - m.applyGamma(low) - m.updateStateFromSchedule() - return - } - - if !m.hasValidSchedule() { - m.updateStateFromSchedule() - return - } - - now := time.Now() - pos := m.getSunPosition(now) - temp := m.getTempFromPosition(pos) - - m.applyGamma(temp) - m.updateStateFromSchedule() -} - -func (m *Manager) applyGamma(temp int) { - m.configMutex.RLock() - gamma := m.config.Gamma - m.configMutex.RUnlock() - - switch { - case m.connectionDead.Load(): - return - case !m.controlsInitialized: - return - case m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma: - return - } - - var outs []*outputState - m.outputs.Range(func(_ uint32, out *outputState) bool { - outs = append(outs, out) - return true - }) - if len(outs) == 0 { - return - } - - type job struct { - out *outputState - data []byte - } - var jobs []job - - for _, out := range outs { - switch { - case out.failed: - continue - case out.rampSize == 0: - continue - case out.gammaControl == nil: - continue - case !m.outputStillValid(out): - continue - } - ramp := GenerateGammaRamp(out.rampSize, temp, gamma) - buf := bytes.NewBuffer(make([]byte, 0, int(out.rampSize)*6)) - for _, v := range ramp.Red { - binary.Write(buf, binary.LittleEndian, v) - } - for _, v := range ramp.Green { - binary.Write(buf, binary.LittleEndian, v) - } - for _, v := range ramp.Blue { - binary.Write(buf, binary.LittleEndian, v) - } - jobs = append(jobs, job{out: out, data: buf.Bytes()}) - } - - for _, j := range jobs { - err := m.setGammaBytes(j.out, j.data) - if err == nil { - continue - } - log.Warnf("gamma: failed to set output %d: %v", j.out.id, err) - j.out.failed = true - j.out.rampSize = 0 - if isConnectionDeadErr(err) { - m.markConnectionDead(err) - return - } - } - - m.lastAppliedTemp = temp - m.lastAppliedGamma = gamma -} - -func (m *Manager) setGammaBytes(out *outputState, data []byte) error { - if out.gammaControl == nil { - return fmt.Errorf("no gamma control") - } - ctrl, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) - if !ok || ctrl == nil || ctrl.IsZombie() { - return fmt.Errorf("gamma control invalid") - } - - fd, err := MemfdCreate("gamma-ramp", 0) - if err != nil { - return err - } - defer syscall.Close(fd) - - if err := syscall.Ftruncate(fd, int64(len(data))); err != nil { - return err - } - - dupFd, err := syscall.Dup(fd) - if err != nil { - return err - } - f := os.NewFile(uintptr(dupFd), "gamma") - defer f.Close() - - if _, err := f.Write(data); err != nil { - return err - } - syscall.Seek(fd, 0, 0) - - return ctrl.SetGamma(fd) -} - -func (m *Manager) updateStateFromSchedule() { - now := time.Now() - - m.configMutex.RLock() - config := m.config - m.configMutex.RUnlock() - - m.scheduleMutex.RLock() - times := m.schedule.times - m.scheduleMutex.RUnlock() - - var pos float64 - var temp int - var isDay bool - var deadline time.Time - - if times.Sunrise.IsZero() { - pos = 1.0 - temp = config.HighTemp - isDay = true - deadline = m.tomorrow(now) - } else { - pos = m.getSunPosition(now) - temp = m.getTempFromPosition(pos) - deadline = m.getNextDeadline(now) - isDay = now.After(times.Sunrise) && now.Before(times.Sunset) - } - - newState := State{ - Config: config, - CurrentTemp: temp, - NextTransition: deadline, - SunriseTime: times.Sunrise, - SunsetTime: times.Sunset, - DawnTime: times.Dawn, - NightTime: times.Night, - IsDay: isDay, - SunPosition: pos, - } - - m.stateMutex.Lock() - m.state = &newState - m.stateMutex.Unlock() - - m.notifySubscribers() -} - -func (m *Manager) notifier() { - defer m.notifierWg.Done() - const minGap = 100 * time.Millisecond - timer := time.NewTimer(minGap) - timer.Stop() - var pending bool - - for { - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.dirty: - if pending { - continue - } - pending = true - timer.Reset(minGap) - case <-timer.C: - if !pending { - continue - } - currentState := m.GetState() - if m.lastNotified != nil && !stateChanged(m.lastNotified, ¤tState) { - pending = false - continue - } - m.subscribers.Range(func(_ string, ch chan State) bool { - select { - case ch <- currentState: - default: - } - return true - }) - stateCopy := currentState - m.lastNotified = &stateCopy - pending = false - } - } -} - -func (m *Manager) dbusMonitor() { - defer m.wg.Done() - for { - select { - case <-m.stopChan: - return - case sig := <-m.dbusSignal: - if sig == nil { - continue - } - m.handleDBusSignal(sig) - } - } -} - -func (m *Manager) handleDBusSignal(sig *dbus.Signal) { - switch { - case sig.Name != "org.freedesktop.login1.Manager.PrepareForSleep": - return - case len(sig.Body) == 0: - return - } - preparing, ok := sig.Body[0].(bool) - if !ok || preparing { - return - } - m.configMutex.RLock() - enabled := m.config.Enabled - m.configMutex.RUnlock() - if !enabled { - return - } - time.AfterFunc(500*time.Millisecond, func() { - m.post(m.handleResume) - }) -} - -func (m *Manager) handleResume() { - m.configMutex.RLock() - stillEnabled := m.config.Enabled - m.configMutex.RUnlock() - - switch { - case !stillEnabled: - return - case !m.controlsInitialized: - return - case m.connectionDead.Load(): - return - } - - // Compositors (Niri, Hyprland, wlroots-based) re-apply the cached gamma - // ramp to DRM on resume; gamma_control objects stay valid. We just need - // to force a resend so the schedule catches up with the current time of - // day — the original #1235 regression was caused by lastAppliedTemp - // matching and the send being skipped. - m.recalcSchedule(time.Now()) - m.lastAppliedTemp = 0 - m.applyCurrentTemp("resume") -} - -func (m *Manager) triggerUpdate() { - select { - case m.updateTrigger <- struct{}{}: - default: - } -} - -func (m *Manager) SetConfig(config Config) error { - if err := config.Validate(); err != nil { - return err - } - m.configMutex.Lock() - m.config = config - m.configMutex.Unlock() - m.triggerUpdate() - return nil -} - -func (m *Manager) SetTemperature(low, high int) error { - m.configMutex.Lock() - if m.config.LowTemp == low && m.config.HighTemp == high { - m.configMutex.Unlock() - return nil - } - m.config.LowTemp = low - m.config.HighTemp = high - err := m.config.Validate() - m.configMutex.Unlock() - if err != nil { - return err - } - m.triggerUpdate() - return nil -} - -func (m *Manager) SetLocation(lat, lon float64) error { - m.configMutex.Lock() - if m.config.Latitude != nil && m.config.Longitude != nil && - *m.config.Latitude == lat && *m.config.Longitude == lon && !m.config.UseIPLocation { - m.configMutex.Unlock() - return nil - } - m.config.Latitude = &lat - m.config.Longitude = &lon - m.config.UseIPLocation = false - err := m.config.Validate() - m.configMutex.Unlock() - if err != nil { - return err - } - m.triggerUpdate() - return nil -} - -func (m *Manager) SetUseIPLocation(use bool) { - m.configMutex.Lock() - if m.config.UseIPLocation == use { - m.configMutex.Unlock() - return - } - m.config.UseIPLocation = use - if use { - m.config.Latitude = nil - m.config.Longitude = nil - } - m.configMutex.Unlock() - - if use { - m.locationMutex.Lock() - m.cachedIPLat = nil - m.cachedIPLon = nil - m.locationMutex.Unlock() - } - m.triggerUpdate() -} - -func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error { - m.configMutex.Lock() - if m.config.ManualSunrise != nil && m.config.ManualSunset != nil && - m.config.ManualSunrise.Hour() == sunrise.Hour() && m.config.ManualSunrise.Minute() == sunrise.Minute() && - m.config.ManualSunset.Hour() == sunset.Hour() && m.config.ManualSunset.Minute() == sunset.Minute() { - m.configMutex.Unlock() - return nil - } - m.config.ManualSunrise = &sunrise - m.config.ManualSunset = &sunset - err := m.config.Validate() - m.configMutex.Unlock() - if err != nil { - return err - } - m.triggerUpdate() - return nil -} - -func (m *Manager) ClearManualTimes() { - m.configMutex.Lock() - if m.config.ManualSunrise == nil && m.config.ManualSunset == nil { - m.configMutex.Unlock() - return - } - m.config.ManualSunrise = nil - m.config.ManualSunset = nil - m.configMutex.Unlock() - m.triggerUpdate() -} - -func (m *Manager) SetGamma(gamma float64) error { - m.configMutex.Lock() - if m.config.Gamma == gamma { - m.configMutex.Unlock() - return nil - } - m.config.Gamma = gamma - err := m.config.Validate() - m.configMutex.Unlock() - if err != nil { - return err - } - m.triggerUpdate() - return nil -} - -func (m *Manager) SetEnabled(enabled bool) { - m.configMutex.Lock() - wasEnabled := m.config.Enabled - if wasEnabled == enabled { - m.configMutex.Unlock() - return - } - m.config.Enabled = enabled - highTemp := m.config.HighTemp - m.configMutex.Unlock() - - switch { - case enabled && !m.controlsInitialized: - m.post(func() { - gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) - m.availOutputsMu.RLock() - outs := slices.Clone(m.availableOutputs) - m.availOutputsMu.RUnlock() - if err := m.setupOutputControls(outs, gammaMgr); err != nil { - log.Errorf("gamma: failed to create controls: %v", err) - return - } - m.controlsInitialized = true - m.triggerUpdate() - }) - case enabled && !wasEnabled: - m.triggerUpdate() - case !enabled && m.controlsInitialized: - m.post(func() { - m.outputs.Range(func(id uint32, out *outputState) bool { - if out.gammaControl != nil { - out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy() - } - return true - }) - m.outputs.Range(func(key uint32, _ *outputState) bool { - m.outputs.Delete(key) - return true - }) - m.controlsInitialized = false - }) - _ = highTemp - } -} - -func (m *Manager) Close() { - close(m.stopChan) - m.wg.Wait() - m.notifierWg.Wait() - - m.subscribers.Range(func(key string, ch chan State) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - - m.outputs.Range(func(_ uint32, out *outputState) bool { - if ctrl, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok { - ctrl.Destroy() - } - return true - }) - m.outputs.Range(func(key uint32, _ *outputState) bool { - m.outputs.Delete(key) - return true - }) - - if manager, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1); ok { - manager.Destroy() - } - - if m.dbusConn != nil { - m.dbusConn.RemoveSignal(m.dbusSignal) - m.dbusConn.Close() - } -} - -func MemfdCreate(name string, flags int) (int, error) { - fd, err := unix.MemfdCreate(name, flags) - if err != nil { - return -1, err - } - return fd, nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager_test.go deleted file mode 100644 index 8c00129..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager_test.go +++ /dev/null @@ -1,414 +0,0 @@ -package wayland - -import ( - "errors" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" -) - -func TestManager_ActorSerializesOutputStateAccess(t *testing.T) { - m := &Manager{ - cmdq: make(chan cmd, 8192), - stopChan: make(chan struct{}), - } - - m.wg.Add(1) - go m.waylandActor() - - state := &outputState{ - id: 1, - registryName: 100, - rampSize: 256, - } - m.outputs.Store(state.id, state) - - var wg sync.WaitGroup - const goroutines = 50 - const iterations = 100 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.post(func() { - if out, ok := m.outputs.Load(state.id); ok { - out.rampSize = uint32(j) - out.failed = j%2 == 0 - out.retryCount = j - out.lastFailTime = time.Now() - } - }) - } - }(i) - } - - wg.Wait() - - done := make(chan struct{}) - m.post(func() { close(done) }) - <-done - - close(m.stopChan) - m.wg.Wait() -} - -func TestManager_ConcurrentSubscriberAccess(t *testing.T) { - m := &Manager{ - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - updateTrigger: make(chan struct{}, 1), - } - - var wg sync.WaitGroup - const goroutines = 20 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - subID := string(rune('a' + id)) - ch := m.Subscribe(subID) - assert.NotNil(t, ch) - time.Sleep(time.Millisecond) - m.Unsubscribe(subID) - }(i) - } - - wg.Wait() -} - -func TestManager_ConcurrentGetState(t *testing.T) { - m := &Manager{ - state: &State{ - CurrentTemp: 5000, - IsDay: true, - }, - } - - var wg sync.WaitGroup - const goroutines = 50 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - s := m.GetState() - assert.GreaterOrEqual(t, s.CurrentTemp, 0) - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.stateMutex.Lock() - m.state = &State{ - CurrentTemp: 4000 + i*100, - IsDay: j%2 == 0, - } - m.stateMutex.Unlock() - } - }(i) - } - - wg.Wait() -} - -func TestManager_ConcurrentConfigAccess(t *testing.T) { - m := &Manager{ - config: DefaultConfig(), - } - - var wg sync.WaitGroup - const goroutines = 30 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.configMutex.RLock() - _ = m.config.LowTemp - _ = m.config.HighTemp - _ = m.config.Enabled - m.configMutex.RUnlock() - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.configMutex.Lock() - m.config.LowTemp = 3000 + j - m.config.HighTemp = 7000 - j - m.config.Enabled = j%2 == 0 - m.configMutex.Unlock() - } - }(i) - } - - wg.Wait() -} - -func TestManager_SyncmapOutputsConcurrentAccess(t *testing.T) { - m := &Manager{} - - var wg sync.WaitGroup - const goroutines = 30 - const iterations = 50 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - key := uint32(id) - - for j := 0; j < iterations; j++ { - state := &outputState{ - id: key, - rampSize: uint32(j), - failed: j%2 == 0, - } - m.outputs.Store(key, state) - - if loaded, ok := m.outputs.Load(key); ok { - assert.Equal(t, key, loaded.id) - } - - m.outputs.Range(func(k uint32, v *outputState) bool { - _ = v.rampSize - _ = v.failed - return true - }) - } - - m.outputs.Delete(key) - }(i) - } - - wg.Wait() -} - -func TestManager_LocationCacheConcurrentAccess(t *testing.T) { - m := &Manager{} - - var wg sync.WaitGroup - const goroutines = 20 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.locationMutex.RLock() - _ = m.cachedIPLat - _ = m.cachedIPLon - m.locationMutex.RUnlock() - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - lat := float64(40 + i) - lon := float64(-74 + j) - m.locationMutex.Lock() - m.cachedIPLat = &lat - m.cachedIPLon = &lon - m.locationMutex.Unlock() - } - }(i) - } - - wg.Wait() -} - -func TestManager_ScheduleConcurrentAccess(t *testing.T) { - now := time.Now() - m := &Manager{ - schedule: sunSchedule{ - times: SunTimes{ - Dawn: now, - Sunrise: now.Add(time.Hour), - Sunset: now.Add(12 * time.Hour), - Night: now.Add(13 * time.Hour), - }, - }, - } - - var wg sync.WaitGroup - const goroutines = 20 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.scheduleMutex.RLock() - _ = m.schedule.times.Dawn - _ = m.schedule.times.Sunrise - _ = m.schedule.times.Sunset - _ = m.schedule.condition - m.scheduleMutex.RUnlock() - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.scheduleMutex.Lock() - m.schedule.times.Dawn = time.Now() - m.schedule.times.Sunrise = time.Now().Add(time.Hour) - m.schedule.condition = SunNormal - m.scheduleMutex.Unlock() - } - }() - } - - wg.Wait() -} - -func TestInterpolate_EdgeCases(t *testing.T) { - now := time.Now() - - tests := []struct { - name string - now time.Time - start time.Time - stop time.Time - expected float64 - }{ - { - name: "same start and stop", - now: now, - start: now, - stop: now, - expected: 1.0, - }, - { - name: "now before start", - now: now, - start: now.Add(time.Hour), - stop: now.Add(2 * time.Hour), - expected: 0.0, - }, - { - name: "now after stop", - now: now.Add(3 * time.Hour), - start: now, - stop: now.Add(time.Hour), - expected: 1.0, - }, - { - name: "now at midpoint", - now: now.Add(30 * time.Minute), - start: now, - stop: now.Add(time.Hour), - expected: 0.5, - }, - { - name: "now equals start", - now: now, - start: now, - stop: now.Add(time.Hour), - expected: 0.0, - }, - { - name: "now equals stop", - now: now.Add(time.Hour), - start: now, - stop: now.Add(time.Hour), - expected: 1.0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := interpolate(tt.now, tt.start, tt.stop) - assert.InDelta(t, tt.expected, result, 0.01) - }) - } -} - -func TestGenerateGammaRamp_ZeroSize(t *testing.T) { - ramp := GenerateGammaRamp(0, 5000, 1.0) - assert.Empty(t, ramp.Red) - assert.Empty(t, ramp.Green) - assert.Empty(t, ramp.Blue) -} - -func TestGenerateGammaRamp_ValidSizes(t *testing.T) { - sizes := []uint32{1, 256, 1024} - temps := []int{1000, 4000, 6500, 10000} - gammas := []float64{0.5, 1.0, 2.0} - - for _, size := range sizes { - for _, temp := range temps { - for _, gamma := range gammas { - ramp := GenerateGammaRamp(size, temp, gamma) - assert.Len(t, ramp.Red, int(size)) - assert.Len(t, ramp.Green, int(size)) - assert.Len(t, ramp.Blue, int(size)) - } - } - } -} - -func TestNotifySubscribers_NonBlocking(t *testing.T) { - m := &Manager{ - dirty: make(chan struct{}, 1), - } - - for i := 0; i < 10; i++ { - m.notifySubscribers() - } - - assert.Len(t, m.dirty, 1) -} - -func TestNewManager_GetRegistryError(t *testing.T) { - mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) - - mockDisplay.EXPECT().Context().Return(nil) - mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry")) - - config := DefaultConfig() - _, err := NewManager(mockDisplay, config) - assert.Error(t, err) - assert.Contains(t, err.Error(), "get registry") -} - -func TestNewManager_InvalidConfig(t *testing.T) { - mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) - - config := Config{ - LowTemp: 500, - HighTemp: 6500, - Gamma: 1.0, - } - - _, err := NewManager(mockDisplay, config) - assert.Error(t, err) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc.go deleted file mode 100644 index c978a8e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc.go +++ /dev/null @@ -1,122 +0,0 @@ -package wayland - -import ( - "math" - "time" -) - -const ( - degToRad = math.Pi / 180.0 - radToDeg = 180.0 / math.Pi -) - -type SunCondition int - -const ( - SunNormal SunCondition = iota - SunMidnightSun - SunPolarNight -) - -type SunTimes struct { - Dawn time.Time - Sunrise time.Time - Sunset time.Time - Night time.Time -} - -func daysInYear(year int) int { - if (year%4 == 0 && year%100 != 0) || year%400 == 0 { - return 366 - } - return 365 -} - -func dateOrbitAngle(t time.Time) float64 { - return 2 * math.Pi / float64(daysInYear(t.Year())) * float64(t.YearDay()-1) -} - -func equationOfTime(orbitAngle float64) float64 { - return 4 * (0.000075 + - 0.001868*math.Cos(orbitAngle) - - 0.032077*math.Sin(orbitAngle) - - 0.014615*math.Cos(2*orbitAngle) - - 0.040849*math.Sin(2*orbitAngle)) -} - -func sunDeclination(orbitAngle float64) float64 { - return 0.006918 - - 0.399912*math.Cos(orbitAngle) + - 0.070257*math.Sin(orbitAngle) - - 0.006758*math.Cos(2*orbitAngle) + - 0.000907*math.Sin(2*orbitAngle) - - 0.002697*math.Cos(3*orbitAngle) + - 0.00148*math.Sin(3*orbitAngle) -} - -func sunHourAngle(latRad, declination, targetSunRad float64) float64 { - return math.Acos(math.Cos(targetSunRad)/ - math.Cos(latRad)*math.Cos(declination) - - math.Tan(latRad)*math.Tan(declination)) -} - -func hourAngleToSeconds(hourAngle, eqtime float64) float64 { - return radToDeg * (4.0*math.Pi - 4*hourAngle - eqtime) * 60 -} - -func sunCondition(latRad, declination float64) SunCondition { - signLat := latRad >= 0 - signDecl := declination >= 0 - if signLat == signDecl { - return SunMidnightSun - } - return SunPolarNight -} - -func CalculateSunTimesWithTwilight(lat, lon float64, date time.Time, elevTwilight, elevDaylight float64) (SunTimes, SunCondition) { - latRad := lat * degToRad - elevTwilightRad := (90.833 - elevTwilight) * degToRad - elevDaylightRad := (90.833 - elevDaylight) * degToRad - - utc := date.UTC() - orbitAngle := dateOrbitAngle(utc) - decl := sunDeclination(orbitAngle) - eqtime := equationOfTime(orbitAngle) - - haTwilight := sunHourAngle(latRad, decl, elevTwilightRad) - haDaylight := sunHourAngle(latRad, decl, elevDaylightRad) - - if math.IsNaN(haTwilight) || math.IsNaN(haDaylight) { - cond := sunCondition(latRad, decl) - return SunTimes{}, cond - } - - dayStart := time.Date(utc.Year(), utc.Month(), utc.Day(), 0, 0, 0, 0, time.UTC) - lonOffset := time.Duration(-lon*4) * time.Minute - - dawnSecs := hourAngleToSeconds(math.Abs(haTwilight), eqtime) - sunriseSecs := hourAngleToSeconds(math.Abs(haDaylight), eqtime) - sunsetSecs := hourAngleToSeconds(-math.Abs(haDaylight), eqtime) - nightSecs := hourAngleToSeconds(-math.Abs(haTwilight), eqtime) - - return SunTimes{ - Dawn: dayStart.Add(time.Duration(dawnSecs)*time.Second + lonOffset).In(date.Location()), - Sunrise: dayStart.Add(time.Duration(sunriseSecs)*time.Second + lonOffset).In(date.Location()), - Sunset: dayStart.Add(time.Duration(sunsetSecs)*time.Second + lonOffset).In(date.Location()), - Night: dayStart.Add(time.Duration(nightSecs)*time.Second + lonOffset).In(date.Location()), - }, SunNormal -} - -func CalculateSunTimes(lat, lon float64, date time.Time) SunTimes { - times, cond := CalculateSunTimesWithTwilight(lat, lon, date, -6.0, 3.0) - switch cond { - case SunMidnightSun: - dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location()) - dayEnd := dayStart.Add(24*time.Hour - time.Second) - return SunTimes{Dawn: dayStart, Sunrise: dayStart, Sunset: dayEnd, Night: dayEnd} - case SunPolarNight: - dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location()) - return SunTimes{Dawn: dayStart, Sunrise: dayStart, Sunset: dayStart, Night: dayStart} - } - return times -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc_test.go deleted file mode 100644 index b2815d9..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc_test.go +++ /dev/null @@ -1,387 +0,0 @@ -package wayland - -import ( - "math" - "testing" - "time" -) - -func calculateTemperature(config Config, now time.Time) int { - if !config.Enabled { - return config.HighTemp - } - - var sunrise, sunset time.Time - - if config.ManualSunrise != nil && config.ManualSunset != nil { - year, month, day := now.Date() - loc := now.Location() - - sunrise = time.Date(year, month, day, - config.ManualSunrise.Hour(), - config.ManualSunrise.Minute(), - config.ManualSunrise.Second(), 0, loc) - sunset = time.Date(year, month, day, - config.ManualSunset.Hour(), - config.ManualSunset.Minute(), - config.ManualSunset.Second(), 0, loc) - - if sunset.Before(sunrise) { - sunset = sunset.Add(24 * time.Hour) - } - } else if config.UseIPLocation { - lat, lon, err := FetchIPLocation() - if err != nil { - return config.HighTemp - } - times := CalculateSunTimes(*lat, *lon, now) - sunrise = times.Sunrise - sunset = times.Sunset - } else if config.Latitude != nil && config.Longitude != nil { - times := CalculateSunTimes(*config.Latitude, *config.Longitude, now) - sunrise = times.Sunrise - sunset = times.Sunset - } else { - return config.HighTemp - } - - if now.Before(sunrise) || now.After(sunset) { - return config.LowTemp - } - return config.HighTemp -} - -func calculateNextTransition(config Config, now time.Time) time.Time { - if !config.Enabled { - return now.Add(24 * time.Hour) - } - - var sunrise, sunset time.Time - - if config.ManualSunrise != nil && config.ManualSunset != nil { - year, month, day := now.Date() - loc := now.Location() - - sunrise = time.Date(year, month, day, - config.ManualSunrise.Hour(), - config.ManualSunrise.Minute(), - config.ManualSunrise.Second(), 0, loc) - sunset = time.Date(year, month, day, - config.ManualSunset.Hour(), - config.ManualSunset.Minute(), - config.ManualSunset.Second(), 0, loc) - - if sunset.Before(sunrise) { - sunset = sunset.Add(24 * time.Hour) - } - } else if config.UseIPLocation { - lat, lon, err := FetchIPLocation() - if err != nil { - return now.Add(24 * time.Hour) - } - times := CalculateSunTimes(*lat, *lon, now) - sunrise = times.Sunrise - sunset = times.Sunset - } else if config.Latitude != nil && config.Longitude != nil { - times := CalculateSunTimes(*config.Latitude, *config.Longitude, now) - sunrise = times.Sunrise - sunset = times.Sunset - } else { - return now.Add(24 * time.Hour) - } - - if now.Before(sunrise) { - return sunrise - } - if now.Before(sunset) { - return sunset - } - - if config.ManualSunrise != nil && config.ManualSunset != nil { - year, month, day := now.Add(24 * time.Hour).Date() - loc := now.Location() - nextSunrise := time.Date(year, month, day, - config.ManualSunrise.Hour(), - config.ManualSunrise.Minute(), - config.ManualSunrise.Second(), 0, loc) - return nextSunrise - } - - if config.UseIPLocation { - lat, lon, err := FetchIPLocation() - if err != nil { - return now.Add(24 * time.Hour) - } - nextDayTimes := CalculateSunTimes(*lat, *lon, now.Add(24*time.Hour)) - return nextDayTimes.Sunrise - } - - if config.Latitude != nil && config.Longitude != nil { - nextDayTimes := CalculateSunTimes(*config.Latitude, *config.Longitude, now.Add(24*time.Hour)) - return nextDayTimes.Sunrise - } - - return now.Add(24 * time.Hour) -} - -func TestCalculateSunTimes(t *testing.T) { - tests := []struct { - name string - lat float64 - lon float64 - date time.Time - checkFunc func(*testing.T, SunTimes) - }{ - { - name: "new_york_summer", - lat: 40.7128, - lon: -74.0060, - date: time.Date(2024, 6, 21, 12, 0, 0, 0, time.Local), - checkFunc: func(t *testing.T, times SunTimes) { - if times.Sunrise.Hour() < 4 || times.Sunrise.Hour() > 6 { - t.Logf("sunrise: %v", times.Sunrise) - } - if times.Sunset.Hour() < 19 || times.Sunset.Hour() > 21 { - t.Logf("sunset: %v", times.Sunset) - } - if !times.Sunset.After(times.Sunrise) { - t.Error("sunset should be after sunrise") - } - }, - }, - { - name: "london_winter", - lat: 51.5074, - lon: -0.1278, - date: time.Date(2024, 12, 21, 12, 0, 0, 0, time.UTC), - checkFunc: func(t *testing.T, times SunTimes) { - if times.Sunrise.Hour() < 7 || times.Sunrise.Hour() > 9 { - t.Errorf("unexpected sunrise hour: %d", times.Sunrise.Hour()) - } - if times.Sunset.Hour() < 15 || times.Sunset.Hour() > 17 { - t.Errorf("unexpected sunset hour: %d", times.Sunset.Hour()) - } - }, - }, - { - name: "equator_equinox", - lat: 0.0, - lon: 0.0, - date: time.Date(2024, 3, 20, 12, 0, 0, 0, time.UTC), - checkFunc: func(t *testing.T, times SunTimes) { - if times.Sunrise.Hour() < 5 || times.Sunrise.Hour() > 7 { - t.Errorf("unexpected sunrise hour: %d", times.Sunrise.Hour()) - } - if times.Sunset.Hour() < 17 || times.Sunset.Hour() > 19 { - t.Errorf("unexpected sunset hour: %d", times.Sunset.Hour()) - } - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - times := CalculateSunTimes(tt.lat, tt.lon, tt.date) - tt.checkFunc(t, times) - }) - } -} - -func TestCalculateTemperature(t *testing.T) { - lat := 40.7128 - lon := -74.0060 - date := time.Date(2024, 6, 21, 0, 0, 0, 0, time.Local) - - config := Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: &lat, - Longitude: &lon, - Enabled: true, - } - - times := CalculateSunTimes(lat, lon, date) - - tests := []struct { - name string - timeFunc func() time.Time - wantTemp int - }{ - { - name: "midnight", - timeFunc: func() time.Time { return times.Sunrise.Add(-4 * time.Hour) }, - wantTemp: 4000, - }, - { - name: "sunrise", - timeFunc: func() time.Time { return times.Sunrise }, - wantTemp: 6500, - }, - { - name: "noon", - timeFunc: func() time.Time { return times.Sunrise.Add(6 * time.Hour) }, - wantTemp: 6500, - }, - { - name: "after_sunset_transition", - timeFunc: func() time.Time { return times.Sunset.Add(2 * time.Hour) }, - wantTemp: 4000, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - temp := calculateTemperature(config, tt.timeFunc()) - - if math.Abs(float64(temp-tt.wantTemp)) > 500 { - t.Errorf("temperature = %d, want approximately %d", temp, tt.wantTemp) - } - }) - } -} - -func TestCalculateTemperatureManualTimes(t *testing.T) { - sunrise := time.Date(0, 1, 1, 6, 30, 0, 0, time.Local) - sunset := time.Date(0, 1, 1, 18, 30, 0, 0, time.Local) - - config := Config{ - LowTemp: 4000, - HighTemp: 6500, - ManualSunrise: &sunrise, - ManualSunset: &sunset, - Enabled: true, - } - - tests := []struct { - name string - time time.Time - want int - }{ - {"before_sunrise", time.Date(2024, 1, 1, 3, 0, 0, 0, time.Local), 4000}, - {"at_sunrise", time.Date(2024, 1, 1, 6, 30, 0, 0, time.Local), 6500}, - {"midday", time.Date(2024, 1, 1, 12, 0, 0, 0, time.Local), 6500}, - {"at_sunset", time.Date(2024, 1, 1, 18, 30, 0, 0, time.Local), 6500}, - {"after_sunset", time.Date(2024, 1, 1, 22, 0, 0, 0, time.Local), 4000}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - temp := calculateTemperature(config, tt.time) - if math.Abs(float64(temp-tt.want)) > 500 { - t.Errorf("temperature = %d, want approximately %d", temp, tt.want) - } - }) - } -} - -func TestCalculateTemperatureDisabled(t *testing.T) { - lat := 40.7128 - lon := -74.0060 - - config := Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: &lat, - Longitude: &lon, - Enabled: false, - } - - temp := calculateTemperature(config, time.Now()) - if temp != 6500 { - t.Errorf("disabled should return high temp, got %d", temp) - } -} - -func TestCalculateNextTransition(t *testing.T) { - lat := 40.7128 - lon := -74.0060 - date := time.Date(2024, 6, 21, 0, 0, 0, 0, time.Local) - - config := Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: &lat, - Longitude: &lon, - Enabled: true, - } - - times := CalculateSunTimes(lat, lon, date) - - tests := []struct { - name string - now time.Time - checkFunc func(*testing.T, time.Time) - }{ - { - name: "before_sunrise", - now: times.Sunrise.Add(-2 * time.Hour), - checkFunc: func(t *testing.T, next time.Time) { - if !next.Equal(times.Sunrise) && !next.After(times.Sunrise.Add(-1*time.Minute)) { - t.Error("next transition should be at or near sunrise") - } - }, - }, - { - name: "after_sunrise", - now: times.Sunrise.Add(2 * time.Hour), - checkFunc: func(t *testing.T, next time.Time) { - if !next.After(times.Sunrise) { - t.Error("next transition should be after sunrise") - } - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - next := calculateNextTransition(config, tt.now) - tt.checkFunc(t, next) - }) - } -} - -func TestSunTimesWithTwilight(t *testing.T) { - lat := 40.7128 - lon := -74.0060 - date := time.Date(2024, 6, 21, 12, 0, 0, 0, time.Local) - - times, cond := CalculateSunTimesWithTwilight(lat, lon, date, -6.0, 3.0) - - if cond != SunNormal { - t.Errorf("expected SunNormal, got %v", cond) - } - if !times.Dawn.Before(times.Sunrise) { - t.Error("dawn should be before sunrise") - } - if !times.Sunrise.Before(times.Sunset) { - t.Error("sunrise should be before sunset") - } - if !times.Sunset.Before(times.Night) { - t.Error("sunset should be before night") - } -} - -func TestSunConditions(t *testing.T) { - tests := []struct { - name string - lat float64 - date time.Time - expected SunCondition - }{ - { - name: "normal_conditions", - lat: 40.0, - date: time.Date(2024, 6, 21, 12, 0, 0, 0, time.UTC), - expected: SunNormal, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, cond := CalculateSunTimesWithTwilight(tt.lat, 0, tt.date, -6.0, 3.0) - if cond != tt.expected { - t.Errorf("expected condition %v, got %v", tt.expected, cond) - } - }) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types.go deleted file mode 100644 index 31bac15..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types.go +++ /dev/null @@ -1,216 +0,0 @@ -package wayland - -import ( - "math" - "sync" - "sync/atomic" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation" - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" - "github.com/godbus/dbus/v5" -) - -type GammaState int - -const ( - StateNormal GammaState = iota - StateTransition - StateStatic -) - -type Config struct { - Outputs []string - LowTemp int - HighTemp int - Latitude *float64 - Longitude *float64 - UseIPLocation bool - ManualSunrise *time.Time - ManualSunset *time.Time - ManualDuration *time.Duration - Gamma float64 - Enabled bool - ElevationTwilight float64 - ElevationDaylight float64 -} - -type State struct { - Config Config `json:"config"` - CurrentTemp int `json:"currentTemp"` - NextTransition time.Time `json:"nextTransition"` - SunriseTime time.Time `json:"sunriseTime"` - SunsetTime time.Time `json:"sunsetTime"` - DawnTime time.Time `json:"dawnTime"` - NightTime time.Time `json:"nightTime"` - IsDay bool `json:"isDay"` - SunPosition float64 `json:"sunPosition"` -} - -type cmd struct { - fn func() -} - -type sunSchedule struct { - times SunTimes - condition SunCondition - dawnStepTime time.Duration - nightStepTime time.Duration - calcDay time.Time -} - -type Manager struct { - config Config - configMutex sync.RWMutex - state *State - stateMutex sync.RWMutex - - display wlclient.WaylandDisplay - ctx *wlclient.Context - registry *wlclient.Registry - gammaControl any - availableOutputs []*wlclient.Output - availOutputsMu sync.RWMutex - outputRegNames syncmap.Map[uint32, uint32] - outputs syncmap.Map[uint32, *outputState] - controlsInitialized bool - connectionDead atomic.Bool - - cmdq chan cmd - alive bool - - stopChan chan struct{} - updateTrigger chan struct{} - wg sync.WaitGroup - - schedule sunSchedule - scheduleMutex sync.RWMutex - gammaState GammaState - - cachedIPLat *float64 - cachedIPLon *float64 - locationMutex sync.RWMutex - - subscribers syncmap.Map[string, chan State] - dirty chan struct{} - notifierWg sync.WaitGroup - lastNotified *State - - dbusConn *dbus.Conn - dbusSignal chan *dbus.Signal - - geoClient geolocation.Client - - lastAppliedTemp int - lastAppliedGamma float64 -} - -type outputState struct { - id uint32 - registryName uint32 - output *wlclient.Output - gammaControl any - rampSize uint32 - failed bool - isVirtual bool - retryCount int - lastFailTime time.Time -} - -func DefaultConfig() Config { - return Config{ - Outputs: []string{}, - LowTemp: 4000, - HighTemp: 6500, - Gamma: 1.0, - Enabled: false, - ElevationTwilight: -6.0, - ElevationDaylight: 3.0, - } -} - -func (c *Config) Validate() error { - if c.LowTemp < 1000 || c.LowTemp > 10000 { - return errdefs.ErrInvalidTemperature - } - if c.HighTemp < 1000 || c.HighTemp > 10000 { - return errdefs.ErrInvalidTemperature - } - if c.LowTemp > c.HighTemp { - return errdefs.ErrInvalidTemperature - } - if c.Gamma <= 0 || c.Gamma > 10 { - return errdefs.ErrInvalidGamma - } - if c.Latitude != nil && (math.Abs(*c.Latitude) > 90) { - return errdefs.ErrInvalidLocation - } - if c.Longitude != nil && (math.Abs(*c.Longitude) > 180) { - return errdefs.ErrInvalidLocation - } - if (c.Latitude != nil) != (c.Longitude != nil) { - return errdefs.ErrInvalidLocation - } - if (c.ManualSunrise != nil) != (c.ManualSunset != nil) { - return errdefs.ErrInvalidManualTimes - } - return nil -} - -func (m *Manager) GetState() State { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - if m.state == nil { - return State{} - } - return *m.state -} - -func (m *Manager) Subscribe(id string) chan State { - ch := make(chan State, 64) - m.subscribers.Store(id, ch) - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) notifySubscribers() { - select { - case m.dirty <- struct{}{}: - default: - } -} - -func stateChanged(old, new *State) bool { - if old == nil || new == nil { - return true - } - if old.CurrentTemp != new.CurrentTemp { - return true - } - if old.IsDay != new.IsDay { - return true - } - if !old.NextTransition.Equal(new.NextTransition) { - return true - } - if !old.SunriseTime.Equal(new.SunriseTime) { - return true - } - if !old.SunsetTime.Equal(new.SunsetTime) { - return true - } - if old.Config.Enabled != new.Config.Enabled { - return true - } - if old.SunPosition != new.SunPosition { - return true - } - return false -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types_test.go deleted file mode 100644 index f5bbb42..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types_test.go +++ /dev/null @@ -1,330 +0,0 @@ -package wayland - -import ( - "testing" - "time" -) - -func TestConfigValidate(t *testing.T) { - tests := []struct { - name string - config Config - wantErr bool - }{ - { - name: "valid_default", - config: DefaultConfig(), - wantErr: false, - }, - { - name: "valid_with_location", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: floatPtr(40.7128), - Longitude: floatPtr(-74.0060), - Gamma: 1.0, - Enabled: true, - }, - wantErr: false, - }, - { - name: "valid_manual_times", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - ManualSunrise: timePtr(time.Date(0, 1, 1, 6, 30, 0, 0, time.Local)), - ManualSunset: timePtr(time.Date(0, 1, 1, 18, 30, 0, 0, time.Local)), - Gamma: 1.0, - Enabled: true, - }, - wantErr: false, - }, - { - name: "invalid_low_temp_too_low", - config: Config{ - LowTemp: 500, - HighTemp: 6500, - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_low_temp_too_high", - config: Config{ - LowTemp: 15000, - HighTemp: 20000, - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_high_temp_too_low", - config: Config{ - LowTemp: 4000, - HighTemp: 500, - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "valid_temps_equal", - config: Config{ - LowTemp: 5000, - HighTemp: 5000, - Gamma: 1.0, - }, - wantErr: false, - }, - { - name: "invalid_temps_reversed", - config: Config{ - LowTemp: 6500, - HighTemp: 4000, - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_gamma_zero", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Gamma: 0, - }, - wantErr: true, - }, - { - name: "invalid_gamma_negative", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Gamma: -1.0, - }, - wantErr: true, - }, - { - name: "invalid_gamma_too_high", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Gamma: 15.0, - }, - wantErr: true, - }, - { - name: "invalid_latitude_too_high", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: floatPtr(100), - Longitude: floatPtr(0), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_latitude_too_low", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: floatPtr(-100), - Longitude: floatPtr(0), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_longitude_too_high", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: floatPtr(40), - Longitude: floatPtr(200), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_longitude_too_low", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: floatPtr(40), - Longitude: floatPtr(-200), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_latitude_without_longitude", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: floatPtr(40), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_longitude_without_latitude", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Longitude: floatPtr(-74), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_sunrise_without_sunset", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - ManualSunrise: timePtr(time.Date(0, 1, 1, 6, 30, 0, 0, time.Local)), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_sunset_without_sunrise", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - ManualSunset: timePtr(time.Date(0, 1, 1, 18, 30, 0, 0, time.Local)), - Gamma: 1.0, - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.config.Validate() - if (err != nil) != tt.wantErr { - t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestDefaultConfig(t *testing.T) { - config := DefaultConfig() - - if config.LowTemp != 4000 { - t.Errorf("default low temp = %d, want 4000", config.LowTemp) - } - if config.HighTemp != 6500 { - t.Errorf("default high temp = %d, want 6500", config.HighTemp) - } - if config.Gamma != 1.0 { - t.Errorf("default gamma = %f, want 1.0", config.Gamma) - } - if config.Enabled { - t.Error("default should be disabled") - } - if config.Latitude != nil { - t.Error("default should not have latitude") - } - if config.Longitude != nil { - t.Error("default should not have longitude") - } -} - -func TestStateChanged(t *testing.T) { - baseState := &State{ - CurrentTemp: 5000, - NextTransition: time.Now(), - SunriseTime: time.Now().Add(6 * time.Hour), - SunsetTime: time.Now().Add(18 * time.Hour), - IsDay: true, - Config: DefaultConfig(), - } - - tests := []struct { - name string - old *State - new *State - wantChanged bool - }{ - { - name: "nil_old", - old: nil, - new: baseState, - wantChanged: true, - }, - { - name: "nil_new", - old: baseState, - new: nil, - wantChanged: true, - }, - { - name: "same_state", - old: baseState, - new: baseState, - wantChanged: false, - }, - { - name: "temp_changed", - old: baseState, - new: &State{ - CurrentTemp: 6000, - NextTransition: baseState.NextTransition, - SunriseTime: baseState.SunriseTime, - SunsetTime: baseState.SunsetTime, - IsDay: baseState.IsDay, - Config: baseState.Config, - }, - wantChanged: true, - }, - { - name: "is_day_changed", - old: baseState, - new: &State{ - CurrentTemp: baseState.CurrentTemp, - NextTransition: baseState.NextTransition, - SunriseTime: baseState.SunriseTime, - SunsetTime: baseState.SunsetTime, - IsDay: false, - Config: baseState.Config, - }, - wantChanged: true, - }, - { - name: "enabled_changed", - old: baseState, - new: &State{ - CurrentTemp: baseState.CurrentTemp, - NextTransition: baseState.NextTransition, - SunriseTime: baseState.SunriseTime, - SunsetTime: baseState.SunsetTime, - IsDay: baseState.IsDay, - Config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Gamma: 1.0, - Enabled: true, - }, - }, - wantChanged: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - changed := stateChanged(tt.old, tt.new) - if changed != tt.wantChanged { - t.Errorf("stateChanged() = %v, want %v", changed, tt.wantChanged) - } - }) - } -} - -func floatPtr(f float64) *float64 { - return &f -} - -func timePtr(t time.Time) *time.Time { - return &t -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlcontext/context.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlcontext/context.go deleted file mode 100644 index 9bbb30a..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlcontext/context.go +++ /dev/null @@ -1,200 +0,0 @@ -package wlcontext - -import ( - "fmt" - "golang.org/x/sys/unix" - "os" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" -) - -type WaylandContext interface { - Display() *wlclient.Display - Post(fn func()) - FatalError() <-chan error - Start() - Close() -} - -var _ WaylandContext = (*SharedContext)(nil) - -type SharedContext struct { - display *wlclient.Display - stopChan chan struct{} - fatalError chan error - cmdQueue chan func() - wakeR int - wakeW int - wg sync.WaitGroup - mu sync.Mutex - started bool -} - -func New() (*SharedContext, error) { - display, err := wlclient.Connect("") - if err != nil { - return nil, fmt.Errorf("%w: %v", errdefs.ErrNoWaylandDisplay, err) - } - - fds := make([]int, 2) - if err := unix.Pipe(fds); err != nil { - display.Context().Close() - return nil, fmt.Errorf("failed to create wake pipe: %w", err) - } - if err := unix.SetNonblock(fds[0], true); err != nil { - unix.Close(fds[0]) - unix.Close(fds[1]) - display.Context().Close() - return nil, fmt.Errorf("failed to set wake pipe nonblock: %w", err) - } - if err := unix.SetNonblock(fds[1], true); err != nil { - unix.Close(fds[0]) - unix.Close(fds[1]) - display.Context().Close() - return nil, fmt.Errorf("failed to set wake pipe nonblock: %w", err) - } - - sc := &SharedContext{ - display: display, - stopChan: make(chan struct{}), - fatalError: make(chan error, 1), - cmdQueue: make(chan func(), 256), - wakeR: fds[0], - wakeW: fds[1], - started: false, - } - - return sc, nil -} - -func (sc *SharedContext) Start() { - sc.mu.Lock() - defer sc.mu.Unlock() - - if sc.started { - return - } - - sc.started = true - sc.wg.Add(1) - go sc.eventDispatcher() -} - -func (sc *SharedContext) Display() *wlclient.Display { - return sc.display -} - -func (sc *SharedContext) Post(fn func()) { - select { - case sc.cmdQueue <- fn: - if _, err := unix.Write(sc.wakeW, []byte{1}); err != nil && err != unix.EAGAIN { - log.Errorf("wake pipe write error: %v", err) - } - default: - } -} - -func (sc *SharedContext) FatalError() <-chan error { - return sc.fatalError -} - -func (sc *SharedContext) eventDispatcher() { - defer sc.wg.Done() - defer func() { - if r := recover(); r != nil { - err := fmt.Errorf("FATAL: Wayland event dispatcher panic: %v", r) - log.Error(err) - select { - case sc.fatalError <- err: - default: - } - } - }() - - ctx := sc.display.Context() - wlFd := ctx.Fd() - - pollFds := []unix.PollFd{ - {Fd: int32(wlFd), Events: unix.POLLIN}, - {Fd: int32(sc.wakeR), Events: unix.POLLIN}, - } - - consecutiveErrors := 0 - const maxConsecutiveErrors = 20 - - for { - sc.drainCmdQueue() - - select { - case <-sc.stopChan: - return - default: - } - - _, err := unix.Poll(pollFds, -1) - switch { - case err == unix.EINTR: - continue - case err != nil: - log.Errorf("Poll error: %v", err) - return - } - - if pollFds[1].Revents&unix.POLLIN != 0 { - var buf [64]byte - if _, err := unix.Read(sc.wakeR, buf[:]); err != nil && err != unix.EAGAIN { - log.Errorf("wake pipe read error: %v", err) - } - } - - if pollFds[0].Revents&unix.POLLIN == 0 { - continue - } - - if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) { - consecutiveErrors++ - log.Warnf("Wayland connection error (%d/%d): %v", consecutiveErrors, maxConsecutiveErrors, err) - - if consecutiveErrors >= maxConsecutiveErrors { - log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors) - return - } - - time.Sleep(100 * time.Millisecond * time.Duration(consecutiveErrors)) - continue - } - - consecutiveErrors = 0 - } -} - -func (sc *SharedContext) drainCmdQueue() { - for { - select { - case fn := <-sc.cmdQueue: - fn() - default: - return - } - } -} - -func (sc *SharedContext) Close() { - close(sc.stopChan) - if _, err := unix.Write(sc.wakeW, []byte{1}); err != nil && err != unix.EAGAIN { - log.Errorf("wake pipe write error on close: %v", err) - } - sc.wg.Wait() - - unix.Close(sc.wakeR) - unix.Close(sc.wakeW) - - if sc.display == nil { - return - } - sc.display.Context().Close() -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlcontext/context_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlcontext/context_test.go deleted file mode 100644 index 11ecfbf..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlcontext/context_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package wlcontext - -import ( - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "golang.org/x/sys/unix" -) - -func newTestSharedContext(t *testing.T, queueSize int) *SharedContext { - t.Helper() - fds := make([]int, 2) - if err := unix.Pipe(fds); err != nil { - t.Fatalf("failed to create test pipe: %v", err) - } - t.Cleanup(func() { - unix.Close(fds[0]) - unix.Close(fds[1]) - }) - return &SharedContext{ - cmdQueue: make(chan func(), queueSize), - stopChan: make(chan struct{}), - wakeR: fds[0], - wakeW: fds[1], - } -} - -func TestSharedContext_ConcurrentPostNonBlocking(t *testing.T) { - sc := newTestSharedContext(t, 256) - - var wg sync.WaitGroup - const goroutines = 100 - const iterations = 50 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - sc.Post(func() { - _ = id + j - }) - } - }(i) - } - - wg.Wait() -} - -func TestSharedContext_PostQueueFull(t *testing.T) { - sc := newTestSharedContext(t, 2) - - sc.Post(func() {}) - sc.Post(func() {}) - sc.Post(func() {}) - sc.Post(func() {}) - - assert.Len(t, sc.cmdQueue, 2) -} - -func TestSharedContext_StartMultipleTimes(t *testing.T) { - sc := newTestSharedContext(t, 256) - sc.started = true - - var wg sync.WaitGroup - const goroutines = 10 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func() { - defer wg.Done() - sc.Start() - }() - } - - wg.Wait() - - assert.True(t, sc.started) -} - -func TestSharedContext_DrainCmdQueue(t *testing.T) { - sc := newTestSharedContext(t, 256) - - counter := 0 - for i := 0; i < 10; i++ { - sc.cmdQueue <- func() { - counter++ - } - } - - sc.drainCmdQueue() - - assert.Equal(t, 10, counter) - assert.Len(t, sc.cmdQueue, 0) -} - -func TestSharedContext_DrainCmdQueueEmpty(t *testing.T) { - sc := newTestSharedContext(t, 256) - - sc.drainCmdQueue() - - assert.Len(t, sc.cmdQueue, 0) -} - -func TestSharedContext_ConcurrentDrainAndPost(t *testing.T) { - sc := newTestSharedContext(t, 256) - - var wg sync.WaitGroup - - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < 100; i++ { - sc.Post(func() {}) - } - }() - - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < 50; i++ { - sc.drainCmdQueue() - } - }() - - wg.Wait() -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/handlers.go deleted file mode 100644 index c885e13..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/handlers.go +++ /dev/null @@ -1,282 +0,0 @@ -package wlroutput - -import ( - "encoding/json" - "fmt" - "net" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -type HeadConfig struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - ModeID *uint32 `json:"modeId,omitempty"` - CustomMode *struct { - Width int32 `json:"width"` - Height int32 `json:"height"` - Refresh int32 `json:"refresh"` - } `json:"customMode,omitempty"` - Position *struct{ X, Y int32 } `json:"position,omitempty"` - Transform *int32 `json:"transform,omitempty"` - Scale *float64 `json:"scale,omitempty"` - AdaptiveSync *uint32 `json:"adaptiveSync,omitempty"` -} - -type ConfigurationRequest struct { - Heads []HeadConfig `json:"heads"` - Test bool `json:"test"` -} - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - if manager == nil { - models.RespondError(conn, req.ID, "wlroutput manager not initialized") - return - } - - switch req.Method { - case "wlroutput.getState": - handleGetState(conn, req, manager) - case "wlroutput.applyConfiguration": - handleApplyConfiguration(conn, req, manager, false) - case "wlroutput.testConfiguration": - handleApplyConfiguration(conn, req, manager, true) - case "wlroutput.subscribe": - handleSubscribe(conn, req, manager) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleGetState(conn net.Conn, req models.Request, manager *Manager) { - models.Respond(conn, req.ID, manager.GetState()) -} - -func handleApplyConfiguration(conn net.Conn, req models.Request, manager *Manager, test bool) { - headsParam, ok := models.Get[any](req, "heads") - if !ok { - models.RespondError(conn, req.ID, "missing 'heads' parameter") - return - } - - headsJSON, err := json.Marshal(headsParam) - if err != nil { - models.RespondError(conn, req.ID, "invalid 'heads' parameter format") - return - } - - var heads []HeadConfig - if err := json.Unmarshal(headsJSON, &heads); err != nil { - models.RespondError(conn, req.ID, fmt.Sprintf("invalid heads configuration: %v", err)) - return - } - - if err := manager.ApplyConfiguration(heads, test); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - msg := "configuration applied" - if test { - msg = "configuration test succeeded" - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: msg}) -} - -func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) { - clientID := fmt.Sprintf("client-%p", conn) - stateChan := manager.Subscribe(clientID) - defer manager.Unsubscribe(clientID) - - initialState := manager.GetState() - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - ID: req.ID, - Result: &initialState, - }); err != nil { - return - } - - for state := range stateChan { - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - Result: &state, - }); err != nil { - return - } - } -} - -func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error { - if m.manager == nil { - return fmt.Errorf("output manager not initialized") - } - - resultChan := make(chan error, 1) - - m.post(func() { - m.wlMutex.Lock() - defer m.wlMutex.Unlock() - - config, err := m.manager.CreateConfiguration(m.serial) - if err != nil { - resultChan <- fmt.Errorf("failed to create configuration: %w", err) - return - } - - responded := false - - config.SetSucceededHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1SucceededEvent) { - if responded { - return - } - responded = true - log.Info("WlrOutput: configuration succeeded") - config.Destroy() - resultChan <- nil - }) - - config.SetFailedHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1FailedEvent) { - if responded { - return - } - responded = true - log.Warn("WlrOutput: configuration failed") - config.Destroy() - resultChan <- fmt.Errorf("compositor rejected configuration") - }) - - config.SetCancelledHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1CancelledEvent) { - if responded { - return - } - responded = true - log.Warn("WlrOutput: configuration cancelled") - config.Destroy() - resultChan <- fmt.Errorf("configuration cancelled (outdated serial)") - }) - - time.AfterFunc(time.Second, func() { - m.post(func() { - if responded { - return - } - responded = true - config.Destroy() - resultChan <- fmt.Errorf("timeout waiting for configuration response") - }) - }) - - headsByName := make(map[string]*headState) - m.heads.Range(func(key uint32, head *headState) bool { - if !head.finished { - headsByName[head.name] = head - } - return true - }) - - for _, headCfg := range heads { - head, exists := headsByName[headCfg.Name] - if !exists { - config.Destroy() - resultChan <- fmt.Errorf("head not found: %s", headCfg.Name) - return - } - - if !headCfg.Enabled { - if err := config.DisableHead(head.handle); err != nil { - config.Destroy() - resultChan <- fmt.Errorf("failed to disable head %s: %w", headCfg.Name, err) - return - } - continue - } - - headConfig, err := config.EnableHead(head.handle) - if err != nil { - config.Destroy() - resultChan <- fmt.Errorf("failed to enable head %s: %w", headCfg.Name, err) - return - } - - if headCfg.ModeID != nil { - mode, exists := m.modes.Load(*headCfg.ModeID) - - if !exists { - config.Destroy() - resultChan <- fmt.Errorf("mode not found: %d", *headCfg.ModeID) - return - } - - if err := headConfig.SetMode(mode.handle); err != nil { - config.Destroy() - resultChan <- fmt.Errorf("failed to set mode for %s: %w", headCfg.Name, err) - return - } - } else if headCfg.CustomMode != nil { - if err := headConfig.SetCustomMode( - headCfg.CustomMode.Width, - headCfg.CustomMode.Height, - headCfg.CustomMode.Refresh, - ); err != nil { - config.Destroy() - resultChan <- fmt.Errorf("failed to set custom mode for %s: %w", headCfg.Name, err) - return - } - } - - if headCfg.Position != nil { - if err := headConfig.SetPosition(headCfg.Position.X, headCfg.Position.Y); err != nil { - config.Destroy() - resultChan <- fmt.Errorf("failed to set position for %s: %w", headCfg.Name, err) - return - } - } - - if headCfg.Transform != nil { - if err := headConfig.SetTransform(*headCfg.Transform); err != nil { - config.Destroy() - resultChan <- fmt.Errorf("failed to set transform for %s: %w", headCfg.Name, err) - return - } - } - - if headCfg.Scale != nil { - if err := headConfig.SetScale(*headCfg.Scale); err != nil { - config.Destroy() - resultChan <- fmt.Errorf("failed to set scale for %s: %w", headCfg.Name, err) - return - } - } - - if headCfg.AdaptiveSync != nil { - if err := headConfig.SetAdaptiveSync(*headCfg.AdaptiveSync); err != nil { - config.Destroy() - resultChan <- fmt.Errorf("failed to set adaptive sync for %s: %w", headCfg.Name, err) - return - } - } - } - - var applyErr error - if test { - applyErr = config.Test() - } else { - applyErr = config.Apply() - } - - if applyErr != nil { - responded = true - config.Destroy() - action := "apply" - if test { - action = "test" - } - resultChan <- fmt.Errorf("failed to %s configuration: %w", action, applyErr) - return - } - }) - - return <-resultChan -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager.go deleted file mode 100644 index cbd89a3..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager.go +++ /dev/null @@ -1,492 +0,0 @@ -package wlroutput - -import ( - "fmt" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" -) - -func NewManager(display wlclient.WaylandDisplay) (*Manager, error) { - m := &Manager{ - display: display, - ctx: display.Context(), - cmdq: make(chan cmd, 512), - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - fatalError: make(chan error, 1), - } - - m.wg.Add(1) - go m.waylandActor() - - if err := m.setupRegistry(); err != nil { - close(m.stopChan) - m.wg.Wait() - return nil, err - } - - m.updateState() - - m.notifierWg.Add(1) - go m.notifier() - - return m, nil -} - -func (m *Manager) post(fn func()) { - select { - case m.cmdq <- cmd{fn: fn}: - default: - log.Warn("WlrOutput actor command queue full, dropping command") - } -} - -func (m *Manager) waylandActor() { - defer m.wg.Done() - defer func() { - if r := recover(); r != nil { - err := fmt.Errorf("waylandActor panic: %v", r) - log.Errorf("WlrOutput: %v", err) - - select { - case m.fatalError <- err: - default: - } - - select { - case <-m.stopChan: - default: - close(m.stopChan) - } - } - }() - - for { - select { - case <-m.stopChan: - return - case c := <-m.cmdq: - func() { - defer func() { - if r := recover(); r != nil { - log.Errorf("WlrOutput: command execution panic: %v", r) - } - }() - c.fn() - }() - } - } -} - -func (m *Manager) setupRegistry() error { - log.Info("WlrOutput: starting registry setup") - - registry, err := m.display.GetRegistry() - if err != nil { - return fmt.Errorf("failed to get registry: %w", err) - } - m.registry = registry - - registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { - if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName { - log.Infof("WlrOutput: found %s", wlr_output_management.ZwlrOutputManagerV1InterfaceName) - manager := wlr_output_management.NewZwlrOutputManagerV1(m.ctx) - version := e.Version - if version > 4 { - version = 4 - } - - manager.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) { - m.handleHead(e) - }) - - manager.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) { - log.Debugf("WlrOutput: done event received (serial=%d)", e.Serial) - m.serial = e.Serial - m.post(func() { - m.updateState() - }) - }) - - manager.SetFinishedHandler(func(e wlr_output_management.ZwlrOutputManagerV1FinishedEvent) { - log.Info("WlrOutput: finished event received") - }) - - if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil { - m.manager = manager - log.Info("WlrOutput: manager bound successfully") - } else { - log.Errorf("WlrOutput: failed to bind manager: %v", err) - } - } - }) - - log.Info("WlrOutput: registry setup complete (events will be processed async)") - return nil -} - -func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) { - handle := e.Head - headID := handle.ID() - - log.Debugf("WlrOutput: New head (id=%d)", headID) - - head := &headState{ - id: headID, - handle: handle, - modeIDs: make([]uint32, 0), - } - - m.heads.Store(headID, head) - - handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) { - log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name) - head.name = e.Name - head.ready = true - m.post(func() { - m.updateState() - }) - }) - - handle.SetDescriptionHandler(func(e wlr_output_management.ZwlrOutputHeadV1DescriptionEvent) { - log.Debugf("WlrOutput: Head %d description: %s", headID, e.Description) - head.description = e.Description - m.post(func() { - m.updateState() - }) - }) - - handle.SetPhysicalSizeHandler(func(e wlr_output_management.ZwlrOutputHeadV1PhysicalSizeEvent) { - log.Debugf("WlrOutput: Head %d physical size: %dx%d", headID, e.Width, e.Height) - head.physicalWidth = e.Width - head.physicalHeight = e.Height - m.post(func() { - m.updateState() - }) - }) - - handle.SetModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModeEvent) { - m.handleMode(headID, e) - }) - - handle.SetEnabledHandler(func(e wlr_output_management.ZwlrOutputHeadV1EnabledEvent) { - log.Debugf("WlrOutput: Head %d enabled: %d", headID, e.Enabled) - head.enabled = e.Enabled != 0 - m.post(func() { - m.updateState() - }) - }) - - handle.SetCurrentModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1CurrentModeEvent) { - modeID := e.Mode.ID() - log.Debugf("WlrOutput: Head %d current mode: %d", headID, modeID) - head.currentModeID = modeID - m.post(func() { - m.updateState() - }) - }) - - handle.SetPositionHandler(func(e wlr_output_management.ZwlrOutputHeadV1PositionEvent) { - log.Debugf("WlrOutput: Head %d position: %d,%d", headID, e.X, e.Y) - head.x = e.X - head.y = e.Y - m.post(func() { - m.updateState() - }) - }) - - handle.SetTransformHandler(func(e wlr_output_management.ZwlrOutputHeadV1TransformEvent) { - log.Debugf("WlrOutput: Head %d transform: %d", headID, e.Transform) - head.transform = e.Transform - m.post(func() { - m.updateState() - }) - }) - - handle.SetScaleHandler(func(e wlr_output_management.ZwlrOutputHeadV1ScaleEvent) { - log.Debugf("WlrOutput: Head %d scale: %f", headID, e.Scale) - head.scale = e.Scale - m.post(func() { - m.updateState() - }) - }) - - handle.SetMakeHandler(func(e wlr_output_management.ZwlrOutputHeadV1MakeEvent) { - log.Debugf("WlrOutput: Head %d make: %s", headID, e.Make) - head.make = e.Make - m.post(func() { - m.updateState() - }) - }) - - handle.SetModelHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModelEvent) { - log.Debugf("WlrOutput: Head %d model: %s", headID, e.Model) - head.model = e.Model - m.post(func() { - m.updateState() - }) - }) - - handle.SetSerialNumberHandler(func(e wlr_output_management.ZwlrOutputHeadV1SerialNumberEvent) { - log.Debugf("WlrOutput: Head %d serial: %s", headID, e.SerialNumber) - head.serialNumber = e.SerialNumber - m.post(func() { - m.updateState() - }) - }) - - handle.SetAdaptiveSyncHandler(func(e wlr_output_management.ZwlrOutputHeadV1AdaptiveSyncEvent) { - log.Debugf("WlrOutput: Head %d adaptive sync: %d", headID, e.State) - head.adaptiveSync = e.State - head.adaptiveSyncSupported = true - m.post(func() { - m.updateState() - }) - }) - - handle.SetFinishedHandler(func(e wlr_output_management.ZwlrOutputHeadV1FinishedEvent) { - log.Debugf("WlrOutput: Head %d finished", headID) - head.finished = true - - m.heads.Delete(headID) - - m.wlMutex.Lock() - handle.Release() - m.wlMutex.Unlock() - - m.post(func() { - m.updateState() - }) - }) -} - -func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHeadV1ModeEvent) { - handle := e.Mode - modeID := handle.ID() - - log.Debugf("WlrOutput: Head %d new mode (id=%d)", headID, modeID) - - mode := &modeState{ - id: modeID, - handle: handle, - } - - m.modes.Store(modeID, mode) - - if head, ok := m.heads.Load(headID); ok { - head.modeIDs = append(head.modeIDs, modeID) - m.heads.Store(headID, head) - } - - handle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) { - log.Debugf("WlrOutput: Mode %d size: %dx%d", modeID, e.Width, e.Height) - mode.width = e.Width - mode.height = e.Height - m.post(func() { - m.updateState() - }) - }) - - handle.SetRefreshHandler(func(e wlr_output_management.ZwlrOutputModeV1RefreshEvent) { - log.Debugf("WlrOutput: Mode %d refresh: %d", modeID, e.Refresh) - mode.refresh = e.Refresh - m.post(func() { - m.updateState() - }) - }) - - handle.SetPreferredHandler(func(e wlr_output_management.ZwlrOutputModeV1PreferredEvent) { - log.Debugf("WlrOutput: Mode %d preferred", modeID) - mode.preferred = true - m.post(func() { - m.updateState() - }) - }) - - handle.SetFinishedHandler(func(e wlr_output_management.ZwlrOutputModeV1FinishedEvent) { - log.Debugf("WlrOutput: Mode %d finished", modeID) - mode.finished = true - - m.modes.Delete(modeID) - - m.wlMutex.Lock() - handle.Release() - m.wlMutex.Unlock() - - m.post(func() { - m.updateState() - }) - }) -} - -func (m *Manager) updateState() { - outputs := make([]Output, 0) - - m.heads.Range(func(key uint32, head *headState) bool { - if head.finished { - return true - } - - if !head.ready { - return true - } - - modes := make([]OutputMode, 0) - var currentMode *OutputMode - - for _, modeID := range head.modeIDs { - mode, exists := m.modes.Load(modeID) - if !exists { - continue - } - if mode.finished { - continue - } - - outMode := OutputMode{ - Width: mode.width, - Height: mode.height, - Refresh: mode.refresh, - Preferred: mode.preferred, - ID: modeID, - } - modes = append(modes, outMode) - - if modeID == head.currentModeID { - currentMode = &outMode - } - } - - output := Output{ - Name: head.name, - Description: head.description, - Make: head.make, - Model: head.model, - SerialNumber: head.serialNumber, - PhysicalWidth: head.physicalWidth, - PhysicalHeight: head.physicalHeight, - Enabled: head.enabled, - X: head.x, - Y: head.y, - Transform: head.transform, - Scale: head.scale, - CurrentMode: currentMode, - Modes: modes, - AdaptiveSync: head.adaptiveSync, - AdaptiveSyncSupported: head.adaptiveSyncSupported, - ID: head.id, - } - outputs = append(outputs, output) - return true - }) - - newState := State{ - Outputs: outputs, - Serial: m.serial, - } - - m.stateMutex.Lock() - m.state = &newState - m.stateMutex.Unlock() - - m.notifySubscribers() -} - -func (m *Manager) notifier() { - defer m.notifierWg.Done() - defer func() { - if r := recover(); r != nil { - err := fmt.Errorf("notifier panic: %v", r) - log.Errorf("WlrOutput: %v", err) - - select { - case m.fatalError <- err: - default: - } - - select { - case <-m.stopChan: - default: - close(m.stopChan) - } - } - }() - - const minGap = 100 * time.Millisecond - timer := time.NewTimer(minGap) - timer.Stop() - var pending bool - - for { - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.dirty: - if pending { - continue - } - pending = true - timer.Reset(minGap) - case <-timer.C: - if !pending { - continue - } - - currentState := m.GetState() - - if m.lastNotified != nil && !stateChanged(m.lastNotified, ¤tState) { - pending = false - continue - } - - m.subscribers.Range(func(key string, ch chan State) bool { - select { - case ch <- currentState: - default: - log.Warn("WlrOutput: subscriber channel full, dropping update") - } - return true - }) - - stateCopy := currentState - m.lastNotified = &stateCopy - pending = false - } - } -} - -func (m *Manager) Close() { - close(m.stopChan) - m.wg.Wait() - m.notifierWg.Wait() - - m.subscribers.Range(func(key string, ch chan State) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - - m.modes.Range(func(key uint32, mode *modeState) bool { - if mode.handle != nil { - mode.handle.Release() - } - m.modes.Delete(key) - return true - }) - - m.heads.Range(func(key uint32, head *headState) bool { - if head.handle != nil { - head.handle.Release() - } - m.heads.Delete(key) - return true - }) - - if m.manager != nil { - m.manager.Stop() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager_test.go deleted file mode 100644 index e90c70c..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager_test.go +++ /dev/null @@ -1,414 +0,0 @@ -package wlroutput - -import ( - "errors" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" -) - -func TestStateChanged_BothNil(t *testing.T) { - assert.True(t, stateChanged(nil, nil)) -} - -func TestStateChanged_OneNil(t *testing.T) { - s := &State{Serial: 1} - assert.True(t, stateChanged(s, nil)) - assert.True(t, stateChanged(nil, s)) -} - -func TestStateChanged_SerialDiffers(t *testing.T) { - a := &State{Serial: 1, Outputs: []Output{}} - b := &State{Serial: 2, Outputs: []Output{}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_OutputCountDiffers(t *testing.T) { - a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1"}}} - b := &State{Serial: 1, Outputs: []Output{}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_OutputNameDiffers(t *testing.T) { - a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Enabled: true}}} - b := &State{Serial: 1, Outputs: []Output{{Name: "HDMI-A-1", Enabled: true}}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_OutputEnabledDiffers(t *testing.T) { - a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Enabled: true}}} - b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Enabled: false}}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_OutputPositionDiffers(t *testing.T) { - a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", X: 0, Y: 0}}} - b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", X: 1920, Y: 0}}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_OutputTransformDiffers(t *testing.T) { - a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Transform: 0}}} - b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Transform: 1}}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_OutputScaleDiffers(t *testing.T) { - a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Scale: 1.0}}} - b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Scale: 2.0}}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_OutputAdaptiveSyncDiffers(t *testing.T) { - a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", AdaptiveSync: 0}}} - b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", AdaptiveSync: 1}}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_CurrentModeNilVsNonNil(t *testing.T) { - a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: nil}}} - b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: &OutputMode{Width: 1920}}}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_CurrentModeDiffers(t *testing.T) { - a := &State{Serial: 1, Outputs: []Output{{ - Name: "eDP-1", - CurrentMode: &OutputMode{Width: 1920, Height: 1080, Refresh: 60000}, - }}} - b := &State{Serial: 1, Outputs: []Output{{ - Name: "eDP-1", - CurrentMode: &OutputMode{Width: 2560, Height: 1440, Refresh: 60000}, - }}} - assert.True(t, stateChanged(a, b)) - - b.Outputs[0].CurrentMode.Width = 1920 - b.Outputs[0].CurrentMode.Height = 1080 - b.Outputs[0].CurrentMode.Refresh = 144000 - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_ModesLengthDiffers(t *testing.T) { - a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Modes: []OutputMode{{Width: 1920}}}}} - b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Modes: []OutputMode{{Width: 1920}, {Width: 1280}}}}} - assert.True(t, stateChanged(a, b)) -} - -func TestStateChanged_Equal(t *testing.T) { - mode := OutputMode{Width: 1920, Height: 1080, Refresh: 60000, Preferred: true} - a := &State{ - Serial: 5, - Outputs: []Output{{ - Name: "eDP-1", - Description: "Built-in display", - Make: "BOE", - Model: "0x0ABC", - SerialNumber: "12345", - PhysicalWidth: 309, - PhysicalHeight: 174, - Enabled: true, - X: 0, - Y: 0, - Transform: 0, - Scale: 1.0, - CurrentMode: &mode, - Modes: []OutputMode{mode}, - AdaptiveSync: 0, - }}, - } - b := &State{ - Serial: 5, - Outputs: []Output{{ - Name: "eDP-1", - Description: "Built-in display", - Make: "BOE", - Model: "0x0ABC", - SerialNumber: "12345", - PhysicalWidth: 309, - PhysicalHeight: 174, - Enabled: true, - X: 0, - Y: 0, - Transform: 0, - Scale: 1.0, - CurrentMode: &mode, - Modes: []OutputMode{mode}, - AdaptiveSync: 0, - }}, - } - assert.False(t, stateChanged(a, b)) -} - -func TestManager_ConcurrentGetState(t *testing.T) { - m := &Manager{ - state: &State{ - Serial: 1, - Outputs: []Output{{Name: "eDP-1", Enabled: true}}, - }, - } - - var wg sync.WaitGroup - const goroutines = 50 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - s := m.GetState() - _ = s.Serial - _ = s.Outputs - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.stateMutex.Lock() - m.state = &State{ - Serial: uint32(j), - Outputs: []Output{{Name: "eDP-1", Scale: float64(j % 3)}}, - } - m.stateMutex.Unlock() - } - }(i) - } - - wg.Wait() -} - -func TestManager_ConcurrentSubscriberAccess(t *testing.T) { - m := &Manager{ - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - - var wg sync.WaitGroup - const goroutines = 20 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - subID := string(rune('a' + id)) - ch := m.Subscribe(subID) - assert.NotNil(t, ch) - time.Sleep(time.Millisecond) - m.Unsubscribe(subID) - }(i) - } - - wg.Wait() -} - -func TestManager_SyncmapHeadsConcurrentAccess(t *testing.T) { - m := &Manager{} - - var wg sync.WaitGroup - const goroutines = 30 - const iterations = 50 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - key := uint32(id) - - for j := 0; j < iterations; j++ { - state := &headState{ - id: key, - name: "test-head", - enabled: j%2 == 0, - scale: float64(j % 3), - modeIDs: []uint32{uint32(j)}, - } - m.heads.Store(key, state) - - if loaded, ok := m.heads.Load(key); ok { - assert.Equal(t, key, loaded.id) - } - - m.heads.Range(func(k uint32, v *headState) bool { - _ = v.name - _ = v.enabled - return true - }) - } - - m.heads.Delete(key) - }(i) - } - - wg.Wait() -} - -func TestManager_SyncmapModesConcurrentAccess(t *testing.T) { - m := &Manager{} - - var wg sync.WaitGroup - const goroutines = 30 - const iterations = 50 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - key := uint32(id) - - for j := 0; j < iterations; j++ { - state := &modeState{ - id: key, - width: int32(1920 + j), - height: int32(1080 + j), - refresh: 60000, - preferred: j == 0, - } - m.modes.Store(key, state) - - if loaded, ok := m.modes.Load(key); ok { - assert.Equal(t, key, loaded.id) - } - - m.modes.Range(func(k uint32, v *modeState) bool { - _ = v.width - _ = v.height - return true - }) - } - - m.modes.Delete(key) - }(i) - } - - wg.Wait() -} - -func TestManager_NotifySubscribersNonBlocking(t *testing.T) { - m := &Manager{ - dirty: make(chan struct{}, 1), - } - - for i := 0; i < 10; i++ { - m.notifySubscribers() - } - - assert.Len(t, m.dirty, 1) -} - -func TestManager_PostQueueFull(t *testing.T) { - m := &Manager{ - cmdq: make(chan cmd, 2), - stopChan: make(chan struct{}), - } - - m.post(func() {}) - m.post(func() {}) - m.post(func() {}) - m.post(func() {}) - - assert.Len(t, m.cmdq, 2) -} - -func TestManager_GetStateNilState(t *testing.T) { - m := &Manager{} - - s := m.GetState() - assert.NotNil(t, s.Outputs) - assert.Equal(t, uint32(0), s.Serial) -} - -func TestManager_FatalErrorChannel(t *testing.T) { - m := &Manager{ - fatalError: make(chan error, 1), - } - - ch := m.FatalError() - assert.NotNil(t, ch) - - m.fatalError <- assert.AnError - err := <-ch - assert.Error(t, err) -} - -func TestOutputMode_Fields(t *testing.T) { - mode := OutputMode{ - Width: 1920, - Height: 1080, - Refresh: 60000, - Preferred: true, - ID: 42, - } - - assert.Equal(t, int32(1920), mode.Width) - assert.Equal(t, int32(1080), mode.Height) - assert.Equal(t, int32(60000), mode.Refresh) - assert.True(t, mode.Preferred) - assert.Equal(t, uint32(42), mode.ID) -} - -func TestOutput_Fields(t *testing.T) { - out := Output{ - Name: "eDP-1", - Description: "Built-in display", - Make: "BOE", - Model: "0x0ABC", - SerialNumber: "12345", - PhysicalWidth: 309, - PhysicalHeight: 174, - Enabled: true, - X: 0, - Y: 0, - Transform: 0, - Scale: 1.5, - AdaptiveSync: 1, - ID: 1, - } - - assert.Equal(t, "eDP-1", out.Name) - assert.Equal(t, "Built-in display", out.Description) - assert.True(t, out.Enabled) - assert.Equal(t, float64(1.5), out.Scale) - assert.Equal(t, uint32(1), out.AdaptiveSync) -} - -func TestHeadState_ModeIDsSlice(t *testing.T) { - head := &headState{ - id: 1, - modeIDs: make([]uint32, 0), - } - - head.modeIDs = append(head.modeIDs, 1, 2, 3) - assert.Len(t, head.modeIDs, 3) - assert.Equal(t, uint32(1), head.modeIDs[0]) -} - -func TestStateChanged_BothCurrentModeNil(t *testing.T) { - a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: nil}}} - b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: nil}}} - assert.False(t, stateChanged(a, b)) -} - -func TestStateChanged_IndexOutOfBounds(t *testing.T) { - a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1"}}} - b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1"}, {Name: "HDMI-A-1"}}} - assert.True(t, stateChanged(a, b)) -} - -func TestNewManager_GetRegistryError(t *testing.T) { - mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) - - mockDisplay.EXPECT().Context().Return(nil) - mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry")) - - _, err := NewManager(mockDisplay) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to get registry") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/types.go deleted file mode 100644 index 9c79b1a..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/types.go +++ /dev/null @@ -1,192 +0,0 @@ -package wlroutput - -import ( - "sync" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" -) - -type OutputMode struct { - Width int32 `json:"width"` - Height int32 `json:"height"` - Refresh int32 `json:"refresh"` - Preferred bool `json:"preferred"` - ID uint32 `json:"id"` -} - -type Output struct { - Name string `json:"name"` - Description string `json:"description"` - Make string `json:"make"` - Model string `json:"model"` - SerialNumber string `json:"serialNumber"` - PhysicalWidth int32 `json:"physicalWidth"` - PhysicalHeight int32 `json:"physicalHeight"` - Enabled bool `json:"enabled"` - X int32 `json:"x"` - Y int32 `json:"y"` - Transform int32 `json:"transform"` - Scale float64 `json:"scale"` - CurrentMode *OutputMode `json:"currentMode"` - Modes []OutputMode `json:"modes"` - AdaptiveSync uint32 `json:"adaptiveSync"` - AdaptiveSyncSupported bool `json:"adaptiveSyncSupported"` - ID uint32 `json:"id"` -} - -type State struct { - Outputs []Output `json:"outputs"` - Serial uint32 `json:"serial"` -} - -type cmd struct { - fn func() -} - -type Manager struct { - display wlclient.WaylandDisplay - ctx *wlclient.Context - registry *wlclient.Registry - manager *wlr_output_management.ZwlrOutputManagerV1 - - heads syncmap.Map[uint32, *headState] - modes syncmap.Map[uint32, *modeState] - - serial uint32 - - wlMutex sync.Mutex - cmdq chan cmd - stopChan chan struct{} - wg sync.WaitGroup - - subscribers syncmap.Map[string, chan State] - dirty chan struct{} - notifierWg sync.WaitGroup - lastNotified *State - - stateMutex sync.RWMutex - state *State - - fatalError chan error -} - -type headState struct { - id uint32 - handle *wlr_output_management.ZwlrOutputHeadV1 - name string - description string - make string - model string - serialNumber string - physicalWidth int32 - physicalHeight int32 - enabled bool - x int32 - y int32 - transform int32 - scale float64 - currentModeID uint32 - modeIDs []uint32 - adaptiveSync uint32 - adaptiveSyncSupported bool - finished bool - ready bool -} - -type modeState struct { - id uint32 - handle *wlr_output_management.ZwlrOutputModeV1 - width int32 - height int32 - refresh int32 - preferred bool - finished bool -} - -func (m *Manager) GetState() State { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - if m.state == nil { - return State{ - Outputs: []Output{}, - Serial: 0, - } - } - stateCopy := *m.state - return stateCopy -} - -func (m *Manager) Subscribe(id string) chan State { - ch := make(chan State, 64) - - m.subscribers.Store(id, ch) - - return ch -} - -func (m *Manager) Unsubscribe(id string) { - - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - - } - -} - -func (m *Manager) notifySubscribers() { - select { - case m.dirty <- struct{}{}: - default: - } -} - -func (m *Manager) FatalError() <-chan error { - return m.fatalError -} - -func stateChanged(old, new *State) bool { - if old == nil || new == nil { - return true - } - if old.Serial != new.Serial { - return true - } - if len(old.Outputs) != len(new.Outputs) { - return true - } - for i := range new.Outputs { - if i >= len(old.Outputs) { - return true - } - oldOut := &old.Outputs[i] - newOut := &new.Outputs[i] - if oldOut.Name != newOut.Name || oldOut.Enabled != newOut.Enabled { - return true - } - if oldOut.X != newOut.X || oldOut.Y != newOut.Y { - return true - } - if oldOut.Transform != newOut.Transform || oldOut.Scale != newOut.Scale { - return true - } - if oldOut.AdaptiveSync != newOut.AdaptiveSync || oldOut.AdaptiveSyncSupported != newOut.AdaptiveSyncSupported { - return true - } - if (oldOut.CurrentMode == nil) != (newOut.CurrentMode == nil) { - return true - } - if oldOut.CurrentMode != nil && newOut.CurrentMode != nil { - if oldOut.CurrentMode.Width != newOut.CurrentMode.Width || - oldOut.CurrentMode.Height != newOut.CurrentMode.Height || - oldOut.CurrentMode.Refresh != newOut.CurrentMode.Refresh { - return true - } - } - if len(oldOut.Modes) != len(newOut.Modes) { - return true - } - } - return false -} -- cgit v1.3