summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Tay <sam.chong.tay@gmail.com>2020-06-06 19:55:23 -0700
committerSam Tay <sam.chong.tay@gmail.com>2020-06-07 02:21:54 -0700
commit799936479d3e80237c6e6595baade9737e137011 (patch)
treed74d5bad458d08cfd2f74f125c4c45fd3128d82c
parent735fd480e43d6037f3ae7890c2795fdf0e431fd7 (diff)
Propagate errors throughout app
and add stdout styles via crossterm
-rw-r--r--Cargo.lock102
-rw-r--r--Cargo.toml2
-rw-r--r--README.md3
-rw-r--r--TODO.md4
-rw-r--r--src/cli.rs12
-rw-r--r--src/config.rs32
-rw-r--r--src/error.rs83
-rw-r--r--src/macros.rs15
-rw-r--r--src/main.rs117
-rw-r--r--src/stackexchange.rs105
-rw-r--r--src/term.rs60
11 files changed, 429 insertions, 106 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 0689438..b54aaa0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -16,10 +16,10 @@ dependencies = [
]
[[package]]
-name = "anyhow"
-version = "1.0.31"
+name = "arc-swap"
+version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f"
+checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034"
[[package]]
name = "arrayref"
@@ -119,6 +119,15 @@ dependencies = [
]
[[package]]
+name = "cloudabi"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -161,6 +170,31 @@ dependencies = [
]
[[package]]
+name = "crossterm"
+version = "0.17.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9851d20b9809e561297ec3ca85d7cba3a57507fe8d01d07ba7b52469e1c89a11"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "lazy_static",
+ "libc",
+ "mio",
+ "parking_lot",
+ "signal-hook",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "057b7146d02fb50175fd7dbe5158f6097f33d02831f43b4ee8ae4ddf67b68f5c"
+dependencies = [
+ "winapi 0.3.8",
+]
+
+[[package]]
name = "directories"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -473,6 +507,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a"
[[package]]
+name = "lock_api"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
name = "log"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -628,6 +671,30 @@ dependencies = [
]
[[package]]
+name = "parking_lot"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3"
+dependencies = [
+ "cfg-if",
+ "cloudabi",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi 0.3.8",
+]
+
+[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -825,6 +892,12 @@ dependencies = [
]
[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
name = "security-framework"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -903,6 +976,27 @@ dependencies = [
]
[[package]]
+name = "signal-hook"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ff2db2112d6c761e12522c65f7768548bd6e8cd23d2a9dae162520626629bd6"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41"
+dependencies = [
+ "arc-swap",
+ "libc",
+]
+
+[[package]]
name = "slab"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -918,8 +1012,8 @@ checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4"
name = "so"
version = "0.1.0"
dependencies = [
- "anyhow",
"clap",
+ "crossterm",
"directories",
"flate2",
"reqwest",
diff --git a/Cargo.toml b/Cargo.toml
index 44cd9c5..708d51f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,7 +7,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-anyhow = "1.0"
+crossterm = "0.17"
clap = "2.33"
directories = "2.0"
flate2 = "1.0"
diff --git a/README.md b/README.md
index defc45f..4661553 100644
--- a/README.md
+++ b/README.md
@@ -11,3 +11,6 @@ run into throttling issues, get a key
```
so --set-api-key <KEY>
```
+
+Recall my api key is: `8o9g7WcfwnwbB*Qp4VsGsw((`
+
diff --git a/TODO.md b/TODO.md
index d3c904b..ecd720c 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,8 +1,6 @@
# TODO
### initial release
-0. Install sites when file not found
-0. Implement --update-sites command
3. Parse markdown (`pulldown_cmark`)
4. Maybe default --validate-sites off (parsing 30k file a big hit)
5. Print to stderr in [style](https://github.com/BurntSushi/termcolor)
@@ -20,3 +18,5 @@
7. App Distribution
[cross-platform binaries via travis](https://github.com/rustwasm/wasm-pack/blob/51e6351c28fbd40745719e6d4a7bf26dadd30c85/.travis.yml#L74-L91)
also see lobster script in this [repo](https://git.sr.ht/~wezm/lobsters).
+
+9. Great tui-rs [example app](https://github.com/SoptikHa2/desed/blob/master/src/ui/tui.rs)
diff --git a/src/cli.rs b/src/cli.rs
index b7c7de7..8eddc1e 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -2,6 +2,7 @@ use clap::{App, AppSettings, Arg};
use crate::config;
use crate::config::Config;
+use crate::error::Result;
// TODO maybe consts for these keywords?
@@ -17,8 +18,8 @@ pub struct Opts {
pub config: Config,
}
-pub fn get_opts() -> Opts {
- let config = config::user_config();
+pub fn get_opts() -> Result<Opts> {
+ let config = config::user_config()?;
let limit = &config.limit.to_string();
let matches = App::new("so")
.setting(AppSettings::ColoredHelp)
@@ -69,20 +70,21 @@ pub fn get_opts() -> Opts {
.required_unless_one(&["list-sites", "update-sites"]),
)
.get_matches();
- Opts {
+
+ Ok(Opts {
list_sites: matches.is_present("list-sites"),
update_sites: matches.is_present("update-sites"),
query: matches
.values_of("query")
.map(|q| q.into_iter().collect::<Vec<_>>().join(" ")),
config: Config {
- // these unwraps are safe b.c. default value
+ // these unwraps are safe via clap default values & validators
limit: matches.value_of("limit").unwrap().parse::<u16>().unwrap(),
site: matches.value_of("site").unwrap().to_string(),
// TODO if set_api_key passed, pass it here too
..config
},
- }
+ })
}
#[cfg(test)]
diff --git a/src/config.rs b/src/config.rs
index 957a46d..88666d5 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -4,6 +4,8 @@ use serde_yaml;
use std::fs;
use std::fs::File;
+use crate::error::{Error, Result};
+
#[derive(Deserialize, Serialize, Debug)]
pub struct Config {
pub api_key: Option<String>,
@@ -22,32 +24,28 @@ impl Default for Config {
}
/// Get user config (writes default if none found)
-pub fn user_config() -> Config {
- let project = project_dir();
+pub fn user_config() -> Result<Config> {
+ let project = project_dir()?;
let dir = project.config_dir();
- fs::create_dir_all(&dir).unwrap(); // TODO bubble to main
+ fs::create_dir_all(&dir).map_err(|_| Error::create_dir(&dir.to_path_buf()))?;
let filename = dir.join("config.yml");
match File::open(&filename) {
Err(_) => {
- let file = File::create(&filename).unwrap();
+ let file = File::create(&filename).map_err(|_| Error::create_file(&filename))?;
let def = Config::default();
- serde_yaml::to_writer(file, &def).unwrap();
- def
+ serde_yaml::to_writer(file, &def).map_err(|_| Error::write_file(&filename))?;
+ Ok(def)
}
- Ok(file) => serde_yaml::from_reader(file).expect(&format!(
- "Local config corrupted; try removing it `rm {}`",
- filename.display()
- )),
+ Ok(file) => serde_yaml::from_reader(file).map_err(|_| Error::malformed(&filename)),
}
}
-/// Get project directory; might panic on unexpected OS
-pub fn project_dir() -> ProjectDirs {
- ProjectDirs::from("io", "Sam Tay", "so").expect(
- "Couldn't find
- a suitable project directory to store cache and configuration; this
- application may not be supported on your operating system.",
- )
+/// Get project directory
+pub fn project_dir() -> Result<ProjectDirs> {
+ ProjectDirs::from("io", "Sam Tay", "so").ok_or(Error::os(
+ "Couldn't find a suitable project directory to store cache and configuration;\n\
+ this application may not be supported on your operating system.",
+ ))
}
#[cfg(test)]
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..b6046af
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,83 @@
+use std::path::PathBuf;
+
+pub type Result<T, E = Error> = std::result::Result<T, E>;
+
+pub struct Error {
+ #[allow(dead_code)]
+ pub kind: ErrorKind,
+ pub error: String,
+}
+pub enum ErrorKind {
+ Malformed,
+ StackExchange,
+ Permissions,
+ OperatingSystem,
+ Panic,
+ EmptySites,
+ NoResults,
+}
+
+impl From<&str> for Error {
+ fn from(err: &str) -> Self {
+ Error {
+ kind: ErrorKind::Panic,
+ error: String::from(err),
+ }
+ }
+}
+
+// TODO add others
+impl Error {
+ pub fn malformed(path: &PathBuf) -> Self {
+ Error {
+ kind: ErrorKind::Malformed,
+ error: format!("File {} is malformed; try removing it.", path.display()),
+ }
+ }
+ pub fn se(err: String) -> Self {
+ Error {
+ kind: ErrorKind::StackExchange,
+ error: err,
+ }
+ }
+ pub fn create_dir(path: &PathBuf) -> Self {
+ Error {
+ kind: ErrorKind::Permissions,
+ error: format!(
+ "Couldn't create directory {}; please check the permissions
+ on the parent directory",
+ path.display()
+ ),
+ }
+ }
+ pub fn create_file(path: &PathBuf) -> Self {
+ Error {
+ kind: ErrorKind::Permissions,
+ error: format!(
+ "Couldn't create file {}; please check the directory permissions",
+ path.display()
+ ),
+ }
+ }
+ pub fn write_file(path: &PathBuf) -> Self {
+ Error {
+ kind: ErrorKind::Permissions,
+ error: format!(
+ "Couldn't write to file {}; please check its permissions",
+ path.display()
+ ),
+ }
+ }
+ pub fn os(err: &str) -> Self {
+ Error {
+ kind: ErrorKind::OperatingSystem,
+ error: String::from(err),
+ }
+ }
+ pub fn no_results() -> Self {
+ Error {
+ kind: ErrorKind::NoResults,
+ error: String::from("Sorry, no answers found for your question. Try another query."),
+ }
+ }
+}
diff --git a/src/macros.rs b/src/macros.rs
new file mode 100644
index 0000000..a41443c
--- /dev/null
+++ b/src/macros.rs
@@ -0,0 +1,15 @@
+#[macro_export]
+macro_rules! printerr {
+ ($($arg:tt)*) => ({
+ use std::io::{Write, stderr};
+ use crossterm::{execute};
+ use crossterm::style::{Print, SetForegroundColor, ResetColor, Color};
+ execute!(
+ stderr(),
+ SetForegroundColor(Color::Red),
+ Print("✖ ".to_string()),
+ Print($($arg)*.to_string()),
+ ResetColor
+ ).ok();
+ })
+}
diff --git a/src/main.rs b/src/main.rs
index e9f0d99..e48323d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,63 +1,94 @@
mod cli;
mod config;
+mod error;
+mod macros;
mod stackexchange;
+mod term;
use config::Config;
+use error::{Error, ErrorKind};
use stackexchange::{LocalStorage, StackExchange};
+use std::io::stderr;
+use term::ColoredOutput;
fn main() {
- // TODO wrap inner function with Result<(), ErrorMessage>, propagate, print to stderr at the top level.
- let opts = cli::get_opts();
- let config = opts.config;
- let site = &config.site;
- let mut ls = LocalStorage::new();
+ (|| {
+ let opts = cli::get_opts()?;
+ let config = opts.config;
+ let site = &config.site;
+ let mut ls = LocalStorage::new()?;
- if opts.update_sites {
- ls.update_sites();
- }
+ if opts.update_sites {
+ ls.update_sites()?;
+ }
- if opts.list_sites {
- let sites = ls.sites();
- match sites.into_iter().map(|s| s.api_site_parameter.len()).max() {
- Some(max_w) => {
- for s in ls.sites() {
- println!("{:>w$}: {}", s.api_site_parameter, s.site_url, w = max_w);
+ if opts.list_sites {
+ let sites = ls.sites()?;
+ match sites.into_iter().map(|s| s.api_site_parameter.len()).max() {
+ Some(max_w) => {
+ for s in sites {
+ println!("{:>w$}: {}", s.api_site_parameter, s.site_url, w = max_w);
+ }
+ }
+ None => {
+ stderr()
+ .queue_error("The site list is empty. Try running ")
+ .queue_code_inline("so --update-sites")
+ .unsafe_flush();
}
}
- None => {
- // TODO stderr
- println!("The site list is empty. Try running `so --update-sites`.");
+ return Ok(());
+ }
+
+ match ls.validate_site(site) {
+ Ok(true) => (),
+ Ok(false) => {
+ stderr()
+ .queue_error(&format!("{} is not a valid StackExchange site.\n\n", site)[..])
+ .queue_notice("If you think this is in error, try running\n\n")
+ .queue_code("so --update-sites\n\n")
+ .queue_notice_inline("to update the cached site listing. You can also run ")
+ .queue_code_inline("so --list-sites")
+ .queue_notice_inline(" to list all available sites.")
+ .unsafe_flush();
+ return Ok(());
}
+ Err(Error {
+ kind: ErrorKind::EmptySites,
+ ..
+ }) => {
+ stderr()
+ .queue_error("The cached site list is empty. This can likely be fixed by\n\n")
+ .queue_code("so --update-sites\n\n")
+ .unsafe_flush();
+ return Ok(());
+ }
+ Err(e) => return Err(e),
}
- return;
- }
- // TODO make this validation optional
- if !ls.validate_site(site) {
- // TODO tooling for printing to stderr with color, etc.
- println!(
- "{} is not a valid StackExchange site. If you think this
- is in error, try running `so --update-sites` to update
- the cached site listing. Run `so --list-sites` for all
- available sites.",
- site
- );
- return;
- }
+ let se = StackExchange::new(Config {
+ api_key: Some(String::from("8o9g7WcfwnwbB*Qp4VsGsw((")), // TODO remove when releasing
+ ..config
+ });
- let se = StackExchange::new(Config {
- api_key: Some(String::from("8o9g7WcfwnwbB*Qp4VsGsw((")), // TODO stash this
- ..config
- });
+ if let Some(q) = opts.query {
+ let que = se.search(&q)?;
+ let ans = que
+ .first()
+ .ok_or(Error::no_results())?
+ .answers
+ .first()
+ .ok_or(Error::from(
+ "StackExchange returned a question with no answers; this shouldn't be possible!",
+ ))?;
+ println!("{}", ans.body);
+ }
- let query = opts.query;
- (|| -> Option<_> {
- let q = query?;
- let que = se.search(&q).unwrap(); // TODO eventually be graceful
- let ans = que.first()?.answers.first()?;
- println!("{}", ans.body);
- Some(())
- })();
+ Ok(())
+ })()
+ .unwrap_or_else(|e| match e {
+ Error { error, .. } => printerr!(error),
+ })
}
#[cfg(test)]
diff --git a/src/stackexchange.rs b/src/stackexchange.rs
index 1aed7d8..9cd9767 100644
--- a/src/stackexchange.rs
+++ b/src/stackexchange.rs
@@ -1,4 +1,3 @@
-use anyhow;
use flate2::read::GzDecoder;
use reqwest::blocking::Client;
use reqwest::Url;
@@ -9,6 +8,7 @@ use std::fs::File;
use std::path::PathBuf;
use crate::config::{project_dir, Config};
+use crate::error::{Error, ErrorKind, Result};
/// StackExchange API v2.2 URL
const SE_URL: &str = "http://api.stackexchange.com/2.2/";
@@ -81,10 +81,10 @@ impl StackExchange {
/// Only fetches questions that have at least one answer.
/// TODO async
/// TODO parallel requests over multiple sites
- pub fn search(&self, q: &str) -> Result<Vec<Question>, anyhow::Error> {
+ pub fn search(&self, q: &str) -> Result<Vec<Question>> {
let resp_body = self
.client
- .get(stackechange_url("search/advanced"))
+ .get(stackexchange_url("search/advanced"))
.header("Accepts", "application/json")
.query(&self.get_default_opts())
.query(&[
@@ -95,9 +95,22 @@ impl StackExchange {
("order", "desc"),
("sort", "relevance"),
])
- .send()?;
+ .send()
+ .map_err(|e| {
+ // TODO explore legit errors such as not connected to internet
+ Error::se(format!(
+ "Error encountered while querying StackExchange: {}",
+ e
+ ))
+ })?;
+
let gz = GzDecoder::new(resp_body);
- let wrapper: ResponseWrapper<Question> = serde_json::from_reader(gz)?;
+ let wrapper: ResponseWrapper<Question> = serde_json::from_reader(gz).map_err(|e| {
+ Error::se(format!(
+ "Error decoding questions from the StackExchange API: {}",
+ e
+ ))
+ })?;
let qs = wrapper
.items
.into_iter()
@@ -121,69 +134,93 @@ impl StackExchange {
}
impl LocalStorage {
- pub fn new() -> Self {
- let project = project_dir();
+ pub fn new() -> Result<Self> {
+ let project = project_dir()?;
let dir = project.cache_dir();
- fs::create_dir_all(&dir).unwrap(); // TODO bubble to main
- LocalStorage {
+ fs::create_dir_all(&dir).map_err(|_| Error::create_dir(&dir.to_path_buf()))?;
+ Ok(LocalStorage {
sites: None,
filename: dir.join("sites.json"),
- }
+ })
}
// TODO make this async, inform user if we are downloading
- pub fn sites(&mut self) -> &Vec<Site> {
+ pub fn sites(&mut self) -> Result<&Vec<Site>> {
+ // Stop once Option ~ Some or Result ~ Err
+ if let Some(_) = self.sites {
+ return Ok(self.sites.as_ref().unwrap()); // safe
+ }
+ if let Some(_) = self.fetch_local_sites()? {
+ return Ok(self.sites.as_ref().unwrap()); // safe
+ }
+ self.fetch_remote_sites()?;
self.sites
.as_ref()
- .map(|_| ()) // stop if we already have sites
- .or_else(|| self.fetch_local_sites()) // otherwise try local cache
- .unwrap_or_else(|| self.fetch_remote_sites()); // otherwise remote fetch
- self.sites.as_ref().unwrap() // we will have paniced earlier on failure
+ .ok_or(Error::from("Code failure in site listing retrieval"))
}
- pub fn update_sites(&mut self) {
- self.fetch_remote_sites();
+ pub fn update_sites(&mut self) -> Result<()> {
+ self.fetch_remote_sites()
}
- pub fn validate_site(&mut self, site_code: &String) -> bool {
- self.sites()
+ pub fn validate_site(&mut self, site_code: &String) -> Result<bool> {
+ let sites = self.sites()?;
+ if sites.is_empty() {
+ return Err(Error {
+ kind: ErrorKind::EmptySites,
+ error: String::from(""),
+ });
+ }
+ Ok(sites
.iter()
- .any(|site| site.api_site_parameter == *site_code)
+ .any(|site| site.api_site_parameter == *site_code))
}
- fn fetch_local_sites(&mut self) -> Option<()> {
- let file = File::open(&self.filename).ok()?;
- self.sites = serde_json::from_reader(file)
- .expect("Local cache corrupted; try running `so --update-sites`");
- Some(())
+ fn fetch_local_sites(&mut self) -> Result<Option<()>> {
+ if let Some(file) = File::open(&self.filename).ok() {
+ self.sites =
+ serde_json::from_reader(file).map_err(|_| Error::malformed(&self.filename))?;
+ return Ok(Some(()));
+ }
+ Ok(None)
}
// TODO decide whether or not I should give LocalStorage an api key..
// TODO cool loading animation?
- fn fetch_remote_sites(&mut self) {
+ fn fetch_remote_sites(&mut self) -> Result<()> {
let resp_body = Client::new()
- .get(stackechange_url("sites"))
+ .get(stackexchange_url("sites"))
.header("Accepts", "application/json")
.query(&[
("pagesize", SE_SITES_PAGESIZE.to_string()),
("page", "1".to_string()),
])
.send()
- .unwrap(); // TODO inspect response for errors e.g. throttle
+ .map_err(|e| {
+ Error::se(format!(
+ "Error requesting sites from StackExchange API: {}",
+ e
+ ))
+ })?;
let gz = GzDecoder::new(resp_body);
- let wrapper: ResponseWrapper<Site> = serde_json::from_reader(gz).unwrap();
+ let wrapper: ResponseWrapper<Site> = serde_json::from_reader(gz).map_err(|e| {
+ Error::se(format!(
+ "Error decoding sites from the StackExchange API: {}",
+ e
+ ))
+ })?;
self.sites = Some(wrapper.items);
- self.store_local_sites();
+ self.store_local_sites()
}
- fn store_local_sites(&self) {
- let file = File::create(&self.filename).unwrap();
- serde_json::to_writer(file, &self.sites).unwrap();
+ fn store_local_sites(&self) -> Result<()> {
+ let file = File::create(&self.filename).map_err(|_| Error::create_file(&self.filename))?;
+ serde_json::to_writer(file, &self.sites).map_err(|_| Error::write_file(&self.filename))
}
}
/// Creates url from const string; can technically panic
-fn stackechange_url(path: &str) -> Url {
+fn stackexchange_url(path: &str) -> Url {
let mut url = Url::parse(SE_URL).unwrap();
url.set_path(path);
url
diff --git a/src/term.rs b/src/term.rs
new file mode 100644
index 0000000..9356bf6
--- /dev/null
+++ b/src/term.rs
@@ -0,0 +1,60 @@
+use crossterm::style::{Color, Print, ResetColor, SetForegroundColor};
+use crossterm::QueueableCommand;
+use std::io::{Stderr, Write};
+
+pub trait ColoredOutput {
+ fn queue_general(&mut self, color: Color, prefix: &str, s: &str) -> &mut Self;
+
+ // TODO is it cool to unwrap flushing some known text?
+ fn unsafe_flush(&mut self);
+
+ fn queue_error(&mut self, s: &str) -> &mut Self {
+ self.queue_general(Color::Red, "✖ ", s)
+ }
+
+ fn queue_success(&mut self, s: &str) -> &mut Self {
+ self.queue_general(Color::Green, "✔ ", s)
+ }
+
+ fn queue_notice(&mut self, s: &str) -> &mut Self {
+ self.queue_general(Color::Yellow, "➜ ", s)
+ }
+
+ fn queue_notice_inline(&mut self, s: &str) -> &mut Self {
+ self.queue_general(Color::Yellow, "", s)
+ }
+
+ fn queue_log(&mut self, s: &str) -> &mut Self {
+ self.queue_general(Color::Blue, "• ", s)
+ }
+
+ fn queue_code(&mut self, s: &str) -> &mut Self {
+ self.queue_general(Color::Cyan, "\t", s)
+ }
+
+ fn queue_code_inline(&mut self, s: &str) -> &mut Self {
+ self.queue_general(Color::Cyan, "", s)
+ }
+
+ fn queue_warn(&mut self, s: &str) -> &mut Self {
+ self.queue_general(Color::Magenta, "⚡", s)
+ }
+}
+
+impl ColoredOutput for Stderr {
+ fn queue_general(&mut self, color: Color, prefix: &str, s: &str) -> &mut Self {
+ (|| -> Result<(), crossterm::ErrorKind> {
+ self.queue(SetForegroundColor(color))?
+ .queue(Print(prefix.to_string()))?
+ .queue(Print(s.to_string()))?
+ .queue(ResetColor)?;
+ Ok(())
+ })()
+ .unwrap();
+ self
+ }
+
+ fn unsafe_flush(&mut self) {
+ self.flush().unwrap();
+ }
+}