1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//! Terminal setup, event loop, and teardown.
use std::io;
use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event};
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use crate::error::Result;
use super::app::App;
use super::ui;
/// Enter raw mode, switch to the alternate screen, and create a ratatui
/// [`Terminal`].
///
/// Call [`restore_terminal`] when you are done to undo the changes.
pub fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
/// Undo the changes made by [`init_terminal`]: leave the alternate screen,
/// disable mouse capture, show the cursor, and restore canonical mode.
pub fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
DisableMouseCapture,
LeaveAlternateScreen
)?;
terminal.show_cursor()?;
Ok(())
}
/// Run the event loop on an already-initialised terminal.
///
/// The caller is responsible for calling [`init_terminal`] before and
/// [`restore_terminal`] after this function.
pub fn run_event_loop(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
mut app: App,
) -> Result<()> {
event_loop(terminal, &mut app)
}
fn event_loop(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, app: &mut App) -> Result<()> {
use super::state::LiveMode;
let mut last_render = std::time::Instant::now();
loop {
// Throttle rendering during indexing to ~60 fps so we spend more
// time indexing and less time redrawing.
let indexing_active = app.index_progress.is_some() || app.bg_indexer.is_some();
let should_render =
!indexing_active || last_render.elapsed() >= std::time::Duration::from_millis(16);
if should_render {
terminal.draw(|f| ui::render(f, app))?;
last_render = std::time::Instant::now();
}
// Drive chunked file indexing (non-blocking — user can interact).
let was_indexing = indexing_active;
if was_indexing {
app.index_tick();
}
let indexing_now = app.index_progress.is_some() || app.bg_indexer.is_some();
// Indexing just finished — loop back to redraw immediately so the
// user sees the final packet list instead of a stale "Indexing"
// screen while we block on event::read().
if was_indexing && !indexing_now {
continue;
}
// If a filter scan is in progress, drive it in chunks.
if app.filter_progress.is_some() {
if event::poll(std::time::Duration::from_millis(0))?
&& let Event::Key(key) = event::read()?
&& key.code == crossterm::event::KeyCode::Esc
{
app.filter_progress = None;
continue;
}
app.filter_tick();
continue;
}
// If a stats collection is in progress, drive it in chunks.
if app.stats_progress.is_some() {
if event::poll(std::time::Duration::from_millis(0))?
&& let Event::Key(key) = event::read()?
&& key.code == crossterm::event::KeyCode::Esc
{
app.stats_progress = None;
continue;
}
app.stats_tick();
continue;
}
// If a stream build is in progress, drive it in chunks.
if app.stream_build_progress.is_some() {
if event::poll(std::time::Duration::from_millis(0))?
&& let Event::Key(key) = event::read()?
&& key.code == crossterm::event::KeyCode::Esc
{
app.stream_build_progress = None;
continue;
}
app.stream_tick();
continue;
}
// Live capture: drive tick and use poll-based event reading.
if app.live_mode.is_some() {
if matches!(app.live_mode, Some(LiveMode::Live)) {
app.live_tick();
} else if matches!(app.live_mode, Some(LiveMode::Paused)) {
// Still check for EOF while paused.
app.check_eof();
}
let timeout = match app.live_mode {
Some(LiveMode::Live) => std::time::Duration::from_millis(200),
Some(LiveMode::Paused) => std::time::Duration::from_millis(500),
_ => std::time::Duration::from_secs(60),
};
if event::poll(timeout)? {
match event::read()? {
Event::Key(key) => app.handle_key(key),
Event::Mouse(mouse) => app.handle_mouse(mouse),
Event::Resize(_, _) => app.on_resize(),
_ => {}
}
}
} else if indexing_now {
// File indexing in progress: use a short poll timeout so the OS
// scheduler can run the background indexer thread efficiently.
// 1 ms is imperceptible to the user but avoids a tight CPU spin.
if event::poll(std::time::Duration::from_millis(1))? {
match event::read()? {
Event::Key(key) => app.handle_key(key),
Event::Mouse(mouse) => app.handle_mouse(mouse),
Event::Resize(_, _) => app.on_resize(),
_ => {}
}
}
} else {
// Static file mode: blocking event read.
match event::read()? {
Event::Key(key) => app.handle_key(key),
Event::Mouse(mouse) => app.handle_mouse(mouse),
Event::Resize(_, _) => app.on_resize(),
_ => {}
}
}
if !app.running {
break;
}
}
super::state::save_history(&app.filter.history);
Ok(())
}
#[cfg(all(test, feature = "tui"))]
mod tests {
// `init_terminal`, `restore_terminal`, and `run_event_loop` require a
// real TTY and mutate global terminal state (raw mode, alternate screen),
// so they cannot be exercised safely from parallel unit tests. This
// module exists to satisfy the repo-wide convention that every `src/tui/`
// file carries a `cfg(test)` block. Integration coverage lives in the
// end-to-end CLI tests.
#[test]
fn module_compiles() {}
}