Skip to content

Commit 278ce4c

Browse files
authored
feat: add hotkey to clear all connections (x) (#108)
1 parent f0141be commit 278ce4c

7 files changed

Lines changed: 110 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ See [INSTALL.md](INSTALL.md) for detailed permission setup and [USAGE.md](USAGE.
152152
|-----|--------|
153153
| `q` | Quit (press twice to confirm) |
154154
| `Ctrl+C` | Quit immediately |
155+
| `x` | Clear all connections (press twice to confirm) |
155156
| `Tab` | Switch between tabs |
156157
| `i` | Toggle interface statistics view |
157158
| `↑/k` `↓/j` | Navigate up/down |

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ The experimental eBPF support provides efficient process identification but has
121121

122122
- [x] **Terminal User Interface**: TUI built with ratatui with adjustable column widths
123123
- [x] **Sortable Columns**: Keyboard-based sorting by all table columns
124-
- [x] **Keyboard Controls**: Comprehensive keyboard navigation (q, Ctrl+C, Tab, arrows, j/k, PageUp/Down, Enter, Esc, c, p, s, S, h, /)
124+
- [x] **Keyboard Controls**: Comprehensive keyboard navigation (q, Ctrl+C, x, Tab, arrows, j/k, PageUp/Down, Enter, Esc, c, p, s, S, h, /)
125125
- [x] **Connection Details View**: Detailed information about selected connections (Enter key)
126126
- [x] **Help Screen**: Toggle help screen with keyboard shortcuts (h key)
127127
- [x] **Clipboard Support**: Copy remote address to clipboard (c key)

USAGE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ Log files are created in the `logs/` directory with timestamp: `rustnet_YYYY-MM-
259259
- `p` - Toggle between service names and port numbers
260260
- `d` - Toggle between hostnames and IP addresses (requires `--resolve-dns`)
261261
- `/` - Enter filter mode (vim-style search with real-time results)
262+
- `x` - Clear all connections and reset statistics (press twice to confirm)
262263

263264
### Sorting
264265

src/app.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ pub struct App {
230230
/// Control flag for graceful shutdown
231231
should_stop: Arc<AtomicBool>,
232232

233+
/// Active connections map (shared with background threads)
234+
connections: Arc<DashMap<String, Connection>>,
235+
233236
/// Current connections snapshot for UI
234237
connections_snapshot: Arc<RwLock<Vec<Connection>>>,
235238

@@ -294,6 +297,7 @@ impl App {
294297
Ok(Self {
295298
config,
296299
should_stop: Arc::new(AtomicBool::new(false)),
300+
connections: Arc::new(DashMap::new()),
297301
connections_snapshot: Arc::new(RwLock::new(Vec::new())),
298302
service_lookup: Arc::new(service_lookup),
299303
stats: Arc::new(AppStats::default()),
@@ -316,8 +320,8 @@ impl App {
316320
pub fn start(&mut self) -> Result<()> {
317321
info!("Starting network monitor application");
318322

319-
// Create shared connection map
320-
let connections: Arc<DashMap<String, Connection>> = Arc::new(DashMap::new());
323+
// Use stored connection map
324+
let connections = Arc::clone(&self.connections);
321325

322326
// Start packet capture pipeline
323327
self.start_packet_capture_pipeline(connections.clone())?;
@@ -1251,6 +1255,50 @@ impl App {
12511255
self.dns_resolver.is_some()
12521256
}
12531257

1258+
/// Clear all connections and related data, starting fresh
1259+
/// This clears:
1260+
/// - All tracked connections
1261+
/// - Traffic history (graph data)
1262+
/// - RTT measurements
1263+
/// - QUIC connection mappings
1264+
/// - Resets statistics counters
1265+
pub fn clear_all_connections(&self) {
1266+
info!("Clearing all connections and resetting statistics");
1267+
1268+
// Clear the main connections map
1269+
self.connections.clear();
1270+
1271+
// Clear the UI snapshot
1272+
if let Ok(mut snapshot) = self.connections_snapshot.write() {
1273+
snapshot.clear();
1274+
}
1275+
1276+
// Clear traffic history
1277+
if let Ok(mut history) = self.traffic_history.write() {
1278+
history.clear();
1279+
}
1280+
1281+
// Clear RTT tracker
1282+
if let Ok(mut tracker) = self.rtt_tracker.lock() {
1283+
tracker.clear();
1284+
}
1285+
1286+
// Clear QUIC connection ID mappings
1287+
if let Ok(mut mapping) = QUIC_CONNECTION_MAPPING.lock() {
1288+
mapping.clear();
1289+
}
1290+
1291+
// Reset statistics counters
1292+
self.stats.packets_processed.store(0, Ordering::Relaxed);
1293+
self.stats.packets_dropped.store(0, Ordering::Relaxed);
1294+
self.stats.connections_tracked.store(0, Ordering::Relaxed);
1295+
self.stats.total_tcp_retransmits.store(0, Ordering::Relaxed);
1296+
self.stats.total_tcp_out_of_order.store(0, Ordering::Relaxed);
1297+
self.stats.total_tcp_fast_retransmits.store(0, Ordering::Relaxed);
1298+
1299+
info!("All connections cleared successfully");
1300+
}
1301+
12541302
/// Stop all threads gracefully
12551303
pub fn stop(&self) {
12561304
info!("Stopping application");

src/main.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,12 +462,14 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
462462
// Tab navigation (forward)
463463
(KeyCode::Tab, KeyModifiers::NONE) => {
464464
ui_state.quit_confirmation = false;
465+
ui_state.clear_confirmation = false;
465466
ui_state.selected_tab = (ui_state.selected_tab + 1) % 5;
466467
}
467468

468469
// Shift+Tab navigation (backward)
469470
(KeyCode::BackTab, _) | (KeyCode::Tab, KeyModifiers::SHIFT) => {
470471
ui_state.quit_confirmation = false;
472+
ui_state.clear_confirmation = false;
471473
ui_state.selected_tab = if ui_state.selected_tab == 0 {
472474
4 // Wrap to last tab
473475
} else {
@@ -478,6 +480,7 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
478480
// Help toggle
479481
(KeyCode::Char('h'), _) => {
480482
ui_state.quit_confirmation = false;
483+
ui_state.clear_confirmation = false;
481484
ui_state.show_help = !ui_state.show_help;
482485
if ui_state.show_help {
483486
ui_state.selected_tab = 4; // Switch to help tab
@@ -489,6 +492,7 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
489492
// Interface stats toggle (shortcut to Interface tab)
490493
(KeyCode::Char('i'), _) | (KeyCode::Char('I'), _) => {
491494
ui_state.quit_confirmation = false;
495+
ui_state.clear_confirmation = false;
492496
if ui_state.selected_tab == 2 {
493497
ui_state.selected_tab = 0; // Back to overview
494498
} else {
@@ -499,6 +503,7 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
499503
// Navigation in connection list
500504
(KeyCode::Up, _) | (KeyCode::Char('k'), _) => {
501505
ui_state.quit_confirmation = false;
506+
ui_state.clear_confirmation = false;
502507
// Use the SAME sorted connections list from the main loop
503508
// to ensure index consistency with the displayed table
504509
debug!("Navigation UP: {} connections available", connections.len());
@@ -507,6 +512,7 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
507512

508513
(KeyCode::Down, _) | (KeyCode::Char('j'), _) => {
509514
ui_state.quit_confirmation = false;
515+
ui_state.clear_confirmation = false;
510516
// Use the SAME sorted connections list from the main loop
511517
// to ensure index consistency with the displayed table
512518
debug!(
@@ -519,13 +525,15 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
519525
// Page Up/Down navigation
520526
(KeyCode::PageUp, _) => {
521527
ui_state.quit_confirmation = false;
528+
ui_state.clear_confirmation = false;
522529
// Use the SAME sorted connections list from the main loop
523530
// Move up by roughly 10 items (or adjust based on terminal height)
524531
ui_state.move_selection_page_up(&connections, 10);
525532
}
526533

527534
(KeyCode::PageDown, _) => {
528535
ui_state.quit_confirmation = false;
536+
ui_state.clear_confirmation = false;
529537
// Use the SAME sorted connections list from the main loop
530538
// Move down by roughly 10 items (or adjust based on terminal height)
531539
ui_state.move_selection_page_down(&connections, 10);
@@ -534,19 +542,22 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
534542
// Vim-style jump to first/last (g/G)
535543
(KeyCode::Char('g'), KeyModifiers::NONE) => {
536544
ui_state.quit_confirmation = false;
545+
ui_state.clear_confirmation = false;
537546
// Jump to first connection (vim-style 'g')
538547
ui_state.move_selection_to_first(&connections);
539548
}
540549

541550
(KeyCode::Char('G'), _) | (KeyCode::Char('g'), KeyModifiers::SHIFT) => {
542551
ui_state.quit_confirmation = false;
552+
ui_state.clear_confirmation = false;
543553
// Jump to last connection (vim-style 'G')
544554
ui_state.move_selection_to_last(&connections);
545555
}
546556

547557
// Enter to view details
548558
(KeyCode::Enter, _) => {
549559
ui_state.quit_confirmation = false;
560+
ui_state.clear_confirmation = false;
550561
if ui_state.selected_tab == 0 && !connections.is_empty() {
551562
ui_state.selected_tab = 1; // Switch to details view
552563
}
@@ -555,6 +566,7 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
555566
// Toggle port number display
556567
(KeyCode::Char('p'), _) => {
557568
ui_state.quit_confirmation = false;
569+
ui_state.clear_confirmation = false;
558570
ui_state.show_port_numbers = !ui_state.show_port_numbers;
559571
info!(
560572
"Toggled port display: {}",
@@ -570,6 +582,7 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
570582
(KeyCode::Char('d'), _) => {
571583
if app.is_dns_resolution_enabled() {
572584
ui_state.quit_confirmation = false;
585+
ui_state.clear_confirmation = false;
573586
ui_state.show_hostnames = !ui_state.show_hostnames;
574587
info!(
575588
"Toggled hostname display: {}",
@@ -585,6 +598,7 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
585598
// Cycle sort column with 's'
586599
(KeyCode::Char('s'), KeyModifiers::NONE) => {
587600
ui_state.quit_confirmation = false;
601+
ui_state.clear_confirmation = false;
588602
ui_state.cycle_sort_column();
589603
info!(
590604
"Sort column: {} ({})",
@@ -600,6 +614,7 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
600614
// Toggle sort direction with 'S' (Shift+s)
601615
(KeyCode::Char('S'), _) => {
602616
ui_state.quit_confirmation = false;
617+
ui_state.clear_confirmation = false;
603618
ui_state.toggle_sort_direction();
604619
info!(
605620
"Sort direction: {} ({})",
@@ -615,6 +630,7 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
615630
// Copy remote address to clipboard
616631
(KeyCode::Char('c'), _) => {
617632
ui_state.quit_confirmation = false;
633+
ui_state.clear_confirmation = false;
618634
if let Some(selected_idx) = ui_state.get_selected_index(&connections)
619635
&& let Some(conn) = connections.get(selected_idx)
620636
{
@@ -671,9 +687,28 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
671687
}
672688
}
673689

690+
// Clear all connections with confirmation
691+
(KeyCode::Char('x'), _) => {
692+
ui_state.quit_confirmation = false;
693+
if ui_state.clear_confirmation {
694+
info!("User confirmed clear all connections");
695+
app.clear_all_connections();
696+
ui_state.clear_confirmation = false;
697+
ui_state.selected_connection_key = None;
698+
ui_state.clipboard_message = Some((
699+
"All connections cleared".to_string(),
700+
std::time::Instant::now(),
701+
));
702+
} else {
703+
info!("User requested clear - showing confirmation");
704+
ui_state.clear_confirmation = true;
705+
}
706+
}
707+
674708
// Escape to go back or clear filter
675709
(KeyCode::Esc, _) => {
676710
ui_state.quit_confirmation = false;
711+
ui_state.clear_confirmation = false;
677712
if !ui_state.filter_query.is_empty() {
678713
// Clear filter if one is active
679714
ui_state.clear_filter();
@@ -684,9 +719,10 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
684719
}
685720
}
686721

687-
// Any other key resets quit confirmation
722+
// Any other key resets confirmations
688723
_ => {
689724
ui_state.quit_confirmation = false;
725+
ui_state.clear_confirmation = false;
690726
}
691727
}
692728
}

src/network/types.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,11 @@ impl TrafficHistory {
796796
pub fn has_enough_data(&self) -> bool {
797797
self.samples.len() >= 2
798798
}
799+
800+
/// Clear all traffic history samples
801+
pub fn clear(&mut self) {
802+
self.samples.clear();
803+
}
799804
}
800805

801806
impl Default for TrafficHistory {
@@ -898,6 +903,12 @@ impl RttTracker {
898903
let cutoff = Instant::now() - self.max_pending_age;
899904
self.pending_syns.retain(|_, ts| *ts > cutoff);
900905
}
906+
907+
/// Clear all RTT tracking data
908+
pub fn clear(&mut self) {
909+
self.pending_syns.clear();
910+
self.recent_rtts.clear();
911+
}
901912
}
902913

903914
impl Default for RttTracker {

src/ui.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ pub struct UIState {
116116
pub selected_connection_key: Option<String>,
117117
pub show_help: bool,
118118
pub quit_confirmation: bool,
119+
pub clear_confirmation: bool,
119120
pub clipboard_message: Option<(String, std::time::Instant)>,
120121
pub filter_mode: bool,
121122
pub filter_query: String,
@@ -134,6 +135,7 @@ impl Default for UIState {
134135
selected_connection_key: None,
135136
show_help: false,
136137
quit_confirmation: false,
138+
clear_confirmation: false,
137139
clipboard_message: None,
138140
filter_mode: false,
139141
filter_query: String::new(),
@@ -2053,6 +2055,10 @@ fn draw_help(f: &mut Frame, area: Rect) -> Result<()> {
20532055
Span::styled("Ctrl+C ", Style::default().fg(Color::Yellow)),
20542056
Span::raw("Quit immediately"),
20552057
]),
2058+
Line::from(vec![
2059+
Span::styled("x ", Style::default().fg(Color::Yellow)),
2060+
Span::raw("Clear all connections (press twice to confirm)"),
2061+
]),
20562062
Line::from(vec![
20572063
Span::styled("Tab ", Style::default().fg(Color::Yellow)),
20582064
Span::raw("Switch between tabs"),
@@ -2361,6 +2367,8 @@ fn draw_filter_input(f: &mut Frame, ui_state: &UIState, area: Rect) {
23612367
fn draw_status_bar(f: &mut Frame, ui_state: &UIState, connection_count: usize, area: Rect) {
23622368
let status = if ui_state.quit_confirmation {
23632369
" Press 'q' again to quit or any other key to cancel ".to_string()
2370+
} else if ui_state.clear_confirmation {
2371+
" Press 'x' again to clear all connections or any other key to cancel ".to_string()
23642372
} else if let Some((ref msg, ref time)) = ui_state.clipboard_message {
23652373
// Show clipboard message for 3 seconds
23662374
if time.elapsed().as_secs() < 3 {
@@ -2383,7 +2391,7 @@ fn draw_status_bar(f: &mut Frame, ui_state: &UIState, connection_count: usize, a
23832391
)
23842392
};
23852393

2386-
let style = if ui_state.quit_confirmation {
2394+
let style = if ui_state.quit_confirmation || ui_state.clear_confirmation {
23872395
Style::default().fg(Color::Black).bg(Color::Yellow)
23882396
} else if ui_state.clipboard_message.is_some()
23892397
&& ui_state

0 commit comments

Comments
 (0)