From c34efe38a47ffcb7f491c3616e78745a2ff95b59 Mon Sep 17 00:00:00 2001 From: FujiApple Date: Mon, 15 Jan 2024 18:57:14 +0800 Subject: [PATCH] feat(tui): allow modifying column visibility and order in settings (#1026) --- src/frontend.rs | 6 + src/frontend/columns.rs | 303 +++++++++++++++++++++----------- src/frontend/render/settings.rs | 58 ++++-- src/frontend/render/table.rs | 49 +++--- src/frontend/tui_app.rs | 45 ++++- 5 files changed, 320 insertions(+), 141 deletions(-) diff --git a/src/frontend.rs b/src/frontend.rs index 8813d69ab..bb22c7ba8 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -97,6 +97,12 @@ fn run_app( app.next_settings_item(); } else if bindings.previous_hop.check(key) { app.previous_settings_item(); + } else if bindings.toggle_chart.check(key) { + app.toggle_column_visibility(); + } else if bindings.next_hop_address.check(key) { + app.move_column_down(); + } else if bindings.previous_hop_address.check(key) { + app.move_column_up(); } } else if bindings.toggle_help.check(key) || bindings.toggle_help_alt.check(key) { diff --git a/src/frontend/columns.rs b/src/frontend/columns.rs index 0fd7e2a01..602c855f1 100644 --- a/src/frontend/columns.rs +++ b/src/frontend/columns.rs @@ -1,10 +1,11 @@ use crate::config::{TuiColumn, TuiColumns}; use ratatui::layout::{Constraint, Rect}; use std::fmt::{Display, Formatter}; +use strum::{EnumIter, IntoEnumIterator}; /// The columns to display in the hops table of the TUI. #[derive(Debug, Clone, Eq, PartialEq)] -pub struct Columns(pub Vec); +pub struct Columns(Vec); impl Columns { /// Column width constraints. @@ -17,46 +18,130 @@ impl Columns { /// dividing by the number of `Variable` columns. pub fn constraints(&self, rect: Rect) -> Vec { let total_fixed_width = self - .0 - .iter() - .map(|c| match c.width() { + .columns() + .map(|c| match c.typ.width() { ColumnWidth::Fixed(width) => width, ColumnWidth::Variable => 0, }) .sum(); let variable_width_count = self - .0 - .iter() - .filter(|c| matches!(c.width(), ColumnWidth::Variable)) + .columns() + .filter(|c| matches!(c.typ.width(), ColumnWidth::Variable)) .count() as u16; let variable_width = rect.width.saturating_sub(total_fixed_width) / variable_width_count.max(1); - self.0 - .iter() - .map(|c| match c.width() { + self.columns() + .map(|c| match c.typ.width() { ColumnWidth::Fixed(width) => Constraint::Min(width), ColumnWidth::Variable => Constraint::Min(variable_width), }) .collect() } + + pub fn columns(&self) -> impl Iterator { + self.0 + .iter() + .filter(|c| matches!(c.status, ColumnStatus::Shown)) + } + + pub fn all_columns(&self) -> impl Iterator { + self.0.iter() + } + + pub fn all_columns_count(&self) -> usize { + self.0.len() + } + + pub fn toggle(&mut self, index: usize) { + self.0[index].status = match self.0[index].status { + ColumnStatus::Shown => ColumnStatus::Hidden, + ColumnStatus::Hidden => ColumnStatus::Shown, + }; + } + + pub fn move_down(&mut self, index: usize) { + if index < self.0.len() { + let removed = self.0.remove(index); + self.0.insert(index + 1, removed); + } + } + + pub fn move_up(&mut self, index: usize) { + if index > 0 { + let removed = self.0.remove(index); + self.0.insert(index - 1, removed); + } + } } impl From for Columns { fn from(value: TuiColumns) -> Self { - Self(value.0.into_iter().map(Column::from).collect()) + let enabled: Vec<_> = value.0.into_iter().map(Column::from).collect(); + let disabled: Vec<_> = ColumnType::iter() + .filter(|ct| enabled.iter().all(|c| c.typ != *ct)) + .map(Column::new_hidden) + .collect(); + let all = enabled.into_iter().chain(disabled).collect(); + Self(all) } } impl Display for Columns { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let output: Vec = self.0.clone().into_iter().map(Column::into).collect(); + let output: Vec = self + .0 + .iter() + .filter_map(|c| { + if c.status == ColumnStatus::Shown { + Some(c.typ.into()) + } else { + None + } + }) + .collect(); write!(f, "{}", String::from_iter(output)) } } +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Column { + pub typ: ColumnType, + pub status: ColumnStatus, +} + +impl Column { + pub fn new_shown(typ: ColumnType) -> Self { + Self { + typ, + status: ColumnStatus::Shown, + } + } + pub fn new_hidden(typ: ColumnType) -> Self { + Self { + typ, + status: ColumnStatus::Hidden, + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ColumnStatus { + Shown, + Hidden, +} + +impl Display for ColumnStatus { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Shown => write!(f, "on"), + Self::Hidden => write!(f, "off"), + } + } +} + /// A TUI hops table column. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum Column { +#[derive(Debug, Copy, Clone, Eq, PartialEq, EnumIter)] +pub enum ColumnType { /// The ttl for a hop. Ttl, /// The hostname for a hostname. @@ -95,27 +180,27 @@ pub enum Column { LastSeq, } -impl From for char { - fn from(col_type: Column) -> Self { +impl From for char { + fn from(col_type: ColumnType) -> Self { match col_type { - Column::Ttl => 'h', - Column::Host => 'o', - Column::LossPct => 'l', - Column::Sent => 's', - Column::Received => 'r', - Column::Last => 'a', - Column::Average => 'v', - Column::Best => 'b', - Column::Worst => 'w', - Column::StdDev => 'd', - Column::Status => 't', - Column::Jitter => 'j', - Column::Javg => 'g', - Column::Jmax => 'x', - Column::Jinta => 'i', - Column::LastSrcPort => 'S', - Column::LastDestPort => 'P', - Column::LastSeq => 'Q', + ColumnType::Ttl => 'h', + ColumnType::Host => 'o', + ColumnType::LossPct => 'l', + ColumnType::Sent => 's', + ColumnType::Received => 'r', + ColumnType::Last => 'a', + ColumnType::Average => 'v', + ColumnType::Best => 'b', + ColumnType::Worst => 'w', + ColumnType::StdDev => 'd', + ColumnType::Status => 't', + ColumnType::Jitter => 'j', + ColumnType::Javg => 'g', + ColumnType::Jmax => 'x', + ColumnType::Jinta => 'i', + ColumnType::LastSrcPort => 'S', + ColumnType::LastDestPort => 'P', + ColumnType::LastSeq => 'Q', } } } @@ -123,29 +208,29 @@ impl From for char { impl From for Column { fn from(value: TuiColumn) -> Self { match value { - TuiColumn::Ttl => Self::Ttl, - TuiColumn::Host => Self::Host, - TuiColumn::LossPct => Self::LossPct, - TuiColumn::Sent => Self::Sent, - TuiColumn::Received => Self::Received, - TuiColumn::Last => Self::Last, - TuiColumn::Average => Self::Average, - TuiColumn::Best => Self::Best, - TuiColumn::Worst => Self::Worst, - TuiColumn::StdDev => Self::StdDev, - TuiColumn::Status => Self::Status, - TuiColumn::Jitter => Self::Jitter, - TuiColumn::Javg => Self::Javg, - TuiColumn::Jmax => Self::Jmax, - TuiColumn::Jinta => Self::Jinta, - TuiColumn::LastSrcPort => Self::LastSrcPort, - TuiColumn::LastDestPort => Self::LastDestPort, - TuiColumn::LastSeq => Self::LastSeq, + TuiColumn::Ttl => Self::new_shown(ColumnType::Ttl), + TuiColumn::Host => Self::new_shown(ColumnType::Host), + TuiColumn::LossPct => Self::new_shown(ColumnType::LossPct), + TuiColumn::Sent => Self::new_shown(ColumnType::Sent), + TuiColumn::Received => Self::new_shown(ColumnType::Received), + TuiColumn::Last => Self::new_shown(ColumnType::Last), + TuiColumn::Average => Self::new_shown(ColumnType::Average), + TuiColumn::Best => Self::new_shown(ColumnType::Best), + TuiColumn::Worst => Self::new_shown(ColumnType::Worst), + TuiColumn::StdDev => Self::new_shown(ColumnType::StdDev), + TuiColumn::Status => Self::new_shown(ColumnType::Status), + TuiColumn::Jitter => Self::new_shown(ColumnType::Jitter), + TuiColumn::Javg => Self::new_shown(ColumnType::Javg), + TuiColumn::Jmax => Self::new_shown(ColumnType::Jmax), + TuiColumn::Jinta => Self::new_shown(ColumnType::Jinta), + TuiColumn::LastSrcPort => Self::new_shown(ColumnType::LastSrcPort), + TuiColumn::LastDestPort => Self::new_shown(ColumnType::LastDestPort), + TuiColumn::LastSeq => Self::new_shown(ColumnType::LastSeq), } } } -impl Display for Column { +impl Display for ColumnType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Ttl => write!(f, "#"), @@ -170,7 +255,7 @@ impl Display for Column { } } -impl Column { +impl ColumnType { /// The width of the column. pub(self) fn width(self) -> ColumnWidth { #[allow(clippy::match_same_arms)] @@ -219,17 +304,36 @@ mod tests { TuiColumn::Host, TuiColumn::LossPct, TuiColumn::Sent, + TuiColumn::Received, + TuiColumn::Last, + TuiColumn::Average, + TuiColumn::Best, + TuiColumn::Worst, + TuiColumn::StdDev, + TuiColumn::Status, ]); - let columns = Columns::from(tui_columns); - assert_eq!( columns, Columns(vec![ - Column::Ttl, - Column::Host, - Column::LossPct, - Column::Sent, + Column::new_shown(ColumnType::Ttl), + Column::new_shown(ColumnType::Host), + Column::new_shown(ColumnType::LossPct), + Column::new_shown(ColumnType::Sent), + Column::new_shown(ColumnType::Received), + Column::new_shown(ColumnType::Last), + Column::new_shown(ColumnType::Average), + Column::new_shown(ColumnType::Best), + Column::new_shown(ColumnType::Worst), + Column::new_shown(ColumnType::StdDev), + Column::new_shown(ColumnType::Status), + Column::new_hidden(ColumnType::Jitter), + Column::new_hidden(ColumnType::Javg), + Column::new_hidden(ColumnType::Jmax), + Column::new_hidden(ColumnType::Jinta), + Column::new_hidden(ColumnType::LastSrcPort), + Column::new_hidden(ColumnType::LastDestPort), + Column::new_hidden(ColumnType::LastSeq), ]) ); } @@ -239,28 +343,29 @@ mod tests { let tui_column = TuiColumn::Received; let column = Column::from(tui_column); - assert_eq!(column, Column::Received); + assert_eq!(column.typ, ColumnType::Received); + assert_eq!(column.status, ColumnStatus::Shown); } - #[test_case(Column::Ttl, "#")] - #[test_case(Column::Host, "Host")] - #[test_case(Column::LossPct, "Loss%")] - #[test_case(Column::Sent, "Snd")] - #[test_case(Column::Received, "Recv")] - #[test_case(Column::Last, "Last")] - #[test_case(Column::Average, "Avg")] - #[test_case(Column::Best, "Best")] - #[test_case(Column::Worst, "Wrst")] - #[test_case(Column::StdDev, "StDev")] - #[test_case(Column::Status, "Sts")] - fn test_column_display_formatting(c: Column, heading: &'static str) { + #[test_case(ColumnType::Ttl, "#")] + #[test_case(ColumnType::Host, "Host")] + #[test_case(ColumnType::LossPct, "Loss%")] + #[test_case(ColumnType::Sent, "Snd")] + #[test_case(ColumnType::Received, "Recv")] + #[test_case(ColumnType::Last, "Last")] + #[test_case(ColumnType::Average, "Avg")] + #[test_case(ColumnType::Best, "Best")] + #[test_case(ColumnType::Worst, "Wrst")] + #[test_case(ColumnType::StdDev, "StDev")] + #[test_case(ColumnType::Status, "Sts")] + fn test_column_display_formatting(c: ColumnType, heading: &'static str) { assert_eq!(format!("{c}"), heading); } - #[test_case(Column::Ttl, & ColumnWidth::Fixed(4))] - #[test_case(Column::Host, & ColumnWidth::Variable)] - #[test_case(Column::LossPct, & ColumnWidth::Fixed(8))] - fn test_column_width(column_type: Column, width: &ColumnWidth) { + #[test_case(ColumnType::Ttl, & ColumnWidth::Fixed(4))] + #[test_case(ColumnType::Host, & ColumnWidth::Variable)] + #[test_case(ColumnType::LossPct, & ColumnWidth::Fixed(8))] + fn test_column_width(column_type: ColumnType, width: &ColumnWidth) { assert_eq!(column_type.width(), *width); } @@ -290,10 +395,10 @@ mod tests { #[test] fn test_columns_into_string_short() { let cols = Columns(vec![ - Column::Ttl, - Column::Host, - Column::LossPct, - Column::Sent, + Column::new_shown(ColumnType::Ttl), + Column::new_shown(ColumnType::Host), + Column::new_shown(ColumnType::LossPct), + Column::new_shown(ColumnType::Sent), ]); assert_eq!("hols", format!("{cols}")); } @@ -302,17 +407,17 @@ mod tests { #[test] fn test_columns_into_string_happy_path() { let cols = Columns(vec![ - Column::Ttl, - Column::Host, - Column::LossPct, - Column::Sent, - Column::Received, - Column::Last, - Column::Average, - Column::Best, - Column::Worst, - Column::StdDev, - Column::Status, + Column::new_shown(ColumnType::Ttl), + Column::new_shown(ColumnType::Host), + Column::new_shown(ColumnType::LossPct), + Column::new_shown(ColumnType::Sent), + Column::new_shown(ColumnType::Received), + Column::new_shown(ColumnType::Last), + Column::new_shown(ColumnType::Average), + Column::new_shown(ColumnType::Best), + Column::new_shown(ColumnType::Worst), + Column::new_shown(ColumnType::StdDev), + Column::new_shown(ColumnType::Status), ]); assert_eq!("holsravbwdt", format!("{cols}")); } @@ -321,13 +426,13 @@ mod tests { #[test] fn test_columns_into_string_reverse_str() { let cols = Columns(vec![ - Column::Status, - Column::Last, - Column::StdDev, - Column::Worst, - Column::Best, - Column::Average, - Column::Received, + Column::new_shown(ColumnType::Status), + Column::new_shown(ColumnType::Last), + Column::new_shown(ColumnType::StdDev), + Column::new_shown(ColumnType::Worst), + Column::new_shown(ColumnType::Best), + Column::new_shown(ColumnType::Average), + Column::new_shown(ColumnType::Received), ]); assert_eq!("tadwbvr", format!("{cols}")); } diff --git a/src/frontend/render/settings.rs b/src/frontend/render/settings.rs index f9cba6388..a18fc700f 100644 --- a/src/frontend/render/settings.rs +++ b/src/frontend/render/settings.rs @@ -6,7 +6,9 @@ use humantime::format_duration; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, BorderType, Borders, Cell, Clear, Paragraph, Row, Table, Tabs}; +use ratatui::widgets::{ + Block, BorderType, Borders, Cell, Clear, Paragraph, Row, Table, Tabs, Wrap, +}; use ratatui::Frame; use trippy::dns::ResolveMethod; use trippy::tracing::PortDirection; @@ -69,8 +71,11 @@ fn render_settings_table( .height(1) .bottom_margin(0); let rows = items.iter().map(|item| { - Row::new(vec![Cell::from(item.item), Cell::from(item.value.as_str())]) - .style(Style::default().fg(app.tui_config.theme.settings_table_row_text_color)) + Row::new(vec![ + Cell::from(item.item.as_str()), + Cell::from(item.value.as_str()), + ]) + .style(Style::default().fg(app.tui_config.theme.settings_table_row_text_color)) }); let item_width = items .iter() @@ -102,6 +107,7 @@ fn render_settings_table( fn render_settings_info(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, info: &str) { let info = Paragraph::new(info) .style(Style::default()) + .wrap(Wrap::default()) .block( Block::default() .title(" Info ") @@ -115,32 +121,41 @@ fn render_settings_info(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, info: &str) } /// Format all settings. -fn format_all_settings(app: &TuiApp) -> Vec<(&'static str, &'static str, Vec)> { +fn format_all_settings(app: &TuiApp) -> Vec<(&'static str, String, Vec)> { let tui_settings = format_tui_settings(app); let trace_settings = format_trace_settings(app); let dns_settings = format_dns_settings(app); let geoip_settings = format_geoip_settings(app); let bindings_settings = format_binding_settings(app); let theme_settings = format_theme_settings(app); + let columns_settings = format_columns_settings(app); + let toggle_column = app.tui_config.bindings.toggle_chart.to_string(); + let move_down = app.tui_config.bindings.next_hop_address.to_string(); + let move_up = app.tui_config.bindings.previous_hop_address.to_string(); vec![ ( "Tui", - "Settings which control how data is displayed in this Tui", + String::from("Settings which control how data is displayed in this Tui"), tui_settings, ), ( "Trace", - "Settings which control the tracing strategy", + String::from("Settings which control the tracing strategy"), trace_settings, ), ( "Dns", - "Settings which control how DNS lookups are performed", + String::from("Settings which control how DNS lookups are performed"), dns_settings, ), - ("GeoIp", "Settings relating to GeoIp", geoip_settings), - ("Bindings", "Tui key bindings", bindings_settings), - ("Theme", "Tui theme colors", theme_settings), + ("GeoIp", String::from("Settings relating to GeoIp"), geoip_settings), + ("Bindings", String::from("Tui key bindings"), bindings_settings), + ("Theme", String::from("Tui theme colors"), theme_settings), + ( + "Columns", + format!("Tui table columns (press {toggle_column} to toggle a column on or off and use the {move_down} and {move_up} keys to change the column order)"), + columns_settings, + ), ] } @@ -421,14 +436,26 @@ fn format_theme_settings(app: &TuiApp) -> Vec { ] } +/// Format columns settings. +fn format_columns_settings(app: &TuiApp) -> Vec { + app.tui_config + .tui_columns + .all_columns() + .map(|c| SettingsItem::new(c.typ.to_string(), c.status.to_string())) + .collect() +} + +pub const SETTINGS_TAB_COLUMNS: usize = 6; + /// The name and number of items for each tabs in the setting dialog. -pub const SETTINGS_TABS: [(&str, usize); 6] = [ +pub const SETTINGS_TABS: [(&str, usize); 7] = [ ("Tui", 10), ("Trace", 15), ("Dns", 4), ("GeoIp", 1), ("Bindings", 29), ("Theme", 31), + ("Columns", 0), ]; /// The settings table header. @@ -441,13 +468,16 @@ const SETTINGS_TABLE_WIDTH: [Constraint; 3] = [ ]; struct SettingsItem { - item: &'static str, + item: String, value: String, } impl SettingsItem { - pub fn new(item: &'static str, value: String) -> Self { - Self { item, value } + pub fn new(item: impl Into, value: String) -> Self { + Self { + item: item.into(), + value, + } } } diff --git a/src/frontend/render/table.rs b/src/frontend/render/table.rs index d5952ec33..835ddaa6a 100644 --- a/src/frontend/render/table.rs +++ b/src/frontend/render/table.rs @@ -1,6 +1,6 @@ use crate::backend::trace::Hop; use crate::config::{AddressMode, AsMode, GeoIpMode, IcmpExtensionMode}; -use crate::frontend::columns::{Column, Columns}; +use crate::frontend::columns::{ColumnType, Columns}; use crate::frontend::config::TuiConfig; use crate::frontend::theme::Theme; use crate::frontend::tui_app::TuiApp; @@ -73,8 +73,8 @@ pub fn render(f: &mut Frame<'_>, app: &mut TuiApp, rect: Rect) { /// Render the table header. fn render_table_header(theme: Theme, table_columns: &Columns) -> Row<'static> { - let header_cells = table_columns.0.iter().map(|c| { - Cell::from(c.to_string()).style(Style::default().fg(theme.hops_table_header_text_color)) + let header_cells = table_columns.columns().map(|c| { + Cell::from(c.typ.to_string()).style(Style::default().fg(theme.hops_table_header_text_color)) }); Row::new(header_cells) .style(Style::default().bg(theme.hops_table_header_bg_color)) @@ -99,11 +99,10 @@ fn render_table_row( render_hostname(app, hop, dns, geoip_lookup) }; let cells: Vec> = custom_columns - .0 - .iter() + .columns() .map(|column| { new_cell( - *column, + column.typ, is_selected_hop, app, hop, @@ -126,7 +125,7 @@ fn render_table_row( ///Returns a Cell matched on short char of the Column fn new_cell( - column: Column, + column: ColumnType, is_selected_hop: bool, app: &TuiApp, hop: &Hop, @@ -137,8 +136,8 @@ fn new_cell( let is_target = app.tracer_data().is_target(hop, app.selected_flow); let total_recv = hop.total_recv(); match column { - Column::Ttl => render_usize_cell(hop.ttl().into()), - Column::Host => { + ColumnType::Ttl => render_usize_cell(hop.ttl().into()), + ColumnType::Host => { let (host_cell, _) = if is_selected_hop && app.show_hop_details { render_hostname_with_details(app, hop, dns, geoip_lookup, config) } else { @@ -146,22 +145,22 @@ fn new_cell( }; host_cell } - Column::LossPct => render_loss_pct_cell(hop), - Column::Sent => render_usize_cell(hop.total_sent()), - Column::Received => render_usize_cell(hop.total_recv()), - Column::Last => render_float_cell(hop.last_ms(), 1, total_recv), - Column::Average => render_avg_cell(hop), - Column::Best => render_float_cell(hop.best_ms(), 1, total_recv), - Column::Worst => render_float_cell(hop.worst_ms(), 1, total_recv), - Column::StdDev => render_stddev_cell(hop), - Column::Status => render_status_cell(hop, is_target), - Column::Jitter => render_float_cell(hop.jitter_ms(), 1, total_recv), - Column::Javg => render_float_cell(Some(hop.javg_ms()), 1, total_recv), - Column::Jmax => render_float_cell(hop.jmax_ms(), 1, total_recv), - Column::Jinta => render_float_cell(Some(hop.jinta()), 1, total_recv), - Column::LastSrcPort => render_port_cell(hop.last_src_port()), - Column::LastDestPort => render_port_cell(hop.last_dest_port()), - Column::LastSeq => render_usize_cell(usize::from(hop.last_sequence())), + ColumnType::LossPct => render_loss_pct_cell(hop), + ColumnType::Sent => render_usize_cell(hop.total_sent()), + ColumnType::Received => render_usize_cell(hop.total_recv()), + ColumnType::Last => render_float_cell(hop.last_ms(), 1, total_recv), + ColumnType::Average => render_avg_cell(hop), + ColumnType::Best => render_float_cell(hop.best_ms(), 1, total_recv), + ColumnType::Worst => render_float_cell(hop.worst_ms(), 1, total_recv), + ColumnType::StdDev => render_stddev_cell(hop), + ColumnType::Status => render_status_cell(hop, is_target), + ColumnType::Jitter => render_float_cell(hop.jitter_ms(), 1, total_recv), + ColumnType::Javg => render_float_cell(Some(hop.javg_ms()), 1, total_recv), + ColumnType::Jmax => render_float_cell(hop.jmax_ms(), 1, total_recv), + ColumnType::Jinta => render_float_cell(Some(hop.jinta()), 1, total_recv), + ColumnType::LastSrcPort => render_port_cell(hop.last_src_port()), + ColumnType::LastDestPort => render_port_cell(hop.last_dest_port()), + ColumnType::LastSeq => render_usize_cell(usize::from(hop.last_sequence())), } } diff --git a/src/frontend/tui_app.rs b/src/frontend/tui_app.rs index 2d6c93828..283ca8e2e 100644 --- a/src/frontend/tui_app.rs +++ b/src/frontend/tui_app.rs @@ -2,7 +2,7 @@ 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::frontend::render::settings::{SETTINGS_TABS, SETTINGS_TAB_COLUMNS}; use crate::geoip::GeoIpLookup; use crate::TraceInfo; use itertools::Itertools; @@ -253,7 +253,7 @@ impl TuiApp { } pub fn next_settings_item(&mut self) { - let count = SETTINGS_TABS[self.settings_tab_selected].1; + let count = self.get_settings_items_count(); let max_index = 0.max(count.saturating_sub(1)); let i = match self.setting_table_state.selected() { Some(i) => { @@ -269,7 +269,7 @@ impl TuiApp { } pub fn previous_settings_item(&mut self) { - let count = SETTINGS_TABS[self.settings_tab_selected].1; + let count = self.get_settings_items_count(); let i = match self.setting_table_state.selected() { Some(i) => { if i > 0 { @@ -283,6 +283,45 @@ impl TuiApp { self.setting_table_state.select(Some(i)); } + fn get_settings_items_count(&self) -> usize { + if self.settings_tab_selected == SETTINGS_TAB_COLUMNS { + self.tui_config.tui_columns.all_columns_count() + } else { + SETTINGS_TABS[self.settings_tab_selected].1 + } + } + + pub fn toggle_column_visibility(&mut self) { + if self.settings_tab_selected == SETTINGS_TAB_COLUMNS { + if let Some(selected) = self.setting_table_state.selected() { + self.tui_config.tui_columns.toggle(selected); + } + } + } + + pub fn move_column_down(&mut self) { + if self.settings_tab_selected == SETTINGS_TAB_COLUMNS { + let count = self.tui_config.tui_columns.all_columns_count(); + if let Some(selected) = self.setting_table_state.selected() { + if selected < count - 1 { + self.tui_config.tui_columns.move_down(selected); + self.setting_table_state.select(Some(selected + 1)); + } + } + } + } + + pub fn move_column_up(&mut self) { + if self.settings_tab_selected == SETTINGS_TAB_COLUMNS { + if let Some(selected) = self.setting_table_state.selected() { + if selected > 0 { + self.tui_config.tui_columns.move_up(selected); + self.setting_table_state.select(Some(selected - 1)); + } + } + } + } + pub fn clear(&mut self) { self.table_state.select(None); self.selected_hop_address = 0;