refactor(cli): migrate all command modules from console::style to Theme
Replace all console::style() calls in command modules with the centralized Theme API and render:: utility functions. This ensures consistent color behavior across the entire CLI, proper NO_COLOR/--color never support via the LoreRenderer singleton, and eliminates duplicated formatting code. Changes per module: - count.rs: Theme for table headers, render::format_number replacing local duplicate. Removed local format_number implementation. - doctor.rs: Theme::success/warning/error for check status symbols and messages. Unicode escapes for check/warning/cross symbols. - drift.rs: Theme::bold/error/success for drift detection headers and status messages. - embed.rs: Compact output format — headline with count, zero-suppressed detail lines, 'nothing to embed' short-circuit for no-op runs. - generate_docs.rs: Same compact pattern — headline + detail + hint for next step. No-op short-circuit when regenerated==0. - ingest.rs: Theme for project summaries, sync status, dry-run preview. All console::style -> Theme replacements. - list.rs: Replace comfy-table with render::LoreTable for issue/MR listing. Remove local colored_cell, colored_cell_hex, format_relative_time, truncate_with_ellipsis, and format_labels (all moved to render.rs). - list_tests.rs: Update test assertions to use render:: functions. - search.rs: Add render_snippet() for FTS5 <mark> tag highlighting via Theme::bold().underline(). Compact result layout with type badges. - show.rs: Theme for entity detail views, delegate format_date and wrap_text to render module. - stats.rs: Section-based layout using render::section_divider. Compact middle-dot format for document counts. Color-coded embedding coverage percentage (green >=95%, yellow >=50%, red <50%). - sync.rs: Compact sync summary — headline with counts and elapsed time, zero-suppressed detail lines, visually prominent error-only section. - sync_status.rs: Theme for run history headers, removed local format_number duplicate. - timeline.rs: Theme for headers/footers, render:: for date/truncate, standard format! padding replacing console::pad_str. - who.rs: Theme for all expert/workload/active/overlap/review output modes, render:: for relative time and truncation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table};
|
||||
use crate::cli::render::{self, Align, StyledCell, Table as LoreTable, Theme};
|
||||
use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -9,39 +9,7 @@ use crate::core::error::{LoreError, Result};
|
||||
use crate::core::path_resolver::escape_like as note_escape_like;
|
||||
use crate::core::paths::get_db_path;
|
||||
use crate::core::project::resolve_project;
|
||||
use crate::core::time::{ms_to_iso, now_ms, parse_since};
|
||||
|
||||
fn colored_cell(content: impl std::fmt::Display, color: Color) -> Cell {
|
||||
let cell = Cell::new(content);
|
||||
if console::colors_enabled() {
|
||||
cell.fg(color)
|
||||
} else {
|
||||
cell
|
||||
}
|
||||
}
|
||||
|
||||
fn colored_cell_hex(content: &str, hex: Option<&str>) -> Cell {
|
||||
if !console::colors_enabled() {
|
||||
return Cell::new(content);
|
||||
}
|
||||
let Some(hex) = hex else {
|
||||
return Cell::new(content);
|
||||
};
|
||||
let hex = hex.trim_start_matches('#');
|
||||
if hex.len() != 6 {
|
||||
return Cell::new(content);
|
||||
}
|
||||
let Ok(r) = u8::from_str_radix(&hex[0..2], 16) else {
|
||||
return Cell::new(content);
|
||||
};
|
||||
let Ok(g) = u8::from_str_radix(&hex[2..4], 16) else {
|
||||
return Cell::new(content);
|
||||
};
|
||||
let Ok(b) = u8::from_str_radix(&hex[4..6], 16) else {
|
||||
return Cell::new(content);
|
||||
};
|
||||
Cell::new(content).fg(Color::Rgb { r, g, b })
|
||||
}
|
||||
use crate::core::time::{ms_to_iso, parse_since};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IssueListRow {
|
||||
@@ -669,60 +637,6 @@ fn query_mrs(conn: &Connection, filters: &MrListFilters) -> Result<MrListResult>
|
||||
Ok(MrListResult { mrs, total_count })
|
||||
}
|
||||
|
||||
fn format_relative_time(ms_epoch: i64) -> String {
|
||||
let now = now_ms();
|
||||
let diff = now - ms_epoch;
|
||||
|
||||
if diff < 0 {
|
||||
return "in the future".to_string();
|
||||
}
|
||||
|
||||
match diff {
|
||||
d if d < 60_000 => "just now".to_string(),
|
||||
d if d < 3_600_000 => format!("{} min ago", d / 60_000),
|
||||
d if d < 86_400_000 => {
|
||||
let n = d / 3_600_000;
|
||||
format!("{n} {} ago", if n == 1 { "hour" } else { "hours" })
|
||||
}
|
||||
d if d < 604_800_000 => {
|
||||
let n = d / 86_400_000;
|
||||
format!("{n} {} ago", if n == 1 { "day" } else { "days" })
|
||||
}
|
||||
d if d < 2_592_000_000 => {
|
||||
let n = d / 604_800_000;
|
||||
format!("{n} {} ago", if n == 1 { "week" } else { "weeks" })
|
||||
}
|
||||
_ => {
|
||||
let n = diff / 2_592_000_000;
|
||||
format!("{n} {} ago", if n == 1 { "month" } else { "months" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate_with_ellipsis(s: &str, max_width: usize) -> String {
|
||||
if s.chars().count() <= max_width {
|
||||
s.to_string()
|
||||
} else {
|
||||
let truncated: String = s.chars().take(max_width.saturating_sub(3)).collect();
|
||||
format!("{truncated}...")
|
||||
}
|
||||
}
|
||||
|
||||
fn format_labels(labels: &[String], max_shown: usize) -> String {
|
||||
if labels.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let shown: Vec<&str> = labels.iter().take(max_shown).map(|s| s.as_str()).collect();
|
||||
let overflow = labels.len().saturating_sub(max_shown);
|
||||
|
||||
if overflow > 0 {
|
||||
format!("[{} +{}]", shown.join(", "), overflow)
|
||||
} else {
|
||||
format!("[{}]", shown.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
fn format_assignees(assignees: &[String]) -> String {
|
||||
if assignees.is_empty() {
|
||||
return "-".to_string();
|
||||
@@ -732,7 +646,7 @@ fn format_assignees(assignees: &[String]) -> String {
|
||||
let shown: Vec<String> = assignees
|
||||
.iter()
|
||||
.take(max_shown)
|
||||
.map(|s| format!("@{}", truncate_with_ellipsis(s, 10)))
|
||||
.map(|s| format!("@{}", render::truncate(s, 10)))
|
||||
.collect();
|
||||
let overflow = assignees.len().saturating_sub(max_shown);
|
||||
|
||||
@@ -757,7 +671,7 @@ fn format_discussions(total: i64, unresolved: i64) -> String {
|
||||
|
||||
fn format_branches(target: &str, source: &str, max_width: usize) -> String {
|
||||
let full = format!("{} <- {}", target, source);
|
||||
truncate_with_ellipsis(&full, max_width)
|
||||
render::truncate(&full, max_width)
|
||||
}
|
||||
|
||||
pub fn print_list_issues(result: &ListResult) {
|
||||
@@ -774,64 +688,55 @@ pub fn print_list_issues(result: &ListResult) {
|
||||
|
||||
let has_any_status = result.issues.iter().any(|i| i.status_name.is_some());
|
||||
|
||||
let mut header = vec![
|
||||
Cell::new("IID").add_attribute(Attribute::Bold),
|
||||
Cell::new("Title").add_attribute(Attribute::Bold),
|
||||
Cell::new("State").add_attribute(Attribute::Bold),
|
||||
];
|
||||
let mut headers = vec!["IID", "Title", "State"];
|
||||
if has_any_status {
|
||||
header.push(Cell::new("Status").add_attribute(Attribute::Bold));
|
||||
headers.push("Status");
|
||||
}
|
||||
header.extend([
|
||||
Cell::new("Assignee").add_attribute(Attribute::Bold),
|
||||
Cell::new("Labels").add_attribute(Attribute::Bold),
|
||||
Cell::new("Disc").add_attribute(Attribute::Bold),
|
||||
Cell::new("Updated").add_attribute(Attribute::Bold),
|
||||
]);
|
||||
headers.extend(["Assignee", "Labels", "Disc", "Updated"]);
|
||||
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||
.set_header(header);
|
||||
let mut table = LoreTable::new().headers(&headers).align(0, Align::Right);
|
||||
|
||||
for issue in &result.issues {
|
||||
let title = truncate_with_ellipsis(&issue.title, 45);
|
||||
let relative_time = format_relative_time(issue.updated_at);
|
||||
let labels = format_labels(&issue.labels, 2);
|
||||
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 assignee = format_assignees(&issue.assignees);
|
||||
let discussions = format_discussions(issue.discussion_count, issue.unresolved_count);
|
||||
|
||||
let state_cell = if issue.state == "opened" {
|
||||
colored_cell(&issue.state, Color::Green)
|
||||
StyledCell::styled(&issue.state, Theme::success())
|
||||
} else {
|
||||
colored_cell(&issue.state, Color::DarkGrey)
|
||||
StyledCell::styled(&issue.state, Theme::dim())
|
||||
};
|
||||
|
||||
let mut row = vec![
|
||||
colored_cell(format!("#{}", issue.iid), Color::Cyan),
|
||||
Cell::new(title),
|
||||
StyledCell::styled(format!("#{}", issue.iid), Theme::info()),
|
||||
StyledCell::plain(title),
|
||||
state_cell,
|
||||
];
|
||||
if has_any_status {
|
||||
match &issue.status_name {
|
||||
Some(status) => {
|
||||
row.push(colored_cell_hex(status, issue.status_color.as_deref()));
|
||||
row.push(StyledCell::plain(render::style_with_hex(
|
||||
status,
|
||||
issue.status_color.as_deref(),
|
||||
)));
|
||||
}
|
||||
None => {
|
||||
row.push(Cell::new(""));
|
||||
row.push(StyledCell::plain(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
row.extend([
|
||||
colored_cell(assignee, Color::Magenta),
|
||||
colored_cell(labels, Color::Yellow),
|
||||
Cell::new(discussions),
|
||||
colored_cell(relative_time, Color::DarkGrey),
|
||||
StyledCell::styled(assignee, Theme::accent()),
|
||||
StyledCell::styled(labels, Theme::warning()),
|
||||
StyledCell::plain(discussions),
|
||||
StyledCell::styled(relative_time, Theme::dim()),
|
||||
]);
|
||||
table.add_row(row);
|
||||
}
|
||||
|
||||
println!("{table}");
|
||||
println!("{}", table.render());
|
||||
}
|
||||
|
||||
pub fn print_list_issues_json(result: &ListResult, elapsed_ms: u64, fields: Option<&[String]>) {
|
||||
@@ -883,53 +788,46 @@ pub fn print_list_mrs(result: &MrListResult) {
|
||||
result.total_count
|
||||
);
|
||||
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||
.set_header(vec![
|
||||
Cell::new("IID").add_attribute(Attribute::Bold),
|
||||
Cell::new("Title").add_attribute(Attribute::Bold),
|
||||
Cell::new("State").add_attribute(Attribute::Bold),
|
||||
Cell::new("Author").add_attribute(Attribute::Bold),
|
||||
Cell::new("Branches").add_attribute(Attribute::Bold),
|
||||
Cell::new("Disc").add_attribute(Attribute::Bold),
|
||||
Cell::new("Updated").add_attribute(Attribute::Bold),
|
||||
]);
|
||||
let mut table = LoreTable::new()
|
||||
.headers(&[
|
||||
"IID", "Title", "State", "Author", "Branches", "Disc", "Updated",
|
||||
])
|
||||
.align(0, Align::Right);
|
||||
|
||||
for mr in &result.mrs {
|
||||
let title = if mr.draft {
|
||||
format!("[DRAFT] {}", truncate_with_ellipsis(&mr.title, 38))
|
||||
format!("[DRAFT] {}", render::truncate(&mr.title, 38))
|
||||
} else {
|
||||
truncate_with_ellipsis(&mr.title, 45)
|
||||
render::truncate(&mr.title, 45)
|
||||
};
|
||||
|
||||
let relative_time = format_relative_time(mr.updated_at);
|
||||
let relative_time = render::format_relative_time(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" => colored_cell(&mr.state, Color::Green),
|
||||
"merged" => colored_cell(&mr.state, Color::Magenta),
|
||||
"closed" => colored_cell(&mr.state, Color::Red),
|
||||
"locked" => colored_cell(&mr.state, Color::Yellow),
|
||||
_ => colored_cell(&mr.state, Color::DarkGrey),
|
||||
"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()),
|
||||
};
|
||||
|
||||
table.add_row(vec![
|
||||
colored_cell(format!("!{}", mr.iid), Color::Cyan),
|
||||
Cell::new(title),
|
||||
StyledCell::styled(format!("!{}", mr.iid), Theme::info()),
|
||||
StyledCell::plain(title),
|
||||
state_cell,
|
||||
colored_cell(
|
||||
format!("@{}", truncate_with_ellipsis(&mr.author_username, 12)),
|
||||
Color::Magenta,
|
||||
StyledCell::styled(
|
||||
format!("@{}", render::truncate(&mr.author_username, 12)),
|
||||
Theme::accent(),
|
||||
),
|
||||
colored_cell(branches, Color::Blue),
|
||||
Cell::new(discussions),
|
||||
colored_cell(relative_time, Color::DarkGrey),
|
||||
StyledCell::styled(branches, Theme::info()),
|
||||
StyledCell::plain(discussions),
|
||||
StyledCell::styled(relative_time, Theme::dim()),
|
||||
]);
|
||||
}
|
||||
|
||||
println!("{table}");
|
||||
println!("{}", table.render());
|
||||
}
|
||||
|
||||
pub fn print_list_mrs_json(result: &MrListResult, elapsed_ms: u64, fields: Option<&[String]>) {
|
||||
@@ -1016,18 +914,17 @@ pub fn print_list_notes(result: &NoteListResult) {
|
||||
result.total_count
|
||||
);
|
||||
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||
.set_header(vec![
|
||||
Cell::new("ID").add_attribute(Attribute::Bold),
|
||||
Cell::new("Author").add_attribute(Attribute::Bold),
|
||||
Cell::new("Type").add_attribute(Attribute::Bold),
|
||||
Cell::new("Body").add_attribute(Attribute::Bold),
|
||||
Cell::new("Path:Line").add_attribute(Attribute::Bold),
|
||||
Cell::new("Parent").add_attribute(Attribute::Bold),
|
||||
Cell::new("Created").add_attribute(Attribute::Bold),
|
||||
]);
|
||||
let mut table = LoreTable::new()
|
||||
.headers(&[
|
||||
"ID",
|
||||
"Author",
|
||||
"Type",
|
||||
"Body",
|
||||
"Path:Line",
|
||||
"Parent",
|
||||
"Created",
|
||||
])
|
||||
.align(0, Align::Right);
|
||||
|
||||
for note in &result.notes {
|
||||
let body = note
|
||||
@@ -1037,24 +934,24 @@ 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 = format_relative_time(note.created_at);
|
||||
let relative_time = render::format_relative_time(note.created_at);
|
||||
let note_type = format_note_type(note.note_type.as_deref());
|
||||
|
||||
table.add_row(vec![
|
||||
colored_cell(note.gitlab_id, Color::Cyan),
|
||||
colored_cell(
|
||||
format!("@{}", truncate_with_ellipsis(¬e.author_username, 12)),
|
||||
Color::Magenta,
|
||||
StyledCell::styled(note.gitlab_id.to_string(), Theme::info()),
|
||||
StyledCell::styled(
|
||||
format!("@{}", render::truncate(¬e.author_username, 12)),
|
||||
Theme::accent(),
|
||||
),
|
||||
Cell::new(note_type),
|
||||
Cell::new(body),
|
||||
Cell::new(path),
|
||||
Cell::new(parent),
|
||||
colored_cell(relative_time, Color::DarkGrey),
|
||||
StyledCell::plain(note_type),
|
||||
StyledCell::plain(body),
|
||||
StyledCell::plain(path),
|
||||
StyledCell::plain(parent),
|
||||
StyledCell::styled(relative_time, Theme::dim()),
|
||||
]);
|
||||
}
|
||||
|
||||
println!("{table}");
|
||||
println!("{}", table.render());
|
||||
}
|
||||
|
||||
pub fn print_list_notes_json(result: &NoteListResult, elapsed_ms: u64, fields: Option<&[String]>) {
|
||||
|
||||
Reference in New Issue
Block a user