diff --git a/Cargo.toml b/Cargo.toml index 49dfa335..e2f857c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,10 @@ keywords = ["cli", "progress", "terminal", "pb"] license = "MIT" [dependencies] -time = "0.1.35" libc = "0.2.9" +time = "0.1.35" winapi = "0.2" kernel32-sys = "0.2" + +[dev-dependencies] +rand = "*" diff --git a/examples/multi.rs b/examples/multi.rs new file mode 100644 index 00000000..2ee7af4a --- /dev/null +++ b/examples/multi.rs @@ -0,0 +1,64 @@ +extern crate rand; +extern crate pbr; +use rand::Rng; +use pbr::MultiBar; +use std::thread; +use std::time::Duration; + +fn main() { + let mut mb = MultiBar::new(); + mb.println("Your Application Header:"); + mb.println(""); + + for i in 1..6 { + let count = 100 * i; + let mut pb = mb.create_bar(count); + pb.tick_format("▏▎▍▌▋▊▉██▉▊▋▌▍▎▏"); + pb.show_message = true; + thread::spawn(move || { + for _ in 0..count / 20 { + for _ in 0..20 { + pb.message("Waiting : "); + thread::sleep(Duration::from_millis(50)); + pb.tick(); + } + for _ in 0..20 { + let n = rand::thread_rng().gen_range(0, 100 * i); + pb.message("Connected: "); + thread::sleep(Duration::from_millis(n)); + pb.inc(); + } + } + for _ in 0..20 { + pb.message("Cleaning :"); + thread::sleep(Duration::from_millis(100)); + pb.tick(); + } + pb.message("Completed! "); + pb.tick(); + pb.finish(); + }); + } + + + mb.println(""); + mb.println("Text lines separate between two sections: "); + mb.println(""); + + for i in 1..4 { + let count = 100 * i; + let mut pb = mb.create_bar(count); + thread::spawn(move || { + for _ in 0..count { + pb.inc(); + let n = rand::thread_rng().gen_range(0, 100 * i); + thread::sleep(Duration::from_millis(n)); + } + pb.finish(); + }); + } + + mb.listen(); + + println!("\nall bars done!\n"); +} diff --git a/src/lib.rs b/src/lib.rs index fa282d7d..01a01662 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,22 +46,44 @@ //! println!("done!"); //! } //! ``` + +// Macro for writing to the giving writer. +// Used in both pb.rs and multi.rs modules. +// +// # Examples +// +// ``` +// let w = io::stdout(); +// printfl!(w, ""); +// printfl!(w, "\r{}", out); +// +// ``` +macro_rules! printfl { + ($w:expr, $($tt:tt)*) => {{ + $w.write(&format!($($tt)*).as_bytes()).ok().expect("write() fail"); + $w.flush().ok().expect("flush() fail"); + }} +} + +#[macro_use] extern crate time; mod tty; mod pb; +mod multi; pub use pb::{ProgressBar, Units}; +pub use multi::MultiBar; use std::io::{Write, Stdout, stdout}; pub struct PbIter -where I: Iterator, - T: Write + where I: Iterator, + T: Write { iter: I, progress_bar: ProgressBar, } impl PbIter -where I: Iterator + where I: Iterator { pub fn new(iter: I) -> Self { Self::on(stdout(), iter) @@ -69,18 +91,21 @@ where I: Iterator } impl PbIter -where I: Iterator, - T: Write + where I: Iterator, + T: Write { pub fn on(handle: T, iter: I) -> Self { let size = iter.size_hint().0; - PbIter {iter: iter, progress_bar: ProgressBar::on(handle, size as u64)} + PbIter { + iter: iter, + progress_bar: ProgressBar::on(handle, size as u64), + } } } impl Iterator for PbIter -where I: Iterator, - T: Write + where I: Iterator, + T: Write { type Item = I::Item; diff --git a/src/multi.rs b/src/multi.rs new file mode 100644 index 00000000..e19f1581 --- /dev/null +++ b/src/multi.rs @@ -0,0 +1,249 @@ +use pb::ProgressBar; +use std::str::from_utf8; +use tty::move_cursor_up; +use std::io::{Stdout, Result, Write}; +use std::sync::mpsc; +use std::sync::mpsc::{Sender, Receiver}; + +pub struct MultiBar { + nlines: usize, + + lines: Vec, + + nbars: usize, + + chan: (Sender, Receiver), + + handle: T, +} + +impl MultiBar { + /// Create a new MultiBar with stdout as a writer. + /// + /// # Examples + /// + /// ```no_run + /// use std::thread; + /// use pbr::MultiBar; + /// + /// let mut mb = MultiBar::new(); + /// mb.println("Application header:"); + /// + /// let mut p1 = mb.create_bar(count); + /// let _ = thread::spawn(move || { + /// for _ in 0..count { + /// p1.inc(); + /// thread::sleep(Duration::from_millis(100)); + /// } + /// // notify the multibar that this bar finished. + /// p1.finish(); + /// }); + /// + /// mb.println("add a separator between the two bars"); + /// + /// let mut p2 = mb.create_bar(count * 2); + /// let _ = thread::spawn(move || { + /// for _ in 0..count * 2 { + /// p2.inc(); + /// thread::sleep(Duration::from_millis(100)); + /// } + /// // notify the multibar that this bar finished. + /// p2.finish(); + /// }); + /// + /// // start listen to all bars changes. + /// // this is a blocking operation, until all bars will finish. + /// // to ignore blocking, you can run it in a different thread. + /// mb.listen(); + /// ``` + pub fn new() -> MultiBar { + MultiBar::on(::std::io::stdout()) + } +} + +impl MultiBar { + /// Create a new MultiBar with an arbitrary writer. + /// + /// # Examples + /// + /// ```no_run + /// use pbr::MultiBar; + /// use std::io::stderr; + /// + /// let mut mb = MultiBar::on(stderr()); + /// // ... + /// // see full example in `MultiBar::new` + /// // ... + /// ``` + pub fn on(handle: T) -> MultiBar { + MultiBar { + nlines: 0, + nbars: 0, + lines: Vec::new(), + chan: mpsc::channel(), + handle: handle, + } + } + + /// println used to add text lines between the bars. + /// for example: you could add a header to your application, + /// or text separators between bars. + /// + /// /// Create a new MultiBar with stdout as a writer. + /// + /// # Examples + /// + /// ```no_run + /// use pbr::MultiBar; + /// + /// let mut mb = MultiBar::new(); + /// mb.println("Application header:"); + /// + /// let mut p1 = MultiBar::create_bar(count); + /// // ... + /// + /// mb.println("Text line between bar1 and bar2"); + /// + /// let mut p2 = MultiBar::create_bar(count); + /// // ... + /// + /// mb.println("Text line between bar2 and bar3"); + /// + /// // ... + /// // ... + /// mb.listen(); + /// ``` + pub fn println(&mut self, s: &str) { + self.lines.push(s.to_owned()); + self.nlines += 1; + } + + /// create_bar creates new `ProgressBar` with `Pipe` as the writer. + /// + /// The ordering of the method calls is important. it means that in + /// the first call, you get a progress bar in level 1, in the 2nd call, + /// you get a progress bar in level 2, and so on. + /// + /// ProgressBar that finish its work, must call `finish()` to + /// notify the MultiBar about it. + /// + /// # Examples + /// + /// ```no_run + /// use pbr::MultiBar; + /// + /// let mut mb = MultiBar::new(); + /// + /// // progress bar in level 1 + /// let mut p1 = MultiBar::create_bar(count1); + /// // ... + /// + /// // progress bar in level 2 + /// let mut p2 = MultiBar::create_bar(count2); + /// // ... + /// + /// // progress bar in level 3 + /// let mut p3 = MultiBar::create_bar(count3); + /// + /// // ... + /// mb.listen(); + /// ``` + pub fn create_bar(&mut self, total: u64) -> ProgressBar { + self.println(""); + self.nbars += 1; + let mut p = ProgressBar::on(Pipe { + level: self.nlines - 1, + chan: self.chan.0.clone(), + }, + total); + p.add(0); + p + } + + + /// listen start listen to all bars changes. + /// + /// ProgressBar that finish its work, must call `finish()` to + /// notify the MultiBar about it. + /// + /// This is a blocking operation and blocks until all bars will + /// finish. + /// To ignore blocking, you can run it in a different thread. + /// + /// # Examples + /// + /// ```no_run + /// use pbr::MultiBar; + /// + /// let mut mb = MultiBar::new(); + /// + /// // ... + /// // create some bars here + /// // ... + /// + /// thread::spawn(move || { + /// mb.listen(); + /// println!("all bars done!"); + /// }); + /// + /// // ... + /// ``` + pub fn listen(&mut self) { + let mut first = true; + let mut nbars = self.nbars; + while nbars > 0 { + + // receive message + let msg = self.chan.1.recv().unwrap(); + if msg.done { + nbars -= 1; + continue; + } + self.lines[msg.level] = msg.string; + + // and draw + let mut out = String::new(); + if !first { + out += &move_cursor_up(self.nlines); + } else { + first = false; + } + for l in self.lines.iter() { + out.push_str(&format!("\r{}\n", l)); + } + printfl!(self.handle, "{}", out); + } + } +} + +pub struct Pipe { + level: usize, + chan: Sender, +} + +impl Write for Pipe { + fn write(&mut self, buf: &[u8]) -> Result { + let s = from_utf8(buf).unwrap().to_owned(); + self.chan + .send(WriteMsg { + // finish method emit empty string + done: s == "", + level: self.level, + string: s, + }) + .unwrap(); + Ok(1) + } + + fn flush(&mut self) -> Result<()> { + Ok(()) + } +} + +// WriteMsg is the message format used to communicate +// between MultiBar and its bars +struct WriteMsg { + done: bool, + level: usize, + string: String, +} diff --git a/src/pb.rs b/src/pb.rs index 0d26cf2e..4bf4b5f6 100644 --- a/src/pb.rs +++ b/src/pb.rs @@ -5,13 +5,6 @@ use time::{self, SteadyTime}; use std::io::Stdout; use tty::{Width, terminal_size}; -macro_rules! printfl { - ($w:expr, $($tt:tt)*) => {{ - $w.write(&format!($($tt)*).as_bytes()).ok().expect("write() fail"); - $w.flush().ok().expect("flush() fail"); - }} -} - macro_rules! kb_fmt { ($n: ident) => {{ let kb = 1024f64; diff --git a/src/tty/mod.rs b/src/tty/mod.rs index 1c4ef310..65470201 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -1,7 +1,7 @@ -//! Most of the code in this module(tty) taken from: +//! Most of the code in for the `terminal_size()` function taken from: //! https://github.com/eminence/terminal-size //! -//! A simple utility for getting the size of a terminal. +//! A simple utility for getting the size of a terminal, and moving `n` lines up. //! //! Supports both Linux and Windows, but help is needed to test other platforms //! @@ -15,9 +15,9 @@ pub struct Height(pub u16); #[cfg(unix)] mod unix; #[cfg(unix)] -pub use self::unix::terminal_size; +pub use self::unix::*; #[cfg(windows)] mod windows; #[cfg(windows)] -pub use self::windows::terminal_size; +pub use self::windows::*; diff --git a/src/tty/unix.rs b/src/tty/unix.rs index 751e8f01..0fdefd16 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -40,6 +40,11 @@ pub fn terminal_size() -> Option<(Width, Height)> { } } +/// Return string that move the cursor `n` lines up. +pub fn move_cursor_up(n: usize) -> String { + format!("\x1B[{}A", n) +} + #[test] /// Compare with the output of `stty size` fn compare_with_stty() { diff --git a/src/tty/windows.rs b/src/tty/windows.rs index c340c325..c7fe29ca 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -8,6 +8,32 @@ use super::{Width, Height}; /// Note that this returns the size of the actual command window, and /// not the overall size of the command window buffer pub fn terminal_size() -> Option<(Width, Height)> { + if let Some((_, csbi)) = get_csbi() { + let w: Width = Width((csbi.srWindow.Right - csbi.srWindow.Left) as u16); + let h: Height = Height((csbi.srWindow.Bottom - csbi.srWindow.Top) as u16); + Some((w, h)) + } else { + None + } +} + +/// move the cursor `n` lines up; return an empty string, just to +/// be aligned with the unix version. +pub fn move_cursor_up(n: usize) -> String { + use self::kernel32::SetConsoleCursorPosition; + use self::winapi::COORD; + if let Some((hand, csbi)) = get_csbi() { + unsafe { + SetConsoleCursorPosition(hand, COORD { + X: 0, + Y: csbi.dwCursorPosition.Y - n as i16, + }); + } + } + "".to_string() +} + +fn get_csbi() -> Option<(self::winapi::HANDLE, self::winapi::CONSOLE_SCREEN_BUFFER_INFO)> { use self::winapi::HANDLE; use self::kernel32::{GetStdHandle, GetConsoleScreenBufferInfo}; use self::winapi::STD_OUTPUT_HANDLE; @@ -28,12 +54,8 @@ pub fn terminal_size() -> Option<(Width, Height)> { }, dwMaximumWindowSize: zc, }; - let success: bool = unsafe { GetConsoleScreenBufferInfo(hand, &mut csbi) != 0 }; - if success { - let w: Width = Width((csbi.srWindow.Right - csbi.srWindow.Left) as u16); - let h: Height = Height((csbi.srWindow.Bottom - csbi.srWindow.Top) as u16); - Some((w, h)) - } else { - None + match unsafe { GetConsoleScreenBufferInfo(hand, &mut csbi) } { + 0 => None, + _ => Some((hand, csbi)), } }