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}