detached_shell/
scrollback.rs1use 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 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 let (_, height) = terminal::size().unwrap_or((80, 24));
28 let viewport_height = (height - 3) as usize; ScrollbackViewer {
31 lines,
32 viewport_start: 0,
33 viewport_height,
34 total_lines,
35 }
36 }
37
38 pub fn run(&mut self) -> Result<()> {
39 let mut stdout = io::stdout();
41 terminal::enable_raw_mode()?;
42 execute!(stdout, EnterAlternateScreen, Hide)?;
43
44 let result = self.event_loop();
45
46 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, 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, 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 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 let displayed = end - self.viewport_start;
120 for _ in displayed..self.viewport_height {
121 execute!(stdout, Print("~\r\n"))?;
122 }
123
124 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 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}