falcon_cli/
lib.rs

1//#![allow(warnings)]
2// Copyright 2025 Aquila Labs of Alberta, Canada <matt@cicero.sh>
3// Licensed under either the Apache License, Version 2.0 OR the MIT License, at your option.
4// You may not use this file except in compliance with one of the Licenses.
5// Apache License text: https://www.apache.org/licenses/LICENSE-2.0
6// MIT License text: https://opensource.org/licenses/MIT
7
8pub use self::error::CliError;
9pub use self::help::CliHelpScreen;
10pub use self::macros::*;
11pub use self::request::{CliFormat, CliRequest};
12pub use self::router::CliRouter;
13pub use anyhow;
14pub use indexmap::{IndexMap, indexmap};
15
16use rpassword::read_password;
17use std::fmt::Display;
18use std::hash::Hash;
19use std::process::{Command, exit};
20use std::str::FromStr;
21use std::{env, fs};
22use zxcvbn::zxcvbn;
23
24pub mod error;
25mod help;
26pub mod macros;
27mod request;
28mod router;
29
30/// Trait that all CLI commands must implement.
31///
32/// This trait defines the interface for CLI commands, requiring implementations
33/// to provide both a `process` method for executing the command and a `help` method
34/// for generating help documentation.
35///
36/// # Example
37///
38/// ```
39/// use falcon_cli::{CliCommand, CliRequest, CliHelpScreen};
40///
41/// struct MyCommand;
42///
43/// impl CliCommand for MyCommand {
44///     fn process(&self, req: &CliRequest) -> anyhow::Result<()> {
45///         println!("Executing command");
46///         Ok(())
47///     }
48///
49///     fn help(&self) -> CliHelpScreen {
50///         CliHelpScreen::new("My Command", "myapp mycommand", "Does something useful")
51///     }
52/// }
53/// ```
54pub trait CliCommand {
55    /// Processes the CLI command with the given request.
56    ///
57    /// # Arguments
58    ///
59    /// * `req` - The CLI request containing arguments, flags, and other parsed data
60    ///
61    /// # Returns
62    ///
63    /// Returns `Ok(())` on success or an error on failure.
64    fn process(&self, req: &CliRequest) -> anyhow::Result<()>;
65
66    /// Returns the help screen for this command.
67    ///
68    /// This method should create and return a `CliHelpScreen` with information
69    /// about how to use the command, including parameters, flags, and examples.
70    fn help(&self) -> CliHelpScreen;
71}
72
73/// Executes the CLI command router and processes the appropriate command.
74///
75/// This function should be called once all necessary routes have been defined via
76/// `router.add()`. It will parse command line arguments, look up the appropriate
77/// command handler, and execute it or display help as needed.
78///
79/// # Arguments
80///
81/// * `router` - A mutable reference to the configured CLI router
82///
83/// # Example
84///
85/// ```no_run
86/// use falcon_cli::{CliRouter, cli_run};
87///
88/// let mut router = CliRouter::new();
89/// router.app_name("My App");
90/// // Add commands here...
91/// cli_run(&mut router);
92/// ```
93pub fn cli_run(router: &mut CliRouter) {
94    // Lookup route
95    let (req, cmd) = match router.lookup() {
96        Some(r) => r,
97        None => {
98            CliHelpScreen::render_index(&router);
99            exit(0);
100        }
101    };
102
103    // Process as needed
104    if req.is_help {
105        CliHelpScreen::render(&cmd, &req.cmd_alias, &req.shortcuts);
106    } else if let Err(e) = cmd.process(&req) {
107        cli_send!("ERROR: {}\n", e);
108    }
109}
110
111/// Displays a formatted header in the terminal.
112///
113/// Outputs the given text with 30 dashes at the top and bottom to create a header section.
114///
115/// # Arguments
116///
117/// * `text` - The text to display in the header
118///
119/// # Example
120///
121/// ```
122/// use falcon_cli::cli_header;
123///
124/// cli_header("My Application");
125/// // Output:
126/// // ------------------------------
127/// // -- My Application
128/// // ------------------------------
129/// ```
130pub fn cli_header(text: &str) {
131    println!("------------------------------");
132    println!("-- {}", text);
133    println!("------------------------------\n");
134}
135
136/// Prompts the user to select an option from a list.
137///
138/// Displays a question and list of options, then waits for the user to select one.
139/// The function will continue prompting until a valid option is selected.
140///
141/// # Arguments
142///
143/// * `question` - The question or prompt to display
144/// * `options` - An `IndexMap` of options where keys are option identifiers and values are descriptions
145///
146/// # Returns
147///
148/// Returns the key of the selected option.
149///
150/// # Example
151///
152/// ```no_run
153/// use falcon_cli::{cli_get_option, indexmap};
154/// use indexmap::IndexMap;
155///
156/// let options = indexmap! {
157///     1 => "First option",
158///     2 => "Second option",
159///     3 => "Third option",
160/// };
161///
162/// let selected = cli_get_option("Which option do you prefer?", &options);
163/// println!("You selected: {}", selected);
164/// ```
165pub fn cli_get_option<K, V>(question: &str, options: &IndexMap<K, V>) -> K
166where
167    K: Display + Eq + PartialEq + Hash + FromStr,
168    <K as FromStr>::Err: Display,
169    V: Display,
170{
171    let message = format!("{}\n\n", question);
172    cli_send!(&message);
173    for (key, value) in options.iter() {
174        cli_send!(&format!("    [{}] {}\n", key, value));
175    }
176    cli_send!("\nSelect One: ");
177
178    // Get user input
179    let mut input: String;
180    loop {
181        input = String::new();
182
183        io::stdin().read_line(&mut input).expect("Failed to read line");
184        let input = input.trim();
185
186        if let Ok(value) = input.parse::<K>() {
187            if options.contains_key(&value) {
188                return value;
189            }
190        }
191
192        print!("\r\nInvalid option, try again: ");
193        io::stdout().flush().unwrap();
194    }
195}
196
197/// Gets text input from the user.
198///
199/// Displays a prompt message and waits for the user to enter text. If the user
200/// enters nothing, the default value is returned.
201///
202/// # Arguments
203///
204/// * `message` - The prompt message to display
205/// * `default_value` - The value to return if the user enters nothing
206///
207/// # Returns
208///
209/// Returns the user's input as a `String`, or the default value if no input was provided.
210///
211/// # Example
212///
213/// ```no_run
214/// use falcon_cli::cli_get_input;
215///
216/// let name = cli_get_input("Enter your name: ", "Anonymous");
217/// println!("Hello, {}!", name);
218/// ```
219pub fn cli_get_input(message: &str, default_value: &str) -> String {
220    // Display message
221    cli_send!(message);
222    io::stdout().flush().unwrap();
223
224    // Get user input
225    let mut input = String::new();
226    io::stdin().read_line(&mut input).expect("Failed to read line");
227    let mut input = input.trim();
228
229    // Default value, if needed
230    if input.trim().is_empty() {
231        input = default_value;
232    }
233
234    input.to_string()
235}
236
237/// Gets multi-line text input from the user.
238///
239/// Displays a prompt message and collects multiple lines of input from the user.
240/// Input collection stops when the user enters an empty line.
241///
242/// # Arguments
243///
244/// * `message` - The prompt message to display
245///
246/// # Returns
247///
248/// Returns all entered lines joined with newline characters as a single `String`.
249///
250/// # Example
251///
252/// ```no_run
253/// use falcon_cli::cli_get_multiline_input;
254///
255/// let description = cli_get_multiline_input("Enter description:");
256/// println!("You entered:\n{}", description);
257/// ```
258pub fn cli_get_multiline_input(message: &str) -> String {
259    // Display message
260    cli_send!(&format!("{} (empty line to stop)\n\n", message));
261    io::stdout().flush().unwrap();
262
263    // Get user input
264    let mut res: Vec<String> = Vec::new();
265    loop {
266        let mut input = String::new();
267        io::stdin().read_line(&mut input).expect("Failed to read line");
268        let input = input.trim();
269
270        if input.is_empty() {
271            break;
272        }
273        res.push(input.to_string());
274    }
275
276    res.join("\n").to_string()
277}
278
279/// Requests confirmation from the user.
280///
281/// Displays a message and prompts the user to answer yes (y) or no (n).
282/// The function will continue prompting until a valid response is received.
283///
284/// # Arguments
285///
286/// * `message` - The confirmation message to display
287///
288/// # Returns
289///
290/// Returns `true` if the user answered 'y', `false` if they answered 'n'.
291///
292/// # Example
293///
294/// ```no_run
295/// use falcon_cli::cli_confirm;
296///
297/// if cli_confirm("Do you want to continue?") {
298///     println!("Continuing...");
299/// } else {
300///     println!("Cancelled.");
301/// }
302/// ```
303pub fn cli_confirm(message: &str) -> bool {
304    // Send message
305    let confirm_message = format!("{} (y/n): ", message);
306    cli_send!(&confirm_message);
307
308    // Get user input
309    let mut _input = "".to_string();
310    loop {
311        _input = String::new();
312
313        io::stdin().read_line(&mut _input).expect("Failed to read line");
314        let _input = _input.trim().to_lowercase();
315
316        if _input != "y" && _input != "n" {
317            cli_send!("Invalid option, please try again.  Enter (y/n): ");
318        } else {
319            break;
320        }
321    }
322
323    // Return
324    let res_char = _input.chars().next().unwrap();
325
326    res_char == 'y'
327}
328
329/// Gets a password from the user without displaying the input on screen.
330///
331/// Prompts the user for a password with input hidden from the terminal.
332/// Optionally can require a non-empty password.
333///
334/// # Arguments
335///
336/// * `message` - The prompt message to display (defaults to "Password: " if empty)
337/// * `allow_blank` - Whether to allow an empty password
338///
339/// # Returns
340///
341/// Returns the entered password as a `String`.
342///
343/// # Example
344///
345/// ```no_run
346/// use falcon_cli::cli_get_password;
347///
348/// let password = cli_get_password("Enter password: ", false);
349/// println!("Password entered successfully");
350/// ```
351#[cfg(not(feature="mock"))]
352pub fn cli_get_password(message: &str, allow_blank: bool) -> String {
353    // Get message
354    let password_message = if message.is_empty() {
355        "Password: "
356    } else {
357        message
358    };
359
360    // Get password
361    let mut _password = String::new();
362    loop {
363        cli_send!(password_message);
364        _password = read_password().unwrap();
365
366        if _password.is_empty() && !allow_blank {
367            cli_send!("You did not specify a password");
368        } else {
369            break;
370        }
371    }
372
373    _password
374}
375
376#[cfg(feature="mock")]
377pub fn cli_get_password(message: &str, allow_blank: bool) -> String {
378    cli_get_input(message, if allow_blank { "" } else { "password" })
379}
380
381/// Gets a new password from the user with confirmation and strength validation.
382///
383/// Prompts the user to enter a password twice for confirmation and validates it
384/// against a required strength level using the zxcvbn algorithm. The function will
385/// continue prompting until a password meeting all requirements is entered.
386///
387/// # Arguments
388///
389/// * `req_strength` - Required password strength (0-4, where 4 is strongest)
390///   - 0: Too guessable
391///   - 1: Very guessable
392///   - 2: Somewhat guessable
393///   - 3: Safely unguessable
394///   - 4: Very unguessable
395///
396/// # Returns
397///
398/// Returns the validated password as a `String`.
399///
400/// # Example
401///
402/// ```no_run
403/// use falcon_cli::cli_get_new_password;
404///
405/// // Require a password with strength level 3
406/// let password = cli_get_new_password(3);
407/// println!("Strong password created successfully");
408/// ```
409#[cfg(not(feature = "mock"))]
410pub fn cli_get_new_password(req_strength: u8) -> String {
411println!("Nope, at the prod one");
412    // Initialize
413    let mut _password = String::new();
414    let mut _confirm_password = String::new();
415
416    // Get new password
417    loop {
418        cli_send!("Desired Password: ");
419        _password = read_password().unwrap();
420
421        if _password.is_empty() {
422            cli_send!("You did not specify a password");
423            continue;
424        }
425
426        // Check strength
427        let strength = zxcvbn(&_password, &[]).unwrap();
428        if strength.score() < req_strength {
429            cli_send!("Password is not strong enough.  Please try again.\n\n");
430            continue;
431        }
432
433        // Confirm password
434        cli_send!("Confirm Password: ");
435        _confirm_password = read_password().unwrap();
436        if _password != _confirm_password {
437            cli_send!("Passwords do not match, please try again.\n\n");
438            continue;
439        }
440        break;
441    }
442
443    _password
444}
445
446#[cfg(feature = "mock")]
447pub fn cli_get_new_password(req_strength: u8) -> String {
448println!("Yes at the testing one");
449    // Initialize
450    let mut _password = String::new();
451    let mut _confirm_password = String::new();
452
453    // Get new password
454    loop {
455        _password = cli_get_input("Desired Password: ", "");
456
457        if _password.is_empty() {
458            cli_send!("You did not specify a password");
459            continue;
460        }
461
462        // Check strength
463        let strength = zxcvbn(&_password, &[]).unwrap();
464        if strength.score() < req_strength {
465            cli_send!("Password is not strong enough.  Please try again.\n\n");
466            continue;
467        }
468
469        // Confirm password
470        _confirm_password = cli_get_input("Confirm Password: ", "");
471        if _password != _confirm_password {
472            cli_send!("Passwords do not match, please try again.\n\n");
473            continue;
474        }
475        break;
476    }
477
478    _password
479}
480
481/// Displays data in a formatted table.
482///
483/// Renders data in a tabular format similar to SQL database output, with borders
484/// and properly aligned columns. Column widths are automatically calculated based
485/// on the content.
486///
487/// # Arguments
488///
489/// * `columns` - Slice of column headers
490/// * `rows` - Slice of rows, where each row is a vector of cell values
491///
492/// # Example
493///
494/// ```no_run
495/// use falcon_cli::cli_display_table;
496///
497/// let columns = ["Name", "Age", "City"];
498/// let rows = vec![
499///     vec!["Alice", "30", "New York"],
500///     vec!["Bob", "25", "London"],
501///     vec!["Charlie", "35", "Tokyo"],
502/// ];
503///
504/// cli_display_table(&columns, &rows);
505/// ```
506pub fn cli_display_table<C: Display, R: Display>(columns: &[C], rows: &[Vec<R>]) {
507    // Return if no rows
508    if rows.is_empty() {
509        println!("No rows to display.\n");
510        return;
511    }
512
513    // Initialize sizes - using index-based approach since we can't use T as HashMap key
514    let mut sizes: Vec<usize> = vec![0; columns.len()];
515
516    // Get sizes of column headers
517    for (i, col) in columns.iter().enumerate() {
518        let col_str = col.to_string();
519        sizes[i] = col_str.len();
520    }
521
522    // Get maximum sizes by checking all row values
523    for row in rows {
524        for (i, val) in row.iter().enumerate() {
525            if i < sizes.len() {
526                let val_str = val.to_string();
527                let val_len = val_str.len();
528                if val_len > sizes[i] {
529                    sizes[i] = val_len;
530                }
531            }
532        }
533    }
534
535    // Add padding to all column sizes
536    for size in sizes.iter_mut() {
537        *size += 3;
538    }
539
540    // Initialize header variables
541    let mut header = String::from("+");
542    let mut col_header = String::from("|");
543
544    // Print column headers
545    for (i, col) in columns.iter().enumerate() {
546        let col_str = col.to_string();
547        let padded_col = format!("{}{}", col_str, " ".repeat(sizes[i] - col_str.len()));
548        header += &("-".repeat(sizes[i] + 1) + "+");
549        col_header += &format!(" {}|", padded_col);
550    }
551
552    println!("{}\n{}\n{}", header, col_header, header);
553
554    // Display the rows
555    for row in rows {
556        let mut line = String::from("|");
557        for (i, val) in row.iter().enumerate() {
558            if i < sizes.len() {
559                let val_str = val.to_string();
560                let padded_val = format!(" {}{}", val_str, " ".repeat(sizes[i] - val_str.len()));
561                line += &format!("{}|", padded_val);
562            }
563        }
564        println!("{}", line);
565    }
566    println!("{}\n", header);
567}
568
569/// Displays a two-column array with proper spacing and word wrapping.
570///
571/// Formats and displays key-value pairs in two columns with automatic text wrapping.
572/// This function is primarily used by the help system to display parameters and flags,
573/// but can be used for any two-column data display.
574///
575/// # Arguments
576///
577/// * `rows` - An `IndexMap` where keys are displayed in the left column and values in the right
578///
579/// # Example
580///
581/// ```no_run
582/// use falcon_cli::{cli_display_array, indexmap};
583/// use indexmap::IndexMap;
584///
585/// let mut items = indexmap! {
586///     "--verbose" => "Enable verbose output with detailed logging",
587///     "--output" => "Specify the output file path",
588///     "--help" => "Display this help message",
589/// };
590///
591/// cli_display_array(&items);
592/// ```
593pub fn cli_display_array<K: Display, V: Display>(rows: &IndexMap<K, V>) {
594    // Get max left column size
595    let mut size = 0;
596    for key in rows.keys() {
597        let key_str = key.to_string();
598        if key_str.len() + 8 > size {
599            size = key_str.len() + 8;
600        }
601    }
602    let indent = " ".repeat(size);
603    let indent_size = size - 4;
604
605    // Go through rows
606    for (key, value) in rows {
607        let key_str = key.to_string();
608        let value_str = value.to_string();
609        let left_col = format!("    {}{}", key_str, " ".repeat(indent_size - key_str.len()));
610        let options =
611            textwrap::Options::new(75).initial_indent(&left_col).subsequent_indent(&indent);
612        let line = textwrap::fill(&value_str, &options);
613        println!("{}", line);
614    }
615    println!("");
616}
617
618/// Clears the terminal screen.
619///
620/// Sends the ANSI escape sequence to clear all lines and reset the cursor position.
621///
622/// # Example
623///
624/// ```no_run
625/// use falcon_cli::cli_clear_screen;
626///
627/// cli_clear_screen();
628/// println!("Screen cleared!");
629/// ```
630pub fn cli_clear_screen() {
631    print!("\x1B[2J");
632}
633
634/// Opens a text editor for the user to edit content.
635///
636/// Creates a temporary file with the provided contents, opens it in the user's
637/// preferred text editor, and returns the edited content. The editor used is
638/// determined by the `VISUAL` or `EDITOR` environment variables, with sensible
639/// defaults for each platform.
640///
641/// # Arguments
642///
643/// * `contents` - The initial content to populate the editor with
644///
645/// # Returns
646///
647/// Returns `Ok(String)` with the edited content on success, or a `CliError` if
648/// the editor fails to launch or exits with an error.
649///
650/// # Example
651///
652/// ```no_run
653/// use falcon_cli::cli_text_editor;
654///
655/// let initial = "Edit this text...";
656/// match cli_text_editor(initial) {
657///     Ok(edited) => println!("New content: {}", edited),
658///     Err(e) => eprintln!("Error: {}", e),
659/// }
660/// ```
661pub fn cli_text_editor(contents: &str) -> Result<String, CliError> {
662    // Create temp file
663    let temp_dir = env::temp_dir();
664    let temp_file = temp_dir.join(format!(
665        "cli_edit_{}.tmp",
666        std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis()
667    ));
668
669    // Write initial contents to temp file
670    fs::write(&temp_file, contents)
671        .map_err(|e| CliError::Generic(format!("Failed to create temp file: {}", e)))?;
672
673    // Get editor command
674    let editor = get_editor();
675
676    // Launch editor
677    let status = if cfg!(target_os = "windows") {
678        Command::new("cmd")
679            .args(&["/C", &format!("{} \"{}\"", editor, temp_file.display())])
680            .status()
681    } else {
682        Command::new(&editor).arg(&temp_file).status()
683    };
684
685    match status {
686        Ok(exit_status) if exit_status.success() => {
687            // Read the file contents
688            let result = fs::read_to_string(&temp_file).unwrap_or_else(|_| String::new());
689
690            // Delete temp file
691            let _ = fs::remove_file(&temp_file);
692
693            Ok(result)
694        }
695        Ok(_) => {
696            let _ = fs::remove_file(&temp_file);
697            Err(CliError::Generic("Editor exited with error".to_string()))
698        }
699        Err(e) => {
700            let _ = fs::remove_file(&temp_file);
701            Err(CliError::Generic(format!("Failed to launch editor: {}", e)))
702        }
703    }
704}
705
706/// Determines the text editor to use based on environment variables and platform.
707///
708/// Checks environment variables in order of preference (`VISUAL`, then `EDITOR`),
709/// falling back to platform-specific defaults if neither is set.
710fn get_editor() -> String {
711    // Check environment variables in order of preference
712    if let Ok(editor) = env::var("VISUAL") {
713        return editor;
714    }
715    if let Ok(editor) = env::var("EDITOR") {
716        return editor;
717    }
718
719    // Platform-specific defaults
720    if cfg!(target_os = "windows") {
721        // Try notepad++ first, fall back to notepad
722        if Command::new("notepad++").arg("--version").output().is_ok() {
723            "notepad++".to_string()
724        } else {
725            "notepad".to_string()
726        }
727    } else if cfg!(target_os = "macos") {
728        // macOS - try nano first (comes default), then vim
729        if Command::new("which").arg("nano").output().is_ok() {
730            "nano".to_string()
731        } else {
732            "vim".to_string()
733        }
734    } else {
735        // Linux/Unix - try in order of user-friendliness
736        for editor in &["nano", "vim", "vi"] {
737            if Command::new("which")
738                .arg(editor)
739                .output()
740                .map(|o| o.status.success())
741                .unwrap_or(false)
742            {
743                return editor.to_string();
744            }
745        }
746        "vi".to_string() // Last resort, should always exist on Unix
747    }
748}
749
750/// Creates and displays a new progress bar.
751///
752/// Initializes a progress bar with the specified message and total value,
753/// and immediately renders it at 0% completion.
754///
755/// # Arguments
756///
757/// * `message` - The message to display alongside the progress bar
758/// * `total` - The total value representing 100% completion
759///
760/// # Returns
761///
762/// Returns a `CliProgressBar` instance that can be updated with `increment()` or `set()`.
763///
764/// # Example
765///
766/// ```no_run
767/// use falcon_cli::cli_progress_bar;
768///
769/// let mut bar = cli_progress_bar("Processing files", 100);
770/// for i in 0..100 {
771///     // Do work...
772///     bar.increment(1);
773/// }
774/// bar.finish();
775/// ```
776pub fn cli_progress_bar(message: &str, total: usize) -> CliProgressBar {
777    let bar = CliProgressBar {
778        value: 0,
779        total,
780        message: message.to_string(),
781    };
782    bar.start();
783    bar
784}
785
786/// A progress bar for displaying task completion in the terminal.
787///
788/// This struct maintains the state of a progress bar and provides methods
789/// to update and render it. The bar displays percentage, a message, and
790/// a visual indicator of progress.
791pub struct CliProgressBar {
792    /// Current value of progress (0 to total).
793    pub value: usize,
794    /// Total value representing 100% completion.
795    pub total: usize,
796    /// Message displayed alongside the progress bar.
797    pub message: String,
798}
799
800impl CliProgressBar {
801    /// Initializes and displays the progress bar.
802    ///
803    /// Renders the progress bar on a new line at its initial state (0%).
804    pub fn start(&self) {
805        self.render();
806    }
807
808    /// Increments the progress value and updates the display.
809    ///
810    /// # Arguments
811    ///
812    /// * `num` - The amount to increment the progress by
813    ///
814    /// # Example
815    ///
816    /// ```no_run
817    /// # use falcon_cli::cli_progress_bar;
818    /// let mut bar = cli_progress_bar("Processing", 100);
819    /// bar.increment(10); // Progress is now at 10%
820    /// bar.increment(15); // Progress is now at 25%
821    /// ```
822    pub fn increment(&mut self, num: usize) {
823        self.value = self.value.saturating_add(num).min(self.total);
824        self.render();
825    }
826
827    /// Sets the progress to a specific value and updates the display.
828    ///
829    /// # Arguments
830    ///
831    /// * `value` - The new progress value (clamped to `total`)
832    ///
833    /// # Example
834    ///
835    /// ```no_run
836    /// # use falcon_cli::cli_progress_bar;
837    /// let mut bar = cli_progress_bar("Processing", 100);
838    /// bar.set(50); // Set progress to 50%
839    /// ```
840    pub fn set(&mut self, value: usize) {
841        self.value = value.min(self.total);
842        self.render();
843    }
844
845    /// Completes the progress bar.
846    ///
847    /// Sets the progress to 100%, renders the final state, and moves to a new line.
848    ///
849    /// # Example
850    ///
851    /// ```no_run
852    /// # use falcon_cli::cli_progress_bar;
853    /// let mut bar = cli_progress_bar("Processing", 100);
854    /// // ... do work ...
855    /// bar.finish();
856    /// ```
857    pub fn finish(&mut self) {
858        self.value = self.total;
859        self.render();
860        println!("");
861    }
862
863    /// Renders the progress bar to the terminal.
864    ///
865    /// Internal method that calculates and displays the progress bar with
866    /// percentage, message, and visual indicator.
867    fn render(&self) {
868        let percent = if self.total > 0 {
869            (self.value * 100) / self.total
870        } else {
871            0
872        };
873
874        // Calculate available space
875        // Format: [ xx% ] <MESSAGE> [******      ]
876        // Fixed parts: "[ ", "% ] ", " [", "]" = 8 chars
877        // Percent: 1-3 chars (0-100)
878        let percent_str = format!("{}", percent);
879        let fixed_overhead = 8 + percent_str.len();
880
881        // Available space for message and bar
882        let available = 75_usize.saturating_sub(fixed_overhead);
883
884        // Reserve minimum 10 chars for bar (including brackets)
885        let bar_size = 10;
886        let message_max = available.saturating_sub(bar_size);
887
888        // Truncate message if needed
889        let display_message = if self.message.len() > message_max {
890            format!("{}...", &self.message[..message_max.saturating_sub(3)])
891        } else {
892            self.message.clone()
893        };
894
895        // Calculate actual bar width (inner width without brackets)
896        let bar_width = available.saturating_sub(display_message.len()).max(8);
897        let filled = (bar_width * self.value) / self.total.max(1);
898        let empty = bar_width.saturating_sub(filled);
899
900        // Build the bar
901        let bar = format!("{}{}", "*".repeat(filled), " ".repeat(empty));
902
903        // Print with carriage return to overwrite line
904        print!("\r[ {}% ] {} [{}]", percent, display_message, bar);
905        io::stdout().flush().unwrap();
906
907        // Print newline when complete
908        if self.value >= self.total {
909            println!();
910        }
911    }
912}