Complete Gotcha issue CLI workflow

This commit is contained in:
Georg Bauer
2026-07-31 15:59:28 +02:00
parent 0583d05170
commit b468a2670c
5 changed files with 730 additions and 153 deletions

View File

@@ -169,20 +169,30 @@ fn print_user(user: &User) {
}
fn print_repositories(repositories: &[Repository]) {
println!("REPOSITORY\tVISIBILITY\tUPDATED\tDESCRIPTION");
for repository in repositories {
println!(
"{}\t{}\t{}\t{}",
repository.full_name.as_deref().unwrap_or("unknown"),
if repository.private.unwrap_or(false) {
"private"
} else {
"public"
},
repository.updated_at.as_deref().unwrap_or("-"),
repository.description.as_deref().unwrap_or("")
);
}
print_table(
&[
("REPOSITORY", 40),
("VISIBILITY", 10),
("UPDATED", 25),
("DESCRIPTION", 60),
],
repositories
.iter()
.map(|repository| {
vec![
repository.full_name.as_deref().unwrap_or("unknown").into(),
if repository.private.unwrap_or(false) {
"private"
} else {
"public"
}
.into(),
repository.updated_at.as_deref().unwrap_or("-").into(),
repository.description.as_deref().unwrap_or("").into(),
]
})
.collect(),
);
}
fn print_repository(repository: &Repository) {
@@ -216,6 +226,93 @@ fn print_json(value: &Value) -> Result<(), serde_json::Error> {
Ok(())
}
fn print_table(columns: &[(&str, usize)], rows: Vec<Vec<String>>) {
println!("{}", format_table(columns, &rows, terminal_width()));
}
fn format_table(columns: &[(&str, usize)], rows: &[Vec<String>], width: usize) -> String {
let minimums = columns
.iter()
.map(|(header, _)| header.chars().count())
.collect::<Vec<_>>();
let mut widths = minimums.clone();
for (column, (_, maximum)) in columns.iter().enumerate() {
widths[column] = rows
.iter()
.filter_map(|row| row.get(column))
.map(|value| value.chars().count())
.max()
.unwrap_or_default()
.max(widths[column])
.min(*maximum);
}
let separator_width = columns.len().saturating_sub(1) * 2;
while widths.iter().sum::<usize>() + separator_width > width {
let Some(column) = widths
.iter()
.zip(&minimums)
.enumerate()
.filter(|(_, (current, minimum))| current > minimum)
.max_by_key(|(_, (current, minimum))| *current - *minimum)
.map(|(column, _)| column)
else {
break;
};
widths[column] -= 1;
}
let mut lines = Vec::with_capacity(rows.len() + 2);
lines.push(table_row(
&columns
.iter()
.map(|(header, _)| (*header).to_owned())
.collect::<Vec<_>>(),
&widths,
));
lines.push(
widths
.iter()
.map(|width| "-".repeat(*width))
.collect::<Vec<_>>()
.join(" "),
);
lines.extend(rows.iter().map(|row| table_row(row, &widths)));
lines.join("\n")
}
fn table_row(values: &[String], widths: &[usize]) -> String {
values
.iter()
.zip(widths)
.map(|(value, width)| table_cell(value, *width))
.collect::<Vec<_>>()
.join(" ")
.trim_end()
.to_owned()
}
fn table_cell(value: &str, width: usize) -> String {
let value = value.lines().next().unwrap_or_default();
let length = value.chars().count();
if length <= width {
return format!("{value}{}", " ".repeat(width - length));
}
let mut shortened = value
.chars()
.take(width.saturating_sub(1))
.collect::<String>();
shortened.push('…');
shortened
}
fn terminal_width() -> usize {
env::var("COLUMNS")
.ok()
.and_then(|value| value.parse().ok())
.filter(|width| *width >= 40)
.unwrap_or(100)
}
fn requested_help(command: &[String]) -> Result<Option<&'static str>, String> {
match command {
[] => Ok(Some(ROOT_HELP)),
@@ -393,4 +490,23 @@ mod tests {
.starts_with("Usage: gotcha repo show")
);
}
#[test]
fn formats_bounded_aligned_tables_without_tabs() {
let table = format_table(
&[("INDEX", 8), ("STATE", 10), ("TITLE", 60)],
&[vec![
"22".into(),
"open".into(),
"A deliberately long issue title that must be shortened".into(),
]],
40,
);
assert!(!table.contains('\t'));
assert!(table.contains('…'));
assert_eq!(table.lines().count(), 3);
assert!(table.lines().all(|line| line.chars().count() <= 40));
assert!(table.lines().next().unwrap().starts_with("INDEX STATE"));
}
}