diff --git a/Cargo.toml b/Cargo.toml index 997d5db..ab8ee6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ base64 = "0.21" sha1 = "0.10" clap = { version = "4.4", features = ["derive"] } directories = "5.0" -owo-colors = "3" +owo-colors = { version = "3", features = ["supports-colors"] } rpassword = "7.3" rprompt = "2.1" tabled = { version = "0.14", features = ["color"] } diff --git a/README.md b/README.md index 016015d..71e3348 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,9 @@ Options: -V, --version Print version ``` +> [!TIP] +> Use environment variable `NO_COLOR=true` to disable colored output. + ## Credentials User credentials are stored in config file `bit-user.json`. Available config file paths can be listed with: diff --git a/src/client.rs b/src/client.rs index 3b34437..ba9bb01 100644 --- a/src/client.rs +++ b/src/client.rs @@ -10,6 +10,7 @@ use hmac::Mac; use md5::Digest; use md5::Md5; use owo_colors::OwoColorize; +use owo_colors::Stream::Stdout; use reqwest::Client; use serde::Deserialize; @@ -121,19 +122,27 @@ pub async fn get_login_state(client: &Client) -> Result { /// Get the ac_id of the current device async fn get_acid(client: &Client) -> Result { - let resp = client - .get(SRUN_PORTAL) - .send() - .await - .with_context(|| format!("failed to get ac_id from `{}`", SRUN_PORTAL.underline()))?; + let resp = client.get(SRUN_PORTAL).send().await.with_context(|| { + format!( + "failed to get ac_id from `{}`", + SRUN_PORTAL.if_supports_color(Stdout, |t| t.underline()) + ) + })?; let redirect_url = resp.url().to_string(); - let parsed_url = url::Url::parse(&redirect_url) - .with_context(|| format!("failed to parse url `{}`", redirect_url.underline()))?; + let parsed_url = url::Url::parse(&redirect_url).with_context(|| { + format!( + "failed to parse url `{}`", + redirect_url.if_supports_color(Stdout, |t| t.underline()) + ) + })?; let mut query = parsed_url.query_pairs().into_owned(); - let ac_id = query - .find(|(key, _)| key == "ac_id") - .with_context(|| format!("failed to get ac_id from `{}`", redirect_url.underline()))?; + let ac_id = query.find(|(key, _)| key == "ac_id").with_context(|| { + format!( + "failed to get ac_id from `{}`", + redirect_url.if_supports_color(Stdout, |t| t.underline()) + ) + })?; Ok(ac_id.1) } @@ -215,7 +224,10 @@ impl SrunClient { if self.login_state.error == "ok" { bail!( "{} already logged in", - self.login_state.online_ip.to_string().underline() + self.login_state + .online_ip + .to_string() + .if_supports_color(Stdout, |t| t.underline()) ) } @@ -286,7 +298,12 @@ impl SrunClient { pub async fn logout(&self) -> Result { // check if already logged out if self.login_state.error == "not_online_error" { - bail!("{} already logged out", self.ip.to_string().underline()) + bail!( + "{} already logged out", + self.ip + .to_string() + .if_supports_color(Stdout, |t| t.underline()) + ) } // check if username match @@ -294,7 +311,7 @@ impl SrunClient { if logged_in_username != self.username { println!( "{} logged in user {} does not match yourself {}", - "warning:".yellow(), + "warning:".if_supports_color(Stdout, |t| t.yellow()), format!("({})", logged_in_username).dimmed(), format!("({})", self.username).dimmed() ); @@ -302,7 +319,7 @@ impl SrunClient { // tip to provide user override println!( "{:>8} provide username argument {} to override and logout current session", - "tip:".cyan(), + "tip:".if_supports_color(Stdout, |t| t.cyan()), format!("`--user {}`", logged_in_username) .bold() .bright_green() @@ -314,9 +331,13 @@ impl SrunClient { if logged_in_ip != self.ip { println!( "{} logged in ip (`{}`) does not match `{}`", - "warning:".yellow(), - logged_in_ip.to_string().underline(), - self.ip.to_string().underline() + "warning:".if_supports_color(Stdout, |t| t.yellow()), + logged_in_ip + .to_string() + .if_supports_color(Stdout, |t| t.underline()), + self.ip + .to_string() + .if_supports_color(Stdout, |t| t.underline()) ); } diff --git a/src/main.rs b/src/main.rs index 1023e48..2be3064 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,8 @@ use cli::Commands; use client::get_login_state; use client::SrunClient; use owo_colors::OwoColorize; +use owo_colors::Stream::Stderr; +use owo_colors::Stream::Stdout; use clap::Parser; use tables::print_config_paths; @@ -19,7 +21,12 @@ use tables::print_login_state; #[tokio::main] async fn main() { if let Err(err) = cli().await { - eprintln!("{} {} {}", "bitsrun:".bright_red(), "[error]".dimmed(), err); + eprintln!( + "{} {} {}", + "bitsrun:".if_supports_color(Stderr, |t| t.bright_red()), + "[error]".if_supports_color(Stderr, |t| t.dimmed()), + err + ); std::process::exit(1); } } @@ -47,9 +54,13 @@ async fn cli() -> Result<()> { if login_state.error == "ok" { println!( "{} {} {} is online", - "bitsrun:".bright_green(), - &login_state.online_ip.to_string().underline(), - format!("({})", login_state.user_name.clone().unwrap_or_default()).dimmed() + "bitsrun:".if_supports_color(Stdout, |t| t.bright_green()), + &login_state + .online_ip + .to_string() + .if_supports_color(Stdout, |t| t.underline()), + format!("({})", login_state.user_name.clone().unwrap_or_default()) + .if_supports_color(Stdout, |t| t.dimmed()) ); // print status table @@ -57,8 +68,11 @@ async fn cli() -> Result<()> { } else { println!( "{} {} is offline", - "bitsrun:".blue(), - login_state.online_ip.to_string().underline() + "bitsrun:".if_supports_color(Stdout, |t| t.blue()), + login_state + .online_ip + .to_string() + .if_supports_color(Stdout, |t| t.underline()) ); } } @@ -86,41 +100,54 @@ async fn cli() -> Result<()> { match resp.error.as_str() { "ok" => println!( "{} {} {} logged in", - "bitsrun:".bright_green(), - resp.online_ip.to_string().underline(), - format!("({})", resp.username.clone().unwrap_or_default()).dimmed() + "bitsrun:".if_supports_color(Stdout, |t| t.bright_green()), + resp.online_ip + .to_string() + .if_supports_color(Stdout, |t| t.underline()), + format!("({})", resp.username.clone().unwrap_or_default()) + .if_supports_color(Stdout, |t| t.dimmed()) ), _ => println!( "{} failed to login, {} {}", - "bitsrun:".red(), + "bitsrun:".if_supports_color(Stdout, |t| t.red()), resp.error, - format!("({})", resp.error_msg).dimmed() + format!("({})", resp.error_msg).if_supports_color(Stdout, |t| t.dimmed()) ), } if args.verbose { let pretty_json = serde_json::to_string_pretty(&resp)?; - println!("{} response from API\n{}", "bitsrun:".blue(), pretty_json); + println!( + "{} response from API\n{}", + "bitsrun:".if_supports_color(Stdout, |t| t.blue()), + pretty_json + ); } } else if matches!(args.command, Some(Commands::Logout(_))) { let resp = srun_client.logout().await?; match resp.error.as_str() { "ok" => println!( "{} {} logged out", - "bitsrun:".green(), - resp.online_ip.to_string().underline() + "bitsrun:".if_supports_color(Stdout, |t| t.green()), + resp.online_ip + .to_string() + .if_supports_color(Stdout, |t| t.underline()) ), _ => println!( "{} failed to logout, {} {}", - "bitsrun:".red(), + "bitsrun:".if_supports_color(Stdout, |t| t.red()), resp.error, - format!("({})", resp.error_msg).dimmed() + format!("({})", resp.error_msg).if_supports_color(Stdout, |t| t.dimmed()) ), } if args.verbose { let pretty_json = serde_json::to_string_pretty(&resp)?; - println!("{} response from API\n{}", "bitsrun:".blue(), pretty_json); + println!( + "{} response from API\n{}", + "bitsrun:".if_supports_color(Stdout, |t| t.blue()), + pretty_json + ); } } } diff --git a/src/tables.rs b/src/tables.rs index 395b1cc..2be3b35 100644 --- a/src/tables.rs +++ b/src/tables.rs @@ -7,6 +7,7 @@ use chrono_humanize::Tense::Present; use humansize::format_size; use humansize::BINARY; use owo_colors::OwoColorize; +use owo_colors::Stream::Stdout; use tabled::builder::Builder; use tabled::settings::Style; use tabled::settings::Width; @@ -21,7 +22,10 @@ use tabled::settings::Width; /// │ 1 │ C:\Users\{USERNAME}\AppData\Roaming\bitsrun\bit-user.json │ /// └──────────┴───────────────────────────────────────────────────────────┘ pub fn print_config_paths() { - println!("{} list of possible config paths", "bitsrun:".blue()); + println!( + "{} list of possible config paths", + "bitsrun:".if_supports_color(Stdout, |t| t.blue()) + ); let mut builder = Builder::default(); builder.set_header(["Priority", "Possible Config Path"]); @@ -35,6 +39,14 @@ pub fn print_config_paths() { } /// Print login state table +/// +/// # Example output +/// +/// ┌────────────────┬───────────────┬───────────────┬─────────┐ +/// │ Traffic Used │ Online Time │ User Balance │ Wallet │ +/// ├────────────────┼───────────────┼───────────────┼─────────┤ +/// │ 188.10 GiB │ 2 months │ 10.00 │ 0.00 │ +/// └────────────────┴───────────────┴───────────────┴─────────┘ pub fn print_login_state(state: SrunLoginState) { let mut builder = Builder::default(); builder.set_header(["Traffic Used", "Online Time", "User Balance", "Wallet"]); @@ -49,10 +61,19 @@ pub fn print_login_state(state: SrunLoginState) { let wallet = state.wallet_balance.unwrap_or(0) as f32; builder.push_record([ - format_size(traffic_used, BINARY).green().to_string(), - human_time.to_text_en(Rough, Present).yellow().to_string(), - format!("{:.2}", user_balance).cyan().to_string(), - format!("{:.2}", wallet).magenta().to_string(), + format_size(traffic_used, BINARY) + .if_supports_color(Stdout, |t| t.green()) + .to_string(), + human_time + .to_text_en(Rough, Present) + .if_supports_color(Stdout, |t| t.yellow()) + .to_string(), + format!("{:.2}", user_balance) + .if_supports_color(Stdout, |t| t.cyan()) + .to_string(), + format!("{:.2}", wallet) + .if_supports_color(Stdout, |t| t.magenta()) + .to_string(), ]); let mut table = builder.build(); diff --git a/src/user.rs b/src/user.rs index 1ada5d3..f685fdf 100644 --- a/src/user.rs +++ b/src/user.rs @@ -5,6 +5,7 @@ use anyhow::anyhow; use anyhow::Context; use anyhow::Result; use owo_colors::OwoColorize; +use owo_colors::Stream::Stdout; use serde::Deserialize; use serde::Serialize; @@ -101,14 +102,23 @@ fn parse_config_file(config_path: &Option) -> Result { if config.is_empty() { Err(anyhow!( "config file `{}` not found, available paths can be found with `{}`", - "bit-user.json".underline(), - "bitsrun config-paths".cyan().bold().underline() + "bit-user.json".if_supports_color(Stdout, |t| t.underline()), + "bitsrun config-paths".if_supports_color(Stdout, |t| t.cyan()) )) } else { - let user_str_from_file = fs::read_to_string(&config) - .with_context(|| format!("failed to read config file `{}`", &config.underline()))?; + let user_str_from_file = fs::read_to_string(&config).with_context(|| { + format!( + "failed to read config file `{}`", + &config.if_supports_color(Stdout, |t| t.underline()) + ) + })?; let user_from_file = serde_json::from_str::(&user_str_from_file) - .with_context(|| format!("failed to parse config file `{}`", &config.underline()))?; + .with_context(|| { + format!( + "failed to parse config file `{}`", + &config.if_supports_color(Stdout, |t| t.underline()) + ) + })?; Ok(user_from_file) } } @@ -130,15 +140,21 @@ pub fn get_bit_user( let mut user_from_file = BitUserPartial::default(); match parse_config_file(config_path) { Ok(value) => user_from_file = value, - Err(e) => println!("{} {}", "warning:".yellow(), e), + Err(e) => println!( + "{} {}", + "warning:".if_supports_color(Stdout, |t| t.yellow()), + e + ), } match user_from_file.username { Some(username) => bit_user.username.get_or_insert(username), None => bit_user.username.get_or_insert_with(|| { - rprompt::prompt_reply("-> please enter your campus id: ".dimmed()) - .with_context(|| "failed to read username") - .unwrap() + rprompt::prompt_reply( + "-> please enter your campus id: ".if_supports_color(Stdout, |t| t.dimmed()), + ) + .with_context(|| "failed to read username") + .unwrap() }), }; @@ -146,9 +162,11 @@ pub fn get_bit_user( Some(password) => bit_user.password.get_or_insert(password), None => bit_user.password.get_or_insert_with(|| { if require_password { - rpassword::prompt_password("-> please enter your password: ".dimmed()) - .with_context(|| "failed to read password") - .unwrap() + rpassword::prompt_password( + "-> please enter your password: ".if_supports_color(Stdout, |t| t.dimmed()), + ) + .with_context(|| "failed to read password") + .unwrap() } else { "".into() }