1mod app;
2mod event;
3mod state;
4mod stats;
5mod ui;
6
7use std::io::{self, Stdout};
8use std::sync::Arc;
9use std::sync::mpsc;
10use std::time::{Duration, Instant};
11
12use pagers_core::Cancellation;
13use pagers_core::events::Event as CoreEvent;
14use pagers_core::mincore::PageMap;
15use pagers_core::ops::Stats;
16use ratatui_core::buffer::Buffer;
17use ratatui_core::layout::Rect;
18use ratatui_core::terminal::{Terminal, TerminalOptions, Viewport};
19use ratatui_core::widgets::Widget;
20use ratatui_crossterm::{CrosstermBackend, crossterm};
21
22use app::App;
23use state::FileState;
24
25const MAX_DISPLAY_FILES: u16 = 8;
26const MAX_DISPLAY_PAGES: usize = 32;
27const FRAME_BUDGET: Duration = Duration::from_millis(100);
28
29struct TerminalGuard {
30 terminal: Terminal<CrosstermBackend<Stdout>>,
31 _state: TerminalStateGuard,
32}
33
34struct TerminalStateGuard;
35
36impl TerminalStateGuard {
37 fn new() -> io::Result<Self> {
38 crossterm::terminal::enable_raw_mode()?;
39 Ok(Self)
40 }
41}
42
43impl Drop for TerminalStateGuard {
44 fn drop(&mut self) {
45 let _ = crossterm::execute!(io::stdout(), crossterm::cursor::Show);
46 let _ = crossterm::terminal::disable_raw_mode();
47 }
48}
49
50impl TerminalGuard {
51 fn new(viewport_height: u16) -> io::Result<Self> {
52 let state = TerminalStateGuard::new()?;
53 crossterm::execute!(io::stdout(), crossterm::cursor::Hide)?;
54 let backend = CrosstermBackend::new(io::stdout());
55 let terminal = Terminal::with_options(
56 backend,
57 TerminalOptions {
58 viewport: Viewport::Inline(viewport_height),
59 },
60 )?;
61 Ok(Self {
62 terminal,
63 _state: state,
64 })
65 }
66}
67
68struct RenderContext<'a> {
69 core_stats: &'a Stats,
70 label: &'a str,
71 action_sign: isize,
72}
73
74impl RenderContext<'_> {
75 fn render<PM: PageMap>(
76 &self,
77 files: &[&FileState<PM>],
78 file_rows_hwm: u16,
79 elapsed: f64,
80 area: Rect,
81 buf: &mut Buffer,
82 ) {
83 let [files_area, stats_area] = ui::layout(file_rows_hwm, area);
84 ui::FileListWidget {
85 files,
86 max_rows: file_rows_hwm,
87 }
88 .render(files_area, buf);
89 stats::SummaryWidget {
90 stats: self.core_stats,
91 elapsed,
92 label: self.label,
93 action_sign: self.action_sign,
94 }
95 .render(stats_area, buf);
96 }
97}
98
99fn drain_events<PM: PageMap>(
100 app: &mut App<PM>,
101 rx: &mpsc::Receiver<event::TuiEvent<PM>>,
102) -> app::ControlFlow {
103 while let Ok(evt) = rx.try_recv() {
104 match app.handle_event(evt) {
105 app::ControlFlow::Continue => {}
106 flow => return flow,
107 }
108 }
109 app::ControlFlow::Continue
110}
111
112pub fn run<PM: PageMap + Send + 'static>(
113 rx: mpsc::Receiver<CoreEvent<PM>>,
114 cancellation: Cancellation,
115 core_stats: Arc<Stats>,
116 label: &str,
117 action_sign: isize,
118 start: Instant,
119) -> io::Result<()> {
120 let viewport_height = MAX_DISPLAY_FILES + stats::SUMMARY_LINES;
121 let mut guard = TerminalGuard::new(viewport_height)?;
122
123 let tui_rx = event::spawn_event_threads(rx, cancellation.clone());
124 let mut app = App::new();
125 let mut file_rows_hwm: u16 = 0;
126 let ctx = RenderContext {
127 core_stats: &core_stats,
128 label,
129 action_sign,
130 };
131
132 let flow = loop {
133 let flow = match tui_rx.recv_timeout(FRAME_BUDGET) {
134 Ok(evt) => app.handle_event(evt),
135 Err(mpsc::RecvTimeoutError::Timeout) => app::ControlFlow::Continue,
136 Err(mpsc::RecvTimeoutError::Disconnected) => break app::ControlFlow::Quit,
137 };
138
139 let flow = match flow {
140 app::ControlFlow::Continue => drain_events(&mut app, &tui_rx),
141 other => other,
142 };
143
144 let elapsed = start.elapsed().as_secs_f64();
145 let files = app.visible_files(MAX_DISPLAY_FILES as usize);
146 file_rows_hwm = file_rows_hwm.max(files.len().min(MAX_DISPLAY_FILES as usize) as u16);
147 guard.terminal.draw(|frame| {
148 ctx.render(
149 &files,
150 file_rows_hwm,
151 elapsed,
152 frame.area(),
153 frame.buffer_mut(),
154 );
155 })?;
156
157 match flow {
158 app::ControlFlow::Continue => {}
159 other => break other,
160 }
161 };
162
163 if matches!(flow, app::ControlFlow::Quit) {
164 cancellation.cancel();
165 }
166
167 if matches!(flow, app::ControlFlow::Done) {
168 let elapsed = start.elapsed().as_secs_f64();
169 let files = app.visible_files(MAX_DISPLAY_FILES as usize);
170 file_rows_hwm = file_rows_hwm.max(files.len().min(MAX_DISPLAY_FILES as usize) as u16);
171 let total_lines = file_rows_hwm + stats::SUMMARY_LINES;
172
173 let _ = guard.terminal.insert_before(total_lines, |buf| {
174 ctx.render(&files, file_rows_hwm, elapsed, buf.area, buf);
175 });
176 }
177
178 Ok(())
179}