#![deny(missing_docs, rust_2018_idioms, unused, unused_crate_dependencies, unused_import_braces, unused_qualifications, warnings)]
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use {
std::{
borrow::Cow,
collections::BTreeMap,
convert::TryInto,
fmt,
iter::FromIterator,
process,
vec,
},
if_chain::if_chain,
url::Url,
};
#[cfg(feature = "tokio")] use std::{
future::Future,
pin::Pin,
};
pub use {
bitbar_derive::{
command,
fallback_command,
main,
},
crate::flavor::Flavor,
};
#[cfg(feature = "tokio")] #[doc(hidden)] pub use tokio;
pub mod attr;
pub mod flavor;
#[derive(Debug, Default)]
pub struct ContentItem {
pub text: String,
pub extra: Option<attr::Extra>,
pub href: Option<Url>,
pub color: Option<attr::Color>,
pub font: Option<String>,
pub size: Option<usize>,
pub command: Option<attr::Command>,
pub refresh: bool,
pub image: Option<attr::Image>,
pub flavor_attrs: Option<flavor::Attrs>,
}
impl ContentItem {
pub fn new(text: impl ToString) -> ContentItem {
ContentItem {
text: text.to_string(),
..ContentItem::default()
}
}
pub fn sub(mut self, items: impl IntoIterator<Item = MenuItem>) -> Self {
self.extra = Some(attr::Extra::Submenu(Menu::from_iter(items)));
self
}
pub fn href(mut self, href: impl attr::IntoUrl) -> Result<Self, url::ParseError> {
self.href = Some(href.into_url()?);
Ok(self)
}
pub fn color<C: TryInto<attr::Color>>(mut self, color: C) -> Result<Self, C::Error> {
self.color = Some(color.try_into()?);
Ok(self)
}
pub fn font(mut self, font: impl ToString) -> Self {
self.font = Some(font.to_string());
self
}
pub fn size(mut self, size: usize) -> Self {
self.size = Some(size);
self
}
pub fn command<C: TryInto<attr::Command>>(mut self, cmd: C) -> Result<Self, C::Error> {
self.command = Some(cmd.try_into()?);
Ok(self)
}
pub fn refresh(mut self) -> Self {
self.refresh = true;
self
}
pub fn alt(mut self, alt: impl Into<ContentItem>) -> Self {
self.extra = Some(attr::Extra::Alternate(Box::new(alt.into())));
self
}
pub fn template_image<T: TryInto<attr::Image>>(mut self, img: T) -> Result<Self, T::Error> {
self.image = Some(attr::Image::template(img)?);
Ok(self)
}
pub fn image<T: TryInto<attr::Image>>(mut self, img: T) -> Result<Self, T::Error> {
self.image = Some(img.try_into()?);
Ok(self)
}
fn render(&self, f: &mut fmt::Formatter<'_>, is_alt: bool) -> fmt::Result {
write!(f, "{}", self.text.replace('|', "¦").replace('\n', " "))?;
let mut rendered_params = BTreeMap::default();
if let Some(ref href) = self.href {
rendered_params.insert(Cow::Borrowed("href"), Cow::Borrowed(href.as_ref()));
}
if let Some(ref color) = self.color {
rendered_params.insert(Cow::Borrowed("color"), Cow::Owned(color.to_string()));
}
if let Some(ref font) = self.font {
rendered_params.insert(Cow::Borrowed("font"), Cow::Borrowed(font));
}
if let Some(size) = self.size {
rendered_params.insert(Cow::Borrowed("size"), Cow::Owned(size.to_string()));
}
if let Some(ref cmd) = self.command {
rendered_params.insert(Cow::Borrowed("bash"), Cow::Borrowed(&cmd.params.cmd));
for (i, param) in cmd.params.params.iter().enumerate() {
rendered_params.insert(Cow::Owned(format!("param{}", i + 1)), Cow::Borrowed(param));
}
if !cmd.terminal {
rendered_params.insert(Cow::Borrowed("terminal"), Cow::Borrowed("false"));
}
}
if self.refresh {
rendered_params.insert(Cow::Borrowed("refresh"), Cow::Borrowed("true"));
}
if is_alt {
rendered_params.insert(Cow::Borrowed("alternate"), Cow::Borrowed("true"));
}
if let Some(ref img) = self.image {
rendered_params.insert(Cow::Borrowed(if img.is_template { "templateImage" } else { "image" }), Cow::Borrowed(&img.base64_data));
}
if let Some(ref flavor_attrs) = self.flavor_attrs {
flavor_attrs.render(&mut rendered_params);
}
if !rendered_params.is_empty() {
write!(f, " |")?;
for (name, value) in rendered_params {
let quoted_value = if value.contains(' ') {
Cow::Owned(format!("\"{}\"", value))
} else {
value
}; write!(f, " {}={}", name, quoted_value)?;
}
}
writeln!(f)?;
match &self.extra {
Some(attr::Extra::Alternate(ref alt)) => { alt.render(f, true)?; }
Some(attr::Extra::Submenu(ref sub)) => {
let sub_fmt = format!("{}", sub);
for line in sub_fmt.lines() {
writeln!(f, "--{}", line)?;
}
}
None => {}
}
Ok(())
}
}
impl fmt::Display for ContentItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.render(f, false)
}
}
#[derive(Debug)]
pub enum MenuItem {
Content(ContentItem),
Sep
}
impl MenuItem {
pub fn new(text: impl fmt::Display) -> MenuItem {
MenuItem::Content(ContentItem::new(text))
}
}
impl Default for MenuItem {
fn default() -> MenuItem {
MenuItem::Content(ContentItem::default())
}
}
impl From<ContentItem> for MenuItem {
fn from(i: ContentItem) -> MenuItem {
MenuItem::Content(i)
}
}
impl fmt::Display for MenuItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MenuItem::Content(content) => write!(f, "{}", content),
MenuItem::Sep => writeln!(f, "---")
}
}
}
#[derive(Debug, Default)]
pub struct Menu(pub Vec<MenuItem>);
impl Menu {
pub fn push(&mut self, item: impl Into<MenuItem>) {
self.0.push(item.into());
}
}
impl<A: Into<MenuItem>> FromIterator<A> for Menu {
fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Menu {
Menu(iter.into_iter().map(Into::into).collect())
}
}
impl<A: Into<MenuItem>> Extend<A> for Menu {
fn extend<T: IntoIterator<Item = A>>(&mut self, iter: T) {
self.0.extend(iter.into_iter().map(Into::into))
}
}
impl IntoIterator for Menu {
type Item = MenuItem;
type IntoIter = vec::IntoIter<MenuItem>;
fn into_iter(self) -> vec::IntoIter<MenuItem> { self.0.into_iter() }
}
impl fmt::Display for Menu {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for menu_item in &self.0 {
write!(f, "{}", menu_item)?;
}
Ok(())
}
}
pub trait MainOutput {
fn main_output(self, error_template_image: Option<attr::Image>);
}
impl<T: Into<Menu>> MainOutput for T {
fn main_output(self, _: Option<attr::Image>) {
print!("{}", self.into());
}
}
impl<T: MainOutput, E: MainOutput> MainOutput for Result<T, E> {
fn main_output(self, error_template_image: Option<attr::Image>) {
match self {
Ok(x) => x.main_output(error_template_image),
Err(e) => {
let mut header = ContentItem::new("?");
if let Some(error_template_image) = error_template_image {
header = match header.template_image(error_template_image) {
Ok(header) => header,
Err(never) => match never {},
};
}
print!("{}", Menu(vec![header.into(), MenuItem::Sep]));
e.main_output(None);
}
}
}
}
#[cfg(feature = "tokio")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
pub trait AsyncMainOutput<'a> {
fn main_output(self, error_template_image: Option<attr::Image>) -> Pin<Box<dyn Future<Output = ()> + 'a>>;
}
#[cfg(feature = "tokio")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
impl<'a, T: MainOutput + 'a> AsyncMainOutput<'a> for T {
fn main_output(self, error_template_image: Option<attr::Image>) -> Pin<Box<dyn Future<Output = ()> + 'a>> {
Box::pin(async move {
MainOutput::main_output(self, error_template_image);
})
}
}
pub trait CommandOutput {
fn report(self, cmd_name: &str);
}
impl CommandOutput for () {
fn report(self, _: &str) {}
}
impl<T: CommandOutput, E: fmt::Debug + fmt::Display> CommandOutput for Result<T, E> {
fn report(self, cmd_name: &str) {
match self {
Ok(x) => x.report(cmd_name),
Err(e) => {
notify_error(&format!("{}: {}", cmd_name, e), &format!("{e:?}"));
process::exit(1);
}
}
}
}
#[doc(hidden)] pub fn notify(body: impl fmt::Display) { if_chain! {
if let Flavor::SwiftBar(swiftbar) = Flavor::check();
if let Ok(notification) = flavor::swiftbar::Notification::new(swiftbar);
then {
let _ = notification
.title(env!("CARGO_PKG_NAME"))
.body(body.to_string())
.send();
} else {
#[cfg(target_os = "macos")] {
let _ = notify_rust::set_application(¬ify_rust::get_bundle_identifier_or_default("BitBar"));
let _ = notify_rust::Notification::default()
.summary(&env!("CARGO_PKG_NAME"))
.sound_name("Funky")
.body(&body.to_string())
.show();
}
#[cfg(not(target_os = "macos"))] {
eprintln!("{body}");
}
}
}
}
#[doc(hidden)] pub fn notify_error(display: &str, debug: &str) { if_chain! {
if let Flavor::SwiftBar(swiftbar) = Flavor::check();
if let Ok(notification) = flavor::swiftbar::Notification::new(swiftbar);
then {
let _ = notification
.title(env!("CARGO_PKG_NAME"))
.subtitle(display)
.body(format!("debug: {debug}"))
.send();
} else {
#[cfg(target_os = "macos")] {
let _ = notify_rust::set_application(¬ify_rust::get_bundle_identifier_or_default("BitBar"));
let _ = notify_rust::Notification::default()
.summary(display)
.sound_name("Funky")
.body(&format!("debug: {debug}"))
.show();
}
#[cfg(not(target_os = "macos"))] {
eprintln!("{display}");
eprintln!("debug: {debug}");
}
}
}
}