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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
target/
CLAUDE.md

# macOS resource fork files
._*
.DS_Store
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

## [Unreleased]

### Added

* Add hierarchical process tree view feature #1 - @wezm

### Fixed

* Update CONTRIBUTING information #438 - @YJDoc2 @cyqsimon
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,19 @@ Options:
-a, --addresses Show remote addresses table only
-u, --unit-family <UNIT_FAMILY> Choose a specific family of units [default: bin-bytes] [possible values: bin-bytes, bin-bits, si-bytes, si-bits]
-t, --total-utilization Show total (cumulative) usages
--tree-view Display processes in hierarchical tree view showing parent-child relationships
-h, --help Print help (see more with '--help')
-V, --version Print version
```

### Process Tree View

The `--tree-view` flag enables hierarchical process visualization, showing parent-child relationships between processes. This is particularly useful for understanding which services or daemons are responsible for network activity, and for visualizing complex applications with multiple child processes. When enabled:

- Processes are displayed in a tree structure with indentation showing the hierarchy
- Bandwidth usage is aggregated up the tree (parent processes show their own usage plus their children's)
- Each process shows its own PID and its relationship to other processes

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).
Expand Down
4 changes: 4 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ pub struct RenderOpts {
#[arg(short, long)]
/// Show total (cumulative) usages
pub total_utilization: bool,

#[arg(long)]
/// Display processes in hierarchical tree view showing parent-child relationships
pub tree_view: bool,
}

// IMPRV: it would be nice if we can `#[cfg_attr(not(build), derive(strum::EnumIter))]` this
Expand Down
67 changes: 50 additions & 17 deletions src/display/components/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,12 @@ impl Table {
pub fn create_processes_table(state: &UIState) -> Self {
use DisplayLayout as D;

let title = "Utilization by process name";
let title = if state.tree_view {
"Utilization by process tree"
} else {
"Utilization by process name"
};

let width_cutoffs = vec![
(0, D::C2([16, 18])),
(50, D::C3([16, 12, 20])),
Expand All @@ -279,22 +284,50 @@ impl Table {
"Rate (Up / Down)"
},
];
let rows = state
.processes
.iter()
.map(|(proc_info, data_for_process)| {
[
proc_info.name.to_string(),
proc_info.pid.to_string(),
data_for_process.connection_count.to_string(),
display_upload_and_download(
data_for_process,
state.unit_family,
state.cumulative_mode,
),
]
})
.collect();

let rows = if state.tree_view {
// Build hierarchical process list from process trees using aggregated data
let mut tree_rows = Vec::new();
for tree in &state.process_trees {
for (proc_info, depth) in tree.iter_depth_first() {
// Use aggregated data which includes children's bandwidth
if let Some(data_for_process) = state.aggregated_processes_map.get(proc_info) {
let indent = " ".repeat(depth);
let process_name = format!("{}{}", indent, proc_info.name);
tree_rows.push([
process_name,
proc_info.pid.to_string(),
data_for_process.connection_count.to_string(),
display_upload_and_download(
data_for_process,
state.unit_family,
state.cumulative_mode,
),
]);
}
}
}
tree_rows
} else {
// Regular flat process list
state
.processes
.iter()
.map(|(proc_info, data_for_process)| {
[
proc_info.name.to_string(),
proc_info.pid.to_string(),
data_for_process.connection_count.to_string(),
display_upload_and_download(
data_for_process,
state.unit_family,
state.cumulative_mode,
),
]
})
.collect()
};

let column_selector = Rc::new(|layout: &D| match layout {
D::C2(_) => vec![0, 3],
D::C3(_) => vec![0, 2, 3],
Expand Down
7 changes: 7 additions & 0 deletions src/display/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
//! Terminal user interface components
//!
//! This module provides the display functionality for bandwhich:
//! - Terminal UI rendering using ratatui
//! - Raw output mode for piping to other programs
//! - UI state management and component rendering

mod components;
mod raw_terminal_backend;
mod ui;
Expand Down
15 changes: 8 additions & 7 deletions src/display/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,16 @@ where
B: Backend,
{
pub fn new(terminal_backend: B, opts: &Opt) -> Self {
let mut terminal = Terminal::new(terminal_backend).unwrap();
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
let mut terminal = Terminal::new(terminal_backend).expect("Failed to create terminal");
terminal.clear().expect("Failed to clear terminal");
terminal.hide_cursor().expect("Failed to hide cursor");
let state = {
let mut state = UIState::default();
state.interface_name.clone_from(&opts.interface);
state.unit_family = opts.render_opts.unit_family.into();
state.cumulative_mode = opts.render_opts.total_utilization;
state.show_dns = opts.show_dns;
state.tree_view = opts.render_opts.tree_view;
state
};
Ui {
Expand Down Expand Up @@ -140,9 +141,9 @@ where
show_dns: self.state.show_dns,
},
};
self.terminal
.draw(|frame| layout.render(frame, frame.area(), table_cycle_offset))
.unwrap();
let _ = self
.terminal
.draw(|frame| layout.render(frame, frame.area(), table_cycle_offset));
}

fn get_tables_to_display(&self) -> Vec<Table> {
Expand Down Expand Up @@ -187,6 +188,6 @@ where
self.ip_to_host.extend(ip_to_host);
}
pub fn end(&mut self) {
self.terminal.show_cursor().unwrap();
let _ = self.terminal.show_cursor();
}
}
15 changes: 14 additions & 1 deletion src/display/ui_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use log::warn;
use crate::{
display::BandwidthUnitFamily,
network::{Connection, LocalSocket, Utilization},
os::ProcessInfo,
os::{aggregate_bandwidth_by_tree, build_process_trees, ProcessInfo, ProcessTreeNode},
};

static RECALL_LENGTH: usize = 5;
Expand Down Expand Up @@ -89,11 +89,14 @@ pub struct UIState {
pub total_bytes_uploaded: u128,
pub cumulative_mode: bool,
pub show_dns: bool,
pub tree_view: bool,
pub unit_family: BandwidthUnitFamily,
pub utilization_data: VecDeque<UtilizationData>,
pub processes_map: HashMap<ProcessInfo, NetworkData>,
pub remote_addresses_map: HashMap<IpAddr, NetworkData>,
pub connections_map: HashMap<Connection, ConnectionData>,
pub process_trees: Vec<ProcessTreeNode>,
pub aggregated_processes_map: HashMap<ProcessInfo, NetworkData>,
/// Used for reducing logging noise.
known_orphan_sockets: VecDeque<LocalSocket>,
}
Expand Down Expand Up @@ -224,6 +227,16 @@ impl UIState {
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);

// Build process trees if tree view is enabled
if self.tree_view {
let all_processes: Vec<ProcessInfo> = self.processes_map.keys().cloned().collect();
self.process_trees = build_process_trees(all_processes);

// Aggregate bandwidth by process tree
self.aggregated_processes_map =
aggregate_bandwidth_by_tree(&self.process_trees, &self.processes_map);
}
}
}

Expand Down
56 changes: 56 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Custom error types for bandwhich
//!
//! This module provides structured error handling throughout the application,
//! replacing generic error types with domain-specific ones for better
//! error messages and recovery strategies.

use std::io;
use thiserror::Error;

/// Main error type for bandwhich operations
#[derive(Debug, Error)]
pub enum BandwhichError {
/// Terminal initialization or operation failed
#[error("Terminal error: {0}")]
Terminal(#[from] io::Error),

/// Thread spawning or joining failed
#[error("Thread error: {0}")]
#[allow(dead_code)]
Thread(String),

/// Lock acquisition failed (poisoned mutex)
#[error("Lock poisoned: {0}")]
LockPoisoned(String),

/// DNS resolution error
#[error("DNS error: {0}")]
#[allow(dead_code)]
Dns(String),

/// Network interface error
#[error("Network interface error: {0}")]
#[allow(dead_code)]
NetworkInterface(String),

/// Process information retrieval error
#[error("Process info error: {0}")]
#[allow(dead_code)]
ProcessInfo(String),

/// Configuration or CLI argument error
#[error("Configuration error: {0}")]
#[allow(dead_code)]
Config(String),
}

/// Result type alias for bandwhich operations
#[allow(dead_code)]
pub type Result<T> = std::result::Result<T, BandwhichError>;

/// Convert from std::sync::PoisonError to BandwhichError
impl<T> From<std::sync::PoisonError<T>> for BandwhichError {
fn from(err: std::sync::PoisonError<T>) -> Self {
BandwhichError::LockPoisoned(format!("Mutex poisoned: {err}"))
}
}
Loading