fix(explain): align human output with render module conventions
The explain command's human-mode output was hand-rolled with raw println! formatting that didn't use any of the shared render.rs infrastructure. This made it visually inconsistent with every other command (me, who, search, timeline). Changes to print_explain(): - Section headers now use render::section_divider() with counts, producing the same box-drawing divider lines as the me command - Entity refs use Theme::issue_ref()/mr_ref() color styling - Entity state uses Theme::state_opened/closed/merged() styling - Authors/usernames use Theme::username() with @ prefix - Project paths use Theme::muted() - Timestamps use format_relative_time() for recency fields (created, first/last event, last note) and format_date() for point-in-time fields (key decisions, timeline events), matching the conventions in me, who, and timeline respectively - Note excerpts use render::truncate() instead of manual byte slicing - Related entity titles are truncated via render::truncate() - Indentation aligned to 4-space content under section dividers Robot JSON output is unchanged -- it continues to use ms_to_iso() for all timestamp fields, consistent with the rest of the robot API.
This commit is contained in:
@@ -3,7 +3,7 @@ use serde::Serialize;
|
||||
|
||||
use crate::core::error::{LoreError, Result};
|
||||
use crate::core::project::resolve_project;
|
||||
use crate::core::time::ms_to_iso;
|
||||
use crate::core::time::{iso_to_ms, ms_to_iso};
|
||||
use crate::timeline::collect::collect_events;
|
||||
use crate::timeline::seed::seed_timeline_direct;
|
||||
|
||||
@@ -1127,41 +1127,71 @@ pub fn print_explain_json(result: &ExplainResult, elapsed_ms: u64) -> Result<()>
|
||||
}
|
||||
|
||||
pub fn print_explain(result: &ExplainResult) {
|
||||
use crate::cli::render::{Icons, Theme};
|
||||
use crate::cli::render::{self, Icons, Theme};
|
||||
|
||||
let to_relative = |iso: &str| -> String {
|
||||
iso_to_ms(iso)
|
||||
.map(render::format_relative_time)
|
||||
.unwrap_or_else(|| iso.to_string())
|
||||
};
|
||||
let to_date = |iso: &str| -> String {
|
||||
iso_to_ms(iso)
|
||||
.map(render::format_date)
|
||||
.unwrap_or_else(|| iso.to_string())
|
||||
};
|
||||
|
||||
// Entity header
|
||||
let type_label = match result.entity.entity_type.as_str() {
|
||||
"issue" => "Issue",
|
||||
"merge_request" => "MR",
|
||||
_ => &result.entity.entity_type,
|
||||
let (type_label, ref_style, ref_str) = match result.entity.entity_type.as_str() {
|
||||
"issue" => ("Issue", Theme::issue_ref(), format!("#{}", result.entity.iid)),
|
||||
"merge_request" => ("MR", Theme::mr_ref(), format!("!{}", result.entity.iid)),
|
||||
_ => (
|
||||
result.entity.entity_type.as_str(),
|
||||
Theme::info(),
|
||||
format!("#{}", result.entity.iid),
|
||||
),
|
||||
};
|
||||
let state_style = match result.entity.state.as_str() {
|
||||
"opened" => Theme::state_opened(),
|
||||
"closed" => Theme::state_closed(),
|
||||
"merged" => Theme::state_merged(),
|
||||
_ => Theme::dim(),
|
||||
};
|
||||
println!(
|
||||
"{} {} #{} — {}",
|
||||
"{} {} {} — {}",
|
||||
Icons::info(),
|
||||
Theme::bold().render(type_label),
|
||||
result.entity.iid,
|
||||
ref_style.render(&ref_str),
|
||||
Theme::bold().render(&result.entity.title)
|
||||
);
|
||||
println!(
|
||||
" Project: {} State: {} Author: {} Created: {}",
|
||||
result.entity.project_path,
|
||||
result.entity.state,
|
||||
result.entity.author,
|
||||
result.entity.created_at
|
||||
" {} {} {} {}",
|
||||
Theme::muted().render(&result.entity.project_path),
|
||||
state_style.render(&result.entity.state),
|
||||
Theme::username().render(&format!("@{}", result.entity.author)),
|
||||
Theme::dim().render(&to_relative(&result.entity.created_at)),
|
||||
);
|
||||
if !result.entity.assignees.is_empty() {
|
||||
println!(" Assignees: {}", result.entity.assignees.join(", "));
|
||||
let styled: Vec<String> = result
|
||||
.entity
|
||||
.assignees
|
||||
.iter()
|
||||
.map(|a| Theme::username().render(&format!("@{a}")))
|
||||
.collect();
|
||||
println!(" Assignees: {}", styled.join(", "));
|
||||
}
|
||||
if !result.entity.labels.is_empty() {
|
||||
println!(" Labels: {}", result.entity.labels.join(", "));
|
||||
println!(
|
||||
" Labels: {}",
|
||||
Theme::dim().render(&result.entity.labels.join(", "))
|
||||
);
|
||||
}
|
||||
if let Some(ref url) = result.entity.url {
|
||||
println!(" URL: {url}");
|
||||
println!(" {}", Theme::dim().render(url));
|
||||
}
|
||||
|
||||
// Description
|
||||
if let Some(ref desc) = result.description_excerpt {
|
||||
println!("\n{}", Theme::bold().render("Description"));
|
||||
println!("{}", render::section_divider("Description"));
|
||||
for line in desc.lines() {
|
||||
println!(" {line}");
|
||||
}
|
||||
@@ -1172,15 +1202,14 @@ pub fn print_explain(result: &ExplainResult) {
|
||||
&& !decisions.is_empty()
|
||||
{
|
||||
println!(
|
||||
"\n{} {}",
|
||||
Icons::info(),
|
||||
Theme::bold().render("Key Decisions")
|
||||
"{}",
|
||||
render::section_divider(&format!("Key Decisions ({})", decisions.len()))
|
||||
);
|
||||
for d in decisions {
|
||||
println!(
|
||||
" {} {} — {}",
|
||||
Theme::muted().render(&d.timestamp),
|
||||
Theme::bold().render(&d.actor),
|
||||
Theme::muted().render(&to_date(&d.timestamp)),
|
||||
Theme::username().render(&format!("@{}", d.actor)),
|
||||
d.action,
|
||||
);
|
||||
for line in d.context_note.lines() {
|
||||
@@ -1191,16 +1220,22 @@ pub fn print_explain(result: &ExplainResult) {
|
||||
|
||||
// Activity
|
||||
if let Some(ref act) = result.activity {
|
||||
println!("\n{}", Theme::bold().render("Activity"));
|
||||
println!("{}", render::section_divider("Activity"));
|
||||
println!(
|
||||
" {} state changes, {} label changes, {} notes",
|
||||
act.state_changes, act.label_changes, act.notes
|
||||
);
|
||||
if let Some(ref first) = act.first_event {
|
||||
println!(" First event: {first}");
|
||||
println!(
|
||||
" First event: {}",
|
||||
Theme::dim().render(&to_relative(first))
|
||||
);
|
||||
}
|
||||
if let Some(ref last) = act.last_event {
|
||||
println!(" Last event: {last}");
|
||||
println!(
|
||||
" Last event: {}",
|
||||
Theme::dim().render(&to_relative(last))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1209,26 +1244,20 @@ pub fn print_explain(result: &ExplainResult) {
|
||||
&& !threads.is_empty()
|
||||
{
|
||||
println!(
|
||||
"\n{} {} ({})",
|
||||
Icons::warning(),
|
||||
Theme::bold().render("Open Threads"),
|
||||
threads.len()
|
||||
"{}",
|
||||
render::section_divider(&format!("Open Threads ({})", threads.len()))
|
||||
);
|
||||
for t in threads {
|
||||
println!(
|
||||
" {} by {} ({} notes, last: {})",
|
||||
t.discussion_id,
|
||||
t.started_by.as_deref().unwrap_or("unknown"),
|
||||
Theme::dim().render(&t.discussion_id),
|
||||
Theme::username()
|
||||
.render(&format!("@{}", t.started_by.as_deref().unwrap_or("unknown"))),
|
||||
t.note_count,
|
||||
t.last_note_at
|
||||
Theme::dim().render(&to_relative(&t.last_note_at))
|
||||
);
|
||||
if let Some(ref excerpt) = t.first_note_excerpt {
|
||||
let preview = if excerpt.len() > 100 {
|
||||
let b = excerpt.floor_char_boundary(100);
|
||||
format!("{}...", &excerpt[..b])
|
||||
} else {
|
||||
excerpt.clone()
|
||||
};
|
||||
let preview = render::truncate(excerpt, 100);
|
||||
// Show first line only in human output
|
||||
if let Some(line) = preview.lines().next() {
|
||||
println!(" {}", Theme::muted().render(line));
|
||||
@@ -1241,14 +1270,21 @@ pub fn print_explain(result: &ExplainResult) {
|
||||
if let Some(ref related) = result.related
|
||||
&& (!related.closing_mrs.is_empty() || !related.related_issues.is_empty())
|
||||
{
|
||||
println!("\n{}", Theme::bold().render("Related"));
|
||||
let total = related.closing_mrs.len() + related.related_issues.len();
|
||||
println!("{}", render::section_divider(&format!("Related ({total})")));
|
||||
for mr in &related.closing_mrs {
|
||||
let mr_state = match mr.state.as_str() {
|
||||
"merged" => Theme::state_merged(),
|
||||
"closed" => Theme::state_closed(),
|
||||
"opened" => Theme::state_opened(),
|
||||
_ => Theme::dim(),
|
||||
};
|
||||
println!(
|
||||
" {} MR !{} — {} [{}]",
|
||||
" {} {} — {} {}",
|
||||
Icons::success(),
|
||||
mr.iid,
|
||||
mr.title,
|
||||
mr.state
|
||||
Theme::mr_ref().render(&format!("!{}", mr.iid)),
|
||||
render::truncate(&mr.title, 60),
|
||||
mr_state.render(&format!("[{}]", mr.state))
|
||||
);
|
||||
}
|
||||
for ri in &related.related_issues {
|
||||
@@ -1261,13 +1297,16 @@ pub fn print_explain(result: &ExplainResult) {
|
||||
} else {
|
||||
"->"
|
||||
};
|
||||
let (ref_style, ref_prefix) = match ri.entity_type.as_str() {
|
||||
"issue" => (Theme::issue_ref(), "#"),
|
||||
"merge_request" => (Theme::mr_ref(), "!"),
|
||||
_ => (Theme::info(), "#"),
|
||||
};
|
||||
println!(
|
||||
" {} {arrow} {} #{} — {}{state_str} ({})",
|
||||
Icons::info(),
|
||||
ri.entity_type,
|
||||
ri.iid,
|
||||
ri.title.as_deref().unwrap_or("(untitled)"),
|
||||
ri.reference_type
|
||||
" {arrow} {} {}{state_str} ({})",
|
||||
ref_style.render(&format!("{ref_prefix}{}", ri.iid)),
|
||||
render::truncate(ri.title.as_deref().unwrap_or("(untitled)"), 50),
|
||||
Theme::dim().render(&ri.reference_type)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1276,26 +1315,25 @@ pub fn print_explain(result: &ExplainResult) {
|
||||
if let Some(ref excerpt) = result.timeline_excerpt
|
||||
&& !excerpt.events.is_empty()
|
||||
{
|
||||
let truncation_note = if excerpt.truncated {
|
||||
let title = if excerpt.truncated {
|
||||
format!(
|
||||
" (showing {} of {})",
|
||||
"Timeline (showing {} of {})",
|
||||
excerpt.events.len(),
|
||||
excerpt.total_events
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
format!("Timeline ({})", excerpt.total_events)
|
||||
};
|
||||
println!(
|
||||
"\n{} {}{}",
|
||||
Icons::info(),
|
||||
Theme::bold().render("Timeline"),
|
||||
truncation_note
|
||||
);
|
||||
println!("{}", render::section_divider(&title));
|
||||
for e in &excerpt.events {
|
||||
let actor_str = e.actor.as_deref().unwrap_or("");
|
||||
let actor_str = e
|
||||
.actor
|
||||
.as_deref()
|
||||
.map(|a| Theme::username().render(&format!("@{a}")))
|
||||
.unwrap_or_default();
|
||||
println!(
|
||||
" {} {} {} {}",
|
||||
Theme::muted().render(&e.timestamp),
|
||||
Theme::muted().render(&to_date(&e.timestamp)),
|
||||
e.event_type,
|
||||
actor_str,
|
||||
e.summary
|
||||
|
||||
Reference in New Issue
Block a user