From ded9fcc8c34ad4692411609512c0692d15fbcf18 Mon Sep 17 00:00:00 2001 From: DashieTM Date: Mon, 13 Oct 2025 17:26:08 +0200 Subject: [PATCH] feature(iced): Introduce iced version --- .envrc | 3 +- .gitignore | 6 ++- Cargo.toml | 27 ++++++++---- flake.lock | 83 +++++++++++++++++++++++++++++++++++ flake.nix | 56 +++++++++++++++++++++++ shell.nix | 16 ------- src/audio/audio_structures.rs | 19 +++++--- src/lib.rs | 8 ++-- src/tests.rs | 4 +- src/utils/any.rs | 68 ++++++++++++++++++++++++++++ src/utils/config.rs | 55 +++++++++++------------ src/utils/error.rs | 23 ++++++++++ src/utils/iced_sidebar.rs | 46 +++++++++++++++++++ src/utils/mod.rs | 3 ++ src/utils/plugin_setup.rs | 54 +++++++++++++---------- src/utils/variant.rs | 4 +- 16 files changed, 379 insertions(+), 96 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 shell.nix create mode 100644 src/utils/any.rs create mode 100644 src/utils/error.rs create mode 100644 src/utils/iced_sidebar.rs diff --git a/.envrc b/.envrc index 06506cf..3550a30 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1 @@ -#!/usr/bin/env bash -use nix +use flake diff --git a/.gitignore b/.gitignore index ed8c408..2cd684b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,8 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb -.idea \ No newline at end of file +.idea + +# direnv +.envrc +.direnv/ diff --git a/Cargo.toml b/Cargo.toml index 7db6f99..60cbec2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,27 @@ [package] name = "re_set-lib" version = "5.2.5" -edition = "2021" +edition = "2024" description = "Data structure library for ReSet" repository = "https://github.com/Xetibo/ReSet-Lib" license = "GPL-3.0-or-later" [dependencies] -dbus = "0.9.7" +dbus = "0.9.9" dbus-crossroads = "0.5.2" -pulse = { version = "2.0", package = "libpulse-binding" } -once_cell = "1.19.0" -libloading = "0.8.3" -gtk = { version = "0.8.1", package = "gtk4", features = ["v4_12"] } -serial_test = "3.0.0" -toml = "0.8.12" -xdg = "2.5.2" +pulse = { version = "2.30.1", package = "libpulse-binding" } +once_cell = "1.21.3" +libloading = "0.8.9" +gtk = { version = "0.10.1", package = "gtk4", features = ["v4_16"] } +serial_test = "3.2.0" +toml = "0.9.8" +xdg = "3.0.0" +zbus = "5.11.0" +iced = { version = "0.14.0-dev", features = [ + "advanced", + "canvas", + "image", + "svg", + "tokio", +], git = "https://github.com/iced-rs/iced" } +serde = "1.0.228" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..b518133 --- /dev/null +++ b/flake.lock @@ -0,0 +1,83 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1759362264, + "narHash": "sha256-wfG0S7pltlYyZTM+qqlhJ7GMw2fTF4mLKCIVhLii/4M=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "758cf7296bee11f1706a574c77d072b8a7baa881", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1760038930, + "narHash": "sha256-Oncbh0UmHjSlxO7ErQDM3KM0A5/Znfofj2BSzlHLeVw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0b4defa2584313f3b781240b29d61f6f9f7e0df3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1760323082, + "narHash": "sha256-SKhC9tyt+gVgQHnZGMVPSdptlDYNqApT56JF5t8RwBY=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "c73e6874fe8dce0bab82c0387b510875f1eff9f8", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..22a6d47 --- /dev/null +++ b/flake.nix @@ -0,0 +1,56 @@ +{ + description = "A library for ReSet applications."; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-parts = { + url = "github:hercules-ci/flake-parts"; + inputs.nixpkgs-lib.follows = "nixpkgs"; + }; + rust-overlay.url = "github:oxalica/rust-overlay"; + }; + + outputs = inputs @ { + self, + flake-parts, + ... + }: + flake-parts.lib.mkFlake {inherit inputs;} { + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; + + perSystem = { + pkgs, + system, + ... + }: { + _module.args.pkgs = import self.inputs.nixpkgs { + inherit system; + overlays = [ + ( + import + inputs.rust-overlay + ) + ]; + }; + devShells.default = pkgs.mkShell { + nativeBuildInputs = [ + pkgs.pkg-config + pkgs.dbus + ]; + + buildInputs = [ + pkgs.dbus + pkgs.libadwaita + pkgs.pulseaudio + (pkgs.rust-bin.selectLatestNightlyWith + (toolchain: toolchain.default)) + pkgs.rust-analyzer + pkgs.clippy + ]; + }; + }; + }; +} diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 0b05850..0000000 --- a/shell.nix +++ /dev/null @@ -1,16 +0,0 @@ -{ pkgs ? import {} }: - -with pkgs; -mkShell { - nativeBuildInputs = [ - pkg-config - ]; - - buildInputs = [ - dbus - gtk4 - libadwaita - pulseaudio - ]; - -} diff --git a/src/audio/audio_structures.rs b/src/audio/audio_structures.rs index 791634e..690cfdd 100644 --- a/src/audio/audio_structures.rs +++ b/src/audio/audio_structures.rs @@ -5,6 +5,7 @@ use dbus::{ use pulse::context::introspect::{ CardInfo, CardProfileInfo, SinkInfo, SinkInputInfo, SourceInfo, SourceOutputInfo, }; +use zbus::zvariant::{DeserializeDict, SerializeDict, Type}; use crate::network::connection::Enum; @@ -113,7 +114,8 @@ impl Enum for DeviceState { } } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, DeserializeDict, SerializeDict, Type)] +#[zvariant(signature = "dict")] pub struct Source { pub index: u32, pub name: String, @@ -227,7 +229,8 @@ impl TAudioObject for Source { } } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, DeserializeDict, SerializeDict, Type)] +#[zvariant(signature = "dict")] pub struct Sink { pub index: u32, pub name: String, @@ -341,7 +344,8 @@ impl TAudioObject for Sink { } } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, DeserializeDict, SerializeDict, Type)] +#[zvariant(signature = "dict")] pub struct InputStream { pub index: u32, pub name: String, @@ -459,7 +463,8 @@ impl TAudioStreamObject for InputStream { } } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, DeserializeDict, SerializeDict, Type)] +#[zvariant(signature = "dict")] pub struct OutputStream { pub index: u32, pub name: String, @@ -577,7 +582,8 @@ impl TAudioStreamObject for OutputStream { } } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, DeserializeDict, SerializeDict, Type)] +#[zvariant(signature = "dict")] pub struct Card { pub index: u32, pub name: String, @@ -642,7 +648,8 @@ impl From<&CardInfo<'_>> for Card { } } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, DeserializeDict, SerializeDict, Type)] +#[zvariant(signature = "dict")] pub struct CardProfile { pub name: String, pub description: String, diff --git a/src/lib.rs b/src/lib.rs index e7f8415..2eaf782 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,4 @@ -#![feature(trait_upcasting)] #![feature(string_remove_matches)] -#![feature(unsized_fn_params)] #![feature(unboxed_closures)] #![feature(fn_traits)] use std::{ @@ -40,14 +38,14 @@ impl fmt::Display for PathNotFoundError { pub fn create_config_directory(project_name: &str) -> Option { let base_dir = xdg::BaseDirectories::new(); - if let Err(_error) = base_dir { + if base_dir.config_home.is_none() { ERROR!( - format!("Could not get base directories: {}", _error), + format!("Could not get base directories"), ErrorLevel::Critical ); return None; } - let base_dir = base_dir.unwrap().get_config_home(); + let base_dir = base_dir.config_home.unwrap(); let base_dir = flatpak_fix(base_dir); let project_dir = base_dir.join(project_name); let res = fs::create_dir_all(&project_dir); diff --git a/src/tests.rs b/src/tests.rs index abf9d51..2145af9 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -4,7 +4,7 @@ use serial_test::serial; #[cfg(test)] use crate::utils::plugin::plugin_tests; #[cfg(test)] -use crate::{utils::macros::ErrorLevel, write_log_to_file, ERROR, LOG}; +use crate::{ERROR, LOG, utils::macros::ErrorLevel, write_log_to_file}; #[cfg(test)] use crate::{utils::plugin::PluginTestError, utils::plugin::PluginTestFunc}; @@ -19,8 +19,8 @@ fn test_config_dir() { assert_eq!( config_file, xdg::BaseDirectories::new() - .unwrap() .get_config_home() + .unwrap() .join("globiTM/ReSet.toml") .to_str() .unwrap() diff --git a/src/utils/any.rs b/src/utils/any.rs new file mode 100644 index 0000000..0fd0a74 --- /dev/null +++ b/src/utils/any.rs @@ -0,0 +1,68 @@ +use std::any::TypeId; +use std::fmt::Debug; + +use iced::advanced::graphics::futures::MaybeSend; + +// Taken from https://doc.rust-lang.org/stable/src/core/any.rs.html +// adjusted to fit ReSet +pub trait ReSetAny: 'static + MaybeSend + Debug + Send + Sync { + fn type_id(&self) -> TypeId; +} + +impl ReSetAny for T { + fn type_id(&self) -> TypeId { + TypeId::of::() + } +} + +impl dyn ReSetAny { + #[inline] + pub fn is(&self) -> bool { + // Get `TypeId` of the type this function is instantiated with. + let t = TypeId::of::(); + // Get `TypeId` of the type in the trait object (`self`). + let concrete = self.type_id(); + // Compare both `TypeId`s on equality. + t == concrete + } + + #[inline] + pub fn downcast_ref(&self) -> Option<&T> { + if self.is::() { + // SAFETY: just checked whether we are pointing to the correct type, and we can rely on + // that check for memory safety because we have implemented Any for all types; no other + // impls can exist as they would conflict with our impl. + unsafe { Some(self.downcast_ref_unchecked()) } + } else { + None + } + } + + #[inline] + pub fn downcast_mut(&mut self) -> Option<&mut T> { + if self.is::() { + // SAFETY: just checked whether we are pointing to the correct type, and we can rely on + // that check for memory safety because we have implemented Any for all types; no other + // impls can exist as they would conflict with our impl. + unsafe { Some(self.downcast_mut_unchecked()) } + } else { + None + } + } + + #[inline] + /// # Safety + pub unsafe fn downcast_mut_unchecked(&mut self) -> &mut T { + debug_assert!(self.is::()); + // SAFETY: caller guarantees that T is the correct type + unsafe { &mut *(self as *mut dyn ReSetAny as *mut T) } + } + + #[inline] + /// # Safety + pub unsafe fn downcast_ref_unchecked(&self) -> &T { + debug_assert!(self.is::()); + // SAFETY: caller guarantees that T is the correct type + unsafe { &*(self as *const dyn ReSetAny as *const T) } + } +} diff --git a/src/utils/config.rs b/src/utils/config.rs index 5c1bde6..fa8e998 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -3,11 +3,11 @@ use std::{fs, io::Read}; use once_cell::sync::Lazy; use toml::Table; -use crate::{create_config, ERROR, LOG}; +use crate::{ERROR, LOG, create_config}; #[cfg(debug_assertions)] use crate::{utils::macros::ErrorLevel, write_log_to_file}; -pub static mut CONFIG_STRING: Lazy = Lazy::new(|| { +pub static CONFIG_STRING: Lazy = Lazy::new(|| { let config = create_config("reset"); if let Some(config) = config { config.to_str().unwrap().to_string() @@ -19,31 +19,29 @@ pub static mut CONFIG_STRING: Lazy = Lazy::new(|| { pub static CONFIG: Lazy = Lazy::new(parse_config); pub fn parse_config() -> Table { - unsafe { - let config_file = fs::File::open(CONFIG_STRING.as_str()); - LOG!(format!( - "Using config file path: {}", - CONFIG_STRING.as_str() - )); - if let Err(_errorr) = config_file { - ERROR!( - format!("Could not write config file: {}", _errorr), - ErrorLevel::Recoverable - ); - return Table::new(); - } - let mut config_string = String::from(""); - let err = config_file.unwrap().read_to_string(&mut config_string); - if let Err(_error) = err { - ERROR!( - format!("Could not read config file: {}", _error), - ErrorLevel::Recoverable - ); - return Table::new(); - } - LOG!(format!("Config file content:\n {}", config_string)); - config_string.parse::
().expect("Config has errors") + let config_file = fs::File::open(CONFIG_STRING.as_str()); + LOG!(format!( + "Using config file path: {}", + CONFIG_STRING.as_str() + )); + if let Err(_errorr) = config_file { + ERROR!( + format!("Could not write config file: {}", _errorr), + ErrorLevel::Recoverable + ); + return Table::new(); } + let mut config_string = String::from(""); + let err = config_file.unwrap().read_to_string(&mut config_string); + if let Err(_error) = err { + ERROR!( + format!("Could not read config file: {}", _error), + ErrorLevel::Recoverable + ); + return Table::new(); + } + LOG!(format!("Config file content:\n {}", config_string)); + config_string.parse::
().expect("Config has errors") } pub fn get_config_value T>( @@ -52,11 +50,10 @@ pub fn get_config_value T>( callback: F, ) -> bool { #[allow(clippy::borrow_interior_mutable_const)] - if let Some(monitor_config) = CONFIG.get(category) { - if let Some(value) = monitor_config.get(entry) { + if let Some(monitor_config) = CONFIG.get(category) + && let Some(value) = monitor_config.get(entry) { (callback(value)); return true; } - } false } diff --git a/src/utils/error.rs b/src/utils/error.rs new file mode 100644 index 0000000..e14b131 --- /dev/null +++ b/src/utils/error.rs @@ -0,0 +1,23 @@ +use std::fmt; + +pub type ReSetError = Box; + +pub trait TReSetError: fmt::Debug + fmt::Display + Send + Sync + 'static {} + +pub fn create_error(err: impl TReSetError) -> ReSetError { + Box::new(err) +} + +impl TReSetError for zbus::Error {} +impl From for ReSetError { + fn from(value: zbus::Error) -> Self { + create_error(value) + } +} + +impl TReSetError for String {} +impl From for ReSetError { + fn from(value: String) -> Self { + create_error(value) + } +} diff --git a/src/utils/iced_sidebar.rs b/src/utils/iced_sidebar.rs new file mode 100644 index 0000000..1c3aea5 --- /dev/null +++ b/src/utils/iced_sidebar.rs @@ -0,0 +1,46 @@ +use super::any::ReSetAny; + +pub enum EntryButtonLevel { + TopLevel, + SubLevel, +} + +pub struct EntryButton { + pub title: &'static str, + pub icon: Option, + pub msg: Box<&'static dyn ReSetAny>, + pub level: EntryButtonLevel, +} + +impl EntryButton { + pub fn top_level( + title: &'static str, + icon: Option>, + msg: impl Into>, + ) -> Self { + Self { + title, + icon: icon.map(|icon| icon.into()), + msg: msg.into(), + level: EntryButtonLevel::TopLevel, + } + } + + pub fn sub_level( + title: &'static str, + icon: Option>, + msg: impl Into>, + ) -> Self { + Self { + title, + icon: icon.map(|icon| icon.into()), + msg: msg.into(), + level: EntryButtonLevel::SubLevel, + } + } +} + +pub struct EntryCategory { + pub main_entry: EntryButton, + pub sub_entries: Vec, +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 6e7040e..9541f16 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,7 +1,10 @@ +pub mod any; pub mod config; pub mod dbus_utils; +pub mod error; pub mod flags; pub mod gtk; +pub mod iced_sidebar; pub mod macros; pub mod plugin; pub mod plugin_setup; diff --git a/src/utils/plugin_setup.rs b/src/utils/plugin_setup.rs index 8d064b5..d3eaae1 100644 --- a/src/utils/plugin_setup.rs +++ b/src/utils/plugin_setup.rs @@ -3,7 +3,7 @@ use std::{ hint::spin_loop, io::ErrorKind, path::PathBuf, - sync::{atomic::AtomicBool, Arc, RwLock}, + sync::{Arc, LazyLock, RwLock, atomic::AtomicBool}, }; use dbus_crossroads::{Crossroads, IfaceToken}; @@ -11,7 +11,7 @@ use libloading::Library; use once_cell::sync::Lazy; use toml::Value; -use crate::{create_config_directory, ERROR, LOG}; +use crate::{ERROR, LOG, create_config_directory}; #[cfg(debug_assertions)] use crate::{utils::macros::ErrorLevel, write_log_to_file}; @@ -22,16 +22,16 @@ use super::{ pub static LIBS_LOADED: AtomicBool = AtomicBool::new(false); pub static LIBS_LOADING: AtomicBool = AtomicBool::new(false); -pub static mut FRONTEND_PLUGINS: Lazy> = Lazy::new(|| { +pub static FRONTEND_PLUGINS: Lazy> = Lazy::new(|| { SETUP_LIBS(); setup_frontend_plugins() }); -pub static mut BACKEND_PLUGINS: Lazy> = Lazy::new(|| { +pub static BACKEND_PLUGINS: Lazy> = Lazy::new(|| { SETUP_LIBS(); setup_backend_plugins() }); -static mut LIBS: Vec = Vec::new(); -pub static mut PLUGIN_DIR: Lazy = Lazy::new(|| PathBuf::from("")); +static LIBS: LazyLock> = LazyLock::new(SETUP_LIBS); +pub static PLUGIN_DIR: Lazy = Lazy::new(|| PathBuf::from("")); static SETUP_PLUGIN_DIR: fn() -> Option = || -> Option { let config = create_config_directory("reset").expect("Could not create config directory"); @@ -48,15 +48,19 @@ static SETUP_PLUGIN_DIR: fn() -> Option = || -> Option { } }; -static SETUP_LIBS: fn() = || { +static SETUP_LIBS: fn() -> Vec = || -> Vec { + let mut libs = Vec::new(); if LIBS_LOADING.load(std::sync::atomic::Ordering::SeqCst) { while !LIBS_LOADED.load(std::sync::atomic::Ordering::SeqCst) { spin_loop(); } - return; + return Vec::new(); } LIBS_LOADING.store(true, std::sync::atomic::Ordering::SeqCst); - let read_dir: fn(PathBuf) = |dir: PathBuf| { + let read_dir: fn(PathBuf, &mut Vec) = |dir: PathBuf, + libs: &mut Vec< + libloading::Library, + >| { let plugins = CONFIG.get("plugins"); if plugins.is_none() { LOG!("No plugins entry found in config"); @@ -64,7 +68,10 @@ static SETUP_LIBS: fn() = || { } let plugins = plugins.unwrap().as_array(); if plugins.is_none() { - ERROR!("Wrong config, please write plugins entry as array of strings: e.g [\"libyourplugin.so\"]", ErrorLevel::PartialBreakage); + ERROR!( + "Wrong config, please write plugins entry as array of strings: e.g [\"libyourplugin.so\"]", + ErrorLevel::PartialBreakage + ); return; } let plugins = plugins.unwrap(); @@ -85,7 +92,7 @@ static SETUP_LIBS: fn() = || { let path = file.path(); let lib = libloading::Library::new(&path); if let Ok(lib) = lib { - LIBS.push(lib); + libs.push(lib); } else { ERROR!( format!( @@ -101,30 +108,29 @@ static SETUP_LIBS: fn() = || { }; #[allow(clippy::borrow_interior_mutable_const)] let plugin_dir = if let Some(config) = CONFIG.get("plugin_path") { - let config = config.as_str(); - if config.is_none() { - SETUP_PLUGIN_DIR() - } else { - let maybe_dir = PathBuf::from(config.unwrap()); + let config_opt = config.as_str(); + if let Some(config) = config_opt { + let maybe_dir = PathBuf::from(config); if maybe_dir.is_dir() { Some(maybe_dir) } else { SETUP_PLUGIN_DIR() } + } else { + SETUP_PLUGIN_DIR() } } else { SETUP_PLUGIN_DIR() }; - unsafe { - if PLUGIN_DIR.is_dir() { - read_dir(PLUGIN_DIR.clone()); - read_dir(PathBuf::from("/usr/lib/reset/")); - } else if let Some(plugin_dir) = plugin_dir { - read_dir(plugin_dir); - read_dir(PathBuf::from("/usr/lib/reset/")); - } + if PLUGIN_DIR.is_dir() { + read_dir(PLUGIN_DIR.clone(), &mut libs); + read_dir(PathBuf::from("/usr/lib/reset/"), &mut libs); + } else if let Some(plugin_dir) = plugin_dir { + read_dir(plugin_dir, &mut libs); + read_dir(PathBuf::from("/usr/lib/reset/"), &mut libs); } LIBS_LOADED.store(true, std::sync::atomic::Ordering::SeqCst); + libs }; fn setup_backend_plugins() -> Vec { diff --git a/src/utils/variant.rs b/src/utils/variant.rs index 0377e7c..0013260 100644 --- a/src/utils/variant.rs +++ b/src/utils/variant.rs @@ -373,9 +373,9 @@ impl Variant { unsafe { Ok(self.to_value_unchecked::()) } } - unsafe fn to_value_unchecked(&self) -> &T { + unsafe fn to_value_unchecked(&self) -> &T { unsafe { &*(self.value.deref() as *const dyn Any as *mut T) - } + }} } #[derive(Debug)]