Refactor to Cargo Workspace (#129)

* Refactor project into a Cargo workspace with distinct packages

- Created a root `Cargo.toml` defining a workspace.
- Moved `src/types` and `src/utils` into a new `pwsp-lib` crate for shared logic.
- Split binaries into their own crates: `pwsp-daemon`, `pwsp-cli`, and `pwsp-gui`.
- Shifted all dependencies into `[workspace.dependencies]` for centralized version management.
- Updated import paths across all crates (e.g. from `pwsp::` to `pwsp_lib::`).
- Updated build scripts, GitHub actions, Flatpak manifest, and AUR PKGBUILD to support the new workspace structure.
- Ensured no core application logic was altered.

Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>

* Fix cargo-deb build process in GitHub actions for workspace architecture

Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>

* Fix cargo-deb asset discovery by using exact target/release paths

Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>

* refactor deps in Cargo.toml files

* fix incorrect assets path

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
Tarasov Aleksandr
2026-06-02 21:12:44 +03:00
committed by GitHub
parent 6c59137639
commit 0476329798
47 changed files with 224 additions and 87 deletions
+197
View File
@@ -0,0 +1,197 @@
use anyhow::{Result, anyhow};
use pwsp_lib::{
types::socket::{MAX_MESSAGE_SIZE, Request, Response},
utils::{
commands::parse_command,
daemon::{
create_runtime_dir, get_audio_player, get_daemon_config, get_runtime_dir,
is_daemon_running,
},
global_hotkeys::start_global_hotkey_listener,
pipewire::create_virtual_mic,
},
};
use std::os::unix::fs::PermissionsExt;
use std::{fs, time::Duration};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{UnixListener, UnixStream},
time::sleep,
};
#[tokio::main]
async fn main() -> Result<()> {
create_runtime_dir()?;
if is_daemon_running()? {
return Err(anyhow!("Another instance is already running."));
}
get_daemon_config(); // Initialize daemon config
create_virtual_mic()?;
if let Err(err) = get_audio_player().await {
eprintln!("Failed to initialize audio player: {}", err);
} // Initialize audio player
tokio::spawn(async {
start_global_hotkey_listener().await;
});
let runtime_dir = get_runtime_dir();
let lock_file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(runtime_dir.join("daemon.lock"))?;
lock_file.lock()?;
let socket_path = runtime_dir.join("daemon.sock");
if let Err(e) = fs::remove_file(&socket_path)
&& e.kind() != std::io::ErrorKind::NotFound
{
return Err(e.into());
}
let listener = UnixListener::bind(&socket_path)?;
fs::set_permissions(&socket_path, fs::Permissions::from_mode(0o600))?;
println!(
"Daemon started. Listening on {}",
socket_path.to_str().unwrap_or_default()
);
let commands_loop_handle = tokio::spawn(async {
commands_loop(listener).await.ok();
});
let player_loop_handle = tokio::spawn(async {
player_loop().await;
});
tokio::select! {
_ = commands_loop_handle => {
eprint!("Commands loop was finished, stopping program...");
}
_ = player_loop_handle => {
eprint!("Audio Player loop was finished, stopping program...");
}
}
Ok(())
}
async fn commands_loop(listener: UnixListener) -> Result<()> {
loop {
let (stream, _addr) = listener.accept().await?;
tokio::spawn(async move {
handle_connection(stream).await;
});
}
}
async fn handle_connection(mut stream: UnixStream) {
// ---------- Read request (start) ----------
let mut len_bytes = [0u8; 4];
if stream.read_exact(&mut len_bytes).await.is_err() {
eprintln!("Failed to read message length from client!");
return;
}
let request_len = u32::from_le_bytes(len_bytes) as usize;
if request_len > MAX_MESSAGE_SIZE {
eprintln!(
"Failed to read message from client: request too large ({} bytes)!",
request_len
);
return;
}
let mut buffer = Vec::new();
if (&mut stream)
.take(request_len as u64)
.read_to_end(&mut buffer)
.await
.is_err()
|| buffer.len() != request_len
{
eprintln!("Failed to read message from client!");
return;
}
let request: Request = match serde_json::from_slice(&buffer) {
Ok(req) => req,
Err(err) => {
let response = Response::new(false, format!("Failed to parse request: {}", err));
let response_data = match serde_json::to_vec(&response) {
Ok(data) => data,
Err(_) => return, // Should not happen with this simple Response
};
let response_len = response_data.len() as u32;
let _ = stream.write_all(&response_len.to_le_bytes()).await;
let _ = stream.write_all(&response_data).await;
return;
}
};
// ---------- Read request (end) ----------
// ---------- Generate response (start) ----------
let command = parse_command(&request);
let response: Response;
if let Some(command) = command {
response = command.execute().await;
} else {
response = Response::new(false, "Unknown command");
}
// ---------- Generate response (end) ----------
// ---------- Send response (start) ----------
let response_data = match serde_json::to_vec(&response) {
Ok(data) => data,
Err(err) => {
eprintln!("Failed to serialize response: {}", err);
return;
}
};
let response_len = response_data.len() as u32;
if stream.write_all(&response_len.to_le_bytes()).await.is_err() {
eprintln!("Failed to write response length to client!");
return;
}
if stream.write_all(&response_data).await.is_err() {
eprintln!("Failed to write response to client!");
return;
}
// ---------- Send response (end) ----------
if response.status && response.message.eq("killed") {
std::process::exit(0);
}
}
async fn player_loop() {
let mut device_check_counter: u32 = 0;
loop {
let is_idle = match get_audio_player().await {
Ok(player_mutex) => {
let mut audio_player = player_mutex.lock().await;
let check_devices = device_check_counter == 0;
audio_player.update(check_devices).await;
audio_player.tracks.is_empty()
}
Err(_err) => true,
};
if is_idle {
device_check_counter = 0;
sleep(Duration::from_secs(2)).await;
} else {
// Check devices every ~5 seconds (50 * 100ms) while playing
device_check_counter = (device_check_counter + 1) % 50;
sleep(Duration::from_millis(100)).await;
}
}
}