use anyhow::{bail, Context, Result};
use clap::{crate_authors, crate_description, crate_version, AppSettings, Parser};
use indicatif::ProgressStyle;
use std::iter::IntoIterator;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::thread;
use crate::cmd::{Check, Clean, List, Push, Rename, Verify};
#[derive(Parser, Debug)]
#[clap(
version=crate_version!(),
author=crate_authors!(),
about=crate_description!(),
global_setting=AppSettings::InferSubcommands)]
pub struct Opts {
#[clap(short = 'c', long = "config")]
pub config: Option<String>,
#[clap(short, long, parse(from_occurrences))]
pub verbose: i32,
#[clap(short, long, parse(from_occurrences))]
pub quiet: i32,
#[clap(
short,
long,
possible_values = &["trace", "debug", "info", "warn", "error"],
)]
pub loglevel: Option<String>,
#[clap(short = 'H', long)]
pub host: Option<String>,
#[clap(subcommand)]
pub cmd: UserCommand,
}
impl Opts {
pub fn verify(&self) -> Result<()> {
match (self.verbose, self.quiet, self.loglevel.as_deref()) {
(_, 0, None) => Ok(()),
(0, _, None) => Ok(()),
(v, q, _) if v + q > 0 => {
bail!("Cannot specify --verbose and --quiet.");
}
(v, _, Some(_)) if v > 0 => {
bail!("Cannot specify --verbose and --loglevel");
}
(_, q, Some(_)) if q > 0 => {
bail!("Cannot specify --quiet and --loglevel");
}
_ => Ok(()),
}
}
}
#[derive(Parser, Debug)]
pub enum UserCommand {
#[clap(name = "check")]
Check(Check),
#[clap(name = "clean")]
Clean(Clean),
#[clap(name = "list")]
List(List),
#[clap(name = "mv")]
Mv(Rename),
#[clap(name = "rename")]
Rename(Rename),
#[clap(name = "push")]
Push(Push),
#[clap(name = "verify")]
Verify(Verify),
}
pub fn style_progress_bar_transfer() -> Result<indicatif::ProgressStyle> {
Ok(ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes} / {total_bytes} \
@ {bytes_per_sec} ({eta})",
)?
.progress_chars("#>-"))
}
pub fn style_progress_bar_count() -> Result<indicatif::ProgressStyle> {
Ok(ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] {msg}[{bar:40.cyan/blue}] {pos} / {len} \
@ {per_sec} ({eta})",
)?
.progress_chars("#>-"))
}
pub fn spinner() -> Result<indicatif::ProgressBar> {
let bar = indicatif::ProgressBar::new(!0);
bar.set_style(style_spinner()?);
Ok(bar)
}
pub fn style_spinner() -> Result<indicatif::ProgressStyle> {
Ok(ProgressStyle::default_spinner().template("{spinner:.green} {msg}")?)
}
pub fn draw_boxed<'a, H: AsRef<str>, I: IntoIterator<Item = &'a str>>(
header: H,
content: I,
color_box: &console::Style,
) -> Result<()> {
let corner_top_left = color_box.apply_to("┌");
let corner_top_right = color_box.apply_to("┐");
let corner_bottom_left = color_box.apply_to("└");
let corner_bottom_right = color_box.apply_to("┘");
let header_left = color_box.apply_to("┤");
let header_right = color_box.apply_to("├");
let content: Vec<&str> = content.into_iter().collect();
let header_len = console::strip_ansi_codes(header.as_ref()).chars().count();
let line_len = {
let content_max = content
.iter()
.map(|l| console::strip_ansi_codes(l).chars().count())
.max()
.with_context(|| "Nothing to show.")?;
*[60, content_max, header_len + 2].iter().max().unwrap()
};
let line_horizontal = |len: usize| color_box.apply_to("─".repeat(len));
let line_vertical = color_box.apply_to("│");
let header_raw = format!(
"{cl}{hl}{hdr}{hr}{fl}{cr}",
cl = corner_top_left,
cr = corner_top_right,
hl = header_left,
hr = header_right,
hdr = header.as_ref(),
fl = line_horizontal(line_len - 2 - header_len)
);
println!("{}", join_frames(content[0], &header_raw, '┬'));
for line in content.iter() {
let pad_width = line_len - console::strip_ansi_codes(line).chars().count();
println!(
"{border}{line}{pad}{border}",
line = line,
border = line_vertical,
pad = " ".repeat(pad_width)
);
}
let last_line_raw = format!(
"{cl}{l}{cr}",
cl = corner_bottom_left,
cr = corner_bottom_right,
l = line_horizontal(line_len)
);
let last_line = join_frames(content[content.len() - 1], &last_line_raw, '┴');
println!("{}", last_line);
Ok(())
}
fn join_frames(content: &str, raw: &str, joiner: char) -> String {
let mut replacer = text::ColoredTextReplacer::new(raw.to_string());
let nocolor: Vec<_> = console::strip_ansi_codes(raw).chars().collect();
for idx_separator in console::strip_ansi_codes(content)
.chars()
.enumerate()
.filter_map(|(idx, c)| if c == '│' { Some(idx) } else { None })
{
let idx = idx_separator + 1;
if nocolor[idx] == '─' {
replacer.replace(idx_separator + 1, joiner);
}
}
replacer.get()
}
pub struct WaitingSpinner {
handle: thread::JoinHandle<()>,
stop_token: Arc<AtomicBool>,
tx: mpsc::Sender<SpinnerSetting>,
}
enum SpinnerSetting {
Println(String),
Message(String),
}
impl WaitingSpinner {
pub fn new(message: String) -> Self {
let (tx, rx) = channel();
let stop_token = Arc::new(AtomicBool::new(false));
let stop_token_pbar = Arc::clone(&stop_token);
let handle = thread::spawn(move || {
let spinner = crate::cli::spinner().expect("couldn't create spinner");
spinner.set_message(message.clone());
let handle_messages = || {
while let Ok(msg) = rx.try_recv() {
match msg {
SpinnerSetting::Message(msg) => spinner.set_message(msg.clone()),
SpinnerSetting::Println(msg) => spinner.println(&msg),
}
}
};
while !stop_token_pbar.load(Ordering::Relaxed) {
handle_messages();
spinner.inc(1);
std::thread::sleep(std::time::Duration::from_millis(25));
}
handle_messages();
spinner.inc(1);
spinner.finish_and_clear();
});
Self {
handle,
stop_token,
tx,
}
}
pub fn set_message(&self, message: String) -> Result<()> {
self.tx.send(SpinnerSetting::Message(message))?;
Ok(())
}
pub fn println(&self, msg: String) -> Result<()> {
self.tx.send(SpinnerSetting::Println(msg))?;
Ok(())
}
pub fn finish(self) {
self.stop_token.store(true, Ordering::Relaxed);
self.handle.join().unwrap();
}
}
#[allow(non_upper_case_globals)]
pub mod color {
use console::Style;
lazy_static::lazy_static! {
pub static ref dot : Style = Style::new().cyan();
pub static ref entry : Style = Style::new();
pub static ref expire : Style = Style::new().red();
pub static ref failure : Style = Style::new().red().bright();
pub static ref filename : Style = Style::new().blue().bright();
pub static ref frame : Style = Style::new().blue();
pub static ref success : Style = Style::new().green().bright();
}
}
#[allow(non_upper_case_globals)]
pub mod text {
use std::collections::HashMap;
use std::io::IsTerminal;
pub fn separator() -> String {
if std::io::stdout().is_terminal() {
format!(" {} ", super::color::frame.apply_to("│"))
} else {
String::from('\t')
}
}
pub struct ColoredTextReplacer {
original: String,
nocolor: String,
replacements: HashMap<usize, char>,
}
impl ColoredTextReplacer {
pub fn new(original: String) -> Self {
let nocolor = console::strip_ansi_codes(&original).to_string();
let replacements = HashMap::new();
Self {
original,
nocolor,
replacements,
}
}
pub fn replace(&mut self, idx: usize, replacement: char) -> &mut Self {
self.replacements.insert(idx, replacement);
self
}
pub fn get(&self) -> String {
let mut iter_nocolor = self.nocolor.chars().enumerate().peekable();
self.original
.chars()
.map(|char_orig| {
let current_nocolor = iter_nocolor.peek().cloned();
match current_nocolor {
Some((idx_strip, char_nocolor)) if char_nocolor == char_orig => {
iter_nocolor.next();
*self.replacements.get(&idx_strip).unwrap_or(&char_orig)
}
_ => char_orig,
}
})
.collect()
}
}
}