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 {
411    // Initialize
412    let mut _password = String::new();
413    let mut _confirm_password = String::new();
414
415    // Get new password
416    loop {
417        cli_send!("Desired Password: ");
418        _password = read_password().unwrap();
419
420        if _password.is_empty() {
421            cli_send!("You did not specify a password");
422            continue;
423        }
424
425        // Check strength
426        let strength = zxcvbn(&_password, &[]).unwrap();
427        if strength.score() < req_strength {
428            cli_send!("Password is not strong enough.  Please try again.\n\n");
429            continue;
430        }
431
432        // Confirm password
433        cli_send!("Confirm Password: ");
434        _confirm_password = read_password().unwrap();
435        if _password != _confirm_password {
436            cli_send!("Passwords do not match, please try again.\n\n");
437            continue;
438        }
439        break;
440    }
441
442    _password
443}
444
445#[cfg(feature = "mock")]
446pub fn cli_get_new_password(req_strength: u8) -> String {
447    // Initialize
448    let mut _password = String::new();
449    let mut _confirm_password = String::new();
450
451    // Get new password
452    loop {
453        _password = cli_get_input("Desired Password: ", "");
454
455        if _password.is_empty() {
456            cli_send!("You did not specify a password");
457            continue;
458        }
459
460        // Check strength
461        let strength = zxcvbn(&_password, &[]).unwrap();
462        if strength.score() < req_strength {
463            cli_send!("Password is not strong enough.  Please try again.\n\n");
464            continue;
465        }
466
467        // Confirm password
468        _confirm_password = cli_get_input("Confirm Password: ", "");
469        if _password != _confirm_password {
470            cli_send!("Passwords do not match, please try again.\n\n");
471            continue;
472        }
473        break;
474    }
475
476    _password
477}
478
479/// Displays data in a formatted table.
480///
481/// Renders data in a tabular format similar to SQL database output, with borders
482/// and properly aligned columns. Column widths are automatically calculated based
483/// on the content.
484///
485/// # Arguments
486///
487/// * `columns` - Slice of column headers
488/// * `rows` - Slice of rows, where each row is a vector of cell values
489///
490/// # Example
491///
492/// ```no_run
493/// use falcon_cli::cli_display_table;
494///
495/// let columns = ["Name", "Age", "City"];
496/// let rows = vec![
497///     vec!["Alice", "30", "New York"],
498///     vec!["Bob", "25", "London"],
499///     vec!["Charlie", "35", "Tokyo"],
500/// ];
501///
502/// cli_display_table(&columns, &rows);
503/// ```
504pub fn cli_display_table<C: Display, R: Display>(columns: &[C], rows: &[Vec<R>]) {
505    // Return if no rows
506    if rows.is_empty() {
507        println!("No rows to display.\n");
508        return;
509    }
510
511    // Initialize sizes - using index-based approach since we can't use T as HashMap key
512    let mut sizes: Vec<usize> = vec![0; columns.len()];
513
514    // Get sizes of column headers
515    for (i, col) in columns.iter().enumerate() {
516        let col_str = col.to_string();
517        sizes[i] = col_str.len();
518    }
519
520    // Get maximum sizes by checking all row values
521    for row in rows {
522        for (i, val) in row.iter().enumerate() {
523            if i < sizes.len() {
524                let val_str = val.to_string();
525                let val_len = val_str.len();
526                if val_len > sizes[i] {
527                    sizes[i] = val_len;
528                }
529            }
530        }
531    }
532
533    // Add padding to all column sizes
534    for size in sizes.iter_mut() {
535        *size += 3;
536    }
537
538    // Initialize header variables
539    let mut header = String::from("+");
540    let mut col_header = String::from("|");
541
542    // Print column headers
543    for (i, col) in columns.iter().enumerate() {
544        let col_str = col.to_string();
545        let padded_col = format!("{}{}", col_str, " ".repeat(sizes[i] - col_str.len()));
546        header += &("-".repeat(sizes[i] + 1) + "+");
547        col_header += &format!(" {}|", padded_col);
548    }
549
550    println!("{}\n{}\n{}", header, col_header, header);
551
552    // Display the rows
553    for row in rows {
554        let mut line = String::from("|");
555        for (i, val) in row.iter().enumerate() {
556            if i < sizes.len() {
557                let val_str = val.to_string();
558                let padded_val = format!(" {}{}", val_str, " ".repeat(sizes[i] - val_str.len()));
559                line += &format!("{}|", padded_val);
560            }
561        }
562        println!("{}", line);
563    }
564    println!("{}\n", header);
565}
566
567/// Displays a two-column array with proper spacing and word wrapping.
568///
569/// Formats and displays key-value pairs in two columns with automatic text wrapping.
570/// This function is primarily used by the help system to display parameters and flags,
571/// but can be used for any two-column data display.
572///
573/// # Arguments
574///
575/// * `rows` - An `IndexMap` where keys are displayed in the left column and values in the right
576///
577/// # Example
578///
579/// ```no_run
580/// use falcon_cli::{cli_display_array, indexmap};
581/// use indexmap::IndexMap;
582///
583/// let mut items = indexmap! {
584///     "--verbose" => "Enable verbose output with detailed logging",
585///     "--output" => "Specify the output file path",
586///     "--help" => "Display this help message",
587/// };
588///
589/// cli_display_array(&items);
590/// ```
591pub fn cli_display_array<K: Display, V: Display>(rows: &IndexMap<K, V>) {
592    // Get max left column size
593    let mut size = 0;
594    for key in rows.keys() {
595        let key_str = key.to_string();
596        if key_str.len() + 8 > size {
597            size = key_str.len() + 8;
598        }
599    }
600    let indent = " ".repeat(size);
601    let indent_size = size - 4;
602
603    // Go through rows
604    for (key, value) in rows {
605        let key_str = key.to_string();
606        let value_str = value.to_string();
607        let left_col = format!("    {}{}", key_str, " ".repeat(indent_size - key_str.len()));
608        let options =
609            textwrap::Options::new(75).initial_indent(&left_col).subsequent_indent(&indent);
610        let line = textwrap::fill(&value_str, &options);
611        println!("{}", line);
612    }
613    println!("");
614}
615
616/// Clears the terminal screen.
617///
618/// Sends the ANSI escape sequence to clear all lines and reset the cursor position.
619///
620/// # Example
621///
622/// ```no_run
623/// use falcon_cli::cli_clear_screen;
624///
625/// cli_clear_screen();
626/// println!("Screen cleared!");
627/// ```
628pub fn cli_clear_screen() {
629    print!("\x1B[2J");
630}
631
632/// Opens a text editor for the user to edit content.
633///
634/// Creates a temporary file with the provided contents, opens it in the user's
635/// preferred text editor, and returns the edited content. The editor used is
636/// determined by the `VISUAL` or `EDITOR` environment variables, with sensible
637/// defaults for each platform.
638///
639/// # Arguments
640///
641/// * `contents` - The initial content to populate the editor with
642///
643/// # Returns
644///
645/// Returns `Ok(String)` with the edited content on success, or a `CliError` if
646/// the editor fails to launch or exits with an error.
647///
648/// # Example
649///
650/// ```no_run
651/// use falcon_cli::cli_text_editor;
652///
653/// let initial = "Edit this text...";
654/// match cli_text_editor(initial) {
655///     Ok(edited) => println!("New content: {}", edited),
656///     Err(e) => eprintln!("Error: {}", e),
657/// }
658/// ```
659pub fn cli_text_editor(contents: &str) -> Result<String, CliError> {
660    // Create temp file
661    let temp_dir = env::temp_dir();
662    let temp_file = temp_dir.join(format!(
663        "cli_edit_{}.tmp",
664        std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis()
665    ));
666
667    // Write initial contents to temp file
668    fs::write(&temp_file, contents)
669        .map_err(|e| CliError::Generic(format!("Failed to create temp file: {}", e)))?;
670
671    // Get editor command
672    let editor = get_editor();
673
674    // Launch editor
675    let status = if cfg!(target_os = "windows") {
676        Command::new("cmd")
677            .args(&["/C", &format!("{} \"{}\"", editor, temp_file.display())])
678            .status()
679    } else {
680        Command::new(&editor).arg(&temp_file).status()
681    };
682
683    match status {
684        Ok(exit_status) if exit_status.success() => {
685            // Read the file contents
686            let result = fs::read_to_string(&temp_file).unwrap_or_else(|_| String::new());
687
688            // Delete temp file
689            let _ = fs::remove_file(&temp_file);
690
691            Ok(result)
692        }
693        Ok(_) => {
694            let _ = fs::remove_file(&temp_file);
695            Err(CliError::Generic("Editor exited with error".to_string()))
696        }
697        Err(e) => {
698            let _ = fs::remove_file(&temp_file);
699            Err(CliError::Generic(format!("Failed to launch editor: {}", e)))
700        }
701    }
702}
703
704/// Determines the text editor to use based on environment variables and platform.
705///
706/// Checks environment variables in order of preference (`VISUAL`, then `EDITOR`),
707/// falling back to platform-specific defaults if neither is set.
708fn get_editor() -> String {
709    // Check environment variables in order of preference
710    if let Ok(editor) = env::var("VISUAL") {
711        return editor;
712    }
713    if let Ok(editor) = env::var("EDITOR") {
714        return editor;
715    }
716
717    // Platform-specific defaults
718    if cfg!(target_os = "windows") {
719        // Try notepad++ first, fall back to notepad
720        if Command::new("notepad++").arg("--version").output().is_ok() {
721            "notepad++".to_string()
722        } else {
723            "notepad".to_string()
724        }
725    } else if cfg!(target_os = "macos") {
726        // macOS - try nano first (comes default), then vim
727        if Command::new("which").arg("nano").output().is_ok() {
728            "nano".to_string()
729        } else {
730            "vim".to_string()
731        }
732    } else {
733        // Linux/Unix - try in order of user-friendliness
734        for editor in &["nano", "vim", "vi"] {
735            if Command::new("which")
736                .arg(editor)
737                .output()
738                .map(|o| o.status.success())
739                .unwrap_or(false)
740            {
741                return editor.to_string();
742            }
743        }
744        "vi".to_string() // Last resort, should always exist on Unix
745    }
746}
747
748/// Creates and displays a new progress bar.
749///
750/// Initializes a progress bar with the specified message and total value,
751/// and immediately renders it at 0% completion.
752///
753/// # Arguments
754///
755/// * `message` - The message to display alongside the progress bar
756/// * `total` - The total value representing 100% completion
757///
758/// # Returns
759///
760/// Returns a `CliProgressBar` instance that can be updated with `increment()` or `set()`.
761///
762/// # Example
763///
764/// ```no_run
765/// use falcon_cli::cli_progress_bar;
766///
767/// let mut bar = cli_progress_bar("Processing files", 100);
768/// for i in 0..100 {
769///     // Do work...
770///     bar.increment(1);
771/// }
772/// bar.finish();
773/// ```
774pub fn cli_progress_bar(message: &str, total: usize) -> CliProgressBar {
775    let bar = CliProgressBar {
776        value: 0,
777        total,
778        message: message.to_string(),
779    };
780    bar.start();
781    bar
782}
783
784/// A progress bar for displaying task completion in the terminal.
785///
786/// This struct maintains the state of a progress bar and provides methods
787/// to update and render it. The bar displays percentage, a message, and
788/// a visual indicator of progress.
789pub struct CliProgressBar {
790    /// Current value of progress (0 to total).
791    pub value: usize,
792    /// Total value representing 100% completion.
793    pub total: usize,
794    /// Message displayed alongside the progress bar.
795    pub message: String,
796}
797
798impl CliProgressBar {
799    /// Initializes and displays the progress bar.
800    ///
801    /// Renders the progress bar on a new line at its initial state (0%).
802    pub fn start(&self) {
803        self.render();
804    }
805
806    /// Increments the progress value and updates the display.
807    ///
808    /// # Arguments
809    ///
810    /// * `num` - The amount to increment the progress by
811    ///
812    /// # Example
813    ///
814    /// ```no_run
815    /// # use falcon_cli::cli_progress_bar;
816    /// let mut bar = cli_progress_bar("Processing", 100);
817    /// bar.increment(10); // Progress is now at 10%
818    /// bar.increment(15); // Progress is now at 25%
819    /// ```
820    pub fn increment(&mut self, num: usize) {
821        self.value = self.value.saturating_add(num).min(self.total);
822        self.render();
823    }
824
825    /// Sets the progress to a specific value and updates the display.
826    ///
827    /// # Arguments
828    ///
829    /// * `value` - The new progress value (clamped to `total`)
830    ///
831    /// # Example
832    ///
833    /// ```no_run
834    /// # use falcon_cli::cli_progress_bar;
835    /// let mut bar = cli_progress_bar("Processing", 100);
836    /// bar.set(50); // Set progress to 50%
837    /// ```
838    pub fn set(&mut self, value: usize) {
839        self.value = value.min(self.total);
840        self.render();
841    }
842
843    /// Completes the progress bar.
844    ///
845    /// Sets the progress to 100%, renders the final state, and moves to a new line.
846    ///
847    /// # Example
848    ///
849    /// ```no_run
850    /// # use falcon_cli::cli_progress_bar;
851    /// let mut bar = cli_progress_bar("Processing", 100);
852    /// // ... do work ...
853    /// bar.finish();
854    /// ```
855    pub fn finish(&mut self) {
856        self.value = self.total;
857        self.render();
858        println!("");
859    }
860
861    /// Renders the progress bar to the terminal.
862    ///
863    /// Internal method that calculates and displays the progress bar with
864    /// percentage, message, and visual indicator.
865    fn render(&self) {
866        let percent = if self.total > 0 {
867            (self.value * 100) / self.total
868        } else {
869            0
870        };
871
872        // Calculate available space
873        // Format: [ xx% ] <MESSAGE> [******      ]
874        // Fixed parts: "[ ", "% ] ", " [", "]" = 8 chars
875        // Percent: 1-3 chars (0-100)
876        let percent_str = format!("{}", percent);
877        let fixed_overhead = 8 + percent_str.len();
878
879        // Available space for message and bar
880        let available = 75_usize.saturating_sub(fixed_overhead);
881
882        // Reserve minimum 10 chars for bar (including brackets)
883        let bar_size = 10;
884        let message_max = available.saturating_sub(bar_size);
885
886        // Truncate message if needed
887        let display_message = if self.message.len() > message_max {
888            format!("{}...", &self.message[..message_max.saturating_sub(3)])
889        } else {
890            self.message.clone()
891        };
892
893        // Calculate actual bar width (inner width without brackets)
894        let bar_width = available.saturating_sub(display_message.len()).max(8);
895        let filled = (bar_width * self.value) / self.total.max(1);
896        let empty = bar_width.saturating_sub(filled);
897
898        // Build the bar
899        let bar = format!("{}{}", "*".repeat(filled), " ".repeat(empty));
900
901        // Print with carriage return to overwrite line
902        print!("\r[ {}% ] {} [{}]", percent, display_message, bar);
903        io::stdout().flush().unwrap();
904
905        // Print newline when complete
906        if self.value >= self.total {
907            println!();
908        }
909    }
910}