use crate::error::Error;
use anyhow::{Context, Result};
use console::Term;
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::Client;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::{Output, Stdio};
use std::time::Duration;
use tokio::process::Command;
use tokio::time::timeout;
use tracing::{debug, info};
pub mod process_utils {
use super::*;
pub async fn execute_command_with_output(
program: &str,
args: &[&str],
working_dir: Option<&Path>,
timeout_secs: u64,
) -> Result<Output> {
debug!("Executing command: {} {}", program, args.join(" "));
if program.is_empty() {
anyhow::bail!("Program name cannot be empty");
}
let mut cmd = Command::new(program);
cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
if let Some(dir) = working_dir {
cmd.current_dir(dir);
}
let output = timeout(Duration::from_secs(timeout_secs), async {
cmd.output().await
})
.await
.with_context(|| format!("Command '{}' timed out", program))?
.with_context(|| format!("Failed to execute command: {}", program))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("{} {} failed: {}", program, args.join(" "), stderr);
}
debug!("Command executed successfully");
Ok(output)
}
pub fn execute_interactive(
program: &str,
args: &[&str],
working_dir: Option<&Path>,
) -> Result<()> {
debug!("Executing interactively: {} {}", program, args.join(" "));
let mut cmd = std::process::Command::new(program);
cmd.args(args);
if let Some(dir) = working_dir {
cmd.current_dir(dir);
}
let status = cmd
.status()
.with_context(|| format!("Failed to execute command: {}", program))?;
if !status.success() {
anyhow::bail!("Command '{} {}' failed", program, args.join(" "));
}
Ok(())
}
pub fn command_exists(command: &str) -> bool {
std::process::Command::new(command)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}
pub fn find_python() -> Option<String> {
let candidates = vec!["python", "python3", "py"];
for candidate in candidates {
if command_exists(candidate) {
if let Ok(output) = std::process::Command::new(candidate)
.arg("--version")
.output()
{
let version = String::from_utf8_lossy(&output.stdout);
if version.contains("Python 3") {
return Some(candidate.to_string());
}
}
}
}
None
}
pub async fn get_python_version(python_executable: &str) -> Result<String> {
let output =
execute_command_with_output(python_executable, &["--version"], None, 10).await?;
let version = String::from_utf8_lossy(&output.stdout);
Ok(version.trim_start_matches("Python").trim().to_string())
}
}
pub mod net_utils {
use super::*;
pub async fn download_file(url: &str, destination: &Path, show_progress: bool) -> Result<()> {
let client = Client::new();
let response = client
.get(url)
.send()
.await
.with_context(|| format!("Failed to send request to {}", url))?;
if !response.status().is_success() {
anyhow::bail!("Failed to download {}: HTTP {}", url, response.status());
}
let total_size = response.content_length().unwrap_or(0);
let pb = if show_progress && total_size > 0 {
let pb = ProgressBar::new(total_size);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
.context("Failed to create progress bar template")?
.progress_chars("█▉▊▋▌▍▎▏ "),
);
pb.set_message(format!("Downloading {}", url));
Some(pb)
} else {
None
};
let mut file = fs::File::create(destination)
.with_context(|| format!("Failed to create file: {:?}", destination))?;
let mut downloaded = 0u64;
let mut stream = response.bytes_stream();
use futures_util::StreamExt;
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(Error::Network)?;
file.write_all(&chunk)
.with_context(|| format!("Failed to write to file: {:?}", destination))?;
downloaded += chunk.len() as u64;
if let Some(ref pb) = pb {
pb.set_position(downloaded);
}
}
if let Some(pb) = pb {
pb.finish_with_message("Download completed");
}
info!("Downloaded {} ({} bytes)", url, downloaded);
Ok(())
}
pub async fn check_url_accessible(url: &str) -> bool {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()
.unwrap_or_default();
match client.head(url).send().await {
Ok(response) => response.status().is_success(),
Err(_) => false,
}
}
}
pub mod terminal_utils {
use super::*;
pub fn get_terminal_size() -> (usize, usize) {
let term = Term::stdout();
term.size_checked()
.map(|(rows, cols)| (rows as usize, cols as usize))
.unwrap_or((24, 80))
}
pub fn is_tty() -> bool {
Term::stdout().is_term()
}
pub fn clear_screen() -> Result<()> {
Term::stdout()
.clear_screen()
.context("Failed to clear screen")?;
Ok(())
}
pub fn create_progress_bar(len: u64, message: impl Into<String>) -> ProgressBar {
let pb = ProgressBar::new(len);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} {msg}")
.expect("Failed to create progress bar template")
.progress_chars("█▉▊▋▌▍▎▏ "),
);
pb.set_message(message.into());
pb
}
pub fn create_spinner(message: impl Into<String>) -> ProgressBar {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::with_template("{spinner:.green.bold} {msg:.green.bold}")
.expect("Failed to create spinner template"),
);
pb.set_message(message.into());
pb.enable_steady_tick(Duration::from_millis(100));
pb
}
pub fn spinner_with_message<T>(message: &str, f: impl FnOnce() -> T) -> T {
let pb = create_spinner(message);
let result = f();
pb.finish_and_clear();
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_download_file() {
let url =
"https://github.com/fllesser/nbr/releases/latest/download/nbr-Linux-musl-x86_64.tar.gz";
let temp_dir = tempfile::tempdir().unwrap();
let destination = temp_dir.path().join("nbr.tar.gz");
let show_progress = true;
let result = net_utils::download_file(url, &destination, show_progress).await;
temp_dir.close().unwrap();
assert!(result.is_ok());
}
}