diff --git a/src/common.rs b/src/common.rs index 3991501..8fe1c0d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -48,7 +48,7 @@ pub(crate) use crate::{consts, error}; // traits pub(crate) use crate::{ into_u64::IntoU64, into_usize::IntoUsize, path_ext::PathExt, - platform_interface::PlatformInterface, reckoner::Reckoner, + platform_interface::PlatformInterface, reckoner::Reckoner, step::Step, }; // structs and enums diff --git a/src/consts.rs b/src/consts.rs index cdedfdc..43a5c07 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -25,3 +25,44 @@ pub(crate) const AUTHOR: &str = env!("CARGO_PKG_AUTHORS"); pub(crate) const HELP_MESSAGE: &str = "Print help message."; pub(crate) const VERSION_MESSAGE: &str = "Print version number."; + +/// The pogress chars are from the +/// [Block Elements unicode block](https://en.wikipedia.org/wiki/Block_Elements). +pub(crate) const PROGRESS_CHARS: &str = "█▉▊▋▌▍▎▏ "; + +/// The tick chars are from the +/// [Braille Patterns unicode block](https://en.wikipedia.org/wiki/Braille_Patterns). +/// +/// The chars are ordered to represent the 8 bit numbers in increasing +/// order. The individual braille cells represent bits, with empty cells +/// representing `0` and full cells representing `1`. +/// +/// Digits are ordered from least significant to most significant from +/// top to bottom, and then left to right, like so: +/// +/// ``` +/// ╔═════╗ +/// ║ 0 4 ║ +/// ║ 1 5 ║ +/// ║ 2 6 ║ +/// ║ 3 7 ║ +/// ╚═════╝ +/// ``` +pub(crate) const TICK_CHARS: &str = concat!( + "⠀⠁⠂⠃⠄⠅⠆⠇⡀⡁⡂⡃⡄⡅⡆⡇", // 0b0000---- + "⠈⠉⠊⠋⠌⠍⠎⠏⡈⡉⡊⡋⡌⡍⡎⡏", // 0b0001---- + "⠐⠑⠒⠓⠔⠕⠖⠗⡐⡑⡒⡓⡔⡕⡖⡗", // 0b0010---- + "⠘⠙⠚⠛⠜⠝⠞⠟⡘⡙⡚⡛⡜⡝⡞⡟", // 0b0011---- + "⠠⠡⠢⠣⠤⠥⠦⠧⡠⡡⡢⡣⡤⡥⡦⡧", // 0b0100---- + "⠨⠩⠪⠫⠬⠭⠮⠯⡨⡩⡪⡫⡬⡭⡮⡯", // 0b0101---- + "⠰⠱⠲⠳⠴⠵⠶⠷⡰⡱⡲⡳⡴⡵⡶⡷", // 0b0110---- + "⠸⠹⠺⠻⠼⠽⠾⠿⡸⡹⡺⡻⡼⡽⡾⡿", // 0b0111---- + "⢀⢁⢂⢃⢄⢅⢆⢇⣀⣁⣂⣃⣄⣅⣆⣇", // 0b1000---- + "⢈⢉⢊⢋⢌⢍⢎⢏⣈⣉⣊⣋⣌⣍⣎⣏", // 0b1001---- + "⢐⢑⢒⢓⢔⢕⢖⢗⣐⣑⣒⣓⣔⣕⣖⣗", // 0b1010---- + "⢘⢙⢚⢛⢜⢝⢞⢟⣘⣙⣚⣛⣜⣝⣞⣟", // 0b1011---- + "⢠⢡⢢⢣⢤⢥⢦⢧⣠⣡⣢⣣⣤⣥⣦⣧", // 0b1100---- + "⢨⢩⢪⢫⢬⢭⢮⢯⣨⣩⣪⣫⣬⣭⣮⣯", // 0b1101---- + "⢰⢱⢲⢳⢴⢵⢶⢷⣰⣱⣲⣳⣴⣵⣶⣷", // 0b1110---- + "⢸⢹⢺⢻⢼⢽⢾⢿⣸⣹⣺⣻⣼⣽⣾⣿", // 0b1111---- +); diff --git a/src/info.rs b/src/info.rs index 2ba58a0..e1c6480 100644 --- a/src/info.rs +++ b/src/info.rs @@ -21,3 +21,9 @@ pub(crate) struct Info { #[serde(flatten)] pub(crate) mode: Mode, } + +impl Info { + pub(crate) fn content_size(&self) -> Bytes { + self.mode.content_size() + } +} diff --git a/src/main.rs b/src/main.rs index 5d5c253..9eaa382 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,6 +79,7 @@ mod platform_interface; mod reckoner; mod sha1_digest; mod status; +mod step; mod style; mod subcommand; mod table; diff --git a/src/metainfo.rs b/src/metainfo.rs index b789ade..1b4a75c 100644 --- a/src/metainfo.rs +++ b/src/metainfo.rs @@ -92,8 +92,12 @@ impl Metainfo { } } - pub(crate) fn verify(&self, base: &Path) -> Result { - Verifier::verify(self, base) + pub(crate) fn verify(&self, base: &Path, progress_bar: Option) -> Result { + Verifier::verify(self, base, progress_bar) + } + + pub(crate) fn content_size(&self) -> Bytes { + self.info.content_size() } } diff --git a/src/mode.rs b/src/mode.rs index 5d26877..f58083d 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -18,7 +18,7 @@ pub(crate) enum Mode { } impl Mode { - pub(crate) fn total_size(&self) -> Bytes { + pub(crate) fn content_size(&self) -> Bytes { match self { Self::Single { length, .. } => *length, Self::Multiple { files } => files.iter().map(|file| file.length).sum(), diff --git a/src/step.rs b/src/step.rs new file mode 100644 index 0000000..798eaca --- /dev/null +++ b/src/step.rs @@ -0,0 +1,34 @@ +use crate::common::*; + +pub(crate) trait Step { + fn print(&self, env: &mut Env) -> Result<(), Error> { + let style = env.err_style(); + let dim = style.dim(); + let message = style.message(); + + err!( + env, + "{}[{}/{}]{} ", + dim.prefix(), + self.n(), + Self::total(), + dim.suffix() + )?; + + err!(env, "{}{} ", message.prefix(), self.symbol())?; + + self.write_message(&mut env.err).context(error::Stderr)?; + + errln!(env, "{}", message.suffix())?; + + Ok(()) + } + + fn n(&self) -> usize; + + fn total() -> usize; + + fn write_message(&self, write: &mut dyn Write) -> io::Result<()>; + + fn symbol(&self) -> &str; +} diff --git a/src/subcommand/torrent/create.rs b/src/subcommand/torrent/create.rs index e6cab19..0c29f6f 100644 --- a/src/subcommand/torrent/create.rs +++ b/src/subcommand/torrent/create.rs @@ -1,4 +1,7 @@ use crate::common::*; +use create_step::CreateStep; + +mod create_step; #[derive(StructOpt)] #[structopt( @@ -203,12 +206,12 @@ impl Create { announce_list.push(tier); } - Step::Searching.print(env)?; + CreateStep::Searching.print(env)?; let spinner = if env.err_is_term() { let style = ProgressStyle::default_spinner() .template("{spinner:.green} {msg:.bold}…") - .tick_chars(&Self::tick_chars()); + .tick_chars(consts::TICK_CHARS); Some(ProgressBar::new_spinner().with_style(style)) } else { @@ -295,7 +298,7 @@ impl Create { Some(String::from(consts::CREATED_BY_DEFAULT)) }; - Step::Hashing.print(env)?; + CreateStep::Hashing.print(env)?; let progress_bar = if env.err_is_term() { let style = ProgressStyle::default_bar() @@ -303,8 +306,8 @@ impl Create { "{spinner:.green} ⟪{elapsed_precise}⟫ ⟦{bar:40.cyan}⟧ \ {binary_bytes}/{binary_total_bytes} ⟨{binary_bytes_per_sec}, {eta}⟩", ) - .tick_chars(&Self::tick_chars()) - .progress_chars("█▉▊▋▌▍▎▏ "); + .tick_chars(consts::TICK_CHARS) + .progress_chars(consts::PROGRESS_CHARS); Some(ProgressBar::new(files.total_size().count()).with_style(style)) } else { @@ -318,7 +321,7 @@ impl Create { progress_bar, )?; - Step::Writing { output: &output }.print(env)?; + CreateStep::Writing { output: &output }.print(env)?; let info = Info { source: self.source, @@ -374,7 +377,7 @@ impl Create { assert_eq!(deserialized, metainfo); - let status = metainfo.verify(&input)?; + let status = metainfo.verify(&input, None)?; if !status.good() { return Err(Error::Verify { status }); @@ -395,82 +398,6 @@ impl Create { Ok(()) } - - fn tick_chars() -> &'static str { - // The tick chars are from the Braille Patterns unicode block: - // https://en.wikipedia.org/wiki/Braille_Patterns - // - // The chars are ordered to represent the 8 bit numbers in increasing - // order. The individual braille cells represent bits, with empty cells - // representing `0` and full cells representing `1`. - // - // Digits are ordered from least significant to most significant from - // top to bottom, and then left to right, like so: - // - // ``` - // ╔═════╗ - // ║ 0 4 ║ - // ║ 1 5 ║ - // ║ 2 6 ║ - // ║ 3 7 ║ - // ╚═════╝ - // ``` - concat!( - "⠀⠁⠂⠃⠄⠅⠆⠇⡀⡁⡂⡃⡄⡅⡆⡇", // 0b0000---- - "⠈⠉⠊⠋⠌⠍⠎⠏⡈⡉⡊⡋⡌⡍⡎⡏", // 0b0001---- - "⠐⠑⠒⠓⠔⠕⠖⠗⡐⡑⡒⡓⡔⡕⡖⡗", // 0b0010---- - "⠘⠙⠚⠛⠜⠝⠞⠟⡘⡙⡚⡛⡜⡝⡞⡟", // 0b0011---- - "⠠⠡⠢⠣⠤⠥⠦⠧⡠⡡⡢⡣⡤⡥⡦⡧", // 0b0100---- - "⠨⠩⠪⠫⠬⠭⠮⠯⡨⡩⡪⡫⡬⡭⡮⡯", // 0b0101---- - "⠰⠱⠲⠳⠴⠵⠶⠷⡰⡱⡲⡳⡴⡵⡶⡷", // 0b0110---- - "⠸⠹⠺⠻⠼⠽⠾⠿⡸⡹⡺⡻⡼⡽⡾⡿", // 0b0111---- - "⢀⢁⢂⢃⢄⢅⢆⢇⣀⣁⣂⣃⣄⣅⣆⣇", // 0b1000---- - "⢈⢉⢊⢋⢌⢍⢎⢏⣈⣉⣊⣋⣌⣍⣎⣏", // 0b1001---- - "⢐⢑⢒⢓⢔⢕⢖⢗⣐⣑⣒⣓⣔⣕⣖⣗", // 0b1010---- - "⢘⢙⢚⢛⢜⢝⢞⢟⣘⣙⣚⣛⣜⣝⣞⣟", // 0b1011---- - "⢠⢡⢢⢣⢤⢥⢦⢧⣠⣡⣢⣣⣤⣥⣦⣧", // 0b1100---- - "⢨⢩⢪⢫⢬⢭⢮⢯⣨⣩⣪⣫⣬⣭⣮⣯", // 0b1101---- - "⢰⢱⢲⢳⢴⢵⢶⢷⣰⣱⣲⣳⣴⣵⣶⣷", // 0b1110---- - "⢸⢹⢺⢻⢼⢽⢾⢿⣸⣹⣺⣻⣼⣽⣾⣿", // 0b1111---- - ) - } -} - -#[derive(Clone, Copy)] -enum Step<'output> { - Searching, - Hashing, - Writing { output: &'output OutputTarget }, -} - -impl<'output> Step<'output> { - fn print(self, env: &mut Env) -> Result<(), Error> { - let style = env.err_style(); - let dim = style.dim(); - let message = style.message(); - - err!(env, "{}[{}/3]{} ", dim.prefix(), self.n(), dim.suffix())?; - - err!(env, "{}", message.prefix())?; - - match self { - Self::Searching => err!(env, "\u{1F9FF} Searching for files…")?, - Self::Hashing => err!(env, "\u{1F9EE} Hashing pieces…")?, - Self::Writing { output } => err!(env, "\u{1F4BE} Writing metainfo to {}…", output)?, - } - - errln!(env, "{}", message.suffix())?; - - Ok(()) - } - - fn n(self) -> usize { - match self { - Self::Searching => 1, - Self::Hashing => 2, - Self::Writing { .. } => 3, - } - } } #[cfg(test)] diff --git a/src/subcommand/torrent/create/create_step.rs b/src/subcommand/torrent/create/create_step.rs new file mode 100644 index 0000000..002c5d4 --- /dev/null +++ b/src/subcommand/torrent/create/create_step.rs @@ -0,0 +1,38 @@ +use crate::common::*; + +#[derive(Clone, Copy)] +pub(crate) enum CreateStep<'output> { + Searching, + Hashing, + Writing { output: &'output OutputTarget }, +} + +impl<'output> Step for CreateStep<'output> { + fn n(&self) -> usize { + match self { + Self::Searching => 1, + Self::Hashing => 2, + Self::Writing { .. } => 3, + } + } + + fn symbol(&self) -> &str { + match self { + Self::Searching => "\u{1F9FF}", + Self::Hashing => "\u{1F9EE}", + Self::Writing { .. } => "\u{1F4BE}", + } + } + + fn total() -> usize { + 3 + } + + fn write_message(&self, write: &mut dyn Write) -> io::Result<()> { + match self { + Self::Searching => write!(write, "Searching for files…"), + Self::Hashing => write!(write, "Hashing pieces…"), + Self::Writing { output } => write!(write, "Writing metainfo to {}…", output), + } + } +} diff --git a/src/subcommand/torrent/verify.rs b/src/subcommand/torrent/verify.rs index 6f08e75..16ac090 100644 --- a/src/subcommand/torrent/verify.rs +++ b/src/subcommand/torrent/verify.rs @@ -1,4 +1,7 @@ use crate::common::*; +use verify_step::VerifyStep; + +mod verify_step; #[derive(StructOpt)] #[structopt( @@ -31,16 +34,40 @@ impl Verify { let metainfo_path = env.resolve(&self.metainfo); let metainfo = Metainfo::load(&metainfo_path)?; + VerifyStep::Loading { + metainfo: &metainfo_path, + } + .print(env)?; + let base = if let Some(content) = &self.content { env.resolve(content) } else { metainfo_path.parent().unwrap().join(&metainfo.info.name) }; - let status = metainfo.verify(&base)?; + let progress_bar = if env.err_is_term() { + let style = ProgressStyle::default_bar() + .template( + "{spinner:.green} ⟪{elapsed_precise}⟫ ⟦{bar:40.cyan}⟧ \ + {binary_bytes}/{binary_total_bytes} ⟨{binary_bytes_per_sec}, {eta}⟩", + ) + .tick_chars(consts::TICK_CHARS) + .progress_chars(consts::PROGRESS_CHARS); + + Some(ProgressBar::new(metainfo.content_size().count()).with_style(style)) + } else { + None + }; + + VerifyStep::Verifying { content: &base }.print(env)?; + + let status = metainfo.verify(&base, progress_bar)?; if status.good() { - errln!(env, "Verification succeeded.")?; + errln!( + env, + "\u{2728}\u{2728} Verification succeeded! \u{2728}\u{2728}" + )?; Ok(()) } else { Err(Error::Verify { status }) @@ -90,14 +117,22 @@ mod tests { "torrent", "verify", "--input", - torrent, + &torrent, ], tree: {}, }; assert_matches!(verify_env.run(), Ok(())); - assert_eq!(verify_env.err(), "Verification succeeded.\n"); + let want = format!( + "[1/2] \u{1F4BE} Loading metainfo from `{}`…\n[2/2] \u{1F9EE} Verifying pieces from \ + `{}`…\n\u{2728}\u{2728} Verification succeeded! \u{2728}\u{2728}\n", + torrent.display(), + create_env.resolve("foo").display() + ); + + assert_eq!(verify_env.err(), want); + assert_eq!(verify_env.out(), ""); Ok(()) } @@ -133,14 +168,21 @@ mod tests { "torrent", "verify", "--input", - torrent, + &torrent, ], tree: {}, }; assert_matches!(verify_env.run(), Err(Error::Verify { .. })); - assert_eq!(verify_env.err(), ""); + let want = format!( + "[1/2] \u{1F4BE} Loading metainfo from `{}`…\n[2/2] \u{1F9EE} Verifying pieces from `{}`…\n", + torrent.display(), + create_env.resolve("foo").display() + ); + + assert_eq!(verify_env.err(), want); + assert_eq!(verify_env.out(), ""); Ok(()) } @@ -180,16 +222,24 @@ mod tests { "torrent", "verify", "--input", - torrent, + &torrent, "--content", - bar, + &bar, ], tree: {}, }; assert_matches!(verify_env.run(), Ok(())); - assert_eq!(verify_env.err(), "Verification succeeded.\n"); + let want = format!( + "[1/2] \u{1F4BE} Loading metainfo from `{}`…\n[2/2] \u{1F9EE} Verifying pieces from \ + `{}`…\n\u{2728}\u{2728} Verification succeeded! \u{2728}\u{2728}\n", + torrent.display(), + bar.display(), + ); + + assert_eq!(verify_env.err(), want); + assert_eq!(verify_env.out(), ""); Ok(()) } diff --git a/src/subcommand/torrent/verify/verify_step.rs b/src/subcommand/torrent/verify/verify_step.rs new file mode 100644 index 0000000..6f57052 --- /dev/null +++ b/src/subcommand/torrent/verify/verify_step.rs @@ -0,0 +1,38 @@ +use crate::common::*; + +#[derive(Clone, Copy)] +pub(crate) enum VerifyStep<'a> { + Loading { metainfo: &'a Path }, + Verifying { content: &'a Path }, +} + +impl<'a> Step for VerifyStep<'a> { + fn n(&self) -> usize { + match self { + Self::Loading { .. } => 1, + Self::Verifying { .. } => 2, + } + } + + fn symbol(&self) -> &str { + match self { + Self::Loading { .. } => "\u{1F4BE}", + Self::Verifying { .. } => "\u{1F9EE}", + } + } + + fn total() -> usize { + 2 + } + + fn write_message(&self, write: &mut dyn Write) -> io::Result<()> { + match self { + Self::Loading { metainfo } => { + write!(write, "Loading metainfo from `{}`…", metainfo.display()) + } + Self::Verifying { content } => { + write!(write, "Verifying pieces from `{}`…", content.display()) + } + } + } +} diff --git a/src/torrent_summary.rs b/src/torrent_summary.rs index 1088d89..7d0dafc 100644 --- a/src/torrent_summary.rs +++ b/src/torrent_summary.rs @@ -95,7 +95,7 @@ impl TorrentSummary { table.size("Torrent Size", self.size); - table.size("Content Size", self.metainfo.info.mode.total_size()); + table.size("Content Size", self.metainfo.content_size()); table.row( "Private", diff --git a/src/verifier.rs b/src/verifier.rs index 5f724f8..8559a79 100644 --- a/src/verifier.rs +++ b/src/verifier.rs @@ -8,10 +8,15 @@ pub(crate) struct Verifier<'a> { pieces: PieceList, sha1: Sha1, piece_bytes_hashed: usize, + progress_bar: Option, } impl<'a> Verifier<'a> { - fn new(metainfo: &'a Metainfo, base: &'a Path) -> Result> { + fn new( + metainfo: &'a Metainfo, + base: &'a Path, + progress_bar: Option, + ) -> Result> { let piece_length = metainfo.info.piece_length.as_piece_length()?.into_usize(); Ok(Verifier { @@ -22,11 +27,16 @@ impl<'a> Verifier<'a> { base, metainfo, piece_length, + progress_bar, }) } - pub(crate) fn verify(metainfo: &'a Metainfo, base: &'a Path) -> Result { - Self::new(metainfo, base)?.verify_metainfo() + pub(crate) fn verify( + metainfo: &'a Metainfo, + base: &'a Path, + progress_bar: Option, + ) -> Result { + Self::new(metainfo, base, progress_bar)?.verify_metainfo() } fn verify_metainfo(mut self) -> Result { @@ -76,6 +86,10 @@ impl<'a> Verifier<'a> { } remaining -= buffer.len().into_u64(); + + if let Some(progress_bar) = &self.progress_bar { + progress_bar.inc(to_buffer.into_u64()); + } } Ok(()) @@ -110,7 +124,7 @@ mod tests { let metainfo = env.load_metainfo("foo.torrent"); - assert!(metainfo.verify(&env.resolve("foo"))?.good()); + assert!(metainfo.verify(&env.resolve("foo"), None)?.good()); Ok(()) } @@ -141,7 +155,7 @@ mod tests { let metainfo = env.load_metainfo("foo.torrent"); - let status = metainfo.verify(&env.resolve("foo"))?; + let status = metainfo.verify(&env.resolve("foo"), None)?; assert!(status.files().iter().all(FileStatus::good));