From 9f328a376fcebfe661eab3f769023ad6ff80d5c9 Mon Sep 17 00:00:00 2001 From: Andrew Phillips Date: Mon, 8 Sep 2025 18:09:47 -0300 Subject: [PATCH] refactor: Migrate from prettytable to comfy-table for output formatting Co-authored-by: aider (openai/andrew/openrouter/deepseek/deepseek-chat-v3.1) --- src/modes/common.rs | 20 ------ src/modes/info.rs | 131 ++++++++++-------------------------- src/modes/list.rs | 23 ++++--- src/modes/status.rs | 6 +- src/modes/status_plugins.rs | 19 +++--- 5 files changed, 59 insertions(+), 140 deletions(-) diff --git a/src/modes/common.rs b/src/modes/common.rs index 21d2926..fa99484 100644 --- a/src/modes/common.rs +++ b/src/modes/common.rs @@ -4,7 +4,6 @@ use crate::meta_plugin::MetaPluginType; use clap::Command; use clap::error::ErrorKind; use log::debug; -use prettytable::format::TableFormat; use regex::Regex; use std::collections::HashMap; use std::env; @@ -111,25 +110,6 @@ impl ColumnType { // impl TryFrom<&str> for ColumnType is already implemented by strum_macros // so we remove this conflicting implementation -pub fn get_format_box_chars_no_border_line_separator() -> TableFormat { - prettytable::format::FormatBuilder::new() - .column_separator('│') - .borders('│') - .separators( - &[prettytable::format::LinePosition::Top], - prettytable::format::LineSeparator::new('─', '┬', '┌', '┐'), - ) - .separators( - &[prettytable::format::LinePosition::Title], - prettytable::format::LineSeparator::new('─', '┼', '├', '┤'), - ) - .separators( - &[prettytable::format::LinePosition::Bottom], - prettytable::format::LineSeparator::new('─', '┴', '└', '┘'), - ) - .padding(1, 1) - .build() -} diff --git a/src/modes/info.rs b/src/modes/info.rs index d6b1134..fdcbaf7 100644 --- a/src/modes/info.rs +++ b/src/modes/info.rs @@ -12,8 +12,9 @@ use crate::services::item_service::ItemService; use crate::modes::common::get_format_box_chars_no_border_line_separator; use chrono::prelude::*; use is_terminal::IsTerminal; -use prettytable::format; -use prettytable::{Attr, Cell, Row, Table}; +use comfy_table::{Table, ContentArrangement, Cell, Attribute}; +use comfy_table::presets::UTF8_FULL; +use comfy_table::modifiers::UTF8_ROUND_CORNERS; pub fn mode_info( cmd: &mut Command, @@ -71,136 +72,72 @@ fn show_item( let mut table = Table::new(); if std::io::stdout().is_terminal() { - table.set_format(get_format_box_chars_no_border_line_separator()); + table + .load_preset(UTF8_FULL) + .apply_modifier(UTF8_ROUND_CORNERS); } else { - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.set_content_arrangement(ContentArrangement::Dynamic); } - // Collect all rows to calculate column widths - let mut rows = Vec::new(); - // Add all the rows - rows.push(Row::new(vec![ - Cell::new("ID").with_style(Attr::Bold), + table.add_row(vec![ + Cell::new("ID").add_attribute(Attribute::Bold), Cell::new(&item_id.to_string()), - ])); + ]); let timestamp_str = item.ts.with_timezone(&Local).format("%F %T %Z").to_string(); - rows.push(Row::new(vec![ - Cell::new("Timestamp").with_style(Attr::Bold), + table.add_row(vec![ + Cell::new("Timestamp").add_attribute(Attribute::Bold), Cell::new(×tamp_str), - ])); + ]); let mut item_path_buf = data_path.clone(); item_path_buf.push(item.id.unwrap().to_string()); let path_str = item_path_buf.to_str().expect("Unable to get item path").to_string(); - rows.push(Row::new(vec![ - Cell::new("Path").with_style(Attr::Bold), + table.add_row(vec![ + Cell::new("Path").add_attribute(Attribute::Bold), Cell::new(&path_str), - ])); + ]); let size_str = match item.size { Some(size) => format_size(size as u64, settings.human_readable), None => "Missing".to_string(), }; - rows.push(Row::new(vec![ - Cell::new("Stream Size").with_style(Attr::Bold), + table.add_row(vec![ + Cell::new("Stream Size").add_attribute(Attribute::Bold), Cell::new(&size_str), - ])); + ]); - rows.push(Row::new(vec![ - Cell::new("Compression").with_style(Attr::Bold), + table.add_row(vec![ + Cell::new("Compression").add_attribute(Attribute::Bold), Cell::new(&item.compression), - ])); + ]); let file_size_str = match item_path_buf.metadata() { Ok(metadata) => format_size(metadata.len(), settings.human_readable), Err(_) => "Missing".to_string(), }; - rows.push(Row::new(vec![ - Cell::new("File Size").with_style(Attr::Bold), + table.add_row(vec![ + Cell::new("File Size").add_attribute(Attribute::Bold), Cell::new(&file_size_str), - ])); + ]); let tags_str = item_tags.join(" "); - rows.push(Row::new(vec![ - Cell::new("Tags").with_style(Attr::Bold), + table.add_row(vec![ + Cell::new("Tags").add_attribute(Attribute::Bold), Cell::new(&tags_str), - ])); + ]); - // Collect meta rows to potentially truncate the last one - let mut meta_rows = Vec::new(); + // Add meta rows for meta in item_with_meta.meta { let meta_name = format!("Meta: {}", &meta.name); - meta_rows.push((meta_name, meta.value)); + table.add_row(vec![ + Cell::new(&meta_name).add_attribute(Attribute::Bold), + Cell::new(&meta.value), + ]); } - // Calculate the maximum width for the last column if we're in a terminal - let terminal_width = crate::modes::common::get_terminal_width(); - debug!("Terminal width: {}", terminal_width); - if std::io::stdout().is_terminal() && terminal_width > 0 { - // For the info table, we always have exactly 2 columns - // Find the maximum width of the first column (labels) - let max_label_width = rows.iter() - .map(|row| row.get_cell(0).map(|c| c.get_content().chars().count()).unwrap_or(0)) - .max() - .unwrap_or(0); - debug!("Max label width: {}", max_label_width); - debug!("Terminal width: {}", terminal_width); - debug!("Calculated max_value_width: {}", terminal_width.saturating_sub(max_label_width + 8)); - - // Total width used: 1 (left border) + 1 (left padding) + max_label_width + 1 (right padding) + - // 1 (middle separator) + 1 (left padding) + value_width + 1 (right padding) + 1 (right border) - // = max_label_width + value_width + 8 - // We want: max_label_width + value_width + 8 <= terminal_width - // So max value width is: terminal_width - max_label_width - 8 - let max_value_width = terminal_width.saturating_sub(max_label_width + 8); - debug!("Max value width: {}", max_value_width); - - // Process all existing rows to truncate their value cells - for row in &mut rows { - if let Some(value_cell) = row.get_mut_cell(1) { - let content = value_cell.get_content(); - if content.chars().count() > max_value_width { - let truncated = crate::modes::common::truncate_with_ellipsis(&content, max_value_width); - *value_cell = Cell::new(&truncated); - } - } - } - - // Process meta rows to truncate values if necessary - for (meta_name, meta_value) in meta_rows { - let truncated_value = if max_value_width > 0 { - crate::modes::common::truncate_with_ellipsis(&meta_value, max_value_width) - } else { - // If max_value_width is 0, we can't show anything, but let's at least show something - if max_value_width == 0 { - "…".to_string() - } else { - meta_value - } - }; - rows.push(Row::new(vec![ - Cell::new(&meta_name).with_style(Attr::Bold), - Cell::new(&truncated_value), - ])); - } - } else { - // Not a terminal or no terminal width, add all meta rows without truncation - for (meta_name, meta_value) in meta_rows { - rows.push(Row::new(vec![ - Cell::new(&meta_name).with_style(Attr::Bold), - Cell::new(&meta_value), - ])); - } - } - - // Add all rows to the table - for row in rows { - table.add_row(row); - } - - table.printstd(); + println!("{}", table); Ok(()) } diff --git a/src/modes/list.rs b/src/modes/list.rs index f865774..b9cd71c 100644 --- a/src/modes/list.rs +++ b/src/modes/list.rs @@ -5,9 +5,10 @@ use crate::modes::common::ColumnType; use crate::modes::common::{size_column, string_column, OutputFormat}; use anyhow::{Result}; use log::debug; -use comfy_table::{Table, ContentArrangement, Cell, Color, Attribute}; +use comfy_table::{Table, ContentArrangement, Cell, Row, Color, Attribute}; use comfy_table::presets::UTF8_FULL; use comfy_table::modifiers::UTF8_ROUND_CORNERS; +use comfy_table::CellAlignment; use serde::{Deserialize, Serialize}; use serde_json; use serde_yaml; @@ -84,7 +85,7 @@ pub fn mode_list( let mut item_path = data_path.clone(); item_path.push(item.id.unwrap().to_string()); - let mut table_row = Row::new(vec![]); + let mut table_row = Row::new(); for column in &settings.list_format { let column_type = ColumnType::from_str(&column.name) @@ -144,10 +145,10 @@ pub fn mode_list( Cell::new(&string_column(item.id.unwrap_or(0).to_string(), column_width)); match column.align { crate::config::ColumnAlignment::Right => { - cell.align(Alignment::RIGHT); + cell = cell.alignment(CellAlignment::Right); } crate::config::ColumnAlignment::Left => { - cell.align(Alignment::LEFT); + cell = cell.alignment(CellAlignment::Left); } } cell @@ -190,11 +191,11 @@ pub fn mode_list( None => { let mut cell = match item_path.metadata() { Ok(_) => Cell::new("Unknown") - .with_style(Attr::ForegroundColor(color::YELLOW)) - .with_style(Attr::Bold), + .fg(Color::Yellow) + .add_attribute(Attribute::Bold), Err(_) => Cell::new("Missing") - .with_style(Attr::ForegroundColor(color::RED)) - .with_style(Attr::Bold), + .fg(Color::Red) + .add_attribute(Attribute::Bold), }; match column.align { crate::config::ColumnAlignment::Right => { @@ -239,8 +240,8 @@ pub fn mode_list( } Err(_) => { let mut cell = Cell::new("Missing") - .with_style(Attr::ForegroundColor(color::RED)) - .with_style(Attr::Bold); + .fg(Color::Red) + .add_attribute(Attribute::Bold); match column.align { crate::config::ColumnAlignment::Right => { cell.align(Alignment::RIGHT); @@ -326,7 +327,7 @@ pub fn mode_list( table.add_row(table_row); } - table.printstd(); + println!("{}", table); Ok(()) } diff --git a/src/modes/status.rs b/src/modes/status.rs index 823cef4..1c27df7 100644 --- a/src/modes/status.rs +++ b/src/modes/status.rs @@ -234,17 +234,17 @@ pub fn mode_status( match output_format { OutputFormat::Table => { println!("CONFIG:"); - build_config_table(settings).printstd(); + println!("{}", build_config_table(settings)); println!(); println!("PATHS:"); - build_path_table(&status_info.paths).printstd(); + println!("{}", build_path_table(&status_info.paths)); println!(); // Always try to print META PLUGINS CONFIGURED section using status_info if let Some(meta_plugins_table) = build_meta_plugins_configured_table(&status_info) { println!("META PLUGINS CONFIGURED:"); - meta_plugins_table.printstd(); + println!("{}", meta_plugins_table); println!(); } else { println!("META PLUGINS CONFIGURED:"); diff --git a/src/modes/status_plugins.rs b/src/modes/status_plugins.rs index b312740..f4956c1 100644 --- a/src/modes/status_plugins.rs +++ b/src/modes/status_plugins.rs @@ -142,16 +142,16 @@ fn build_compression_table(compression_info: &Vec) -> Table { compression_table.add_row(vec![ info.compression_type.clone(), match info.found { - true => Cell::new("Yes").fg(Color::Green), - false => Cell::new("No").fg(Color::Red), + true => Cell::new("Yes").fg(Color::Green).to_string(), + false => Cell::new("No").fg(Color::Red).to_string(), }, match info.default { - true => Cell::new("Yes").fg(Color::Green), - false => Cell::new("No"), + true => Cell::new("Yes").fg(Color::Green).to_string(), + false => Cell::new("No").to_string(), }, match info.binary.as_str() { - "" => Cell::new(&info.binary).fg(Color::DarkGrey), - _ => Cell::new(&info.binary), + "" => Cell::new(&info.binary).fg(Color::DarkGrey).to_string(), + _ => info.binary.clone(), }, info.compress.clone(), info.decompress.clone(), @@ -287,15 +287,16 @@ pub fn mode_status_plugins( match output_format { OutputFormat::Table => { println!("META PLUGINS:"); - build_meta_plugin_table(&status_info.meta_plugins).printstd(); + println!("META PLUGINS:"); + println!("{}", build_meta_plugin_table(&status_info.meta_plugins)); println!(); println!("COMPRESSION PLUGINS:"); - build_compression_table(&status_info.compression).printstd(); + println!("{}", build_compression_table(&status_info.compression)); println!(); println!("FILTER PLUGINS:"); - build_filter_plugin_table(&status_info.filter_plugins).printstd(); + println!("{}", build_filter_plugin_table(&status_info.filter_plugins)); println!(); Ok(()) },