├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── release.sh └── crate ├── bitbar-derive ├── Cargo.toml └── src │ └── lib.rs ├── bitbar ├── Cargo.toml └── src │ ├── attr.rs │ ├── flavor │ ├── mod.rs │ └── swiftbar.rs │ └── lib.rs └── cargo-bitbar ├── Cargo.toml └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.10.1 2 | 3 | * Fix a compile error caused by the function used internally for error notifications being private. 4 | 5 | # 0.10.0 6 | 7 | * **Breaking:** Errors from parsing command parameters must now implement `Debug` in addition to `Display`. The `Debug` representation is now included in the error notification along with some error context. 8 | 9 | # 0.9.2 10 | 11 | * The `bitbar` crate can now be compiled for other platforms for debugging purposes. System notifications will be replaced with printing to stderr when not compiling for macOS. 12 | 13 | # 0.9.1 14 | 15 | * Internally generated notifications now use SwiftBar's notification system when available, instead of the `notify-rust` crate. 16 | 17 | # 0.9.0 18 | 19 | * Improved error notifications for subcommands 20 | * **Breaking:** The `CommandOutput` implementation requires the error type to implement `Debug` 21 | 22 | # 0.8.2 23 | 24 | * Fix `SwiftBar::checked` not doing anything 25 | 26 | # 0.8.1 27 | 28 | * New `SwiftBar::checked` method 29 | 30 | # 0.8.0 31 | 32 | * **Breaking:** A command declared using `#[bitbar::command]` must also be registered via `#[bitbar::main(commands(...))]` 33 | * **Breaking:** A fallback command declared using `#[bitbar::fallback_command]` must also be registered via `#[bitbar::main(fallback_command = "...")]` 34 | * **Breaking:** The `tokio02` and `tokio03` features have been removed 35 | * **Breaking:** Upgraded the optional `image` dependency from 0.22 to 0.24 36 | * **Breaking:** Upgraded the optional `serenity` dependency from 0.10 to 0.11 37 | 38 | # 0.7.3 39 | 40 | * New `swiftbar::Notification::command` method 41 | 42 | # 0.7.2 43 | 44 | * New `SwiftBar::plugin_name` method 45 | * New `swiftbar::Notification` type 46 | 47 | # 0.7.1 (`cargo-bitbar` 0.1.1) 48 | 49 | * Support for trailing stream separators in the newest SwiftBar beta (see [swiftbar/SwiftBar#273](https://github.com/swiftbar/SwiftBar/issues/273) for details) 50 | 51 | # 0.7.0 (`cargo-bitbar` 0.1.0) 52 | 53 | * **Breaking:** The `MainOutput` trait now prints the menu instead of returning it 54 | * `cargo bitbar` is a new `cargo` subcommand that can add plugin metadata to binary SwiftBar plugins 55 | * Support for streamable SwiftBar plugins via `bitbar::flavor::swiftbar::BlockingStream` and (with one of the tokio features) `bitbar::flavor::swiftbar::Stream` 56 | * New `AsyncMainOutput` trait if printing the menu requires `async` (requires one of the tokio features) 57 | 58 | # 0.6.0 59 | 60 | * **Breaking:** `command` functions now take any number of parameters that will be parsed from command-line args; use `#[command(varargs)]` to take a `Vec` instead 61 | * **Breaking:** The `fallback_command` function now takes the command name as a `String` and the remaining arguments as a `Vec` 62 | * `command` functions now generate functions that return `Params` 63 | * Added a `push` method to `Menu` 64 | 65 | # 0.5.2 66 | 67 | Documentation fixes. 68 | 69 | # 0.5.1 70 | 71 | Documentation fixes. 72 | 73 | # 0.5.0 74 | 75 | * **Breaking:** Types used for configuring menu items have been moved to a new `attr` module 76 | * **Breaking:** `Params` is now an opaque type 77 | * `main` functions may optionally take a `Flavor` argument 78 | * New `ContentItem::size` method to set font size 79 | * SwiftBar only: Support for commands with more than 5 parameters 80 | * SwiftBar only: Support for using different colors depending on whether the system is in dark mode 81 | * SwiftBar only: Support for adding an SF Symbols image to a menu item 82 | 83 | # 0.4.4 84 | 85 | * Added the `flavor` module 86 | * Added the `assume-flavor` feature 87 | 88 | # 0.4.3 89 | 90 | * **Breaking:** The `tokio` feature now uses `tokio` 1; added the `tokio03` feature to use `tokio` 0.3 instead 91 | * **Breaking:** Upgraded the optional `serenity` dependency from 0.9 to 0.10 92 | * `command` functions may be async (requires one of the tokio features) 93 | * Added the `fallback_command` attribute macro 94 | * Added the `MainOutput` trait; `main` functions may return any member of it 95 | * Added the `CommandOutput` trait; `command` and `fallback_command` functions may return any member of it 96 | * This crate is now `#![forbid(unsafe_code)]` 97 | 98 | # 0.4.2 99 | 100 | Documentation fixes. 101 | 102 | # 0.4.1 103 | 104 | * `main` functions may return plain `Menu` rather than only `Result` 105 | * Added an optional `error_template_image` parameter to `main` attribute 106 | * The `tokio` 0.3 dependency is now optional (enabled by default), and a `tokio02` feature has been added to use `tokio` 0.2 instead 107 | 108 | # 0.4.0 109 | 110 | * **Breaking:** Upgraded the optional `serenity` dependency from 0.7 to 0.9 111 | * Added the `main` and `command` attribute macros 112 | * Added `Extend` implementations for `Menu` (where `A: Into`) 113 | * Added an `IntoIterator` implementation for `Menu` 114 | 115 | # 0.3.1 116 | 117 | * Added an `impl From>` implementation for `Image` (requires `base64` feature) 118 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crate/*"] 3 | default-members = ["crate/bitbar"] 4 | resolver = "2" 5 | 6 | [workspace.package] 7 | version = "0.10.1" 8 | authors = ["Fenhl "] 9 | edition = "2021" 10 | repository = "https://github.com/fenhl/rust-bitbar" 11 | 12 | [profile.release] 13 | lto = true 14 | strip = true 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Max Dominik Weber ("Fenhl") 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![crates.io badge]][crates.io link] [![docs.rs badge]][docs.rs link] 2 | 3 | This is `bitbar`, a library crate which includes helpers for writing BitBar plugins in Rust. BitBar is a system that makes it easy to add menus to the macOS menu bar. There are two apps implementing the BitBar system: [SwiftBar](https://swiftbar.app/) and [xbar](https://xbarapp.com/). This crate supports both of them, as well as [the discontinued original BitBar app](https://github.com/matryer/xbar/tree/a595e3bdbb961526803b60be6fd32dd0c667b6ec). 4 | 5 | # Example plugins 6 | 7 | Here are some BitBar plugins that use this library: 8 | 9 | * [BitBar version](https://github.com/fenhl/bitbar-version) 10 | * [Mediawiki watchlist](https://github.com/fenhl/bitbar-mediawiki-watchlist) 11 | * [speedrun.com](https://github.com/fenhl/bitbar-speedruncom) 12 | * [twitch.tv](https://github.com/fenhl/bitbar-twitch) 13 | * [Wurstmineberg server status](https://github.com/wurstmineberg/bitbar-server-status) 14 | 15 | If you have a BitBar plugin that uses this library, feel free to open a pull request to add it to this list. 16 | 17 | [crates.io badge]: https://img.shields.io/crates/v/bitbar.svg?style=flat-square 18 | [crates.io link]: https://crates.io/crates/bitbar 19 | [docs.rs badge]: https://img.shields.io/badge/docs-online-dddddd.svg?style=flat-square 20 | [docs.rs link]: https://docs.rs/bitbar 21 | -------------------------------------------------------------------------------- /assets/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | set -e 4 | 5 | git push 6 | cd crate/bitbar-derive 7 | cargo publish 8 | #TODO only release cargo-bitbar if changed 9 | #cd ../cargo-bitbar 10 | #cargo publish 11 | cd ../bitbar 12 | cargo publish 13 | -------------------------------------------------------------------------------- /crate/bitbar-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bitbar-derive" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | description = "Proc-macros for the bitbar crate" 7 | license = "MIT" 8 | readme = "../../README.md" 9 | repository.workspace = true 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [features] 15 | tokio = [] 16 | 17 | [dependencies] 18 | itertools = "0.12" 19 | proc-macro2 = "1" 20 | quote = "1" 21 | 22 | [dependencies.syn] 23 | version = "2" 24 | features = ["full"] 25 | -------------------------------------------------------------------------------- /crate/bitbar-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Proc macros for the `bitbar` crate. 2 | 3 | #![deny( 4 | missing_docs, 5 | rust_2018_idioms, // this lint is actually about idioms that are *outdated* in Rust 2018 6 | unused, 7 | unused_import_braces, 8 | unused_lifetimes, 9 | unused_qualifications, 10 | warnings, 11 | )] 12 | 13 | use { 14 | itertools::Itertools as _, 15 | proc_macro::TokenStream, 16 | proc_macro2::Span, 17 | quote::{ 18 | quote, 19 | quote_spanned, 20 | }, 21 | syn::{ 22 | *, 23 | punctuated::Punctuated, 24 | spanned::Spanned as _, 25 | }, 26 | }; 27 | 28 | /// Registers a subcommand that you can run from a menu item's `command`. 29 | /// 30 | /// Commands may take any number of parameters implementing `FromStr` (with errors implementing `Debug` and `Display`) and `ToString`, and should return `Result<(), Error>`, where `Error` is any type that implements `Display`. If a command errors, `bitbar` will attempt to send a macOS notification containing the error message. 31 | /// 32 | /// Alternatively, use this arrtibute as `#[command(varargs)]` and define the command function with a single parameter of type `Vec`. 33 | /// 34 | /// The `command` attribute generates a function that can be called with arguments of references to the original parameter types to obtain a `std::io::Result`. If the command has more than 5 parameters or is declared with `#[command(varargs)]`, the function takes an additional first parameter of type `SwiftBar`. 35 | /// 36 | /// The function must also be registered via `#[bitbar::main(commands(...))]`. 37 | #[proc_macro_attribute] 38 | pub fn command(args: TokenStream, item: TokenStream) -> TokenStream { 39 | let args = parse_macro_input!(args with Punctuated::::parse_terminated); 40 | let varargs = match args.into_iter().at_most_one() { 41 | Ok(None) => false, 42 | Ok(Some(arg)) if arg.path().is_ident("varargs") => true, 43 | _ => return quote!(compile_error!("unexpected bitbar::command arguments");).into(), 44 | }; 45 | let command_fn = parse_macro_input!(item as ItemFn); 46 | let vis = &command_fn.vis; 47 | let asyncness = &command_fn.sig.asyncness; 48 | let command_name = &command_fn.sig.ident; 49 | let command_name_str = command_name.to_string(); 50 | let wrapper_name = Ident::new(&format!("bitbar_{command_name}_wrapper"), Span::call_site()); 51 | let awaitness = asyncness.as_ref().map(|_| quote!(.await)); 52 | let (wrapper_body, command_params, command_args) = if varargs { 53 | ( 54 | quote!(::bitbar::CommandOutput::report(#command_name(args)#awaitness, #command_name_str)), 55 | quote!(::std::iter::Iterator::collect(::std::iter::Iterator::chain(::std::iter::once(::std::string::ToString::to_string(#command_name_str)), args))), 56 | quote!(_: ::bitbar::flavor::SwiftBar, args: ::std::vec::Vec<::std::string::String>), 57 | ) 58 | } else { 59 | let mut wrapper_params = Vec::default(); 60 | let mut wrapped_args = Vec::default(); 61 | let mut command_params = Vec::default(); 62 | let mut command_args = Vec::default(); 63 | for (arg_idx, arg) in command_fn.sig.inputs.iter().enumerate() { 64 | match arg { 65 | FnArg::Receiver(_) => return quote_spanned! {arg.span()=> 66 | compile_error("unexpected `self` parameter in bitbar::command"); 67 | }.into(), 68 | FnArg::Typed(PatType { ty, .. }) => { 69 | let ident = Ident::new(&format!("arg{}", arg_idx), arg.span()); 70 | wrapper_params.push(quote_spanned! {arg.span()=> 71 | #ident 72 | }); 73 | wrapped_args.push(quote_spanned! {arg.span()=> 74 | match #ident.parse() { 75 | ::core::result::Result::Ok(arg) => arg, 76 | ::core::result::Result::Err(e) => { 77 | ::bitbar::notify_error( 78 | &::std::format!("{}: error parsing parameter {}: {}", #command_name_str, #arg_idx, e), 79 | &::std::format!("{e:?}"), 80 | ); 81 | ::std::process::exit(1) 82 | } 83 | } 84 | }); 85 | command_params.push(quote_spanned! {arg.span()=> 86 | #ident.to_string() 87 | }); 88 | command_args.push(quote_spanned! {arg.span()=> 89 | #ident: &#ty 90 | }); 91 | } 92 | } 93 | } 94 | if command_args.len() > 5 { 95 | command_args.insert(0, quote!(_: ::bitbar::flavor::SwiftBar)); 96 | } 97 | ( 98 | quote! { 99 | match &*args { 100 | [#(#wrapper_params),*] => ::bitbar::CommandOutput::report(#command_name(#(#wrapped_args),*)#awaitness, #command_name_str), 101 | _ => { 102 | ::bitbar::notify("wrong number of command arguments"); 103 | ::std::process::exit(1) 104 | } 105 | } 106 | }, 107 | quote!(::std::vec![ 108 | ::std::string::ToString::to_string(#command_name_str), 109 | #(#command_params,)* 110 | ]), 111 | quote!(#(#command_args),*), 112 | ) 113 | }; 114 | #[cfg(not(feature = "tokio"))] let (wrapper_ret, wrapper_body) = (quote!(), wrapper_body); 115 | #[cfg(feature = "tokio")] let (wrapper_ret, wrapper_body) = ( 116 | quote!(-> ::std::pin::Pin<::std::boxed::Box>>), 117 | quote!(::std::boxed::Box::pin(async move { #wrapper_body })), 118 | ); 119 | TokenStream::from(quote! { 120 | fn #wrapper_name(args: ::std::vec::Vec<::std::string::String>) #wrapper_ret { 121 | #command_fn 122 | 123 | #wrapper_body 124 | } 125 | 126 | #vis fn #command_name(#command_args) -> ::std::io::Result<::bitbar::attr::Params> { 127 | ::std::io::Result::Ok( 128 | ::bitbar::attr::Params::new(::std::env::current_exe()?.into_os_string().into_string().expect("non-UTF-8 plugin path"), #command_params) 129 | ) 130 | } 131 | }) 132 | } 133 | 134 | /// Defines a function that is called when no other `bitbar::command` matches. 135 | /// 136 | /// * It must take as arguments the subcommand name as a `String` and the remaining arguments as a `Vec`. 137 | /// * It must return a member of the `bitbar::CommandOutput` trait. 138 | /// * It can be a `fn` or an `async fn`. In the latter case, `tokio`'s threaded runtime will be used. (This requires the `tokio` feature, which is on by default.) 139 | /// 140 | /// If this attribute isn't used, `bitbar` will handle unknown subcommands by sending a notification and exiting. 141 | /// 142 | /// The function must also be registered via `#[bitbar::main(fallback_command = "...")]`. 143 | #[proc_macro_attribute] 144 | pub fn fallback_command(_: TokenStream, item: TokenStream) -> TokenStream { 145 | let fallback_fn = parse_macro_input!(item as ItemFn); 146 | let asyncness = &fallback_fn.sig.asyncness; 147 | let fn_name = &fallback_fn.sig.ident; 148 | let wrapper_name = Ident::new(&format!("bitbar_{fn_name}_wrapper"), Span::call_site()); 149 | let awaitness = asyncness.as_ref().map(|_| quote!(.await)); 150 | let wrapper_body = quote! { 151 | ::bitbar::CommandOutput::report(#fn_name(cmd.clone(), args)#awaitness, &cmd); 152 | }; 153 | #[cfg(not(feature = "tokio"))] let (wrapper_ret, wrapper_body) = (quote!(), wrapper_body); 154 | #[cfg(feature = "tokio")] let (wrapper_ret, wrapper_body) = ( 155 | quote!(-> ::std::pin::Pin<::std::boxed::Box>>), 156 | quote!(::std::boxed::Box::pin(async move { #wrapper_body })), 157 | ); 158 | TokenStream::from(quote! { 159 | fn #wrapper_name(cmd: ::std::string::String, args: ::std::vec::Vec<::std::string::String>) #wrapper_ret { 160 | #fallback_fn 161 | 162 | #wrapper_body 163 | } 164 | }) 165 | } 166 | 167 | /// Annotate your `main` function with this. 168 | /// 169 | /// * It can optionally take an argument of type `bitbar::Flavor`. 170 | /// * It must return a member of the `bitbar::MainOutput` trait. 171 | /// * It can be a `fn` or an `async fn`. In the latter case, `tokio`'s threaded runtime will be used. (This requires the `tokio` feature, which is on by default.) 172 | /// 173 | /// The `main` attribute optionally takes the following parameter: 174 | /// 175 | /// * `commands` can be set to a list of subcommand names (in parentheses) which will be used if the binary is called with command-line parameters. 176 | /// * `fallback_command` can be set to a function name (in quotes) which will be used if the binary is called with command-line parameters and the first parameter does not match any subcommand. 177 | /// * `error_template_image` can be set to a path (relative to the current file) to a PNG file which will be used as the template image for the menu when displaying an error. 178 | #[proc_macro_attribute] 179 | pub fn main(args: TokenStream, item: TokenStream) -> TokenStream { 180 | let args = parse_macro_input!(args with Punctuated::::parse_terminated); 181 | let mut error_template_image = quote!(::core::option::Option::None); 182 | let mut fallback_lit = None; 183 | let mut subcommand_names = Vec::default(); 184 | let mut subcommand_fns = Vec::default(); 185 | for arg in args { 186 | if arg.path().is_ident("commands") { 187 | match arg.require_list() { 188 | Ok(list) => match list.parse_args_with(Punctuated::::parse_terminated) { 189 | Ok(nested) => for cmd in nested { 190 | match cmd.require_path_only() { 191 | Ok(path) => if let Some(ident) = path.get_ident() { 192 | subcommand_names.push(ident.to_string()); 193 | subcommand_fns.push(Ident::new(&format!("bitbar_{ident}_wrapper"), ident.span())); 194 | } else { 195 | return quote_spanned! {cmd.span()=> 196 | compile_error!("bitbar subcommands must be simple identifiers"); 197 | }.into() 198 | }, 199 | Err(e) => return e.into_compile_error().into(), 200 | } 201 | }, 202 | Err(e) => return e.into_compile_error().into(), 203 | } 204 | Err(e) => return e.into_compile_error().into(), 205 | } 206 | } else if arg.path().is_ident("error_template_image") { 207 | match arg.require_name_value() { 208 | Ok(MetaNameValue { value, .. }) => if let Expr::Lit(ExprLit { lit: Lit::Str(lit), .. }) = value { 209 | error_template_image = quote!(::core::option::Option::Some(::bitbar::attr::Image::from(&include_bytes!(#lit)[..]))); 210 | } else { 211 | return quote_spanned! {value.span()=> 212 | compile_error!("error_template_image value must be a string literal"); 213 | }.into() 214 | }, 215 | Err(e) => return e.into_compile_error().into(), 216 | } 217 | } else if arg.path().is_ident("fallback_command") { 218 | match arg.require_name_value() { 219 | Ok(MetaNameValue { value, .. }) => if let Expr::Lit(ExprLit { lit: Lit::Str(lit), .. }) = value { 220 | fallback_lit = Some(Ident::new(&format!("bitbar_{}_wrapper", lit.value()), lit.span())); 221 | } else { 222 | return quote_spanned! {value.span()=> 223 | compile_error!("fallback_command value must be a string literal"); 224 | }.into() 225 | }, 226 | Err(e) => return e.into_compile_error().into(), 227 | } 228 | } else { 229 | return quote_spanned! {arg.span()=> 230 | compile_error!("unexpected bitbar::main attribute argument"); 231 | }.into() 232 | } 233 | } 234 | let main_fn = parse_macro_input!(item as ItemFn); 235 | let asyncness = &main_fn.sig.asyncness; 236 | let inner_params = &main_fn.sig.inputs; 237 | let inner_args = if inner_params.len() >= 1 { 238 | quote!(::bitbar::Flavor::check()) 239 | } else { 240 | quote!() 241 | }; 242 | #[cfg(not(feature = "tokio"))] let (cmd_awaitness, wrapper_body) = ( 243 | quote!(), 244 | quote!(::bitbar::MainOutput::main_output(main_inner(#inner_args), #error_template_image);), 245 | ); 246 | #[cfg(feature = "tokio")] let awaitness = asyncness.as_ref().map(|_| quote!(.await)); 247 | #[cfg(feature = "tokio")] let (cmd_awaitness, wrapper_body) = ( 248 | quote!(.await), 249 | quote!(::bitbar::AsyncMainOutput::main_output(main_inner(#inner_args)#awaitness, #error_template_image).await;), 250 | ); 251 | let fallback = if let Some(fallback_lit) = fallback_lit { 252 | quote!(#fallback_lit(subcommand, args.collect())#cmd_awaitness) 253 | } else { 254 | quote! {{ 255 | ::bitbar::notify(format!("no such subcommand: {}", subcommand)); 256 | ::std::process::exit(1) 257 | }} 258 | }; 259 | let wrapper_body = quote!({ 260 | //TODO set up a more friendly panic hook (similar to human-panic but rendering the panic message as a menu) 261 | let mut args = ::std::env::args(); 262 | let _ = args.next().expect("missing program name"); 263 | if let ::core::option::Option::Some(subcommand) = args.next() { 264 | match &*subcommand { 265 | #( 266 | #subcommand_names => #subcommand_fns(args.collect())#cmd_awaitness, 267 | )* 268 | _ => #fallback, 269 | } 270 | } else { 271 | #wrapper_body 272 | } 273 | }); 274 | #[cfg(feature = "tokio")] let wrapper_body = quote!({ 275 | ::bitbar::tokio::runtime::Builder::new_multi_thread() 276 | .enable_all() 277 | .build() 278 | .unwrap() 279 | .block_on(async #wrapper_body) 280 | }); 281 | let ret = main_fn.sig.output; 282 | let inner_body = main_fn.block; 283 | TokenStream::from(quote! { 284 | #asyncness fn main_inner(#inner_params) #ret #inner_body 285 | 286 | fn main() #wrapper_body 287 | }) 288 | } 289 | -------------------------------------------------------------------------------- /crate/bitbar/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bitbar" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | description = "Helper library for writing BitBar plugins" 7 | license = "MIT" 8 | readme = "../../README.md" 9 | repository.workspace = true 10 | keywords = ["plugin", "macos", "menu-bar", "swiftbar", "xbar"] 11 | 12 | [package.metadata.docs.rs] 13 | all-features = true 14 | rustdoc-args = ["--cfg", "docsrs"] 15 | 16 | [features] 17 | default = ["tokio"] 18 | assume-flavor = ["static_assertions"] 19 | tokio = ["dep:tokio", "futures", "bitbar-derive/tokio"] 20 | 21 | [dependencies] 22 | css-color-parser = "0.1.2" 23 | if_chain = "1" 24 | open = "5" 25 | semver = "1" 26 | thiserror = "1" 27 | url = "2" 28 | 29 | [dependencies.base64] 30 | version = "0.21" 31 | optional = true 32 | 33 | [dependencies.bitbar-derive] 34 | path = "../bitbar-derive" 35 | version = "=0.10.1" 36 | 37 | [dependencies.css-colors] 38 | version = "1" 39 | optional = true 40 | 41 | [dependencies.futures] 42 | version = "0.3" 43 | optional = true 44 | 45 | [dependencies.image] 46 | version = "0.24" 47 | optional = true 48 | 49 | [dependencies.serenity] 50 | version = "0.11" 51 | default-features = false 52 | features = [ 53 | "model", "rustls_backend", #TODO remove once https://github.com/serenity-rs/serenity/issues/763 is fixed 54 | "utils" 55 | ] 56 | optional = true 57 | 58 | [dependencies.static_assertions] 59 | version = "1" 60 | optional = true 61 | 62 | [dependencies.tokio] # used in proc macro 63 | version = "1" 64 | features = ["macros", "rt-multi-thread"] 65 | optional = true 66 | 67 | [target.'cfg(target_os = "macos")'.dependencies] 68 | notify-rust = "4" 69 | -------------------------------------------------------------------------------- /crate/bitbar/src/attr.rs: -------------------------------------------------------------------------------- 1 | //! Parameters for modifying the appearance or behavior of [`ContentItem`]s. 2 | 3 | use { 4 | std::{ 5 | convert::{ 6 | TryFrom, 7 | TryInto, 8 | }, 9 | fmt, 10 | str::FromStr, 11 | }, 12 | css_color_parser::ColorParseError, 13 | url::Url, 14 | crate::{ 15 | ContentItem, 16 | Menu, 17 | }, 18 | }; 19 | #[cfg(feature = "base64")] use base64::{ 20 | Engine as _, 21 | engine::general_purpose::STANDARD as BASE64, 22 | }; 23 | #[cfg(all(feature = "base64", feature = "image"))] use { 24 | std::io::Cursor, 25 | image::{ 26 | DynamicImage, 27 | ImageError, 28 | ImageOutputFormat::Png, 29 | ImageResult, 30 | }, 31 | }; 32 | 33 | /// Used in [`ContentItem::color`](ContentItem::color()). 34 | /// 35 | /// Construct via [`Into`] or [`TryInto`](std::convert::TryInto) implementations. 36 | #[derive(Debug, Clone, Copy, PartialEq)] 37 | pub struct Color { 38 | pub(crate) light: css_color_parser::Color, 39 | /// SwiftBar only: separate color for dark system theme. If `None`, use `light`. 40 | pub(crate) dark: Option, 41 | } 42 | 43 | impl From for Color { 44 | fn from(light: css_color_parser::Color) -> Color { 45 | Color { light, dark: None } 46 | } 47 | } 48 | 49 | impl FromStr for Color { 50 | type Err = ColorParseError; 51 | 52 | fn from_str(s: &str) -> Result { 53 | Ok(Color { 54 | light: s.parse()?, 55 | dark: None, 56 | }) 57 | } 58 | } 59 | 60 | impl<'a> TryFrom<&'a str> for Color { 61 | type Error = ColorParseError; 62 | 63 | fn try_from(s: &str) -> Result { 64 | s.parse() 65 | } 66 | } 67 | 68 | #[cfg(feature = "css-colors")] 69 | macro_rules! css_color_try_into_color { 70 | ($t:ty) => { 71 | #[cfg_attr(docsrs, doc(cfg(feature = "css-colors")))] 72 | impl TryFrom<$t> for Color { 73 | type Error = ColorParseError; 74 | 75 | fn try_from(color: $t) -> Result { 76 | Ok(Color { 77 | light: color.to_string().parse()?, 78 | dark: None, 79 | }) 80 | } 81 | } 82 | }; 83 | } 84 | 85 | #[cfg(feature = "css-colors")] css_color_try_into_color!(css_colors::RGB); 86 | #[cfg(feature = "css-colors")] css_color_try_into_color!(css_colors::RGBA); 87 | #[cfg(feature = "css-colors")] css_color_try_into_color!(css_colors::HSL); 88 | #[cfg(feature = "css-colors")] css_color_try_into_color!(css_colors::HSLA); 89 | 90 | #[cfg(feature = "serenity")] 91 | #[cfg_attr(docsrs, doc(cfg(feature = "serenity")))] 92 | impl From for Color { 93 | fn from(c: serenity::utils::Colour) -> Color { 94 | Color { 95 | light: css_color_parser::Color { 96 | r: c.r(), 97 | g: c.g(), 98 | b: c.b(), 99 | a: 1.0, 100 | }, 101 | dark: None, 102 | } 103 | } 104 | } 105 | 106 | impl fmt::Display for Color { 107 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 108 | write!(f, "#{:02x}{:02x}{:02x}", self.light.r, self.light.g, self.light.b)?; 109 | if let Some(dark) = self.dark { 110 | write!(f, ",#{:02x}{:02x}{:02x}", dark.r, dark.g, dark.b)?; 111 | } 112 | Ok(()) 113 | } 114 | } 115 | 116 | #[derive(Debug)] 117 | /// A menu item's alternate mode or submenu. 118 | pub enum Extra { 119 | /// A menu item's alternate mode, shown when is held. 120 | Alternate(Box), //TODO make sure alts don't have submenus 121 | /// A submenu. 122 | Submenu(Menu), 123 | } 124 | 125 | /// Used by [`ContentItem::href`](ContentItem::href()). 126 | pub trait IntoUrl { 127 | /// Converts `self` into a [`Url`]. 128 | fn into_url(self) -> Result; 129 | } 130 | 131 | impl IntoUrl for Url { 132 | fn into_url(self) -> Result { 133 | Ok(self) 134 | } 135 | } 136 | 137 | impl IntoUrl for String { 138 | fn into_url(self) -> Result { 139 | Url::parse(&self) 140 | } 141 | } 142 | 143 | impl<'a> IntoUrl for &'a str { 144 | fn into_url(self) -> Result { 145 | Url::parse(self) 146 | } 147 | } 148 | 149 | /// BitBar only supports up to five parameters for `bash=` commands (see ). 150 | #[derive(Debug)] 151 | pub struct Params { 152 | pub(crate) cmd: String, 153 | pub(crate) params: Vec, 154 | } 155 | 156 | impl Params { 157 | #[doc(hidden)] // used in proc macro 158 | pub fn new(cmd: String, params: Vec) -> Self { 159 | Self { cmd, params } 160 | } 161 | } 162 | 163 | macro_rules! params_from { 164 | ($n:literal$(, $elt:ident: $t:ident)*) => { 165 | impl From<[T; $n]> for Params { 166 | fn from([cmd, $($elt),*]: [T; $n]) -> Params { 167 | Params { 168 | cmd: cmd.to_string(), 169 | params: vec![$($elt.to_string()),*], 170 | } 171 | } 172 | } 173 | 174 | impl From<(Cmd, $($t),*)> for Params { 175 | fn from((cmd, $($elt),*): (Cmd, $($t),*)) -> Params { 176 | Params { 177 | cmd: cmd.to_string(), 178 | params: vec![$($elt.to_string()),*], 179 | } 180 | } 181 | } 182 | }; 183 | } 184 | 185 | params_from!(1); 186 | params_from!(2, param1: A); 187 | params_from!(3, param1: A, param2: B); 188 | params_from!(4, param1: A, param2: B, param3: C); 189 | params_from!(5, param1: A, param2: B, param3: C, param4: D); 190 | params_from!(6, param1: A, param2: B, param3: C, param4: D, param5: E); 191 | 192 | impl<'a, T: ToString> TryFrom<&'a [T]> for Params { 193 | type Error = &'a [T]; 194 | 195 | fn try_from(slice: &[T]) -> Result { 196 | match slice { 197 | [cmd] => Ok(Params { cmd: cmd.to_string(), params: Vec::default() }), 198 | [cmd, param1] => Ok(Params { cmd: cmd.to_string(), params: vec![param1.to_string()] }), 199 | [cmd, param1, param2] => Ok(Params { cmd: cmd.to_string(), params: vec![param1.to_string(), param2.to_string()] }), 200 | [cmd, param1, param2, param3] => Ok(Params { cmd: cmd.to_string(), params: vec![param1.to_string(), param2.to_string(), param3.to_string()] }), 201 | [cmd, param1, param2, param3, param4] => Ok(Params { cmd: cmd.to_string(), params: vec![param1.to_string(), param2.to_string(), param3.to_string(), param4.to_string()] }), 202 | [cmd, param1, param2, param3, param4, param5] => Ok(Params { cmd: cmd.to_string(), params: vec![param1.to_string(), param2.to_string(), param3.to_string(), param4.to_string(), param5.to_string()] }), 203 | slice => Err(slice), 204 | } 205 | } 206 | } 207 | 208 | impl TryFrom> for Params { 209 | type Error = Vec; 210 | 211 | fn try_from(mut v: Vec) -> Result> { 212 | match v.len() { 213 | 1..=6 => Ok(Params { 214 | cmd: v.remove(0).to_string(), 215 | params: v.into_iter().map(|x| x.to_string()).collect(), 216 | }), 217 | _ => Err(v), 218 | } 219 | } 220 | } 221 | 222 | /// Used by [`ContentItem::command`](ContentItem::command()). 223 | /// 224 | /// A `Command` contains the [`Params`], which includes the actual command (called `bash=` by BitBar) and its parameters, and the value of `terminal=`. 225 | /// 226 | /// It is usually constructed via conversion, unless `terminal=true` is required. 227 | /// 228 | /// **Note:** Unlike BitBar's default of `true`, `Command` assumes a default of `terminal=false`. 229 | #[derive(Debug)] 230 | pub struct Command { 231 | pub(crate) params: Params, 232 | pub(crate) terminal: bool, 233 | } 234 | 235 | impl Command { 236 | /// Creates a `Command` with the `terminal=` value set to `true`. 237 | pub fn terminal(args: impl Into) -> Command { 238 | Command { 239 | params: args.into(), 240 | terminal: true, 241 | } 242 | } 243 | 244 | /// Attempts to construct a `Command` with `terminal=` set to `false` from the given arguments. 245 | /// 246 | /// This is not a `TryFrom` implementation due to a limitation in Rust. 247 | pub fn try_from>(args: P) -> Result { 248 | Ok(Command { 249 | params: args.try_into()?, 250 | terminal: false, 251 | }) 252 | } 253 | 254 | /// Same as `Command::terminal` but for types that might not convert to `Params`. 255 | pub fn try_terminal>(args: P) -> Result { 256 | Ok(Command { 257 | params: args.try_into()?, 258 | terminal: true, 259 | }) 260 | } 261 | } 262 | 263 | /// Converts an array containing a command string and 0–5 parameters to a command argument vector. The `terminal=` value will be `false`. 264 | impl> From

for Command { 265 | fn from(args: P) -> Command { 266 | Command { 267 | params: args.into(), 268 | terminal: false, 269 | } 270 | } 271 | } 272 | 273 | /// Used by `ContentItem::image` and `ContentItem::template_image`. 274 | #[derive(Debug, Clone)] 275 | pub struct Image { 276 | /// The base64-encoded image data. 277 | pub base64_data: String, 278 | /// If this is `true`, the image will be used with BitBar's `templateImage=` instead of `image=`. 279 | pub is_template: bool, 280 | } 281 | 282 | impl Image { 283 | /// Constructs a template image, even if the `TryInto` implementation would otherwise construct a non-template image. 284 | pub fn template>(img: T) -> Result { 285 | let mut result = img.try_into()?; 286 | result.is_template = true; 287 | Ok(result) 288 | } 289 | } 290 | 291 | /// Converts already-encoded base64 data to a non-template image. 292 | impl From for Image { 293 | fn from(base64_data: String) -> Image { 294 | Image { 295 | base64_data, 296 | is_template: false, 297 | } 298 | } 299 | } 300 | 301 | /// Converts a PNG file to a non-template image. 302 | #[cfg(feature = "base64")] 303 | #[cfg_attr(docsrs, doc(cfg(feature = "base64")))] 304 | impl From> for Image { 305 | fn from(input: Vec) -> Image { 306 | Image { 307 | base64_data: BASE64.encode(&input), 308 | is_template: false, 309 | } 310 | } 311 | } 312 | 313 | /// Converts a PNG file to a non-template image. 314 | #[cfg(feature = "base64")] 315 | #[cfg_attr(docsrs, doc(cfg(feature = "base64")))] 316 | impl> From<&T> for Image { 317 | fn from(input: &T) -> Image { 318 | Image { 319 | base64_data: BASE64.encode(input), 320 | is_template: false, 321 | } 322 | } 323 | } 324 | 325 | #[cfg(all(feature = "base64", feature = "image"))] 326 | #[cfg_attr(docsrs, doc(cfg(all(feature = "base64", feature = "image"))))] 327 | impl TryFrom for Image { 328 | type Error = ImageError; 329 | 330 | fn try_from(img: DynamicImage) -> ImageResult { 331 | let mut buf = Cursor::>::default(); 332 | img.write_to(&mut buf, Png)?; 333 | Ok(Image::from(buf.into_inner())) 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /crate/bitbar/src/flavor/mod.rs: -------------------------------------------------------------------------------- 1 | //! Features specific to individual BitBar implementations (e.g. [SwiftBar](https://swiftbar.app/)) 2 | 3 | use std::{ 4 | borrow::Cow, 5 | collections::BTreeMap, 6 | fmt, 7 | }; 8 | pub use self::swiftbar::SwiftBar; 9 | 10 | pub mod swiftbar; 11 | 12 | #[derive(Debug, Clone, Copy)] 13 | /// A BitBar implementation. 14 | pub enum Flavor { 15 | /// The original, now discontinued implementation, with just the base features. This is also returned if a plugin is run on its own. 16 | BitBar, 17 | /// [SwiftBar](https://swiftbar.app/) 18 | SwiftBar(SwiftBar), 19 | //TODO xbar support, blocked on https://github.com/matryer/xbar/issues/753 20 | //TODO Argos (https://github.com/p-e-w/argos) support? (envar ARGOS_VERSION) 21 | //TODO kargos (https://github.com/lipido/kargos) support? (needs envar) 22 | } 23 | 24 | impl Flavor { 25 | /// Checks which of the supported BitBar implementations the plugin is currently running on, 26 | /// returning a handle allowing use of implementation-specific features, if any are supported. 27 | /// Any unsupported implementation will be reported as `BitBar`. 28 | pub fn check() -> Flavor { 29 | if let Some(swiftbar) = SwiftBar::check() { 30 | Flavor::SwiftBar(swiftbar) 31 | } else { 32 | Flavor::BitBar 33 | } 34 | } 35 | } 36 | 37 | impl fmt::Display for Flavor { 38 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 39 | match self { 40 | Flavor::SwiftBar(_) => write!(f, "SwiftBar"), 41 | Flavor::BitBar => write!(f, "BitBar"), 42 | } 43 | } 44 | } 45 | 46 | /// Flavor-specific [`ContentItem`](crate::ContentItem) attributes. 47 | #[derive(Debug)] 48 | #[allow(missing_docs)] 49 | pub enum Attrs { 50 | SwiftBar(swiftbar::Attrs), 51 | } 52 | 53 | impl Attrs { 54 | pub(crate) fn render<'a>(&'a self, rendered_params: &mut BTreeMap, Cow<'a, str>>) { 55 | match self { 56 | Attrs::SwiftBar(params) => params.render(rendered_params), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crate/bitbar/src/flavor/swiftbar.rs: -------------------------------------------------------------------------------- 1 | //! Features specific to [SwiftBar](https://swiftbar.app/) 2 | 3 | use { 4 | std::{ 5 | borrow::Cow, 6 | collections::BTreeMap, 7 | convert::TryInto, 8 | env, 9 | io, 10 | iter, 11 | path::Path, 12 | sync::Arc, 13 | }, 14 | open::that as open, 15 | semver::Version, 16 | thiserror::Error, 17 | url::Url, 18 | crate::{ 19 | ContentItem, 20 | MainOutput, 21 | Menu, 22 | MenuItem, 23 | attr::{ 24 | Color, 25 | Command, 26 | Image, 27 | IntoUrl, 28 | Params, 29 | }, 30 | }, 31 | }; 32 | #[cfg(feature = "assume-flavor")] use static_assertions::const_assert; 33 | #[cfg(feature = "tokio")] use { 34 | std::pin::Pin, 35 | futures::{ 36 | future::Future, 37 | stream::StreamExt as _, 38 | }, 39 | crate::AsyncMainOutput, 40 | }; 41 | 42 | /// The highest build number checked for conditional features. 43 | #[cfg(feature = "assume-flavor")] const MAX_BUILD: usize = 402; 44 | 45 | macro_rules! build_ge { 46 | ($swiftbar:expr, $build:expr) => {{ 47 | #[cfg(feature = "assume-flavor")] const_assert!($build <= MAX_BUILD); 48 | $swiftbar.build >= $build 49 | }}; 50 | } 51 | 52 | /// A type-safe handle for [SwiftBar](https://swiftbar.app/)-specific features. 53 | /// 54 | /// Some SwiftBar-specific features are currently unsupported: 55 | /// 56 | /// * [Script metadata](https://github.com/swiftbar/SwiftBar#script-metadata) is unsupported since `cargo` does not support adding metadata to binaries it produces. You will have to [add any metadata via `xattr`](https://github.com/swiftbar/SwiftBar#metadata-for-binary-plugins). 57 | #[derive(Debug, Clone, Copy)] 58 | pub struct SwiftBar { 59 | build: usize, 60 | } 61 | 62 | impl SwiftBar { 63 | /// Checks whether the plugins is running in SwiftBar by checking environment variables. 64 | /// If it does, returns a handle allowing use of SwiftBar-specific features. 65 | pub fn check() -> Option { 66 | Some(Self { 67 | build: env::var("SWIFTBAR_BUILD").ok()?.parse().ok()?, 68 | }) 69 | } 70 | 71 | #[cfg(feature = "assume-flavor")] 72 | #[cfg_attr(docsrs, doc(cfg(feature = "assume-flavor")))] 73 | /// Returns a handle allowing use of SwiftBar-specific features **without checking whether the plugin is actually running inside SwiftBar**. 74 | /// If the plugin is actually running in a different implementation or an outdated version of SwiftBar, this may lead to incorrect behavior. 75 | pub fn assume() -> Self { 76 | Self { 77 | build: MAX_BUILD, 78 | } 79 | } 80 | 81 | /// The name of the plugin, including refresh time and file extension, as used in [`swiftbar:` URLs](https://github.com/swiftbar/SwiftBar#url-scheme). 82 | pub fn plugin_name(&self) -> Result { 83 | Ok(Path::new(&env::var_os("SWIFTBAR_PLUGIN_PATH").ok_or(PluginNameError::Env)?) 84 | .file_name().ok_or(PluginNameError::NoFileName)? 85 | .to_str().ok_or(PluginNameError::NonUtf8FileName)? 86 | .to_owned() 87 | ) 88 | } 89 | 90 | /// Returns the SwiftBar version on which the plugin is running by checking environment variables. 91 | pub fn running_version(&self) -> Result { 92 | Ok(env::var("SWIFTBAR_VERSION")?.parse()?) 93 | } 94 | 95 | /// Unlike BitBar, SwiftBar supports more than 5 parameters for `bash=` commands. 96 | pub fn command(&self, cmd: impl IntoParams) -> Params { 97 | cmd.into_params(self) 98 | } 99 | 100 | /// Returns a [`Color`](crate::param::Color) that renders differently depending on whether the system is in dark mode. 101 | pub fn themed_color(&self, light: Color, dark: Color) -> Color { 102 | Color { 103 | light: light.light, 104 | dark: Some(dark.dark.unwrap_or(dark.light)), 105 | } 106 | } 107 | 108 | /// Adds a [SF Symbols](https://developer.apple.com/sf-symbols/) image to a menu item. 109 | pub fn sf_image(&self, item: &mut ContentItem, image: impl ToString) { 110 | Attrs::for_item(item).sf_image = Some(image.to_string()); 111 | } 112 | 113 | /// Adds a checkmark to a menu item. 114 | pub fn checked(&self, item: &mut ContentItem) { 115 | Attrs::for_item(item).checked = true; 116 | } 117 | } 118 | 119 | /// A type that can be used as `bash=` command parameters for SwiftBar, which unlike BitBar supports more than five parameters. 120 | pub trait IntoParams { 121 | /// Converts this value into command parameters. 122 | /// 123 | /// Equivalent to `swiftbar.command(self)`. 124 | fn into_params(self, swiftbar: &SwiftBar) -> Params; 125 | } 126 | 127 | impl IntoParams for Params { 128 | fn into_params(self, _: &SwiftBar) -> Params { 129 | self 130 | } 131 | } 132 | 133 | macro_rules! impl_into_params { 134 | ($n:literal$(, $elt:ident: $t:ident)*) => { 135 | impl IntoParams for [T; $n] { 136 | fn into_params(self, _: &SwiftBar) -> Params { 137 | let [cmd, $($elt),*] = self; 138 | Params { 139 | cmd: cmd.to_string(), 140 | params: vec![$($elt.to_string()),*], 141 | } 142 | } 143 | } 144 | 145 | impl IntoParams for (Cmd, $($t),*) { 146 | fn into_params(self, _: &SwiftBar) -> Params { 147 | let (cmd, $($elt),*) = self; 148 | Params { 149 | cmd: cmd.to_string(), 150 | params: vec![$($elt.to_string()),*], 151 | } 152 | } 153 | } 154 | }; 155 | } 156 | 157 | impl_into_params!(1); 158 | impl_into_params!(2, param1: A); 159 | impl_into_params!(3, param1: A, param2: B); 160 | impl_into_params!(4, param1: A, param2: B, param3: C); 161 | impl_into_params!(5, param1: A, param2: B, param3: C, param4: D); 162 | impl_into_params!(6, param1: A, param2: B, param3: C, param4: D, param5: E); 163 | impl_into_params!(7, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F); 164 | impl_into_params!(8, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F, param7: G); 165 | impl_into_params!(9, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F, param7: G, param8: H); 166 | impl_into_params!(10, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F, param7: G, param8: H, param9: I); 167 | impl_into_params!(11, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F, param7: G, param8: H, param9: I, param10: J); 168 | impl_into_params!(12, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F, param7: G, param8: H, param9: I, param10: J, param11: K); 169 | impl_into_params!(13, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F, param7: G, param8: H, param9: I, param10: J, param11: K, param12: L); 170 | impl_into_params!(14, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F, param7: G, param8: H, param9: I, param10: J, param11: K, param12: L, param13: M); 171 | impl_into_params!(15, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F, param7: G, param8: H, param9: I, param10: J, param11: K, param12: L, param13: M, param14: N); 172 | impl_into_params!(16, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F, param7: G, param8: H, param9: I, param10: J, param11: K, param12: L, param13: M, param14: N, param15: O); 173 | 174 | impl<'a, T: ToString> IntoParams for &'a [T] { 175 | /// # Panics 176 | /// 177 | /// If `self` is empty. 178 | fn into_params(self, _: &SwiftBar) -> Params { 179 | Params { 180 | cmd: self[0].to_string(), 181 | params: self[1..].iter().map(|param| param.to_string()).collect(), 182 | } 183 | } 184 | } 185 | 186 | impl IntoParams for Vec { 187 | /// # Panics 188 | /// 189 | /// If `self` is empty. 190 | fn into_params(mut self, _: &SwiftBar) -> Params { 191 | Params { 192 | cmd: self.remove(0).to_string(), 193 | params: self.into_iter().map(|param| param.to_string()).collect(), 194 | } 195 | } 196 | } 197 | 198 | /// Flavor-specific [`ContentItem`] attributes. 199 | #[derive(Debug)] 200 | pub struct Attrs { 201 | checked: bool, 202 | sf_image: Option, 203 | } 204 | 205 | impl Attrs { 206 | fn for_item(item: &mut ContentItem) -> &mut Attrs { 207 | match item.flavor_attrs.get_or_insert_with(|| super::Attrs::SwiftBar(Attrs { 208 | checked: false, 209 | sf_image: None, 210 | })) { 211 | super::Attrs::SwiftBar(ref mut params) => params, 212 | } 213 | } 214 | 215 | pub(crate) fn render<'a>(&'a self, rendered_params: &mut BTreeMap, Cow<'a, str>>) { 216 | if self.checked { 217 | rendered_params.insert(Cow::Borrowed("checked"), Cow::Borrowed("true")); 218 | } 219 | if let Some(ref sf_image) = self.sf_image { 220 | rendered_params.insert(Cow::Borrowed("sfimage"), Cow::Borrowed(sf_image)); 221 | } 222 | } 223 | } 224 | 225 | /// An error that can occur when checking the running SwiftBar version. 226 | #[derive(Debug, Error, Clone)] 227 | pub enum VersionCheckError { 228 | /// The `SWIFTBAR_VERSION` environment variable was unset or not valid UTF-8 229 | #[error(transparent)] Env(#[from] env::VarError), 230 | /// The `SWIFTBAR_VERSION` environment variable was not a valid semantic version 231 | #[error(transparent)] Parse(Arc), 232 | } 233 | 234 | impl From for VersionCheckError { 235 | fn from(e: semver::Error) -> VersionCheckError { 236 | VersionCheckError::Parse(Arc::new(e)) 237 | } 238 | } 239 | 240 | impl From for Menu { 241 | fn from(e: VersionCheckError) -> Menu { 242 | let mut menu = vec![MenuItem::new("Error checking running SwiftBar version")]; 243 | match e { 244 | VersionCheckError::Env(e) => menu.push(MenuItem::new(e)), 245 | VersionCheckError::Parse(e) => { 246 | menu.push(MenuItem::new(format!("error parsing version: {}", e))); 247 | menu.push(MenuItem::new(format!("{:?}", e))); 248 | } 249 | } 250 | Menu(menu) 251 | } 252 | } 253 | 254 | /// An error that can occur when checking the running SwiftBar plugin name. 255 | #[derive(Debug, Error, Clone)] 256 | pub enum PluginNameError { 257 | /// The `SWIFTBAR_PLUGIN_PATH` environment variable was unset 258 | #[error("missing `SWIFTBAR_PLUGIN_PATH` environment variable")] 259 | Env, 260 | /// The `SWIFTBAR_PLUGIN_PATH` environment variable did not end in a file name 261 | #[error("no filename in `SWIFTBAR_PLUGIN_PATH` environment variable")] 262 | NoFileName, 263 | /// The file name was not valid UTF-8 264 | #[error("plugin filename is not valid UTF-8")] 265 | NonUtf8FileName, 266 | } 267 | 268 | impl From for Menu { 269 | fn from(e: PluginNameError) -> Menu { 270 | Menu(vec![ 271 | MenuItem::new("Error checking running SwiftBar plugin name"), 272 | MenuItem::new(e.to_string()), 273 | ]) 274 | } 275 | } 276 | 277 | /// An error that can occur in [`Notification::command`]. 278 | #[derive(Debug, Error, Clone)] 279 | pub enum NotificationCommandError> 280 | where C::Error: std::error::Error { 281 | /// Converting the parameter to a `Command` failed 282 | #[error(transparent)] Command(C::Error), 283 | /// Running commands on notification click is only supported on SwiftBar 1.4.3 beta 4 or newer 284 | #[error("running commands on notification click is only supported on SwiftBar 1.4.3 beta 4 or newer")] 285 | UnsupportedSwiftBarVersion, 286 | } 287 | 288 | impl> From> for Menu 289 | where C::Error: std::error::Error { 290 | fn from(e: NotificationCommandError) -> Menu { 291 | let mut menu = vec![MenuItem::new("Error adding command to notification")]; 292 | match e { 293 | NotificationCommandError::Command(e) => { 294 | menu.push(MenuItem::new(format!("error building command: {}", e))); 295 | menu.push(MenuItem::new(format!("{:?}", e))); 296 | } 297 | NotificationCommandError::UnsupportedSwiftBarVersion => menu.push(MenuItem::new("running commands on notification click is only supported on SwiftBar 1.4.3 beta 4 or newer")), 298 | } 299 | Menu(menu) 300 | } 301 | } 302 | 303 | /// A SwiftBar notification that can be opened as a URL. 304 | pub struct Notification { 305 | swiftbar: SwiftBar, 306 | plugin_name: String, 307 | title: Option, 308 | subtitle: Option, 309 | body: Option, 310 | href: Option, 311 | command: Option, 312 | silent: bool, 313 | } 314 | 315 | impl Notification { 316 | /// Creates a new notification with default options. 317 | /// 318 | /// Call methods on the returned instance to configure it. 319 | pub fn new(swiftbar: SwiftBar) -> Result { 320 | Ok(Self { 321 | swiftbar, 322 | plugin_name: swiftbar.plugin_name()?, 323 | title: None, 324 | subtitle: None, 325 | body: None, 326 | href: None, 327 | command: None, 328 | silent: false, 329 | }) 330 | } 331 | 332 | /// Sets the title for this notification. 333 | pub fn title(mut self, title: impl ToString) -> Self { 334 | self.title = Some(title.to_string()); 335 | self 336 | } 337 | 338 | /// Sets the subtitle for this notification. 339 | pub fn subtitle(mut self, subtitle: impl ToString) -> Self { 340 | self.subtitle = Some(subtitle.to_string()); 341 | self 342 | } 343 | 344 | /// Sets the text for this notification. 345 | pub fn body(mut self, body: impl ToString) -> Self { 346 | self.body = Some(body.to_string()); 347 | self 348 | } 349 | 350 | /// Adds an URL that will be opened when this notification is clicked. 351 | pub fn href(mut self, href: impl IntoUrl) -> Result { 352 | self.href = Some(href.into_url()?); 353 | Ok(self) 354 | } 355 | 356 | /// Makes this notification run the given command when clicked. 357 | pub fn command>(mut self, cmd: C) -> Result> 358 | where C::Error: std::error::Error { 359 | if build_ge!(self.swiftbar, 402) { 360 | self.command = Some(cmd.try_into().map_err(NotificationCommandError::Command)?); 361 | Ok(self) 362 | } else { 363 | Err(NotificationCommandError::UnsupportedSwiftBarVersion) 364 | } 365 | } 366 | 367 | /// Disables sound for this notification. 368 | pub fn silent(mut self) -> Self { 369 | self.silent = true; 370 | self 371 | } 372 | 373 | /// Displays this notification. 374 | pub fn send(&self) -> io::Result<()> { 375 | open(self.into_url().expect("failed to build SwiftBar notification URL").as_str()) 376 | } 377 | } 378 | 379 | impl IntoUrl for Notification { 380 | fn into_url(self) -> Result { 381 | (&self).into_url() 382 | } 383 | } 384 | 385 | impl<'a> IntoUrl for &'a Notification { 386 | fn into_url(self) -> Result { 387 | let Notification { swiftbar: _, plugin_name, title, subtitle, body, command, href, silent } = self; 388 | Url::parse_with_params("swiftbar://notify", iter::once((Cow::Borrowed("plugin"), &**plugin_name)) 389 | .chain(title.as_deref().map(|title| (Cow::Borrowed("title"), title))) 390 | .chain(subtitle.as_deref().map(|subtitle| (Cow::Borrowed("subtitle"), subtitle))) 391 | .chain(body.as_deref().map(|body| (Cow::Borrowed("body"), body))) 392 | .chain(command.iter().flat_map(|command| iter::once((Cow::Borrowed("bash"), &*command.params.cmd)) 393 | .chain(command.params.params.iter().enumerate().map(|(n, arg)| (Cow::Owned(format!("param{}", n + 1)), &**arg))) 394 | .chain((!command.terminal).then(|| (Cow::Borrowed("terminal"), "false"))) 395 | )) 396 | .chain(href.as_ref().map(|href| (Cow::Borrowed("href"), href.as_str()))) 397 | .chain(silent.then(|| (Cow::Borrowed("silent"), "true"))) 398 | ) 399 | } 400 | } 401 | 402 | /// A type that [streams](https://github.com/swiftbar/SwiftBar#streamable) menus from an iterator. 403 | /// 404 | /// Note that the following [plugin metadata](https://github.com/swiftbar/SwiftBar#script-metadata) items must be set for this to work: 405 | /// * `streamable` 406 | /// * `true` 407 | /// 408 | /// The [`cargo-bitbar`](https://crates.io/crates/cargo-bitbar) crate can be used to add this metadata to the plugin. First, add this to your *workspace* manifest: 409 | /// 410 | /// ```toml 411 | /// [workspace.metadata.bitbar] 412 | /// type = "streamable" 413 | /// ``` 414 | /// 415 | /// Then, after building the plugin, run `cargo bitbar attr target/release/my-bitbar-plugin`. 416 | pub struct BlockingStream<'a, I: MainOutput> { 417 | swiftbar: SwiftBar, 418 | inner: Box + 'a>, 419 | } 420 | 421 | impl<'a, I: MainOutput> BlockingStream<'a, I> { 422 | #[allow(missing_docs)] 423 | pub fn new(swiftbar: SwiftBar, iter: impl IntoIterator + 'a) -> Self { 424 | Self { swiftbar, inner: Box::new(iter.into_iter()) } 425 | } 426 | } 427 | 428 | impl<'a, I: MainOutput> MainOutput for BlockingStream<'a, I> { 429 | fn main_output(self, error_template_image: Option) { 430 | if build_ge!(self.swiftbar, 399) { 431 | for elt in self.inner { 432 | elt.main_output(error_template_image.clone()); 433 | println!("~~~"); 434 | } 435 | } else { 436 | for elt in self.inner { 437 | println!("~~~"); 438 | elt.main_output(error_template_image.clone()); 439 | } 440 | } 441 | } 442 | } 443 | 444 | #[cfg(feature = "tokio")] 445 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] 446 | /// A type that [streams](https://github.com/swiftbar/SwiftBar#streamable) menus from a stream (async iterator). 447 | /// 448 | /// Note that the following [plugin metadata](https://github.com/swiftbar/SwiftBar#script-metadata) items must be set for this to work: 449 | /// * `streamable` 450 | /// * `true` 451 | /// 452 | /// The [`cargo-bitbar`](https://crates.io/crates/cargo-bitbar) crate can be used to add this metadata to the plugin. First, add this to your *workspace* manifest: 453 | /// 454 | /// ```toml 455 | /// [workspace.metadata.bitbar] 456 | /// type = "streamable" 457 | /// ``` 458 | /// 459 | /// Then, after building the plugin, run `cargo bitbar attr target/release/my-bitbar-plugin`. 460 | pub struct Stream<'a, I: AsyncMainOutput<'a> + 'a> { 461 | swiftbar: SwiftBar, 462 | inner: Pin + 'a>>, 463 | } 464 | 465 | #[cfg(feature = "tokio")] 466 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] 467 | impl<'a, I: AsyncMainOutput<'a> + 'a> Stream<'a, I> { 468 | #[allow(missing_docs)] 469 | pub fn new(swiftbar: SwiftBar, stream: impl futures::stream::Stream + 'a) -> Self { 470 | Self { swiftbar, inner: Box::pin(stream) } 471 | } 472 | } 473 | 474 | #[cfg(feature = "tokio")] 475 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] 476 | impl<'a, I: AsyncMainOutput<'a> + 'a> AsyncMainOutput<'a> for Stream<'a, I> { 477 | fn main_output(mut self, error_template_image: Option) -> Pin + 'a>> { 478 | if build_ge!(self.swiftbar, 399) { 479 | Box::pin(async move { 480 | while let Some(elt) = self.inner.next().await { 481 | elt.main_output(error_template_image.clone()).await; 482 | println!("~~~"); 483 | } 484 | }) 485 | } else { 486 | Box::pin(async move { 487 | while let Some(elt) = self.inner.next().await { 488 | println!("~~~"); 489 | elt.main_output(error_template_image.clone()).await; 490 | } 491 | }) 492 | } 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /crate/bitbar/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs, rust_2018_idioms, unused, unused_crate_dependencies, unused_import_braces, unused_qualifications, warnings)] 2 | #![forbid(unsafe_code)] 3 | 4 | #![cfg_attr(docsrs, feature(doc_cfg))] 5 | 6 | //! This is `bitbar`, a library crate which includes helpers for writing BitBar plugins in Rust. BitBar is a system that makes it easy to add menus to the macOS menu bar. There are two apps implementing the BitBar system: [SwiftBar](https://swiftbar.app/) and [xbar](https://xbarapp.com/). This crate supports both of them, as well as [the discontinued original BitBar app](https://github.com/matryer/xbar/tree/a595e3bdbb961526803b60be6fd32dd0c667b6ec). 7 | //! 8 | //! There are two main entry points: 9 | //! 10 | //! * It's recommended to use the [`main`](crate::main) attribute and write a `main` function that returns a [`Menu`](crate::Menu), along with optional [`command`](crate::command) functions and an optional [`fallback_command`](crate::fallback_command) function. 11 | //! * For additional control over your plugin's behavior, you can directly [`Display`](std::fmt::Display) a [`Menu`](crate::Menu). 12 | //! 13 | //! BitBar plugins must have filenames of the format `name.duration.extension`, even though macOS binaries normally don't have extensions. You will have to add an extension, e.g. `.o`, to make Rust binaries work as plugins. 14 | //! 15 | //! # Example 16 | //! 17 | //! ```rust 18 | //! use bitbar::{Menu, MenuItem}; 19 | //! 20 | //! #[bitbar::main] 21 | //! fn main() -> Menu { 22 | //! Menu(vec![ 23 | //! MenuItem::new("Title"), 24 | //! MenuItem::Sep, 25 | //! MenuItem::new("Menu Item"), 26 | //! ]) 27 | //! } 28 | //! ``` 29 | //! 30 | //! Or: 31 | //! 32 | //! ```rust 33 | //! use bitbar::{Menu, MenuItem}; 34 | //! 35 | //! fn main() { 36 | //! print!("{}", Menu(vec![ 37 | //! MenuItem::new("Title"), 38 | //! MenuItem::Sep, 39 | //! MenuItem::new("Menu Item"), 40 | //! ])); 41 | //! } 42 | //! ``` 43 | //! 44 | //! There is also [a list of real-world examples](https://github.com/fenhl/rust-bitbar#example-plugins). 45 | 46 | use { 47 | std::{ 48 | borrow::Cow, 49 | collections::BTreeMap, 50 | convert::TryInto, 51 | fmt, 52 | iter::FromIterator, 53 | process, 54 | vec, 55 | }, 56 | if_chain::if_chain, 57 | url::Url, 58 | }; 59 | #[cfg(feature = "tokio")] use std::{ 60 | future::Future, 61 | pin::Pin, 62 | }; 63 | pub use { 64 | bitbar_derive::{ 65 | command, 66 | fallback_command, 67 | main, 68 | }, 69 | crate::flavor::Flavor, 70 | }; 71 | #[cfg(feature = "tokio")] #[doc(hidden)] pub use tokio; 72 | 73 | pub mod attr; 74 | pub mod flavor; 75 | 76 | /// A menu item that's not a separator. 77 | #[derive(Debug, Default)] 78 | pub struct ContentItem { 79 | /// This menu item's main content text. 80 | /// 81 | /// Any `|` in the text will be displayed as `¦`, and any newlines will be displayed as spaces. 82 | pub text: String, 83 | /// This menu item's alternate-mode menu item or submenu. 84 | pub extra: Option, 85 | /// Corresponds to BitBar's `href=` parameter. 86 | pub href: Option, 87 | /// Corresponds to BitBar's `color=` parameter. 88 | pub color: Option, 89 | /// Corresponds to BitBar's `font=` parameter. 90 | pub font: Option, 91 | /// Corresponds to BitBar's `size=` parameter. 92 | pub size: Option, 93 | /// Corresponds to BitBar's `bash=`, `terminal=`, `param1=`, etc. parameters. 94 | pub command: Option, 95 | /// Corresponds to BitBar's `refresh=` parameter. 96 | pub refresh: bool, 97 | /// Corresponds to BitBar's `image=` or `templateImage=` parameter. 98 | pub image: Option, 99 | /// Parameters for flavor-specific features. 100 | pub flavor_attrs: Option, 101 | } 102 | 103 | impl ContentItem { 104 | /// Returns a new menu item with the given text. 105 | /// 106 | /// Any `|` in the text will be displayed as `¦`, and any newlines will be displayed as spaces. 107 | pub fn new(text: impl ToString) -> ContentItem { 108 | ContentItem { 109 | text: text.to_string(), 110 | ..ContentItem::default() 111 | } 112 | } 113 | 114 | /// Adds a submenu to this menu item. 115 | pub fn sub(mut self, items: impl IntoIterator) -> Self { 116 | self.extra = Some(attr::Extra::Submenu(Menu::from_iter(items))); 117 | self 118 | } 119 | 120 | /// Adds a clickable link to this menu item. 121 | pub fn href(mut self, href: impl attr::IntoUrl) -> Result { 122 | self.href = Some(href.into_url()?); 123 | Ok(self) 124 | } 125 | 126 | /// Sets this menu item's text color. Alpha channel is ignored. 127 | pub fn color>(mut self, color: C) -> Result { 128 | self.color = Some(color.try_into()?); 129 | Ok(self) 130 | } 131 | 132 | /// Sets this menu item's text font. 133 | pub fn font(mut self, font: impl ToString) -> Self { 134 | self.font = Some(font.to_string()); 135 | self 136 | } 137 | 138 | /// Sets this menu item's font size. 139 | pub fn size(mut self, size: usize) -> Self { 140 | self.size = Some(size); 141 | self 142 | } 143 | 144 | /// Make this menu item run the given command when clicked. 145 | pub fn command>(mut self, cmd: C) -> Result { 146 | self.command = Some(cmd.try_into()?); 147 | Ok(self) 148 | } 149 | 150 | /// Causes the BitBar plugin to be refreshed when this menu item is clicked. 151 | pub fn refresh(mut self) -> Self { 152 | self.refresh = true; 153 | self 154 | } 155 | 156 | /// Adds an alternate menu item, which is shown instead of this one as long as the option key ⌥ is held. 157 | pub fn alt(mut self, alt: impl Into) -> Self { 158 | self.extra = Some(attr::Extra::Alternate(Box::new(alt.into()))); 159 | self 160 | } 161 | 162 | /// Adds a template image to this menu item. 163 | pub fn template_image>(mut self, img: T) -> Result { 164 | self.image = Some(attr::Image::template(img)?); 165 | Ok(self) 166 | } 167 | 168 | /// Adds an image to this menu item. The image will not be considered a template image unless specified as such by the `img` parameter. 169 | pub fn image>(mut self, img: T) -> Result { 170 | self.image = Some(img.try_into()?); 171 | Ok(self) 172 | } 173 | 174 | fn render(&self, f: &mut fmt::Formatter<'_>, is_alt: bool) -> fmt::Result { 175 | // main text 176 | write!(f, "{}", self.text.replace('|', "¦").replace('\n', " "))?; 177 | // parameters 178 | let mut rendered_params = BTreeMap::default(); 179 | if let Some(ref href) = self.href { 180 | rendered_params.insert(Cow::Borrowed("href"), Cow::Borrowed(href.as_ref())); 181 | } 182 | if let Some(ref color) = self.color { 183 | rendered_params.insert(Cow::Borrowed("color"), Cow::Owned(color.to_string())); 184 | } 185 | if let Some(ref font) = self.font { 186 | rendered_params.insert(Cow::Borrowed("font"), Cow::Borrowed(font)); 187 | } 188 | if let Some(size) = self.size { 189 | rendered_params.insert(Cow::Borrowed("size"), Cow::Owned(size.to_string())); 190 | } 191 | if let Some(ref cmd) = self.command { 192 | //TODO (xbar) prefer “shell” over “bash” 193 | rendered_params.insert(Cow::Borrowed("bash"), Cow::Borrowed(&cmd.params.cmd)); 194 | for (i, param) in cmd.params.params.iter().enumerate() { 195 | rendered_params.insert(Cow::Owned(format!("param{}", i + 1)), Cow::Borrowed(param)); 196 | } 197 | if !cmd.terminal { 198 | rendered_params.insert(Cow::Borrowed("terminal"), Cow::Borrowed("false")); 199 | } 200 | } 201 | if self.refresh { 202 | rendered_params.insert(Cow::Borrowed("refresh"), Cow::Borrowed("true")); 203 | } 204 | if is_alt { 205 | rendered_params.insert(Cow::Borrowed("alternate"), Cow::Borrowed("true")); 206 | } 207 | if let Some(ref img) = self.image { 208 | rendered_params.insert(Cow::Borrowed(if img.is_template { "templateImage" } else { "image" }), Cow::Borrowed(&img.base64_data)); 209 | } 210 | if let Some(ref flavor_attrs) = self.flavor_attrs { 211 | flavor_attrs.render(&mut rendered_params); 212 | } 213 | if !rendered_params.is_empty() { 214 | write!(f, " |")?; 215 | for (name, value) in rendered_params { 216 | let quoted_value = if value.contains(' ') { 217 | Cow::Owned(format!("\"{}\"", value)) 218 | } else { 219 | value 220 | }; //TODO check for double quotes in value, fall back to single quotes? (test if BitBar supports these first) 221 | write!(f, " {}={}", name, quoted_value)?; 222 | } 223 | } 224 | writeln!(f)?; 225 | // additional items 226 | match &self.extra { 227 | Some(attr::Extra::Alternate(ref alt)) => { alt.render(f, true)?; } 228 | Some(attr::Extra::Submenu(ref sub)) => { 229 | let sub_fmt = format!("{}", sub); 230 | for line in sub_fmt.lines() { 231 | writeln!(f, "--{}", line)?; 232 | } 233 | } 234 | None => {} 235 | } 236 | Ok(()) 237 | } 238 | } 239 | 240 | impl fmt::Display for ContentItem { 241 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 242 | self.render(f, false) 243 | } 244 | } 245 | 246 | /// A menu item can either be a separator or a content item. 247 | #[derive(Debug)] 248 | pub enum MenuItem { 249 | /// A content item, i.e. any menu item that's not a separator. 250 | Content(ContentItem), 251 | /// A separator bar. 252 | Sep 253 | } 254 | 255 | impl MenuItem { 256 | /// Returns a new menu item with the given text. See `ContentItem::new` for details. 257 | pub fn new(text: impl fmt::Display) -> MenuItem { 258 | MenuItem::Content(ContentItem::new(text)) 259 | } 260 | } 261 | 262 | impl Default for MenuItem { 263 | fn default() -> MenuItem { 264 | MenuItem::Content(ContentItem::default()) 265 | } 266 | } 267 | 268 | impl From for MenuItem { 269 | fn from(i: ContentItem) -> MenuItem { 270 | MenuItem::Content(i) 271 | } 272 | } 273 | 274 | impl fmt::Display for MenuItem { 275 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 276 | match self { 277 | MenuItem::Content(content) => write!(f, "{}", content), 278 | MenuItem::Sep => writeln!(f, "---") 279 | } 280 | } 281 | } 282 | 283 | /// A BitBar menu. 284 | /// 285 | /// Usually constructed by calling [`collect`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect) on an [`Iterator`](https://doc.rust-lang.org/std/iter/trait.Iterator.html) of `MenuItem`s. 286 | #[derive(Debug, Default)] 287 | pub struct Menu(pub Vec); 288 | 289 | impl Menu { 290 | /// Adds a menu item to the bottom of the menu. 291 | pub fn push(&mut self, item: impl Into) { 292 | self.0.push(item.into()); 293 | } 294 | } 295 | 296 | impl> FromIterator for Menu { 297 | fn from_iter>(iter: T) -> Menu { 298 | Menu(iter.into_iter().map(Into::into).collect()) 299 | } 300 | } 301 | 302 | impl> Extend for Menu { 303 | fn extend>(&mut self, iter: T) { 304 | self.0.extend(iter.into_iter().map(Into::into)) 305 | } 306 | } 307 | 308 | impl IntoIterator for Menu { 309 | type Item = MenuItem; 310 | type IntoIter = vec::IntoIter; 311 | 312 | fn into_iter(self) -> vec::IntoIter { self.0.into_iter() } 313 | } 314 | 315 | /// This provides the main functionality of this crate: rendering a BitBar plugin. 316 | /// 317 | /// Note that the output this generates already includes a trailing newline, so it should be used with `print!` instead of `println!`. 318 | impl fmt::Display for Menu { 319 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 320 | for menu_item in &self.0 { 321 | write!(f, "{}", menu_item)?; 322 | } 323 | Ok(()) 324 | } 325 | } 326 | 327 | /// Members of this trait can be returned from a main function annotated with [`main`]. 328 | pub trait MainOutput { 329 | /// Displays this value as a menu, using the given template image in case of an error. 330 | fn main_output(self, error_template_image: Option); 331 | } 332 | 333 | impl> MainOutput for T { 334 | fn main_output(self, _: Option) { 335 | print!("{}", self.into()); 336 | } 337 | } 338 | 339 | /// In the `Err` case, the menu will be prefixed with a menu item displaying the `error_template_image` and the text `?`. 340 | impl MainOutput for Result { 341 | fn main_output(self, error_template_image: Option) { 342 | match self { 343 | Ok(x) => x.main_output(error_template_image), 344 | Err(e) => { 345 | let mut header = ContentItem::new("?"); 346 | if let Some(error_template_image) = error_template_image { 347 | header = match header.template_image(error_template_image) { 348 | Ok(header) => header, 349 | Err(never) => match never {}, 350 | }; 351 | } 352 | print!("{}", Menu(vec![header.into(), MenuItem::Sep])); 353 | e.main_output(None); 354 | } 355 | } 356 | } 357 | } 358 | 359 | #[cfg(feature = "tokio")] 360 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] 361 | /// Members of this trait can be returned from a main function annotated with [`main`]. 362 | pub trait AsyncMainOutput<'a> { 363 | /// Displays this value as a menu, using the given template image in case of an error. 364 | fn main_output(self, error_template_image: Option) -> Pin + 'a>>; 365 | } 366 | 367 | #[cfg(feature = "tokio")] 368 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] 369 | impl<'a, T: MainOutput + 'a> AsyncMainOutput<'a> for T { 370 | fn main_output(self, error_template_image: Option) -> Pin + 'a>> { 371 | Box::pin(async move { 372 | MainOutput::main_output(self, error_template_image); 373 | }) 374 | } 375 | } 376 | 377 | /// Members of this trait can be returned from a subcommand function annotated with [`command`] or [`fallback_command`]. 378 | pub trait CommandOutput { 379 | /// Reports any errors in this command output as macOS notifications. 380 | fn report(self, cmd_name: &str); 381 | } 382 | 383 | impl CommandOutput for () { 384 | fn report(self, _: &str) {} 385 | } 386 | 387 | impl CommandOutput for Result { 388 | fn report(self, cmd_name: &str) { 389 | match self { 390 | Ok(x) => x.report(cmd_name), 391 | Err(e) => { 392 | notify_error(&format!("{}: {}", cmd_name, e), &format!("{e:?}")); 393 | process::exit(1); 394 | } 395 | } 396 | } 397 | } 398 | 399 | #[doc(hidden)] pub fn notify(body: impl fmt::Display) { // used in proc macro 400 | if_chain! { 401 | if let Flavor::SwiftBar(swiftbar) = Flavor::check(); 402 | if let Ok(notification) = flavor::swiftbar::Notification::new(swiftbar); 403 | then { 404 | let _ = notification 405 | .title(env!("CARGO_PKG_NAME")) 406 | .body(body.to_string()) 407 | .send(); 408 | } else { 409 | #[cfg(target_os = "macos")] { 410 | let _ = notify_rust::set_application(¬ify_rust::get_bundle_identifier_or_default("BitBar")); 411 | let _ = notify_rust::Notification::default() 412 | .summary(&env!("CARGO_PKG_NAME")) 413 | .sound_name("Funky") 414 | .body(&body.to_string()) 415 | .show(); 416 | } 417 | #[cfg(not(target_os = "macos"))] { 418 | eprintln!("{body}"); 419 | } 420 | } 421 | } 422 | } 423 | 424 | #[doc(hidden)] pub fn notify_error(display: &str, debug: &str) { // used in proc macro 425 | if_chain! { 426 | if let Flavor::SwiftBar(swiftbar) = Flavor::check(); 427 | if let Ok(notification) = flavor::swiftbar::Notification::new(swiftbar); 428 | then { 429 | let _ = notification 430 | .title(env!("CARGO_PKG_NAME")) 431 | .subtitle(display) 432 | .body(format!("debug: {debug}")) 433 | .send(); 434 | } else { 435 | #[cfg(target_os = "macos")] { 436 | let _ = notify_rust::set_application(¬ify_rust::get_bundle_identifier_or_default("BitBar")); 437 | let _ = notify_rust::Notification::default() 438 | .summary(display) 439 | .sound_name("Funky") 440 | .body(&format!("debug: {debug}")) 441 | .show(); 442 | } 443 | #[cfg(not(target_os = "macos"))] { 444 | eprintln!("{display}"); 445 | eprintln!("debug: {debug}"); 446 | } 447 | } 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /crate/cargo-bitbar/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-bitbar" 3 | version = "0.1.1" 4 | authors.workspace = true 5 | edition.workspace = true 6 | description = "Cargo subcommand for the bitbar crate" 7 | license = "MIT" 8 | readme = "../../README.md" 9 | repository.workspace = true 10 | 11 | [dependencies] 12 | anyhow = "1" 13 | base64 = "0.21" 14 | cargo_metadata = "0.17" 15 | itertools = "0.11" 16 | serde_json = "1" 17 | serde_with = "3" 18 | xattr = "1" 19 | 20 | [dependencies.clap] 21 | version = "4" 22 | features = ["derive"] 23 | 24 | [dependencies.serde] 25 | version = "1" 26 | features = ["derive"] 27 | -------------------------------------------------------------------------------- /crate/cargo-bitbar/src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms, unused, unused_crate_dependencies, unused_import_braces, unused_lifetimes, unused_qualifications, warnings)] 2 | #![forbid(unsafe_code)] 3 | 4 | use { 5 | std::{ 6 | collections::HashMap, 7 | io::prelude::*, 8 | path::PathBuf, 9 | }, 10 | anyhow::Result, 11 | cargo_metadata::{ 12 | MetadataCommand, 13 | Package, 14 | }, 15 | clap::Parser as _, 16 | itertools::Itertools as _, 17 | serde::Deserialize, 18 | }; 19 | 20 | #[derive(Deserialize)] 21 | struct CustomMetadata { 22 | #[serde(default)] 23 | bitbar: BitBarMetadata, 24 | } 25 | 26 | #[derive(Deserialize)] 27 | #[serde(rename_all = "kebab-case")] 28 | enum PluginKind { 29 | Default, 30 | Streamable, 31 | } 32 | 33 | impl Default for PluginKind { 34 | fn default() -> Self { 35 | Self::Default 36 | } 37 | } 38 | 39 | #[derive(Default, Deserialize)] 40 | #[serde(rename_all = "kebab-case")] 41 | struct BitBarMetadata { 42 | #[serde(default, with = "serde_with::rust::double_option")] 43 | title: Option>, 44 | #[serde(default, with = "serde_with::rust::double_option")] 45 | version: Option>, 46 | #[serde(default, with = "serde_with::rust::double_option")] 47 | author: Option>, 48 | author_github: Option, 49 | #[serde(default, with = "serde_with::rust::double_option")] 50 | desc: Option>, 51 | image: Option, 52 | #[serde(default, with = "serde_with::rust::double_option")] 53 | dependencies: Option>, 54 | #[serde(default, with = "serde_with::rust::double_option")] 55 | abouturl: Option>, 56 | //TODO xbar variables? (unsure if xbar supports binary plugin metadata) 57 | //TODO SwiftBar droptypes 58 | #[serde(default)] 59 | hide_about: bool, 60 | #[serde(default)] 61 | hide_run_in_terminal: bool, 62 | #[serde(default)] 63 | hide_last_updated: bool, 64 | #[serde(default)] 65 | hide_disable_plugin: bool, 66 | #[serde(default)] 67 | hide_swiftbar: bool, 68 | schedule: Option, 69 | #[serde(default)] 70 | refresh_on_open: bool, 71 | #[serde(default)] 72 | run_in_bash: bool, 73 | #[serde(default, rename = "type")] 74 | kind: PluginKind, 75 | #[serde(default)] 76 | environment: HashMap, 77 | } 78 | 79 | impl BitBarMetadata { 80 | fn format(self, package: Option<&Package>) -> Result> { 81 | let Self { title, version, author, author_github, desc, image, dependencies, abouturl, hide_about, hide_run_in_terminal, hide_last_updated, hide_disable_plugin, hide_swiftbar, schedule, refresh_on_open, run_in_bash, kind, environment } = self; 82 | let mut buf = base64::write::EncoderWriter::new(Vec::default(), &base64::engine::general_purpose::STANDARD); 83 | 84 | macro_rules! double_option { 85 | ($field:ident, $fallback:expr) => { 86 | match $field { 87 | Some(Some(field)) => { writeln!(&mut buf, concat!("# {}"), field)?; } 88 | Some(None) => {} 89 | None => { writeln!(&mut buf, concat!("# {}"), $fallback)?; } 90 | } 91 | }; 92 | } 93 | 94 | macro_rules! triple_option { 95 | ($field:ident, $fallback:expr) => { 96 | match $field { 97 | Some(Some(field)) => { writeln!(&mut buf, concat!("# {}"), field)?; } 98 | Some(None) => {} 99 | None => if let Some(ref fallback) = $fallback { 100 | writeln!(&mut buf, concat!("# {}"), fallback)?; 101 | }, 102 | } 103 | }; 104 | } 105 | 106 | triple_option!(title, package.map(|package| &package.name)); 107 | triple_option!(version, package.map(|package| format!("v{}", package.version))); 108 | triple_option!(author, package.map(|package| package.authors.iter().map(|author| author.rsplit_once(" <").map(|(name, _)| name).unwrap_or(author)).join(", "))); 109 | if let Some(author_github) = author_github { writeln!(&mut buf, "# {}", author_github)?; } 110 | triple_option!(desc, package.and_then(|package| package.description.as_ref())); 111 | if let Some(image) = image { writeln!(&mut buf, "# {}", image)?; } 112 | double_option!(dependencies, "rust"); 113 | triple_option!(abouturl, package.and_then(|package| package.homepage.as_ref())); 114 | if hide_about { writeln!(&mut buf, "# true")?; } 115 | if hide_run_in_terminal { writeln!(&mut buf, "# true")?; } 116 | if hide_last_updated { writeln!(&mut buf, "# true")?; } 117 | if hide_disable_plugin { writeln!(&mut buf, "# true")?; } 118 | if hide_swiftbar { writeln!(&mut buf, "# true")?; } 119 | if let Some(schedule) = schedule { writeln!(&mut buf, "# {}", schedule)?; } 120 | if refresh_on_open { writeln!(&mut buf, "# true")?; } 121 | if !run_in_bash { writeln!(&mut buf, "# false")?; } 122 | match kind { 123 | PluginKind::Default => {} 124 | PluginKind::Streamable => { 125 | writeln!(&mut buf, "# streamable")?; 126 | writeln!(&mut buf, "# true")?; 127 | } 128 | } 129 | if !environment.is_empty() { 130 | writeln!(&mut buf, "# [{}], 149 | /// The path to the binary that should be edited. 150 | exe_path: PathBuf, 151 | }, 152 | } 153 | 154 | fn main() -> Result<()> { 155 | let Args::Bitbar(args) = Args::parse(); 156 | match args { 157 | ArgsInner::Meta { manifest, exe_path } => { 158 | let mut metadata_cmd = MetadataCommand::new(); 159 | metadata_cmd.no_deps(); 160 | if let Some(manifest) = manifest { 161 | metadata_cmd.manifest_path(manifest); 162 | } 163 | let metadata = metadata_cmd.exec()?; 164 | let package = metadata.root_package(); 165 | let custom_metadata = if let Some(package) = package { 166 | package.metadata.clone() 167 | } else { 168 | metadata.workspace_metadata.clone() 169 | }; 170 | let bitbar_metadata = serde_json::from_value::(custom_metadata)?.bitbar.format(package)?; 171 | xattr::set(exe_path, "com.ameba.SwiftBar", &bitbar_metadata)?; 172 | } 173 | } 174 | Ok(()) 175 | } 176 | --------------------------------------------------------------------------------