use crate::config::Config;
use crate::error::Result;
use crate::storage::Store;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BoardSummary {
pub columns: Vec<(String, usize)>,
pub total: usize,
}
pub fn run(config: &Config, store: &Store) -> Result<()> {
let summary = board(config, store)?;
let column_width = config
.default_config()
.columns
.iter()
.map(|c| c.chars().count())
.chain([6, 5])
.max()
.unwrap()
.min(30);
let count_width = summary.total.to_string().len().max(5).max(1);
let row =
|column: String, count: String| format!("{column:<column_width$} {count:<count_width$}");
println!("{}", row("COLUMN".into(), "COUNT".into()));
for (column, count) in &summary.columns {
println!("{}", row(column.clone(), count.to_string()));
}
println!("{}", row("Total".into(), summary.total.to_string()));
Ok(())
}
pub fn board(config: &Config, store: &Store) -> Result<BoardSummary> {
let issues = store.list()?;
let total = issues.len();
let columns = config
.default_config()
.columns
.iter()
.map(|column| {
let count = issues.iter().filter(|i| i.status.is(column)).count();
(column.clone(), count)
})
.collect();
Ok(BoardSummary { columns, total })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Issue, Priority, Status};
use std::fs;
use tempfile::TempDir;
fn make_store() -> (TempDir, Store) {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("issues");
fs::create_dir_all(&dir).unwrap();
let config_path = tmp.path().join("tix.toml");
let config = crate::config::Config {
issues_dir: dir.clone(),
..crate::config::Config::default()
};
config.save(&config_path).unwrap();
let config = crate::config::Config::load_with_warning(&config_path, |_| {}).unwrap();
(
tmp,
Store {
issues_dir: dir,
config,
},
)
}
fn seed(store: &Store, number: u32, title: &str, status: &str) {
let mut issue = Issue {
number,
title: title.to_string(),
status: Status::new(status.to_string()),
labels: vec![],
priority: Priority::default(),
assignee: Some("alice".to_string()),
estimate: None,
commit: None,
gitlab_id: None,
linear_id: None,
mission: None,
task: None,
created: "2026-01-01".to_string(),
updated: "2026-01-01".to_string(),
filename: String::new(),
body: "body".to_string(),
};
issue.filename = format!("alice/2026-01-01-{}.md", crate::slug::slugify(title));
fs::create_dir_all(store.issues_dir.join("alice")).unwrap();
fs::write(
store.issues_dir.join(&issue.filename),
issue.to_file_contents(),
)
.unwrap();
}
#[test]
fn board_counts_columns_and_total() {
let (tmp, store) = make_store();
let _ = tmp;
seed(&store, 1, "One", "todo");
seed(&store, 2, "Two", "todo");
seed(&store, 3, "Three", "in progress");
seed(&store, 4, "Four", "done");
let summary = board(&store.config, &store).unwrap();
assert_eq!(
summary
.columns
.iter()
.map(|(c, n)| (c.as_str(), *n))
.collect::<Vec<_>>(),
vec![
("todo", 2),
("in progress", 1),
("in review", 0),
("done", 1),
],
"columns in config order with per-column counts"
);
assert_eq!(summary.total, 4);
}
}