Skip to content

Commit

Permalink
Merge pull request #34 from noamteyssier/convert_stdout_to_stderr
Browse files Browse the repository at this point in the history
print to stderr instead of stdout
  • Loading branch information
FGRibreau authored Aug 4, 2022
2 parents 16ed2c6 + 00a5dd9 commit 8b4d130
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 20 deletions.
65 changes: 45 additions & 20 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
use std::thread::JoinHandle;
use std::time::Instant;
use std::{
io::{stdout, Write},
sync::mpsc::{channel, Sender, TryRecvError},
thread,
time::Duration,
};

pub use crate::utils::spinner_names::SpinnerNames as Spinners;
use crate::utils::spinners_data::SPINNERS as SpinnersMap;
pub use crate::utils::stream::Stream;

mod utils;

pub struct Spinner {
sender: Sender<(Instant, Option<String>)>,
join: Option<JoinHandle<()>>,
stream: Stream
}

impl Drop for Spinner {
Expand Down Expand Up @@ -47,24 +48,57 @@ impl Spinner {
/// let sp = Spinner::new(Spinners::Dots, String::new());
/// ```
pub fn new(spinner: Spinners, message: String) -> Self {
Self::new_inner(spinner, message, None)
Self::new_inner(spinner, message, None, None)
}

/// Create a new spinner that logs the time since it was created
pub fn with_timer(spinner: Spinners, message: String) -> Self {
Self::new_inner(spinner, message, Some(Instant::now()))
Self::new_inner(spinner, message, Some(Instant::now()), None)
}

fn new_inner(spinner: Spinners, message: String, start_time: Option<Instant>) -> Self {
/// Creates a new spinner along with a message with a specified output stream
///
/// # Examples
///
/// Basic Usage:
///
/// ```
/// use spinners::{Spinner, Spinners, Stream};
///
/// let sp = Spinner::with_stream(Spinners::Dots, String::new(), Stream::Stderr);
/// ```
pub fn with_stream(spinner: Spinners, message: String, stream: Stream) -> Self {
Self::new_inner(spinner, message, None, Some(stream))
}

/// Creates a new spinner that logs the time since it was created with a specified output stream
///
/// # Examples
///
/// Basic Usage:
///
/// ```
/// use spinners::{Spinner, Spinners, Stream};
///
/// let sp = Spinner::with_timer_and_stream(Spinners::Dots, String::new(), Stream::Stderr);
/// ```
pub fn with_timer_and_stream(spinner: Spinners, message: String, stream: Stream) -> Self {
Self::new_inner(spinner, message, Some(Instant::now()), Some(stream))
}

fn new_inner(spinner: Spinners, message: String, start_time: Option<Instant>, stream: Option<Stream>) -> Self
{
let spinner_name = spinner.to_string();
let spinner_data = SpinnersMap
.get(&spinner_name)
.unwrap_or_else(|| panic!("No Spinner found with the given name: {}", spinner_name));

let stream = if let Some(stream) = stream { stream } else { Stream::default() };

let (sender, recv) = channel::<(Instant, Option<String>)>();

let join = thread::spawn(move || 'outer: loop {
let mut stdout = stdout();

for frame in spinner_data.frames.iter() {
let (do_stop, stop_time, stop_symbol) = match recv.try_recv() {
Ok((stop_time, stop_symbol)) => (true, Some(stop_time), stop_symbol),
Expand All @@ -73,18 +107,8 @@ impl Spinner {
};

let frame = stop_symbol.unwrap_or_else(|| frame.to_string());
match start_time {
None => {
print!("\r{} {}", frame, message);
}
Some(start_time) => {
let now = stop_time.unwrap_or_else(Instant::now);
let duration = now.duration_since(start_time).as_secs_f64();
print!("\r{}{:>10.3} s\t{}", frame, duration, message);
}
}

stdout.flush().unwrap();
stream.write(&frame, &message, start_time, stop_time).expect("IO Error");

if do_stop {
break 'outer;
Expand All @@ -97,6 +121,7 @@ impl Spinner {
Self {
sender,
join: Some(join),
stream
}
}

Expand Down Expand Up @@ -153,7 +178,7 @@ impl Spinner {
/// ```
pub fn stop_with_symbol(&mut self, symbol: &str) {
self.stop_inner(Instant::now(), Some(symbol.to_owned()));
println!();
self.stream.stop(None, Some(symbol)).expect("IO error");
}

/// Stops the spinner and prints a new line
Expand All @@ -171,7 +196,7 @@ impl Spinner {
/// ```
pub fn stop_with_newline(&mut self) {
self.stop();
println!();
self.stream.stop(None, None).expect("IO error");
}

/// Stops the spinner and prints the provided message
Expand All @@ -189,7 +214,7 @@ impl Spinner {
/// ```
pub fn stop_with_message(&mut self, msg: String) {
self.stop();
println!("\x1b[2K\r{}", msg);
self.stream.stop(Some(&msg), None).expect("IO Error");
}

/// Stops the spinner with a provided symbol and message
Expand All @@ -207,7 +232,7 @@ impl Spinner {
/// ```
pub fn stop_and_persist(&mut self, symbol: &str, msg: String) {
self.stop();
println!("\x1b[2K\r{} {}", symbol, msg);
self.stream.stop(Some(&msg), Some(symbol)).expect("IO Error");
}

fn stop_inner(&mut self, stop_time: Instant, stop_symbol: Option<String>) {
Expand Down
1 change: 1 addition & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod spinner_data;
pub mod spinner_names;
pub mod spinners_data;
pub mod stream;
80 changes: 80 additions & 0 deletions src/utils/stream.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use std::{io::{Write, stdout, stderr, Result}, time::Instant};

/// Handles the Printing logic for the Spinner
#[derive(Default, Copy, Clone)]
pub enum Stream {
#[default]
Stderr,
Stdout,
}
impl Stream {
/// Matches on self and returns the internal writer
fn match_target(&self) -> Box<dyn Write> {
match self {
Self::Stderr => Box::new(stderr()),
Self::Stdout => Box::new(stdout())
}
}

/// Writes the message without duration
fn print_message(
writer: &mut Box<dyn Write>,
frame: &str,
message: &str) -> Result<()>
{
write!(writer, "\r{} {}", frame, message)?;
writer.flush()
}

/// Writes the message with the duration
fn print_message_with_duration(
writer: &mut Box<dyn Write>,
frame: &str,
message: &str,
start_time: Instant,
stop_time: Option<Instant>) -> Result<()>
{
let now = stop_time.unwrap_or_else(Instant::now);
let duration = now.duration_since(start_time).as_secs_f64();
write!(writer, "\r{}{:>10.3} s\t{}", frame, duration, message)?;
writer.flush()
}

/// Writes the current message and optionally prints the durations
pub fn write(
&self,
frame: &str,
message: &str,
start_time: Option<Instant>,
stop_time: Option<Instant>) -> Result<()>
{
let mut writer = self.match_target();
match start_time {
None => Self::print_message(
&mut writer, frame, message)?,
Some(start_time) => Self::print_message_with_duration(
&mut writer, frame, message, start_time, stop_time)?
};
Ok(())
}

/// Handles the stopping logic given an optional message and symbol
pub fn stop(
&self,
message: Option<&str>,
symbol: Option<&str>) -> Result<()>
{
let mut writer = self.match_target();
match (message, symbol) {
// persist with symbol and message
(Some(m), Some(s)) => writeln!(writer, "\x1b[2K\r{} {}", s, m),

// persist with message only
(Some(m), None) => writeln!(writer, "\x1b[2K\r{}", m),

// simple newline
_ => writeln!(writer)
}?;
writer.flush()
}
}

0 comments on commit 8b4d130

Please sign in to comment.