1use std::io::Write;
2
3use crossterm::{
4 cursor::{Hide, Show},
5 execute,
6 style::ResetColor,
7 terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
8};
9use unicode_width::UnicodeWidthStr;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct Size {
13 pub width: u16,
14 pub height: u16,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct Position {
19 pub column: u16,
20 pub row: u16,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct Rect {
25 pub origin: Position,
26 pub size: Size,
27}
28
29pub fn size(width: u16, height: u16) -> Size {
30 Size { width, height }
31}
32
33pub fn position(column: u16, row: u16) -> Position {
34 Position { column, row }
35}
36
37pub fn rect(origin: Position, size: Size) -> Rect {
38 Rect { origin, size }
39}
40
41#[cfg(not(tarpaulin_include))]
42pub fn terminal_size() -> std::io::Result<Size> {
43 let (width, height) = terminal::size()?;
44 Ok(size(width, height))
45}
46
47pub fn enter_alternate_screen<W: Write>(writer: &mut W) -> std::io::Result<()> {
48 execute!(writer, EnterAlternateScreen, Clear(ClearType::All), Hide)
49}
50
51pub fn leave_alternate_screen<W: Write>(writer: &mut W) -> std::io::Result<()> {
52 execute!(writer, Show, ResetColor, LeaveAlternateScreen)
53}
54
55#[cfg(not(tarpaulin_include))]
56pub fn enable_raw_mode() -> std::io::Result<()> {
57 terminal::enable_raw_mode()
58}
59
60#[cfg(not(tarpaulin_include))]
61pub fn disable_raw_mode() -> std::io::Result<()> {
62 terminal::disable_raw_mode()
63}
64
65pub fn top_two_thirds(terminal_size: Size) -> Rect {
66 let height = terminal_size.height.saturating_mul(2) / 3;
67 rect(position(0, 0), size(terminal_size.width, height.max(1)))
68}
69
70pub fn bottom_third(terminal_size: Size) -> Rect {
71 let top = top_two_thirds(terminal_size);
72 let row = top.size.height.min(terminal_size.height);
73 let height = terminal_size.height.saturating_sub(row);
74 rect(position(0, row), size(terminal_size.width, height))
75}
76
77pub fn centered_block_origin(region: Rect, block_size: Size) -> Position {
78 let column = center_axis(region.origin.column, region.size.width, block_size.width);
79 let row = center_axis(region.origin.row, region.size.height, block_size.height);
80 position(column, row)
81}
82
83pub fn text_block_size(lines: &[&str]) -> Size {
84 let width = lines
85 .iter()
86 .map(|line| UnicodeWidthStr::width(*line))
87 .max()
88 .unwrap_or(0);
89
90 size(width as u16, lines.len() as u16)
91}
92
93pub fn centered_column(terminal_width: u16, content_width: u16) -> u16 {
94 terminal_width.saturating_sub(content_width) / 2
95}
96
97pub fn lerp_usize(from: usize, to: usize, progress: f64) -> usize {
98 let from_f = from as f64;
99 let to_f = to as f64;
100 (from_f + (to_f - from_f) * progress).round() as usize
101}
102
103fn center_axis(origin: u16, region: u16, content: u16) -> u16 {
104 origin.saturating_add(region.saturating_sub(content) / 2)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn top_two_thirds_uses_upper_region() {
113 let region = top_two_thirds(size(120, 30));
114
115 assert_eq!(region, rect(position(0, 0), size(120, 20)));
116 }
117
118 #[test]
119 fn bottom_third_starts_after_upper_region() {
120 let region = bottom_third(size(120, 30));
121
122 assert_eq!(region, rect(position(0, 20), size(120, 10)));
123 }
124
125 #[test]
126 fn centered_block_origin_centers_within_region() {
127 let region = rect(position(0, 0), size(100, 30));
128
129 assert_eq!(
130 centered_block_origin(region, size(20, 10)),
131 position(40, 10)
132 );
133 }
134
135 #[test]
136 fn centered_block_origin_keeps_large_blocks_at_region_origin() {
137 let region = rect(position(5, 7), size(10, 3));
138
139 assert_eq!(centered_block_origin(region, size(20, 10)), position(5, 7));
140 }
141
142 #[test]
143 fn text_block_size_uses_display_width() {
144 let lines = ["hi", "▓▓"];
145
146 assert_eq!(text_block_size(&lines), size(2, 2));
147 }
148
149 #[test]
150 fn centered_column_centers_content() {
151 assert_eq!(centered_column(80, 20), 30);
152 assert_eq!(centered_column(80, 80), 0);
153 assert_eq!(centered_column(80, 100), 0);
154 }
155
156 #[test]
157 fn lerp_usize_interpolates_between_values() {
158 assert_eq!(lerp_usize(10, 20, 0.0), 10);
159 assert_eq!(lerp_usize(10, 20, 0.5), 15);
160 assert_eq!(lerp_usize(10, 20, 1.0), 20);
161 assert_eq!(lerp_usize(20, 10, 0.5), 15);
162 }
163
164 #[test]
165 fn alternate_screen_helpers_write_terminal_sequences() {
166 let mut buffer = Vec::new();
167
168 enter_alternate_screen(&mut buffer).unwrap();
169 leave_alternate_screen(&mut buffer).unwrap();
170
171 assert!(!buffer.is_empty());
172 }
173
174 #[test]
175 fn rect_constructor_creates_expected_value() {
176 let r = rect(position(3, 7), size(40, 20));
177
178 assert_eq!(r.origin.column, 3);
179 assert_eq!(r.origin.row, 7);
180 assert_eq!(r.size.width, 40);
181 assert_eq!(r.size.height, 20);
182 }
183
184 #[test]
185 fn size_and_position_constructors() {
186 let s = size(80, 24);
187 assert_eq!(s.width, 80);
188 assert_eq!(s.height, 24);
189
190 let p = position(10, 5);
191 assert_eq!(p.column, 10);
192 assert_eq!(p.row, 5);
193 }
194
195 #[test]
196 fn bottom_third_handles_small_terminal() {
197 let region = bottom_third(size(80, 3));
198 assert!(region.size.height <= 3);
199 }
200
201 #[test]
202 fn top_two_thirds_min_height_is_one() {
203 let region = top_two_thirds(size(80, 1));
204 assert_eq!(region.size.height, 1);
205 }
206}