use std::time::Duration;
use eyre::Result;
use serde::{Deserialize, Serialize};
use crate::file::modified_duration;
use crate::http::HTTP;
use crate::{dirs, duration, file};
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Patrons {
#[clap(long, short = 'J')]
json: bool,
#[clap(long)]
refresh: bool,
}
#[derive(Debug, Serialize, Deserialize)]
struct PatronsPayload {
#[serde(default, skip_serializing_if = "Option::is_none")]
schema: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
generated_at: Option<String>,
#[serde(default)]
patrons: Vec<Patron>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Patron {
name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
url: Option<String>,
}
const PATRONS_URL: &str = "https://en.dev/patrons.json";
const SPONSOR_URL: &str = "https://en.dev";
const CACHE_TTL: Duration = duration::DAILY;
impl Patrons {
pub async fn run(self) -> Result<()> {
let payload = load_patrons(self.refresh).await?;
if self.json {
miseprintln!("{}", serde_json::to_string_pretty(&payload)?);
} else {
render_human(&payload)?;
}
Ok(())
}
}
async fn load_patrons(refresh: bool) -> Result<PatronsPayload> {
let cache_path = dirs::CACHE.join("patrons.json");
if !refresh
&& let Ok(age) = modified_duration(&cache_path)
&& age < CACHE_TTL
&& let Ok(body) = file::read_to_string(&cache_path)
&& let Ok(payload) = serde_json::from_str::<PatronsPayload>(&body)
{
return Ok(payload);
}
let (stage, err) = match HTTP.get_text(PATRONS_URL).await {
Ok(body) => match serde_json::from_str::<PatronsPayload>(&body) {
Ok(payload) => {
let _ = file::create_dir_all(*dirs::CACHE);
let _ = file::write(&cache_path, &body);
return Ok(payload);
}
Err(err) => ("parse", eyre::Report::from(err)),
},
Err(err) => ("fetch", err),
};
if let Ok(cached_body) = file::read_to_string(&cache_path)
&& let Ok(payload) = serde_json::from_str::<PatronsPayload>(&cached_body)
{
warn!("failed to {stage} patrons.json, using stale cache: {err:#}");
return Ok(payload);
}
Err(err)
}
fn render_human(payload: &PatronsPayload) -> Result<()> {
if payload.patrons.is_empty() {
miseprintln!(
"No patrons yet — be the first: {}",
hyperlink(SPONSOR_URL, SPONSOR_URL),
);
return Ok(());
}
miseprintln!("mise is supported by these patrons — thank you ❤");
miseprintln!("");
for p in &payload.patrons {
let name = strip_control(&p.name);
let label = match &p.url {
Some(url) => hyperlink(&strip_control(url), &name),
None => name,
};
miseprintln!(" • {label}");
}
miseprintln!("");
miseprintln!("Become a patron: {}", hyperlink(SPONSOR_URL, SPONSOR_URL));
Ok(())
}
fn hyperlink(url: &str, text: &str) -> String {
if supports_hyperlinks::on(supports_hyperlinks::Stream::Stdout) {
format!("\x1b]8;;{url}\x1b\\{text}\x1b]8;;\x1b\\")
} else {
text.to_string()
}
}
fn strip_control(s: &str) -> String {
s.chars().filter(|c| !c.is_control()).collect()
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
$ <bold>mise patrons</bold>
$ <bold>mise patrons -J</bold>
$ <bold>mise patrons --refresh</bold>"#
);