use std::env;
const DEFAULT_TERMINAL_WIDTH: usize = 80;
const MINIMUM_TERMINAL_WIDTH: usize = 40;
const MAXIMUM_TERMINAL_WIDTH: usize = 120;
#[must_use]
pub fn get_terminal_width() -> usize {
if let Ok(columns_str) = env::var("COLUMNS") {
if let Ok(columns) = columns_str.parse::<usize>() {
return clamp_width(columns);
}
}
if let Some(width) = detect_terminal_width_platform() {
return clamp_width(width);
}
DEFAULT_TERMINAL_WIDTH
}
fn detect_terminal_width_platform() -> Option<usize> {
#[cfg(unix)]
{
detect_terminal_width_unix()
}
#[cfg(windows)]
{
detect_terminal_width_windows()
}
#[cfg(not(any(unix, windows)))]
{
None
}
}
#[cfg(unix)]
fn detect_terminal_width_unix() -> Option<usize> {
use std::io::IsTerminal;
if !std::io::stdout().is_terminal() {
return None;
}
None
}
#[cfg(windows)]
fn detect_terminal_width_windows() -> Option<usize> {
None
}
fn clamp_width(width: usize) -> usize {
width.clamp(MINIMUM_TERMINAL_WIDTH, MAXIMUM_TERMINAL_WIDTH)
}
pub fn wrap_text(text: &str, width: usize, indent: Option<usize>) -> String {
if text.is_empty() || width == 0 {
return text.to_string();
}
let mut result = Vec::new();
let indent_str = " ".repeat(indent.unwrap_or(0));
for paragraph in text.split('\n') {
if paragraph.trim().is_empty() {
result.push(String::new());
continue;
}
let mut lines = Vec::new();
let mut current_line = String::new();
let words: Vec<&str> = paragraph.split_whitespace().collect();
for word in &words {
let space_needed = usize::from(!current_line.is_empty());
let line_with_word_len = current_line.len() + space_needed + word.len();
if line_with_word_len <= width || current_line.is_empty() {
if !current_line.is_empty() {
current_line.push(' ');
}
current_line.push_str(word);
} else {
lines.push(current_line);
current_line = format!("{indent_str}{word}");
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
result.extend(lines);
}
result.join("\n")
}
pub fn wrap_text_to_terminal(text: &str, indent: Option<usize>) -> String {
let width = get_terminal_width();
wrap_text(text, width, indent)
}
pub fn format_help_entry(
left_column: &str,
right_column: &str,
left_width: usize,
total_width: usize,
) -> String {
if right_column.is_empty() {
return left_column.to_string();
}
let right_width = total_width.saturating_sub(left_width + 2);
if left_column.len() <= left_width {
let padding = " ".repeat(left_width - left_column.len());
let wrapped_right = wrap_text(right_column, right_width, Some(left_width + 2));
format!("{left_column}{padding} {wrapped_right}")
} else {
let indent = " ".repeat(left_width + 2);
let wrapped_right = wrap_text(right_column, right_width, Some(left_width + 2));
format!("{left_column}\n{indent}{wrapped_right}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_terminal_width_default() {
let width = get_terminal_width();
assert!(width >= MINIMUM_TERMINAL_WIDTH);
assert!(width <= MAXIMUM_TERMINAL_WIDTH);
}
#[test]
fn test_clamp_width() {
assert_eq!(clamp_width(10), MINIMUM_TERMINAL_WIDTH);
assert_eq!(clamp_width(80), 80);
assert_eq!(clamp_width(200), MAXIMUM_TERMINAL_WIDTH);
}
#[test]
fn test_wrap_text_simple() {
let text = "This is a test";
let wrapped = wrap_text(text, 20, None);
assert_eq!(wrapped, "This is a test");
}
#[test]
fn test_wrap_text_long_line() {
let text = "This is a very long line that needs to be wrapped";
let wrapped = wrap_text(text, 20, None);
assert!(wrapped.contains('\n'));
for line in wrapped.lines() {
assert!(line.len() <= 20);
}
}
#[test]
fn test_wrap_text_with_indent() {
let text = "This is a very long line that needs to be wrapped with indentation";
let wrapped = wrap_text(text, 20, Some(4));
let lines: Vec<&str> = wrapped.lines().collect();
assert!(lines.len() > 1);
assert!(!lines[0].starts_with(" "));
for line in &lines[1..] {
assert!(line.starts_with(" "));
}
}
#[test]
fn test_format_help_entry_normal() {
let result = format_help_entry(" -v, --verbose", "Enable verbose output", 20, 60);
assert!(result.contains(" -v, --verbose"));
assert!(result.contains("Enable verbose output"));
}
#[test]
fn test_format_help_entry_long_left() {
let result = format_help_entry(" --very-long-flag-name", "Description", 15, 60);
assert!(result.contains('\n'));
}
#[test]
fn test_wrap_text_preserves_empty_lines() {
let text = "First paragraph\n\nSecond paragraph";
let wrapped = wrap_text(text, 50, None);
assert_eq!(wrapped, "First paragraph\n\nSecond paragraph");
}
}