Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ pub struct FilterActivitiesArgs {
#[arg(short = 't', long = "filter-by-tags", value_name = "TAGS", value_delimiter = ',', action = ArgAction::Append)]
pub filter_by_tags: Option<Vec<String>>,

/// Select and order which fields are shown in the table (comma-separated)
#[arg(short = 'f', long = "fields", value_name = "FIELDS", value_delimiter = ',', action = ArgAction::Append)]
pub fields: Option<Vec<String>>,

/// Output in JSON format
#[arg(short = 'j', long = "json")]
pub use_json_format: bool,
Expand Down
7 changes: 6 additions & 1 deletion src/commands/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ pub fn list_activity_logs(
.unwrap_or(GroupBy::None);
info!("grouping by: {group_by_value}");

let fields = args
.fields
.clone()
.or_else(|| config.commands.list.fields.clone());

info!("getting all activities");
let db_acts: Vec<_> = activities::get_all(conn)?;
let boat_data = BoatData::create_filtered_data(db_acts, period);
Expand Down Expand Up @@ -67,7 +72,7 @@ pub fn list_activity_logs(
let (text, tooltip) = utils::display::get_group_by_display_values(group_by_value, group)?;
let ribbon = utils::display::format_ascii_ribbon(&text, tooltip.as_deref());
println!("{ribbon}");
utils::common::list_printable_items(act_logs, false)?;
utils::common::list_printable_items(act_logs, false, fields.as_deref())?;
}

Ok(())
Expand Down
15 changes: 13 additions & 2 deletions src/commands/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,24 @@ pub fn show_report(
// }
}

list_activity_summaries(&boat_data, args.use_json_format, &args.filter_by_tags)
let fields = args
.fields
.clone()
.or_else(|| config.commands.report.fields.clone());

list_activity_summaries(
&boat_data,
args.use_json_format,
&args.filter_by_tags,
fields.as_deref(),
)
}

fn list_activity_summaries(
boat_data: &BoatData,
use_json: bool,
filter_by_tags: &Option<Vec<String>>,
fields: Option<&[String]>,
) -> Result<()> {
info!("filtering logs by tags");
let prt_acts = boat_data
Expand All @@ -85,7 +96,7 @@ fn list_activity_summaries(
.collect();

info!("listing activity summaries");
utils::common::list_printable_items(&prt_acts, use_json)?;
utils::common::list_printable_items(&prt_acts, use_json, fields)?;

if !use_json && !prt_acts.is_empty() {
let total_sec: i64 = prt_acts.iter().map(|pa| pa.duration).sum();
Expand Down
12 changes: 12 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ pub struct ListCommandConfig {
#[serde(rename = "group_by")]
#[serde(skip_serializing_if = "Option::is_none")]
pub group_by: Option<GroupBy>,

/// Select and order which fields are shown in the table
#[serde(rename = "fields")]
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<Vec<String>>,
// /// Specify how entries should be sorted
// #[serde(rename = "sort_by")]
// pub sort_by: Option<String>,
Expand All @@ -144,6 +149,11 @@ pub struct ReportCommandConfig {
#[serde(rename = "period")]
#[serde(skip_serializing_if = "Option::is_none")]
pub period: Option<PeriodInput>,

/// Select and order which fields are shown in the table
#[serde(rename = "fields")]
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<Vec<String>>,
// /// Specify how entries should be grouped
// #[serde(rename = "group_by")]
// #[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -184,11 +194,13 @@ impl Configuration {
list: ListCommandConfig {
period: None,
group_by: None,
fields: None,
// sort_by: None,
// format: None,
},
report: ReportCommandConfig {
period: None,
fields: None,
// group_by: None,
// sort_by: None,
// format: None,
Expand Down
19 changes: 9 additions & 10 deletions src/models/activity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;

use crate::models::RowPrintable;
use crate::models::{FieldSpec, RowPrintable};
use crate::utils;

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand Down Expand Up @@ -58,15 +58,14 @@ impl PrintableActivity {
}

impl RowPrintable for PrintableActivity {
fn row_spec() -> String {
"{:>} {:<} {:<} {:<} {:<}".to_string()
}

fn header_names() -> Vec<String> {
["ID", "Name", "Description", "Tags", "Duration"]
.iter()
.map(|s| s.to_string())
.collect()
fn field_specs() -> Vec<FieldSpec> {
vec![
("id", "ID", '>'),
("name", "Name", '<'),
("description", "Description", '<'),
("tags", "Tags", '<'),
("duration", "Duration", '<'),
]
}

fn row_values(&self) -> Vec<String> {
Expand Down
27 changes: 10 additions & 17 deletions src/models/activity_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use yansi::Paint;

use crate::{
models::{RowPrintable, activity::SimpleActivity, log::PrintableLog},
models::{FieldSpec, RowPrintable, activity::SimpleActivity, log::PrintableLog},
utils::{self, date::DateTimeRenderMode},
};

Expand All @@ -23,23 +23,16 @@ impl PrintableActivityLog {
}

impl RowPrintable for PrintableActivityLog {
fn row_spec() -> String {
" {:>} {:<} {:<} {:<} {:^} {:^} {:<}".to_string()
}

fn header_names() -> Vec<String> {
[
"ID",
"Name",
"Description",
"Tags",
"Start",
"End",
"Duration",
fn field_specs() -> Vec<FieldSpec> {
vec![
("id", "ID", '>'),
("name", "Name", '<'),
("description", "Description", '<'),
("tags", "Tags", '<'),
("start", "Start", '^'),
("end", "End", '^'),
("duration", "Duration", '<'),
]
.iter()
.map(|s| s.to_string())
.collect()
}

fn row_values(&self) -> Vec<String> {
Expand Down
58 changes: 48 additions & 10 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use anyhow::{Result, anyhow};
use tabular::{Row, Table};
use yansi::Paint;

Expand All @@ -7,41 +8,78 @@ pub mod boat_data;
pub mod log;
pub mod tag;

/// A single displayable column: (key used in --fields/config, header label, alignment)
pub type FieldSpec = (&'static str, &'static str, char);

pub trait TablePrintable {
fn to_printable_table(&self) -> Table;
/// Renders as a table. `fields` selects and orders columns by key (see `RowPrintable::field_specs`).
/// `None` uses every column in its default order.
fn to_printable_table(&self, fields: Option<&[String]>) -> Result<Table>;
}

pub trait RowPrintable {
fn row_spec() -> String;
fn header_names() -> Vec<String>;
/// Every available column, in default display order.
fn field_specs() -> Vec<FieldSpec>;
/// Values in the same order as `field_specs()`.
fn row_values(&self) -> Vec<String>;
fn style_cell(&self, value: String) -> String {
value
}
}

fn resolve_field_indices(specs: &[FieldSpec], fields: Option<&[String]>) -> Result<Vec<usize>> {
let Some(keys) = fields else {
return Ok((0..specs.len()).collect());
};

keys.iter()
.map(|key| {
specs
.iter()
.position(|(field_key, _, _)| field_key.eq_ignore_ascii_case(key))
.ok_or_else(|| {
let available = specs
.iter()
.map(|(k, _, _)| *k)
.collect::<Vec<_>>()
.join(", ");
anyhow!("unknown field '{key}' (available: {available})")
})
})
.collect()
}

impl<T> TablePrintable for Vec<T>
where
T: RowPrintable,
{
fn to_printable_table(&self) -> Table {
let mut table = Table::new(&T::row_spec());
fn to_printable_table(&self, fields: Option<&[String]>) -> Result<Table> {
let specs = T::field_specs();
let indices = resolve_field_indices(&specs, fields)?;

let row_spec = indices
.iter()
.map(|&i| format!("{{:{}}}", specs[i].2))
.collect::<Vec<_>>()
.join(" ");
let mut table = Table::new(&row_spec);

let mut header = Row::new();
for h in T::header_names() {
header.add_ansi_cell(Paint::new(h).underline().to_string());
for &i in &indices {
header.add_ansi_cell(Paint::new(specs[i].1).underline().to_string());
}
table.add_row(header);

for item in self.iter() {
let values = item.row_values();
let mut row = Row::new();
for value in item.row_values() {
let styled = item.style_cell(value.to_string());
for &i in &indices {
let styled = item.style_cell(values[i].clone());
row.add_ansi_cell(styled);
}
table.add_row(row);
}

table
Ok(table)
}
}
10 changes: 3 additions & 7 deletions src/models/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use boat_lib::models::tag::Tag as DatabaseTag;
use boat_lib::repository::Id;
use serde::{Deserialize, Serialize};

use crate::models::RowPrintable;
use crate::models::{FieldSpec, RowPrintable};

#[derive(Debug, Serialize, Deserialize)]
pub struct PrintableTag {
Expand All @@ -20,12 +20,8 @@ impl PrintableTag {
}

impl RowPrintable for PrintableTag {
fn row_spec() -> String {
"{:>} {:<}".to_string()
}

fn header_names() -> Vec<String> {
["ID", "Name"].iter().map(|s| s.to_string()).collect()
fn field_specs() -> Vec<FieldSpec> {
vec![("id", "ID", '>'), ("name", "Name", '<')]
}

fn row_values(&self) -> Vec<String> {
Expand Down
3 changes: 2 additions & 1 deletion src/utils/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub fn prompt_for_confirmation(msg: &str, default_value: bool) -> Result<bool> {
pub fn list_printable_items<T: RowPrintable + Serialize>(
items: &Vec<T>,
show_as_json: bool,
fields: Option<&[String]>,
) -> Result<()> {
if show_as_json {
let json = serde_json::to_string(&items)?;
Expand All @@ -45,7 +46,7 @@ pub fn list_printable_items<T: RowPrintable + Serialize>(
return Ok(());
}

let table = items.to_printable_table();
let table = items.to_printable_table(fields)?;
println!("{table}");
Ok(())
}
Expand Down
Loading