1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
use chrono::{NaiveDate, Utc};
use eyre::Result;
use image::DynamicImage;
use random::Source;
use serde::Deserialize;
use viuer::{Config, KittySupport};


#[derive(Debug)]
pub struct Comic {
    pub date: NaiveDate,
    #[allow(dead_code)]
    pub num: u32,
    pub img: String,
    pub link: String,
    pub title: String,
    pub transcript: String,
}


impl Comic {
    pub async fn fetch(num: u32) -> Result<Self> {
        let comic = fetch_comic(Some(num)).await?;
        Ok(comic.into())
    }

    pub async fn latest() -> Result<Self> {
        let comic = fetch_comic(None).await?;
        Ok(comic.into())
    }

    pub async fn random() -> Result<Self> {
        let mut rnd = random::default(Utc::now().timestamp_millis() as u64);
        let latest = fetch_comic(None).await?;
        let num = (rnd.read::<u32>() % latest.num) + 1;
        let comic = fetch_comic(Some(num)).await?;
        Ok(comic.into())
    }

    pub fn open(&self) -> Result<()> {
        webbrowser::open(&self.link)?;
        Ok(())
    }

    pub async fn render(&self) -> Result<()> {
        if !viuer::is_iterm_supported() && viuer::get_kitty_support() == KittySupport::None {
            return self.open();
        }

        println!(
            "\x1b[33;1m{}\x1b[0m \x1b[31m{}\x1b[0m\n",
            &self.title,
            &self.date.format("%Y-%m-%d"),
        );
        println!("\x1b[37;40m{}\x1b[0m\n", &self.transcript);
        let img = download_img(&self.img).await?;
        let height = (img.height() / 12 as u32).min(20);
        let config = Config {
            absolute_offset: false,
            height: Some(height),
            restore_cursor: false,
            ..Default::default()
        };
        viuer::print(&img, &config)?;
        println!("\n{}", &self.link);

        Ok(())
    }
}


impl From<RawComic> for Comic {
    fn from(value: RawComic) -> Self {

        let title = match &value.title {
            title if title.is_empty() => value.safe_title.to_owned(),
            title => title.to_owned(),
        };

        let transcript = match &value.transcript {
            transcript if transcript.is_empty() => value.alt.to_owned(),
            transcript => transcript.to_owned(),
        };

        let link = match &value.link {
            link if link.is_empty() => format!("https://xkcd.com/{}", value.num),
            link => link.to_owned(),
        };

        Self {
            title, transcript, link,
            date: value.date().unwrap_or(NaiveDate::MIN),
            num: value.num,
            img: value.img,
        }
    }
}


#[derive(Debug, Deserialize)]
struct RawComic {
    month: String,
    num: u32,
    link: String,
    year: String,
    safe_title: String,
    transcript: String,
    alt: String,
    img: String,
    title: String,
    day: String,
}

impl RawComic {
    fn date(&self) -> Option<NaiveDate> {
        NaiveDate::from_ymd_opt(
            self.year.parse().ok()?,
            self.month.parse().ok()?,
            self.day.parse().ok()?,
        )
    }
}


async fn fetch_comic(num: Option<u32>) -> Result<RawComic> {
    let url = match num {
        Some(num) => format!("https://xkcd.com/{}/info.0.json", num),
        None => "https://xkcd.com/info.0.json".to_string(),
    };
    let resp = reqwest::get(url).await?;
    let comic: RawComic = resp.json().await?;
    Ok(comic)
}


async fn download_img(url: &str) -> Result<DynamicImage> {
    let resp = reqwest::get(url).await?;
    let payload = resp.bytes().await?.iter().copied().collect::<Vec<u8>>();
    Ok(image::load_from_memory(&payload)?)
}