diff --git a/Cargo.lock b/Cargo.lock index cda8c3d2..e82347e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -476,6 +476,15 @@ dependencies = [ "strsim 0.11.0", ] +[[package]] +name = "clap_complete" +version = "4.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b4be9c4c4b1f30b78d8a750e0822b6a6102d97e62061c583a6c1dea2dfb33ae" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.18" @@ -494,6 +503,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "clap_mangen" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "color-eyre" version = "0.6.3" @@ -1653,6 +1672,8 @@ dependencies = [ "cfg-if", "chrono", "clap", + "clap_complete", + "clap_mangen", "color-eyre", "ctrlc", "dirs", @@ -2633,6 +2654,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "roff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" + [[package]] name = "ron" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 318a1c2c..0d8412d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -173,4 +173,10 @@ schemars = { version = "0.8.21", optional = true } # -- PATCH -- # temp fix for tracing-appender/time -time = "0.3.36" \ No newline at end of file +time = "0.3.36" + +[build-dependencies] +clap = { version = "4.5.9", features = ["derive"] } +clap_complete = "4.5.2" +clap_mangen = "0.2.20" +serde = { version = "1.0.204", features = ["derive"] } diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..a5c4d9b3 --- /dev/null +++ b/build.rs @@ -0,0 +1,69 @@ +#[path = "src/cli.rs"] +mod cli; + +use clap::Command; +use clap::CommandFactory; +use clap_complete::generate_to; +use clap_complete::Shell::{Bash, Fish, Zsh}; +use clap_mangen::Man; +use cli::Args; +use std::fs; +use std::path::PathBuf; + +static NAME: &str = "ironbar"; + +fn generate_man_pages(cmd: Command) { + let man_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/man"); + let mut buffer = Vec::default(); + + Man::new(cmd.clone()).render(&mut buffer).unwrap(); + fs::create_dir_all(&man_dir).unwrap(); + fs::write(man_dir.join(NAME.to_owned() + ".1"), buffer).unwrap(); + + for subcommand in cmd.get_subcommands() { + let mut buffer = Vec::default(); + + Man::new(subcommand.clone()).render(&mut buffer).unwrap(); + fs::write( + man_dir.join(NAME.to_owned() + "-" + subcommand.get_name() + ".1"), + buffer, + ) + .unwrap(); + + for subsubcommand in subcommand.get_subcommands() { + let mut buffer = Vec::default(); + + Man::new(subsubcommand.clone()).render(&mut buffer).unwrap(); + fs::write( + man_dir.join( + NAME.to_owned() + + "-" + + subcommand.get_name() + + "-" + + subsubcommand.get_name() + + ".1", + ), + buffer, + ) + .unwrap(); + } + } +} + +fn generate_shell_completions(mut cmd: Command) { + let comp_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/completions"); + + fs::create_dir_all(&comp_dir).unwrap(); + + for shell in [Bash, Fish, Zsh] { + generate_to(shell, &mut cmd, NAME, &comp_dir).unwrap(); + } +} + +fn main() { + let mut cmd = Args::command(); + cmd.set_bin_name(NAME); + + generate_man_pages(cmd.clone()); + generate_shell_completions(cmd); +} diff --git a/nix/default.nix b/nix/default.nix index db1e3b89..1be85b93 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -17,6 +17,7 @@ luajit, luajitPackages, pkg-config, + installShellFiles, hicolor-icon-theme, rustPlatform, lib, @@ -41,6 +42,7 @@ pkg-config wrapGAppsHook gobject-introspection + installShellFiles ]; buildInputs = [ @@ -83,6 +85,15 @@ ) ''; + postInstall = '' + installManPage target/man/* + + installShellCompletion --cmd ironbar \ + --bash target/completions/ironbar.bash \ + --fish target/completions/ironbar.fish \ + --zsh target/completions/_ironbar + ''; + passthru = { updateScript = gnome.updateScript { packageName = pname; diff --git a/src/cli.rs b/src/cli.rs index 769d706b..e8e1a50e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,9 +1,134 @@ -use crate::error::ExitCode; -use crate::ipc::commands::Command; -use crate::ipc::responses::Response; -use clap::{Parser, ValueEnum}; +use clap::ArgAction; +use clap::{Args as ClapArgs, Parser, Subcommand, ValueEnum}; use serde::{Deserialize, Serialize}; -use std::process::exit; +use std::path::PathBuf; + +#[derive(Subcommand, Debug, Serialize, Deserialize)] +#[serde(tag = "command", rename_all = "snake_case")] +pub enum Command { + /// Pong + Ping, + + /// Open the GTK inspector. + Inspect, + + /// 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. + /// Keys and values can be any valid UTF-8 string. + Set { + /// Variable key. Can be any alphanumeric ASCII string. + key: Box, + /// Variable value. Can be any valid UTF-8 string. + value: String, + }, + + /// Get the current value of an `ironvar`. + Get { + /// Variable key. + key: Box, + }, + + /// Gets the current value of all `ironvar`s. + List, +} + +#[derive(ClapArgs, Debug, Serialize, Deserialize)] +pub struct BarCommand { + /// The name of the bar. + pub name: String, + + #[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 { + /// 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, + + // == 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 configured name of the widget. + widget_name: String, + }, + /// Get the popup's current visibility state. + GetPopupVisible, + + // == Exclusivity == \\ + /// Set whether the bar reserves an exclusive zone. + SetExclusive { + #[clap( + num_args(1), + require_equals(true), + action = ArgAction::Set, + )] + exclusive: bool, + }, +} #[derive(Parser, Debug, Serialize, Deserialize)] #[command(version)] @@ -38,23 +163,3 @@ pub enum Format { 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/ipc/client.rs b/src/ipc/client.rs index 820ace5e..1956bba0 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -1,5 +1,6 @@ use super::Ipc; -use crate::ipc::{Command, Response}; +use crate::cli::Command; +use crate::ipc::Response; use color_eyre::Result; use color_eyre::{Help, Report}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; diff --git a/src/main.rs b/src/main.rs index 2d1699ce..07c7e92b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,11 +27,15 @@ use tracing::{debug, error, info, warn}; use universal_config::ConfigLoader; use crate::bar::{create_bar, Bar}; +#[cfg(feature = "cli")] +use crate::cli::Format; use crate::clients::wayland::OutputEventType; use crate::clients::Clients; use crate::config::{Config, MonitorConfig}; use crate::error::ExitCode; #[cfg(feature = "ipc")] +use crate::ipc::responses::Response; +#[cfg(feature = "ipc")] use crate::ironvar::VariableManager; use crate::style::load_css; @@ -100,7 +104,7 @@ fn run_with_args() { eprintln!("RESPONSE: {res:?}"); } - cli::handle_response(res, args.format.unwrap_or_default()); + handle_response(res, args.format.unwrap_or_default()); } Err(err) => error!("{err:?}"), }; @@ -110,6 +114,27 @@ fn run_with_args() { } } +#[cfg(feature = "cli")] +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) + } +} + #[derive(Debug)] pub struct Ironbar { bars: Rc>>,