diff --git a/src/app.rs b/src/app.rs index 4d9fbd7..2e9bff4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,26 +14,56 @@ use itertools::Itertools; use std::collections::HashMap; use tokio::sync::mpsc::UnboundedSender; +/// Response that the app may send as a response to a event. +/// This is used in `DaemonCommand`s that contain a response sender. +#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, derive_more::Display)] +pub enum DaemonResponse { + Success(String), + Failure(String), +} + +impl DaemonResponse { + pub fn is_success(&self) -> bool { + match self { + DaemonResponse::Success(_) => true, + _ => false, + } + } + + pub fn is_failure(&self) -> bool { + !self.is_success() + } +} + +pub type DaemonResponseSender = tokio::sync::mpsc::UnboundedSender; +pub type DaemonResponseReceiver = tokio::sync::mpsc::UnboundedReceiver; + #[derive(Debug)] -pub enum EwwCommand { +pub enum DaemonCommand { NoOp, UpdateVars(Vec<(VarName, PrimitiveValue)>), ReloadConfig(config::EwwConfig), ReloadCss(String), - OpenMany(Vec), + OpenMany { + windows: Vec, + sender: DaemonResponseSender, + }, OpenWindow { window_name: WindowName, pos: Option, size: Option, anchor: Option, + sender: DaemonResponseSender, }, CloseWindow { window_name: WindowName, + sender: DaemonResponseSender, }, KillServer, CloseAll, - PrintState(tokio::sync::mpsc::UnboundedSender), - PrintDebug(tokio::sync::mpsc::UnboundedSender), + PrintState(DaemonResponseSender), + PrintDebug(DaemonResponseSender), + PrintWindows(DaemonResponseSender), } #[derive(Debug, Clone, PartialEq)] @@ -55,68 +85,91 @@ pub struct App { pub eww_config: config::EwwConfig, pub windows: HashMap, pub css_provider: gtk::CssProvider, - pub app_evt_send: UnboundedSender, + pub app_evt_send: UnboundedSender, #[debug_stub = "ScriptVarHandler(...)"] pub script_var_handler: ScriptVarHandlerHandle, } impl App { - /// Handle an EwwCommand event. - pub fn handle_command(&mut self, event: EwwCommand) { + /// Handle a DaemonCommand event. + pub fn handle_command(&mut self, event: DaemonCommand) { log::debug!("Handling event: {:?}", &event); let result: Result<_> = try { match event { - EwwCommand::NoOp => {} - EwwCommand::UpdateVars(mappings) => { + DaemonCommand::NoOp => {} + DaemonCommand::UpdateVars(mappings) => { for (var_name, new_value) in mappings { self.update_state(var_name, new_value)?; } } - EwwCommand::ReloadConfig(config) => { + DaemonCommand::ReloadConfig(config) => { self.reload_all_windows(config)?; } - EwwCommand::ReloadCss(css) => { + DaemonCommand::ReloadCss(css) => { self.load_css(&css)?; } - EwwCommand::KillServer => { + DaemonCommand::KillServer => { log::info!("Received kill command, stopping server!"); self.stop_application(); let _ = crate::application_lifecycle::send_exit(); } - EwwCommand::CloseAll => { + DaemonCommand::CloseAll => { log::info!("Received close command, closing all windows"); for (window_name, _window) in self.windows.clone() { self.close_window(&window_name)?; } } - EwwCommand::OpenMany(windows) => { - for window in windows { - self.open_window(&window, None, None, None)?; - } + DaemonCommand::OpenMany { windows, sender } => { + let result = windows + .iter() + .map(|w| self.open_window(w, None, None, None)) + .collect::>(); + respond_with_error(sender, result)?; } - EwwCommand::OpenWindow { + DaemonCommand::OpenWindow { window_name, pos, size, anchor, + sender, } => { - self.open_window(&window_name, pos, size, anchor)?; + let result = self.open_window(&window_name, pos, size, anchor); + respond_with_error(sender, result)?; } - EwwCommand::CloseWindow { window_name } => { - self.close_window(&window_name)?; + DaemonCommand::CloseWindow { window_name, sender } => { + let result = self.close_window(&window_name); + respond_with_error(sender, result)?; } - EwwCommand::PrintState(sender) => { + DaemonCommand::PrintState(sender) => { let output = self .eww_state .get_variables() .iter() .map(|(key, value)| format!("{}: {}", key, value)) .join("\n"); - sender.send(output).context("sending response from main thread")? + sender + .send(DaemonResponse::Success(output)) + .context("sending response from main thread")? } - EwwCommand::PrintDebug(sender) => { + DaemonCommand::PrintWindows(sender) => { + let output = self + .eww_config + .get_windows() + .keys() + .map(|window_name| { + let is_open = self.windows.contains_key(window_name); + format!("{}{}", if is_open { "*" } else { "" }, window_name) + }) + .join("\n"); + sender + .send(DaemonResponse::Success(output)) + .context("sending response from main thread")? + } + DaemonCommand::PrintDebug(sender) => { let output = format!("state: {:#?}\n\nconfig: {:#?}", &self.eww_state, &self.eww_config); - sender.send(output).context("sending response from main thread")? + sender + .send(DaemonResponse::Success(output)) + .context("sending response from main thread")? } } }; @@ -329,3 +382,12 @@ fn get_monitor_geometry(n: i32) -> gdk::Rectangle { .get_default_screen() .get_monitor_geometry(n) } + +/// In case of an Err, send the error message to a sender. +fn respond_with_error(sender: DaemonResponseSender, result: Result) -> Result<()> { + match result { + Ok(_) => sender.send(DaemonResponse::Success(String::new())), + Err(e) => sender.send(DaemonResponse::Failure(format!("{:?}", e))), + } + .context("sending response from main thread") +} diff --git a/src/client.rs b/src/client.rs index d1af291..fb3b758 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,6 +1,9 @@ use std::process::Stdio; -use crate::opts::{self, ActionClientOnly}; +use crate::{ + app, + opts::{self, ActionClientOnly}, +}; use anyhow::*; use std::{ io::{Read, Write}, @@ -20,7 +23,7 @@ pub fn handle_client_only_action(action: ActionClientOnly) -> Result<()> { Ok(()) } -pub fn do_server_call(mut stream: UnixStream, action: opts::ActionWithServer) -> Result> { +pub fn do_server_call(mut stream: UnixStream, action: opts::ActionWithServer) -> Result> { log::info!("Forwarding options to server"); stream .set_nonblocking(false) @@ -36,13 +39,16 @@ pub fn do_server_call(mut stream: UnixStream, action: opts::ActionWithServer) -> .write_all(&message_bytes) .context("Failed to write command to IPC stream")?; - let mut buf = String::new(); + let mut buf = Vec::new(); stream .set_read_timeout(Some(std::time::Duration::from_millis(100))) .context("Failed to set read timeout")?; - stream - .read_to_string(&mut buf) - .context("Error reading response from server")?; + stream.read_to_end(&mut buf).context("Error reading response from server")?; - Ok(if buf.is_empty() { None } else { Some(buf) }) + Ok(if buf.is_empty() { + None + } else { + let buf = bincode::deserialize(&buf)?; + Some(buf) + }) } diff --git a/src/ipc_server.rs b/src/ipc_server.rs index c9e6681..85dd209 100644 --- a/src/ipc_server.rs +++ b/src/ipc_server.rs @@ -6,7 +6,7 @@ use tokio::{ sync::mpsc::*, }; -pub async fn run_server(evt_send: UnboundedSender) -> Result<()> { +pub async fn run_server(evt_send: UnboundedSender) -> Result<()> { let listener = tokio::net::UnixListener::bind(&*crate::IPC_SOCKET_PATH)?; log::info!("IPC server initialized"); crate::loop_select_exiting! { @@ -25,21 +25,22 @@ pub async fn run_server(evt_send: UnboundedSender) -> Result<() } /// Handle a single IPC connection from start to end. -async fn handle_connection(mut stream: tokio::net::UnixStream, evt_send: UnboundedSender) -> Result<()> { +async fn handle_connection(mut stream: tokio::net::UnixStream, evt_send: UnboundedSender) -> Result<()> { let (mut stream_read, mut stream_write) = stream.split(); let action: opts::ActionWithServer = read_action_from_stream(&mut stream_read).await?; log::info!("received command from IPC: {:?}", &action); - let (command, maybe_response_recv) = action.into_eww_command(); + let (command, maybe_response_recv) = action.into_daemon_command(); evt_send.send(command)?; if let Some(mut response_recv) = maybe_response_recv { log::info!("Waiting for response for IPC client"); if let Ok(Some(response)) = tokio::time::timeout(Duration::from_millis(100), response_recv.recv()).await { - let result = &stream_write.write_all(response.as_bytes()).await; + let response = bincode::serialize(&response)?; + let result = &stream_write.write_all(&response).await; crate::print_result_err!("sending text response to ipc client", &result); } } diff --git a/src/main.rs b/src/main.rs index 3dd47c4..9d4fa46 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,6 +69,9 @@ fn main() { client::do_server_call(stream, action).context("Error while forwarding command to server")?; if let Some(response) = response { println!("{}", response); + if response.is_failure() { + std::process::exit(1); + } } } Err(_) => { diff --git a/src/opts.rs b/src/opts.rs index 3908bcb..bd17950 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -99,6 +99,10 @@ pub enum ActionWithServer { #[structopt(name = "state")] ShowState, + /// Print the names of all configured windows. Windows with a * in front of them are currently opened. + #[structopt(name = "windows")] + ShowWindows, + /// Print out the widget structure as seen by eww. /// /// This may be useful if you are facing issues with how eww is interpreting your configuration, @@ -129,38 +133,49 @@ fn parse_var_update_arg(s: &str) -> Result<(VarName, PrimitiveValue)> { } impl ActionWithServer { - pub fn into_eww_command(self) -> (app::EwwCommand, Option>) { + pub fn into_daemon_command(self) -> (app::DaemonCommand, Option) { let command = match self { + ActionWithServer::Update { mappings } => app::DaemonCommand::UpdateVars(mappings.into_iter().collect()), + + ActionWithServer::KillServer => app::DaemonCommand::KillServer, + ActionWithServer::CloseAll => app::DaemonCommand::CloseAll, ActionWithServer::Ping => { let (send, recv) = tokio::sync::mpsc::unbounded_channel(); - let _ = send.send("pong".to_owned()); - return (app::EwwCommand::NoOp, Some(recv)); + let _ = send.send(app::DaemonResponse::Success("pong".to_owned())); + return (app::DaemonCommand::NoOp, Some(recv)); + } + ActionWithServer::OpenMany { windows } => { + return with_response_channel(|sender| app::DaemonCommand::OpenMany { windows, sender }); } - ActionWithServer::Update { mappings } => app::EwwCommand::UpdateVars(mappings.into_iter().collect()), - ActionWithServer::OpenMany { windows } => app::EwwCommand::OpenMany(windows), ActionWithServer::OpenWindow { window_name, pos, size, anchor, - } => app::EwwCommand::OpenWindow { - window_name, - pos, - size, - anchor, - }, - ActionWithServer::CloseWindow { window_name } => app::EwwCommand::CloseWindow { window_name }, - ActionWithServer::KillServer => app::EwwCommand::KillServer, - ActionWithServer::CloseAll => app::EwwCommand::CloseAll, - ActionWithServer::ShowState => { - let (send, recv) = tokio::sync::mpsc::unbounded_channel(); - return (app::EwwCommand::PrintState(send), Some(recv)); + } => { + return with_response_channel(|sender| app::DaemonCommand::OpenWindow { + window_name, + pos, + size, + anchor, + sender, + }) } - ActionWithServer::ShowDebug => { - let (send, recv) = tokio::sync::mpsc::unbounded_channel(); - return (app::EwwCommand::PrintDebug(send), Some(recv)); + ActionWithServer::CloseWindow { window_name } => { + return with_response_channel(|sender| app::DaemonCommand::CloseWindow { window_name, sender }); } + ActionWithServer::ShowWindows => return with_response_channel(app::DaemonCommand::PrintWindows), + ActionWithServer::ShowState => return with_response_channel(app::DaemonCommand::PrintState), + ActionWithServer::ShowDebug => return with_response_channel(app::DaemonCommand::PrintDebug), }; (command, None) } } + +fn with_response_channel(f: F) -> (O, Option>) +where + F: FnOnce(tokio::sync::mpsc::UnboundedSender) -> O, +{ + let (sender, recv) = tokio::sync::mpsc::unbounded_channel(); + (f(sender), Some(recv)) +} diff --git a/src/script_var_handler.rs b/src/script_var_handler.rs index 81eea59..53b4e4d 100644 --- a/src/script_var_handler.rs +++ b/src/script_var_handler.rs @@ -5,7 +5,7 @@ use crate::{ value::{PrimitiveValue, VarName}, }; use anyhow::*; -use app::EwwCommand; +use app::DaemonCommand; use tokio::{ io::{AsyncBufReadExt, BufReader}, @@ -15,7 +15,7 @@ use tokio_util::sync::CancellationToken; /// Initialize the script var handler, and return a handle to that handler, which can be used to control /// the script var execution. -pub fn init(evt_send: UnboundedSender) -> ScriptVarHandlerHandle { +pub fn init(evt_send: UnboundedSender) -> ScriptVarHandlerHandle { let (msg_send, mut msg_recv) = tokio::sync::mpsc::unbounded_channel(); let handle = ScriptVarHandlerHandle { msg_send }; std::thread::spawn(move || { @@ -116,12 +116,12 @@ impl ScriptVarHandler { } struct PollVarHandler { - evt_send: UnboundedSender, + evt_send: UnboundedSender, poll_handles: HashMap, } impl PollVarHandler { - fn new(evt_send: UnboundedSender) -> Result { + fn new(evt_send: UnboundedSender) -> Result { let handler = PollVarHandler { evt_send, poll_handles: HashMap::new(), @@ -139,7 +139,7 @@ impl PollVarHandler { _ = cancellation_token.cancelled() => break, _ = tokio::time::sleep(var.interval) => { let result: Result<_> = try { - evt_send.send(app::EwwCommand::UpdateVars(vec![(var.name.clone(), var.run_once()?)]))?; + evt_send.send(app::DaemonCommand::UpdateVars(vec![(var.name.clone(), var.run_once()?)]))?; }; crate::print_result_err!("while running script-var command", &result); } @@ -166,12 +166,12 @@ impl Drop for PollVarHandler { } struct TailVarHandler { - evt_send: UnboundedSender, + evt_send: UnboundedSender, tail_process_handles: HashMap, } impl TailVarHandler { - fn new(evt_send: UnboundedSender) -> Result { + fn new(evt_send: UnboundedSender) -> Result { let handler = TailVarHandler { evt_send, tail_process_handles: HashMap::new(), @@ -199,7 +199,7 @@ impl TailVarHandler { _ = cancellation_token.cancelled() => break, Ok(Some(line)) = stdout_lines.next_line() => { let new_value = PrimitiveValue::from_string(line.to_owned()); - evt_send.send(EwwCommand::UpdateVars(vec![(var.name.to_owned(), new_value)]))?; + evt_send.send(DaemonCommand::UpdateVars(vec![(var.name.to_owned(), new_value)]))?; } else => break, } diff --git a/src/server.rs b/src/server.rs index 7c14fcc..a1b454a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -67,7 +67,7 @@ pub fn initialize_server() -> Result<()> { Ok(()) } -fn init_async_part(config_file_path: PathBuf, scss_file_path: PathBuf, ui_send: UnboundedSender) { +fn init_async_part(config_file_path: PathBuf, scss_file_path: PathBuf, ui_send: UnboundedSender) { std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -91,7 +91,7 @@ fn init_async_part(config_file_path: PathBuf, scss_file_path: PathBuf, ui_send: let _ = crate::application_lifecycle::recv_exit().await; log::info!("Forward task received exit event"); // Then forward that to the application - let _ = ui_send.send(app::EwwCommand::KillServer); + let _ = ui_send.send(app::DaemonCommand::KillServer); }) }; @@ -108,7 +108,7 @@ fn init_async_part(config_file_path: PathBuf, scss_file_path: PathBuf, ui_send: async fn run_filewatch>( config_file_path: P, scss_file_path: P, - evt_send: UnboundedSender, + evt_send: UnboundedSender, ) -> Result<()> { let mut inotify = inotify::Inotify::init().context("Failed to initialize inotify")?; let config_file_descriptor = inotify @@ -127,11 +127,11 @@ async fn run_filewatch>( if event.wd == config_file_descriptor { log::info!("Reloading eww configuration"); let new_eww_config = config::EwwConfig::read_from_file(config_file_path.as_ref())?; - evt_send.send(app::EwwCommand::ReloadConfig(new_eww_config))?; + evt_send.send(app::DaemonCommand::ReloadConfig(new_eww_config))?; } else if event.wd == scss_file_descriptor { log::info!("reloading eww css file"); let eww_css = crate::util::parse_scss_from_file(scss_file_path.as_ref())?; - evt_send.send(app::EwwCommand::ReloadCss(eww_css))?; + evt_send.send(app::DaemonCommand::ReloadCss(eww_css))?; } else { eprintln!("Got inotify event for unknown thing: {:?}", event); }