use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::thread;
use crate::core::command::{Command, OpenTarget};
use crate::core::msg::Msg;
use crate::core::runtime::Mailbox;
pub fn socket_path() -> PathBuf {
let base = std::env::var_os("XDG_RUNTIME_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/tmp"));
base.join("qbrsh").join("ipc.sock")
}
pub fn parse_request(line: &str) -> Result<Command, String> {
let value: serde_json::Value = serde_json::from_str(line).map_err(|e| e.to_string())?;
let method = value
.get("method")
.and_then(|m| m.as_str())
.ok_or("missing method")?;
let params = value.get("params");
match method {
"run_command" => {
let command = params
.and_then(|p| p.get("command"))
.and_then(|c| c.as_str())
.ok_or("run_command needs params.command")?;
Command::parse(command)
}
"open_url" => {
let url = params
.and_then(|p| p.get("url"))
.and_then(|u| u.as_str())
.ok_or("open_url needs params.url")?;
Ok(Command::Open {
target: OpenTarget::Tab,
input: url.to_string(),
})
}
other => Err(format!("unknown method: {other}")),
}
}
pub fn serve(mailbox: Mailbox) {
let path = socket_path();
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if path.exists() && UnixStream::connect(&path).is_err() {
let _ = std::fs::remove_file(&path);
}
let listener = match UnixListener::bind(&path) {
Ok(l) => l,
Err(e) => {
eprintln!("[qbrsh] ipc: cannot bind {}: {e}", path.display());
return;
}
};
thread::spawn(move || {
for stream in listener.incoming().flatten() {
handle_client(stream, &mailbox);
}
});
}
fn handle_client(stream: UnixStream, mailbox: &Mailbox) {
let Ok(read_half) = stream.try_clone() else {
return;
};
let reader = BufReader::new(read_half);
let mut writer = stream;
for line in reader.lines().map_while(Result::ok) {
if line.trim().is_empty() {
continue;
}
let response = match parse_request(&line) {
Ok(cmd) => {
mailbox.send(Msg::Command(cmd));
serde_json::json!({ "ok": true })
}
Err(error) => serde_json::json!({ "error": error }),
};
let _ = writeln!(writer, "{response}");
}
}
pub fn forward_url(url: &str) -> bool {
let Ok(mut stream) = UnixStream::connect(socket_path()) else {
return false;
};
let request = serde_json::json!({ "method": "open_url", "params": { "url": url } });
if writeln!(stream, "{request}").is_err() {
return false;
}
let _ = stream.flush();
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_run_command() {
let cmd =
parse_request(r#"{"method":"run_command","params":{"command":"tabopen https://x"}}"#)
.unwrap();
assert!(matches!(cmd, Command::Open { target: OpenTarget::Tab, .. }));
}
#[test]
fn parses_open_url() {
let cmd = parse_request(r#"{"method":"open_url","params":{"url":"https://a.test"}}"#).unwrap();
assert_eq!(
cmd,
Command::Open {
target: OpenTarget::Tab,
input: "https://a.test".to_string()
}
);
}
#[test]
fn rejects_unknown_method() {
assert!(parse_request(r#"{"method":"nope"}"#).is_err());
}
#[test]
fn rejects_malformed_json() {
assert!(parse_request("not json at all").is_err());
}
#[test]
fn rejects_missing_params() {
assert!(parse_request(r#"{"method":"open_url"}"#).is_err());
}
}