diff --git a/Cargo.lock b/Cargo.lock index df8804e..9bd618a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "anyhow" version = "1.0.71" @@ -395,6 +444,48 @@ dependencies = [ "winapi", ] +[[package]] +name = "clap" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80672091db20273a15cf9fdd4e47ed43b5091ec9841bf4c6145c9dfbbcae09ed" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1458a1df40e1e2afebb7ab60ce55c1fa8f431146205aa5f4887e0b111c27636" +dependencies = [ + "anstream", + "anstyle", + "bitflags 1.3.2", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote 1.0.28", + "syn 2.0.18", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + [[package]] name = "color-eyre" version = "0.6.2" @@ -422,6 +513,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "concurrent-queue" version = "2.2.0" @@ -515,6 +612,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctrlc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a011bbe2c35ce9c1f143b7af6f94f29a167beb4cd1d29e6740ce836f723120e" +dependencies = [ + "nix 0.26.2", + "windows-sys 0.48.0", +] + [[package]] name = "darling" version = "0.14.4" @@ -1453,7 +1560,9 @@ dependencies = [ "async_once", "cfg-if", "chrono", + "clap", "color-eyre", + "ctrlc", "dirs", "futures-lite", "futures-util", @@ -1470,6 +1579,7 @@ dependencies = [ "regex", "reqwest", "serde", + "serde_json", "smithay-client-toolkit", "stray", "strip-ansi-escapes", @@ -1489,6 +1599,18 @@ dependencies = [ "zbus", ] +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itoa" version = "1.0.6" diff --git a/Cargo.toml b/Cargo.toml index d35de71..49486b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,12 @@ version = "0.12.1" edition = "2021" license = "MIT" description = "Customisable GTK Layer Shell wlroots/sway bar" +repository = "https://github.com/jakestanger/ironbar" [features] default = [ + "cli", + "ipc", "http", "config+all", "clipboard", @@ -17,8 +20,11 @@ default = [ "upower", "workspaces+all" ] + +cli = ["dep:clap", "ipc"] +ipc = ["dep:serde_json"] + http = ["dep:reqwest"] -upower = ["upower_dbus", "zbus", "futures-lite"] "config+all" = ["config+json", "config+yaml", "config+toml", "config+corn", "config+ron"] "config+json" = ["universal-config/json"] @@ -40,6 +46,8 @@ sys_info = ["sysinfo", "regex"] tray = ["stray"] +upower = ["upower_dbus", "zbus", "futures-lite"] + workspaces = ["futures-util"] "workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"] "workspaces+sway" = ["workspaces", "swayipc-async"] @@ -67,11 +75,18 @@ wayland-protocols = { version = "0.30.0", features = ["unstable", "client"] } wayland-protocols-wlr = { version = "0.1.0", features = ["client"] } smithay-client-toolkit = { version = "0.17.0", default-features = false, features = ["calloop"] } universal-config = { version = "0.4.0", default_features = false } +ctrlc = "3.4.0" lazy_static = "1.4.0" async_once = "0.2.6" cfg-if = "1.0.0" +# cli +clap = { version = "4.2.7", optional = true, features = ["derive"] } + +# ipc +serde_json = { version = "1.0.96", optional = true } + # http reqwest = { version = "0.11.18", optional = true } diff --git a/src/bridge_channel.rs b/src/bridge_channel.rs index 2208e0c..101eb10 100644 --- a/src/bridge_channel.rs +++ b/src/bridge_channel.rs @@ -2,7 +2,7 @@ use crate::send; use tokio::spawn; use tokio::sync::mpsc; -/// MPSC async -> sync channel. +/// MPSC async -> GTK sync channel. /// The sender uses `tokio::sync::mpsc` /// while the receiver uses `glib::MainContext::channel`. /// diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..feb2933 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,19 @@ +use crate::ipc::commands::Command; +use crate::ipc::responses::Response; +use clap::Parser; +use serde::{Deserialize, Serialize}; + +#[derive(Parser, Debug, Serialize, Deserialize)] +#[command(version)] +pub struct Args { + #[command(subcommand)] + pub command: 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()), + } +} diff --git a/src/ipc/client.rs b/src/ipc/client.rs new file mode 100644 index 0000000..b7e1da9 --- /dev/null +++ b/src/ipc/client.rs @@ -0,0 +1,28 @@ +use super::Ipc; +use crate::ipc::{Command, Response}; +use color_eyre::Result; +use color_eyre::{Help, Report}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +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 { + let mut stream = match UnixStream::connect(&self.path).await { + Ok(stream) => Ok(stream), + Err(err) => Err(Report::new(err) + .wrap_err("Failed to connect to Ironbar IPC server") + .suggestion("Is Ironbar running?")), + }?; + + let write_buffer = serde_json::to_vec(&command)?; + stream.write_all(&write_buffer).await?; + + let mut read_buffer = vec![0; 1024]; + let bytes = stream.read(&mut read_buffer).await?; + + let response = serde_json::from_slice(&read_buffer[..bytes])?; + Ok(response) + } +} diff --git a/src/ipc/commands.rs b/src/ipc/commands.rs new file mode 100644 index 0000000..51fc4d3 --- /dev/null +++ b/src/ipc/commands.rs @@ -0,0 +1,14 @@ +use clap::Subcommand; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Subcommand, Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum Command { + /// Return "ok" + Ping, + + /// Open the GTK inspector + Inspect, +} + diff --git a/src/ipc/mod.rs b/src/ipc/mod.rs new file mode 100644 index 0000000..87aab89 --- /dev/null +++ b/src/ipc/mod.rs @@ -0,0 +1,33 @@ +mod client; +pub mod commands; +pub mod responses; +mod server; + +use std::path::PathBuf; +use tracing::warn; + +pub use commands::Command; +pub use responses::Response; + +#[derive(Debug)] +pub struct Ipc { + path: PathBuf, +} + +impl Ipc { + /// Creates a new IPC instance. + /// This can be used as both a server and client. + pub fn new() -> Self { + let ipc_socket_file = std::env::var("XDG_RUNTIME_DIR") + .map_or_else(|_| PathBuf::from("/tmp"), PathBuf::from) + .join("ironbar-ipc.sock"); + + if format!("{}", ipc_socket_file.display()).len() > 100 { + warn!("The IPC socket file's absolute path exceeds 100 bytes, the socket may fail to create."); + } + + Self { + path: ipc_socket_file, + } + } +} diff --git a/src/ipc/responses.rs b/src/ipc/responses.rs new file mode 100644 index 0000000..506732c --- /dev/null +++ b/src/ipc/responses.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum Response { + Ok, + Err { message: Option }, +} + +impl Response { + /// Creates a new `Response::Error`. + pub fn error(message: &str) -> Self { + Self::Err { + message: Some(message.to_string()), + } + } +} diff --git a/src/ipc/server.rs b/src/ipc/server.rs new file mode 100644 index 0000000..d24ed48 --- /dev/null +++ b/src/ipc/server.rs @@ -0,0 +1,119 @@ +use super::Ipc; +use crate::bridge_channel::BridgeChannel; +use crate::ipc::{Command, Response}; +use crate::ironvar::get_variable_manager; +use crate::{read_lock, send_async, try_send, write_lock}; +use color_eyre::{Report, Result}; +use glib::Continue; +use std::fs; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{UnixListener, UnixStream}; +use tokio::spawn; +use tokio::sync::mpsc; +use tokio::sync::mpsc::{Receiver, Sender}; +use tracing::{debug, error, info, warn}; + +impl Ipc { + /// Starts the IPC server on its socket. + /// + /// Once started, the server will begin accepting connections. + pub fn start(&self) { + let bridge = BridgeChannel::::new(); + let cmd_tx = bridge.create_sender(); + 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(); + } + + 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:?}"); + } + } + } + }); + + bridge.recv(move |command| { + let res = Self::handle_command(command); + try_send!(res_tx, res); + Continue(true) + }); + } + + /// 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) -> Response { + match command { + Command::Inspect => { + gtk::Window::set_interactive_debugging(true); + Response::Ok + } + Command::Ping => Response::Ok, + } + } + + /// Shuts down the IPC server, + /// removing the socket file in the process. + pub fn shutdown(&self) { + fs::remove_file(&self.path).ok(); + } +} diff --git a/src/main.rs b/src/main.rs index 8523a52..6ddcfb0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,8 @@ mod bar; mod bridge_channel; +#[cfg(feature = "cli")] +mod cli; mod clients; mod config; mod desktop_file; @@ -9,6 +11,8 @@ mod dynamic_string; mod error; mod gtk_helpers; mod image; +#[cfg(feature = "ipc")] +mod ipc; mod logging; mod macros; mod modules; @@ -20,6 +24,9 @@ mod unique_id; use crate::bar::create_bar; use crate::config::{Config, MonitorConfig}; use crate::style::load_css; +use cfg_if::cfg_if; +#[cfg(feature = "cli")] +use clap::Parser; use color_eyre::eyre::Result; use color_eyre::Report; use dirs::config_dir; @@ -32,8 +39,9 @@ use std::future::Future; use std::path::PathBuf; use std::process::exit; use std::rc::Rc; +use std::sync::mpsc; use tokio::runtime::Handle; -use tokio::task::block_in_place; +use tokio::task::{block_in_place, spawn_blocking}; use crate::error::ExitCode; use clients::wayland::{self, WaylandClient}; @@ -47,6 +55,32 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); async fn main() { let _guard = logging::install_logging(); + cfg_if! { + if #[cfg(feature = "cli")] { + run_with_args().await + } else { + start_ironbar().await + } + } +} + +#[cfg(feature = "cli")] +async fn run_with_args() { + let args = cli::Args::parse(); + + match args.command { + Some(command) => { + let ipc = ipc::Ipc::new(); + match ipc.send(command).await { + Ok(res) => cli::handle_response(res), + Err(err) => error!("{err:?}"), + }; + } + None => start_ironbar().await, + } +} + +async fn start_ironbar() { info!("Ironbar version {}", VERSION); info!("Starting application"); @@ -64,6 +98,13 @@ async fn main() { running.set(true); + cfg_if! { + if #[cfg(feature = "ipc")] { + let ipc = ipc::Ipc::new(); + ipc.start(); + } + } + let display = Display::default().map_or_else( || { let report = Report::msg("Failed to get default GTK display"); @@ -112,14 +153,27 @@ async fn main() { if style_path.exists() { load_css(style_path); } + + let (tx, rx) = mpsc::channel(); + + spawn_blocking(move || { + rx.recv().expect("to receive from channel"); + + info!("Shutting down"); + + #[cfg(feature = "ipc")] + ipc.shutdown(); + + exit(0); + }); + + ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel.")) + .expect("Error setting Ctrl-C handler"); }); // Ignore CLI args // Some are provided by swaybar_config but not currently supported app.run_with_args(&Vec::<&str>::new()); - - info!("Shutting down"); - exit(0); } /// Creates each of the bars across each of the (configured) outputs.