use std::io::{self, BufRead, Write};
use std::process::{Command as ProcessCommand, Stdio};
use std::time::Instant;
use crate::error::{ClickError, Result};
#[macro_export]
macro_rules! echo {
($msg:expr) => {
$crate::termui::echo($msg, true, false, None)
};
($fmt:expr, $($arg:tt)*) => {{
echo!(@parse $fmt, $($arg)*)
}};
(@parse $fmt:expr, nl = $nl:expr) => {
$crate::termui::echo($fmt, $nl, false, None)
};
(@parse $fmt:expr, err = $err:expr) => {
$crate::termui::echo($fmt, true, $err, None)
};
(@parse $fmt:expr, nl = $nl:expr, err = $err:expr) => {
$crate::termui::echo($fmt, $nl, $err, None)
};
(@parse $fmt:expr, err = $err:expr, nl = $nl:expr) => {
$crate::termui::echo($fmt, $nl, $err, None)
};
(@parse $fmt:expr, color = $color:expr) => {
$crate::termui::echo($fmt, true, false, $color)
};
(@parse $fmt:expr, nl = $nl:expr, color = $color:expr) => {
$crate::termui::echo($fmt, $nl, false, $color)
};
(@parse $fmt:expr, err = $err:expr, color = $color:expr) => {
$crate::termui::echo($fmt, true, $err, $color)
};
(@parse $fmt:expr, nl = $nl:expr, err = $err:expr, color = $color:expr) => {
$crate::termui::echo($fmt, $nl, $err, $color)
};
(@parse $fmt:expr, $($arg:tt)*) => {
$crate::termui::echo(&format!($fmt, $($arg)*), true, false, None)
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
Reset,
}
impl Color {
pub fn fg_code(self) -> u8 {
match self {
Color::Black => 30,
Color::Red => 31,
Color::Green => 32,
Color::Yellow => 33,
Color::Blue => 34,
Color::Magenta => 35,
Color::Cyan => 36,
Color::White => 37,
Color::BrightBlack => 90,
Color::BrightRed => 91,
Color::BrightGreen => 92,
Color::BrightYellow => 93,
Color::BrightBlue => 94,
Color::BrightMagenta => 95,
Color::BrightCyan => 96,
Color::BrightWhite => 97,
Color::Reset => 39,
}
}
pub fn bg_code(self) -> u8 {
match self {
Color::Black => 40,
Color::Red => 41,
Color::Green => 42,
Color::Yellow => 43,
Color::Blue => 44,
Color::Magenta => 45,
Color::Cyan => 46,
Color::White => 47,
Color::BrightBlack => 100,
Color::BrightRed => 101,
Color::BrightGreen => 102,
Color::BrightYellow => 103,
Color::BrightBlue => 104,
Color::BrightMagenta => 105,
Color::BrightCyan => 106,
Color::BrightWhite => 107,
Color::Reset => 49,
}
}
}
pub const BLACK: Color = Color::Black;
pub const RED: Color = Color::Red;
pub const GREEN: Color = Color::Green;
pub const YELLOW: Color = Color::Yellow;
pub const BLUE: Color = Color::Blue;
pub const MAGENTA: Color = Color::Magenta;
pub const CYAN: Color = Color::Cyan;
pub const WHITE: Color = Color::White;
pub const BRIGHT_BLACK: Color = Color::BrightBlack;
pub const BRIGHT_RED: Color = Color::BrightRed;
pub const BRIGHT_GREEN: Color = Color::BrightGreen;
pub const BRIGHT_YELLOW: Color = Color::BrightYellow;
pub const BRIGHT_BLUE: Color = Color::BrightBlue;
pub const BRIGHT_MAGENTA: Color = Color::BrightMagenta;
pub const BRIGHT_CYAN: Color = Color::BrightCyan;
pub const BRIGHT_WHITE: Color = Color::BrightWhite;
pub const RESET: Color = Color::Reset;
pub fn isatty(stream: &str) -> bool {
use std::io::IsTerminal;
match stream {
"stdin" => std::io::stdin().is_terminal(),
"stdout" => std::io::stdout().is_terminal(),
"stderr" => std::io::stderr().is_terminal(),
_ => false,
}
}
pub fn stdout_isatty() -> bool {
isatty("stdout")
}
pub fn stderr_isatty() -> bool {
isatty("stderr")
}
pub fn stdin_isatty() -> bool {
isatty("stdin")
}
pub fn get_terminal_size() -> (usize, usize) {
if let (Ok(cols), Ok(rows)) = (std::env::var("COLUMNS"), std::env::var("LINES")) {
if let (Ok(w), Ok(h)) = (cols.parse::<usize>(), rows.parse::<usize>()) {
if w > 0 && h > 0 {
return (w, h);
}
}
}
match crossterm::terminal::size() {
Ok((cols, rows)) if cols > 0 && rows > 0 => (cols as usize, rows as usize),
_ => (80, 24),
}
}
pub fn clear() {
if stdout_isatty() {
print!("\x1b[2J\x1b[H");
let _ = io::stdout().flush();
}
}
fn should_use_color(color: Option<bool>, err: bool) -> bool {
match color {
Some(true) => true,
Some(false) => false,
None => {
if err {
stderr_isatty()
} else {
stdout_isatty()
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn style(
text: &str,
fg: Option<Color>,
bg: Option<Color>,
bold: bool,
dim: bool,
underline: bool,
overline: bool,
italic: bool,
blink: bool,
strikethrough: bool,
reset: bool,
) -> String {
let mut codes = Vec::new();
if bold {
codes.push("1".to_string());
}
if dim {
codes.push("2".to_string());
}
if italic {
codes.push("3".to_string());
}
if underline {
codes.push("4".to_string());
}
if blink {
codes.push("5".to_string());
}
if overline {
codes.push("53".to_string()); }
if strikethrough {
codes.push("9".to_string());
}
if let Some(color) = fg {
codes.push(color.fg_code().to_string());
}
if let Some(color) = bg {
codes.push(color.bg_code().to_string());
}
if codes.is_empty() {
return text.to_string();
}
let style_start = format!("\x1b[{}m", codes.join(";"));
let style_end = if reset { "\x1b[0m" } else { "" };
format!("{}{}{}", style_start, text, style_end)
}
pub fn echo(message: &str, nl: bool, err: bool, color: Option<bool>) {
let output = if should_use_color(color, err) {
message.to_string()
} else {
strip_ansi_codes(message)
};
if err {
if nl {
eprintln!("{}", output);
let _ = io::stderr().flush();
} else {
eprint!("{}", output);
let _ = io::stderr().flush();
}
} else if nl {
println!("{}", output);
let _ = io::stdout().flush();
} else {
print!("{}", output);
let _ = io::stdout().flush();
}
}
#[allow(clippy::too_many_arguments)]
pub fn secho(
message: &str,
fg: Option<Color>,
bg: Option<Color>,
bold: bool,
dim: bool,
underline: bool,
overline: bool,
italic: bool,
blink: bool,
strikethrough: bool,
reset: bool,
nl: bool,
err: bool,
color: Option<bool>,
) {
let styled = if should_use_color(color, err) {
style(
message,
fg,
bg,
bold,
dim,
underline,
overline,
italic,
blink,
strikethrough,
reset,
)
} else {
message.to_string()
};
if err {
if nl {
eprintln!("{}", styled);
} else {
eprint!("{}", styled);
let _ = io::stderr().flush();
}
} else if nl {
println!("{}", styled);
} else {
print!("{}", styled);
let _ = io::stdout().flush();
}
}
pub fn echo_via_pager(text: &str, color: Option<bool>) {
if !stdin_isatty() || !stdout_isatty() {
echo(text, true, false, color);
return;
}
let pager = std::env::var("PAGER")
.ok()
.filter(|p| !p.is_empty())
.unwrap_or_else(|| {
if which_pager("less").is_some() {
"less".to_string()
} else if which_pager("more").is_some() {
"more".to_string()
} else {
String::new()
}
});
if pager.is_empty() {
echo(text, true, false, color);
return;
}
let output_text = if color == Some(false) {
strip_ansi_codes(text)
} else {
text.to_string()
};
let mut parts = pager.split_whitespace();
let cmd_name = match parts.next() {
Some(name) => name,
None => {
echo(&output_text, true, false, color);
return;
}
};
let mut cmd = ProcessCommand::new(cmd_name);
for arg in parts {
cmd.arg(arg);
}
if cmd_name == "less" && color != Some(false) {
cmd.arg("-R");
}
match cmd.stdin(Stdio::piped()).spawn() {
Ok(mut child) => {
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(output_text.as_bytes());
}
let _ = child.wait();
}
Err(_) => {
echo(&output_text, true, false, color);
}
}
}
fn which_pager(name: &str) -> Option<String> {
if let Ok(path) = std::env::var("PATH") {
for dir in path.split(':') {
let full_path = std::path::Path::new(dir).join(name);
if full_path.exists() {
return Some(full_path.to_string_lossy().into_owned());
}
}
}
None
}
pub fn launch(url: &str, wait: bool, locate: bool) -> Result<()> {
let (cmd, args) = get_launch_command(url, locate)?;
let mut command = ProcessCommand::new(&cmd);
command.args(&args);
if wait {
let status = command.status().map_err(|e| {
ClickError::usage(format!("Failed to launch '{}': {}", url, e))
})?;
if !status.success() {
return Err(ClickError::usage(format!(
"Launch command failed with exit code: {:?}",
status.code()
)));
}
} else {
command.spawn().map_err(|e| {
ClickError::usage(format!("Failed to launch '{}': {}", url, e))
})?;
}
Ok(())
}
fn get_launch_command(url: &str, locate: bool) -> Result<(String, Vec<String>)> {
#[cfg(target_os = "macos")]
{
if locate {
Ok(("open".to_string(), vec!["-R".to_string(), url.to_string()]))
} else {
Ok(("open".to_string(), vec![url.to_string()]))
}
}
#[cfg(target_os = "linux")]
{
if locate {
let path = std::path::Path::new(url);
if let Some(parent) = path.parent() {
Ok((
"xdg-open".to_string(),
vec![parent.to_string_lossy().into_owned()],
))
} else {
Err(ClickError::usage(format!(
"Cannot locate file: {}",
url
)))
}
} else {
Ok(("xdg-open".to_string(), vec![url.to_string()]))
}
}
#[cfg(target_os = "windows")]
{
if locate {
Ok((
"explorer".to_string(),
vec!["/select,".to_string() + url],
))
} else {
Ok((
"cmd".to_string(),
vec!["/c".to_string(), "start".to_string(), "".to_string(), url.to_string()],
))
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
let _ = locate; Err(ClickError::usage(format!(
"Platform not supported for launch: {}",
url
)))
}
}
pub fn strip_ansi_codes(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' {
if chars.peek() == Some(&'[') {
chars.next(); while let Some(&next) = chars.peek() {
chars.next();
if next.is_ascii_alphabetic() {
break;
}
}
}
} else {
result.push(c);
}
}
result
}
pub fn prompt<T, F>(
text: &str,
default: Option<T>,
hide_input: bool,
confirmation: bool,
type_converter: F,
) -> Result<T>
where
T: Clone + std::fmt::Display,
F: Fn(&str) -> std::result::Result<T, String>,
{
loop {
let prompt_text = if let Some(ref def) = default {
format!("{} [{}]: ", text, def)
} else {
format!("{}: ", text)
};
let input = if hide_input {
read_hidden_input(&prompt_text)?
} else {
read_line(&prompt_text)?
};
let value = if input.is_empty() {
if let Some(def) = default.clone() {
return Ok(def);
} else {
echo("Error: This field is required.", true, true, None);
continue;
}
} else {
input
};
let converted = match type_converter(&value) {
Ok(v) => v,
Err(msg) => {
echo(&format!("Error: {}", msg), true, true, None);
continue;
}
};
if confirmation {
let confirm_prompt = "Repeat for confirmation: ".to_string();
let confirm_input = if hide_input {
read_hidden_input(&confirm_prompt)?
} else {
read_line(&confirm_prompt)?
};
if confirm_input != value {
echo(
"Error: The two entered values do not match.",
true,
true,
None,
);
continue;
}
}
return Ok(converted);
}
}
pub fn confirm(text: &str, default: Option<bool>, abort: bool) -> Result<bool> {
let suffix = match default {
Some(true) => " [Y/n]: ",
Some(false) => " [y/N]: ",
None => " [y/n]: ",
};
loop {
let prompt_text = format!("{}{}", text, suffix);
let input = read_line(&prompt_text)?;
let input_lower = input.to_lowercase();
let result = if input.is_empty() {
default
} else if input_lower == "y" || input_lower == "yes" {
Some(true)
} else if input_lower == "n" || input_lower == "no" {
Some(false)
} else {
echo("Error: invalid input", true, true, None);
continue;
};
match result {
Some(true) => return Ok(true),
Some(false) => {
if abort {
return Err(ClickError::Abort);
}
return Ok(false);
}
None => {
echo("Error: invalid input", true, true, None);
continue;
}
}
}
}
pub fn getchar(echo_char: bool) -> Result<char> {
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
use crossterm::terminal;
if terminal::enable_raw_mode().is_ok() {
let result = loop {
match event::read() {
Ok(Event::Key(KeyEvent {
code, modifiers, ..
})) => {
if modifiers.contains(KeyModifiers::CONTROL) {
if let KeyCode::Char('c') = code {
break Err(ClickError::Abort);
}
}
match code {
KeyCode::Char(c) => break Ok(c),
KeyCode::Enter => break Ok('\n'),
KeyCode::Backspace => break Ok('\x7f'),
KeyCode::Tab => break Ok('\t'),
KeyCode::Esc => break Ok('\x1b'),
_ => continue,
}
}
Ok(_) => continue,
Err(e) => {
break Err(ClickError::usage(format!("Failed to read key: {}", e)))
}
}
};
let _ = terminal::disable_raw_mode();
if let Ok(c) = &result {
if echo_char {
print!("{}", c);
let _ = io::stdout().flush();
}
}
return result;
}
let input = read_line("")?;
input.chars().next().ok_or(ClickError::Abort)
}
pub fn pause(info: Option<&str>) {
let message = info.unwrap_or("Press any key to continue...");
echo(message, false, false, None);
let _ = getchar(false);
println!();
}
fn read_line(prompt: &str) -> Result<String> {
if !prompt.is_empty() {
print!("{}", prompt);
let _ = io::stdout().flush();
}
let stdin = io::stdin();
let mut line = String::new();
stdin
.lock()
.read_line(&mut line)
.map_err(|e| ClickError::usage(format!("Failed to read input: {}", e)))?;
if line.ends_with('\n') {
line.pop();
if line.ends_with('\r') {
line.pop();
}
}
Ok(line)
}
fn read_hidden_input(prompt: &str) -> Result<String> {
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
use crossterm::terminal;
if !prompt.is_empty() {
print!("{}", prompt);
let _ = io::stdout().flush();
}
if terminal::enable_raw_mode().is_ok() {
let mut input = String::new();
let result = loop {
match event::read() {
Ok(Event::Key(KeyEvent {
code, modifiers, ..
})) => {
if modifiers.contains(KeyModifiers::CONTROL) {
if let KeyCode::Char('c') = code {
break Err(ClickError::Abort);
}
}
match code {
KeyCode::Enter => break Ok(input.clone()),
KeyCode::Char(c) => input.push(c),
KeyCode::Backspace => {
input.pop();
}
_ => {}
}
}
Ok(_) => continue,
Err(_) => break Ok(input.clone()),
}
};
let _ = terminal::disable_raw_mode();
println!();
return result;
}
echo("(Warning: Input will be visible)", true, true, None);
read_line("")
}
pub struct ProgressBar {
length: usize,
position: usize,
label: Option<String>,
show_eta: bool,
show_percent: bool,
show_pos: bool,
width: usize,
start_time: Instant,
finished: bool,
is_tty: bool,
#[allow(dead_code)]
last_output_len: usize,
fill_char: char,
empty_char: char,
}
impl ProgressBar {
pub fn new(
length: usize,
label: Option<&str>,
show_eta: bool,
show_percent: bool,
show_pos: bool,
width: usize,
) -> Self {
let bar = Self {
length,
position: 0,
label: label.map(String::from),
show_eta,
show_percent,
show_pos,
width,
start_time: Instant::now(),
finished: false,
is_tty: stdout_isatty(),
last_output_len: 0,
fill_char: '#',
empty_char: '-',
};
bar.render_internal();
bar
}
pub fn fill_char(mut self, c: char) -> Self {
self.fill_char = c;
self
}
pub fn empty_char(mut self, c: char) -> Self {
self.empty_char = c;
self
}
pub fn update(&mut self, n: usize) {
if self.finished {
return;
}
self.position = (self.position + n).min(self.length);
self.render_internal();
}
pub fn set_position(&mut self, pos: usize) {
if self.finished {
return;
}
self.position = pos.min(self.length);
self.render_internal();
}
pub fn finish(&mut self) {
if self.finished {
return;
}
self.position = self.length;
self.finished = true;
self.render_internal();
if self.is_tty {
println!();
}
}
pub fn render(&self) -> String {
let mut parts = Vec::new();
if let Some(ref label) = self.label {
parts.push(label.clone());
}
let progress = if self.length > 0 {
self.position as f64 / self.length as f64
} else {
0.0
};
let filled = (progress * self.width as f64) as usize;
let empty = self.width.saturating_sub(filled);
let bar = format!(
"[{}{}]",
self.fill_char.to_string().repeat(filled),
self.empty_char.to_string().repeat(empty)
);
parts.push(bar);
if self.show_percent {
parts.push(format!("{:3.0}%", progress * 100.0));
}
if self.show_pos {
parts.push(format!("{}/{}", self.position, self.length));
}
if self.show_eta && self.position > 0 && !self.finished {
let elapsed = self.start_time.elapsed();
let rate = self.position as f64 / elapsed.as_secs_f64();
let remaining = self.length - self.position;
let eta_secs = if rate > 0.0 {
remaining as f64 / rate
} else {
0.0
};
if eta_secs < 3600.0 {
let mins = (eta_secs / 60.0) as u64;
let secs = (eta_secs % 60.0) as u64;
parts.push(format!("eta {:02}:{:02}", mins, secs));
} else {
let hours = (eta_secs / 3600.0) as u64;
let mins = ((eta_secs % 3600.0) / 60.0) as u64;
parts.push(format!("eta {}h {:02}m", hours, mins));
}
}
parts.join(" ")
}
fn render_internal(&self) {
let output = self.render();
if self.is_tty {
print!("\r{}", output);
let clear_len = self.last_output_len.saturating_sub(output.len());
if clear_len > 0 {
print!("{}", " ".repeat(clear_len));
print!("\r{}", output);
}
let _ = io::stdout().flush();
} else {
println!("{}", output);
}
}
}
impl Drop for ProgressBar {
fn drop(&mut self) {
if !self.finished && self.is_tty {
println!();
}
}
}
pub fn progressbar<I>(
iter: I,
length: Option<usize>,
label: Option<&str>,
) -> ProgressBarIter<I::IntoIter>
where
I: IntoIterator,
I::IntoIter: ExactSizeIterator,
{
let iter = iter.into_iter();
let len = length.unwrap_or_else(|| iter.len());
ProgressBarIter {
iter,
bar: ProgressBar::new(len, label, true, true, true, 30),
}
}
pub struct ProgressBarIter<I> {
iter: I,
bar: ProgressBar,
}
impl<I> Iterator for ProgressBarIter<I>
where
I: Iterator,
{
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
match self.iter.next() {
Some(item) => {
self.bar.update(1);
Some(item)
}
None => {
self.bar.finish();
None
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.iter.size_hint()
}
}
impl<I: ExactSizeIterator> ExactSizeIterator for ProgressBarIter<I> {
fn len(&self) -> usize {
self.iter.len()
}
}
pub fn edit_text(
text: Option<&str>,
editor: Option<&str>,
extension: &str,
require_save: bool,
) -> Result<Option<String>> {
use std::fs;
use std::process::Command;
let temp_dir = std::env::temp_dir();
let temp_file = temp_dir.join(format!("click_edit_{}.{}", std::process::id(), extension));
if let Some(initial) = text {
fs::write(&temp_file, initial)
.map_err(|e| ClickError::file_error(&temp_file, e.to_string()))?;
}
let mtime_before = fs::metadata(&temp_file)
.ok()
.and_then(|m| m.modified().ok());
let editor_cmd = editor
.map(String::from)
.or_else(|| std::env::var("VISUAL").ok())
.or_else(|| std::env::var("EDITOR").ok())
.unwrap_or_else(|| "vi".to_string());
let status = Command::new(&editor_cmd)
.arg(&temp_file)
.status()
.map_err(|e| ClickError::usage(format!("Failed to run editor '{}': {}", editor_cmd, e)))?;
if !status.success() {
let _ = fs::remove_file(&temp_file);
return Err(ClickError::usage(format!(
"Editor '{}' exited with error",
editor_cmd
)));
}
if require_save {
let mtime_after = fs::metadata(&temp_file)
.ok()
.and_then(|m| m.modified().ok());
if mtime_before == mtime_after {
let _ = fs::remove_file(&temp_file);
return Ok(None);
}
}
let content = fs::read_to_string(&temp_file)
.map_err(|e| ClickError::file_error(&temp_file, e.to_string()))?;
let _ = fs::remove_file(&temp_file);
Ok(Some(content))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_codes() {
assert_eq!(Color::Red.fg_code(), 31);
assert_eq!(Color::Red.bg_code(), 41);
assert_eq!(Color::BrightGreen.fg_code(), 92);
assert_eq!(Color::BrightGreen.bg_code(), 102);
assert_eq!(Color::Reset.fg_code(), 39);
assert_eq!(Color::Reset.bg_code(), 49);
}
#[test]
fn test_style_basic() {
let styled = style(
"hello", None, None, false, false, false, false, false, false, false, false,
);
assert_eq!(styled, "hello");
}
#[test]
fn test_style_with_color() {
let styled = style(
"hello",
Some(Color::Red),
None,
false,
false,
false,
false,
false,
false,
false,
true,
);
assert_eq!(styled, "\x1b[31mhello\x1b[0m");
}
#[test]
fn test_style_bold() {
let styled = style(
"hello", None, None, true, false, false, false, false, false, false, true,
);
assert_eq!(styled, "\x1b[1mhello\x1b[0m");
}
#[test]
fn test_style_multiple() {
let styled = style(
"hello",
Some(Color::Green),
Some(Color::Black),
true,
false,
true,
false,
false,
false,
false,
true,
);
assert!(styled.starts_with("\x1b["));
assert!(styled.contains("1"));
assert!(styled.contains("4"));
assert!(styled.contains("32"));
assert!(styled.contains("40"));
assert!(styled.ends_with("\x1b[0m"));
}
#[test]
fn test_strip_ansi_codes() {
let styled = "\x1b[31mhello\x1b[0m world";
let stripped = strip_ansi_codes(styled);
assert_eq!(stripped, "hello world");
let plain = "no codes here";
assert_eq!(strip_ansi_codes(plain), "no codes here");
}
#[test]
fn test_strip_ansi_codes_complex() {
let styled = "\x1b[1;31;40mcomplex\x1b[0m";
let stripped = strip_ansi_codes(styled);
assert_eq!(stripped, "complex");
}
#[test]
fn test_get_terminal_size_returns_valid() {
let (width, height) = get_terminal_size();
assert!(width > 0);
assert!(height > 0);
}
#[test]
fn test_progress_bar_render() {
let bar = ProgressBar::new(100, Some("Test"), false, true, true, 20);
let output = bar.render();
assert!(output.contains("Test"));
assert!(output.contains("["));
assert!(output.contains("]"));
assert!(output.contains("0/100"));
assert!(output.contains("0%"));
}
#[test]
fn test_progress_bar_update() {
let mut bar = ProgressBar::new(100, None, false, true, false, 10);
bar.update(50);
let output = bar.render();
assert!(output.contains("50%"));
}
#[test]
fn test_progress_bar_finish() {
let mut bar = ProgressBar::new(100, None, false, true, false, 10);
bar.finish();
let output = bar.render();
assert!(output.contains("100%"));
assert!(bar.finished);
}
#[test]
fn test_progress_bar_zero_length() {
let bar = ProgressBar::new(0, None, false, true, false, 10);
let output = bar.render();
assert!(output.contains("0%"));
}
#[test]
fn test_progress_bar_custom_chars() {
let mut bar = ProgressBar::new(100, None, false, false, false, 10)
.fill_char('=')
.empty_char(' ');
bar.set_position(50);
let output = bar.render();
assert!(output.contains("[===== ]"));
}
#[test]
fn test_progress_bar_unicode_chars() {
let bar = ProgressBar::new(100, None, false, false, false, 4)
.fill_char('\u{2588}') .empty_char('\u{2591}'); let output = bar.render();
assert!(output.contains("[\u{2591}\u{2591}\u{2591}\u{2591}]"));
}
#[test]
fn test_color_constants() {
assert_eq!(BLACK, Color::Black);
assert_eq!(RED, Color::Red);
assert_eq!(GREEN, Color::Green);
assert_eq!(YELLOW, Color::Yellow);
assert_eq!(BLUE, Color::Blue);
assert_eq!(MAGENTA, Color::Magenta);
assert_eq!(CYAN, Color::Cyan);
assert_eq!(WHITE, Color::White);
assert_eq!(BRIGHT_BLACK, Color::BrightBlack);
assert_eq!(BRIGHT_RED, Color::BrightRed);
assert_eq!(BRIGHT_GREEN, Color::BrightGreen);
assert_eq!(BRIGHT_YELLOW, Color::BrightYellow);
assert_eq!(BRIGHT_BLUE, Color::BrightBlue);
assert_eq!(BRIGHT_MAGENTA, Color::BrightMagenta);
assert_eq!(BRIGHT_CYAN, Color::BrightCyan);
assert_eq!(BRIGHT_WHITE, Color::BrightWhite);
assert_eq!(RESET, Color::Reset);
}
#[test]
fn test_style_all_options() {
let styled = style(
"test",
Some(Color::Blue),
Some(Color::White),
true, true, true, true, true, true, true, true, );
assert!(styled.starts_with("\x1b["));
assert!(styled.contains("1")); assert!(styled.contains("2")); assert!(styled.contains("3")); assert!(styled.contains("4")); assert!(styled.contains("5")); assert!(styled.contains("9")); assert!(styled.contains("53")); assert!(styled.contains("34")); assert!(styled.contains("47")); assert!(styled.ends_with("\x1b[0m"));
}
#[test]
fn test_style_no_reset() {
let styled = style(
"hello",
Some(Color::Red),
None,
false,
false,
false,
false,
false,
false,
false,
false,
);
assert!(styled.starts_with("\x1b[31m"));
assert!(!styled.ends_with("\x1b[0m"));
}
}