Skip to main content

click/
termui.rs

1//! Terminal UI utilities for click-rs.
2//!
3//! This module provides terminal input/output functions for building interactive
4//! command-line applications. It includes styled output, user prompts, progress bars,
5//! and terminal utilities.
6//!
7//! # Features
8//!
9//! - **Output Functions**: `echo`, `secho`, and `style` for formatted terminal output
10//! - **Input Functions**: `prompt`, `confirm`, `getchar`, and `pause` for user input
11//! - **Progress Bars**: Visual progress indication with ETA and percentage
12//! - **Terminal Utilities**: Screen clearing, size detection, TTY checks
13//!
14//! # Example
15//!
16//! ```no_run
17//! use click::termui::{echo, secho, style, prompt, confirm, Color};
18//!
19//! // Simple output
20//! echo("Hello, world!", true, false, None);
21//!
22//! // Styled output
23//! secho("Success!", Some(Color::Green), None, true, false, false, false, false, false, false, true, true, false, None);
24//!
25//! // Create a styled string
26//! let styled = style("Error", Some(Color::Red), None, true, false, false, false, false, false, false, false);
27//!
28//! // Prompt for input
29//! let name = prompt("Enter your name", Some("World".to_string()), false, false, |s| Ok(s.to_string())).unwrap();
30//!
31//! // Confirmation
32//! if confirm("Continue?", Some(true), false).unwrap() {
33//!     // proceed
34//! }
35//! ```
36
37use std::io::{self, BufRead, Write};
38use std::process::{Command as ProcessCommand, Stdio};
39use std::time::Instant;
40
41use crate::error::{ClickError, Result};
42
43// ============================================================================
44// Echo Macro
45// ============================================================================
46
47/// Convenience macro for printing to the terminal.
48///
49/// This macro wraps the `echo` function with a simpler syntax.
50///
51/// # Usage
52///
53/// ```no_run
54/// use click::echo;
55///
56/// // Simple message with newline
57/// echo!("Hello, world!");
58///
59/// // Without newline
60/// echo!("Prompt: ", nl = false);
61///
62/// // To stderr
63/// echo!("Error occurred", err = true);
64///
65/// // Combined
66/// echo!("Warning: ", nl = false, err = true);
67///
68/// // With formatting
69/// let name = "World";
70/// echo!("Hello, {}!", name);
71/// ```
72#[macro_export]
73macro_rules! echo {
74    // Basic message
75    ($msg:expr) => {
76        $crate::termui::echo($msg, true, false, None)
77    };
78    // Message with format args
79    ($fmt:expr, $($arg:tt)*) => {{
80        // Check if first arg after format is a keyword arg
81        echo!(@parse $fmt, $($arg)*)
82    }};
83    // Parse keyword arguments
84    (@parse $fmt:expr, nl = $nl:expr) => {
85        $crate::termui::echo($fmt, $nl, false, None)
86    };
87    (@parse $fmt:expr, err = $err:expr) => {
88        $crate::termui::echo($fmt, true, $err, None)
89    };
90    (@parse $fmt:expr, nl = $nl:expr, err = $err:expr) => {
91        $crate::termui::echo($fmt, $nl, $err, None)
92    };
93    (@parse $fmt:expr, err = $err:expr, nl = $nl:expr) => {
94        $crate::termui::echo($fmt, $nl, $err, None)
95    };
96    (@parse $fmt:expr, color = $color:expr) => {
97        $crate::termui::echo($fmt, true, false, $color)
98    };
99    (@parse $fmt:expr, nl = $nl:expr, color = $color:expr) => {
100        $crate::termui::echo($fmt, $nl, false, $color)
101    };
102    (@parse $fmt:expr, err = $err:expr, color = $color:expr) => {
103        $crate::termui::echo($fmt, true, $err, $color)
104    };
105    (@parse $fmt:expr, nl = $nl:expr, err = $err:expr, color = $color:expr) => {
106        $crate::termui::echo($fmt, $nl, $err, $color)
107    };
108    // Format string with args (no keyword args)
109    (@parse $fmt:expr, $($arg:tt)*) => {
110        $crate::termui::echo(&format!($fmt, $($arg)*), true, false, None)
111    };
112}
113
114// ============================================================================
115// Color Constants
116// ============================================================================
117
118/// Terminal colors for styled output.
119///
120/// These colors correspond to standard ANSI terminal colors.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum Color {
123    /// Black (ANSI code 30/40)
124    Black,
125    /// Red (ANSI code 31/41)
126    Red,
127    /// Green (ANSI code 32/42)
128    Green,
129    /// Yellow (ANSI code 33/43)
130    Yellow,
131    /// Blue (ANSI code 34/44)
132    Blue,
133    /// Magenta (ANSI code 35/45)
134    Magenta,
135    /// Cyan (ANSI code 36/46)
136    Cyan,
137    /// White (ANSI code 37/47)
138    White,
139    /// Bright Black (ANSI code 90/100)
140    BrightBlack,
141    /// Bright Red (ANSI code 91/101)
142    BrightRed,
143    /// Bright Green (ANSI code 92/102)
144    BrightGreen,
145    /// Bright Yellow (ANSI code 93/103)
146    BrightYellow,
147    /// Bright Blue (ANSI code 94/104)
148    BrightBlue,
149    /// Bright Magenta (ANSI code 95/105)
150    BrightMagenta,
151    /// Bright Cyan (ANSI code 96/106)
152    BrightCyan,
153    /// Bright White (ANSI code 97/107)
154    BrightWhite,
155    /// Reset color to default
156    Reset,
157}
158
159impl Color {
160    /// Get the ANSI foreground color code.
161    pub fn fg_code(self) -> u8 {
162        match self {
163            Color::Black => 30,
164            Color::Red => 31,
165            Color::Green => 32,
166            Color::Yellow => 33,
167            Color::Blue => 34,
168            Color::Magenta => 35,
169            Color::Cyan => 36,
170            Color::White => 37,
171            Color::BrightBlack => 90,
172            Color::BrightRed => 91,
173            Color::BrightGreen => 92,
174            Color::BrightYellow => 93,
175            Color::BrightBlue => 94,
176            Color::BrightMagenta => 95,
177            Color::BrightCyan => 96,
178            Color::BrightWhite => 97,
179            Color::Reset => 39,
180        }
181    }
182
183    /// Get the ANSI background color code.
184    pub fn bg_code(self) -> u8 {
185        match self {
186            Color::Black => 40,
187            Color::Red => 41,
188            Color::Green => 42,
189            Color::Yellow => 43,
190            Color::Blue => 44,
191            Color::Magenta => 45,
192            Color::Cyan => 46,
193            Color::White => 47,
194            Color::BrightBlack => 100,
195            Color::BrightRed => 101,
196            Color::BrightGreen => 102,
197            Color::BrightYellow => 103,
198            Color::BrightBlue => 104,
199            Color::BrightMagenta => 105,
200            Color::BrightCyan => 106,
201            Color::BrightWhite => 107,
202            Color::Reset => 49,
203        }
204    }
205}
206
207// Color constants for convenience (Python Click compatibility)
208pub const BLACK: Color = Color::Black;
209pub const RED: Color = Color::Red;
210pub const GREEN: Color = Color::Green;
211pub const YELLOW: Color = Color::Yellow;
212pub const BLUE: Color = Color::Blue;
213pub const MAGENTA: Color = Color::Magenta;
214pub const CYAN: Color = Color::Cyan;
215pub const WHITE: Color = Color::White;
216pub const BRIGHT_BLACK: Color = Color::BrightBlack;
217pub const BRIGHT_RED: Color = Color::BrightRed;
218pub const BRIGHT_GREEN: Color = Color::BrightGreen;
219pub const BRIGHT_YELLOW: Color = Color::BrightYellow;
220pub const BRIGHT_BLUE: Color = Color::BrightBlue;
221pub const BRIGHT_MAGENTA: Color = Color::BrightMagenta;
222pub const BRIGHT_CYAN: Color = Color::BrightCyan;
223pub const BRIGHT_WHITE: Color = Color::BrightWhite;
224pub const RESET: Color = Color::Reset;
225
226// ============================================================================
227// Terminal Detection
228// ============================================================================
229
230/// Check if a file descriptor refers to a TTY.
231///
232/// This checks if the given stream is connected to an interactive terminal.
233///
234/// # Arguments
235///
236/// * `stream` - The stream to check: "stdout", "stderr", or "stdin"
237///
238/// # Returns
239///
240/// `true` if the stream is a TTY, `false` otherwise.
241///
242/// # Note
243///
244/// This uses `std::io::IsTerminal` when available (MSRV: 1.70).
245pub fn isatty(stream: &str) -> bool {
246    use std::io::IsTerminal;
247    match stream {
248        "stdin" => std::io::stdin().is_terminal(),
249        "stdout" => std::io::stdout().is_terminal(),
250        "stderr" => std::io::stderr().is_terminal(),
251        _ => false,
252    }
253}
254
255/// Check if stdout is connected to a TTY.
256pub fn stdout_isatty() -> bool {
257    isatty("stdout")
258}
259
260/// Check if stderr is connected to a TTY.
261pub fn stderr_isatty() -> bool {
262    isatty("stderr")
263}
264
265/// Check if stdin is connected to a TTY.
266pub fn stdin_isatty() -> bool {
267    isatty("stdin")
268}
269
270/// Get the terminal size as (width, height).
271///
272/// Returns a default of (80, 24) if detection fails or if not connected to a TTY.
273///
274/// # Returns
275///
276/// A tuple of (width, height) in characters.
277pub fn get_terminal_size() -> (usize, usize) {
278    // Try environment variables first (portable)
279    if let (Ok(cols), Ok(rows)) = (std::env::var("COLUMNS"), std::env::var("LINES")) {
280        if let (Ok(w), Ok(h)) = (cols.parse::<usize>(), rows.parse::<usize>()) {
281            if w > 0 && h > 0 {
282                return (w, h);
283            }
284        }
285    }
286
287    // Use crossterm for OS-level detection (cross-platform, no unsafe)
288    match crossterm::terminal::size() {
289        Ok((cols, rows)) if cols > 0 && rows > 0 => (cols as usize, rows as usize),
290        _ => (80, 24),
291    }
292}
293
294/// Clear the terminal screen.
295///
296/// Uses ANSI escape sequences to clear the screen and move cursor to home.
297/// On non-TTY outputs, this is a no-op.
298pub fn clear() {
299    if stdout_isatty() {
300        // ANSI escape: clear screen and move to home
301        print!("\x1b[2J\x1b[H");
302        let _ = io::stdout().flush();
303    }
304}
305
306// ============================================================================
307// Styling Functions
308// ============================================================================
309
310/// Determine if ANSI codes should be used.
311///
312/// Returns true if:
313/// - `color` is explicitly `Some(true)`
314/// - `color` is `None` and the output stream is a TTY
315fn should_use_color(color: Option<bool>, err: bool) -> bool {
316    match color {
317        Some(true) => true,
318        Some(false) => false,
319        None => {
320            if err {
321                stderr_isatty()
322            } else {
323                stdout_isatty()
324            }
325        }
326    }
327}
328
329/// Style text with ANSI escape codes.
330///
331/// Creates a styled string with the specified formatting. If the terminal
332/// doesn't support colors or formatting is disabled, returns the text unchanged.
333///
334/// # Arguments
335///
336/// * `text` - The text to style
337/// * `fg` - Foreground color
338/// * `bg` - Background color
339/// * `bold` - Bold text
340/// * `dim` - Dim (faint) text
341/// * `underline` - Underlined text
342/// * `overline` - Overlined text (not widely supported)
343/// * `italic` - Italic text
344/// * `blink` - Blinking text
345/// * `strikethrough` - Strikethrough text
346/// * `reset` - Whether to reset all styles at the end
347///
348/// # Returns
349///
350/// The styled string with ANSI escape codes.
351#[allow(clippy::too_many_arguments)]
352pub fn style(
353    text: &str,
354    fg: Option<Color>,
355    bg: Option<Color>,
356    bold: bool,
357    dim: bool,
358    underline: bool,
359    overline: bool,
360    italic: bool,
361    blink: bool,
362    strikethrough: bool,
363    reset: bool,
364) -> String {
365    let mut codes = Vec::new();
366
367    // Collect style codes
368    if bold {
369        codes.push("1".to_string());
370    }
371    if dim {
372        codes.push("2".to_string());
373    }
374    if italic {
375        codes.push("3".to_string());
376    }
377    if underline {
378        codes.push("4".to_string());
379    }
380    if blink {
381        codes.push("5".to_string());
382    }
383    if overline {
384        codes.push("53".to_string()); // ANSI overline
385    }
386    if strikethrough {
387        codes.push("9".to_string());
388    }
389
390    // Add colors
391    if let Some(color) = fg {
392        codes.push(color.fg_code().to_string());
393    }
394    if let Some(color) = bg {
395        codes.push(color.bg_code().to_string());
396    }
397
398    if codes.is_empty() {
399        return text.to_string();
400    }
401
402    let style_start = format!("\x1b[{}m", codes.join(";"));
403    let style_end = if reset { "\x1b[0m" } else { "" };
404
405    format!("{}{}{}", style_start, text, style_end)
406}
407
408/// Print a message to the terminal.
409///
410/// # Arguments
411///
412/// * `message` - The message to print
413/// * `nl` - Whether to append a newline (default: true)
414/// * `err` - Whether to print to stderr instead of stdout
415/// * `color` - Force color on/off, or None to auto-detect
416pub fn echo(message: &str, nl: bool, err: bool, color: Option<bool>) {
417    let output = if should_use_color(color, err) {
418        message.to_string()
419    } else {
420        // Strip ANSI codes if color is disabled
421        strip_ansi_codes(message)
422    };
423
424    if err {
425        if nl {
426            eprintln!("{}", output);
427            let _ = io::stderr().flush();
428        } else {
429            eprint!("{}", output);
430            let _ = io::stderr().flush();
431        }
432    } else if nl {
433        println!("{}", output);
434        let _ = io::stdout().flush();
435    } else {
436        print!("{}", output);
437        let _ = io::stdout().flush();
438    }
439}
440
441/// Print a styled message to the terminal.
442///
443/// This is a convenience function that combines `style` and `echo`.
444///
445/// # Arguments
446///
447/// * `message` - The message to print
448/// * `fg` - Foreground color
449/// * `bg` - Background color
450/// * `bold` - Bold text
451/// * `dim` - Dim text
452/// * `underline` - Underlined text
453/// * `overline` - Overlined text
454/// * `italic` - Italic text
455/// * `blink` - Blinking text
456/// * `strikethrough` - Strikethrough text
457/// * `reset` - Whether to reset styles at end
458/// * `nl` - Whether to append a newline
459/// * `err` - Whether to print to stderr
460/// * `color` - Force color on/off, or None to auto-detect
461#[allow(clippy::too_many_arguments)]
462pub fn secho(
463    message: &str,
464    fg: Option<Color>,
465    bg: Option<Color>,
466    bold: bool,
467    dim: bool,
468    underline: bool,
469    overline: bool,
470    italic: bool,
471    blink: bool,
472    strikethrough: bool,
473    reset: bool,
474    nl: bool,
475    err: bool,
476    color: Option<bool>,
477) {
478    let styled = if should_use_color(color, err) {
479        style(
480            message,
481            fg,
482            bg,
483            bold,
484            dim,
485            underline,
486            overline,
487            italic,
488            blink,
489            strikethrough,
490            reset,
491        )
492    } else {
493        message.to_string()
494    };
495
496    if err {
497        if nl {
498            eprintln!("{}", styled);
499        } else {
500            eprint!("{}", styled);
501            let _ = io::stderr().flush();
502        }
503    } else if nl {
504        println!("{}", styled);
505    } else {
506        print!("{}", styled);
507        let _ = io::stdout().flush();
508    }
509}
510
511// ============================================================================
512// Pager Support
513// ============================================================================
514
515/// Display text via a pager.
516///
517/// Pipes the given text to a pager program ($PAGER, less, or more).
518/// Falls back to `echo()` if no pager is available or if not connected to a TTY.
519///
520/// # Arguments
521///
522/// * `text` - The text to display
523/// * `color` - Whether to preserve ANSI color codes (None = auto-detect)
524///
525/// # Example
526///
527/// ```no_run
528/// use click::termui::echo_via_pager;
529///
530/// let long_text = (0..100).map(|i| format!("Line {}", i)).collect::<Vec<_>>().join("\n");
531/// echo_via_pager(&long_text, None);
532/// ```
533pub fn echo_via_pager(text: &str, color: Option<bool>) {
534    // If not a TTY, just echo the text
535    if !stdin_isatty() || !stdout_isatty() {
536        echo(text, true, false, color);
537        return;
538    }
539
540    // Determine the pager command
541    let pager = std::env::var("PAGER")
542        .ok()
543        .filter(|p| !p.is_empty())
544        .unwrap_or_else(|| {
545            // Try to find less or more
546            if which_pager("less").is_some() {
547                "less".to_string()
548            } else if which_pager("more").is_some() {
549                "more".to_string()
550            } else {
551                String::new()
552            }
553        });
554
555    if pager.is_empty() {
556        // No pager available, fall back to echo
557        echo(text, true, false, color);
558        return;
559    }
560
561    // Prepare the text (strip ANSI codes if color is disabled)
562    let output_text = if color == Some(false) {
563        strip_ansi_codes(text)
564    } else {
565        text.to_string()
566    };
567
568    // Build the pager command with proper arguments
569    let mut parts = pager.split_whitespace();
570    let cmd_name = match parts.next() {
571        Some(name) => name,
572        None => {
573            echo(&output_text, true, false, color);
574            return;
575        }
576    };
577
578    let mut cmd = ProcessCommand::new(cmd_name);
579
580    // Add any additional arguments from $PAGER
581    for arg in parts {
582        cmd.arg(arg);
583    }
584
585    // If using less and color is enabled, add -R flag for raw control characters
586    if cmd_name == "less" && color != Some(false) {
587        cmd.arg("-R");
588    }
589
590    // Try to spawn the pager and pipe text to it
591    match cmd.stdin(Stdio::piped()).spawn() {
592        Ok(mut child) => {
593            if let Some(mut stdin) = child.stdin.take() {
594                let _ = stdin.write_all(output_text.as_bytes());
595            }
596            // Wait for pager to finish
597            let _ = child.wait();
598        }
599        Err(_) => {
600            // Pager failed to start, fall back to echo
601            echo(&output_text, true, false, color);
602        }
603    }
604}
605
606/// Check if a pager command exists in PATH.
607fn which_pager(name: &str) -> Option<String> {
608    if let Ok(path) = std::env::var("PATH") {
609        for dir in path.split(':') {
610            let full_path = std::path::Path::new(dir).join(name);
611            if full_path.exists() {
612                return Some(full_path.to_string_lossy().into_owned());
613            }
614        }
615    }
616    None
617}
618
619// ============================================================================
620// Launch Support
621// ============================================================================
622
623/// Open a URL or file path in the default application.
624///
625/// Uses platform-specific commands to launch the associated application:
626/// - macOS: `open`
627/// - Linux: `xdg-open`
628/// - Windows: `start`
629///
630/// # Arguments
631///
632/// * `url` - The URL or file path to open
633/// * `wait` - If true, wait for the application to finish before returning
634/// * `locate` - If true, open a file manager showing the file location instead
635///
636/// # Returns
637///
638/// `Ok(())` on success, or an error if the launch failed.
639///
640/// # Example
641///
642/// ```no_run
643/// use click::termui::launch;
644///
645/// # fn main() -> Result<(), click::ClickError> {
646/// // Open a URL in the default browser
647/// launch("https://example.com", false, false)?;
648///
649/// // Open a file in its default application
650/// launch("/path/to/document.pdf", false, false)?;
651///
652/// // Show file location in file manager
653/// launch("/path/to/file.txt", false, true)?;
654/// # Ok(())
655/// # }
656/// ```
657pub fn launch(url: &str, wait: bool, locate: bool) -> Result<()> {
658    let (cmd, args) = get_launch_command(url, locate)?;
659
660    let mut command = ProcessCommand::new(&cmd);
661    command.args(&args);
662
663    if wait {
664        let status = command.status().map_err(|e| {
665            ClickError::usage(format!("Failed to launch '{}': {}", url, e))
666        })?;
667
668        if !status.success() {
669            return Err(ClickError::usage(format!(
670                "Launch command failed with exit code: {:?}",
671                status.code()
672            )));
673        }
674    } else {
675        // Spawn without waiting
676        command.spawn().map_err(|e| {
677            ClickError::usage(format!("Failed to launch '{}': {}", url, e))
678        })?;
679    }
680
681    Ok(())
682}
683
684/// Get the platform-specific launch command and arguments.
685fn get_launch_command(url: &str, locate: bool) -> Result<(String, Vec<String>)> {
686    #[cfg(target_os = "macos")]
687    {
688        if locate {
689            Ok(("open".to_string(), vec!["-R".to_string(), url.to_string()]))
690        } else {
691            Ok(("open".to_string(), vec![url.to_string()]))
692        }
693    }
694
695    #[cfg(target_os = "linux")]
696    {
697        if locate {
698            // Try to use a file manager that supports revealing files
699            // First try dbus with nautilus/dolphin, fall back to xdg-open on parent dir
700            let path = std::path::Path::new(url);
701            if let Some(parent) = path.parent() {
702                Ok((
703                    "xdg-open".to_string(),
704                    vec![parent.to_string_lossy().into_owned()],
705                ))
706            } else {
707                Err(ClickError::usage(format!(
708                    "Cannot locate file: {}",
709                    url
710                )))
711            }
712        } else {
713            Ok(("xdg-open".to_string(), vec![url.to_string()]))
714        }
715    }
716
717    #[cfg(target_os = "windows")]
718    {
719        if locate {
720            // Use explorer with /select to highlight the file
721            Ok((
722                "explorer".to_string(),
723                vec!["/select,".to_string() + url],
724            ))
725        } else {
726            // Use cmd /c start for URLs and files
727            Ok((
728                "cmd".to_string(),
729                vec!["/c".to_string(), "start".to_string(), "".to_string(), url.to_string()],
730            ))
731        }
732    }
733
734    #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
735    {
736        let _ = locate; // suppress unused warning
737        Err(ClickError::usage(format!(
738            "Platform not supported for launch: {}",
739            url
740        )))
741    }
742}
743
744/// Strip ANSI escape codes from a string.
745///
746/// # Arguments
747///
748/// * `text` - The text to strip
749///
750/// # Returns
751///
752/// The text with all ANSI escape codes removed.
753pub fn strip_ansi_codes(text: &str) -> String {
754    let mut result = String::with_capacity(text.len());
755    let mut chars = text.chars().peekable();
756
757    while let Some(c) = chars.next() {
758        if c == '\x1b' {
759            // Skip the escape sequence
760            if chars.peek() == Some(&'[') {
761                chars.next(); // consume '['
762                // Skip until we hit a letter (end of sequence)
763                while let Some(&next) = chars.peek() {
764                    chars.next();
765                    if next.is_ascii_alphabetic() {
766                        break;
767                    }
768                }
769            }
770        } else {
771            result.push(c);
772        }
773    }
774
775    result
776}
777
778// ============================================================================
779// Input Functions
780// ============================================================================
781
782/// Prompt the user for input with optional type conversion.
783///
784/// # Arguments
785///
786/// * `text` - The prompt text to display
787/// * `default` - Optional default value if user presses Enter
788/// * `hide_input` - Whether to hide user input (for passwords)
789/// * `confirmation` - Whether to prompt twice and require matching input
790/// * `type_converter` - Function to convert and validate the input
791///
792/// # Returns
793///
794/// The converted user input, or an error.
795///
796/// # Notes
797///
798/// Hidden input requires terminal raw mode. If raw mode is unavailable,
799/// input will be visible with a warning.
800pub fn prompt<T, F>(
801    text: &str,
802    default: Option<T>,
803    hide_input: bool,
804    confirmation: bool,
805    type_converter: F,
806) -> Result<T>
807where
808    T: Clone + std::fmt::Display,
809    F: Fn(&str) -> std::result::Result<T, String>,
810{
811    loop {
812        // Build prompt string
813        let prompt_text = if let Some(ref def) = default {
814            format!("{} [{}]: ", text, def)
815        } else {
816            format!("{}: ", text)
817        };
818
819        // Read input
820        let input = if hide_input {
821            read_hidden_input(&prompt_text)?
822        } else {
823            read_line(&prompt_text)?
824        };
825
826        // Handle empty input with default
827        let value = if input.is_empty() {
828            if let Some(def) = default.clone() {
829                return Ok(def);
830            } else {
831                echo("Error: This field is required.", true, true, None);
832                continue;
833            }
834        } else {
835            input
836        };
837
838        // Convert value
839        let converted = match type_converter(&value) {
840            Ok(v) => v,
841            Err(msg) => {
842                echo(&format!("Error: {}", msg), true, true, None);
843                continue;
844            }
845        };
846
847        // Handle confirmation
848        if confirmation {
849            let confirm_prompt = "Repeat for confirmation: ".to_string();
850            let confirm_input = if hide_input {
851                read_hidden_input(&confirm_prompt)?
852            } else {
853                read_line(&confirm_prompt)?
854            };
855
856            if confirm_input != value {
857                echo(
858                    "Error: The two entered values do not match.",
859                    true,
860                    true,
861                    None,
862                );
863                continue;
864            }
865        }
866
867        return Ok(converted);
868    }
869}
870
871/// Prompt for yes/no confirmation.
872///
873/// # Arguments
874///
875/// * `text` - The prompt text to display
876/// * `default` - Optional default value (true=yes, false=no)
877/// * `abort` - Whether to raise Abort error on "no" answer
878///
879/// # Returns
880///
881/// `true` if user answered yes, `false` if no.
882/// Returns `Err(ClickError::Abort)` if `abort` is true and user answered no.
883pub fn confirm(text: &str, default: Option<bool>, abort: bool) -> Result<bool> {
884    let suffix = match default {
885        Some(true) => " [Y/n]: ",
886        Some(false) => " [y/N]: ",
887        None => " [y/n]: ",
888    };
889
890    loop {
891        let prompt_text = format!("{}{}", text, suffix);
892        let input = read_line(&prompt_text)?;
893        let input_lower = input.to_lowercase();
894
895        let result = if input.is_empty() {
896            default
897        } else if input_lower == "y" || input_lower == "yes" {
898            Some(true)
899        } else if input_lower == "n" || input_lower == "no" {
900            Some(false)
901        } else {
902            echo("Error: invalid input", true, true, None);
903            continue;
904        };
905
906        match result {
907            Some(true) => return Ok(true),
908            Some(false) => {
909                if abort {
910                    return Err(ClickError::Abort);
911                }
912                return Ok(false);
913            }
914            None => {
915                echo("Error: invalid input", true, true, None);
916                continue;
917            }
918        }
919    }
920}
921
922/// Read a single character from the terminal.
923///
924/// # Arguments
925///
926/// * `echo_char` - Whether to echo the character back to the terminal
927///
928/// # Returns
929///
930/// The character read from the terminal.
931///
932/// # Notes
933///
934/// This function attempts to use raw mode for immediate character reading.
935/// If raw mode is unavailable, it falls back to reading a line and returning
936/// the first character.
937pub fn getchar(echo_char: bool) -> Result<char> {
938    use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
939    use crossterm::terminal;
940
941    if terminal::enable_raw_mode().is_ok() {
942        let result = loop {
943            match event::read() {
944                Ok(Event::Key(KeyEvent {
945                    code, modifiers, ..
946                })) => {
947                    if modifiers.contains(KeyModifiers::CONTROL) {
948                        if let KeyCode::Char('c') = code {
949                            break Err(ClickError::Abort);
950                        }
951                    }
952                    match code {
953                        KeyCode::Char(c) => break Ok(c),
954                        KeyCode::Enter => break Ok('\n'),
955                        KeyCode::Backspace => break Ok('\x7f'),
956                        KeyCode::Tab => break Ok('\t'),
957                        KeyCode::Esc => break Ok('\x1b'),
958                        _ => continue,
959                    }
960                }
961                Ok(_) => continue,
962                Err(e) => {
963                    break Err(ClickError::usage(format!("Failed to read key: {}", e)))
964                }
965            }
966        };
967        let _ = terminal::disable_raw_mode();
968
969        if let Ok(c) = &result {
970            if echo_char {
971                print!("{}", c);
972                let _ = io::stdout().flush();
973            }
974        }
975        return result;
976    }
977
978    // Fallback: read a line and return the first character
979    let input = read_line("")?;
980    input.chars().next().ok_or(ClickError::Abort)
981}
982
983/// Pause until the user presses any key.
984///
985/// # Arguments
986///
987/// * `info` - Optional message to display (default: "Press any key to continue...")
988pub fn pause(info: Option<&str>) {
989    let message = info.unwrap_or("Press any key to continue...");
990    echo(message, false, false, None);
991
992    // Try to read a single character
993    let _ = getchar(false);
994
995    // Print newline
996    println!();
997}
998
999/// Read a line of input from stdin.
1000fn read_line(prompt: &str) -> Result<String> {
1001    if !prompt.is_empty() {
1002        print!("{}", prompt);
1003        let _ = io::stdout().flush();
1004    }
1005
1006    let stdin = io::stdin();
1007    let mut line = String::new();
1008
1009    stdin
1010        .lock()
1011        .read_line(&mut line)
1012        .map_err(|e| ClickError::usage(format!("Failed to read input: {}", e)))?;
1013
1014    // Trim the trailing newline
1015    if line.ends_with('\n') {
1016        line.pop();
1017        if line.ends_with('\r') {
1018            line.pop();
1019        }
1020    }
1021
1022    Ok(line)
1023}
1024
1025/// Read hidden input (for passwords).
1026///
1027/// Uses crossterm raw mode to read char-by-char without echo. Falls back to
1028/// visible input when not connected to a TTY (e.g., piped stdin in tests).
1029fn read_hidden_input(prompt: &str) -> Result<String> {
1030    use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
1031    use crossterm::terminal;
1032
1033    if !prompt.is_empty() {
1034        print!("{}", prompt);
1035        let _ = io::stdout().flush();
1036    }
1037
1038    // Try crossterm raw mode for hidden char-by-char reading (cross-platform, no unsafe).
1039    if terminal::enable_raw_mode().is_ok() {
1040        let mut input = String::new();
1041        let result = loop {
1042            match event::read() {
1043                Ok(Event::Key(KeyEvent {
1044                    code, modifiers, ..
1045                })) => {
1046                    if modifiers.contains(KeyModifiers::CONTROL) {
1047                        if let KeyCode::Char('c') = code {
1048                            break Err(ClickError::Abort);
1049                        }
1050                    }
1051                    match code {
1052                        KeyCode::Enter => break Ok(input.clone()),
1053                        KeyCode::Char(c) => input.push(c),
1054                        KeyCode::Backspace => {
1055                            input.pop();
1056                        }
1057                        _ => {}
1058                    }
1059                }
1060                Ok(_) => continue,
1061                Err(_) => break Ok(input.clone()),
1062            }
1063        };
1064        let _ = terminal::disable_raw_mode();
1065        println!();
1066        return result;
1067    }
1068
1069    // Fallback: warn user and read normally (non-TTY, e.g., piped stdin)
1070    echo("(Warning: Input will be visible)", true, true, None);
1071    read_line("")
1072}
1073
1074// ============================================================================
1075// Progress Bar
1076// ============================================================================
1077
1078/// A progress bar for displaying operation progress.
1079///
1080/// # Example
1081///
1082/// ```no_run
1083/// use click::termui::ProgressBar;
1084///
1085/// let mut bar = ProgressBar::new(100, Some("Processing"), true, true, true, 40);
1086///
1087/// for i in 0..100 {
1088///     // Do work...
1089///     bar.update(1);
1090/// }
1091///
1092/// bar.finish();
1093///
1094/// // Custom fill/empty characters
1095/// let mut bar = ProgressBar::new(100, None, true, true, false, 30)
1096///     .fill_char('█')
1097///     .empty_char('░');
1098/// ```
1099pub struct ProgressBar {
1100    /// Total length of the progress (number of items)
1101    length: usize,
1102    /// Current position
1103    position: usize,
1104    /// Optional label to display
1105    label: Option<String>,
1106    /// Whether to show ETA
1107    show_eta: bool,
1108    /// Whether to show percentage
1109    show_percent: bool,
1110    /// Whether to show position/length
1111    show_pos: bool,
1112    /// Width of the progress bar in characters
1113    width: usize,
1114    /// Start time for ETA calculation
1115    start_time: Instant,
1116    /// Whether the bar is finished
1117    finished: bool,
1118    /// Whether output is a TTY
1119    is_tty: bool,
1120    /// Last rendered output length (for TTY updates)
1121    #[allow(dead_code)]
1122    last_output_len: usize,
1123    /// Character used for filled portion of bar (default: '#')
1124    fill_char: char,
1125    /// Character used for empty portion of bar (default: '-')
1126    empty_char: char,
1127}
1128
1129impl ProgressBar {
1130    /// Create a new progress bar.
1131    ///
1132    /// # Arguments
1133    ///
1134    /// * `length` - Total number of items to process
1135    /// * `label` - Optional label to display before the bar
1136    /// * `show_eta` - Whether to show estimated time remaining
1137    /// * `show_percent` - Whether to show percentage complete
1138    /// * `show_pos` - Whether to show position/length
1139    /// * `width` - Width of the bar portion in characters
1140    pub fn new(
1141        length: usize,
1142        label: Option<&str>,
1143        show_eta: bool,
1144        show_percent: bool,
1145        show_pos: bool,
1146        width: usize,
1147    ) -> Self {
1148        let bar = Self {
1149            length,
1150            position: 0,
1151            label: label.map(String::from),
1152            show_eta,
1153            show_percent,
1154            show_pos,
1155            width,
1156            start_time: Instant::now(),
1157            finished: false,
1158            is_tty: stdout_isatty(),
1159            last_output_len: 0,
1160            fill_char: '#',
1161            empty_char: '-',
1162        };
1163
1164        // Initial render
1165        bar.render_internal();
1166        bar
1167    }
1168
1169    /// Set the character used for the filled portion of the bar.
1170    ///
1171    /// Default is '#'.
1172    ///
1173    /// # Example
1174    ///
1175    /// ```no_run
1176    /// use click::termui::ProgressBar;
1177    /// let bar = ProgressBar::new(100, None, true, true, false, 30)
1178    ///     .fill_char('█');
1179    /// ```
1180    pub fn fill_char(mut self, c: char) -> Self {
1181        self.fill_char = c;
1182        self
1183    }
1184
1185    /// Set the character used for the empty portion of the bar.
1186    ///
1187    /// Default is '-'.
1188    ///
1189    /// # Example
1190    ///
1191    /// ```no_run
1192    /// use click::termui::ProgressBar;
1193    /// let bar = ProgressBar::new(100, None, true, true, false, 30)
1194    ///     .empty_char('░');
1195    /// ```
1196    pub fn empty_char(mut self, c: char) -> Self {
1197        self.empty_char = c;
1198        self
1199    }
1200
1201    /// Update the progress bar by advancing by `n` items.
1202    ///
1203    /// # Arguments
1204    ///
1205    /// * `n` - Number of items completed since last update
1206    pub fn update(&mut self, n: usize) {
1207        if self.finished {
1208            return;
1209        }
1210
1211        self.position = (self.position + n).min(self.length);
1212        self.render_internal();
1213    }
1214
1215    /// Set the progress bar to a specific position.
1216    ///
1217    /// # Arguments
1218    ///
1219    /// * `pos` - New position
1220    pub fn set_position(&mut self, pos: usize) {
1221        if self.finished {
1222            return;
1223        }
1224
1225        self.position = pos.min(self.length);
1226        self.render_internal();
1227    }
1228
1229    /// Mark the progress bar as finished and render final state.
1230    pub fn finish(&mut self) {
1231        if self.finished {
1232            return;
1233        }
1234
1235        self.position = self.length;
1236        self.finished = true;
1237        self.render_internal();
1238
1239        // Print newline
1240        if self.is_tty {
1241            println!();
1242        }
1243    }
1244
1245    /// Render the current progress bar state to a string.
1246    pub fn render(&self) -> String {
1247        let mut parts = Vec::new();
1248
1249        // Label
1250        if let Some(ref label) = self.label {
1251            parts.push(label.clone());
1252        }
1253
1254        // Calculate progress
1255        let progress = if self.length > 0 {
1256            self.position as f64 / self.length as f64
1257        } else {
1258            0.0
1259        };
1260
1261        // Progress bar
1262        let filled = (progress * self.width as f64) as usize;
1263        let empty = self.width.saturating_sub(filled);
1264        let bar = format!(
1265            "[{}{}]",
1266            self.fill_char.to_string().repeat(filled),
1267            self.empty_char.to_string().repeat(empty)
1268        );
1269        parts.push(bar);
1270
1271        // Percentage
1272        if self.show_percent {
1273            parts.push(format!("{:3.0}%", progress * 100.0));
1274        }
1275
1276        // Position / Length
1277        if self.show_pos {
1278            parts.push(format!("{}/{}", self.position, self.length));
1279        }
1280
1281        // ETA
1282        if self.show_eta && self.position > 0 && !self.finished {
1283            let elapsed = self.start_time.elapsed();
1284            let rate = self.position as f64 / elapsed.as_secs_f64();
1285            let remaining = self.length - self.position;
1286            let eta_secs = if rate > 0.0 {
1287                remaining as f64 / rate
1288            } else {
1289                0.0
1290            };
1291
1292            if eta_secs < 3600.0 {
1293                let mins = (eta_secs / 60.0) as u64;
1294                let secs = (eta_secs % 60.0) as u64;
1295                parts.push(format!("eta {:02}:{:02}", mins, secs));
1296            } else {
1297                let hours = (eta_secs / 3600.0) as u64;
1298                let mins = ((eta_secs % 3600.0) / 60.0) as u64;
1299                parts.push(format!("eta {}h {:02}m", hours, mins));
1300            }
1301        }
1302
1303        parts.join(" ")
1304    }
1305
1306    /// Internal render method that updates the terminal.
1307    fn render_internal(&self) {
1308        let output = self.render();
1309
1310        if self.is_tty {
1311            // Carriage return to beginning of line
1312            print!("\r{}", output);
1313            // Clear any remaining characters from previous output
1314            let clear_len = self.last_output_len.saturating_sub(output.len());
1315            if clear_len > 0 {
1316                print!("{}", " ".repeat(clear_len));
1317                print!("\r{}", output);
1318            }
1319            let _ = io::stdout().flush();
1320        } else {
1321            // Non-TTY: print on new lines
1322            println!("{}", output);
1323        }
1324    }
1325}
1326
1327impl Drop for ProgressBar {
1328    fn drop(&mut self) {
1329        if !self.finished && self.is_tty {
1330            // Ensure we leave the terminal in a clean state
1331            println!();
1332        }
1333    }
1334}
1335
1336/// Wrap an iterator with a progress bar display.
1337///
1338/// # Arguments
1339///
1340/// * `iter` - The iterator to wrap
1341/// * `length` - Total length (for percentage/ETA), or None to infer from iterator
1342/// * `label` - Optional label to display
1343///
1344/// # Returns
1345///
1346/// An iterator that displays progress as items are consumed.
1347///
1348/// # Example
1349///
1350/// ```no_run
1351/// use click::termui::progressbar;
1352///
1353/// let items = vec![1, 2, 3, 4, 5];
1354/// for item in progressbar(items.iter(), Some(items.len()), Some("Processing")) {
1355///     // Process item
1356/// }
1357/// ```
1358pub fn progressbar<I>(
1359    iter: I,
1360    length: Option<usize>,
1361    label: Option<&str>,
1362) -> ProgressBarIter<I::IntoIter>
1363where
1364    I: IntoIterator,
1365    I::IntoIter: ExactSizeIterator,
1366{
1367    let iter = iter.into_iter();
1368    let len = length.unwrap_or_else(|| iter.len());
1369
1370    ProgressBarIter {
1371        iter,
1372        bar: ProgressBar::new(len, label, true, true, true, 30),
1373    }
1374}
1375
1376/// An iterator wrapper that displays a progress bar.
1377pub struct ProgressBarIter<I> {
1378    iter: I,
1379    bar: ProgressBar,
1380}
1381
1382impl<I> Iterator for ProgressBarIter<I>
1383where
1384    I: Iterator,
1385{
1386    type Item = I::Item;
1387
1388    fn next(&mut self) -> Option<Self::Item> {
1389        match self.iter.next() {
1390            Some(item) => {
1391                self.bar.update(1);
1392                Some(item)
1393            }
1394            None => {
1395                self.bar.finish();
1396                None
1397            }
1398        }
1399    }
1400
1401    fn size_hint(&self) -> (usize, Option<usize>) {
1402        self.iter.size_hint()
1403    }
1404}
1405
1406impl<I: ExactSizeIterator> ExactSizeIterator for ProgressBarIter<I> {
1407    fn len(&self) -> usize {
1408        self.iter.len()
1409    }
1410}
1411
1412// ============================================================================
1413// Editor Support
1414// ============================================================================
1415
1416/// Open a text editor for the user to edit content.
1417///
1418/// # Arguments
1419///
1420/// * `text` - Initial text to populate the editor with
1421/// * `editor` - Editor command to use, or None to use $EDITOR/$VISUAL/vi
1422/// * `extension` - File extension for the temporary file
1423/// * `require_save` - If true, return None if user didn't save
1424///
1425/// # Returns
1426///
1427/// The edited text, or None if editing was cancelled.
1428pub fn edit_text(
1429    text: Option<&str>,
1430    editor: Option<&str>,
1431    extension: &str,
1432    require_save: bool,
1433) -> Result<Option<String>> {
1434    use std::fs;
1435    use std::process::Command;
1436
1437    // Create temporary file
1438    let temp_dir = std::env::temp_dir();
1439    let temp_file = temp_dir.join(format!("click_edit_{}.{}", std::process::id(), extension));
1440
1441    // Write initial content
1442    if let Some(initial) = text {
1443        fs::write(&temp_file, initial)
1444            .map_err(|e| ClickError::file_error(&temp_file, e.to_string()))?;
1445    }
1446
1447    // Get modification time before editing
1448    let mtime_before = fs::metadata(&temp_file)
1449        .ok()
1450        .and_then(|m| m.modified().ok());
1451
1452    // Determine editor
1453    let editor_cmd = editor
1454        .map(String::from)
1455        .or_else(|| std::env::var("VISUAL").ok())
1456        .or_else(|| std::env::var("EDITOR").ok())
1457        .unwrap_or_else(|| "vi".to_string());
1458
1459    // Run editor
1460    let status = Command::new(&editor_cmd)
1461        .arg(&temp_file)
1462        .status()
1463        .map_err(|e| ClickError::usage(format!("Failed to run editor '{}': {}", editor_cmd, e)))?;
1464
1465    if !status.success() {
1466        let _ = fs::remove_file(&temp_file);
1467        return Err(ClickError::usage(format!(
1468            "Editor '{}' exited with error",
1469            editor_cmd
1470        )));
1471    }
1472
1473    // Check if file was modified
1474    if require_save {
1475        let mtime_after = fs::metadata(&temp_file)
1476            .ok()
1477            .and_then(|m| m.modified().ok());
1478
1479        if mtime_before == mtime_after {
1480            let _ = fs::remove_file(&temp_file);
1481            return Ok(None);
1482        }
1483    }
1484
1485    // Read edited content
1486    let content = fs::read_to_string(&temp_file)
1487        .map_err(|e| ClickError::file_error(&temp_file, e.to_string()))?;
1488
1489    // Clean up
1490    let _ = fs::remove_file(&temp_file);
1491
1492    Ok(Some(content))
1493}
1494
1495// ============================================================================
1496// Tests
1497// ============================================================================
1498
1499#[cfg(test)]
1500mod tests {
1501    use super::*;
1502
1503    #[test]
1504    fn test_color_codes() {
1505        assert_eq!(Color::Red.fg_code(), 31);
1506        assert_eq!(Color::Red.bg_code(), 41);
1507        assert_eq!(Color::BrightGreen.fg_code(), 92);
1508        assert_eq!(Color::BrightGreen.bg_code(), 102);
1509        assert_eq!(Color::Reset.fg_code(), 39);
1510        assert_eq!(Color::Reset.bg_code(), 49);
1511    }
1512
1513    #[test]
1514    fn test_style_basic() {
1515        let styled = style(
1516            "hello", None, None, false, false, false, false, false, false, false, false,
1517        );
1518        assert_eq!(styled, "hello");
1519    }
1520
1521    #[test]
1522    fn test_style_with_color() {
1523        let styled = style(
1524            "hello",
1525            Some(Color::Red),
1526            None,
1527            false,
1528            false,
1529            false,
1530            false,
1531            false,
1532            false,
1533            false,
1534            true,
1535        );
1536        assert_eq!(styled, "\x1b[31mhello\x1b[0m");
1537    }
1538
1539    #[test]
1540    fn test_style_bold() {
1541        let styled = style(
1542            "hello", None, None, true, false, false, false, false, false, false, true,
1543        );
1544        assert_eq!(styled, "\x1b[1mhello\x1b[0m");
1545    }
1546
1547    #[test]
1548    fn test_style_multiple() {
1549        let styled = style(
1550            "hello",
1551            Some(Color::Green),
1552            Some(Color::Black),
1553            true,
1554            false,
1555            true,
1556            false,
1557            false,
1558            false,
1559            false,
1560            true,
1561        );
1562        // Should have: bold (1), underline (4), fg green (32), bg black (40)
1563        assert!(styled.starts_with("\x1b["));
1564        assert!(styled.contains("1"));
1565        assert!(styled.contains("4"));
1566        assert!(styled.contains("32"));
1567        assert!(styled.contains("40"));
1568        assert!(styled.ends_with("\x1b[0m"));
1569    }
1570
1571    #[test]
1572    fn test_strip_ansi_codes() {
1573        let styled = "\x1b[31mhello\x1b[0m world";
1574        let stripped = strip_ansi_codes(styled);
1575        assert_eq!(stripped, "hello world");
1576
1577        let plain = "no codes here";
1578        assert_eq!(strip_ansi_codes(plain), "no codes here");
1579    }
1580
1581    #[test]
1582    fn test_strip_ansi_codes_complex() {
1583        let styled = "\x1b[1;31;40mcomplex\x1b[0m";
1584        let stripped = strip_ansi_codes(styled);
1585        assert_eq!(stripped, "complex");
1586    }
1587
1588    #[test]
1589    fn test_get_terminal_size_returns_valid() {
1590        let (width, height) = get_terminal_size();
1591        assert!(width > 0);
1592        assert!(height > 0);
1593    }
1594
1595    #[test]
1596    fn test_progress_bar_render() {
1597        let bar = ProgressBar::new(100, Some("Test"), false, true, true, 20);
1598        let output = bar.render();
1599        assert!(output.contains("Test"));
1600        assert!(output.contains("["));
1601        assert!(output.contains("]"));
1602        assert!(output.contains("0/100"));
1603        assert!(output.contains("0%"));
1604    }
1605
1606    #[test]
1607    fn test_progress_bar_update() {
1608        let mut bar = ProgressBar::new(100, None, false, true, false, 10);
1609        bar.update(50);
1610        let output = bar.render();
1611        assert!(output.contains("50%"));
1612    }
1613
1614    #[test]
1615    fn test_progress_bar_finish() {
1616        let mut bar = ProgressBar::new(100, None, false, true, false, 10);
1617        bar.finish();
1618        let output = bar.render();
1619        assert!(output.contains("100%"));
1620        assert!(bar.finished);
1621    }
1622
1623    #[test]
1624    fn test_progress_bar_zero_length() {
1625        let bar = ProgressBar::new(0, None, false, true, false, 10);
1626        let output = bar.render();
1627        assert!(output.contains("0%"));
1628    }
1629
1630    #[test]
1631    fn test_progress_bar_custom_chars() {
1632        let mut bar = ProgressBar::new(100, None, false, false, false, 10)
1633            .fill_char('=')
1634            .empty_char(' ');
1635        bar.set_position(50);
1636        let output = bar.render();
1637        // Should have 5 '=' chars and 5 ' ' chars (50% of width 10)
1638        assert!(output.contains("[=====     ]"));
1639    }
1640
1641    #[test]
1642    fn test_progress_bar_unicode_chars() {
1643        let bar = ProgressBar::new(100, None, false, false, false, 4)
1644            .fill_char('\u{2588}')  // Full block
1645            .empty_char('\u{2591}'); // Light shade
1646        let output = bar.render();
1647        // At 0%, should be all empty chars
1648        assert!(output.contains("[\u{2591}\u{2591}\u{2591}\u{2591}]"));
1649    }
1650
1651    #[test]
1652    fn test_color_constants() {
1653        assert_eq!(BLACK, Color::Black);
1654        assert_eq!(RED, Color::Red);
1655        assert_eq!(GREEN, Color::Green);
1656        assert_eq!(YELLOW, Color::Yellow);
1657        assert_eq!(BLUE, Color::Blue);
1658        assert_eq!(MAGENTA, Color::Magenta);
1659        assert_eq!(CYAN, Color::Cyan);
1660        assert_eq!(WHITE, Color::White);
1661        assert_eq!(BRIGHT_BLACK, Color::BrightBlack);
1662        assert_eq!(BRIGHT_RED, Color::BrightRed);
1663        assert_eq!(BRIGHT_GREEN, Color::BrightGreen);
1664        assert_eq!(BRIGHT_YELLOW, Color::BrightYellow);
1665        assert_eq!(BRIGHT_BLUE, Color::BrightBlue);
1666        assert_eq!(BRIGHT_MAGENTA, Color::BrightMagenta);
1667        assert_eq!(BRIGHT_CYAN, Color::BrightCyan);
1668        assert_eq!(BRIGHT_WHITE, Color::BrightWhite);
1669        assert_eq!(RESET, Color::Reset);
1670    }
1671
1672    #[test]
1673    fn test_style_all_options() {
1674        let styled = style(
1675            "test",
1676            Some(Color::Blue),
1677            Some(Color::White),
1678            true,  // bold
1679            true,  // dim
1680            true,  // underline
1681            true,  // overline
1682            true,  // italic
1683            true,  // blink
1684            true,  // strikethrough
1685            true,  // reset
1686        );
1687        assert!(styled.starts_with("\x1b["));
1688        assert!(styled.contains("1")); // bold
1689        assert!(styled.contains("2")); // dim
1690        assert!(styled.contains("3")); // italic
1691        assert!(styled.contains("4")); // underline
1692        assert!(styled.contains("5")); // blink
1693        assert!(styled.contains("9")); // strikethrough
1694        assert!(styled.contains("53")); // overline
1695        assert!(styled.contains("34")); // blue fg
1696        assert!(styled.contains("47")); // white bg
1697        assert!(styled.ends_with("\x1b[0m"));
1698    }
1699
1700    #[test]
1701    fn test_style_no_reset() {
1702        let styled = style(
1703            "hello",
1704            Some(Color::Red),
1705            None,
1706            false,
1707            false,
1708            false,
1709            false,
1710            false,
1711            false,
1712            false,
1713            false,
1714        );
1715        assert!(styled.starts_with("\x1b[31m"));
1716        assert!(!styled.ends_with("\x1b[0m"));
1717    }
1718}