xkcd_bin/
lib.rs

1use chrono::{NaiveDate, Utc};
2use eyre::Result;
3use image::DynamicImage;
4use random::Source;
5use serde::Deserialize;
6use viuer::{Config, KittySupport};
7
8
9#[derive(Debug)]
10pub struct Comic {
11    pub date: NaiveDate,
12    #[allow(dead_code)]
13    pub num: u32,
14    pub img: String,
15    pub link: String,
16    pub title: String,
17    pub transcript: String,
18}
19
20
21impl Comic {
22    pub async fn fetch(num: u32) -> Result<Self> {
23        let comic = fetch_comic(Some(num)).await?;
24        Ok(comic.into())
25    }
26
27    pub async fn latest() -> Result<Self> {
28        let comic = fetch_comic(None).await?;
29        Ok(comic.into())
30    }
31
32    pub async fn random() -> Result<Self> {
33        let mut rnd = random::default(Utc::now().timestamp_millis() as u64);
34        let latest = fetch_comic(None).await?;
35        let num = (rnd.read::<u32>() % latest.num) + 1;
36        let comic = fetch_comic(Some(num)).await?;
37        Ok(comic.into())
38    }
39
40    pub fn open(&self) -> Result<()> {
41        webbrowser::open(&self.link)?;
42        Ok(())
43    }
44
45    pub async fn render(&self) -> Result<()> {
46        if !viuer::is_iterm_supported() && viuer::get_kitty_support() == KittySupport::None {
47            return self.open();
48        }
49
50        println!(
51            "\x1b[33;1m{}\x1b[0m \x1b[31m{}\x1b[0m\n",
52            &self.title,
53            &self.date.format("%Y-%m-%d"),
54        );
55        println!("\x1b[37;40m{}\x1b[0m\n", &self.transcript);
56        let img = download_img(&self.img).await?;
57        let height = (img.height() / 12 as u32).min(20);
58        let config = Config {
59            absolute_offset: false,
60            height: Some(height),
61            restore_cursor: false,
62            ..Default::default()
63        };
64        viuer::print(&img, &config)?;
65        println!("\n{}", &self.link);
66
67        Ok(())
68    }
69}
70
71
72impl From<RawComic> for Comic {
73    fn from(value: RawComic) -> Self {
74
75        let title = match &value.title {
76            title if title.is_empty() => value.safe_title.to_owned(),
77            title => title.to_owned(),
78        };
79
80        let transcript = match &value.transcript {
81            transcript if transcript.is_empty() => value.alt.to_owned(),
82            transcript => transcript.to_owned(),
83        };
84
85        let link = match &value.link {
86            link if link.is_empty() => format!("https://xkcd.com/{}", value.num),
87            link => link.to_owned(),
88        };
89
90        Self {
91            title, transcript, link,
92            date: value.date().unwrap_or(NaiveDate::MIN),
93            num: value.num,
94            img: value.img,
95        }
96    }
97}
98
99
100#[derive(Debug, Deserialize)]
101struct RawComic {
102    month: String,
103    num: u32,
104    link: String,
105    year: String,
106    safe_title: String,
107    transcript: String,
108    alt: String,
109    img: String,
110    title: String,
111    day: String,
112}
113
114impl RawComic {
115    fn date(&self) -> Option<NaiveDate> {
116        NaiveDate::from_ymd_opt(
117            self.year.parse().ok()?,
118            self.month.parse().ok()?,
119            self.day.parse().ok()?,
120        )
121    }
122}
123
124
125async fn fetch_comic(num: Option<u32>) -> Result<RawComic> {
126    let url = match num {
127        Some(num) => format!("https://xkcd.com/{}/info.0.json", num),
128        None => "https://xkcd.com/info.0.json".to_string(),
129    };
130    let resp = reqwest::get(url).await?;
131    let comic: RawComic = resp.json().await?;
132    Ok(comic)
133}
134
135
136async fn download_img(url: &str) -> Result<DynamicImage> {
137    let resp = reqwest::get(url).await?;
138    let payload = resp.bytes().await?.iter().copied().collect::<Vec<u8>>();
139    Ok(image::load_from_memory(&payload)?)
140}