Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 23 additions & 19 deletions crates/mergify-queue/src/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@
use std::io::Write;

use anstyle::AnsiColor;
use anstyle::Style;
use chrono::DateTime;
use chrono::Utc;
use mergify_core::CliError;
use mergify_core::CommandContext;
use mergify_core::Output;
use mergify_tui::StyledGlyph;
use mergify_tui::Theme;
use mergify_tui::relative_time;
use mergify_tui::tree;
Expand Down Expand Up @@ -230,11 +230,12 @@ fn print_checks_section(
now: DateTime<Utc>,
) -> std::io::Result<()> {
writeln!(w)?;
let (icon, style) = check_state_glyph(theme, &mc.ci_state);
let glyph = check_state_glyph(theme, &mc.ci_state);
write!(
w,
" CI State: {S}{icon} {state}{R}",
S = style,
S = glyph.style,
icon = glyph.icon,
state = mc.ci_state,
R = theme.reset,
)?;
Expand Down Expand Up @@ -267,7 +268,7 @@ fn print_checks_table(w: &mut dyn Write, theme: &Theme, checks: &[Check]) -> std
.max()
.unwrap_or(0);
for check in checks {
let (icon, style) = check_state_glyph(theme, &check.state);
let glyph = check_state_glyph(theme, &check.state);
let pad = name_width.saturating_sub(check.name.chars().count());
writeln!(
w,
Expand All @@ -276,7 +277,8 @@ fn print_checks_table(w: &mut dyn Write, theme: &Theme, checks: &[Check]) -> std
name = check.name,
spaces = " ".repeat(pad),
R = theme.reset,
S = style,
S = glyph.style,
icon = glyph.icon,
state = check.state,
)?;
}
Expand Down Expand Up @@ -325,11 +327,12 @@ fn print_checks_summary(w: &mut dyn Write, theme: &Theme, checks: &[Check]) -> s
check.state.as_str(),
"failure" | "error" | "timed_out" | "action_required"
) {
let (icon, style) = check_state_glyph(theme, &check.state);
let glyph = check_state_glyph(theme, &check.state);
writeln!(
w,
" {S}{icon} {state}{R} {D}{name}{R}",
S = style,
S = glyph.style,
icon = glyph.icon,
state = check.state,
R = theme.reset,
D = theme.dim,
Expand All @@ -340,17 +343,17 @@ fn print_checks_summary(w: &mut dyn Write, theme: &Theme, checks: &[Check]) -> s
Ok(())
}

/// Map a check state string to (icon, ANSI style). Mirrors Python's
/// Map a check state string to its [`StyledGlyph`]. Mirrors Python's
/// `CHECK_STATE_STYLES`; unknown states fall back to a dim `?` so
/// the renderer never crashes on a new API code.
fn check_state_glyph(theme: &Theme, state: &str) -> (&'static str, Style) {
fn check_state_glyph(theme: &Theme, state: &str) -> StyledGlyph {
match state {
"success" => ("✓", theme.fg(AnsiColor::Green)),
"pending" => ("◌", theme.fg(AnsiColor::Yellow)),
"failure" | "error" | "action_required" => ("✗", theme.fg(AnsiColor::Red)),
"timed_out" => ("⏰", theme.fg(AnsiColor::Red)),
"cancelled" | "neutral" | "skipped" | "stale" => ("○", theme.dim),
_ => ("?", theme.dim),
"success" => StyledGlyph::new("✓", theme.fg(AnsiColor::Green)),
"pending" => StyledGlyph::new("◌", theme.fg(AnsiColor::Yellow)),
"failure" | "error" | "action_required" => StyledGlyph::new("✗", theme.fg(AnsiColor::Red)),
"timed_out" => StyledGlyph::new("⏰", theme.fg(AnsiColor::Red)),
"cancelled" | "neutral" | "skipped" | "stale" => StyledGlyph::new("○", theme.dim),
_ => StyledGlyph::new("?", theme.dim),
}
}

Expand Down Expand Up @@ -446,15 +449,16 @@ fn write_condition_tree(
let last = nodes.len() - 1;
for (i, node) in nodes.iter().enumerate() {
let (branch, continuation) = tree::branch_chars(i == last);
let (icon, style) = if node.r#match {
("✓", theme.fg(AnsiColor::Green))
let glyph = if node.r#match {
StyledGlyph::new("✓", theme.fg(AnsiColor::Green))
} else {
("✗", theme.fg(AnsiColor::Red))
StyledGlyph::new("✗", theme.fg(AnsiColor::Red))
};
writeln!(
w,
"{prefix}{branch}{S}{icon}{R} {label}",
S = style,
S = glyph.style,
icon = glyph.icon,
R = theme.reset,
label = node.label,
)?;
Expand Down
115 changes: 62 additions & 53 deletions crates/mergify-queue/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ use std::collections::HashSet;
use std::io::Write;

use anstyle::AnsiColor;
use anstyle::Style;
use chrono::DateTime;
use chrono::Utc;
use indexmap::IndexMap;
use mergify_core::CliError;
use mergify_core::CommandContext;
use mergify_core::Output;
use mergify_tui::StyledGlyph;
use mergify_tui::Theme;
use mergify_tui::relative_time;
use mergify_tui::tree;
Expand Down Expand Up @@ -194,28 +194,48 @@ fn emit_human(output: &mut dyn Output, repository: &str, view: &StatusView) -> s
})
}

/// Map a queue batch status code to a foreground color, honoring
/// the theme's enabled flag. Mirrors Python's `STATUS_STYLES`;
/// unknown codes render dim.
fn batch_status_style(theme: &Theme, code: &str) -> Style {
if !theme.enabled {
return Style::new();
}
match code {
"running" => theme.fg(AnsiColor::Green),
// Python rendered `merged` as `"dim green"` — bold off,
// green on, dimmed. anstyle composes the same effect by
// setting `dimmed()` on the green style.
"merged" => theme.fg(AnsiColor::Green).dimmed(),
"failed" => theme.fg(AnsiColor::Red),
"bisecting"
| "preparing"
| "waiting_for_previous_batches"
| "waiting_for_requeue"
| "waiting_schedule" => theme.fg(AnsiColor::Yellow),
"waiting_for_merge" | "frozen" => theme.fg(AnsiColor::Cyan),
_ => theme.dim,
}
/// Map a queue batch status code to its [`StyledGlyph`]. Mirrors
/// Python's `STATUS_STYLES` (icon + color side); unknown codes fall
/// back to a dim `?`.
fn batch_glyph(theme: &Theme, code: &str) -> StyledGlyph {
// `theme.fg` already collapses to `Style::new()` when colors are
// off — except for `merged`, which we compose as `green.dimmed()`.
// When colors are off the `dimmed` attribute would still emit a
// dim escape, so short-circuit here to keep the plain output
// truly plain.
let style = if theme.enabled {
match code {
"running" => theme.fg(AnsiColor::Green),
// Python rendered `merged` as `"dim green"` — bold off,
// green on, dimmed. anstyle composes the same effect by
// setting `dimmed()` on the green style.
"merged" => theme.fg(AnsiColor::Green).dimmed(),
"failed" => theme.fg(AnsiColor::Red),
"bisecting"
| "preparing"
| "waiting_for_previous_batches"
| "waiting_for_requeue"
| "waiting_schedule" => theme.fg(AnsiColor::Yellow),
"waiting_for_merge" | "frozen" => theme.fg(AnsiColor::Cyan),
_ => theme.dim,
}
} else {
anstyle::Style::new()
};
let icon = match code {
"running" => "●",
"bisecting" => "◑",
"preparing" => "◌",
"failed" => "✗",
"merged" => "✓",
"waiting_for_merge" => "◎",
"waiting_for_previous_batches" | "waiting_for_batch" => "⏳",
"waiting_for_requeue" => "↻",
"waiting_schedule" => "⏰",
"frozen" => "❄",
_ => "?",
};
StyledGlyph::new(icon, style)
}

fn print_pause(
Expand Down Expand Up @@ -281,12 +301,12 @@ fn print_batch_line(
batch: &Batch,
now: DateTime<Utc>,
) -> std::io::Result<()> {
let icon = status_icon(&batch.status.code);
let icon_style = batch_status_style(theme, &batch.status.code);
let glyph = batch_glyph(theme, &batch.status.code);
write!(
w,
"{branch}{S}{icon} {code}{R}",
S = icon_style,
S = glyph.style,
icon = glyph.icon,
code = batch.status.code,
R = theme.reset,
)?;
Expand Down Expand Up @@ -379,24 +399,6 @@ fn print_waiting_prs(
Ok(())
}

/// Map a batch-status code to a compact Unicode icon. Same icons as
/// the Python implementation; unknown codes fall back to `?`.
fn status_icon(code: &str) -> &'static str {
match code {
"running" => "●",
"bisecting" => "◑",
"preparing" => "◌",
"failed" => "✗",
"merged" => "✓",
"waiting_for_merge" => "◎",
"waiting_for_previous_batches" | "waiting_for_batch" => "⏳",
"waiting_for_requeue" => "↻",
"waiting_schedule" => "⏰",
"frozen" => "❄",
_ => "?",
}
}

/// Topological sort of batches by `parent_ids`. Roots come first,
/// children follow their parents — matches the Python
/// `_topological_sort`. Cycles are impossible by API contract, but
Expand Down Expand Up @@ -543,22 +545,29 @@ mod tests {
}

#[test]
fn status_icon_known_codes() {
assert_eq!(status_icon("running"), "●");
assert_eq!(status_icon("merged"), "✓");
assert_eq!(status_icon("failed"), "✗");
fn batch_glyph_known_codes() {
// Pin a no-color theme so we exercise the icon mapping
// without coupling the assertions to the ANSI escape format.
let theme = Theme::new(false);
assert_eq!(batch_glyph(&theme, "running").icon, "●");
assert_eq!(batch_glyph(&theme, "merged").icon, "✓");
assert_eq!(batch_glyph(&theme, "failed").icon, "✗");
// Two pairs that share an icon vs. a different icon —
// mirrors the Python `STATUS_STYLES` table, so a future
// table edit can't silently swap glyphs without updating
// this test.
assert_eq!(status_icon("waiting_for_previous_batches"), "⏳");
assert_eq!(status_icon("waiting_for_batch"), "⏳");
assert_eq!(status_icon("waiting_for_requeue"), "↻");
assert_eq!(
batch_glyph(&theme, "waiting_for_previous_batches").icon,
"⏳"
);
assert_eq!(batch_glyph(&theme, "waiting_for_batch").icon, "⏳");
assert_eq!(batch_glyph(&theme, "waiting_for_requeue").icon, "↻");
}

#[test]
fn status_icon_unknown_falls_back() {
assert_eq!(status_icon("brand-new-status"), "?");
fn batch_glyph_unknown_falls_back() {
let theme = Theme::new(false);
assert_eq!(batch_glyph(&theme, "brand-new-status").icon, "?");
}

fn sample_batch(id: &str, parents: &[&str]) -> Batch {
Expand Down
27 changes: 27 additions & 0 deletions crates/mergify-tui/src/glyph.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! Pairing of a Unicode icon with its [`anstyle::Style`].
//!
//! Multiple commands map an enum-ish state code (CI check states,
//! merge-queue batch states, …) to "render this icon in that color".
//! Both halves of that pair travel together at every call site: the
//! icon goes in the formatted output, the style wraps it. Bundling
//! them into one named type beats returning a `(&str, Style)` tuple
//! — the field names document what's what at the call site, and
//! future fields (e.g. a dim suffix) can be added without rewriting
//! every consumer.

use anstyle::Style;

#[derive(Clone, Copy)]
pub struct StyledGlyph {
pub icon: &'static str,
pub style: Style,
}

impl StyledGlyph {
/// Convenience constructor — saves a few `StyledGlyph { … }`
/// braces at the dense pattern-match call sites.
#[must_use]
pub const fn new(icon: &'static str, style: Style) -> Self {
Self { icon, style }
}
}
6 changes: 6 additions & 0 deletions crates/mergify-tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
//! palette. The same closure-based emit code paths produce
//! styled output on a TTY and plain text everywhere else with no
//! conditional branching at every write.
//! - [`glyph`]: [`StyledGlyph`] — pairs a Unicode icon with the
//! [`anstyle::Style`] it's drawn in. Used by commands that map
//! state codes (check states, batch states, …) to a small visual
//! token.
//! - [`time`]: [`relative_time`](time::relative_time) formats an
//! ISO-8601/RFC-3339 timestamp as a coarse delta (`Ns` / `Nm` /
//! `Nh` / `Nd`), with `~…` / `… ago` decorators for
Expand All @@ -27,9 +31,11 @@
//! [`LAST_CONTINUATION`](tree::LAST_CONTINUATION)) and the
//! [`branch_chars`](tree::branch_chars) helper.

pub mod glyph;
pub mod theme;
pub mod time;
pub mod tree;

pub use glyph::StyledGlyph;
pub use theme::Theme;
pub use time::relative_time;
Loading