use crate::error::Result;
use crate::model::Issue;
use crate::storage::Store;
pub fn run(store: &Store, status: Option<&str>) -> Result<()> {
let issues = query(store, status)?;
let num_width = 6usize;
let title_width = issues
.iter()
.map(|i| i.title.chars().count())
.chain([5])
.max()
.unwrap()
.min(40);
let status_width = issues
.iter()
.map(|i| i.status.display().chars().count())
.chain([6])
.max()
.unwrap()
.min(16);
let priority_width = issues
.iter()
.map(|i| i.priority.to_string().chars().count())
.chain([8])
.max()
.unwrap()
.min(12);
let row = |number: String, title: String, status: String, priority: String| {
format!(
"{number:<num_width$} {title:<title_width$} {status:<status_width$} {priority:<priority_width$}"
)
};
println!(
"{}",
row(
"NUM".into(),
"TITLE".into(),
"STATUS".into(),
"PRIORITY".into()
)
);
for issue in &issues {
println!(
"{}",
row(
issue.number.to_string(),
clip(&issue.title, title_width),
clip(issue.status.display(), status_width),
clip(&issue.priority.to_string(), priority_width)
)
);
}
Ok(())
}
pub fn query(store: &Store, status: Option<&str>) -> Result<Vec<Issue>> {
let mut issues = store.list()?;
if let Some(col) = status {
issues.retain(|i| i.status.is(col));
}
Ok(issues)
}
fn clip(value: &str, width: usize) -> String {
value.chars().take(width).collect()
}
#[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) -> Issue {
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,
links: vec![],
files: vec![],
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();
issue
}
#[test]
fn query_without_filter_returns_every_issue() {
let (tmp, store) = make_store();
let _ = tmp;
seed(&store, 1, "First", "todo");
seed(&store, 2, "Second", "done");
let issues = query(&store, None).unwrap();
assert_eq!(issues.len(), 2);
assert_eq!(issues[0].number, 1);
assert_eq!(issues[1].number, 2);
}
#[test]
fn query_filters_by_status_exactly_as_the_cli_does() {
let (tmp, store) = make_store();
let _ = tmp;
seed(&store, 1, "Todo one", "todo");
seed(&store, 2, "Todo two", "todo");
seed(&store, 3, "Done one", "done");
let todos = query(&store, Some("todo")).unwrap();
assert_eq!(
todos.iter().map(|i| i.number).collect::<Vec<_>>(),
vec![1, 2]
);
let done = query(&store, Some("done")).unwrap();
assert_eq!(done.len(), 1);
assert_eq!(done[0].number, 3);
let none = query(&store, Some("in review")).unwrap();
assert!(none.is_empty(), "no in-review issues seeded");
}
}