Skip to content

Commit 558d05b

Browse files
committed
Add enhanced navigation and network interface cycling
1 parent d2e3dff commit 558d05b

File tree

2 files changed

+155
-29
lines changed

2 files changed

+155
-29
lines changed

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ A real-time terminal-based system monitor built with Rust and ratatui. Gribble p
99
- **System Monitoring**: Real-time CPU usage with progress bars, memory statistics, system uptime, and architecture details
1010
- **Process Management**: Interactive process viewer with CPU and memory usage, sortable by resource consumption
1111
- **File Explorer**: Navigate filesystem with keyboard controls, directory traversal with visual indicators
12-
- **Network Traffic**: Live network monitoring with sparkline graphs showing download/upload rates and totals
13-
- **Keyboard Navigation**: Vim-style navigation (hjkl) plus arrow keys for intuitive control
12+
- **Network Traffic**: Live network monitoring with sparkline graphs, cycle through multiple network interfaces
13+
- **Keyboard Navigation**: Vim-style navigation (hjkl) plus arrow keys, page navigation (PgUp/PgDn), and jump keys (Home/End)
1414

1515
## Installation
1616

@@ -38,7 +38,9 @@ cargo run
3838
### Navigation
3939

4040
- `←→` or `h l` - Switch between panels
41-
- `↑↓` or `j k` - Navigate within Process Manager and File Explorer
41+
- `↑↓` or `j k` - Navigate within lists, cycle network interfaces
42+
- `PgUp/PgDn` - Jump by page in lists
43+
- `Home/End` - Jump to first/last item in lists
4244
- `Enter` - Open directories in File Explorer
4345
- `r` - Refresh all data
4446
- `?` - Show/hide help
@@ -48,9 +50,9 @@ cargo run
4850

4951
1. **System Monitor** - CPU usage with visual bars, memory statistics, process count, system information
5052
2. **System Status** - Current time/date, disk usage, network interface statistics, system load
51-
3. **Process Manager** - Live process list sorted by CPU usage, shows memory consumption, navigable selection
52-
4. **File Explorer** - Directory browser with folder/file icons, supports navigation up and into directories
53-
5. **Network Graph** - Real-time network traffic visualization with separate RX/TX sparkline graphs
53+
3. **Process Manager** - Live process list sorted by CPU usage, full navigation support
54+
4. **File Explorer** - Directory browser with folder/file icons, full navigation support
55+
5. **Network Graph** - Real-time network traffic with interface cycling, separate RX/TX sparkline graphs
5456

5557
## Technical Details
5658

src/main.rs

Lines changed: 147 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ struct App {
2525
dir_entries: Vec<String>,
2626
selected_process: usize,
2727
selected_file: usize,
28+
selected_network: usize,
2829
show_help: bool,
2930
process_list_state: ListState,
3031
file_list_state: ListState,
@@ -39,6 +40,7 @@ struct NetworkHistory {
3940
last_rx_bytes: u64,
4041
last_tx_bytes: u64,
4142
max_history: usize,
43+
current_interface: String,
4244
}
4345

4446
impl NetworkHistory {
@@ -51,14 +53,31 @@ impl NetworkHistory {
5153
last_rx_bytes: 0,
5254
last_tx_bytes: 0,
5355
max_history: 60, // Keep 60 data points (2 minutes at 2-second intervals)
56+
current_interface: String::new(),
5457
}
5558
}
5659

57-
fn update(&mut self, networks: &Networks) {
58-
let (total_rx, total_tx) = networks.list().iter()
59-
.fold((0, 0), |(rx_acc, tx_acc), (_, network)| {
60-
(rx_acc + network.total_received(), tx_acc + network.total_transmitted())
61-
});
60+
fn update(&mut self, networks: &Networks, selected_interface: &str) {
61+
// Find the selected network interface or use the first available one
62+
let network_list: Vec<_> = networks.list().iter().collect();
63+
let (interface_name, network_data) = if let Some(item) = network_list.get(0) {
64+
// If we have a specific interface selected, try to find it
65+
if !selected_interface.is_empty() {
66+
network_list.iter()
67+
.find(|(name, _)| *name == selected_interface)
68+
.unwrap_or(item)
69+
} else {
70+
item
71+
}
72+
} else {
73+
return; // No network interfaces available
74+
};
75+
76+
// Update current interface name
77+
self.current_interface = interface_name.to_string();
78+
79+
let total_rx = network_data.total_received();
80+
let total_tx = network_data.total_transmitted();
6281

6382
if self.last_rx_bytes > 0 && self.last_tx_bytes > 0 {
6483
// Calculate rate (bytes per 2 seconds)
@@ -132,6 +151,7 @@ impl App {
132151
dir_entries,
133152
selected_process: 0,
134153
selected_file: 0,
154+
selected_network: 0,
135155
show_help: false,
136156
process_list_state,
137157
file_list_state,
@@ -171,7 +191,16 @@ impl App {
171191
self.system.refresh_all();
172192
self.disks.refresh(true);
173193
self.networks.refresh(true);
174-
self.network_history.update(&self.networks);
194+
195+
// Get the selected network interface name
196+
let network_list: Vec<_> = self.networks.list().iter().collect();
197+
let selected_interface_name = if let Some((name, _)) = network_list.get(self.selected_network) {
198+
name.to_string()
199+
} else {
200+
String::new()
201+
};
202+
203+
self.network_history.update(&self.networks, &selected_interface_name);
175204
self.last_update = Instant::now();
176205
}
177206
}
@@ -211,13 +240,25 @@ impl App {
211240
self.file_list_state.select(Some(self.selected_file));
212241
}
213242
}
243+
4 => { // Network panel - cycle to previous interface
244+
let network_count = self.networks.list().len();
245+
if network_count > 0 {
246+
self.selected_network = if self.selected_network == 0 {
247+
network_count - 1
248+
} else {
249+
self.selected_network - 1
250+
};
251+
// Reset network history when switching interfaces
252+
self.network_history = NetworkHistory::new();
253+
}
254+
}
214255
_ => {}
215256
}
216257
}
217258
KeyCode::Down | KeyCode::Char('j') => {
218259
match self.selected_panel {
219260
2 => { // Process manager
220-
let max_processes = self.system.processes().len().min(10);
261+
let max_processes = self.system.processes().len();
221262
if self.selected_process < max_processes - 1 {
222263
self.selected_process += 1;
223264
self.process_list_state.select(Some(self.selected_process));
@@ -229,6 +270,78 @@ impl App {
229270
self.file_list_state.select(Some(self.selected_file));
230271
}
231272
}
273+
4 => { // Network panel - cycle to next interface
274+
let network_count = self.networks.list().len();
275+
if network_count > 0 {
276+
self.selected_network = (self.selected_network + 1) % network_count;
277+
// Reset network history when switching interfaces
278+
self.network_history = NetworkHistory::new();
279+
}
280+
}
281+
_ => {}
282+
}
283+
}
284+
KeyCode::PageUp => {
285+
match self.selected_panel {
286+
2 => { // Process manager
287+
let page_size = 10; // Approximate visible items per page
288+
self.selected_process = self.selected_process.saturating_sub(page_size);
289+
self.process_list_state.select(Some(self.selected_process));
290+
}
291+
3 => { // File browser
292+
let page_size = 10;
293+
self.selected_file = self.selected_file.saturating_sub(page_size);
294+
self.file_list_state.select(Some(self.selected_file));
295+
}
296+
_ => {}
297+
}
298+
}
299+
KeyCode::PageDown => {
300+
match self.selected_panel {
301+
2 => { // Process manager
302+
let page_size = 10;
303+
let max_processes = self.system.processes().len();
304+
self.selected_process = (self.selected_process + page_size).min(max_processes.saturating_sub(1));
305+
self.process_list_state.select(Some(self.selected_process));
306+
}
307+
3 => { // File browser
308+
let page_size = 10;
309+
let max_files = self.dir_entries.len();
310+
self.selected_file = (self.selected_file + page_size).min(max_files.saturating_sub(1));
311+
self.file_list_state.select(Some(self.selected_file));
312+
}
313+
_ => {}
314+
}
315+
}
316+
KeyCode::Home => {
317+
match self.selected_panel {
318+
2 => { // Process manager
319+
self.selected_process = 0;
320+
self.process_list_state.select(Some(0));
321+
}
322+
3 => { // File browser
323+
self.selected_file = 0;
324+
self.file_list_state.select(Some(0));
325+
}
326+
_ => {}
327+
}
328+
}
329+
KeyCode::End => {
330+
match self.selected_panel {
331+
2 => { // Process manager
332+
let max_processes = self.system.processes().len();
333+
if max_processes > 0 {
334+
self.selected_process = max_processes - 1;
335+
self.process_list_state.select(Some(self.selected_process));
336+
}
337+
}
338+
3 => { // File browser
339+
let max_files = self.dir_entries.len();
340+
if max_files > 0 {
341+
self.selected_file = max_files - 1;
342+
self.file_list_state.select(Some(self.selected_file));
343+
}
344+
}
232345
_ => {}
233346
}
234347
}
@@ -381,7 +494,7 @@ fn render(app: &App, frame: &mut Frame) {
381494
let help_text = if app.show_help {
382495
"ESC or ? to close • System Monitor v1.0"
383496
} else {
384-
"Navigation: ←→hl | ↑↓jk (navigate lists) | Enter (open directory) | r (refresh) | ? (help) | q (quit) • Live Updates"
497+
"Navigation: ←→hl | ↑↓jk/PgUp/PgDn/Home/End (navigate/cycle) | Enter (open dir) | r (refresh) | ? (help) | q (quit)"
385498
};
386499
let footer = Paragraph::new(help_text)
387500
.style(Style::default().fg(Color::DarkGray))
@@ -395,7 +508,9 @@ SYSTEM MONITOR - HELP
395508
396509
NAVIGATION:
397510
← → h l - Switch between panels
398-
↑ ↓ j k - Navigate within Task Manager
511+
↑ ↓ j k - Navigate within lists/cycle network interfaces
512+
PgUp/PgDn- Jump by page in lists
513+
Home/End - Jump to first/last item in lists
399514
Enter - Navigate directories (File Browser)
400515
r - Refresh all data
401516
? - Show/hide this help
@@ -404,9 +519,9 @@ NAVIGATION:
404519
PANELS:
405520
1. System Monitor - CPU, Memory, Uptime, Architecture
406521
2. System Status - Time, Disk usage, Network stats
407-
3. Process Manager- Top processes (navigable with j/k)
408-
4. File Explorer - Navigate directories (j/k + Enter)
409-
5. Network Graph - Real-time network traffic visualization
522+
3. Process Manager- Top processes (j/k/PgUp/PgDn/Home/End)
523+
4. File Explorer - Navigate directories (j/k/PgUp/PgDn/Home/End + Enter)
524+
5. Network Graph - Real-time network traffic (↑↓ to cycle interfaces)
410525
411526
FEATURES:
412527
• Real-time system monitoring
@@ -510,16 +625,16 @@ fn render_clock(app: &App, frame: &mut Frame, area: Rect, is_selected: bool) {
510625
(0, 0)
511626
};
512627

513-
// Get network info
514-
let network_info = app.networks.list().iter()
515-
.map(|(name, network)| {
516-
format!("{}: ↓{} MB ↑{} MB",
517-
name,
518-
network.total_received() / 1024 / 1024,
519-
network.total_transmitted() / 1024 / 1024)
520-
})
521-
.next()
522-
.unwrap_or_else(|| "No network data".to_string());
628+
// Get network info for the selected interface
629+
let network_list: Vec<_> = app.networks.list().iter().collect();
630+
let network_info = if let Some((name, network)) = network_list.get(app.selected_network) {
631+
format!("{}: ↓{} MB ↑{} MB",
632+
name,
633+
network.total_received() / 1024 / 1024,
634+
network.total_transmitted() / 1024 / 1024)
635+
} else {
636+
"No network data".to_string()
637+
};
523638

524639
let content = format!("▶ Time: {}\n▶ Date: {}\n▶ Boot disk: {} GB / {} GB\n▶ Disk usage: {:.1}%\n\n▶ Network: \n {}\n\n▶ Load avg: {:.2}",
525640
time_str,
@@ -627,8 +742,17 @@ fn render_network_graph(app: &App, frame: &mut Frame, area: Rect, is_selected: b
627742
Style::default().fg(Color::White)
628743
};
629744

745+
let interface_name = &app.network_history.current_interface;
746+
let network_count = app.networks.list().len();
747+
let title = if network_count > 1 {
748+
format!("📡 Network Traffic Monitor - {} ({}/{}) [↑↓ to cycle]",
749+
interface_name, app.selected_network + 1, network_count)
750+
} else {
751+
format!("📡 Network Traffic Monitor - {}", interface_name)
752+
};
753+
630754
let main_block = Block::default()
631-
.title("📡 Network Traffic Monitor")
755+
.title(title)
632756
.borders(Borders::ALL)
633757
.border_style(border_style);
634758

0 commit comments

Comments
 (0)