borderline 1.0.0

A simple crate to use borders in your terminal.
Documentation
use std::io;
use std::io::Write;

// How big should the fence be?
const LEN: usize = 66;

// How much should the text be indented? Applies to lines, not the header.
const INNER_INDENT: usize = 1;

/// Prints a header.
pub fn print_header(text: &str) {

    /// Left-aligns a string with spaces.
    fn left_aligned_string(s: &str, total_len: usize) -> String {
        let text_len = s.chars().count();
        if text_len >= total_len {
            return s.to_string();
        }

        let padding = total_len - text_len;
        format!("{}{}", s, " ".repeat(padding))
    }

    // Top border:
    print_border();

    //
    // Text processing:
    //
    let mut remaining = text.to_string();
    while !remaining.is_empty() {

        // Fits the remaining text on one line?
        if remaining.chars().count() <= LEN - 4 {
            // Yes, print it:
            println!("| {} |", left_aligned_string(&remaining, LEN - 4));
            break;
        } else {
            // No, we need to split the text into multiple lines.
            // We want to split at word boundaries, though.
            let mut line = String::from(&remaining[..LEN-4]);
            if let Some(last_space) = line.rfind(' ') {
                line.truncate(last_space);
                remaining = remaining[last_space..].trim_start().to_string();
            } else {
                line = String::from(&remaining[..LEN-4]);
                remaining = remaining[LEN-4..].trim_start().to_string();
            }

            println!("| {} |", left_aligned_string(&line, LEN - 4));
        }
    }

    // Bottom border:
    print_border();
}

/// A helper struct to build lines according to the fence.
pub struct LineBuilder {
    /// The current line.
    current_line: String,

    /// The current line prompt, when rendering a progress bar.
    current_line_prompt: String,

    /// The total number of steps in the progress bar.
    pb_num_steps: usize,

    /// The number of characters available for the progress bar.
    pb_chars_available: usize,

    /// The number of steps per character in the progress bar.
    pb_steps_per_char: f64,
}

/// Default implementation for LineBuilder.
impl Default for LineBuilder {
    fn default() -> Self {
        Self::new()
    }
}

/// Implementation for LineBuilder.
impl LineBuilder {
    /// Creates a new LineBuilder.
    #[allow(clippy::repeat_once)]
    pub fn new() -> Self {
        Self {
            current_line: " ".repeat(INNER_INDENT),
            current_line_prompt: " ".repeat(INNER_INDENT),
            pb_num_steps: 0,
            pb_steps_per_char: 0.0,
            pb_chars_available: 0,
        }
    }

    /// Starts a new line. When showing a progress bar, the text will be used as a prompt.
    #[allow(clippy::repeat_once)]
    pub fn line_begin(&mut self, text: &str) {
        // Reset the state:
        self.current_line.clear();
        self.current_line_prompt.clear();

        let inner_indent = " ".repeat(INNER_INDENT);
        self.current_line.push_str(&inner_indent);
        self.current_line_prompt.push_str(&inner_indent);

        self.current_line.push_str(text);
        self.current_line_prompt.push_str(text);
        self.print_and_clear(false);
        io::stdout().flush().unwrap();
    }

    /// Ends the current line. After calling this, a progress bar cannot be shown anymore.
    pub fn line_end(&mut self, text: &str) {
        print!("\r"); // Go back to the beginning of the line
        self.current_line.push(' ');
        self.current_line.push_str(text);
        self.print_and_clear(true);
    }

    /// Starts a progress bar with the given number of total steps. You must start a new line
    /// before calling this.
    pub fn progress_bar_setup(&mut self, num_steps: usize) {
        print!("\r"); // Go back to the beginning of the line

        // Determine how many characters we have left for the progress bar:
        self.pb_chars_available = LEN - 4 - 4 - self.current_line_prompt.chars().count();
        self.pb_num_steps = num_steps;

        // Determine how many steps we can fit into one character:
        if self.pb_chars_available >= self.pb_num_steps {
            self.pb_steps_per_char = self.pb_chars_available as f64 / num_steps as f64;
        } else {
            self.pb_steps_per_char = num_steps as f64 / self.pb_chars_available as f64;
        }

        // Print the prompt:
        self.current_line.clear();
        self.current_line.push_str(self.current_line_prompt.as_str());

        // Print the progress bar:
        self.current_line.push(' ');
        self.current_line.push('>');
        self.current_line.push_str(&".".repeat(self.pb_chars_available));

        self.print_and_clear(false);
        io::stdout().flush().unwrap();
    }

    /// Updates the progress bar with the given current step.
    pub fn progress_bar_update(&mut self, current_step: usize) {

        // Ensure we don't go over the number of steps:
        let current_step = if current_step > self.pb_num_steps {
            self.pb_num_steps
        } else {
            current_step
        };

        // Determine how many characters we need to use to show the passed steps:
        let num_filled = if self.pb_chars_available >= self.pb_num_steps {
            self.pb_steps_per_char.floor() as usize * current_step
        } else {
            current_step / self.pb_steps_per_char as usize
        };

        // Ensure we don't go over the number of available characters:
        let len_bar = if num_filled >= self.pb_chars_available {
            self.pb_chars_available - 1
        } else {
            num_filled
        };

        // Build the progress bar:
        print!("\r"); // Go back to the beginning of the line
        self.current_line.clear();

        // Print the prompt:
        self.current_line.push_str(self.current_line_prompt.as_str());

        // Print the progress bar:
        self.current_line.push(' ');
        self.current_line.push_str(&"#".repeat(len_bar));
        self.current_line.push('>');

        // Print the remaining characters for the future progress area:
        if len_bar < self.pb_chars_available {
            self.current_line.push_str(&".".repeat(self.pb_chars_available - len_bar));
        }

        // Finish the line:
        self.current_line.push(' ');
        self.print_and_clear(false);
        io::stdout().flush().unwrap();
    }

    /// Ends the progress bar. After this call, you cannot update the progress bar anymore.
    /// You must end the current line, though.
    pub fn progressbar_end(&mut self) {
        print!("\r"); // Go back to the beginning of the line
        self.current_line.clear();
        self.current_line.push_str(self.current_line_prompt.as_str());
        self.print_and_clear(false);
        io::stdout().flush().unwrap();
    }

    /// Prints the current line and clears it.
    fn print_and_clear(&mut self, finalize: bool) {
        let mut line_break_occurred = false; // Track if we had a line break

        while !self.current_line.is_empty() {
            if self.current_line.chars().count() <= LEN - 4 {
                if finalize {
                    println!("| {} |", format_line(&self.current_line, LEN - 4));
                    self.current_line.clear();
                    self.current_line_prompt.clear();
                } else {
                    print!("| {} |", format_line(&self.current_line, LEN - 4));
                }

                break; // Break when not finalizing

            } else {

                let mut line = String::from(&self.current_line[..LEN-4]);
                let original_line = self.current_line.clone();

                if let Some(last_space) = line.rfind(' ') {
                    line.truncate(last_space);
                    self.current_line = format!("{}{}", " ".repeat(INNER_INDENT), &self.current_line[last_space..].trim_start());
                } else {
                    line = String::from(&self.current_line[..LEN-4]);
                    self.current_line = format!("{}{}", " ".repeat(INNER_INDENT), &self.current_line[LEN-4..].trim_start());
                }

                if self.current_line == original_line {
                    self.current_line.clear(); // Prevent infinite loop
                }

                println!("| {} |", format_line(&line, LEN - 4));
                line_break_occurred = true; // Set flag since we had a line break
            }
        }

        if line_break_occurred {
            println!("| {} |", " ".repeat(LEN - 4)); // Print an empty line if we had a line break
        }
    }
}

/// Formats a line with spaces.
fn format_line(s: &str, total_len: usize) -> String {
    let text_len = s.chars().count();
    format!("{}{}", s, " ".repeat(total_len - text_len))
}

/// Prints a border.
pub fn print_border() {
    println!("+{}+", "-".repeat(LEN - 2));
}