use crate::config::{TuiColumn, TuiColumns};
use crate::t;
use ratatui::layout::{Constraint, Rect};
use std::borrow::Cow;
use std::fmt::{Debug, Display, Formatter};
use strum::{EnumIter, IntoEnumIterator};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Columns(Vec<Column>);
impl Columns {
pub fn constraints(&self, rect: Rect) -> Vec<Constraint> {
let total_fixed_width = self
.columns()
.map(|c| match c.typ.width() {
ColumnWidth::Fixed(width) => width,
ColumnWidth::Variable => 0,
})
.sum();
let variable_width_count = self
.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.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 {
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
.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 const fn new_shown(typ: ColumnType) -> Self {
Self {
typ,
status: ColumnStatus::Shown,
}
}
pub const 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, "{}", t!("on")),
Self::Hidden => write!(f, "{}", t!("off")),
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, EnumIter)]
pub enum ColumnType {
Ttl,
Host,
LossPct,
Sent,
Received,
Last,
Average,
Best,
Worst,
StdDev,
Status,
Jitter,
Javg,
Jmax,
Jinta,
LastSrcPort,
LastDestPort,
LastSeq,
LastIcmpPacketType,
LastIcmpPacketCode,
LastNatStatus,
Failed,
Floss,
Bloss,
FlossPct,
Dscp,
Ecn,
}
impl From<ColumnType> for char {
fn from(col_type: ColumnType) -> Self {
match col_type {
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',
ColumnType::LastIcmpPacketType => 'T',
ColumnType::LastIcmpPacketCode => 'C',
ColumnType::LastNatStatus => 'N',
ColumnType::Failed => 'f',
ColumnType::Floss => 'F',
ColumnType::Bloss => 'B',
ColumnType::FlossPct => 'D',
ColumnType::Dscp => 'K',
ColumnType::Ecn => 'M',
}
}
}
impl From<TuiColumn> for Column {
fn from(value: TuiColumn) -> Self {
match value {
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),
TuiColumn::LastIcmpPacketType => Self::new_shown(ColumnType::LastIcmpPacketType),
TuiColumn::LastIcmpPacketCode => Self::new_shown(ColumnType::LastIcmpPacketCode),
TuiColumn::LastNatStatus => Self::new_shown(ColumnType::LastNatStatus),
TuiColumn::Failed => Self::new_shown(ColumnType::Failed),
TuiColumn::Floss => Self::new_shown(ColumnType::Floss),
TuiColumn::Bloss => Self::new_shown(ColumnType::Bloss),
TuiColumn::FlossPct => Self::new_shown(ColumnType::FlossPct),
TuiColumn::Dscp => Self::new_shown(ColumnType::Dscp),
TuiColumn::Ecn => Self::new_shown(ColumnType::Ecn),
}
}
}
impl Display for ColumnType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
impl ColumnType {
#[allow(clippy::cognitive_complexity)]
pub(self) fn name(&self) -> Cow<'_, str> {
match self {
Self::Ttl => Cow::Borrowed("#"),
Self::Host => t!("column_host"),
Self::LossPct => t!("column_loss_pct"),
Self::Sent => t!("column_snd"),
Self::Received => t!("column_recv"),
Self::Last => t!("column_last"),
Self::Average => t!("column_avg"),
Self::Best => t!("column_best"),
Self::Worst => t!("column_wrst"),
Self::StdDev => t!("column_stdev"),
Self::Status => t!("column_sts"),
Self::Jitter => t!("column_jttr"),
Self::Javg => t!("column_javg"),
Self::Jmax => t!("column_jmax"),
Self::Jinta => t!("column_jint"),
Self::LastSrcPort => t!("column_sprt"),
Self::LastDestPort => t!("column_dprt"),
Self::LastSeq => t!("column_seq"),
Self::LastIcmpPacketType => t!("column_type"),
Self::LastIcmpPacketCode => t!("column_code"),
Self::LastNatStatus => t!("column_nat"),
Self::Failed => t!("column_fail"),
Self::Floss => t!("column_floss"),
Self::Bloss => t!("column_bloss"),
Self::FlossPct => t!("column_floss_pct"),
Self::Dscp => t!("column_dscp"),
Self::Ecn => t!("column_ecn"),
}
}
pub(self) fn width(self) -> ColumnWidth {
let width = self.name().width() as u16 + 2;
#[allow(clippy::match_same_arms)]
match self {
Self::Ttl => ColumnWidth::Fixed(4),
Self::Host => ColumnWidth::Variable,
Self::LossPct => ColumnWidth::Fixed(width.max(8)),
Self::Sent => ColumnWidth::Fixed(width.max(7)),
Self::Received => ColumnWidth::Fixed(width.max(7)),
Self::Last => ColumnWidth::Fixed(width.max(7)),
Self::Average => ColumnWidth::Fixed(width.max(7)),
Self::Best => ColumnWidth::Fixed(width.max(7)),
Self::Worst => ColumnWidth::Fixed(width.max(7)),
Self::StdDev => ColumnWidth::Fixed(width.max(8)),
Self::Status => ColumnWidth::Fixed(width.max(7)),
Self::Jitter => ColumnWidth::Fixed(width.max(7)),
Self::Javg => ColumnWidth::Fixed(width.max(7)),
Self::Jmax => ColumnWidth::Fixed(width.max(7)),
Self::Jinta => ColumnWidth::Fixed(width.max(8)),
Self::LastSrcPort => ColumnWidth::Fixed(width.max(7)),
Self::LastDestPort => ColumnWidth::Fixed(width.max(7)),
Self::LastSeq => ColumnWidth::Fixed(width.max(7)),
Self::LastIcmpPacketType => ColumnWidth::Fixed(width.max(7)),
Self::LastIcmpPacketCode => ColumnWidth::Fixed(width.max(7)),
Self::LastNatStatus => ColumnWidth::Fixed(width.max(7)),
Self::Failed => ColumnWidth::Fixed(width.max(7)),
Self::Floss => ColumnWidth::Fixed(width.max(7)),
Self::Bloss => ColumnWidth::Fixed(width.max(7)),
Self::FlossPct => ColumnWidth::Fixed(width.max(8)),
Self::Dscp => ColumnWidth::Fixed(width.max(7)),
Self::Ecn => ColumnWidth::Fixed(width.max(7)),
}
}
}
#[derive(Debug, PartialEq)]
enum ColumnWidth {
Fixed(u16),
Variable,
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::layout::Constraint::Min;
use test_case::test_case;
#[test]
fn test_columns_conversion_from_tui_columns() {
let tui_columns = TuiColumns(vec![
TuiColumn::Ttl,
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::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),
Column::new_hidden(ColumnType::LastIcmpPacketType),
Column::new_hidden(ColumnType::LastIcmpPacketCode),
Column::new_hidden(ColumnType::LastNatStatus),
Column::new_hidden(ColumnType::Failed),
Column::new_hidden(ColumnType::Floss),
Column::new_hidden(ColumnType::Bloss),
Column::new_hidden(ColumnType::FlossPct),
Column::new_hidden(ColumnType::Dscp),
Column::new_hidden(ColumnType::Ecn),
])
);
}
#[test]
fn test_column_conversion_from_tui_column() {
let tui_column = TuiColumn::Received;
let column = Column::from(tui_column);
assert_eq!(column.typ, ColumnType::Received);
assert_eq!(column.status, ColumnStatus::Shown);
}
#[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(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);
}
#[test]
fn test_column_constraints() {
let columns = Columns::from(TuiColumns::default());
let constraints = columns.constraints(Rect::new(0, 0, 80, 0));
assert_eq!(
vec![
Min(4),
Min(11),
Min(8),
Min(7),
Min(7),
Min(7),
Min(7),
Min(7),
Min(7),
Min(8),
Min(7)
],
constraints
);
}
#[test]
fn test_columns_into_string_short() {
let cols = Columns(vec![
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}"));
}
#[test]
fn test_columns_into_string_happy_path() {
let cols = Columns(vec![
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}"));
}
#[test]
fn test_columns_into_string_reverse_str() {
let cols = Columns(vec![
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}"));
}
}