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)]
id: Option<u32>,
},
/// 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(),
},
+8 -8
View File
@@ -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();
}
}
+5 -4
View File
@@ -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();
}
}
+8 -4
View File
@@ -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<Self> {
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,
};
+43 -2
View File
@@ -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<String>,
}
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")
}
}
+19
View File
@@ -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![])
}
+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;
@@ -119,6 +119,17 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
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,
}
}
+26 -10
View File
@@ -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<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
.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<Mutex<DaemonConfig>> {
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<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 {
rustix::process::geteuid().as_raw()
}
+14 -1
View File
@@ -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<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> {
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current()