From 8424854a0cfab687224aa6aa5b3f251a6211cf68 Mon Sep 17 00:00:00 2001 From: FujiApple Date: Sat, 11 Nov 2023 13:18:19 +0800 Subject: [PATCH] feat(tui): display individual tracing flows in Tui (#777) - WIP --- src/backend/trace.rs | 26 +++++++++- src/config/binding.rs | 9 ++++ src/config/file.rs | 2 + src/frontend.rs | 20 ++++++-- src/frontend/binding.rs | 2 + src/frontend/render.rs | 1 + src/frontend/render/app.rs | 18 ++++++- src/frontend/render/chart.rs | 3 +- src/frontend/render/flows.rs | 58 +++++++++++++++++++++ src/frontend/render/header.rs | 3 +- src/frontend/render/help.rs | 7 +-- src/frontend/render/settings.rs | 3 +- src/frontend/render/table.rs | 15 +++--- src/frontend/render/world.rs | 4 +- src/frontend/tui_app.rs | 91 ++++++++++++++++++++++++++++++--- trippy-config-sample.toml | 1 + 16 files changed, 230 insertions(+), 33 deletions(-) create mode 100644 src/frontend/render/flows.rs diff --git a/src/backend/trace.rs b/src/backend/trace.rs index fabbc30bf..619ffbafe 100644 --- a/src/backend/trace.rs +++ b/src/backend/trace.rs @@ -11,8 +11,13 @@ use trippy::tracing::{Extensions, Probe, ProbeStatus, TracerRound}; #[derive(Debug, Clone)] pub struct Trace { max_samples: usize, + /// The flow id for the current round. + round_flow_id: FlowId, + /// Tracing data per registered flow id. trace_data: HashMap, + /// Flow registry. registry: FlowRegistry, + /// Tracing error message. error: Option, } @@ -22,6 +27,7 @@ impl Trace { Self { trace_data: once((Self::default_flow_id(), TraceData::new(max_samples))) .collect::>(), + round_flow_id: Self::default_flow_id(), max_samples, registry: FlowRegistry::new(), error: None, @@ -68,6 +74,11 @@ impl Trace { self.trace_data[&flow_id].round_count() } + /// The `FlowId` for the current round. + pub fn round_flow_id(&self) -> FlowId { + self.round_flow_id + } + /// The registry of flows in the trace. pub fn flows(&self) -> &[(Flow, FlowId)] { self.registry.flows() @@ -94,6 +105,7 @@ impl Trace { .map(|p| p.host), ); let flow_id = self.registry.register(flow); + self.round_flow_id = flow_id; self.update_trace_flow(Self::default_flow_id(), round); self.update_trace_flow(flow_id, round); } @@ -110,18 +122,28 @@ impl Trace { /// Information about a single `Hop` within a `Trace`. #[derive(Debug, Clone)] pub struct Hop { + /// The ttl of this hop. ttl: u8, + /// The addrs of this hop and associated counts. addrs: IndexMap, + /// The total probes sent for this hop. total_sent: usize, + /// The total probes received for this hop. total_recv: usize, + /// The total round trip time for this hop across all rounds. total_time: Duration, + /// The round trip time for this hop in the current round. last: Option, + /// The best round trip time for this hop across all rounds. best: Option, + /// The worst round trip time for this hop across all rounds. worst: Option, - mean: f64, - m2: f64, + /// The history of round trip times across the last N rounds. samples: Vec, + /// The ICMP extensions for this hop. extensions: Option, + mean: f64, + m2: f64, } impl Hop { diff --git a/src/config/binding.rs b/src/config/binding.rs index 7528af264..90ab8fba1 100644 --- a/src/config/binding.rs +++ b/src/config/binding.rs @@ -25,6 +25,7 @@ pub struct TuiBindings { pub toggle_freeze: TuiKeyBinding, pub toggle_chart: TuiKeyBinding, pub toggle_map: TuiKeyBinding, + pub toggle_flows: TuiKeyBinding, pub expand_hosts: TuiKeyBinding, pub contract_hosts: TuiKeyBinding, pub expand_hosts_max: TuiKeyBinding, @@ -60,6 +61,7 @@ impl Default for TuiBindings { ), toggle_chart: TuiKeyBinding::new(KeyCode::Char('c')), toggle_map: TuiKeyBinding::new(KeyCode::Char('m')), + toggle_flows: TuiKeyBinding::new(KeyCode::Char('f')), expand_hosts: TuiKeyBinding::new(KeyCode::Char(']')), contract_hosts: TuiKeyBinding::new(KeyCode::Char('[')), expand_hosts_max: TuiKeyBinding::new(KeyCode::Char('}')), @@ -106,6 +108,7 @@ impl TuiBindings { (self.toggle_freeze, TuiCommandItem::ToggleFreeze), (self.toggle_chart, TuiCommandItem::ToggleChart), (self.toggle_map, TuiCommandItem::ToggleMap), + (self.toggle_flows, TuiCommandItem::ToggleFlows), (self.expand_hosts, TuiCommandItem::ExpandHosts), (self.expand_hosts_max, TuiCommandItem::ExpandHostsMax), (self.contract_hosts, TuiCommandItem::ContractHosts), @@ -201,6 +204,10 @@ impl From<(HashMap, ConfigBindings)> for TuiBindi .get(&TuiCommandItem::ToggleChart) .or(cfg.toggle_chart.as_ref()) .unwrap_or(&Self::default().toggle_chart), + toggle_flows: *cmd_items + .get(&TuiCommandItem::ToggleFlows) + .or(cfg.toggle_flows.as_ref()) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('f'))), toggle_map: *cmd_items .get(&TuiCommandItem::ToggleMap) .or(cfg.toggle_map.as_ref()) @@ -506,6 +513,8 @@ pub enum TuiCommandItem { ToggleChart, /// Toggle the map. ToggleMap, + /// Toggle the flows panel. + ToggleFlows, /// Expand hosts. ExpandHosts, /// Expand hosts to max. diff --git a/src/config/file.rs b/src/config/file.rs index ae5253aea..4aa27eee9 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -323,6 +323,7 @@ pub struct ConfigBindings { pub address_mode_both: Option, pub toggle_freeze: Option, pub toggle_chart: Option, + pub toggle_flows: Option, pub toggle_map: Option, pub expand_hosts: Option, pub contract_hosts: Option, @@ -356,6 +357,7 @@ impl Default for ConfigBindings { address_mode_both: Some(bindings.address_mode_both), toggle_freeze: Some(bindings.toggle_freeze), toggle_chart: Some(bindings.toggle_chart), + toggle_flows: Some(bindings.toggle_flows), toggle_map: Some(bindings.toggle_map), expand_hosts: Some(bindings.expand_hosts), contract_hosts: Some(bindings.contract_hosts), diff --git a/src/frontend.rs b/src/frontend.rs index e3c38ab89..0d641668c 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -54,6 +54,7 @@ pub fn run_frontend( Ok(()) } +#[allow(clippy::too_many_lines)] fn run_app( terminal: &mut Terminal, trace_info: Vec, @@ -66,6 +67,7 @@ fn run_app( if app.frozen_start.is_none() { app.snapshot_trace_data(); app.clamp_selected_hop(); + app.update_order_flow_counts(); }; terminal.draw(|f| render::app::render(f, &mut app))?; if event::poll(app.tui_config.refresh_rate)? { @@ -105,11 +107,19 @@ fn run_app( } else if bindings.previous_hop.check(key) { app.previous_hop(); } else if bindings.previous_trace.check(key) { - app.previous_trace(); - app.clear(); + if app.show_flows { + app.previous_flow(); + } else { + app.previous_trace(); + app.clear(); + } } else if bindings.next_trace.check(key) { - app.next_trace(); - app.clear(); + if app.show_flows { + app.next_flow(); + } else { + app.next_trace(); + app.clear(); + } } else if bindings.next_hop_address.check(key) { app.next_hop_address(); } else if bindings.previous_hop_address.check(key) { @@ -126,6 +136,8 @@ fn run_app( app.toggle_chart(); } else if bindings.toggle_map.check(key) { app.toggle_map(); + } else if bindings.toggle_flows.check(key) { + app.toggle_flows(); } else if bindings.contract_hosts_min.check(key) { app.contract_hosts_min(); } else if bindings.expand_hosts_max.check(key) { diff --git a/src/frontend/binding.rs b/src/frontend/binding.rs index 0deb67f40..588848bd1 100644 --- a/src/frontend/binding.rs +++ b/src/frontend/binding.rs @@ -21,6 +21,7 @@ pub struct Bindings { pub toggle_freeze: KeyBinding, pub toggle_chart: KeyBinding, pub toggle_map: KeyBinding, + pub toggle_flows: KeyBinding, pub expand_hosts: KeyBinding, pub contract_hosts: KeyBinding, pub expand_hosts_max: KeyBinding, @@ -53,6 +54,7 @@ impl From for Bindings { toggle_freeze: KeyBinding::from(value.toggle_freeze), toggle_chart: KeyBinding::from(value.toggle_chart), toggle_map: KeyBinding::from(value.toggle_map), + toggle_flows: KeyBinding::from(value.toggle_flows), expand_hosts: KeyBinding::from(value.expand_hosts), contract_hosts: KeyBinding::from(value.contract_hosts), expand_hosts_max: KeyBinding::from(value.expand_hosts_max), diff --git a/src/frontend/render.rs b/src/frontend/render.rs index 7b0b220d3..82b2c2279 100644 --- a/src/frontend/render.rs +++ b/src/frontend/render.rs @@ -2,6 +2,7 @@ pub mod app; pub mod body; pub mod bsod; pub mod chart; +pub mod flows; pub mod footer; pub mod header; pub mod help; diff --git a/src/frontend/render/app.rs b/src/frontend/render/app.rs index 78e29b96a..da7a66bbb 100644 --- a/src/frontend/render/app.rs +++ b/src/frontend/render/app.rs @@ -1,4 +1,4 @@ -use crate::frontend::render::{body, footer, header, help, settings, tabs}; +use crate::frontend::render::{body, flows, footer, header, help, settings, tabs}; use crate::frontend::tui_app::TuiApp; use ratatui::layout::{Constraint, Direction, Layout}; use ratatui::Frame; @@ -33,7 +33,10 @@ use ratatui::Frame; /// On startup a splash screen is shown in place of the hops table, until the completion of the /// first round. pub fn render(f: &mut Frame<'_>, app: &mut TuiApp) { - let constraints = if app.trace_info.len() > 1 { + // TODO ensure we don't allow flows and tabs at the same time or mix up layouts + let constraints = if app.show_flows { + LAYOUT_WITH_FLOWS.as_slice() + } else if app.trace_info.len() > 1 { LAYOUT_WITH_TABS.as_slice() } else { LAYOUT_WITHOUT_TABS.as_slice() @@ -47,6 +50,10 @@ pub fn render(f: &mut Frame<'_>, app: &mut TuiApp) { tabs::render(f, chunks[1], app); body::render(f, chunks[2], app); footer::render(f, chunks[3], app); + } else if app.show_flows { + flows::render(f, chunks[1], app); + body::render(f, chunks[2], app); + footer::render(f, chunks[3], app); } else { body::render(f, chunks[1], app); footer::render(f, chunks[2], app); @@ -70,3 +77,10 @@ const LAYOUT_WITH_TABS: [Constraint; 4] = [ Constraint::Min(10), Constraint::Length(6), ]; + +const LAYOUT_WITH_FLOWS: [Constraint; 4] = [ + Constraint::Length(5), + Constraint::Length(6), + Constraint::Min(10), + Constraint::Length(6), +]; diff --git a/src/frontend/render/chart.rs b/src/frontend/render/chart.rs index a8a3b5fde..60d8f0f41 100644 --- a/src/frontend/render/chart.rs +++ b/src/frontend/render/chart.rs @@ -1,4 +1,3 @@ -use crate::backend::trace::Trace; use crate::frontend::tui_app::TuiApp; use ratatui::layout::{Alignment, Constraint, Rect}; use ratatui::style::Style; @@ -13,7 +12,7 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { let samples = app.tui_config.max_samples / app.zoom_factor; let series_data = app .selected_tracer_data - .hops(Trace::default_flow_id()) + .hops(app.selected_flow) .iter() .map(|hop| { hop.samples() diff --git a/src/frontend/render/flows.rs b/src/frontend/render/flows.rs new file mode 100644 index 000000000..e534e7780 --- /dev/null +++ b/src/frontend/render/flows.rs @@ -0,0 +1,58 @@ +use crate::frontend::tui_app::TuiApp; +use ratatui::layout::{Alignment, Rect}; +use ratatui::prelude::Color; +use ratatui::style::{Modifier, Style}; +use ratatui::text::Line; +use ratatui::widgets::{Bar, BarChart, BarGroup, Block, BorderType, Borders}; +use ratatui::Frame; + +/// Render the flows. +pub fn render(f: &mut Frame<'_>, rect: Rect, app: &TuiApp) { + let round_flow_id = app.tracer_data().round_flow_id(); + let data: Vec<_> = app + .flow_counts + .iter() + .map(|(flow_id, count)| { + let (bar_fg, bg, fg) = if flow_id == &app.selected_flow && flow_id == &round_flow_id { + ( + Color::Green, + app.tui_config.theme.frequency_chart_bar_color, + Color::Green, + ) + } else if flow_id == &app.selected_flow { + ( + Color::Green, + app.tui_config.theme.frequency_chart_bar_color, + app.tui_config.theme.frequency_chart_text_color, + ) + } else if flow_id == &round_flow_id { + (Color::DarkGray, Color::DarkGray, Color::Green) + } else { + (Color::DarkGray, Color::DarkGray, Color::White) + }; + Bar::default() + .label(Line::from(format!("{flow_id}"))) + .value(*count as u64) + .style(Style::default().fg(bar_fg)) + .value_style(Style::default().bg(bg).fg(fg).add_modifier(Modifier::BOLD)) + }) + .collect(); + let block = Block::default() + .title("Flows") + .title_alignment(Alignment::Left) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.tui_config.theme.border_color)) + .style( + Style::default() + .bg(app.tui_config.theme.bg_color) + .fg(app.tui_config.theme.text_color), + ); + let group = BarGroup::default().bars(&data); + let flow_counts = BarChart::default() + .block(block) + .data(group) + .bar_width(4) + .bar_gap(1); + f.render_widget(flow_counts, rect); +} diff --git a/src/frontend/render/header.rs b/src/frontend/render/header.rs index 43e207bbf..be2e3992b 100644 --- a/src/frontend/render/header.rs +++ b/src/frontend/render/header.rs @@ -1,4 +1,3 @@ -use crate::backend::trace::Trace; use crate::frontend::tui_app::TuiApp; use chrono::SecondsFormat; use humantime::format_duration; @@ -95,7 +94,7 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { Span::raw(render_status(app)), Span::raw(format!( ", discovered {} hops", - app.tracer_data().hops(Trace::default_flow_id()).len() + app.tracer_data().hops(app.selected_flow).len() )), ]), ]; diff --git a/src/frontend/render/help.rs b/src/frontend/render/help.rs index 7d62929bd..39117efde 100644 --- a/src/frontend/render/help.rs +++ b/src/frontend/render/help.rs @@ -25,15 +25,16 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp) { f.render_widget(control, area); } -const HELP_LINES: [&str; 20] = [ +const HELP_LINES: [&str; 21] = [ "[up] & [down] - select hop", - "[left] & [right] - select trace", + "[left] & [right] - select trace or flow", ", & . - select hop address", "[esc] - clear selection", "d - toggle hop details", + "f - toggle flows", "c - toggle chart", "m - toggle map", - "ctrl+f - toggle freeze display", + "Ctrl-f - toggle freeze display", "Ctrl+r - reset statistics", "Ctrl+k - flush DNS cache", "i - show IP only", diff --git a/src/frontend/render/settings.rs b/src/frontend/render/settings.rs index cb133004d..d62b8068e 100644 --- a/src/frontend/render/settings.rs +++ b/src/frontend/render/settings.rs @@ -279,6 +279,7 @@ fn format_binding_settings(app: &TuiApp) -> Vec { SettingsItem::new("toggle-freeze", format!("{}", binds.toggle_freeze)), SettingsItem::new("toggle-chart", format!("{}", binds.toggle_chart)), SettingsItem::new("toggle-map", format!("{}", binds.toggle_map)), + SettingsItem::new("toggle-flows", format!("{}", binds.toggle_flows)), SettingsItem::new("expand-hosts", format!("{}", binds.expand_hosts)), SettingsItem::new("expand-hosts-max", format!("{}", binds.expand_hosts_max)), SettingsItem::new("contract-hosts", format!("{}", binds.contract_hosts)), @@ -403,7 +404,7 @@ pub const SETTINGS_TABS: [(&str, usize); 6] = [ ("Trace", 14), ("Dns", 4), ("GeoIp", 1), - ("Bindings", 26), + ("Bindings", 27), ("Theme", 27), ]; diff --git a/src/frontend/render/table.rs b/src/frontend/render/table.rs index 5bae56f5a..c44dc62c4 100644 --- a/src/frontend/render/table.rs +++ b/src/frontend/render/table.rs @@ -1,4 +1,4 @@ -use crate::backend::trace::{Hop, Trace}; +use crate::backend::trace::Hop; use crate::config::{AddressMode, AsMode, GeoIpMode}; use crate::frontend::config::TuiConfig; use crate::frontend::theme::Theme; @@ -31,11 +31,10 @@ use trippy::dns::{AsInfo, DnsEntry, DnsResolver, Resolved, Resolver, Unresolved} pub fn render(f: &mut Frame<'_>, app: &mut TuiApp, rect: Rect) { let header = render_table_header(app.tui_config.theme); let selected_style = Style::default().add_modifier(Modifier::REVERSED); - let rows = app - .tracer_data() - .hops(Trace::default_flow_id()) - .iter() - .map(|hop| render_table_row(app, hop, &app.resolver, &app.geoip_lookup, &app.tui_config)); + let rows = + app.tracer_data().hops(app.selected_flow).iter().map(|hop| { + render_table_row(app, hop, &app.resolver, &app.geoip_lookup, &app.tui_config) + }); let table = Table::new(rows) .header(header) .block( @@ -78,8 +77,8 @@ fn render_table_row( .selected_hop() .map(|h| h.ttl() == hop.ttl()) .unwrap_or_default(); - let is_target = app.tracer_data().is_target(hop, Trace::default_flow_id()); - let is_in_round = app.tracer_data().is_in_round(hop, Trace::default_flow_id()); + let is_target = app.tracer_data().is_target(hop, app.selected_flow); + let is_in_round = app.tracer_data().is_in_round(hop, app.selected_flow); let ttl_cell = render_ttl_cell(hop); let (hostname_cell, row_height) = if is_selected_hop && app.show_hop_details { render_hostname_with_details(app, hop, dns, geoip_lookup, config) diff --git a/src/frontend/render/world.rs b/src/frontend/render/world.rs index 97a3fdffa..4e0c3e1d2 100644 --- a/src/frontend/render/world.rs +++ b/src/frontend/render/world.rs @@ -1,4 +1,4 @@ -use crate::backend::trace::{Hop, Trace}; +use crate::backend::trace::Hop; use crate::frontend::tui_app::TuiApp; use itertools::Itertools; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}; @@ -186,7 +186,7 @@ struct MapEntry { /// Each entry represent a single `GeoIp` location, which may be associated with multiple hops. fn build_map_entries(app: &TuiApp) -> Vec { let mut geo_map: HashMap = HashMap::new(); - for hop in app.tracer_data().hops(Trace::default_flow_id()) { + for hop in app.tracer_data().hops(app.selected_flow) { for addr in hop.addrs() { if let Some(geo) = app.geoip_lookup.lookup(*addr).unwrap_or_default() { if let Some((latitude, longitude, radius)) = geo.coordinates() { diff --git a/src/frontend/tui_app.rs b/src/frontend/tui_app.rs index a081d1821..958816abf 100644 --- a/src/frontend/tui_app.rs +++ b/src/frontend/tui_app.rs @@ -1,9 +1,11 @@ +use crate::backend::flows::FlowId; use crate::backend::trace::Hop; use crate::backend::trace::Trace; use crate::frontend::config::TuiConfig; use crate::frontend::render::settings::SETTINGS_TABS; use crate::geoip::GeoIpLookup; use crate::TraceInfo; +use itertools::Itertools; use ratatui::widgets::TableState; use std::time::SystemTime; use trippy::dns::{DnsResolver, ResolveMethod}; @@ -24,11 +26,18 @@ pub struct TuiApp { /// /// Only used in detail mode. pub selected_hop_address: usize, + /// The FlowId of the selected flow. + /// + /// FlowId(0) represents the unified flow for the trace. + pub selected_flow: FlowId, + /// Ordered flow ids with counts. + pub flow_counts: Vec<(FlowId, usize)>, pub resolver: DnsResolver, pub geoip_lookup: GeoIpLookup, pub show_help: bool, pub show_settings: bool, pub show_hop_details: bool, + pub show_flows: bool, pub show_chart: bool, pub show_map: bool, pub frozen_start: Option, @@ -51,11 +60,14 @@ impl TuiApp { trace_selected: 0, settings_tab_selected: 0, selected_hop_address: 0, + selected_flow: Trace::default_flow_id(), + flow_counts: vec![], resolver, geoip_lookup, show_help: false, show_settings: false, show_hop_details: false, + show_flows: false, show_chart: false, show_map: false, frozen_start: None, @@ -78,15 +90,15 @@ impl TuiApp { pub fn selected_hop_or_target(&self) -> &Hop { self.table_state.selected().map_or_else( - || self.tracer_data().target_hop(Trace::default_flow_id()), - |s| &self.tracer_data().hops(Trace::default_flow_id())[s], + || self.tracer_data().target_hop(self.selected_flow), + |s| &self.tracer_data().hops(self.selected_flow)[s], ) } pub fn selected_hop(&self) -> Option<&Hop> { self.table_state .selected() - .map(|s| &self.tracer_data().hops(Trace::default_flow_id())[s]) + .map(|s| &self.tracer_data().hops(self.selected_flow)[s]) } pub fn tracer_config(&self) -> &TraceInfo { @@ -94,7 +106,7 @@ impl TuiApp { } pub fn clamp_selected_hop(&mut self) { - let hop_count = self.tracer_data().hops(Trace::default_flow_id()).len(); + let hop_count = self.tracer_data().hops(self.selected_flow).len(); if let Some(selected) = self.table_state.selected() { if selected > hop_count - 1 { self.table_state.select(Some(hop_count - 1)); @@ -102,8 +114,31 @@ impl TuiApp { } } + pub fn update_order_flow_counts(&mut self) { + pub fn order_flows( + &(flow_id1, count1): &(FlowId, usize), + &(flow_id2, count2): &(FlowId, usize), + ) -> std::cmp::Ordering { + match count1.cmp(&count2) { + std::cmp::Ordering::Equal => flow_id2.cmp(&flow_id1), + ord => ord, + } + } + self.flow_counts = self + .tracer_data() + .flows() + .iter() + .map(|&(_, flow_id)| { + let count = self.tracer_data().round_count(flow_id); + (flow_id, count) + }) + .sorted_by(order_flows) + .rev() + .collect::>(); + } + pub fn next_hop(&mut self) { - let hop_count = self.tracer_data().hops(Trace::default_flow_id()).len(); + let hop_count = self.tracer_data().hops(self.selected_flow).len(); if hop_count == 0 { return; } @@ -123,7 +158,7 @@ impl TuiApp { } pub fn previous_hop(&mut self) { - let hop_count = self.tracer_data().hops(Trace::default_flow_id()).len(); + let hop_count = self.tracer_data().hops(self.selected_flow).len(); if hop_count == 0 { return; } @@ -167,6 +202,36 @@ impl TuiApp { } } + pub fn flow_count(&self) -> usize { + self.selected_tracer_data.flows().len() + } + + pub fn next_flow(&mut self) { + if self.show_flows { + let (cur_index, _) = self + .flow_counts + .iter() + .find_position(|(flow_id, _)| *flow_id == self.selected_flow) + .unwrap(); + if cur_index < self.flow_counts.len() - 1 { + self.selected_flow = self.flow_counts[cur_index + 1].0; + } + } + } + + pub fn previous_flow(&mut self) { + if self.show_flows { + let (cur_index, _) = self + .flow_counts + .iter() + .find_position(|(flow_id, _)| *flow_id == self.selected_flow) + .unwrap(); + if cur_index > 0 { + self.selected_flow = self.flow_counts[cur_index - 1].0; + } + } + } + pub fn next_settings_tab(&mut self) { if self.settings_tab_selected < SETTINGS_TABS.len() - 1 { self.settings_tab_selected += 1; @@ -251,6 +316,18 @@ impl TuiApp { self.show_chart = false; } + pub fn toggle_flows(&mut self) { + if self.show_flows { + self.selected_flow = FlowId(0); + self.show_flows = false; + self.selected_hop_address = 0; + } else if self.flow_count() > 0 { + self.selected_flow = FlowId(1); + self.show_flows = true; + self.selected_hop_address = 0; + } + } + pub fn toggle_asinfo(&mut self) { match self.resolver.config().resolve_method { ResolveMethod::Resolv | ResolveMethod::Google | ResolveMethod::Cloudflare => { @@ -299,7 +376,7 @@ impl TuiApp { /// The maximum number of hosts per hop for the currently selected trace. pub fn max_hosts(&self) -> u8 { self.selected_tracer_data - .hops(Trace::default_flow_id()) + .hops(self.selected_flow) .iter() .map(|h| h.addrs().count()) .max() diff --git a/trippy-config-sample.toml b/trippy-config-sample.toml index c1a9ee822..07a4767f5 100644 --- a/trippy-config-sample.toml +++ b/trippy-config-sample.toml @@ -328,6 +328,7 @@ address-mode-both = "b" toggle-freeze = "ctrl+f" toggle-chart = "c" toggle-map = "m" +toggle-flows = "f" expand-hosts = "]" expand-hosts-max = "}" contract-hosts = "["