Skip to main content

yarli_cli/
mode.rs

1//! Render mode auto-detection (Section 16.3).
2//!
3//! Determines the appropriate rendering mode based on:
4//! - TTY detection: non-TTY (pipe/redirect) forces stream mode.
5//! - Terminal size: < 80 cols or < 24 rows forces stream mode.
6//! - CLI flags: `--stream` or `--tui` override auto-detection.
7
8use std::io::{self, IsTerminal};
9
10/// Rendering mode for the CLI output.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum RenderMode {
13    /// Inline viewport with live status, completed output in native scrollback.
14    /// Used for CI, pipes, small terminals, non-TTY.
15    Stream,
16    /// Fullscreen panel layout (Milestone 4 — not yet implemented).
17    Dashboard,
18}
19
20/// Minimum terminal width for dashboard mode.
21const MIN_DASHBOARD_COLS: u16 = 80;
22/// Minimum terminal height for dashboard mode.
23const MIN_DASHBOARD_ROWS: u16 = 24;
24
25/// Terminal capability information used for mode detection.
26#[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    /// Probe the current terminal.
35    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    /// Whether the terminal meets minimum size requirements for dashboard mode.
42    pub fn supports_dashboard(&self) -> bool {
43        self.is_tty && self.cols >= MIN_DASHBOARD_COLS && self.rows >= MIN_DASHBOARD_ROWS
44    }
45}
46
47/// Select the render mode based on terminal info and CLI flags.
48///
49/// Priority:
50/// 1. `--stream` flag → Stream
51/// 2. `--tui` flag → Dashboard (requires TTY with sufficient size)
52/// 3. Auto-detect from terminal capabilities
53pub 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    // Auto-detect: dashboard if TTY meets minimum size, stream otherwise.
73    if info.supports_dashboard() {
74        Ok(RenderMode::Dashboard)
75    } else {
76        Ok(RenderMode::Stream)
77    }
78}
79
80/// Errors from render mode selection.
81#[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    // --- supports_dashboard ---
132
133    #[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    // --- select_render_mode ---
169
170    #[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        // --stream flag is checked first, so it takes precedence.
192        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        // Should not panic even in non-TTY (CI) environments.
229        let _info = TerminalInfo::detect();
230    }
231}