use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use rusty_bubbles::progress;
use rusty_bubbletea::commands;
use rusty_bubbletea::model::Model as ModelTrait;
use rusty_bubbletea::screen::WindowSizeMsg;
use rusty_bubbletea::{batch, quit, Cmd, KeyPressMsg, Msg, Program, View};
use rusty_lipgloss::{Color, Style};
fn help_style() -> Style {
Style::new().foreground_color(Color::parse("#626262"))
}
const PADDING: usize = 2;
const MAX_WIDTH: usize = 80;
#[derive(Debug, Clone, Copy)]
struct ProgressMsg(f64);
#[derive(Debug, Clone)]
struct ProgressErrMsg(String);
fn final_pause() -> Cmd {
commands::tick(Duration::from_millis(750), |_t| None)
}
fn wait_for_progress(rx: Arc<Mutex<mpsc::Receiver<Result<f64, String>>>>) -> Cmd {
Some(Box::new(move || match rx.lock().unwrap().recv() {
Ok(Ok(p)) => Some(Box::new(ProgressMsg(p))),
Ok(Err(e)) => Some(Box::new(ProgressErrMsg(e))),
Err(_) => Some(Box::new(ProgressErrMsg("download failed".to_string()))),
}))
}
struct ProgressWriter {
total: usize,
downloaded: usize,
on_progress: Option<Box<dyn Fn(f64) + Send>>,
on_error: Option<Box<dyn Fn(String) + Send>>,
}
impl ProgressWriter {
fn new(total: usize, tx: mpsc::Sender<Result<f64, String>>) -> Self {
ProgressWriter {
total,
downloaded: 0,
on_progress: Some(Box::new({
let tx = tx.clone();
move |ratio: f64| {
let _ = tx.send(Ok(ratio));
}
})),
on_error: Some(Box::new(move |err: String| {
let _ = tx.send(Err(err));
})),
}
}
fn start(&mut self, file: &mut std::fs::File, reader: &mut dyn Read) {
let mut buf = [0u8; 8192];
loop {
let n = match reader.read(&mut buf) {
Ok(0) => break,
Ok(n) => n,
Err(e) => {
if let Some(cb) = &self.on_error {
cb(e.to_string());
}
return;
}
};
if let Err(e) = file.write_all(&buf[..n]) {
if let Some(cb) = &self.on_error {
cb(e.to_string());
}
return;
}
self.downloaded += n;
if self.total > 0 {
if let Some(cb) = &self.on_progress {
cb(self.downloaded as f64 / self.total as f64);
}
}
}
}
}
struct Response {
content_length: usize,
body: Box<dyn Read + Send>,
}
fn get_response(url: &str) -> Result<Response, Box<dyn std::error::Error>> {
let host_port = url
.strip_prefix("http://")
.unwrap_or(url)
.split('/')
.next()
.unwrap_or(url);
let path = url
.strip_prefix("http://")
.and_then(|u| u.find('/').map(|i| &u[i..]))
.unwrap_or("/");
let mut stream = TcpStream::connect(host_port)?;
let request = format!(
"GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\nUser-Agent: rusty-bubbletea-progress-download\r\n\r\n",
path, host_port
);
stream.write_all(request.as_bytes())?;
stream.flush()?;
let mut headers = String::new();
let mut buf = [0u8; 1];
loop {
let n = stream.read(&mut buf)?;
if n == 0 {
return Err("could not read response headers".into());
}
headers.push(buf[0] as char);
if headers.ends_with("\r\n\r\n") {
break;
}
}
let mut lines = headers.split("\r\n");
let status_line = lines.next().unwrap_or("");
let status = status_line
.split(' ')
.nth(1)
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(0);
if status != 200 {
return Err(format!("receiving status of {} for url: {}", status, url).into());
}
let mut content_length: usize = 0;
for line in lines {
let lower = line.to_lowercase();
if let Some(v) = lower.strip_prefix("content-length:") {
content_length = v.trim().parse().unwrap_or(0);
}
}
Ok(Response {
content_length,
body: Box::new(stream),
})
}
struct Model {
#[allow(dead_code)]
pw: Arc<Mutex<ProgressWriter>>,
rx: Arc<Mutex<mpsc::Receiver<Result<f64, String>>>>,
progress: progress::Model,
err: Option<String>,
}
impl ModelTrait for Model {
fn init(&self) -> Cmd {
wait_for_progress(self.rx.clone())
}
fn update(&mut self, msg: &dyn Msg) -> Cmd {
if msg.as_any().downcast_ref::<KeyPressMsg>().is_some() {
return quit();
}
if let Some(w) = msg.as_any().downcast_ref::<WindowSizeMsg>() {
let width = w.width.saturating_sub(PADDING * 2 + 4);
self.progress.set_width(width);
if self.progress.width() > MAX_WIDTH {
self.progress.set_width(MAX_WIDTH);
}
return None;
}
if let Some(e) = msg.as_any().downcast_ref::<ProgressErrMsg>() {
self.err = Some(e.0.clone());
return quit();
}
if let Some(pm) = msg.as_any().downcast_ref::<ProgressMsg>() {
let mut cmds: Vec<Cmd> = Vec::new();
if pm.0 >= 1.0 {
cmds.push(commands::sequence(vec![final_pause(), quit()]));
}
cmds.push(self.progress.set_percent(pm.0));
if pm.0 < 1.0 {
cmds.push(wait_for_progress(self.rx.clone()));
}
return batch(cmds);
}
if msg.as_any().downcast_ref::<progress::FrameMsg>().is_some() {
return self.progress.update(msg);
}
None
}
fn view(&self) -> View {
if let Some(err) = &self.err {
return View::new(&format!("Error downloading: {}\n", err));
}
let pad = " ".repeat(PADDING);
View::new(&format!(
"\n{}{}\n\n{}{}",
pad,
self.progress.view(),
pad,
help_style().render("Press any key to quit")
))
}
}
fn usage() {
println!("Usage: progress_download --url <url>");
println!(" -url string");
println!(" url for the file to download");
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
let mut url = String::new();
let mut i = 1;
while i < args.len() {
if (args[i] == "--url" || args[i] == "-url") && i + 1 < args.len() {
url = args[i + 1].clone();
i += 1;
}
i += 1;
}
if url.is_empty() {
usage();
std::process::exit(1);
}
let resp = match get_response(&url) {
Ok(r) => r,
Err(e) => {
println!("could not get response: {}", e);
std::process::exit(1);
}
};
if resp.content_length == 0 {
println!("can't parse content length, aborting download");
std::process::exit(1);
}
let filename = url.rsplit('/').next().unwrap_or(&url).to_string();
let mut file = match std::fs::File::create(&filename) {
Ok(f) => f,
Err(e) => {
println!("could not create file: {}", e);
std::process::exit(1);
}
};
let (tx, rx) = mpsc::channel::<Result<f64, String>>();
let pw = Arc::new(Mutex::new(ProgressWriter::new(resp.content_length, tx)));
let m = Model {
pw: pw.clone(),
rx: Arc::new(Mutex::new(rx)),
progress: progress::new(vec![progress::with_default_blend()]),
err: None,
};
let pw_thread = pw.clone();
let mut body = resp.body;
std::thread::spawn(move || {
pw_thread.lock().unwrap().start(&mut file, &mut body);
});
let p = Program::new(m);
p.run()?;
Ok(())
}