/// List mode implementation. /// /// This module provides the functionality to list stored items with customizable /// formatting, filtering by tags, and support for different output formats /// including table, JSON, and YAML. use crate::config; use crate::modes::common::ColumnType; use crate::modes::common::{OutputFormat, apply_color, apply_table_attribute, format_size}; use crate::services::item_service::ItemService; use crate::services::types::ItemWithMeta; use anyhow::{Context, Result}; use comfy_table::CellAlignment; use comfy_table::{Attribute, Cell, Color, Row}; use serde::{Deserialize, Serialize}; use serde_json; use serde_yaml; /// Structure representing a list item for structured output formats. /// /// This struct holds all the information needed to serialize an item for JSON or /// YAML output in list mode. #[derive(Serialize, Deserialize)] struct ListItem { /// Item ID. /// /// The unique identifier for the item. id: Option, /// Timestamp. /// /// The formatted timestamp string for the item. time: String, /// Size in bytes. /// /// The raw size of the item content. size: Option, /// Formatted size. /// /// Human-readable size string. size_formatted: String, /// Compression type. /// /// The compression algorithm used for the item. compression: String, /// File size in bytes. /// /// The size of the stored file on disk. file_size: Option, /// Formatted file size. /// /// Human-readable file size string. file_size_formatted: String, /// File path. /// /// The full path to the item's storage file. file_path: String, /// Tags. /// /// Vector of tag names associated with the item. tags: Vec, /// Metadata. /// /// HashMap of metadata key-value pairs. meta: std::collections::HashMap, } /// Main list mode function. /// /// This function handles the listing of items based on tags, applying formatting /// and output options from settings. It supports table, JSON, and YAML output formats. /// /// # Arguments /// /// * `cmd` - Mutable reference to the Clap command for error handling. /// * `settings` - Reference to application settings. /// * `ids` - Mutable vector of item IDs (should be empty for list mode). /// * `tags` - Reference to vector of tags for filtering. /// * `conn` - Mutable reference to database connection. /// * `data_path` - Path to the data directory. /// /// # Returns /// /// * `Result<()>` - Success or error if listing fails. pub fn mode_list( cmd: &mut clap::Command, settings: &config::Settings, ids: &mut [i64], tags: &[String], 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 item_service = ItemService::new(data_path.clone()); let meta_filter: std::collections::HashMap> = settings .meta .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(); let items_with_meta = item_service.list_items(conn, tags, &meta_filter)?; 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); } let mut table = crate::modes::common::create_table_with_config(&settings.table_config); // Create header row let mut header_cells = Vec::new(); for column in &settings.list_format { header_cells.push(Cell::new(&column.label).add_attribute(Attribute::Bold)); } table.set_header(header_cells); 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.context("Item missing ID")?.to_string()); let mut table_row = Row::new(); for column in &settings.list_format { let column_type = column .name .parse::() .with_context(|| format!("Unknown column type {:?} in list format", column.name))?; let mut meta_name: Option<&str> = None; 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_content = match column_type { ColumnType::Id => item.id.unwrap_or(0).to_string(), ColumnType::Time => item .ts .with_timezone(&chrono::Local) .format("%F %T") .to_string(), ColumnType::Size => match item.size { Some(size) => format_size(size as u64, settings.human_readable), None => match item_path.metadata() { Ok(_) => "Unknown".to_string(), Err(_) => "Missing".to_string(), }, }, ColumnType::Compression => item.compression.to_string(), ColumnType::FileSize => match item_path.metadata() { Ok(metadata) => format_size(metadata.len(), settings.human_readable), Err(_) => "Missing".to_string(), }, ColumnType::FilePath => item_path .clone() .into_os_string() .into_string() .unwrap_or_else(|os| os.to_string_lossy().into_owned()), ColumnType::Tags => tags.join(" "), ColumnType::Meta => match meta_name { Some(meta_name) => match meta.get(meta_name) { Some(meta_value) => meta_value.to_string(), None => "".to_string(), }, None => "".to_string(), }, }; // Truncate content to max 3 lines let mut cell_lines: Vec = cell_content.split('\n').map(|s| s.to_string()).collect(); if cell_lines.len() > 3 { cell_lines.truncate(3); // Add ellipsis to the last line if we truncated if let Some(last_line) = cell_lines.last_mut() { if last_line.len() > 3 { last_line.truncate(last_line.len() - 3); } last_line.push_str("..."); } } let truncated_content = cell_lines.join("\n"); let mut cell = Cell::new(truncated_content); // Apply column-specific styling if let Some(fg_color) = &column.fg_color { cell = apply_color(cell, fg_color, true); } if let Some(bg_color) = &column.bg_color { cell = apply_color(cell, bg_color, false); } for attribute in &column.attributes { cell = apply_table_attribute(cell, attribute); } // Apply padding if specified if let Some((_left_padding, _right_padding)) = column.padding { // Note: comfy-table doesn't directly support padding, so we'd need to handle this // by adding spaces to the content, or use a different approach } // Apply styling for specific cases match column_type { ColumnType::Size => { if item.size.is_none() { if item_path.metadata().is_ok() { cell = cell .fg(comfy_table::Color::Yellow) .add_attribute(Attribute::Bold); } else { cell = cell .fg(comfy_table::Color::Red) .add_attribute(Attribute::Bold); } } } ColumnType::FileSize => { if item_path.metadata().is_err() { cell = cell .fg(comfy_table::Color::Red) .add_attribute(Attribute::Bold); } } _ => {} } // Apply alignment cell = match column.align { crate::config::ColumnAlignment::Right => cell.set_alignment(CellAlignment::Right), crate::config::ColumnAlignment::Left => cell.set_alignment(CellAlignment::Left), crate::config::ColumnAlignment::Center => cell.set_alignment(CellAlignment::Center), }; table_row.add_cell(cell); } table.add_row(table_row); } println!( "{}", crate::modes::common::trim_lines_end(&table.trim_fmt()) ); 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.context("Item missing ID")?; 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(()) }