feat(daemon, cli): global DaemonConfig and an ability to get/save it using cli (#174)

* use one global Arc<DaemonConfig>

* implement commands

* replace Arc<DaemonConfig> with Arc<Mutex<DaemonConfig>> 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
This commit is contained in:
Tarasov Aleksandr
2026-07-24 04:49:51 +03:00
committed by GitHub
parent 5c73bc1bea
commit a33d023950
9 changed files with 141 additions and 30 deletions
+6
View File
@@ -69,6 +69,8 @@ enum Actions {
#[clap(short, long)] #[clap(short, long)]
id: Option<u32>, id: Option<u32>,
}, },
/// Daemon configuration
SaveDaemonConfig,
/// Play a sound by hotkey slot name /// Play a sound by hotkey slot name
PlayHotkey { slot: String }, PlayHotkey { slot: String },
/// Remove the hotkey slot /// Remove the hotkey slot
@@ -106,6 +108,8 @@ enum GetCommands {
Inputs, Inputs,
/// Version of the daemon /// Version of the daemon
DaemonVersion, DaemonVersion,
/// Daemon configuration
DaemonConfig,
/// Full player state /// Full player state
FullState, FullState,
/// All hotkey slots /// All hotkey slots
@@ -165,6 +169,7 @@ async fn main() -> Result<()> {
concurrent, concurrent,
} => Request::play(&file_path.to_string_lossy(), concurrent), } => Request::play(&file_path.to_string_lossy(), concurrent),
Actions::ToggleLoop { id } => Request::toggle_loop(id), Actions::ToggleLoop { id } => Request::toggle_loop(id),
Actions::SaveDaemonConfig => Request::save_daemon_config(),
Actions::PlayHotkey { slot } => Request::play_hotkey(&slot), Actions::PlayHotkey { slot } => Request::play_hotkey(&slot),
Actions::ClearHotkey { slot } => Request::clear_hotkey(&slot), Actions::ClearHotkey { slot } => Request::clear_hotkey(&slot),
Actions::ClearHotkeyKey { slot } => Request::clear_hotkey_key(&slot), Actions::ClearHotkeyKey { slot } => Request::clear_hotkey_key(&slot),
@@ -179,6 +184,7 @@ async fn main() -> Result<()> {
GetCommands::Input => Request::get_input(), GetCommands::Input => Request::get_input(),
GetCommands::Inputs => Request::get_inputs(), GetCommands::Inputs => Request::get_inputs(),
GetCommands::DaemonVersion => Request::get_daemon_version(), GetCommands::DaemonVersion => Request::get_daemon_version(),
GetCommands::DaemonConfig => Request::get_daemon_config(),
GetCommands::FullState => Request::get_full_state(), GetCommands::FullState => Request::get_full_state(),
GetCommands::Hotkeys => Request::get_hotkeys(), GetCommands::Hotkeys => Request::get_hotkeys(),
}, },
+8 -8
View File
@@ -9,14 +9,13 @@ use itertools::Itertools;
use pwsp_lib::{ use pwsp_lib::{
types::{ types::{
audio_player::PlayerState, audio_player::PlayerState,
config::GuiConfig, config::{GuiConfig, HotkeyConfig},
config::HotkeyConfig,
gui::{AppState, AudioPlayerState}, gui::{AppState, AudioPlayerState},
socket::Request, socket::Request,
}, },
utils::{ utils::gui::{
daemon::get_daemon_config, get_daemon_config, get_gui_config, make_request_async, make_request_sync,
gui::{get_gui_config, make_request_async, make_request_sync, start_app_state_thread}, start_app_state_thread, update_daemon_config,
}, },
}; };
use rfd::FileDialog; use rfd::FileDialog;
@@ -130,10 +129,11 @@ impl SoundpadGui {
pub fn set_input(&mut self, name: String) { pub fn set_input(&mut self, name: String) {
make_request_async(Request::set_input(&name)); make_request_async(Request::set_input(&name));
if self.config.save_input { if self.config.save_input
let mut daemon_config = get_daemon_config(); && let Ok(mut daemon_config) = get_daemon_config()
{
daemon_config.default_input_name = Some(name); daemon_config.default_input_name = Some(name);
daemon_config.save_to_file().ok(); update_daemon_config(&daemon_config).ok();
} }
} }
+5 -4
View File
@@ -3,7 +3,7 @@ use eframe::{App, Frame as EFrame};
use egui::{CentralPanel, Context, ThemePreference}; use egui::{CentralPanel, Context, ThemePreference};
use pwsp_lib::{ use pwsp_lib::{
types::{config::PreferredTheme, socket::Request}, 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}; use std::time::{Duration, Instant};
@@ -85,10 +85,11 @@ impl App for SoundpadGui {
self.app_state.ignore_volume_update_until = self.app_state.ignore_volume_update_until =
Some(Instant::now() + Duration::from_millis(300)); Some(Instant::now() + Duration::from_millis(300));
if self.config.save_volume { if self.config.save_volume
let mut daemon_config = get_daemon_config(); && let Ok(mut daemon_config) = get_daemon_config()
{
daemon_config.default_volume = Some(self.app_state.volume_slider_value); daemon_config.default_volume = Some(self.app_state.volume_slider_value);
daemon_config.save_to_file().ok(); update_daemon_config(&daemon_config).ok();
} }
} }
+8 -4
View File
@@ -1,7 +1,7 @@
use crate::{ use crate::{
types::pipewire::DeviceType, types::pipewire::DeviceType,
utils::{ utils::{
daemon::get_daemon_config, daemon::with_daemon_config,
pipewire::{PwTerminator, create_link, get_device, link_player_to_virtual_mic}, pipewire::{PwTerminator, create_link, get_device, link_player_to_virtual_mic},
}, },
}; };
@@ -67,8 +67,12 @@ pub struct AudioPlayer {
impl AudioPlayer { impl AudioPlayer {
pub async fn new() -> Result<Self> { pub async fn new() -> Result<Self> {
let daemon_config = get_daemon_config(); let (default_input_name, default_volume) = with_daemon_config(|c| {
let default_volume = daemon_config.default_volume.unwrap_or(1.0); (
c.default_input_name.clone(),
c.default_volume.unwrap_or(1.0),
)
});
let mut audio_player = AudioPlayer { let mut audio_player = AudioPlayer {
stream_handle: None, stream_handle: None,
@@ -77,7 +81,7 @@ impl AudioPlayer {
input_link_sender: None, input_link_sender: None,
player_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, volume: default_volume,
}; };
+43 -2
View File
@@ -1,12 +1,12 @@
use crate::{ use crate::{
types::{ types::{
audio_player::{FullState, PlayerState}, audio_player::{FullState, PlayerState},
config::HotkeyConfig, config::{DaemonConfig, HotkeyConfig},
socket::{Request, Response}, socket::{Request, Response},
}, },
utils::{ utils::{
commands::parse_command, commands::parse_command,
daemon::get_audio_player, daemon::{get_audio_player, with_daemon_config},
pipewire::{get_all_devices, get_device}, pipewire::{get_all_devices, get_device},
}, },
}; };
@@ -127,6 +127,14 @@ pub struct ClearHotkeyKeyCommand {
pub slot: Option<String>, pub slot: Option<String>,
} }
pub struct GetDaemonConfigCommand {}
pub struct SaveDaemonConfigCommand {}
pub struct UpdateDaemonConfigCommand {
pub new_config: DaemonConfig,
}
#[async_trait] #[async_trait]
impl Executable for PingCommand { impl Executable for PingCommand {
async fn execute(&self) -> Response { 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")
}
}
+19
View File
@@ -1,3 +1,4 @@
use crate::types::config::DaemonConfig;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@@ -172,6 +173,24 @@ impl Request {
Request::new("get_daemon_version", vec![]) 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 { pub fn get_full_state() -> Self {
Request::new("get_full_state", vec![]) Request::new("get_full_state", vec![])
} }
+12 -1
View File
@@ -1,4 +1,4 @@
use crate::types::{commands::*, socket::Request}; use crate::types::{commands::*, config::DaemonConfig, socket::Request};
use std::path::PathBuf; use std::path::PathBuf;
@@ -119,6 +119,17 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
key_chord, 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::<DaemonConfig>(nc).ok())
.unwrap_or_default();
Some(Box::new(UpdateDaemonConfigCommand { new_config }))
}
_ => None, _ => None,
} }
} }
+23 -7
View File
@@ -7,36 +7,52 @@ use crate::types::{
use anyhow::Result; use anyhow::Result;
use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt}; use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
use std::path::PathBuf; use std::path::PathBuf;
use std::{env, error::Error, fs}; use std::{
env,
error::Error,
fs,
sync::{Arc, Mutex, OnceLock},
};
use tokio::{ use tokio::{
io::{AsyncReadExt, AsyncWriteExt}, io::{AsyncReadExt, AsyncWriteExt},
net::UnixStream, net::UnixStream,
sync::{Mutex, OnceCell}, sync::{Mutex as AsyncMutex, OnceCell},
time::{Duration, sleep}, time::{Duration, sleep},
}; };
static AUDIO_PLAYER: OnceCell<Mutex<AudioPlayer>> = OnceCell::const_new(); static AUDIO_PLAYER: OnceCell<AsyncMutex<AudioPlayer>> = OnceCell::const_new();
static DAEMON_CONFIG: OnceLock<Arc<Mutex<DaemonConfig>>> = OnceLock::new();
pub async fn get_audio_player() -> Result<&'static Mutex<AudioPlayer>, String> { pub async fn get_audio_player() -> Result<&'static AsyncMutex<AudioPlayer>, String> {
AUDIO_PLAYER AUDIO_PLAYER
.get_or_try_init(|| async { .get_or_try_init(|| async {
println!("Initializing audio player"); println!("Initializing audio player");
match AudioPlayer::new().await { match AudioPlayer::new().await {
Ok(player) => Ok(Mutex::new(player)), Ok(player) => Ok(AsyncMutex::new(player)),
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),
} }
}) })
.await .await
} }
pub fn get_daemon_config() -> DaemonConfig { pub fn get_daemon_config() -> &'static Arc<Mutex<DaemonConfig>> {
DaemonConfig::load_from_file().unwrap_or_else(|_| { DAEMON_CONFIG.get_or_init(|| {
Arc::new(Mutex::new(DaemonConfig::load_from_file().unwrap_or_else(
|_| {
let config = DaemonConfig::default(); let config = DaemonConfig::default();
config.save_to_file().ok(); config.save_to_file().ok();
config config
},
)))
}) })
} }
pub fn with_daemon_config<R>(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 { fn get_current_uid() -> u32 {
rustix::process::geteuid().as_raw() rustix::process::geteuid().as_raw()
} }
+14 -1
View File
@@ -1,7 +1,7 @@
use crate::{ use crate::{
types::{ types::{
audio_player::FullState, audio_player::FullState,
config::{GuiConfig, HotkeyConfig}, config::{DaemonConfig, GuiConfig, HotkeyConfig},
gui::AudioPlayerState, gui::AudioPlayerState,
socket::{Request, Response}, 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<DaemonConfig> {
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<Response> { pub fn make_request_sync(request: Request) -> Result<Response> {
tokio::task::block_in_place(|| { tokio::task::block_in_place(|| {
tokio::runtime::Handle::current() tokio::runtime::Handle::current()