Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/display/components/help_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use ratatui::{
pub struct HelpText {
pub paused: bool,
pub show_dns: bool,
pub focused_pane: Option<usize>,
}

const FIRST_WIDTH_BREAKPOINT: u16 = 76;
Expand All @@ -19,6 +20,8 @@ const TEXT_WHEN_NOT_PAUSED: &str = " Press <SPACE> to pause.";
const TEXT_WHEN_DNS_NOT_SHOWN: &str = " (DNS queries hidden).";
const TEXT_WHEN_DNS_SHOWN: &str = " (DNS queries shown).";
const TEXT_TAB_TIP: &str = " Use <TAB> to rearrange tables.";
const TEXT_FOCUS_TIP: &str = " Use <N> to cycle focus.";
const TEXT_FREEZE_PANE_TIP: &str = " Press <F> to freeze focused pane.";

impl HelpText {
pub fn render(&self, frame: &mut Frame, rect: Rect) {
Expand All @@ -42,8 +45,20 @@ impl HelpText {
TEXT_TAB_TIP
};

let focus_tip = if rect.width <= SECOND_WIDTH_BREAKPOINT {
""
} else {
TEXT_FOCUS_TIP
};

let freeze_tip = if self.focused_pane.is_some() && rect.width > SECOND_WIDTH_BREAKPOINT {
TEXT_FREEZE_PANE_TIP
} else {
""
};

let text = Span::styled(
[pause_content, tab_text, dns_content].concat(),
[pause_content, tab_text, focus_tip, freeze_tip, dns_content].concat(),
Style::default().add_modifier(Modifier::BOLD),
);
let paragraph = Paragraph::new(text).alignment(Alignment::Left);
Expand Down
20 changes: 14 additions & 6 deletions src/display/components/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub struct Layout<'a> {
pub header: HeaderDetails<'a>,
pub children: Vec<Table>,
pub footer: HelpText,
pub pane_paused: [bool; 3],
}

impl Layout<'_> {
Expand Down Expand Up @@ -99,16 +100,23 @@ impl Layout<'_> {
}
}

pub fn render(&self, frame: &mut Frame, rect: Rect, table_cycle_offset: usize) {
pub fn render(
&self,
frame: &mut Frame,
rect: Rect,
table_cycle_offset: usize,
focused_pane: Option<usize>,
) {
let (top, app, bottom) = top_app_and_bottom_split(rect);
let layout_slots = self.build_layout(app);
let num_children = self.children.len();
for i in 0..layout_slots.len() {
if let Some(rect) = layout_slots.get(i) {
if let Some(child) = self
.children
.get((i + table_cycle_offset) % self.children.len())
{
child.render(frame, *rect);
let child_index = (i + table_cycle_offset) % num_children;
if let Some(child) = self.children.get(child_index) {
let focused = focused_pane == Some(child_index);
let paused = self.pane_paused.get(child_index).copied().unwrap_or(false);
child.render(frame, *rect, focused, paused);
}
}
}
Expand Down
30 changes: 27 additions & 3 deletions src/display/components/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use itertools::Itertools;
use ratatui::{
layout::{Constraint, Rect},
style::{Color, Style},
widgets::{Block, Borders, Row},
widgets::{Block, BorderType, Borders, Row},
Frame,
};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
Expand Down Expand Up @@ -371,7 +371,7 @@ impl Table {
}

/// See [`Table`] for layout rules.
pub fn render(&self, frame: &mut Frame, rect: Rect) {
pub fn render(&self, frame: &mut Frame, rect: Rect, focused: bool, paused: bool) {
let (computed_layout, spacer_width) = {
// pick the largest possible layout, constrained by the available width
let &(_, layout) = self
Expand Down Expand Up @@ -411,8 +411,32 @@ impl Table {
.map(Constraint::Length)
.collect();

let title = if paused {
format!("{} [PAUSED]", self.title)
} else {
self.title.to_string()
};
let title_style = if paused {
Style::default().fg(Color::Yellow)
} else {
Style::default()
};
let block = if focused {
Block::default()
.title(title)
.title_style(title_style)
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan))
.border_type(BorderType::Thick)
} else {
Block::default()
.title(title)
.title_style(title_style)
.borders(Borders::ALL)
};

let table = ratatui::widgets::Table::new(tui_rows_iter, widths_constraints)
.block(Block::default().title(self.title).borders(Borders::ALL))
.block(block)
.header(Row::new(column_names).style(Style::default().fg(Color::Yellow)))
.flex(ratatui::layout::Flex::Legacy)
.column_spacing(spacer_width);
Expand Down
8 changes: 8 additions & 0 deletions src/display/input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// An action that can be dispatched to a focused pane.
///
/// New per-widget behaviours should be added as variants here and handled in
/// [`crate::display::Ui::handle_widget_action`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WidgetAction {
TogglePause,
}
2 changes: 2 additions & 0 deletions src/display/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
mod components;
mod input;
mod raw_terminal_backend;
mod ui;
mod ui_state;

pub use components::*;
pub use input::*;
pub use raw_terminal_backend::*;
pub use ui::*;
pub use ui_state::*;
Comment on lines 7 to 11

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is outside the scope of this PR, but I believe use foo::* is bad practice. Imo we should convert this to proper, explicit uses for code hygiene.

34 changes: 32 additions & 2 deletions src/display/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{
cli::{Opt, RenderOpts},
display::{
components::{HeaderDetails, HelpText, Layout, Table},
input::WidgetAction,
UIState,
},
network::{display_connection_string, display_ip_or_host, LocalSocket, Utilization},
Expand Down Expand Up @@ -127,7 +128,13 @@ where
write_to_stdout("");
}

pub fn draw(&mut self, paused: bool, elapsed_time: Duration, table_cycle_offset: usize) {
pub fn draw(
&mut self,
paused: bool,
elapsed_time: Duration,
table_cycle_offset: usize,
focused_pane: Option<usize>,
) {
let layout = Layout {
header: HeaderDetails {
state: &self.state,
Expand All @@ -138,10 +145,12 @@ where
footer: HelpText {
paused,
show_dns: self.state.show_dns,
focused_pane,
},
pane_paused: self.state.pane_paused,
};
self.terminal
.draw(|frame| layout.render(frame, frame.area(), table_cycle_offset))
.draw(|frame| layout.render(frame, frame.area(), table_cycle_offset, focused_pane))
.unwrap();
}

Expand Down Expand Up @@ -177,6 +186,27 @@ where
self.get_tables_to_display().len()
}

/// Dispatch a [`WidgetAction`] to the pane at `pane_index`.
///
/// This is the single entry point for per-pane keybinding actions. New
/// `WidgetAction` variants should be handled here.
pub fn handle_widget_action(&mut self, action: WidgetAction, pane_index: usize) {
match action {
WidgetAction::TogglePause => self.toggle_pane_pause(pane_index),
}
}

/// Toggle the paused state for the pane at `pane_index`.
///
/// When a pane is paused its display data is frozen at the current snapshot;
/// live network updates continue accumulating in the backing maps but are
/// not written to the display vecs until the pane is unpaused.
fn toggle_pane_pause(&mut self, pane_index: usize) {
if let Some(flag) = self.state.pane_paused.get_mut(pane_index) {
*flag = !*flag;
}
}

pub fn update_state(
&mut self,
connections_to_procs: HashMap<LocalSocket, ProcessInfo>,
Expand Down
16 changes: 13 additions & 3 deletions src/display/ui_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ pub struct UIState {
pub processes_map: HashMap<ProcessInfo, NetworkData>,
pub remote_addresses_map: HashMap<IpAddr, NetworkData>,
pub connections_map: HashMap<Connection, ConnectionData>,
/// Per-pane pause state. Index matches logical table order:
/// 0 = processes, 1 = remote_addresses, 2 = connections.
/// When a pane is paused its display data is frozen at the last snapshot.
pub pane_paused: [bool; 3],
/// Used for reducing logging noise.
known_orphan_sockets: VecDeque<LocalSocket>,
}
Expand Down Expand Up @@ -221,9 +225,15 @@ impl UIState {
self.total_bytes_downloaded = total_bytes_downloaded / divide_by;
self.total_bytes_uploaded = total_bytes_uploaded / divide_by;
}
self.processes = sort_and_prune(&mut self.processes_map);
self.remote_addresses = sort_and_prune(&mut self.remote_addresses_map);
self.connections = sort_and_prune(&mut self.connections_map);
if !self.pane_paused[0] {
self.processes = sort_and_prune(&mut self.processes_map);
}
if !self.pane_paused[1] {
self.remote_addresses = sort_and_prune(&mut self.remote_addresses_map);
}
if !self.pane_paused[2] {
self.connections = sort_and_prune(&mut self.connections_map);
}
}
}

Expand Down
54 changes: 51 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crossterm::{
event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
terminal,
};
use display::{elapsed_time, RawTerminalBackend, Ui};
use display::{elapsed_time, RawTerminalBackend, Ui, WidgetAction};
use eyre::bail;
use network::{
dns::{self, IpTable},
Expand Down Expand Up @@ -100,6 +100,7 @@ where
let last_start_time = Arc::new(RwLock::new(Instant::now()));
let cumulative_time = Arc::new(RwLock::new(Duration::new(0, 0)));
let table_cycle_offset = Arc::new(AtomicUsize::new(0));
let focused_pane = Arc::new(Mutex::new(None::<usize>));

// handle SIGINT properly instead of as a keypress
// see https://github.com/imsnif/bandwhich/issues/487
Expand Down Expand Up @@ -130,6 +131,7 @@ where
let running = running.clone();
let paused = paused.clone();
let table_cycle_offset = table_cycle_offset.clone();
let focused_pane = focused_pane.clone();

let network_utilization = network_utilization.clone();
let last_start_time = last_start_time.clone();
Expand All @@ -156,6 +158,7 @@ where
let mut ui = ui.lock().unwrap();
let paused = paused.load(Ordering::SeqCst);
let table_cycle_offset = table_cycle_offset.load(Ordering::SeqCst);
let focused_pane = *focused_pane.lock().unwrap();
if !paused {
ui.update_state(sockets_to_procs, utilization, ip_to_host);
}
Expand All @@ -168,7 +171,7 @@ where
if raw_mode {
ui.output_text(&mut write_to_stdout);
} else {
ui.draw(paused, elapsed_time, table_cycle_offset);
ui.draw(paused, elapsed_time, table_cycle_offset, focused_pane);
}
}
let render_duration = render_start_time.elapsed();
Expand All @@ -189,6 +192,7 @@ where
.spawn({
let running = running.clone();
let display_handler = display_handler.thread().clone();
let focused_pane = focused_pane.clone();

move || {
let mut terminal_events = terminal_events;
Expand All @@ -201,6 +205,7 @@ where
match evt {
Event::Resize(_x, _y) if !raw_mode => {
let paused = paused.load(Ordering::SeqCst);
let focused_pane = *focused_pane.lock().unwrap();
ui.draw(
paused,
elapsed_time(
Expand All @@ -209,6 +214,7 @@ where
paused,
),
table_cycle_offset.load(Ordering::SeqCst),
focused_pane,
);
}
Event::Key(KeyEvent {
Expand Down Expand Up @@ -250,6 +256,18 @@ where

display_handler.unpark();
}
Event::Key(KeyEvent {
modifiers: KeyModifiers::NONE,
code: KeyCode::Char('f'),
kind: KeyEventKind::Press,
..
}) => {
let focused = *focused_pane.lock().unwrap();
if let Some(pane_index) = focused {
ui.handle_widget_action(WidgetAction::TogglePause, pane_index);
display_handler.unpark();
}
}
Event::Key(KeyEvent {
modifiers: KeyModifiers::NONE,
code: KeyCode::Tab,
Expand All @@ -265,7 +283,37 @@ where
let table_count = ui.get_table_count();
let new = table_cycle_offset.load(Ordering::SeqCst) + 1 % table_count;
table_cycle_offset.store(new, Ordering::SeqCst);
ui.draw(paused, elapsed_time, new);
let focused_pane = *focused_pane.lock().unwrap();
ui.draw(paused, elapsed_time, new, focused_pane);
}
Event::Key(KeyEvent {
modifiers: KeyModifiers::NONE,
code: KeyCode::Char('n'),
kind: KeyEventKind::Press,
..
}) => {
let paused = paused.load(Ordering::SeqCst);
let elapsed_time = elapsed_time(
*last_start_time.read().unwrap(),
*cumulative_time.read().unwrap(),
paused,
);
let table_count = ui.get_table_count();
let new_focused = {
let mut fp = focused_pane.lock().unwrap();
let next = Some(match *fp {
None => 0,
Some(i) => (i + 1) % table_count,
});
*fp = next;
next
};
ui.draw(
paused,
elapsed_time,
table_cycle_offset.load(Ordering::SeqCst),
new_focused,
);
}
_ => (),
};
Expand Down