mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-07-26 05:46:59 +00:00
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:
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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![])
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user