Skip to content

Commit 377b271

Browse files
committed
feat(config): add field selection for table display
- Add --fields flag and config support for column selection. Replace row_spec/header_names with field_specs trait method with alignment - Add resolve_field_indices mapping user selections to column indices for reordering and filtering
1 parent 4f08576 commit 377b271

9 files changed

Lines changed: 107 additions & 48 deletions

File tree

src/cli/args.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ pub struct FilterActivitiesArgs {
168168
#[arg(short = 't', long = "filter-by-tags", value_name = "TAGS", value_delimiter = ',', action = ArgAction::Append)]
169169
pub filter_by_tags: Option<Vec<String>>,
170170

171+
/// Select and order which fields are shown in the table (comma-separated)
172+
#[arg(short = 'f', long = "fields", value_name = "FIELDS", value_delimiter = ',', action = ArgAction::Append)]
173+
pub fields: Option<Vec<String>>,
174+
171175
/// Output in JSON format
172176
#[arg(short = 'j', long = "json")]
173177
pub use_json_format: bool,

src/commands/list.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ pub fn list_activity_logs(
3030
.unwrap_or(GroupBy::None);
3131
info!("grouping by: {group_by_value}");
3232

33+
let fields = args
34+
.fields
35+
.clone()
36+
.or_else(|| config.commands.list.fields.clone());
37+
3338
info!("getting all activities");
3439
let db_acts: Vec<_> = activities::get_all(conn)?;
3540
let boat_data = BoatData::create_filtered_data(db_acts, period);
@@ -67,7 +72,7 @@ pub fn list_activity_logs(
6772
let (text, tooltip) = utils::display::get_group_by_display_values(group_by_value, group)?;
6873
let ribbon = utils::display::format_ascii_ribbon(&text, tooltip.as_deref());
6974
println!("{ribbon}");
70-
utils::common::list_printable_items(act_logs, false)?;
75+
utils::common::list_printable_items(act_logs, false, fields.as_deref())?;
7176
}
7277

7378
Ok(())

src/commands/report.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,24 @@ pub fn show_report(
6262
// }
6363
}
6464

65-
list_activity_summaries(&boat_data, args.use_json_format, &args.filter_by_tags)
65+
let fields = args
66+
.fields
67+
.clone()
68+
.or_else(|| config.commands.report.fields.clone());
69+
70+
list_activity_summaries(
71+
&boat_data,
72+
args.use_json_format,
73+
&args.filter_by_tags,
74+
fields.as_deref(),
75+
)
6676
}
6777

6878
fn list_activity_summaries(
6979
boat_data: &BoatData,
7080
use_json: bool,
7181
filter_by_tags: &Option<Vec<String>>,
82+
fields: Option<&[String]>,
7283
) -> Result<()> {
7384
info!("filtering logs by tags");
7485
let prt_acts = boat_data
@@ -85,7 +96,7 @@ fn list_activity_summaries(
8596
.collect();
8697

8798
info!("listing activity summaries");
88-
utils::common::list_printable_items(&prt_acts, use_json)?;
99+
utils::common::list_printable_items(&prt_acts, use_json, fields)?;
89100

90101
if !use_json && !prt_acts.is_empty() {
91102
let total_sec: i64 = prt_acts.iter().map(|pa| pa.duration).sum();

src/config.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ pub struct ListCommandConfig {
130130
#[serde(rename = "group_by")]
131131
#[serde(skip_serializing_if = "Option::is_none")]
132132
pub group_by: Option<GroupBy>,
133+
134+
/// Select and order which fields are shown in the table
135+
#[serde(rename = "fields")]
136+
#[serde(skip_serializing_if = "Option::is_none")]
137+
pub fields: Option<Vec<String>>,
133138
// /// Specify how entries should be sorted
134139
// #[serde(rename = "sort_by")]
135140
// pub sort_by: Option<String>,
@@ -144,6 +149,11 @@ pub struct ReportCommandConfig {
144149
#[serde(rename = "period")]
145150
#[serde(skip_serializing_if = "Option::is_none")]
146151
pub period: Option<PeriodInput>,
152+
153+
/// Select and order which fields are shown in the table
154+
#[serde(rename = "fields")]
155+
#[serde(skip_serializing_if = "Option::is_none")]
156+
pub fields: Option<Vec<String>>,
147157
// /// Specify how entries should be grouped
148158
// #[serde(rename = "group_by")]
149159
// #[serde(skip_serializing_if = "Option::is_none")]
@@ -184,11 +194,13 @@ impl Configuration {
184194
list: ListCommandConfig {
185195
period: None,
186196
group_by: None,
197+
fields: None,
187198
// sort_by: None,
188199
// format: None,
189200
},
190201
report: ReportCommandConfig {
191202
period: None,
203+
fields: None,
192204
// group_by: None,
193205
// sort_by: None,
194206
// format: None,

src/models/activity.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use chrono::Utc;
44
use serde::{Deserialize, Serialize};
55
use std::collections::HashSet;
66

7-
use crate::models::RowPrintable;
7+
use crate::models::{FieldSpec, RowPrintable};
88
use crate::utils;
99

1010
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -58,15 +58,14 @@ impl PrintableActivity {
5858
}
5959

6060
impl RowPrintable for PrintableActivity {
61-
fn row_spec() -> String {
62-
"{:>} {:<} {:<} {:<} {:<}".to_string()
63-
}
64-
65-
fn header_names() -> Vec<String> {
66-
["ID", "Name", "Description", "Tags", "Duration"]
67-
.iter()
68-
.map(|s| s.to_string())
69-
.collect()
61+
fn field_specs() -> Vec<FieldSpec> {
62+
vec![
63+
("id", "ID", '>'),
64+
("name", "Name", '<'),
65+
("description", "Description", '<'),
66+
("tags", "Tags", '<'),
67+
("duration", "Duration", '<'),
68+
]
7069
}
7170

7271
fn row_values(&self) -> Vec<String> {

src/models/activity_log.rs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
33
use yansi::Paint;
44

55
use crate::{
6-
models::{RowPrintable, activity::SimpleActivity, log::PrintableLog},
6+
models::{FieldSpec, RowPrintable, activity::SimpleActivity, log::PrintableLog},
77
utils::{self, date::DateTimeRenderMode},
88
};
99

@@ -23,23 +23,16 @@ impl PrintableActivityLog {
2323
}
2424

2525
impl RowPrintable for PrintableActivityLog {
26-
fn row_spec() -> String {
27-
" {:>} {:<} {:<} {:<} {:^} {:^} {:<}".to_string()
28-
}
29-
30-
fn header_names() -> Vec<String> {
31-
[
32-
"ID",
33-
"Name",
34-
"Description",
35-
"Tags",
36-
"Start",
37-
"End",
38-
"Duration",
26+
fn field_specs() -> Vec<FieldSpec> {
27+
vec![
28+
("id", "ID", '>'),
29+
("name", "Name", '<'),
30+
("description", "Description", '<'),
31+
("tags", "Tags", '<'),
32+
("start", "Start", '^'),
33+
("end", "End", '^'),
34+
("duration", "Duration", '<'),
3935
]
40-
.iter()
41-
.map(|s| s.to_string())
42-
.collect()
4336
}
4437

4538
fn row_values(&self) -> Vec<String> {

src/models/mod.rs

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use anyhow::{Result, anyhow};
12
use tabular::{Row, Table};
23
use yansi::Paint;
34

@@ -7,41 +8,78 @@ pub mod boat_data;
78
pub mod log;
89
pub mod tag;
910

11+
/// A single displayable column: (key used in --fields/config, header label, alignment)
12+
pub type FieldSpec = (&'static str, &'static str, char);
13+
1014
pub trait TablePrintable {
11-
fn to_printable_table(&self) -> Table;
15+
/// Renders as a table. `fields` selects and orders columns by key (see `RowPrintable::field_specs`).
16+
/// `None` uses every column in its default order.
17+
fn to_printable_table(&self, fields: Option<&[String]>) -> Result<Table>;
1218
}
1319

1420
pub trait RowPrintable {
15-
fn row_spec() -> String;
16-
fn header_names() -> Vec<String>;
21+
/// Every available column, in default display order.
22+
fn field_specs() -> Vec<FieldSpec>;
23+
/// Values in the same order as `field_specs()`.
1724
fn row_values(&self) -> Vec<String>;
1825
fn style_cell(&self, value: String) -> String {
1926
value
2027
}
2128
}
2229

30+
fn resolve_field_indices(specs: &[FieldSpec], fields: Option<&[String]>) -> Result<Vec<usize>> {
31+
let Some(keys) = fields else {
32+
return Ok((0..specs.len()).collect());
33+
};
34+
35+
keys.iter()
36+
.map(|key| {
37+
specs
38+
.iter()
39+
.position(|(field_key, _, _)| field_key.eq_ignore_ascii_case(key))
40+
.ok_or_else(|| {
41+
let available = specs
42+
.iter()
43+
.map(|(k, _, _)| *k)
44+
.collect::<Vec<_>>()
45+
.join(", ");
46+
anyhow!("unknown field '{key}' (available: {available})")
47+
})
48+
})
49+
.collect()
50+
}
51+
2352
impl<T> TablePrintable for Vec<T>
2453
where
2554
T: RowPrintable,
2655
{
27-
fn to_printable_table(&self) -> Table {
28-
let mut table = Table::new(&T::row_spec());
56+
fn to_printable_table(&self, fields: Option<&[String]>) -> Result<Table> {
57+
let specs = T::field_specs();
58+
let indices = resolve_field_indices(&specs, fields)?;
59+
60+
let row_spec = indices
61+
.iter()
62+
.map(|&i| format!("{{:{}}}", specs[i].2))
63+
.collect::<Vec<_>>()
64+
.join(" ");
65+
let mut table = Table::new(&row_spec);
2966

3067
let mut header = Row::new();
31-
for h in T::header_names() {
32-
header.add_ansi_cell(Paint::new(h).underline().to_string());
68+
for &i in &indices {
69+
header.add_ansi_cell(Paint::new(specs[i].1).underline().to_string());
3370
}
3471
table.add_row(header);
3572

3673
for item in self.iter() {
74+
let values = item.row_values();
3775
let mut row = Row::new();
38-
for value in item.row_values() {
39-
let styled = item.style_cell(value.to_string());
76+
for &i in &indices {
77+
let styled = item.style_cell(values[i].clone());
4078
row.add_ansi_cell(styled);
4179
}
4280
table.add_row(row);
4381
}
4482

45-
table
83+
Ok(table)
4684
}
4785
}

src/models/tag.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use boat_lib::models::tag::Tag as DatabaseTag;
22
use boat_lib::repository::Id;
33
use serde::{Deserialize, Serialize};
44

5-
use crate::models::RowPrintable;
5+
use crate::models::{FieldSpec, RowPrintable};
66

77
#[derive(Debug, Serialize, Deserialize)]
88
pub struct PrintableTag {
@@ -20,12 +20,8 @@ impl PrintableTag {
2020
}
2121

2222
impl RowPrintable for PrintableTag {
23-
fn row_spec() -> String {
24-
"{:>} {:<}".to_string()
25-
}
26-
27-
fn header_names() -> Vec<String> {
28-
["ID", "Name"].iter().map(|s| s.to_string()).collect()
23+
fn field_specs() -> Vec<FieldSpec> {
24+
vec![("id", "ID", '>'), ("name", "Name", '<')]
2925
}
3026

3127
fn row_values(&self) -> Vec<String> {

src/utils/common.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub fn prompt_for_confirmation(msg: &str, default_value: bool) -> Result<bool> {
3333
pub fn list_printable_items<T: RowPrintable + Serialize>(
3434
items: &Vec<T>,
3535
show_as_json: bool,
36+
fields: Option<&[String]>,
3637
) -> Result<()> {
3738
if show_as_json {
3839
let json = serde_json::to_string(&items)?;
@@ -45,7 +46,7 @@ pub fn list_printable_items<T: RowPrintable + Serialize>(
4546
return Ok(());
4647
}
4748

48-
let table = items.to_printable_table();
49+
let table = items.to_printable_table(fields)?;
4950
println!("{table}");
5051
Ok(())
5152
}

0 commit comments

Comments
 (0)