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
6use {
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};
63pub 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
73pub mod attr;
74pub mod flavor;
75
76#[derive(Debug, Default)]
78pub struct ContentItem {
79 pub text: String,
83 pub extra: Option<attr::Extra>,
85 pub href: Option<Url>,
87 pub color: Option<attr::Color>,
89 pub font: Option<String>,
91 pub size: Option<usize>,
93 pub command: Option<attr::Command>,
95 pub refresh: bool,
97 pub image: Option<attr::Image>,
99 pub flavor_attrs: Option<flavor::Attrs>,
101}
102
103impl ContentItem {
104 pub fn new(text: impl ToString) -> ContentItem {
108 ContentItem {
109 text: text.to_string(),
110 ..ContentItem::default()
111 }
112 }
113
114 pub fn sub(mut self, items: impl IntoIterator<Item = MenuItem>) -> Self {
116 self.extra = Some(attr::Extra::Submenu(Menu::from_iter(items)));
117 self
118 }
119
120 pub fn href(mut self, href: impl attr::IntoUrl) -> Result<Self, url::ParseError> {
122 self.href = Some(href.into_url()?);
123 Ok(self)
124 }
125
126 pub fn color<C: TryInto<attr::Color>>(mut self, color: C) -> Result<Self, C::Error> {
128 self.color = Some(color.try_into()?);
129 Ok(self)
130 }
131
132 pub fn font(mut self, font: impl ToString) -> Self {
134 self.font = Some(font.to_string());
135 self
136 }
137
138 pub fn size(mut self, size: usize) -> Self {
140 self.size = Some(size);
141 self
142 }
143
144 pub fn command<C: TryInto<attr::Command>>(mut self, cmd: C) -> Result<Self, C::Error> {
146 self.command = Some(cmd.try_into()?);
147 Ok(self)
148 }
149
150 pub fn refresh(mut self) -> Self {
152 self.refresh = true;
153 self
154 }
155
156 pub fn alt(mut self, alt: impl Into<ContentItem>) -> Self {
158 self.extra = Some(attr::Extra::Alternate(Box::new(alt.into())));
159 self
160 }
161
162 pub fn template_image<T: TryInto<attr::Image>>(mut self, img: T) -> Result<Self, T::Error> {
164 self.image = Some(attr::Image::template(img)?);
165 Ok(self)
166 }
167
168 pub fn image<T: TryInto<attr::Image>>(mut self, img: T) -> Result<Self, T::Error> {
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 write!(f, "{}", self.text.replace('|', "¦").replace('\n', " "))?;
177 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 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 }; write!(f, " {}={}", name, quoted_value)?;
222 }
223 }
224 writeln!(f)?;
225 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
240impl fmt::Display for ContentItem {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 self.render(f, false)
243 }
244}
245
246#[derive(Debug)]
248pub enum MenuItem {
249 Content(ContentItem),
251 Sep
253}
254
255impl MenuItem {
256 pub fn new(text: impl fmt::Display) -> MenuItem {
258 MenuItem::Content(ContentItem::new(text))
259 }
260}
261
262impl Default for MenuItem {
263 fn default() -> MenuItem {
264 MenuItem::Content(ContentItem::default())
265 }
266}
267
268impl From<ContentItem> for MenuItem {
269 fn from(i: ContentItem) -> MenuItem {
270 MenuItem::Content(i)
271 }
272}
273
274impl 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#[derive(Debug, Default)]
287pub struct Menu(pub Vec<MenuItem>);
288
289impl Menu {
290 pub fn push(&mut self, item: impl Into<MenuItem>) {
292 self.0.push(item.into());
293 }
294}
295
296impl<A: Into<MenuItem>> FromIterator<A> for Menu {
297 fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Menu {
298 Menu(iter.into_iter().map(Into::into).collect())
299 }
300}
301
302impl<A: Into<MenuItem>> Extend<A> for Menu {
303 fn extend<T: IntoIterator<Item = A>>(&mut self, iter: T) {
304 self.0.extend(iter.into_iter().map(Into::into))
305 }
306}
307
308impl IntoIterator for Menu {
309 type Item = MenuItem;
310 type IntoIter = vec::IntoIter<MenuItem>;
311
312 fn into_iter(self) -> vec::IntoIter<MenuItem> { self.0.into_iter() }
313}
314
315impl 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
327pub trait MainOutput {
329 fn main_output(self, error_template_image: Option<attr::Image>);
331}
332
333impl<T: Into<Menu>> MainOutput for T {
334 fn main_output(self, _: Option<attr::Image>) {
335 print!("{}", self.into());
336 }
337}
338
339impl<T: MainOutput, E: MainOutput> MainOutput for Result<T, E> {
341 fn main_output(self, error_template_image: Option<attr::Image>) {
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")))]
361pub trait AsyncMainOutput<'a> {
363 fn main_output(self, error_template_image: Option<attr::Image>) -> Pin<Box<dyn Future<Output = ()> + 'a>>;
365}
366
367#[cfg(feature = "tokio")]
368#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
369impl<'a, T: MainOutput + 'a> AsyncMainOutput<'a> for T {
370 fn main_output(self, error_template_image: Option<attr::Image>) -> Pin<Box<dyn Future<Output = ()> + 'a>> {
371 Box::pin(async move {
372 MainOutput::main_output(self, error_template_image);
373 })
374 }
375}
376
377pub trait CommandOutput {
379 fn report(self, cmd_name: &str);
381}
382
383impl CommandOutput for () {
384 fn report(self, _: &str) {}
385}
386
387impl<T: CommandOutput, E: fmt::Debug + fmt::Display> CommandOutput for Result<T, E> {
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) { 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) { 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}