Add eww windows command and restructure daemon response.

Daemon response is now a dedicated type that contains information
about failure / success, and used in more commands, such as `open` and
`close`. Thus, this fixes #90 and also fixes #89.

For #90, the decision has been made to not make this independent of the
daemon, as that makes it possible to provide more information, and
because in most cases, users will have the daemon running when checking
windows anyways.
This commit is contained in:
elkowar 2021-01-16 18:03:19 +01:00
parent ff756da2c5
commit 8530f90a7c
7 changed files with 156 additions and 69 deletions

View file

@ -14,26 +14,56 @@ use itertools::Itertools;
use std::collections::HashMap; use std::collections::HashMap;
use tokio::sync::mpsc::UnboundedSender; 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<DaemonResponse>;
pub type DaemonResponseReceiver = tokio::sync::mpsc::UnboundedReceiver<DaemonResponse>;
#[derive(Debug)] #[derive(Debug)]
pub enum EwwCommand { pub enum DaemonCommand {
NoOp, NoOp,
UpdateVars(Vec<(VarName, PrimitiveValue)>), UpdateVars(Vec<(VarName, PrimitiveValue)>),
ReloadConfig(config::EwwConfig), ReloadConfig(config::EwwConfig),
ReloadCss(String), ReloadCss(String),
OpenMany(Vec<WindowName>), OpenMany {
windows: Vec<WindowName>,
sender: DaemonResponseSender,
},
OpenWindow { OpenWindow {
window_name: WindowName, window_name: WindowName,
pos: Option<Coords>, pos: Option<Coords>,
size: Option<Coords>, size: Option<Coords>,
anchor: Option<AnchorPoint>, anchor: Option<AnchorPoint>,
sender: DaemonResponseSender,
}, },
CloseWindow { CloseWindow {
window_name: WindowName, window_name: WindowName,
sender: DaemonResponseSender,
}, },
KillServer, KillServer,
CloseAll, CloseAll,
PrintState(tokio::sync::mpsc::UnboundedSender<String>), PrintState(DaemonResponseSender),
PrintDebug(tokio::sync::mpsc::UnboundedSender<String>), PrintDebug(DaemonResponseSender),
PrintWindows(DaemonResponseSender),
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -55,68 +85,91 @@ pub struct App {
pub eww_config: config::EwwConfig, pub eww_config: config::EwwConfig,
pub windows: HashMap<WindowName, EwwWindow>, pub windows: HashMap<WindowName, EwwWindow>,
pub css_provider: gtk::CssProvider, pub css_provider: gtk::CssProvider,
pub app_evt_send: UnboundedSender<EwwCommand>, pub app_evt_send: UnboundedSender<DaemonCommand>,
#[debug_stub = "ScriptVarHandler(...)"] #[debug_stub = "ScriptVarHandler(...)"]
pub script_var_handler: ScriptVarHandlerHandle, pub script_var_handler: ScriptVarHandlerHandle,
} }
impl App { impl App {
/// Handle an EwwCommand event. /// Handle a DaemonCommand event.
pub fn handle_command(&mut self, event: EwwCommand) { pub fn handle_command(&mut self, event: DaemonCommand) {
log::debug!("Handling event: {:?}", &event); log::debug!("Handling event: {:?}", &event);
let result: Result<_> = try { let result: Result<_> = try {
match event { match event {
EwwCommand::NoOp => {} DaemonCommand::NoOp => {}
EwwCommand::UpdateVars(mappings) => { DaemonCommand::UpdateVars(mappings) => {
for (var_name, new_value) in mappings { for (var_name, new_value) in mappings {
self.update_state(var_name, new_value)?; self.update_state(var_name, new_value)?;
} }
} }
EwwCommand::ReloadConfig(config) => { DaemonCommand::ReloadConfig(config) => {
self.reload_all_windows(config)?; self.reload_all_windows(config)?;
} }
EwwCommand::ReloadCss(css) => { DaemonCommand::ReloadCss(css) => {
self.load_css(&css)?; self.load_css(&css)?;
} }
EwwCommand::KillServer => { DaemonCommand::KillServer => {
log::info!("Received kill command, stopping server!"); log::info!("Received kill command, stopping server!");
self.stop_application(); self.stop_application();
let _ = crate::application_lifecycle::send_exit(); let _ = crate::application_lifecycle::send_exit();
} }
EwwCommand::CloseAll => { DaemonCommand::CloseAll => {
log::info!("Received close command, closing all windows"); log::info!("Received close command, closing all windows");
for (window_name, _window) in self.windows.clone() { for (window_name, _window) in self.windows.clone() {
self.close_window(&window_name)?; self.close_window(&window_name)?;
} }
} }
EwwCommand::OpenMany(windows) => { DaemonCommand::OpenMany { windows, sender } => {
for window in windows { let result = windows
self.open_window(&window, None, None, None)?; .iter()
} .map(|w| self.open_window(w, None, None, None))
.collect::<Result<()>>();
respond_with_error(sender, result)?;
} }
EwwCommand::OpenWindow { DaemonCommand::OpenWindow {
window_name, window_name,
pos, pos,
size, size,
anchor, 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 } => { DaemonCommand::CloseWindow { window_name, sender } => {
self.close_window(&window_name)?; let result = self.close_window(&window_name);
respond_with_error(sender, result)?;
} }
EwwCommand::PrintState(sender) => { DaemonCommand::PrintState(sender) => {
let output = self let output = self
.eww_state .eww_state
.get_variables() .get_variables()
.iter() .iter()
.map(|(key, value)| format!("{}: {}", key, value)) .map(|(key, value)| format!("{}: {}", key, value))
.join("\n"); .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); 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_default_screen()
.get_monitor_geometry(n) .get_monitor_geometry(n)
} }
/// In case of an Err, send the error message to a sender.
fn respond_with_error<T>(sender: DaemonResponseSender, result: Result<T>) -> Result<()> {
match result {
Ok(_) => sender.send(DaemonResponse::Success(String::new())),
Err(e) => sender.send(DaemonResponse::Failure(format!("{:?}", e))),
}
.context("sending response from main thread")
}

View file

@ -1,6 +1,9 @@
use std::process::Stdio; use std::process::Stdio;
use crate::opts::{self, ActionClientOnly}; use crate::{
app,
opts::{self, ActionClientOnly},
};
use anyhow::*; use anyhow::*;
use std::{ use std::{
io::{Read, Write}, io::{Read, Write},
@ -20,7 +23,7 @@ pub fn handle_client_only_action(action: ActionClientOnly) -> Result<()> {
Ok(()) Ok(())
} }
pub fn do_server_call(mut stream: UnixStream, action: opts::ActionWithServer) -> Result<Option<String>> { pub fn do_server_call(mut stream: UnixStream, action: opts::ActionWithServer) -> Result<Option<app::DaemonResponse>> {
log::info!("Forwarding options to server"); log::info!("Forwarding options to server");
stream stream
.set_nonblocking(false) .set_nonblocking(false)
@ -36,13 +39,16 @@ pub fn do_server_call(mut stream: UnixStream, action: opts::ActionWithServer) ->
.write_all(&message_bytes) .write_all(&message_bytes)
.context("Failed to write command to IPC stream")?; .context("Failed to write command to IPC stream")?;
let mut buf = String::new(); let mut buf = Vec::new();
stream stream
.set_read_timeout(Some(std::time::Duration::from_millis(100))) .set_read_timeout(Some(std::time::Duration::from_millis(100)))
.context("Failed to set read timeout")?; .context("Failed to set read timeout")?;
stream stream.read_to_end(&mut buf).context("Error reading response from server")?;
.read_to_string(&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)
})
} }

View file

@ -6,7 +6,7 @@ use tokio::{
sync::mpsc::*, sync::mpsc::*,
}; };
pub async fn run_server(evt_send: UnboundedSender<app::EwwCommand>) -> Result<()> { pub async fn run_server(evt_send: UnboundedSender<app::DaemonCommand>) -> Result<()> {
let listener = tokio::net::UnixListener::bind(&*crate::IPC_SOCKET_PATH)?; let listener = tokio::net::UnixListener::bind(&*crate::IPC_SOCKET_PATH)?;
log::info!("IPC server initialized"); log::info!("IPC server initialized");
crate::loop_select_exiting! { crate::loop_select_exiting! {
@ -25,21 +25,22 @@ pub async fn run_server(evt_send: UnboundedSender<app::EwwCommand>) -> Result<()
} }
/// Handle a single IPC connection from start to end. /// Handle a single IPC connection from start to end.
async fn handle_connection(mut stream: tokio::net::UnixStream, evt_send: UnboundedSender<app::EwwCommand>) -> Result<()> { async fn handle_connection(mut stream: tokio::net::UnixStream, evt_send: UnboundedSender<app::DaemonCommand>) -> Result<()> {
let (mut stream_read, mut stream_write) = stream.split(); let (mut stream_read, mut stream_write) = stream.split();
let action: opts::ActionWithServer = read_action_from_stream(&mut stream_read).await?; let action: opts::ActionWithServer = read_action_from_stream(&mut stream_read).await?;
log::info!("received command from IPC: {:?}", &action); 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)?; evt_send.send(command)?;
if let Some(mut response_recv) = maybe_response_recv { if let Some(mut response_recv) = maybe_response_recv {
log::info!("Waiting for response for IPC client"); log::info!("Waiting for response for IPC client");
if let Ok(Some(response)) = tokio::time::timeout(Duration::from_millis(100), response_recv.recv()).await { 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); crate::print_result_err!("sending text response to ipc client", &result);
} }
} }

View file

@ -69,6 +69,9 @@ fn main() {
client::do_server_call(stream, action).context("Error while forwarding command to server")?; client::do_server_call(stream, action).context("Error while forwarding command to server")?;
if let Some(response) = response { if let Some(response) = response {
println!("{}", response); println!("{}", response);
if response.is_failure() {
std::process::exit(1);
}
} }
} }
Err(_) => { Err(_) => {

View file

@ -99,6 +99,10 @@ pub enum ActionWithServer {
#[structopt(name = "state")] #[structopt(name = "state")]
ShowState, 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. /// 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, /// 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 { impl ActionWithServer {
pub fn into_eww_command(self) -> (app::EwwCommand, Option<tokio::sync::mpsc::UnboundedReceiver<String>>) { pub fn into_daemon_command(self) -> (app::DaemonCommand, Option<app::DaemonResponseReceiver>) {
let command = match self { 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 => { ActionWithServer::Ping => {
let (send, recv) = tokio::sync::mpsc::unbounded_channel(); let (send, recv) = tokio::sync::mpsc::unbounded_channel();
let _ = send.send("pong".to_owned()); let _ = send.send(app::DaemonResponse::Success("pong".to_owned()));
return (app::EwwCommand::NoOp, Some(recv)); 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 { ActionWithServer::OpenWindow {
window_name, window_name,
pos, pos,
size, size,
anchor, anchor,
} => app::EwwCommand::OpenWindow { } => {
window_name, return with_response_channel(|sender| app::DaemonCommand::OpenWindow {
pos, window_name,
size, pos,
anchor, size,
}, anchor,
ActionWithServer::CloseWindow { window_name } => app::EwwCommand::CloseWindow { window_name }, sender,
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));
} }
ActionWithServer::ShowDebug => { ActionWithServer::CloseWindow { window_name } => {
let (send, recv) = tokio::sync::mpsc::unbounded_channel(); return with_response_channel(|sender| app::DaemonCommand::CloseWindow { window_name, sender });
return (app::EwwCommand::PrintDebug(send), Some(recv));
} }
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) (command, None)
} }
} }
fn with_response_channel<T, O, F>(f: F) -> (O, Option<tokio::sync::mpsc::UnboundedReceiver<T>>)
where
F: FnOnce(tokio::sync::mpsc::UnboundedSender<T>) -> O,
{
let (sender, recv) = tokio::sync::mpsc::unbounded_channel();
(f(sender), Some(recv))
}

View file

@ -5,7 +5,7 @@ use crate::{
value::{PrimitiveValue, VarName}, value::{PrimitiveValue, VarName},
}; };
use anyhow::*; use anyhow::*;
use app::EwwCommand; use app::DaemonCommand;
use tokio::{ use tokio::{
io::{AsyncBufReadExt, BufReader}, 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 /// Initialize the script var handler, and return a handle to that handler, which can be used to control
/// the script var execution. /// the script var execution.
pub fn init(evt_send: UnboundedSender<EwwCommand>) -> ScriptVarHandlerHandle { pub fn init(evt_send: UnboundedSender<DaemonCommand>) -> ScriptVarHandlerHandle {
let (msg_send, mut msg_recv) = tokio::sync::mpsc::unbounded_channel(); let (msg_send, mut msg_recv) = tokio::sync::mpsc::unbounded_channel();
let handle = ScriptVarHandlerHandle { msg_send }; let handle = ScriptVarHandlerHandle { msg_send };
std::thread::spawn(move || { std::thread::spawn(move || {
@ -116,12 +116,12 @@ impl ScriptVarHandler {
} }
struct PollVarHandler { struct PollVarHandler {
evt_send: UnboundedSender<EwwCommand>, evt_send: UnboundedSender<DaemonCommand>,
poll_handles: HashMap<VarName, CancellationToken>, poll_handles: HashMap<VarName, CancellationToken>,
} }
impl PollVarHandler { impl PollVarHandler {
fn new(evt_send: UnboundedSender<EwwCommand>) -> Result<Self> { fn new(evt_send: UnboundedSender<DaemonCommand>) -> Result<Self> {
let handler = PollVarHandler { let handler = PollVarHandler {
evt_send, evt_send,
poll_handles: HashMap::new(), poll_handles: HashMap::new(),
@ -139,7 +139,7 @@ impl PollVarHandler {
_ = cancellation_token.cancelled() => break, _ = cancellation_token.cancelled() => break,
_ = tokio::time::sleep(var.interval) => { _ = tokio::time::sleep(var.interval) => {
let result: Result<_> = try { 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); crate::print_result_err!("while running script-var command", &result);
} }
@ -166,12 +166,12 @@ impl Drop for PollVarHandler {
} }
struct TailVarHandler { struct TailVarHandler {
evt_send: UnboundedSender<EwwCommand>, evt_send: UnboundedSender<DaemonCommand>,
tail_process_handles: HashMap<VarName, CancellationToken>, tail_process_handles: HashMap<VarName, CancellationToken>,
} }
impl TailVarHandler { impl TailVarHandler {
fn new(evt_send: UnboundedSender<EwwCommand>) -> Result<Self> { fn new(evt_send: UnboundedSender<DaemonCommand>) -> Result<Self> {
let handler = TailVarHandler { let handler = TailVarHandler {
evt_send, evt_send,
tail_process_handles: HashMap::new(), tail_process_handles: HashMap::new(),
@ -199,7 +199,7 @@ impl TailVarHandler {
_ = cancellation_token.cancelled() => break, _ = cancellation_token.cancelled() => break,
Ok(Some(line)) = stdout_lines.next_line() => { Ok(Some(line)) = stdout_lines.next_line() => {
let new_value = PrimitiveValue::from_string(line.to_owned()); 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, else => break,
} }

View file

@ -67,7 +67,7 @@ pub fn initialize_server() -> Result<()> {
Ok(()) Ok(())
} }
fn init_async_part(config_file_path: PathBuf, scss_file_path: PathBuf, ui_send: UnboundedSender<app::EwwCommand>) { fn init_async_part(config_file_path: PathBuf, scss_file_path: PathBuf, ui_send: UnboundedSender<app::DaemonCommand>) {
std::thread::spawn(move || { std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_multi_thread() let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all() .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; let _ = crate::application_lifecycle::recv_exit().await;
log::info!("Forward task received exit event"); log::info!("Forward task received exit event");
// Then forward that to the application // 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<P: AsRef<Path>>( async fn run_filewatch<P: AsRef<Path>>(
config_file_path: P, config_file_path: P,
scss_file_path: P, scss_file_path: P,
evt_send: UnboundedSender<app::EwwCommand>, evt_send: UnboundedSender<app::DaemonCommand>,
) -> Result<()> { ) -> Result<()> {
let mut inotify = inotify::Inotify::init().context("Failed to initialize inotify")?; let mut inotify = inotify::Inotify::init().context("Failed to initialize inotify")?;
let config_file_descriptor = inotify let config_file_descriptor = inotify
@ -127,11 +127,11 @@ async fn run_filewatch<P: AsRef<Path>>(
if event.wd == config_file_descriptor { if event.wd == config_file_descriptor {
log::info!("Reloading eww configuration"); log::info!("Reloading eww configuration");
let new_eww_config = config::EwwConfig::read_from_file(config_file_path.as_ref())?; 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 { } else if event.wd == scss_file_descriptor {
log::info!("reloading eww css file"); log::info!("reloading eww css file");
let eww_css = crate::util::parse_scss_from_file(scss_file_path.as_ref())?; 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 { } else {
eprintln!("Got inotify event for unknown thing: {:?}", event); eprintln!("Got inotify event for unknown thing: {:?}", event);
} }