Repository: fujiapple852/trippy (language: rust)

Issue:
TUI settings columns visibility and ordering
Allow users to add/remove/rename columns in the TUI settings dialog.


Discussion / hints:

Fix patch:
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<B: Backend>(
                             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<Column>);
+pub struct Columns(Vec<Column>);
 
 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<Constraint> {
         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<Item = &Column> {
+        self.0
+            .iter()
+            .filter(|c| matches!(c.status, ColumnStatus::Shown))
+    }
+
+    pub fn all_columns(&self) -> impl Iterator<Item = &Column> {
+        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<TuiColumns> 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<char> = self.0.clone().into_iter().map(Column::into).collect();
+        let output: Vec<char> = 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<Column> for char {
-    fn from(col_type: Column) -> Self {
+impl From<ColumnType> 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<Column> for char {
 impl From<TuiColumn> 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<SettingsItem>)> {
+fn format_all_settings(app: &TuiApp) -> Vec<(&'static str, String, Vec<SettingsItem>)> {
     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<SettingsItem> {
     ]
 }
 
+/// Format columns settings.
+fn format_columns_settings(app: &TuiApp) -> Vec<SettingsItem> {
+    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<String>, 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<Cell<'_>> = 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;


Test patch: