use std::io::{self, Read, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use waybar_dynamic_core::protocol::IpcMessage;
use waybar_dynamic_core::socket::socket_path;
#[derive(Parser)]
#[command(
name = "waybar-dynamic-ctl",
about = "Control a waybar-dynamic CFFI module instance",
version
)]
struct Cli {
#[arg(short, long, default_value = "waybar-dynamic")]
name: String,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Send {
json: Option<String>,
},
Install {
dest: Option<PathBuf>,
},
}
fn run() -> Result<(), String> {
let cli = Cli::parse();
match cli.command {
Command::Send { json } => cmd_send(&cli.name, json),
Command::Install { dest } => cmd_install(dest),
}
}
fn cmd_send(name: &str, json: Option<String>) -> Result<(), String> {
let raw = match json {
Some(s) => s,
None => {
let mut buf = String::new();
io::stdin()
.read_to_string(&mut buf)
.map_err(|e| format!("failed to read stdin: {e}"))?;
buf
}
};
let msg: IpcMessage =
serde_json::from_str(&raw).map_err(|e| format!("invalid JSON: {e}"))?;
let compact =
serde_json::to_string(&msg).map_err(|e| format!("failed to serialize: {e}"))?;
let path = socket_path(name);
let mut stream = UnixStream::connect(&path)
.map_err(|e| format!("could not connect to {}: {e}", path.display()))?;
stream
.write_all(compact.as_bytes())
.map_err(|e| format!("failed to write: {e}"))?;
stream
.write_all(b"\n")
.map_err(|e| format!("failed to write delimiter: {e}"))?;
stream
.flush()
.map_err(|e| format!("failed to flush: {e}"))?;
Ok(())
}
fn cmd_install(dest: Option<PathBuf>) -> Result<(), String> {
let dest_dir = dest.unwrap_or_else(|| {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".config/waybar/modules")
});
std::fs::create_dir_all(&dest_dir)
.map_err(|e| format!("failed to create {}: {e}", dest_dir.display()))?;
eprintln!("Building libwaybar_dynamic.so (release)...");
let status = std::process::Command::new("cargo")
.args(["build", "--release", "-p", "waybar-dynamic"])
.status()
.map_err(|e| format!("failed to run cargo build: {e}"))?;
if !status.success() {
return Err("cargo build failed".to_string());
}
let so_name = so_filename();
let source = find_so(&so_name)?;
let dest_file = dest_dir.join(&so_name);
std::fs::copy(&source, &dest_file)
.map_err(|e| format!("failed to copy {} → {}: {e}", source.display(), dest_file.display()))?;
eprintln!("Installed {} → {}", so_name, dest_file.display());
Ok(())
}
fn so_filename() -> String {
if cfg!(target_os = "macos") {
"libwaybar_dynamic.dylib".to_string()
} else {
"libwaybar_dynamic.so".to_string()
}
}
fn find_so(so_name: &str) -> Result<PathBuf, String> {
let output = std::process::Command::new("cargo")
.args(["metadata", "--format-version=1", "--no-deps"])
.output()
.map_err(|e| format!("failed to run cargo metadata: {e}"))?;
if output.status.success() {
if let Ok(json) = serde_json::from_slice::<serde_json::Value>(&output.stdout) {
if let Some(target_dir) = json.get("target_directory").and_then(|v| v.as_str()) {
let path = PathBuf::from(target_dir).join("release").join(so_name);
if path.exists() {
return Ok(path);
}
}
}
}
let fallback = PathBuf::from("target/release").join(so_name);
if fallback.exists() {
return Ok(fallback);
}
Err(format!(
"could not find {} in target/release/ — are you in the waybar-dynamic workspace?",
so_name
))
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(msg) => {
eprintln!("error: {msg}");
ExitCode::FAILURE
}
}
}