use crate::config; use crate::services::item_service::ItemService; use crate::services::types::ItemWithMeta; use crate::modes::common::ColumnType; use crate::modes::common::{size_column, string_column, OutputFormat}; use anyhow::{Result}; use log::debug; use prettytable::format::Alignment; use prettytable::{color, row, Attr, Cell, Row, Table}; use serde::{Deserialize, Serialize}; use serde_json; use serde_yaml; use std::env; use std::io::{stdout, IsTerminal}; use termsize; #[derive(Serialize, Deserialize)] struct ListItem { id: Option, time: String, size: Option, size_formatted: String, compression: String, file_size: Option, file_size_formatted: String, file_path: String, tags: Vec, meta: std::collections::HashMap, } pub fn mode_list( cmd: &mut clap::Command, settings: &config::Settings, ids: &mut Vec, tags: &Vec, conn: &mut rusqlite::Connection, data_path: std::path::PathBuf, ) -> Result<()> { if !ids.is_empty() { cmd.error( clap::error::ErrorKind::InvalidValue, "ID given, you can only supply tags when using --list", ) .exit(); } let mut meta: std::collections::HashMap = std::collections::HashMap::new(); // Collect metadata from environment variables for (key, value) in std::env::vars() { if key.starts_with("KEEP_META_") && key != "KEEP_META_PLUGINS" { let meta_name = key.strip_prefix("KEEP_META_").unwrap(); meta.insert(meta_name.to_string(), value); } } let item_service = ItemService::new(data_path.clone()); let items_with_meta = item_service.list_items(conn, tags, &meta)?; let output_format = crate::modes::common::settings_output_format(settings); if output_format != OutputFormat::Table { return show_list_structured(items_with_meta, data_path, settings, output_format); } // Check if output is a terminal let is_terminal = stdout().is_terminal(); debug!("Output is terminal: {}", is_terminal); // Get terminal width once, default to 80 if not available // Only use max_len when output is a terminal let term_width = if is_terminal { if let Ok(columns_env) = env::var("COLUMNS") { debug!("COLUMNS environment variable: {:?}", columns_env); columns_env.parse::().unwrap_or(80) } else { // Try to get terminal size using termsize match termsize::get() { Some(size) => { let width = size.cols as usize; debug!("Terminal size detected: {} columns", width); width } None => { debug!("Failed to get terminal size, defaulting to 80"); 80 } } } } else { debug!("Not a terminal, max_len will be ignored"); 0 // Use 0 to indicate no truncation }; debug!("Terminal width: {}", term_width); let mut table = Table::new(); table.set_format(*prettytable::format::consts::FORMAT_CLEAN); let mut title_row = row!(); for column in &settings.list_format { title_row.add_cell(Cell::new(&column.label).with_style(Attr::Bold)); } table.set_titles(title_row); for item_with_meta in items_with_meta { let tags: Vec = item_with_meta.tags.iter().map(|t| t.name.clone()).collect(); let meta = item_with_meta.meta_as_map(); let item = item_with_meta.item; let mut item_path = data_path.clone(); item_path.push(item.id.unwrap().to_string()); let mut table_row = Row::new(vec![]); for column in &settings.list_format { let column_type = ColumnType::from_str(&column.name) .unwrap_or_else(|_| panic!("Unknown column {:?}", column.name)); let mut meta_name: Option<&str> = None; // Parse max_len, handling numbers, percentages, and negative values // Only apply max_len when output is a terminal let column_width = if is_terminal { if let Some(max_len_str) = &column.max_len { debug!("Processing max_len for column '{}': {}", column.name, max_len_str); // Check if it's a negative number if max_len_str.starts_with('-') { // Parse as negative number let abs_value = max_len_str[1..].parse::().unwrap_or(0); if abs_value > term_width { 0 } else { term_width - abs_value } } else if max_len_str.ends_with('%') { // Parse percentage let percent_str = max_len_str.trim_end_matches('%'); let percent = percent_str.parse::().unwrap_or(0.0); debug!("Percentage: {}%", percent); let computed_width = (term_width as f64 * percent / 100.0) as usize; debug!("Computed width: {}", computed_width); computed_width } else { // Parse absolute number let absolute_width = max_len_str.parse::().unwrap_or(0); debug!("Absolute width: {}", absolute_width); absolute_width } } else { debug!("No max_len specified for column '{}'", column.name); 0 } } else { debug!("Output is not a terminal, ignoring max_len"); 0 }; debug!("Final column width for '{}': {}", column.name, column_width); if let ColumnType::Meta = column_type { let parts: Vec<&str> = column.name.split(':').collect(); if parts.len() > 1 { meta_name = Some(parts[1]); } } let cell = match column_type { ColumnType::Id => { let mut cell = Cell::new(&string_column(item.id.unwrap_or(0).to_string(), column_width)); match column.align { crate::config::ColumnAlignment::Right => { cell.align(Alignment::RIGHT); } crate::config::ColumnAlignment::Left => { cell.align(Alignment::LEFT); } } cell } ColumnType::Time => { let mut cell = Cell::new(&string_column( item.ts .with_timezone(&chrono::Local) .format("%F %T") .to_string(), column_width, )); match column.align { crate::config::ColumnAlignment::Right => { cell.align(Alignment::RIGHT); } crate::config::ColumnAlignment::Left => { cell.align(Alignment::LEFT); } } cell } ColumnType::Size => match item.size { Some(size) => { let mut cell = Cell::new(&size_column( size as u64, settings.human_readable, column_width, )); match column.align { crate::config::ColumnAlignment::Right => { cell.align(Alignment::RIGHT); } crate::config::ColumnAlignment::Left => { cell.align(Alignment::LEFT); } } cell } None => { let mut cell = match item_path.metadata() { Ok(_) => Cell::new("Unknown") .with_style(Attr::ForegroundColor(color::YELLOW)) .with_style(Attr::Bold), Err(_) => Cell::new("Missing") .with_style(Attr::ForegroundColor(color::RED)) .with_style(Attr::Bold), }; match column.align { crate::config::ColumnAlignment::Right => { cell.align(Alignment::RIGHT); } crate::config::ColumnAlignment::Left => { cell.align(Alignment::LEFT); } } cell } }, ColumnType::Compression => { let mut cell = Cell::new(&string_column(item.compression.to_string(), column_width)); match column.align { crate::config::ColumnAlignment::Right => { cell.align(Alignment::RIGHT); } crate::config::ColumnAlignment::Left => { cell.align(Alignment::LEFT); } } cell } ColumnType::FileSize => match item_path.metadata() { Ok(metadata) => { let mut cell = Cell::new(&size_column( metadata.len(), settings.human_readable, column_width, )); match column.align { crate::config::ColumnAlignment::Right => { cell.align(Alignment::RIGHT); } crate::config::ColumnAlignment::Left => { cell.align(Alignment::LEFT); } } cell } Err(_) => { let mut cell = Cell::new("Missing") .with_style(Attr::ForegroundColor(color::RED)) .with_style(Attr::Bold); match column.align { crate::config::ColumnAlignment::Right => { cell.align(Alignment::RIGHT); } crate::config::ColumnAlignment::Left => { cell.align(Alignment::LEFT); } } cell } }, ColumnType::FilePath => { let mut cell = Cell::new(&string_column( item_path.clone().into_os_string().into_string().unwrap(), column_width, )); match column.align { crate::config::ColumnAlignment::Right => { cell.align(Alignment::RIGHT); } crate::config::ColumnAlignment::Left => { cell.align(Alignment::LEFT); } } cell } ColumnType::Tags => { let mut cell = Cell::new(&string_column(tags.join(" "), column_width)); match column.align { crate::config::ColumnAlignment::Right => { cell.align(Alignment::RIGHT); } crate::config::ColumnAlignment::Left => { cell.align(Alignment::LEFT); } } cell } ColumnType::Meta => match meta_name { Some(meta_name) => match meta.get(meta_name) { Some(meta_value) => { let mut cell = Cell::new(&string_column(meta_value.to_string(), column_width)); match column.align { crate::config::ColumnAlignment::Right => { cell.align(Alignment::RIGHT); } crate::config::ColumnAlignment::Left => { cell.align(Alignment::LEFT); } } cell } None => { let mut cell = Cell::new(""); match column.align { crate::config::ColumnAlignment::Right => { cell.align(Alignment::RIGHT); } crate::config::ColumnAlignment::Left => { cell.align(Alignment::LEFT); } } cell } }, None => { let mut cell = Cell::new(""); match column.align { crate::config::ColumnAlignment::Right => { cell.align(Alignment::RIGHT); } crate::config::ColumnAlignment::Left => { cell.align(Alignment::LEFT); } } cell } }, }; table_row.add_cell(cell); } table.add_row(table_row); } table.printstd(); Ok(()) } fn show_list_structured( items_with_meta: Vec, data_path: std::path::PathBuf, settings: &config::Settings, output_format: OutputFormat, ) -> Result<()> { let mut list_items = Vec::new(); for item_with_meta in items_with_meta { let tags: Vec = item_with_meta.tags.iter().map(|t| t.name.clone()).collect(); let meta = item_with_meta.meta_as_map(); let item = item_with_meta.item; let item_id = item.id.unwrap(); let mut item_path = data_path.clone(); item_path.push(item_id.to_string()); let file_size = item_path.metadata().map(|m| m.len()).ok(); let file_size_formatted = match file_size { Some(size) => crate::modes::common::format_size(size, settings.human_readable), None => "Missing".to_string(), }; let size_formatted = match item.size { Some(size) => crate::modes::common::format_size(size as u64, settings.human_readable), None => "Unknown".to_string(), }; let list_item = ListItem { id: item.id, time: item .ts .with_timezone(&chrono::Local) .format("%F %T") .to_string(), size: item.size.map(|s| s as u64), size_formatted, compression: item.compression, file_size, file_size_formatted, file_path: item_path.into_os_string().into_string().unwrap_or_default(), tags, meta, }; list_items.push(list_item); } match output_format { OutputFormat::Json => { println!("{}", serde_json::to_string_pretty(&list_items)?); } OutputFormat::Yaml => { println!("{}", serde_yaml::to_string(&list_items)?); } OutputFormat::Table => unreachable!(), } Ok(()) }