use std::io::Write;
use crossterm::{
cursor::{Hide, Show},
execute,
style::ResetColor,
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Size {
pub width: u16,
pub height: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Position {
pub column: u16,
pub row: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rect {
pub origin: Position,
pub size: Size,
}
pub fn size(width: u16, height: u16) -> Size {
Size { width, height }
}
pub fn position(column: u16, row: u16) -> Position {
Position { column, row }
}
pub fn rect(origin: Position, size: Size) -> Rect {
Rect { origin, size }
}
#[cfg(not(tarpaulin_include))]
pub fn terminal_size() -> std::io::Result<Size> {
let (width, height) = terminal::size()?;
Ok(size(width, height))
}
pub fn enter_alternate_screen<W: Write>(writer: &mut W) -> std::io::Result<()> {
execute!(writer, EnterAlternateScreen, Clear(ClearType::All), Hide)
}
pub fn leave_alternate_screen<W: Write>(writer: &mut W) -> std::io::Result<()> {
execute!(writer, Show, ResetColor, LeaveAlternateScreen)
}
#[cfg(not(tarpaulin_include))]
pub fn enable_raw_mode() -> std::io::Result<()> {
terminal::enable_raw_mode()
}
#[cfg(not(tarpaulin_include))]
pub fn disable_raw_mode() -> std::io::Result<()> {
terminal::disable_raw_mode()
}
pub fn top_two_thirds(terminal_size: Size) -> Rect {
let height = terminal_size.height.saturating_mul(2) / 3;
rect(position(0, 0), size(terminal_size.width, height.max(1)))
}
pub fn bottom_third(terminal_size: Size) -> Rect {
let top = top_two_thirds(terminal_size);
let row = top.size.height.min(terminal_size.height);
let height = terminal_size.height.saturating_sub(row);
rect(position(0, row), size(terminal_size.width, height))
}
pub fn centered_block_origin(region: Rect, block_size: Size) -> Position {
let column = center_axis(region.origin.column, region.size.width, block_size.width);
let row = center_axis(region.origin.row, region.size.height, block_size.height);
position(column, row)
}
pub fn text_block_size(lines: &[&str]) -> Size {
let width = lines
.iter()
.map(|line| UnicodeWidthStr::width(*line))
.max()
.unwrap_or(0);
size(width as u16, lines.len() as u16)
}
pub fn centered_column(terminal_width: u16, content_width: u16) -> u16 {
terminal_width.saturating_sub(content_width) / 2
}
pub fn lerp_usize(from: usize, to: usize, progress: f64) -> usize {
let from_f = from as f64;
let to_f = to as f64;
(from_f + (to_f - from_f) * progress).round() as usize
}
fn center_axis(origin: u16, region: u16, content: u16) -> u16 {
origin.saturating_add(region.saturating_sub(content) / 2)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn top_two_thirds_uses_upper_region() {
let region = top_two_thirds(size(120, 30));
assert_eq!(region, rect(position(0, 0), size(120, 20)));
}
#[test]
fn bottom_third_starts_after_upper_region() {
let region = bottom_third(size(120, 30));
assert_eq!(region, rect(position(0, 20), size(120, 10)));
}
#[test]
fn centered_block_origin_centers_within_region() {
let region = rect(position(0, 0), size(100, 30));
assert_eq!(
centered_block_origin(region, size(20, 10)),
position(40, 10)
);
}
#[test]
fn centered_block_origin_keeps_large_blocks_at_region_origin() {
let region = rect(position(5, 7), size(10, 3));
assert_eq!(centered_block_origin(region, size(20, 10)), position(5, 7));
}
#[test]
fn text_block_size_uses_display_width() {
let lines = ["hi", "▓▓"];
assert_eq!(text_block_size(&lines), size(2, 2));
}
#[test]
fn centered_column_centers_content() {
assert_eq!(centered_column(80, 20), 30);
assert_eq!(centered_column(80, 80), 0);
assert_eq!(centered_column(80, 100), 0);
}
#[test]
fn lerp_usize_interpolates_between_values() {
assert_eq!(lerp_usize(10, 20, 0.0), 10);
assert_eq!(lerp_usize(10, 20, 0.5), 15);
assert_eq!(lerp_usize(10, 20, 1.0), 20);
assert_eq!(lerp_usize(20, 10, 0.5), 15);
}
#[test]
fn alternate_screen_helpers_write_terminal_sequences() {
let mut buffer = Vec::new();
enter_alternate_screen(&mut buffer).unwrap();
leave_alternate_screen(&mut buffer).unwrap();
assert!(!buffer.is_empty());
}
#[test]
fn rect_constructor_creates_expected_value() {
let r = rect(position(3, 7), size(40, 20));
assert_eq!(r.origin.column, 3);
assert_eq!(r.origin.row, 7);
assert_eq!(r.size.width, 40);
assert_eq!(r.size.height, 20);
}
#[test]
fn size_and_position_constructors() {
let s = size(80, 24);
assert_eq!(s.width, 80);
assert_eq!(s.height, 24);
let p = position(10, 5);
assert_eq!(p.column, 10);
assert_eq!(p.row, 5);
}
#[test]
fn bottom_third_handles_small_terminal() {
let region = bottom_third(size(80, 3));
assert!(region.size.height <= 3);
}
#[test]
fn top_two_thirds_min_height_is_one() {
let region = top_two_thirds(size(80, 1));
assert_eq!(region.size.height, 1);
}
}