use crate::ErrorRecommendation;
use crate::error::{BoxenError, BoxenResult};
use crate::options::BorderStyle;
use std::sync::OnceLock;
const DEFAULT_TERMINAL_WIDTH: usize = 80;
static CACHED_TERMINAL_SIZE: OnceLock<(usize, Option<usize>)> = OnceLock::new();
#[must_use]
pub fn get_terminal_width() -> usize {
get_terminal_size().0
}
#[must_use]
pub fn get_terminal_height() -> Option<usize> {
get_terminal_size().1
}
pub fn get_terminal_size() -> (usize, Option<usize>) {
*CACHED_TERMINAL_SIZE.get_or_init(|| {
if let (Ok(cols), Ok(lines)) = (std::env::var("COLUMNS"), std::env::var("LINES")) {
if let (Ok(width), Ok(height)) = (cols.parse::<usize>(), lines.parse::<usize>()) {
return (width, Some(height));
}
}
match terminal_size::terminal_size() {
Some((w, h)) => (w.0 as usize, Some(h.0 as usize)),
None => (DEFAULT_TERMINAL_WIDTH, None),
}
})
}
pub fn clear_terminal_cache() {
}
#[must_use]
pub fn calculate_border_width(border_style: &BorderStyle) -> usize {
match border_style {
BorderStyle::None => 0,
_ => 2, }
}
pub fn calculate_max_content_width(
terminal_width: usize,
border_style: &BorderStyle,
horizontal_padding: usize,
horizontal_margin: usize,
specified_width: Option<usize>,
) -> BoxenResult<usize> {
let border_width = calculate_border_width(border_style);
let total_overhead = border_width + horizontal_padding + horizontal_margin;
if let Some(width) = specified_width {
if width < total_overhead {
return Err(BoxenError::invalid_dimensions(
"Width too small for borders and padding".to_string(),
Some(width),
None,
vec![ErrorRecommendation::suggestion_only(
"Width insufficient".to_string(),
format!("Need at least {total_overhead} width"),
)],
));
}
return Ok(width - total_overhead);
}
if terminal_width < total_overhead {
return Err(BoxenError::terminal_size_error(
"Unable to detect terminal size".to_string(),
vec![
crate::error::ErrorRecommendation::suggestion_only(
"Terminal size detection failed".to_string(),
"This may happen in non-interactive environments or unsupported terminals"
.to_string(),
),
crate::error::ErrorRecommendation::with_auto_fix(
"Use fallback dimensions".to_string(),
"Specify explicit dimensions instead".to_string(),
".width(80).height(24)".to_string(),
),
],
));
}
Ok(terminal_width - total_overhead)
}
pub fn validate_terminal_constraints(
box_width: usize,
box_height: usize,
terminal_width: usize,
terminal_height: Option<usize>,
) -> BoxenResult<()> {
if box_width > terminal_width {
return Err(BoxenError::configuration_error(
format!("Box width ({box_width}) exceeds terminal width ({terminal_width})"),
vec![ErrorRecommendation::suggestion_only(
"Width exceeds terminal".to_string(),
format!("Reduce width to fit in {terminal_width} columns"),
)],
));
}
if let Some(term_height) = terminal_height {
if box_height > term_height {
return Err(BoxenError::configuration_error(
format!("Box height ({box_height}) exceeds terminal height ({term_height})"),
vec![ErrorRecommendation::suggestion_only(
"Height exceeds terminal".to_string(),
format!("Reduce height to fit in {term_height} rows"),
)],
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_terminal_width() {
let width = get_terminal_width();
assert!(width > 0);
assert!(width >= DEFAULT_TERMINAL_WIDTH);
}
#[test]
fn test_get_terminal_size() {
let (width, height) = get_terminal_size();
assert!(width > 0);
assert!(width >= DEFAULT_TERMINAL_WIDTH);
if let Some(h) = height {
assert!(h > 0);
}
}
#[test]
fn test_calculate_border_width() {
assert_eq!(calculate_border_width(&BorderStyle::None), 0);
assert_eq!(calculate_border_width(&BorderStyle::Single), 2);
assert_eq!(calculate_border_width(&BorderStyle::Double), 2);
assert_eq!(calculate_border_width(&BorderStyle::Round), 2);
assert_eq!(calculate_border_width(&BorderStyle::Bold), 2);
}
#[test]
fn test_calculate_max_content_width_basic() {
let result = calculate_max_content_width(
80,
&BorderStyle::Single,
4, 0, None,
);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 74); }
#[test]
fn test_calculate_max_content_width_with_margins() {
let result = calculate_max_content_width(
80,
&BorderStyle::Single,
4, 6, None,
);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 68); }
#[test]
fn test_calculate_max_content_width_no_border() {
let result = calculate_max_content_width(
80,
&BorderStyle::None,
4, 0, None,
);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 76); }
#[test]
fn test_calculate_max_content_width_with_specified_width() {
let result = calculate_max_content_width(
80,
&BorderStyle::Single,
4, 0, Some(50), );
assert!(result.is_ok());
assert_eq!(result.unwrap(), 44); }
#[test]
fn test_calculate_max_content_width_invalid_specified_width() {
let result = calculate_max_content_width(
80,
&BorderStyle::Single,
4, 0, Some(5), );
assert!(result.is_err());
matches!(result.unwrap_err(), BoxenError::InvalidDimensions { .. });
}
#[test]
fn test_calculate_max_content_width_terminal_too_small() {
let result = calculate_max_content_width(
5, &BorderStyle::Single,
4, 0, None,
);
assert!(result.is_err());
matches!(result.unwrap_err(), BoxenError::TerminalSizeError { .. });
}
#[test]
fn test_validate_terminal_constraints_valid() {
let result = validate_terminal_constraints(50, 20, 80, Some(30));
assert!(result.is_ok());
}
#[test]
fn test_validate_terminal_constraints_width_too_large() {
let result = validate_terminal_constraints(100, 20, 80, Some(30));
assert!(result.is_err());
matches!(result.unwrap_err(), BoxenError::ConfigurationError { .. });
}
#[test]
fn test_validate_terminal_constraints_height_too_large() {
let result = validate_terminal_constraints(50, 40, 80, Some(30));
assert!(result.is_err());
matches!(result.unwrap_err(), BoxenError::ConfigurationError { .. });
}
#[test]
fn test_validate_terminal_constraints_no_height_limit() {
let result = validate_terminal_constraints(50, 100, 80, None);
assert!(result.is_ok()); }
#[test]
fn test_fallback_behavior() {
let width = get_terminal_width();
assert!(width >= DEFAULT_TERMINAL_WIDTH);
}
#[test]
fn test_edge_cases() {
let result = calculate_max_content_width(80, &BorderStyle::Single, 0, 0, None);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 78);
let result = calculate_max_content_width(80, &BorderStyle::None, 0, 0, None);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 80); }
}