1use std::io::{self, IsTerminal};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum RenderMode {
13 Stream,
16 Dashboard,
18}
19
20const MIN_DASHBOARD_COLS: u16 = 80;
22const MIN_DASHBOARD_ROWS: u16 = 24;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct TerminalInfo {
28 pub is_tty: bool,
29 pub cols: u16,
30 pub rows: u16,
31}
32
33impl TerminalInfo {
34 pub fn detect() -> Self {
36 let is_tty = io::stdout().is_terminal();
37 let (cols, rows) = crossterm::terminal::size().unwrap_or((0, 0));
38 Self { is_tty, cols, rows }
39 }
40
41 pub fn supports_dashboard(&self) -> bool {
43 self.is_tty && self.cols >= MIN_DASHBOARD_COLS && self.rows >= MIN_DASHBOARD_ROWS
44 }
45}
46
47pub fn select_render_mode(
54 info: &TerminalInfo,
55 force_stream: bool,
56 force_tui: bool,
57) -> Result<RenderMode, RenderModeError> {
58 if force_stream {
59 return Ok(RenderMode::Stream);
60 }
61
62 if force_tui {
63 if !info.supports_dashboard() {
64 return Err(RenderModeError::TerminalTooSmall {
65 cols: info.cols,
66 rows: info.rows,
67 });
68 }
69 return Ok(RenderMode::Dashboard);
70 }
71
72 if info.supports_dashboard() {
74 Ok(RenderMode::Dashboard)
75 } else {
76 Ok(RenderMode::Stream)
77 }
78}
79
80#[derive(Debug, thiserror::Error)]
82pub enum RenderModeError {
83 #[error("terminal too small for dashboard mode ({cols}x{rows}, need {MIN_DASHBOARD_COLS}x{MIN_DASHBOARD_ROWS})")]
84 TerminalTooSmall { cols: u16, rows: u16 },
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 fn tty_large() -> TerminalInfo {
92 TerminalInfo {
93 is_tty: true,
94 cols: 120,
95 rows: 40,
96 }
97 }
98
99 fn tty_small() -> TerminalInfo {
100 TerminalInfo {
101 is_tty: true,
102 cols: 60,
103 rows: 20,
104 }
105 }
106
107 fn pipe() -> TerminalInfo {
108 TerminalInfo {
109 is_tty: false,
110 cols: 120,
111 rows: 40,
112 }
113 }
114
115 fn tty_narrow() -> TerminalInfo {
116 TerminalInfo {
117 is_tty: true,
118 cols: 79,
119 rows: 40,
120 }
121 }
122
123 fn tty_short() -> TerminalInfo {
124 TerminalInfo {
125 is_tty: true,
126 cols: 120,
127 rows: 23,
128 }
129 }
130
131 #[test]
134 fn large_tty_supports_dashboard() {
135 assert!(tty_large().supports_dashboard());
136 }
137
138 #[test]
139 fn pipe_does_not_support_dashboard() {
140 assert!(!pipe().supports_dashboard());
141 }
142
143 #[test]
144 fn small_tty_does_not_support_dashboard() {
145 assert!(!tty_small().supports_dashboard());
146 }
147
148 #[test]
149 fn narrow_tty_does_not_support_dashboard() {
150 assert!(!tty_narrow().supports_dashboard());
151 }
152
153 #[test]
154 fn short_tty_does_not_support_dashboard() {
155 assert!(!tty_short().supports_dashboard());
156 }
157
158 #[test]
159 fn exact_minimum_supports_dashboard() {
160 let info = TerminalInfo {
161 is_tty: true,
162 cols: MIN_DASHBOARD_COLS,
163 rows: MIN_DASHBOARD_ROWS,
164 };
165 assert!(info.supports_dashboard());
166 }
167
168 #[test]
171 fn force_stream_always_stream() {
172 let mode = select_render_mode(&tty_large(), true, false).unwrap();
173 assert_eq!(mode, RenderMode::Stream);
174 }
175
176 #[test]
177 fn force_tui_returns_dashboard_on_large_tty() {
178 let mode = select_render_mode(&tty_large(), false, true).unwrap();
179 assert_eq!(mode, RenderMode::Dashboard);
180 }
181
182 #[test]
183 fn force_tui_errors_on_small_terminal() {
184 let result = select_render_mode(&tty_small(), false, true);
185 assert!(result.is_err());
186 assert!(result.unwrap_err().to_string().contains("too small"));
187 }
188
189 #[test]
190 fn force_stream_wins_over_tui() {
191 let mode = select_render_mode(&tty_large(), true, true).unwrap();
193 assert_eq!(mode, RenderMode::Stream);
194 }
195
196 #[test]
197 fn auto_detect_pipe_returns_stream() {
198 let mode = select_render_mode(&pipe(), false, false).unwrap();
199 assert_eq!(mode, RenderMode::Stream);
200 }
201
202 #[test]
203 fn auto_detect_small_tty_returns_stream() {
204 let mode = select_render_mode(&tty_small(), false, false).unwrap();
205 assert_eq!(mode, RenderMode::Stream);
206 }
207
208 #[test]
209 fn auto_detect_large_tty_returns_dashboard() {
210 let mode = select_render_mode(&tty_large(), false, false).unwrap();
211 assert_eq!(mode, RenderMode::Dashboard);
212 }
213
214 #[test]
215 fn auto_detect_narrow_returns_stream() {
216 let mode = select_render_mode(&tty_narrow(), false, false).unwrap();
217 assert_eq!(mode, RenderMode::Stream);
218 }
219
220 #[test]
221 fn auto_detect_short_returns_stream() {
222 let mode = select_render_mode(&tty_short(), false, false).unwrap();
223 assert_eq!(mode, RenderMode::Stream);
224 }
225
226 #[test]
227 fn terminal_info_detect_does_not_panic() {
228 let _info = TerminalInfo::detect();
230 }
231}