diff --git a/docs/Controlling Ironbar.md b/docs/Controlling Ironbar.md index f8f5eeb..9eb3613 100644 --- a/docs/Controlling Ironbar.md +++ b/docs/Controlling Ironbar.md @@ -5,28 +5,38 @@ It also includes a command line interface, which can be used for interacting wit # CLI This is shipped as part of the `ironbar` binary. To view commands, you can use `ironbar --help`. -You can also view help per-command, for example using `ironbar set --help`. +You can also view help per sub-command or command, for example using `ironbar var --help` or `ironbar var set --help`. -Responses are handled by writing their type to stdout, followed by any value starting on the next line. -Error responses are written to stderr in the same format. +The CLI supports plaintext and JSON output. Plaintext will: + +- Print `ok` for empty success responses +- Print the returned body for success responses +- Print `error` to followed by the error on the next line for error responses. This is printed to `stderr`. Example: ```shell -$ ironbar set subject world +$ ironbar var set subject world ok -$ ironbar get subject -ok +$ ironbar var get subject world + +$ ironbar var get foo +error +Variable not found ``` +All error responses will cause the CLI to exit code 3. + # IPC The server listens on a Unix socket. -This can usually be found at `/run/user/$UID/ironbar-ipc.sock`. +The path is printed on startup, and can usually be found at `/run/user/$UID/ironbar-ipc.sock`. -Commands and responses are sent as JSON objects, denoted by their `type` key. +Commands and responses are sent as JSON objects. + +Commands will have a `command` key, and a `subcommand` key when part of a sub-command. The message buffer is currently limited to `1024` bytes. Particularly large messages will be truncated or cause an error. @@ -47,7 +57,7 @@ Responds with `ok`. ```json { - "type": "ping" + "command": "ping" } ``` @@ -59,7 +69,7 @@ Responds with `ok`. ```json { - "type": "inspect" + "command": "inspect" } ``` @@ -73,48 +83,7 @@ Responds with `ok`. ```json { - "type": "reload" -} -``` - -### `get` - -Gets an [ironvar](ironvars) value. - -Responds with `ok_value` if the value exists, otherwise `error`. - -```json -{ - "type": "get", - "key": "foo" -} -``` - -### `set` - -Sets an [ironvar](ironvars) value. - -Responds with `ok`. - -```json -{ - "type": "set", - "key": "foo", - "value": "bar" -} -``` - -### list - -Gets a list of all [ironvar](ironvars) values. - -Responds with `ok_value`. - -Each key/value pair is on its own `\n` separated newline. The key and value are separated by a colon and space `: `. - -```json -{ - "type": "list" + "command": "reload" } ``` @@ -126,26 +95,113 @@ Responds with `ok` if the stylesheet exists, otherwise `error`. ```json { - "type": "load_css", + "command": "load_css", "path": "/path/to/style.css" } ``` -### `set_visible` +### `var` -Sets a bar's visibility. +Subcommand for controlling Ironvars. + +#### `get` + +Gets an [ironvar](ironvars) value. + +Responds with `ok_value` if the value exists, otherwise `error`. + +```json +{ + "command": "var", + "subcommand": "get", + "key": "foo" +} +``` + +#### `set` + +Sets an [ironvar](ironvars) value. + +Responds with `ok`. + +```json +{ + "command": "var", + "subcommand": "set", + "key": "foo", + "value": "bar" +} +``` + +#### `list` + +Gets a list of all [ironvar](ironvars) values. + +Responds with `ok_value`. + +Each key/value pair is on its own `\n` separated newline. The key and value are separated by a colon and space `: `. + +```json +{ + "command": "var", + "subcommand": "list" +} +``` + +### `bar` + +#### `show` + +Forces a bar to be shown, regardless of the current visibility state. + +```json +{ + "command": "bar", + "subcommand": "show", + "name": "bar-123" +} +``` + +#### `hide` + +Forces a bar to be hidden, regardless of the current visibility state. + +```json +{ + "command": "bar", + "subcommand": "hide", + "name": "bar-123" +} +``` + +#### `set_visible` + +Sets a bar's visibility to one of shown/hidden. Responds with `ok` if the bar exists, otherwise `error`. ```json { - "type": "set_visible", - "bar_name": "bar-123", + "command": "bar", + "subcommand": "set_visible", + "name": "bar-123", "visible": true } ``` -### `get_visible` +#### `toggle_visible` + +Toggles the current visibility state of a bar between shown and hidden. + +```json +{ + "command": "bar", + "subcommand": "toggle_visible", + "name": "bar-123" +} +``` + +#### `get_visible` Gets a bar's visibility. @@ -153,50 +209,82 @@ Responds with `ok_value` and the visibility (`true`/`false`) if the bar exists, ```json { - "type": "get_visible", - "bar_name": "bar-123" + "command": "bar", + "subcommand": "get_visible", + "name": "bar-123" } ``` -### `toggle_popup` - -Toggles the open/closed state for a module's popup. -Since each bar only has a single popup, any open popup on the bar is closed. - -Responds with `ok` if the popup exists, otherwise `error`. - -```json -{ - "type": "toggle_popup", - "bar_name": "bar-123", - "name": "clock" -} -``` - -### `open_popup` +#### `show_popup` Sets a module's popup open, regardless of its current state. Since each bar only has a single popup, any open popup on the bar is closed. -Responds with `ok` if the popup exists, otherwise `error`. +Responds with `ok` if the bar and widget exist, otherwise `error`. ```json { - "type": "open_popup", - "bar_name": "bar-123", - "name": "clock" + "command": "bar", + "subcommand": "show_popup", + "name": "bar-123", + "widget_name": "clock" } ``` -### `close_popup` +#### `hide_popup` Sets the popup on a bar closed, regardless of which module it is open for. -Responds with `ok` if the popup exists, otherwise `error`. +Responds with `ok` if the bar and widget exist, otherwise `error`. ```json { - "type": "close_popup", + "command": "bar", + "subcommand": "hide_popup", + "bar_name": "bar-123" +} +``` + +#### `set_popup_visible` + +Sets a popup's visibility to one of shown/hidden. + +Responds with `ok` if the bar and widget exist, otherwise `error`. + +```json +{ + "command": "bar", + "subcommand": "set_popup_visible", + "name": "bar-123", + "widget_name": "clock", + "visible": true +} +``` + +#### `toggle_popup` + +Toggles the open/closed state for a module's popup. +Since each bar only has a single popup, any open popup on the bar is closed. + +Responds with `ok` if the bar and widget exist, otherwise `error`. + +```json +{ + "command": "bar", + "subcommand": "toggle_popup", + "bar_name": "bar-123", + "widget_name": "clock" +} +``` + +#### `get_popup_visible` + +Gets the popup's current visibility state. + +```json +{ + "command": "bar", + "subcommand": "get_popup_visible", "bar_name": "bar-123" } ``` diff --git a/src/bar.rs b/src/bar.rs index 4363806..99f4b04 100644 --- a/src/bar.rs +++ b/src/bar.rs @@ -320,6 +320,15 @@ impl Bar { Inner::Loaded { popup } => popup.clone(), } } + + pub fn visible(&self) -> bool { + self.window.is_visible() + } + + /// Sets the window visibility status + pub fn set_visible(&self, visible: bool) { + self.window.set_visible(visible) + } } /// Creates a `gtk::Box` container to place widgets inside. diff --git a/src/cli.rs b/src/cli.rs index 0938753..769d706 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,9 @@ +use crate::error::ExitCode; use crate::ipc::commands::Command; use crate::ipc::responses::Response; -use clap::Parser; +use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; +use std::process::exit; #[derive(Parser, Debug, Serialize, Deserialize)] #[command(version)] @@ -15,16 +17,44 @@ pub struct Args { #[arg(long("print-schema"))] pub print_schema: bool, + /// Print debug information to stderr + /// TODO: Make bar follow this too + #[arg(long)] + pub debug: bool, + + /// Format to output the response as. + #[arg(short, long)] + pub format: Option, + /// `bar_id` argument passed by `swaybar_command`. /// Not used. #[arg(short('b'), hide(true))] sway_bar_id: Option, } -pub fn handle_response(response: Response) { - match response { - Response::Ok => println!("ok"), - Response::OkValue { value } => println!("ok\n{value}"), - Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()), +#[derive(Debug, Serialize, Deserialize, Default, ValueEnum, Clone, Copy)] +pub enum Format { + #[default] + Plain, + Json, +} + +pub fn handle_response(response: Response, format: Format) { + let is_err = matches!(response, Response::Err { .. }); + + match format { + Format::Plain => match response { + Response::Ok => println!("ok"), + Response::OkValue { value } => println!("{value}"), + Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()), + }, + Format::Json => println!( + "{}", + serde_json::to_string(&response).expect("to be valid json") + ), + } + + if is_err { + exit(ExitCode::IpcResponseError as i32) } } diff --git a/src/error.rs b/src/error.rs index 7ceb6b3..66f30cd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,7 @@ pub enum ExitCode { GtkDisplay = 1, CreateBars = 2, + IpcResponseError = 3, } pub const ERR_MUTEX_LOCK: &str = "Failed to get lock on Mutex"; diff --git a/src/ipc/client.rs b/src/ipc/client.rs index b7e1da9..820ace5 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -8,7 +8,7 @@ use tokio::net::UnixStream; impl Ipc { /// Sends a command to the IPC server. /// The server response is returned. - pub async fn send(&self, command: Command) -> Result { + pub async fn send(&self, command: Command, debug: bool) -> Result { let mut stream = match UnixStream::connect(&self.path).await { Ok(stream) => Ok(stream), Err(err) => Err(Report::new(err) @@ -17,6 +17,11 @@ impl Ipc { }?; let write_buffer = serde_json::to_vec(&command)?; + + if debug { + eprintln!("REQUEST JSON: {}", serde_json::to_string(&command)?); + } + stream.write_all(&write_buffer).await?; let mut read_buffer = vec![0; 1024]; diff --git a/src/ipc/commands.rs b/src/ipc/commands.rs index a080ff8..668519e 100644 --- a/src/ipc/commands.rs +++ b/src/ipc/commands.rs @@ -1,20 +1,39 @@ +use clap::ArgAction; use std::path::PathBuf; -use clap::Subcommand; +use clap::{Args, Subcommand}; use serde::{Deserialize, Serialize}; #[derive(Subcommand, Debug, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] +#[serde(tag = "command", rename_all = "snake_case")] pub enum Command { - /// Return "ok" + /// Pong Ping, - /// Open the GTK inspector + /// Open the GTK inspector. Inspect, - /// Reload the config + /// Reload the config. Reload, + /// Load an additional CSS stylesheet. + /// The sheet is automatically hot-reloaded. + LoadCss { + /// The path to the sheet. + path: PathBuf, + }, + + /// Get and set reactive Ironvar values. + #[command(subcommand)] + Var(IronvarCommand), + + /// Interact with a specific bar. + Bar(BarCommand), +} + +#[derive(Subcommand, Debug, Serialize, Deserialize)] +#[serde(tag = "subcommand", rename_all = "snake_case")] +pub enum IronvarCommand { /// Set an `ironvar` value. /// This creates it if it does not already exist, and updates it if it does. /// Any references to this variable are automatically and immediately updated. @@ -34,49 +53,69 @@ pub enum Command { /// Gets the current value of all `ironvar`s. List, +} - /// Load an additional CSS stylesheet. - /// The sheet is automatically hot-reloaded. - LoadCss { - /// The path to the sheet. - path: PathBuf, - }, +#[derive(Args, Debug, Serialize, Deserialize)] +pub struct BarCommand { + /// The name of the bar. + pub name: String, - /// Set the visibility of the bar with the given name. + #[command(subcommand)] + #[serde(flatten)] + pub subcommand: BarCommandType, +} + +#[derive(Subcommand, Debug, Serialize, Deserialize)] +#[serde(tag = "subcommand", rename_all = "snake_case")] +pub enum BarCommandType { + // == Visibility == \\ + /// Force the bar to be shown, regardless of current visibility state. + Show, + /// Force the bar to be hidden, regardless of current visibility state. + Hide, + /// Set the bar's visibility state via an argument. SetVisible { - ///Bar name to target. - bar_name: String, - /// The visibility status. - #[arg(short, long)] + /// The new visibility state. + #[clap( + num_args(1), + require_equals(true), + action = ArgAction::Set, + )] visible: bool, }, + /// Toggle the current visibility state between shown and hidden. + ToggleVisible, + /// Get the bar's visibility state. + GetVisible, - /// Get the visibility of the bar with the given name. - GetVisible { - /// Bar name to target. - bar_name: String, + // == Popup visibility == \\ + /// Open a popup, regardless of current state. + /// If opening this popup, and a different popup on the same bar is already open, the other is closed. + ShowPopup { + /// The configured name of the widget. + widget_name: String, }, + /// Close a popup, regardless of current state. + HidePopup, + /// Set the popup's visibility state via an argument. + /// If opening this popup, and a different popup on the same bar is already open, the other is closed. + SetPopupVisible { + /// The configured name of the widget. + widget_name: String, + #[clap( + num_args(1), + require_equals(true), + action = ArgAction::Set, + )] + visible: bool, + }, /// Toggle a popup open/closed. /// If opening this popup, and a different popup on the same bar is already open, the other is closed. TogglePopup { - /// The name of the monitor the bar is located on. - bar_name: String, - /// The name of the widget. - name: String, - }, - - /// Open a popup, regardless of current state. - OpenPopup { - /// The name of the monitor the bar is located on. - bar_name: String, - /// The name of the widget. - name: String, - }, - - /// Close a popup, regardless of current state. - ClosePopup { - /// The name of the monitor the bar is located on. - bar_name: String, + /// The configured name of the widget. + widget_name: String, }, + /// Get the popup's current visibility state. + GetPopupVisible, } diff --git a/src/ipc/mod.rs b/src/ipc/mod.rs index efb6ce9..9f58a6b 100644 --- a/src/ipc/mod.rs +++ b/src/ipc/mod.rs @@ -6,7 +6,7 @@ mod server; use std::path::{Path, PathBuf}; use tracing::warn; -pub use commands::Command; +pub use commands::*; pub use responses::Response; #[derive(Debug)] diff --git a/src/ipc/server.rs b/src/ipc/server.rs deleted file mode 100644 index 521545b..0000000 --- a/src/ipc/server.rs +++ /dev/null @@ -1,297 +0,0 @@ -use std::fs; -use std::path::Path; -use std::rc::Rc; - -use color_eyre::{Report, Result}; -use gtk::prelude::*; -use gtk::Application; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::{UnixListener, UnixStream}; -use tokio::sync::mpsc::{self, Receiver, Sender}; -use tracing::{debug, error, info, warn}; - -use crate::ipc::{Command, Response}; -use crate::modules::PopupButton; -use crate::style::load_css; -use crate::{glib_recv_mpsc, read_lock, send_async, spawn, try_send, write_lock, Ironbar}; - -use super::Ipc; - -impl Ipc { - /// Starts the IPC server on its socket. - /// - /// Once started, the server will begin accepting connections. - pub fn start(&self, application: &Application, ironbar: Rc) { - let (cmd_tx, cmd_rx) = mpsc::channel(32); - let (res_tx, mut res_rx) = mpsc::channel(32); - - let path = self.path.clone(); - - if path.exists() { - warn!("Socket already exists. Did Ironbar exit abruptly?"); - warn!("Attempting IPC shutdown to allow binding to address"); - Self::shutdown(&path); - } - - spawn(async move { - info!("Starting IPC on {}", path.display()); - - let listener = match UnixListener::bind(&path) { - Ok(listener) => listener, - Err(err) => { - error!( - "{:?}", - Report::new(err).wrap_err("Unable to start IPC server") - ); - return; - } - }; - - loop { - match listener.accept().await { - Ok((stream, _addr)) => { - if let Err(err) = - Self::handle_connection(stream, &cmd_tx, &mut res_rx).await - { - error!("{err:?}"); - } - } - Err(err) => { - error!("{err:?}"); - } - } - } - }); - - let application = application.clone(); - glib_recv_mpsc!(cmd_rx, command => { - let res = Self::handle_command(command, &application, &ironbar); - try_send!(res_tx, res); - }); - } - - /// Takes an incoming connections, - /// reads the command message, and sends the response. - /// - /// The connection is closed once the response has been written. - async fn handle_connection( - mut stream: UnixStream, - cmd_tx: &Sender, - res_rx: &mut Receiver, - ) -> Result<()> { - let (mut stream_read, mut stream_write) = stream.split(); - - let mut read_buffer = vec![0; 1024]; - let bytes = stream_read.read(&mut read_buffer).await?; - - let command = serde_json::from_slice::(&read_buffer[..bytes])?; - - debug!("Received command: {command:?}"); - - send_async!(cmd_tx, command); - let res = res_rx - .recv() - .await - .unwrap_or(Response::Err { message: None }); - let res = serde_json::to_vec(&res)?; - - stream_write.write_all(&res).await?; - stream_write.shutdown().await?; - - Ok(()) - } - - /// Takes an input command, runs it and returns with the appropriate response. - /// - /// This runs on the main thread, allowing commands to interact with GTK. - fn handle_command( - command: Command, - application: &Application, - ironbar: &Rc, - ) -> Response { - match command { - Command::Inspect => { - gtk::Window::set_interactive_debugging(true); - Response::Ok - } - Command::Reload => { - info!("Closing existing bars"); - ironbar.bars.borrow_mut().clear(); - - let windows = application.windows(); - for window in windows { - window.close(); - } - - let wl = ironbar.clients.borrow_mut().wayland(); - let outputs = wl.output_info_all(); - - ironbar.reload_config(); - - for output in outputs { - match crate::load_output_bars(ironbar, application, &output) { - Ok(mut bars) => ironbar.bars.borrow_mut().append(&mut bars), - Err(err) => error!("{err:?}"), - } - } - - Response::Ok - } - Command::Set { key, value } => { - let variable_manager = Ironbar::variable_manager(); - let mut variable_manager = write_lock!(variable_manager); - match variable_manager.set(key, value) { - Ok(()) => Response::Ok, - Err(err) => Response::error(&format!("{err}")), - } - } - Command::Get { key } => { - let variable_manager = Ironbar::variable_manager(); - let value = read_lock!(variable_manager).get(&key); - match value { - Some(value) => Response::OkValue { value }, - None => Response::error("Variable not found"), - } - } - Command::List => { - let variable_manager = Ironbar::variable_manager(); - - let mut values = read_lock!(variable_manager) - .get_all() - .iter() - .map(|(k, v)| format!("{k}: {}", v.get().unwrap_or_default())) - .collect::>(); - - values.sort(); - let value = values.join("\n"); - - Response::OkValue { value } - } - Command::LoadCss { path } => { - if path.exists() { - load_css(path); - Response::Ok - } else { - Response::error("File not found") - } - } - Command::TogglePopup { bar_name, name } => { - let bar = ironbar.bar_by_name(&bar_name); - - match bar { - Some(bar) => { - let popup = bar.popup(); - let current_widget = popup.current_widget(); - - popup.hide(); - - let data = popup - .container_cache - .borrow() - .iter() - .find(|(_, value)| value.name == name) - .map(|(id, value)| (*id, value.content.buttons.first().cloned())); - - match data { - Some((id, Some(button))) if current_widget != Some(id) => { - let button_id = button.popup_id(); - - if popup.is_visible() { - popup.hide(); - } else { - popup.show(id, button_id); - } - - Response::Ok - } - Some((_, None)) => Response::error("Module has no popup functionality"), - Some(_) => Response::Ok, - None => Response::error("Invalid module name"), - } - } - None => Response::error("Invalid bar name"), - } - } - Command::OpenPopup { bar_name, name } => { - let bar = ironbar.bar_by_name(&bar_name); - - match bar { - Some(bar) => { - let popup = bar.popup(); - - // only one popup per bar, so hide if open for another widget - popup.hide(); - - let data = popup - .container_cache - .borrow() - .iter() - .find(|(_, value)| value.name == name) - .map(|(id, value)| (*id, value.content.buttons.first().cloned())); - - match data { - Some((id, Some(button))) => { - let button_id = button.popup_id(); - popup.show(id, button_id); - - Response::Ok - } - Some((_, None)) => Response::error("Module has no popup functionality"), - None => Response::error("Invalid module name"), - } - } - None => Response::error("Invalid bar name"), - } - } - Command::ClosePopup { bar_name } => { - let bar = ironbar.bar_by_name(&bar_name); - - match bar { - Some(bar) => { - let popup = bar.popup(); - popup.hide(); - - Response::Ok - } - None => Response::error("Invalid bar name"), - } - } - Command::Ping => Response::Ok, - Command::SetVisible { bar_name, visible } => { - let windows = application.windows(); - let found = windows - .iter() - .find(|window| window.widget_name() == bar_name); - - if let Some(window) = found { - window.set_visible(visible); - Response::Ok - } else { - Response::error("Bar not found") - } - } - Command::GetVisible { bar_name } => { - let windows = application.windows(); - let found = windows - .iter() - .find(|window| window.widget_name() == bar_name); - - if let Some(window) = found { - Response::OkValue { - value: window.is_visible().to_string(), - } - } else { - Response::error("Bar not found") - } - } - } - } - - /// Shuts down the IPC server, - /// removing the socket file in the process. - /// - /// Note this is static as the `Ipc` struct is not `Send`. - pub fn shutdown>(path: P) { - fs::remove_file(&path).ok(); - } -} diff --git a/src/ipc/server/bar.rs b/src/ipc/server/bar.rs new file mode 100644 index 0000000..b4861c5 --- /dev/null +++ b/src/ipc/server/bar.rs @@ -0,0 +1,84 @@ +use super::Response; +use crate::bar::Bar; +use crate::ipc::{BarCommand, BarCommandType}; +use crate::modules::PopupButton; +use crate::Ironbar; +use std::rc::Rc; + +pub fn handle_command(command: BarCommand, ironbar: &Rc) -> Response { + let bar = ironbar.bar_by_name(&command.name); + let Some(bar) = bar else { + return Response::error("Invalid bar name"); + }; + + use BarCommandType::*; + match command.subcommand { + Show => set_visible(&bar, true), + Hide => set_visible(&bar, false), + SetVisible { visible } => set_visible(&bar, visible), + ToggleVisible => set_visible(&bar, !bar.visible()), + GetVisible => Response::OkValue { + value: bar.visible().to_string(), + }, + + ShowPopup { widget_name } => show_popup(&bar, widget_name), + HidePopup => hide_popup(&bar), + SetPopupVisible { + widget_name, + visible, + } => { + if visible { + show_popup(&bar, widget_name) + } else { + hide_popup(&bar) + } + } + TogglePopup { widget_name } => { + if bar.popup().visible() { + hide_popup(&bar) + } else { + show_popup(&bar, widget_name) + } + } + GetPopupVisible => Response::OkValue { + value: bar.popup().visible().to_string(), + }, + } +} + +fn set_visible(bar: &Bar, visible: bool) -> Response { + bar.set_visible(visible); + Response::Ok +} + +fn show_popup(bar: &Bar, widget_name: String) -> Response { + let popup = bar.popup(); + + // only one popup per bar, so hide if open for another widget + popup.hide(); + + let data = popup + .container_cache + .borrow() + .iter() + .find(|(_, value)| value.name == widget_name) + .map(|(id, value)| (*id, value.content.buttons.first().cloned())); + + match data { + Some((id, Some(button))) => { + let button_id = button.popup_id(); + popup.show(id, button_id); + + Response::Ok + } + Some((_, None)) => Response::error("Module has no popup functionality"), + None => Response::error("Invalid module name"), + } +} + +fn hide_popup(bar: &Bar) -> Response { + let popup = bar.popup(); + popup.hide(); + + Response::Ok +} diff --git a/src/ipc/server/ironvar.rs b/src/ipc/server/ironvar.rs new file mode 100644 index 0000000..ced8e77 --- /dev/null +++ b/src/ipc/server/ironvar.rs @@ -0,0 +1,38 @@ +use crate::ipc::commands::IronvarCommand; +use crate::ipc::Response; +use crate::{read_lock, write_lock, Ironbar}; + +pub fn handle_command(command: IronvarCommand) -> Response { + match command { + IronvarCommand::Set { key, value } => { + let variable_manager = Ironbar::variable_manager(); + let mut variable_manager = write_lock!(variable_manager); + match variable_manager.set(key, value) { + Ok(()) => Response::Ok, + Err(err) => Response::error(&format!("{err}")), + } + } + IronvarCommand::Get { key } => { + let variable_manager = Ironbar::variable_manager(); + let value = read_lock!(variable_manager).get(&key); + match value { + Some(value) => Response::OkValue { value }, + None => Response::error("Variable not found"), + } + } + IronvarCommand::List => { + let variable_manager = Ironbar::variable_manager(); + + let mut values = read_lock!(variable_manager) + .get_all() + .iter() + .map(|(k, v)| format!("{k}: {}", v.get().unwrap_or_default())) + .collect::>(); + + values.sort(); + let value = values.join("\n"); + + Response::OkValue { value } + } + } +} diff --git a/src/ipc/server/mod.rs b/src/ipc/server/mod.rs new file mode 100644 index 0000000..9ef736a --- /dev/null +++ b/src/ipc/server/mod.rs @@ -0,0 +1,164 @@ +mod bar; +mod ironvar; + +use std::fs; +use std::path::Path; +use std::rc::Rc; + +use color_eyre::{Report, Result}; +use gtk::prelude::*; +use gtk::Application; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{UnixListener, UnixStream}; +use tokio::sync::mpsc::{self, Receiver, Sender}; +use tracing::{debug, error, info, warn}; + +use crate::ipc::{Command, Response}; +use crate::style::load_css; +use crate::{glib_recv_mpsc, send_async, spawn, try_send, Ironbar}; + +use super::Ipc; + +impl Ipc { + /// Starts the IPC server on its socket. + /// + /// Once started, the server will begin accepting connections. + pub fn start(&self, application: &Application, ironbar: Rc) { + let (cmd_tx, cmd_rx) = mpsc::channel(32); + let (res_tx, mut res_rx) = mpsc::channel(32); + + let path = self.path.clone(); + + if path.exists() { + warn!("Socket already exists. Did Ironbar exit abruptly?"); + warn!("Attempting IPC shutdown to allow binding to address"); + Self::shutdown(&path); + } + + spawn(async move { + info!("Starting IPC on {}", path.display()); + + let listener = match UnixListener::bind(&path) { + Ok(listener) => listener, + Err(err) => { + error!( + "{:?}", + Report::new(err).wrap_err("Unable to start IPC server") + ); + return; + } + }; + + loop { + match listener.accept().await { + Ok((stream, _addr)) => { + if let Err(err) = + Self::handle_connection(stream, &cmd_tx, &mut res_rx).await + { + error!("{err:?}"); + } + } + Err(err) => { + error!("{err:?}"); + } + } + } + }); + + let application = application.clone(); + glib_recv_mpsc!(cmd_rx, command => { + let res = Self::handle_command(command, &application, &ironbar); + try_send!(res_tx, res); + }); + } + + /// Takes an incoming connections, + /// reads the command message, and sends the response. + /// + /// The connection is closed once the response has been written. + async fn handle_connection( + mut stream: UnixStream, + cmd_tx: &Sender, + res_rx: &mut Receiver, + ) -> Result<()> { + let (mut stream_read, mut stream_write) = stream.split(); + + let mut read_buffer = vec![0; 1024]; + let bytes = stream_read.read(&mut read_buffer).await?; + + // FIXME: Error on invalid command + let command = serde_json::from_slice::(&read_buffer[..bytes])?; + + debug!("Received command: {command:?}"); + + send_async!(cmd_tx, command); + let res = res_rx + .recv() + .await + .unwrap_or(Response::Err { message: None }); + let res = serde_json::to_vec(&res)?; + + stream_write.write_all(&res).await?; + stream_write.shutdown().await?; + + Ok(()) + } + + /// Takes an input command, runs it and returns with the appropriate response. + /// + /// This runs on the main thread, allowing commands to interact with GTK. + fn handle_command( + command: Command, + application: &Application, + ironbar: &Rc, + ) -> Response { + match command { + Command::Ping => Response::Ok, + Command::Inspect => { + gtk::Window::set_interactive_debugging(true); + Response::Ok + } + Command::Reload => { + info!("Closing existing bars"); + ironbar.bars.borrow_mut().clear(); + + let windows = application.windows(); + for window in windows { + window.close(); + } + + let wl = ironbar.clients.borrow_mut().wayland(); + let outputs = wl.output_info_all(); + + ironbar.reload_config(); + + for output in outputs { + match crate::load_output_bars(ironbar, application, &output) { + Ok(mut bars) => ironbar.bars.borrow_mut().append(&mut bars), + Err(err) => error!("{err:?}"), + } + } + + Response::Ok + } + Command::LoadCss { path } => { + if path.exists() { + load_css(path); + Response::Ok + } else { + Response::error("File not found") + } + } + Command::Var(cmd) => ironvar::handle_command(cmd), + Command::Bar(cmd) => bar::handle_command(cmd, ironbar), + } + } + + /// Shuts down the IPC server, + /// removing the socket file in the process. + /// + /// Note this is static as the `Ipc` struct is not `Send`. + pub fn shutdown>(path: P) { + fs::remove_file(&path).ok(); + } +} diff --git a/src/main.rs b/src/main.rs index 551a25b..4657574 100644 --- a/src/main.rs +++ b/src/main.rs @@ -85,11 +85,21 @@ fn run_with_args() { match args.command { Some(command) => { + if args.debug { + eprintln!("REQUEST: {command:?}") + } + let rt = create_runtime(); rt.block_on(async move { let ipc = ipc::Ipc::new(); - match ipc.send(command).await { - Ok(res) => cli::handle_response(res), + match ipc.send(command, args.debug).await { + Ok(res) => { + if args.debug { + eprintln!("RESPONSE: {res:?}") + } + + cli::handle_response(res, args.format.unwrap_or_default()) + } Err(err) => error!("{err:?}"), }; }); @@ -239,6 +249,7 @@ impl Ironbar { /// Gets a `usize` ID value that is unique to the entire Ironbar instance. /// This is just a static `AtomicUsize` that increments every time this function is called. + #[must_use] pub fn unique_id() -> usize { static COUNTER: AtomicUsize = AtomicUsize::new(1); COUNTER.fetch_add(1, Ordering::Relaxed) diff --git a/src/modules/mod.rs b/src/modules/mod.rs index ec3bb65..2a15252 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -382,7 +382,7 @@ impl ModuleFactory for BarModuleFactory { } ModuleUpdateEvent::TogglePopup(button_id) if !disable_popup => { debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id); - if popup.is_visible() && popup.current_widget().unwrap_or_default() == id { + if popup.visible() && popup.current_widget().unwrap_or_default() == id { popup.hide(); } else { popup.show(id, button_id); @@ -455,7 +455,7 @@ impl ModuleFactory for PopupModuleFactory { } ModuleUpdateEvent::TogglePopup(_) if !disable_popup => { debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id); - if popup.is_visible() && popup.current_widget().unwrap_or_default() == id { + if popup.visible() && popup.current_widget().unwrap_or_default() == id { popup.hide(); } else { popup.show(id, button_id); diff --git a/src/popup.rs b/src/popup.rs index 0a85a7f..5946e99 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -229,7 +229,7 @@ impl Popup { } /// Checks if the popup is currently visible - pub fn is_visible(&self) -> bool { + pub fn visible(&self) -> bool { self.window.is_visible() }