colortty/
provider.rs

1use crate::color::ColorScheme;
2use crate::error::{ErrorKind, Result};
3use async_std::{fs, prelude::*};
4use dirs;
5use failure::ResultExt;
6use futures::future;
7use std::path::PathBuf;
8use surf::RequestBuilder;
9
10/// A GitHub repository that provides color schemes.
11pub struct Provider {
12    user_name: String,
13    repo_name: String,
14    list_path: String,
15    extension: String,
16}
17
18impl Provider {
19    /// Returns a provider for `mbadolato/iTerm2-Color-Schemes`.
20    pub fn iterm() -> Self {
21        Provider::new(
22            "mbadolato",
23            "iTerm2-Color-Schemes",
24            "schemes",
25            ".itermcolors",
26        )
27    }
28
29    /// Returns a provider for `Gogh-Co/Gogh`.
30    pub fn gogh() -> Self {
31        Provider::new("Gogh-Co", "Gogh", "themes", ".sh")
32    }
33
34    /// Returns a provider instance.
35    fn new(user_name: &str, repo_name: &str, list_path: &str, extension: &str) -> Self {
36        Provider {
37            user_name: user_name.to_string(),
38            repo_name: repo_name.to_string(),
39            list_path: list_path.to_string(),
40            extension: extension.to_string(),
41        }
42    }
43
44    /// Fetches the raw content of the color scheme for the given name.
45    pub async fn get(&self, name: &str) -> Result<ColorScheme> {
46        let req = surf::get(&self.individual_url(name));
47        let body = http_get(req).await?;
48        self.parse_color_scheme(&body)
49    }
50
51    /// Returns all color schemes in the provider.
52    ///
53    /// This function caches color schemes in the file system.
54    pub async fn list(self) -> Result<Vec<(String, ColorScheme)>> {
55        match self.read_color_schemes().await {
56            Ok(color_schemes) => {
57                if color_schemes.len() > 0 {
58                    return Ok(color_schemes);
59                }
60            }
61            _ => {}
62        }
63
64        // If there are no cached files, download them.
65        self.download_all().await?;
66        self.read_color_schemes().await
67    }
68
69    /// Download color scheme files into the cache directory.
70    pub async fn download_all(&self) -> Result<()> {
71        let repo_dir = self.repo_dir()?;
72
73        eprintln!(
74            "Downloading color schemes into {}",
75            repo_dir.to_str().unwrap()
76        );
77
78        // Create the cache directory if it doesn't exist.
79        fs::create_dir_all(&repo_dir)
80            .await
81            .context(ErrorKind::CreateDirAll)?;
82
83        let list_req = surf::get(&self.list_url());
84        let list_body = http_get(list_req).await?;
85        let items = json::parse(&list_body).context(ErrorKind::ParseJson)?;
86
87        // Download and save color scheme files.
88        let mut futures = Vec::new();
89        for item in items.members() {
90            let filename = item["name"].as_str().unwrap();
91
92            // Ignoring files starting with `_` for Gogh.
93            if filename.starts_with('_') || !filename.ends_with(&self.extension) {
94                continue;
95            }
96
97            let name = filename.replace(&self.extension, "");
98            let req = surf::get(&self.individual_url(&name));
99            futures.push(self.download_color_scheme(req, name));
100
101            // Download files in batches.
102            //
103            // If this requests all files in parallel, the HTTP client (isahc) throws the
104            // following error:
105            //
106            //   HTTP request error: ConnectFailed: failed to connect to the server
107            //
108            // isahc doesn't limit the number of connections per client by default, but
109            // it exposes an API to limit it. However, surf doesn't expose the API.
110            if futures.len() > 10 {
111                future::try_join_all(futures).await?;
112                futures = Vec::new();
113            }
114        }
115
116        Ok(())
117    }
118
119    /// Read color schemes from the cache directory.
120    async fn read_color_schemes(&self) -> Result<Vec<(String, ColorScheme)>> {
121        let mut entries = fs::read_dir(self.repo_dir()?)
122            .await
123            .context(ErrorKind::ReadDir)?;
124
125        // Collect futures and run them in parallel.
126        let mut futures = Vec::new();
127        while let Some(entry) = entries.next().await {
128            let dir_entry = entry.context(ErrorKind::ReadDirEntry)?;
129            let filename = dir_entry.file_name().into_string().unwrap();
130
131            let name = filename.replace(&self.extension, "").to_string();
132            futures.push(self.read_color_scheme(name));
133        }
134
135        let color_schemes = future::try_join_all(futures).await?;
136
137        Ok(color_schemes)
138    }
139
140    /// Reads a color scheme from the repository cache.
141    async fn read_color_scheme(&self, name: String) -> Result<(String, ColorScheme)> {
142        let file_path = self.individual_path(&name)?;
143
144        let body = fs::read_to_string(file_path)
145            .await
146            .context(ErrorKind::ReadFile)?;
147        let color_scheme = self.parse_color_scheme(&body)?;
148
149        Ok((name, color_scheme))
150    }
151
152    /// Downloads a color scheme file and save it in the cache directory.
153    async fn download_color_scheme(&self, req: RequestBuilder, name: String) -> Result<()> {
154        let body = http_get(req).await?;
155        fs::write(self.individual_path(&name)?, body)
156            .await
157            .context(ErrorKind::WriteFile)?;
158        Ok(())
159    }
160
161    /// The repository cache directory.
162    fn repo_dir(&self) -> Result<PathBuf> {
163        let mut repo_dir = dirs::cache_dir().ok_or(ErrorKind::NoCacheDir)?;
164        repo_dir.push("colortty");
165        repo_dir.push("repositories");
166        repo_dir.push(&self.user_name);
167        repo_dir.push(&self.repo_name);
168        Ok(repo_dir)
169    }
170
171    /// Returns the path for the given color scheme name.
172    fn individual_path(&self, name: &str) -> Result<PathBuf> {
173        let mut file_path = self.repo_dir()?;
174        file_path.push(name);
175        file_path.set_extension(&self.extension[1..]);
176        Ok(file_path)
177    }
178
179    /// Returns the URL for a color scheme on GitHub.
180    fn individual_url(&self, name: &str) -> String {
181        format!(
182            "https://raw.githubusercontent.com/{}/{}/master/{}/{}{}",
183            self.user_name, self.repo_name, self.list_path, name, self.extension
184        )
185    }
186
187    /// Returns the URL for the color scheme list on GitHub API.
188    fn list_url(&self) -> String {
189        format!(
190            "https://api.github.com/repos/{}/{}/contents/{}",
191            self.user_name, self.repo_name, self.list_path
192        )
193    }
194
195    /// Parses a color scheme data.
196    fn parse_color_scheme(&self, body: &str) -> Result<ColorScheme> {
197        // TODO: Think about better abstraction.
198        if self.extension == ".itermcolors" {
199            ColorScheme::from_iterm(&body)
200        } else {
201            ColorScheme::from_gogh(&body)
202        }
203    }
204}
205
206/// Returns the body of the given request.
207///
208/// Fails when the URL responds with non-200 status code. Sends `colortty` as `User-Agent` header
209async fn http_get(req: RequestBuilder) -> Result<String> {
210    let mut res = req.header("User-Agent", "colortty").await.map_err(|e| {
211        println!("HTTP request error: {}", e);
212        ErrorKind::HttpGet
213    })?;
214
215    if !res.status().is_success() {
216        println!("HTTP status code: {}", res.status());
217        return Err(ErrorKind::HttpGet.into());
218    }
219
220    // TODO: Propagate information from the original error.
221    let body = res.body_string().await.map_err(|_| ErrorKind::HttpGet)?;
222    Ok(body)
223}