From a33d0239504d8c1b02ee196c4e1785c59f994866 Mon Sep 17 00:00:00 2001 From: Tarasov Aleksandr <55220741+arabianq@users.noreply.github.com> Date: Fri, 24 Jul 2026 04:49:51 +0300 Subject: [PATCH] feat(daemon, cli): global DaemonConfig and an ability to get/save it using cli (#174) * use one global Arc * implement commands * replace Arc with Arc> and fix pwsp-gui * add pwsp-cli support * replace serde_json::to_string_pretty with to_string * refactor * implement UpdateDaemonConfigCommand and use it in pwsp-gui so it now uses real in-memory config, not the saved one * implement utils::gui get_daemon_config and update_daemon_config to simplify code --- pwsp-cli/src/main.rs | 6 ++++ pwsp-gui/src/gui/mod.rs | 16 +++++------ pwsp-gui/src/gui/update.rs | 9 +++--- pwsp-lib/src/types/audio_player.rs | 12 +++++--- pwsp-lib/src/types/commands.rs | 45 ++++++++++++++++++++++++++++-- pwsp-lib/src/types/socket.rs | 19 +++++++++++++ pwsp-lib/src/utils/commands.rs | 13 ++++++++- pwsp-lib/src/utils/daemon.rs | 36 +++++++++++++++++------- pwsp-lib/src/utils/gui.rs | 15 +++++++++- 9 files changed, 141 insertions(+), 30 deletions(-) diff --git a/pwsp-cli/src/main.rs b/pwsp-cli/src/main.rs index 00db169..0cbf148 100644 --- a/pwsp-cli/src/main.rs +++ b/pwsp-cli/src/main.rs @@ -69,6 +69,8 @@ enum Actions { #[clap(short, long)] id: Option, }, + /// Daemon configuration + SaveDaemonConfig, /// Play a sound by hotkey slot name PlayHotkey { slot: String }, /// Remove the hotkey slot @@ -106,6 +108,8 @@ enum GetCommands { Inputs, /// Version of the daemon DaemonVersion, + /// Daemon configuration + DaemonConfig, /// Full player state FullState, /// All hotkey slots @@ -165,6 +169,7 @@ async fn main() -> Result<()> { concurrent, } => Request::play(&file_path.to_string_lossy(), concurrent), Actions::ToggleLoop { id } => Request::toggle_loop(id), + Actions::SaveDaemonConfig => Request::save_daemon_config(), Actions::PlayHotkey { slot } => Request::play_hotkey(&slot), Actions::ClearHotkey { slot } => Request::clear_hotkey(&slot), Actions::ClearHotkeyKey { slot } => Request::clear_hotkey_key(&slot), @@ -179,6 +184,7 @@ async fn main() -> Result<()> { GetCommands::Input => Request::get_input(), GetCommands::Inputs => Request::get_inputs(), GetCommands::DaemonVersion => Request::get_daemon_version(), + GetCommands::DaemonConfig => Request::get_daemon_config(), GetCommands::FullState => Request::get_full_state(), GetCommands::Hotkeys => Request::get_hotkeys(), }, diff --git a/pwsp-gui/src/gui/mod.rs b/pwsp-gui/src/gui/mod.rs index 25b4cb1..826d64e 100644 --- a/pwsp-gui/src/gui/mod.rs +++ b/pwsp-gui/src/gui/mod.rs @@ -9,14 +9,13 @@ use itertools::Itertools; use pwsp_lib::{ types::{ audio_player::PlayerState, - config::GuiConfig, - config::HotkeyConfig, + config::{GuiConfig, HotkeyConfig}, gui::{AppState, AudioPlayerState}, socket::Request, }, - utils::{ - daemon::get_daemon_config, - gui::{get_gui_config, make_request_async, make_request_sync, start_app_state_thread}, + utils::gui::{ + get_daemon_config, get_gui_config, make_request_async, make_request_sync, + start_app_state_thread, update_daemon_config, }, }; use rfd::FileDialog; @@ -130,10 +129,11 @@ impl SoundpadGui { pub fn set_input(&mut self, name: String) { make_request_async(Request::set_input(&name)); - if self.config.save_input { - let mut daemon_config = get_daemon_config(); + if self.config.save_input + && let Ok(mut daemon_config) = get_daemon_config() + { daemon_config.default_input_name = Some(name); - daemon_config.save_to_file().ok(); + update_daemon_config(&daemon_config).ok(); } } diff --git a/pwsp-gui/src/gui/update.rs b/pwsp-gui/src/gui/update.rs index 2d84d1e..6760ec7 100644 --- a/pwsp-gui/src/gui/update.rs +++ b/pwsp-gui/src/gui/update.rs @@ -3,7 +3,7 @@ use eframe::{App, Frame as EFrame}; use egui::{CentralPanel, Context, ThemePreference}; use pwsp_lib::{ types::{config::PreferredTheme, socket::Request}, - utils::{daemon::get_daemon_config, gui::make_request_async}, + utils::gui::{get_daemon_config, make_request_async, update_daemon_config}, }; use std::time::{Duration, Instant}; @@ -85,10 +85,11 @@ impl App for SoundpadGui { self.app_state.ignore_volume_update_until = Some(Instant::now() + Duration::from_millis(300)); - if self.config.save_volume { - let mut daemon_config = get_daemon_config(); + if self.config.save_volume + && let Ok(mut daemon_config) = get_daemon_config() + { daemon_config.default_volume = Some(self.app_state.volume_slider_value); - daemon_config.save_to_file().ok(); + update_daemon_config(&daemon_config).ok(); } } diff --git a/pwsp-lib/src/types/audio_player.rs b/pwsp-lib/src/types/audio_player.rs index f03fd93..b88d4b0 100644 --- a/pwsp-lib/src/types/audio_player.rs +++ b/pwsp-lib/src/types/audio_player.rs @@ -1,7 +1,7 @@ use crate::{ types::pipewire::DeviceType, utils::{ - daemon::get_daemon_config, + daemon::with_daemon_config, pipewire::{PwTerminator, create_link, get_device, link_player_to_virtual_mic}, }, }; @@ -67,8 +67,12 @@ pub struct AudioPlayer { impl AudioPlayer { pub async fn new() -> Result { - let daemon_config = get_daemon_config(); - let default_volume = daemon_config.default_volume.unwrap_or(1.0); + let (default_input_name, default_volume) = with_daemon_config(|c| { + ( + c.default_input_name.clone(), + c.default_volume.unwrap_or(1.0), + ) + }); let mut audio_player = AudioPlayer { stream_handle: None, @@ -77,7 +81,7 @@ impl AudioPlayer { input_link_sender: None, player_link_sender: None, - input_device_name: daemon_config.default_input_name.clone(), + input_device_name: default_input_name, volume: default_volume, }; diff --git a/pwsp-lib/src/types/commands.rs b/pwsp-lib/src/types/commands.rs index d4f0ccc..225bdf7 100644 --- a/pwsp-lib/src/types/commands.rs +++ b/pwsp-lib/src/types/commands.rs @@ -1,12 +1,12 @@ use crate::{ types::{ audio_player::{FullState, PlayerState}, - config::HotkeyConfig, + config::{DaemonConfig, HotkeyConfig}, socket::{Request, Response}, }, utils::{ commands::parse_command, - daemon::get_audio_player, + daemon::{get_audio_player, with_daemon_config}, pipewire::{get_all_devices, get_device}, }, }; @@ -127,6 +127,14 @@ pub struct ClearHotkeyKeyCommand { pub slot: Option, } +pub struct GetDaemonConfigCommand {} + +pub struct SaveDaemonConfigCommand {} + +pub struct UpdateDaemonConfigCommand { + pub new_config: DaemonConfig, +} + #[async_trait] impl Executable for PingCommand { async fn execute(&self) -> Response { @@ -723,3 +731,36 @@ impl Executable for ClearHotkeyKeyCommand { } } } + +#[async_trait] +impl Executable for GetDaemonConfigCommand { + async fn execute(&self) -> Response { + let serialized = with_daemon_config(|c| serde_json::to_string(&c)); + + match serialized { + Ok(s) => Response::new(true, s), + Err(err) => Response::new(false, format!("Failed to serialize daemon config: {}", err)), + } + } +} + +#[async_trait] +impl Executable for SaveDaemonConfigCommand { + async fn execute(&self) -> Response { + match with_daemon_config(|c| c.save_to_file()) { + Ok(_) => Response::new(true, "Daemon config saved successfully"), + Err(err) => Response::new(false, format!("Failed to save daemon config: {}", err)), + } + } +} + +#[async_trait] +impl Executable for UpdateDaemonConfigCommand { + async fn execute(&self) -> Response { + with_daemon_config(|c| { + c.clone_from(&self.new_config); + }); + + Response::new(true, "Daemon config updated successfully") + } +} diff --git a/pwsp-lib/src/types/socket.rs b/pwsp-lib/src/types/socket.rs index 728a03a..b046a0a 100644 --- a/pwsp-lib/src/types/socket.rs +++ b/pwsp-lib/src/types/socket.rs @@ -1,3 +1,4 @@ +use crate::types::config::DaemonConfig; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -172,6 +173,24 @@ impl Request { Request::new("get_daemon_version", vec![]) } + pub fn get_daemon_config() -> Self { + Request::new("get_daemon_config", vec![]) + } + + pub fn save_daemon_config() -> Self { + Request::new("save_daemon_config", vec![]) + } + + pub fn update_daemon_config(new_config: &DaemonConfig) -> Self { + Request::new( + "update_daemon_config", + vec![( + "new_config", + &serde_json::to_string(&new_config).unwrap_or_default(), + )], + ) + } + pub fn get_full_state() -> Self { Request::new("get_full_state", vec![]) } diff --git a/pwsp-lib/src/utils/commands.rs b/pwsp-lib/src/utils/commands.rs index b44e231..332dd88 100644 --- a/pwsp-lib/src/utils/commands.rs +++ b/pwsp-lib/src/utils/commands.rs @@ -1,4 +1,4 @@ -use crate::types::{commands::*, socket::Request}; +use crate::types::{commands::*, config::DaemonConfig, socket::Request}; use std::path::PathBuf; @@ -119,6 +119,17 @@ pub fn parse_command(request: &Request) -> Option> { key_chord, })) } + "get_daemon_config" => Some(Box::new(GetDaemonConfigCommand {})), + "save_daemon_config" => Some(Box::new(SaveDaemonConfigCommand {})), + "update_daemon_config" => { + let new_config = request + .args + .get("new_config") + .and_then(|nc| serde_json::from_str::(nc).ok()) + .unwrap_or_default(); + + Some(Box::new(UpdateDaemonConfigCommand { new_config })) + } _ => None, } } diff --git a/pwsp-lib/src/utils/daemon.rs b/pwsp-lib/src/utils/daemon.rs index 45e4538..647ab4a 100644 --- a/pwsp-lib/src/utils/daemon.rs +++ b/pwsp-lib/src/utils/daemon.rs @@ -7,36 +7,52 @@ use crate::types::{ use anyhow::Result; use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt}; use std::path::PathBuf; -use std::{env, error::Error, fs}; +use std::{ + env, + error::Error, + fs, + sync::{Arc, Mutex, OnceLock}, +}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::UnixStream, - sync::{Mutex, OnceCell}, + sync::{Mutex as AsyncMutex, OnceCell}, time::{Duration, sleep}, }; -static AUDIO_PLAYER: OnceCell> = OnceCell::const_new(); +static AUDIO_PLAYER: OnceCell> = OnceCell::const_new(); +static DAEMON_CONFIG: OnceLock>> = OnceLock::new(); -pub async fn get_audio_player() -> Result<&'static Mutex, String> { +pub async fn get_audio_player() -> Result<&'static AsyncMutex, String> { AUDIO_PLAYER .get_or_try_init(|| async { println!("Initializing audio player"); match AudioPlayer::new().await { - Ok(player) => Ok(Mutex::new(player)), + Ok(player) => Ok(AsyncMutex::new(player)), Err(err) => Err(err.to_string()), } }) .await } -pub fn get_daemon_config() -> DaemonConfig { - DaemonConfig::load_from_file().unwrap_or_else(|_| { - let config = DaemonConfig::default(); - config.save_to_file().ok(); - config +pub fn get_daemon_config() -> &'static Arc> { + DAEMON_CONFIG.get_or_init(|| { + Arc::new(Mutex::new(DaemonConfig::load_from_file().unwrap_or_else( + |_| { + let config = DaemonConfig::default(); + config.save_to_file().ok(); + config + }, + ))) }) } +pub fn with_daemon_config(f: impl FnOnce(&mut DaemonConfig) -> R) -> R { + let config = get_daemon_config().clone(); + let mut guard = config.lock().unwrap(); + f(&mut guard) +} + fn get_current_uid() -> u32 { rustix::process::geteuid().as_raw() } diff --git a/pwsp-lib/src/utils/gui.rs b/pwsp-lib/src/utils/gui.rs index e45e371..38de1e1 100644 --- a/pwsp-lib/src/utils/gui.rs +++ b/pwsp-lib/src/utils/gui.rs @@ -1,7 +1,7 @@ use crate::{ types::{ audio_player::FullState, - config::{GuiConfig, HotkeyConfig}, + config::{DaemonConfig, GuiConfig, HotkeyConfig}, gui::AudioPlayerState, socket::{Request, Response}, }, @@ -24,6 +24,19 @@ pub fn get_gui_config() -> GuiConfig { }) } +pub fn update_daemon_config(new_config: &DaemonConfig) -> Result<()> { + make_request_sync(Request::update_daemon_config(new_config))?; + make_request_async(Request::save_daemon_config()); + + Ok(()) +} + +pub fn get_daemon_config() -> Result { + let response = make_request_sync(Request::get_daemon_config())?; + let config: DaemonConfig = serde_json::from_str(&response.message)?; + Ok(config) +} + pub fn make_request_sync(request: Request) -> Result { tokio::task::block_in_place(|| { tokio::runtime::Handle::current()