refactor(list): polish list commands with icons, compact timestamps, and styled discussions

Phase 3 of the UX overhaul. Enhances the issues, merge requests, and
notes list displays with visual indicators and improved formatting.

List display changes (src/cli/commands/list.rs):
- Add state icons to issues (opened/closed) and merge requests
  (opened/merged/closed) using Icons:: helpers alongside text labels
- Replace [DRAFT] prefix with Icons::mr_draft() glyph for draft MRs
- Switch from format_relative_time to format_relative_time_compact for
  tighter column widths in tabular output
- Switch from format_labels to format_labels_bare for unlabeled style
- Change format_discussions() return type from String to StyledCell so
  unresolved counts render with Theme::warning() color inline
- Bold the section headers ("Issues", "Merge Requests", "Notes")
  with count separated from the label for cleaner scanning
- Import Icons from render module

Test updates (src/cli/commands/list_tests.rs):
- Update format_discussions tests to assert on StyledCell.text field
  instead of raw String, since the function now returns styled output
- The unresolved-count test checks starts_with/contains to handle
  embedded ANSI escape codes from Theme::warning()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-14 10:01:07 -05:00
committed by teernisse
parent af8fc4af76
commit 4b372dfb38
2 changed files with 37 additions and 27 deletions

View File

@@ -1,4 +1,4 @@
use crate::cli::render::{self, Align, StyledCell, Table as LoreTable, Theme};
use crate::cli::render::{self, Align, Icons, StyledCell, Table as LoreTable, Theme};
use rusqlite::Connection;
use serde::Serialize;
@@ -657,15 +657,17 @@ fn format_assignees(assignees: &[String]) -> String {
}
}
fn format_discussions(total: i64, unresolved: i64) -> String {
fn format_discussions(total: i64, unresolved: i64) -> StyledCell {
if total == 0 {
return String::new();
return StyledCell::plain(String::new());
}
if unresolved > 0 {
format!("{total}/{unresolved}!")
let text = format!("{total}/");
let warn = Theme::warning().render(&format!("{unresolved}!"));
StyledCell::plain(format!("{text}{warn}"))
} else {
format!("{total}")
StyledCell::plain(format!("{total}"))
}
}
@@ -681,7 +683,8 @@ pub fn print_list_issues(result: &ListResult) {
}
println!(
"Issues (showing {} of {})\n",
"{} {} of {}\n",
Theme::bold().render("Issues"),
result.issues.len(),
result.total_count
);
@@ -698,16 +701,17 @@ pub fn print_list_issues(result: &ListResult) {
for issue in &result.issues {
let title = render::truncate(&issue.title, 45);
let relative_time = render::format_relative_time(issue.updated_at);
let labels = render::format_labels(&issue.labels, 2);
let relative_time = render::format_relative_time_compact(issue.updated_at);
let labels = render::format_labels_bare(&issue.labels, 2);
let assignee = format_assignees(&issue.assignees);
let discussions = format_discussions(issue.discussion_count, issue.unresolved_count);
let state_cell = if issue.state == "opened" {
StyledCell::styled(&issue.state, Theme::success())
let (icon, state_style) = if issue.state == "opened" {
(Icons::issue_opened(), Theme::success())
} else {
StyledCell::styled(&issue.state, Theme::dim())
(Icons::issue_closed(), Theme::dim())
};
let state_cell = StyledCell::styled(format!("{icon} {}", issue.state), state_style);
let mut row = vec![
StyledCell::styled(format!("#{}", issue.iid), Theme::info()),
@@ -730,7 +734,7 @@ pub fn print_list_issues(result: &ListResult) {
row.extend([
StyledCell::styled(assignee, Theme::accent()),
StyledCell::styled(labels, Theme::warning()),
StyledCell::plain(discussions),
discussions,
StyledCell::styled(relative_time, Theme::dim()),
]);
table.add_row(row);
@@ -783,7 +787,8 @@ pub fn print_list_mrs(result: &MrListResult) {
}
println!(
"Merge Requests (showing {} of {})\n",
"{} {} of {}\n",
Theme::bold().render("Merge Requests"),
result.mrs.len(),
result.total_count
);
@@ -796,22 +801,23 @@ pub fn print_list_mrs(result: &MrListResult) {
for mr in &result.mrs {
let title = if mr.draft {
format!("[DRAFT] {}", render::truncate(&mr.title, 38))
format!("{} {}", Icons::mr_draft(), render::truncate(&mr.title, 42))
} else {
render::truncate(&mr.title, 45)
};
let relative_time = render::format_relative_time(mr.updated_at);
let relative_time = render::format_relative_time_compact(mr.updated_at);
let branches = format_branches(&mr.target_branch, &mr.source_branch, 25);
let discussions = format_discussions(mr.discussion_count, mr.unresolved_count);
let state_cell = match mr.state.as_str() {
"opened" => StyledCell::styled(&mr.state, Theme::success()),
"merged" => StyledCell::styled(&mr.state, Theme::accent()),
"closed" => StyledCell::styled(&mr.state, Theme::error()),
"locked" => StyledCell::styled(&mr.state, Theme::warning()),
_ => StyledCell::styled(&mr.state, Theme::dim()),
let (icon, style) = match mr.state.as_str() {
"opened" => (Icons::mr_opened(), Theme::success()),
"merged" => (Icons::mr_merged(), Theme::accent()),
"closed" => (Icons::mr_closed(), Theme::error()),
"locked" => (Icons::mr_opened(), Theme::warning()),
_ => (Icons::mr_opened(), Theme::dim()),
};
let state_cell = StyledCell::styled(format!("{icon} {}", mr.state), style);
table.add_row(vec![
StyledCell::styled(format!("!{}", mr.iid), Theme::info()),
@@ -822,7 +828,7 @@ pub fn print_list_mrs(result: &MrListResult) {
Theme::accent(),
),
StyledCell::styled(branches, Theme::info()),
StyledCell::plain(discussions),
discussions,
StyledCell::styled(relative_time, Theme::dim()),
]);
}
@@ -909,7 +915,8 @@ pub fn print_list_notes(result: &NoteListResult) {
}
println!(
"Notes (showing {} of {})\n",
"{} {} of {}\n",
Theme::bold().render("Notes"),
result.notes.len(),
result.total_count
);
@@ -934,7 +941,7 @@ pub fn print_list_notes(result: &NoteListResult) {
.unwrap_or_default();
let path = format_note_path(note.position_new_path.as_deref(), note.position_new_line);
let parent = format_note_parent(note.noteable_type.as_deref(), note.parent_iid);
let relative_time = render::format_relative_time(note.created_at);
let relative_time = render::format_relative_time_compact(note.created_at);
let note_type = format_note_type(note.note_type.as_deref());
table.add_row(vec![