Progress bar Implementation (#7661)

# Description

_(Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.)_

I implemented the status bar we talk about yesterday. The idea was
inspired by the progress bar of `wget`.
I decided to go for the second suggestion by `@Reilly`
> 2. add an Option<usize> or whatever to RawStream (and ListStream?) for
situations where you do know the length ahead of time

For now only works with the command `save` but after the approve of this
PR we can see how we can implement it on commands like `cp` and `mv`

When using `fetch` nushell will check if there is any `content-length`
attribute in the request header. If so, then `fetch` will send it
through the new `Option` variable in the `RawStream` to the `save`.
If we know the total size we show the progress bar 

![nu_pb01](https://user-images.githubusercontent.com/38369407/210298647-07ee55ea-e751-41b1-a84d-f72ec1f6e9e5.jpg)
but if we don't then we just show the stats like: data already saved,
bytes per second, and time lapse.

![nu_pb02](https://user-images.githubusercontent.com/38369407/210298698-1ef65f51-40cc-4481-83de-309cbd1049cb.jpg)

![nu_pb03](https://user-images.githubusercontent.com/38369407/210298701-eef2ef13-9206-4a98-8202-e4fe5531d79d.jpg)

Please let me know If I need to make any changes and I will be happy to
do it.

# User-Facing Changes

A new flag (`--progress` `-p`) was added to the `save` command 
Examples:
```nu
fetch https://github.com/torvalds/linux/archive/refs/heads/master.zip | save --progress -f main.zip
fetch https://releases.ubuntu.com/22.04.1/ubuntu-22.04.1-desktop-amd64.iso | save --progress -f main.zip
open main.zip --raw | save --progress main.copy
```

# Tests + Formatting

Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A
clippy::needless_collect` to check that you're using the standard code
style
- `cargo test --workspace` to check that all tests pass
-
I am getting some errors and its weird because the errors are showing up
in files i haven't touch. Is this normal?

# After Submitting

If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.

Co-authored-by: Reilly Wood <reilly.wood@icloud.com>
This commit is contained in:
Xoffio 2023-01-10 20:57:48 -05:00 committed by GitHub
parent 9a274128ce
commit 82ac590412
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 188 additions and 7 deletions

25
Cargo.lock generated
View file

@ -1785,6 +1785,18 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "indicatif"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4295cbb7573c16d310e99e713cf9e75101eb190ab31fccd35f2d2691b4352b19"
dependencies = [
"console",
"number_prefix",
"portable-atomic",
"unicode-width",
]
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.7.1" version = "0.7.1"
@ -2650,6 +2662,7 @@ dependencies = [
"htmlescape", "htmlescape",
"ical", "ical",
"indexmap", "indexmap",
"indicatif",
"is-root", "is-root",
"itertools", "itertools",
"libc", "libc",
@ -3111,6 +3124,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]] [[package]]
name = "objc" name = "objc"
version = "0.2.7" version = "0.2.7"
@ -3708,6 +3727,12 @@ dependencies = [
"nom", "nom",
] ]
[[package]]
name = "portable-atomic"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b"
[[package]] [[package]]
name = "powierza-coefficient" name = "powierza-coefficient"
version = "1.0.2" version = "1.0.2"

View file

@ -51,6 +51,7 @@ fs_extra = "1.2.0"
htmlescape = "0.3.1" htmlescape = "0.3.1"
ical = "0.7.0" ical = "0.7.0"
indexmap = { version="1.7", features=["serde-1"] } indexmap = { version="1.7", features=["serde-1"] }
indicatif = "0.17.2"
Inflector = "0.11" Inflector = "0.11"
is-root = "0.1.2" is-root = "0.1.2"
itertools = "0.10.0" itertools = "0.10.0"

View file

@ -139,6 +139,7 @@ impl Command for Open {
Box::new(BufferedReader { input: buf_reader }), Box::new(BufferedReader { input: buf_reader }),
ctrlc, ctrlc,
call_span, call_span,
None,
)), )),
stderr: None, stderr: None,
exit_code: None, exit_code: None,

View file

@ -9,6 +9,8 @@ use std::fs::File;
use std::io::{BufWriter, Write}; use std::io::{BufWriter, Write};
use std::path::Path; use std::path::Path;
use crate::progress_bar;
#[derive(Clone)] #[derive(Clone)]
pub struct Save; pub struct Save;
@ -47,6 +49,7 @@ impl Command for Save {
.switch("raw", "save file as raw binary", Some('r')) .switch("raw", "save file as raw binary", Some('r'))
.switch("append", "append input to the end of the file", Some('a')) .switch("append", "append input to the end of the file", Some('a'))
.switch("force", "overwrite the destination", Some('f')) .switch("force", "overwrite the destination", Some('f'))
.switch("progress", "enable progress bar", Some('p'))
.category(Category::FileSystem) .category(Category::FileSystem)
} }
@ -60,6 +63,7 @@ impl Command for Save {
let raw = call.has_flag("raw"); let raw = call.has_flag("raw");
let append = call.has_flag("append"); let append = call.has_flag("append");
let force = call.has_flag("force"); let force = call.has_flag("force");
let progress = call.has_flag("progress");
let span = call.head; let span = call.head;
@ -81,16 +85,16 @@ impl Command for Save {
// delegate a thread to redirect stderr to result. // delegate a thread to redirect stderr to result.
let handler = stderr.map(|stderr_stream| match stderr_file { let handler = stderr.map(|stderr_stream| match stderr_file {
Some(stderr_file) => { Some(stderr_file) => std::thread::spawn(move || {
std::thread::spawn(move || stream_to_file(stderr_stream, stderr_file, span)) stream_to_file(stderr_stream, stderr_file, span, progress)
} }),
None => std::thread::spawn(move || { None => std::thread::spawn(move || {
let _ = stderr_stream.into_bytes(); let _ = stderr_stream.into_bytes();
Ok(PipelineData::empty()) Ok(PipelineData::empty())
}), }),
}); });
let res = stream_to_file(stream, file, span); let res = stream_to_file(stream, file, span, progress);
if let Some(h) = handler { if let Some(h) = handler {
h.join().map_err(|err| { h.join().map_err(|err| {
ShellError::ExternalCommand( ShellError::ExternalCommand(
@ -332,10 +336,29 @@ fn stream_to_file(
mut stream: RawStream, mut stream: RawStream,
file: File, file: File,
span: Span, span: Span,
progress: bool,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let mut writer = BufWriter::new(file); let mut writer = BufWriter::new(file);
stream let mut bytes_processed: u64 = 0;
let bytes_processed_p = &mut bytes_processed;
let file_total_size = stream.known_size;
let mut process_failed = false;
let process_failed_p = &mut process_failed;
// Create the progress bar
// It looks a bit messy but I am doing it this way to avoid
// creating the bar when is not needed
let (mut bar_opt, bar_opt_clone) = if progress {
let tmp_bar = progress_bar::NuProgressBar::new(file_total_size);
let tmp_bar_clone = tmp_bar.clone();
(Some(tmp_bar), Some(tmp_bar_clone))
} else {
(None, None)
};
let result = stream
.try_for_each(move |result| { .try_for_each(move |result| {
let buf = match result { let buf = match result {
Ok(v) => match v { Ok(v) => match v {
@ -353,13 +376,39 @@ fn stream_to_file(
)); ));
} }
}, },
Err(err) => return Err(err), Err(err) => {
*process_failed_p = true;
return Err(err);
}
}; };
// If the `progress` flag is set then
if progress {
// Update the total amount of bytes that has been saved and then print the progress bar
*bytes_processed_p += buf.len() as u64;
if let Some(bar) = &mut bar_opt {
bar.update_bar(*bytes_processed_p);
}
}
if let Err(err) = writer.write(&buf) { if let Err(err) = writer.write(&buf) {
*process_failed_p = true;
return Err(ShellError::IOError(err.to_string())); return Err(ShellError::IOError(err.to_string()));
} }
Ok(()) Ok(())
}) })
.map(|_| PipelineData::empty()) .map(|_| PipelineData::empty());
// If the `progress` flag is set then
if progress {
// If the process failed, stop the progress bar with an error message.
if process_failed {
if let Some(bar) = bar_opt_clone {
bar.abandoned_msg("# Error while saving #".to_owned());
}
}
}
// And finally return the stream result.
result
} }

View file

@ -50,6 +50,7 @@ impl Command for ToText {
}), }),
engine_state.ctrlc.clone(), engine_state.ctrlc.clone(),
span, span,
None,
)), )),
stderr: None, stderr: None,
exit_code: None, exit_code: None,

View file

@ -20,6 +20,7 @@ mod misc;
mod network; mod network;
mod path; mod path;
mod platform; mod platform;
mod progress_bar;
mod random; mod random;
mod shells; mod shells;
mod sort_utils; mod sort_utils;

View file

@ -361,6 +361,26 @@ fn response_to_buffer(
engine_state: &EngineState, engine_state: &EngineState,
span: Span, span: Span,
) -> nu_protocol::PipelineData { ) -> nu_protocol::PipelineData {
// Try to get the size of the file to be downloaded.
// This is helpful to show the progress of the stream.
let buffer_size = match &response.headers().get("content-length") {
Some(content_length) => {
let content_length = &(*content_length).clone(); // binding
let content_length = content_length
.to_str()
.unwrap_or("")
.parse::<u64>()
.unwrap_or(0);
if content_length == 0 {
None
} else {
Some(content_length)
}
}
_ => None,
};
let buffered_input = BufReader::new(response); let buffered_input = BufReader::new(response);
PipelineData::ExternalStream { PipelineData::ExternalStream {
@ -370,6 +390,7 @@ fn response_to_buffer(
}), }),
engine_state.ctrlc.clone(), engine_state.ctrlc.clone(),
span, span,
buffer_size,
)), )),
stderr: None, stderr: None,
exit_code: None, exit_code: None,

View file

@ -415,6 +415,7 @@ fn response_to_buffer(
}), }),
engine_state.ctrlc.clone(), engine_state.ctrlc.clone(),
span, span,
None,
)), )),
stderr: None, stderr: None,
exit_code: None, exit_code: None,

View file

@ -0,0 +1,71 @@
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
use std::fmt;
// This module includes the progress bar used to show the progress when using the command `save`
// Eventually it would be nice to find a better place for it.
pub struct NuProgressBar {
pub pb: ProgressBar,
bytes_processed: u64,
total_bytes: Option<u64>,
}
impl NuProgressBar {
pub fn new(total_bytes: Option<u64>) -> NuProgressBar {
// Let's create the progress bar template.
let template = match total_bytes {
Some(_) => {
// We will use a progress bar if we know the total bytes of the stream
ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} {binary_bytes_per_sec} ({eta}) {msg}")
}
_ => {
// But if we don't know the total then we just show the stats progress
ProgressStyle::with_template(
"{spinner:.green} [{elapsed_precise}] {bytes} {binary_bytes_per_sec} {msg}",
)
}
};
let total_bytes = match total_bytes {
Some(total_size) => total_size,
_ => 0,
};
let new_progress_bar = ProgressBar::new(total_bytes);
new_progress_bar.set_style(
template
.unwrap_or_else(|_| ProgressStyle::default_bar())
.with_key("eta", |state: &ProgressState, w: &mut dyn fmt::Write| {
let _ = fmt::write(w, format_args!("{:.1}s", state.eta().as_secs_f64()));
})
.progress_chars("#>-"),
);
NuProgressBar {
pb: new_progress_bar,
total_bytes: None,
bytes_processed: 0,
}
}
pub fn update_bar(&mut self, bytes_processed: u64) {
self.pb.set_position(bytes_processed);
}
// Commenting this for now but adding it in the future
//pub fn finished_msg(&self, msg: String) {
// self.pb.finish_with_message(msg);
//}
pub fn abandoned_msg(&self, msg: String) {
self.pb.abandon_with_message(msg);
}
pub fn clone(&self) -> NuProgressBar {
NuProgressBar {
pb: self.pb.clone(),
bytes_processed: self.bytes_processed,
total_bytes: self.total_bytes,
}
}
}

View file

@ -476,6 +476,7 @@ impl ExternalCommand {
Box::new(stdout_receiver), Box::new(stdout_receiver),
output_ctrlc.clone(), output_ctrlc.clone(),
head, head,
None,
)) ))
} else { } else {
None None
@ -485,6 +486,7 @@ impl ExternalCommand {
Box::new(stderr_receiver), Box::new(stderr_receiver),
output_ctrlc.clone(), output_ctrlc.clone(),
head, head,
None,
)) ))
} else { } else {
None None

View file

@ -249,6 +249,7 @@ fn handle_table_command(
), ),
ctrlc, ctrlc,
call.head, call.head,
None,
)), )),
stderr: None, stderr: None,
exit_code: None, exit_code: None,
@ -732,6 +733,7 @@ fn handle_row_stream(
}), }),
ctrlc, ctrlc,
head, head,
None,
)), )),
stderr: None, stderr: None,
exit_code: None, exit_code: None,

View file

@ -566,6 +566,7 @@ impl PipelineData {
Box::new(vec![Ok(stderr_bytes)].into_iter()), Box::new(vec![Ok(stderr_bytes)].into_iter()),
stderr_ctrlc, stderr_ctrlc,
stderr_span, stderr_span,
None,
) )
}); });

View file

@ -10,6 +10,7 @@ pub struct RawStream {
pub ctrlc: Option<Arc<AtomicBool>>, pub ctrlc: Option<Arc<AtomicBool>>,
pub is_binary: bool, pub is_binary: bool,
pub span: Span, pub span: Span,
pub known_size: Option<u64>, // (bytes)
} }
impl RawStream { impl RawStream {
@ -17,6 +18,7 @@ impl RawStream {
stream: Box<dyn Iterator<Item = Result<Vec<u8>, ShellError>> + Send + 'static>, stream: Box<dyn Iterator<Item = Result<Vec<u8>, ShellError>> + Send + 'static>,
ctrlc: Option<Arc<AtomicBool>>, ctrlc: Option<Arc<AtomicBool>>,
span: Span, span: Span,
known_size: Option<u64>,
) -> Self { ) -> Self {
Self { Self {
stream, stream,
@ -24,6 +26,7 @@ impl RawStream {
ctrlc, ctrlc,
is_binary: false, is_binary: false,
span, span,
known_size,
} }
} }
@ -62,6 +65,7 @@ impl RawStream {
ctrlc: self.ctrlc, ctrlc: self.ctrlc,
is_binary: self.is_binary, is_binary: self.is_binary,
span: self.span, span: self.span,
known_size: self.known_size,
} }
} }
} }

View file

@ -316,6 +316,7 @@ fn main() -> Result<()> {
Box::new(BufferedReader::new(buf_reader)), Box::new(BufferedReader::new(buf_reader)),
Some(ctrlc), Some(ctrlc),
redirect_stdin.span, redirect_stdin.span,
None,
)), )),
stderr: None, stderr: None,
exit_code: None, exit_code: None,