cargo_plugin_utils/
scrolling.rs

1//! Scrolling region helpers for terminal output.
2
3use std::io::Write;
4
5use anyhow::Context;
6use console::Term;
7
8/// Get terminal size (rows, cols).
9pub fn get_terminal_size() -> anyhow::Result<(u16, u16)> {
10    let term = Term::stdout();
11    term.size_checked().context("Failed to get terminal size")
12}
13
14/// Set scrolling region using DECSTBM (Set Top and Bottom Margins).
15///
16/// Sets the scrolling region to lines `top` through `bottom` (1-indexed).
17/// All scrolling operations will be confined to this region.
18pub fn set_scrolling_region(top: u16, bottom: u16) -> anyhow::Result<()> {
19    // DECSTBM: ESC [ top ; bottom r
20    // top and bottom are 1-indexed
21    let mut stderr = std::io::stderr();
22    write!(stderr, "\x1b[{};{}r", top, bottom).context("Failed to set scrolling region")?;
23    stderr.flush().context("Failed to flush stdout")?;
24    Ok(())
25}
26
27/// Reset scrolling region (restore full terminal scrolling).
28///
29/// Resets the scrolling region to the entire terminal.
30pub fn reset_scrolling_region() -> anyhow::Result<()> {
31    // Reset scrolling region: ESC [ r (no parameters means full terminal)
32    let mut stderr = std::io::stderr();
33    write!(stderr, "\x1b[r").context("Failed to reset scrolling region")?;
34    stderr.flush().context("Failed to flush stdout")?;
35    Ok(())
36}
37
38/// Clear the scrolling region.
39///
40/// Clears all lines within the current scrolling region.
41pub fn clear_scrolling_region() -> anyhow::Result<()> {
42    // Move to top of region and clear to bottom
43    // ESC [ 1 J clears from cursor to bottom of screen
44    // But we want to clear the region, so we need to:
45    // 1. Move to top of region
46    // 2. Clear lines in region
47    let mut stderr = std::io::stderr();
48    // For now, just clear from cursor to end of screen
49    // The actual region clearing will be handled by the caller
50    // who knows the exact region bounds
51    write!(stderr, "\x1b[J").context("Failed to clear scrolling region")?;
52    stderr.flush().context("Failed to flush stdout")?;
53    Ok(())
54}
55
56/// Move cursor to a specific line (1-indexed).
57pub fn move_cursor_to_line(line: u16) -> anyhow::Result<()> {
58    // CUP (Cursor Position): ESC [ row ; col H
59    // line is 1-indexed
60    let mut stderr = std::io::stderr();
61    write!(stderr, "\x1b[{};1H", line).context("Failed to move cursor to line")?;
62    stderr.flush().context("Failed to flush stdout")?;
63    Ok(())
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn test_get_terminal_size() {
72        // Should return Some on a real terminal, None otherwise
73        // We can't easily test the exact values, but we can test it doesn't panic
74        let _size = get_terminal_size();
75    }
76
77    #[test]
78    fn test_set_scrolling_region() {
79        // Test that it doesn't panic
80        // In a real terminal, this would set the scrolling region
81        let _ = set_scrolling_region(1u16, 10u16);
82    }
83
84    #[test]
85    fn test_set_scrolling_region_single_line() {
86        // Test setting a single-line scrolling region
87        let _ = set_scrolling_region(5u16, 5u16);
88    }
89
90    #[test]
91    fn test_set_scrolling_region_large() {
92        // Test setting a large scrolling region
93        let _ = set_scrolling_region(1u16, 1000u16);
94    }
95
96    #[test]
97    fn test_reset_scrolling_region() {
98        // Test that it doesn't panic
99        let _ = reset_scrolling_region();
100    }
101
102    #[test]
103    fn test_clear_scrolling_region() {
104        // Test that it doesn't panic
105        let _ = clear_scrolling_region();
106    }
107
108    #[test]
109    fn test_move_cursor_to_line() {
110        // Test that it doesn't panic
111        let _ = move_cursor_to_line(5u16);
112    }
113
114    #[test]
115    fn test_move_cursor_to_line_first() {
116        // Test moving to line 1
117        let _ = move_cursor_to_line(1u16);
118    }
119
120    #[test]
121    fn test_move_cursor_to_line_large() {
122        // Test moving to a large line number
123        let _ = move_cursor_to_line(1000u16);
124    }
125
126    #[test]
127    fn test_scrolling_region_sequence() {
128        // Test a sequence of operations
129        let _ = set_scrolling_region(1u16, 5u16);
130        let _ = move_cursor_to_line(1u16);
131        let _ = clear_scrolling_region();
132        let _ = reset_scrolling_region();
133    }
134
135    #[test]
136    fn test_scrolling_region_typical_usage() {
137        // Simulate typical usage: set region at bottom of terminal
138        // Assuming 24 rows, reserve last 5 lines for scrolling output
139        let term_rows = 24u16;
140        let scroll_lines = 5u16;
141        let region_top = term_rows - scroll_lines + 1; // 20
142
143        let _ = set_scrolling_region(region_top, term_rows);
144        let _ = move_cursor_to_line(region_top);
145        // ... subprocess output would go here ...
146        let _ = clear_scrolling_region();
147        let _ = reset_scrolling_region();
148    }
149
150    #[test]
151    fn test_scrolling_region_full_terminal() {
152        // Test setting scrolling region to entire terminal
153        let _ = set_scrolling_region(1u16, 24u16);
154        let _ = reset_scrolling_region();
155    }
156
157    #[test]
158    fn test_multiple_cursor_moves() {
159        // Test multiple cursor moves
160        for line in 1u16..=10 {
161            let _ = move_cursor_to_line(line);
162        }
163    }
164
165    #[test]
166    fn test_set_and_reset_multiple_times() {
167        // Test setting and resetting multiple times
168        for idx in 1u16..=5 {
169            let _ = set_scrolling_region(idx, idx + 10);
170            let _ = reset_scrolling_region();
171        }
172    }
173}