detached_shell/
scrollback.rs

1use crossterm::{
2    cursor::{Hide, MoveTo, Show},
3    event::{self, Event, KeyCode, KeyEvent},
4    execute,
5    style::{Color, Print, ResetColor, SetForegroundColor},
6    terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
7};
8use std::io::{self, Write};
9
10use crate::error::Result;
11
12pub struct ScrollbackViewer {
13    lines: Vec<String>,
14    viewport_start: usize,
15    viewport_height: usize,
16    total_lines: usize,
17}
18
19impl ScrollbackViewer {
20    pub fn new(content: &[u8]) -> Self {
21        // Convert bytes to lines
22        let text = String::from_utf8_lossy(content);
23        let lines: Vec<String> = text.lines().map(|s| s.to_string()).collect();
24        let total_lines = lines.len();
25
26        // Get terminal height
27        let (_, height) = terminal::size().unwrap_or((80, 24));
28        let viewport_height = (height - 3) as usize; // Leave room for status bar
29
30        ScrollbackViewer {
31            lines,
32            viewport_start: 0,
33            viewport_height,
34            total_lines,
35        }
36    }
37
38    pub fn run(&mut self) -> Result<()> {
39        // Enter alternate screen
40        let mut stdout = io::stdout();
41        terminal::enable_raw_mode()?;
42        execute!(stdout, EnterAlternateScreen, Hide)?;
43
44        let result = self.event_loop();
45
46        // Clean up
47        execute!(stdout, LeaveAlternateScreen, Show)?;
48        terminal::disable_raw_mode()?;
49
50        result
51    }
52
53    fn event_loop(&mut self) -> Result<()> {
54        let mut stdout = io::stdout();
55
56        loop {
57            self.draw(&mut stdout)?;
58
59            if let Event::Key(key) = event::read()? {
60                match self.handle_key(key) {
61                    true => break, // Exit requested
62                    false => continue,
63                }
64            }
65        }
66
67        Ok(())
68    }
69
70    fn handle_key(&mut self, key: KeyEvent) -> bool {
71        match key.code {
72            KeyCode::Char('q') | KeyCode::Esc => return true, // Exit
73
74            // Navigation
75            KeyCode::Up | KeyCode::Char('k') => {
76                if self.viewport_start > 0 {
77                    self.viewport_start -= 1;
78                }
79            }
80            KeyCode::Down | KeyCode::Char('j') => {
81                if self.viewport_start + self.viewport_height < self.total_lines {
82                    self.viewport_start += 1;
83                }
84            }
85            KeyCode::PageUp | KeyCode::Char('b') => {
86                self.viewport_start = self.viewport_start.saturating_sub(self.viewport_height);
87            }
88            KeyCode::PageDown | KeyCode::Char(' ') | KeyCode::Char('f') => {
89                let new_start = self.viewport_start + self.viewport_height;
90                if new_start + self.viewport_height <= self.total_lines {
91                    self.viewport_start = new_start;
92                } else if self.total_lines > self.viewport_height {
93                    self.viewport_start = self.total_lines - self.viewport_height;
94                }
95            }
96            KeyCode::Home | KeyCode::Char('g') => {
97                self.viewport_start = 0;
98            }
99            KeyCode::End | KeyCode::Char('G') => {
100                if self.total_lines > self.viewport_height {
101                    self.viewport_start = self.total_lines - self.viewport_height;
102                }
103            }
104            _ => {}
105        }
106        false
107    }
108
109    fn draw(&self, stdout: &mut io::Stdout) -> Result<()> {
110        execute!(stdout, Clear(ClearType::All), MoveTo(0, 0))?;
111
112        // Draw content
113        let end = (self.viewport_start + self.viewport_height).min(self.total_lines);
114        for i in self.viewport_start..end {
115            execute!(stdout, Print(&self.lines[i]), Print("\r\n"))?;
116        }
117
118        // Fill empty lines if needed
119        let displayed = end - self.viewport_start;
120        for _ in displayed..self.viewport_height {
121            execute!(stdout, Print("~\r\n"))?;
122        }
123
124        // Draw status bar
125        let (width, _) = terminal::size().unwrap_or((80, 24));
126        let position = if self.total_lines == 0 {
127            "Empty".to_string()
128        } else {
129            let percent = if self.total_lines <= self.viewport_height {
130                100
131            } else {
132                ((self.viewport_start + self.viewport_height) * 100 / self.total_lines).min(100)
133            };
134            format!(
135                "Lines {}-{}/{} ({}%)",
136                self.viewport_start + 1,
137                end,
138                self.total_lines,
139                percent
140            )
141        };
142
143        execute!(
144            stdout,
145            SetForegroundColor(Color::Black),
146            crossterm::style::SetBackgroundColor(Color::White),
147            Print(format!("{:<width$}", position, width = width as usize)),
148            ResetColor,
149            Print("\r\n")
150        )?;
151
152        // Draw help line
153        execute!(
154            stdout,
155            SetForegroundColor(Color::DarkGrey),
156            Print("↑/k:up ↓/j:down PgUp/b:page-up PgDn/f:page-down g:top G:bottom q:quit"),
157            ResetColor
158        )?;
159
160        stdout.flush()?;
161        Ok(())
162    }
163}