use {
std::{
borrow::Cow,
collections::BTreeMap,
convert::TryInto,
env,
io,
iter,
path::Path,
sync::Arc,
},
open::that as open,
semver::Version,
thiserror::Error,
url::Url,
crate::{
ContentItem,
MainOutput,
Menu,
MenuItem,
attr::{
Color,
Command,
Image,
IntoUrl,
Params,
},
},
};
#[cfg(feature = "assume-flavor")] use static_assertions::const_assert;
#[cfg(feature = "tokio")] use {
std::pin::Pin,
futures::{
future::Future,
stream::StreamExt as _,
},
crate::AsyncMainOutput,
};
#[cfg(feature = "assume-flavor")] const MAX_BUILD: usize = 402;
macro_rules! build_ge {
($swiftbar:expr, $build:expr) => {{
#[cfg(feature = "assume-flavor")] const_assert!($build <= MAX_BUILD);
$swiftbar.build >= $build
}};
}
#[derive(Debug, Clone, Copy)]
pub struct SwiftBar {
build: usize,
}
impl SwiftBar {
pub fn check() -> Option<Self> {
Some(Self {
build: env::var("SWIFTBAR_BUILD").ok()?.parse().ok()?,
})
}
#[cfg(feature = "assume-flavor")]
#[cfg_attr(docsrs, doc(cfg(feature = "assume-flavor")))]
pub fn assume() -> Self {
Self {
build: MAX_BUILD,
}
}
pub fn plugin_name(&self) -> Result<String, PluginNameError> {
Ok(Path::new(&env::var_os("SWIFTBAR_PLUGIN_PATH").ok_or(PluginNameError::Env)?)
.file_name().ok_or(PluginNameError::NoFileName)?
.to_str().ok_or(PluginNameError::NonUtf8FileName)?
.to_owned()
)
}
pub fn running_version(&self) -> Result<Version, VersionCheckError> {
Ok(env::var("SWIFTBAR_VERSION")?.parse()?)
}
pub fn command(&self, cmd: impl IntoParams) -> Params {
cmd.into_params(self)
}
pub fn themed_color(&self, light: Color, dark: Color) -> Color {
Color {
light: light.light,
dark: Some(dark.dark.unwrap_or(dark.light)),
}
}
pub fn sf_image(&self, item: &mut ContentItem, image: impl ToString) {
Attrs::for_item(item).sf_image = Some(image.to_string());
}
pub fn checked(&self, item: &mut ContentItem) {
Attrs::for_item(item).checked = true;
}
}
pub trait IntoParams {
fn into_params(self, swiftbar: &SwiftBar) -> Params;
}
impl IntoParams for Params {
fn into_params(self, _: &SwiftBar) -> Params {
self
}
}
macro_rules! impl_into_params {
($n:literal$(, $elt:ident: $t:ident)*) => {
impl<T: ToString> IntoParams for [T; $n] {
fn into_params(self, _: &SwiftBar) -> Params {
let [cmd, $($elt),*] = self;
Params {
cmd: cmd.to_string(),
params: vec![$($elt.to_string()),*],
}
}
}
impl<Cmd: ToString, $($t: ToString),*> IntoParams for (Cmd, $($t),*) {
fn into_params(self, _: &SwiftBar) -> Params {
let (cmd, $($elt),*) = self;
Params {
cmd: cmd.to_string(),
params: vec![$($elt.to_string()),*],
}
}
}
};
}
impl_into_params!(1);
impl_into_params!(2, param1: A);
impl_into_params!(3, param1: A, param2: B);
impl_into_params!(4, param1: A, param2: B, param3: C);
impl_into_params!(5, param1: A, param2: B, param3: C, param4: D);
impl_into_params!(6, param1: A, param2: B, param3: C, param4: D, param5: E);
impl_into_params!(7, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F);
impl_into_params!(8, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F, param7: G);
impl_into_params!(9, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F, param7: G, param8: H);
impl_into_params!(10, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F, param7: G, param8: H, param9: I);
impl_into_params!(11, param1: A, param2: B, param3: C, param4: D, param5: E, param6: F, param7: G, param8: H, param9: I, param10: J);
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);
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);
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);
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);
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);
impl<'a, T: ToString> IntoParams for &'a [T] {
fn into_params(self, _: &SwiftBar) -> Params {
Params {
cmd: self[0].to_string(),
params: self[1..].iter().map(|param| param.to_string()).collect(),
}
}
}
impl<T: ToString> IntoParams for Vec<T> {
fn into_params(mut self, _: &SwiftBar) -> Params {
Params {
cmd: self.remove(0).to_string(),
params: self.into_iter().map(|param| param.to_string()).collect(),
}
}
}
#[derive(Debug)]
pub struct Attrs {
checked: bool,
sf_image: Option<String>,
}
impl Attrs {
fn for_item(item: &mut ContentItem) -> &mut Attrs {
match item.flavor_attrs.get_or_insert_with(|| super::Attrs::SwiftBar(Attrs {
checked: false,
sf_image: None,
})) {
super::Attrs::SwiftBar(ref mut params) => params,
}
}
pub(crate) fn render<'a>(&'a self, rendered_params: &mut BTreeMap<Cow<'a, str>, Cow<'a, str>>) {
if self.checked {
rendered_params.insert(Cow::Borrowed("checked"), Cow::Borrowed("true"));
}
if let Some(ref sf_image) = self.sf_image {
rendered_params.insert(Cow::Borrowed("sfimage"), Cow::Borrowed(sf_image));
}
}
}
#[derive(Debug, Error, Clone)]
pub enum VersionCheckError {
#[error(transparent)] Env(#[from] env::VarError),
#[error(transparent)] Parse(Arc<semver::Error>),
}
impl From<semver::Error> for VersionCheckError {
fn from(e: semver::Error) -> VersionCheckError {
VersionCheckError::Parse(Arc::new(e))
}
}
impl From<VersionCheckError> for Menu {
fn from(e: VersionCheckError) -> Menu {
let mut menu = vec![MenuItem::new("Error checking running SwiftBar version")];
match e {
VersionCheckError::Env(e) => menu.push(MenuItem::new(e)),
VersionCheckError::Parse(e) => {
menu.push(MenuItem::new(format!("error parsing version: {}", e)));
menu.push(MenuItem::new(format!("{:?}", e)));
}
}
Menu(menu)
}
}
#[derive(Debug, Error, Clone)]
pub enum PluginNameError {
#[error("missing `SWIFTBAR_PLUGIN_PATH` environment variable")]
Env,
#[error("no filename in `SWIFTBAR_PLUGIN_PATH` environment variable")]
NoFileName,
#[error("plugin filename is not valid UTF-8")]
NonUtf8FileName,
}
impl From<PluginNameError> for Menu {
fn from(e: PluginNameError) -> Menu {
Menu(vec![
MenuItem::new("Error checking running SwiftBar plugin name"),
MenuItem::new(e.to_string()),
])
}
}
#[derive(Debug, Error, Clone)]
pub enum NotificationCommandError<C: TryInto<Command>>
where C::Error: std::error::Error {
#[error(transparent)] Command(C::Error),
#[error("running commands on notification click is only supported on SwiftBar 1.4.3 beta 4 or newer")]
UnsupportedSwiftBarVersion,
}
impl<C: TryInto<Command>> From<NotificationCommandError<C>> for Menu
where C::Error: std::error::Error {
fn from(e: NotificationCommandError<C>) -> Menu {
let mut menu = vec![MenuItem::new("Error adding command to notification")];
match e {
NotificationCommandError::Command(e) => {
menu.push(MenuItem::new(format!("error building command: {}", e)));
menu.push(MenuItem::new(format!("{:?}", e)));
}
NotificationCommandError::UnsupportedSwiftBarVersion => menu.push(MenuItem::new("running commands on notification click is only supported on SwiftBar 1.4.3 beta 4 or newer")),
}
Menu(menu)
}
}
pub struct Notification {
swiftbar: SwiftBar,
plugin_name: String,
title: Option<String>,
subtitle: Option<String>,
body: Option<String>,
href: Option<Url>,
command: Option<Command>,
silent: bool,
}
impl Notification {
pub fn new(swiftbar: SwiftBar) -> Result<Self, PluginNameError> {
Ok(Self {
swiftbar,
plugin_name: swiftbar.plugin_name()?,
title: None,
subtitle: None,
body: None,
href: None,
command: None,
silent: false,
})
}
pub fn title(mut self, title: impl ToString) -> Self {
self.title = Some(title.to_string());
self
}
pub fn subtitle(mut self, subtitle: impl ToString) -> Self {
self.subtitle = Some(subtitle.to_string());
self
}
pub fn body(mut self, body: impl ToString) -> Self {
self.body = Some(body.to_string());
self
}
pub fn href(mut self, href: impl IntoUrl) -> Result<Self, url::ParseError> {
self.href = Some(href.into_url()?);
Ok(self)
}
pub fn command<C: TryInto<Command>>(mut self, cmd: C) -> Result<Self, NotificationCommandError<C>>
where C::Error: std::error::Error {
if build_ge!(self.swiftbar, 402) {
self.command = Some(cmd.try_into().map_err(NotificationCommandError::Command)?);
Ok(self)
} else {
Err(NotificationCommandError::UnsupportedSwiftBarVersion)
}
}
pub fn silent(mut self) -> Self {
self.silent = true;
self
}
pub fn send(&self) -> io::Result<()> {
open(self.into_url().expect("failed to build SwiftBar notification URL").as_str())
}
}
impl IntoUrl for Notification {
fn into_url(self) -> Result<Url, url::ParseError> {
(&self).into_url()
}
}
impl<'a> IntoUrl for &'a Notification {
fn into_url(self) -> Result<Url, url::ParseError> {
let Notification { swiftbar: _, plugin_name, title, subtitle, body, command, href, silent } = self;
Url::parse_with_params("swiftbar://notify", iter::once((Cow::Borrowed("plugin"), &**plugin_name))
.chain(title.as_deref().map(|title| (Cow::Borrowed("title"), title)))
.chain(subtitle.as_deref().map(|subtitle| (Cow::Borrowed("subtitle"), subtitle)))
.chain(body.as_deref().map(|body| (Cow::Borrowed("body"), body)))
.chain(command.iter().flat_map(|command| iter::once((Cow::Borrowed("bash"), &*command.params.cmd))
.chain(command.params.params.iter().enumerate().map(|(n, arg)| (Cow::Owned(format!("param{}", n + 1)), &**arg)))
.chain((!command.terminal).then(|| (Cow::Borrowed("terminal"), "false")))
))
.chain(href.as_ref().map(|href| (Cow::Borrowed("href"), href.as_str())))
.chain(silent.then(|| (Cow::Borrowed("silent"), "true")))
)
}
}
pub struct BlockingStream<'a, I: MainOutput> {
swiftbar: SwiftBar,
inner: Box<dyn Iterator<Item = I> + 'a>,
}
impl<'a, I: MainOutput> BlockingStream<'a, I> {
#[allow(missing_docs)]
pub fn new(swiftbar: SwiftBar, iter: impl IntoIterator<Item = I> + 'a) -> Self {
Self { swiftbar, inner: Box::new(iter.into_iter()) }
}
}
impl<'a, I: MainOutput> MainOutput for BlockingStream<'a, I> {
fn main_output(self, error_template_image: Option<Image>) {
if build_ge!(self.swiftbar, 399) {
for elt in self.inner {
elt.main_output(error_template_image.clone());
println!("~~~");
}
} else {
for elt in self.inner {
println!("~~~");
elt.main_output(error_template_image.clone());
}
}
}
}
#[cfg(feature = "tokio")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
pub struct Stream<'a, I: AsyncMainOutput<'a> + 'a> {
swiftbar: SwiftBar,
inner: Pin<Box<dyn futures::stream::Stream<Item = I> + 'a>>,
}
#[cfg(feature = "tokio")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
impl<'a, I: AsyncMainOutput<'a> + 'a> Stream<'a, I> {
#[allow(missing_docs)]
pub fn new(swiftbar: SwiftBar, stream: impl futures::stream::Stream<Item = I> + 'a) -> Self {
Self { swiftbar, inner: Box::pin(stream) }
}
}
#[cfg(feature = "tokio")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
impl<'a, I: AsyncMainOutput<'a> + 'a> AsyncMainOutput<'a> for Stream<'a, I> {
fn main_output(mut self, error_template_image: Option<Image>) -> Pin<Box<dyn Future<Output = ()> + 'a>> {
if build_ge!(self.swiftbar, 399) {
Box::pin(async move {
while let Some(elt) = self.inner.next().await {
elt.main_output(error_template_image.clone()).await;
println!("~~~");
}
})
} else {
Box::pin(async move {
while let Some(elt) = self.inner.next().await {
println!("~~~");
elt.main_output(error_template_image.clone()).await;
}
})
}
}
}