From d8ac473475f0d27826432b8d2a2e80d59bdc31b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Wed, 17 Jun 2026 20:46:41 +0200 Subject: [PATCH] Colorize hex.policy and cooldown output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mix hex.policy show and why now highlight the active policy with ANSI colors: the policy name and rule values, ALLOWED/BLOCKED status, and per-reason colors (advisory, retirement, cooldown). The "Versions filtered by cooldown" summary printed during mix deps.get highlights the package, version, and eligible date. Also shorten the cooldown summary line to "PKG VSN — eligible DATE", dropping the redundant "published N days ago" — the eligible date is the actionable part. --- lib/hex/cooldown.ex | 27 +++++++++----- lib/hex/policy/diagnostics.ex | 10 ++++++ lib/mix/tasks/hex.policy.ex | 67 ++++++++++++++++++++--------------- test/hex/cooldown_test.exs | 1 - 4 files changed, 68 insertions(+), 37 deletions(-) diff --git a/lib/hex/cooldown.ex b/lib/hex/cooldown.ex index 4ba324cf..ece91156 100644 --- a/lib/hex/cooldown.ex +++ b/lib/hex/cooldown.ex @@ -187,19 +187,18 @@ defmodule Hex.Cooldown do nil entries -> - today = Date.utc_today() - lines = entries |> Enum.group_by(fn {repo, pkg, _vsn, _} -> {repo, pkg} end) |> Enum.sort() - |> Enum.flat_map(fn {_key, entries} -> package_lines(entries, cutoff, today) end) + |> Enum.flat_map(fn {_key, entries} -> package_lines(entries, cutoff) end) - "\nVersions filtered by cooldown:\n" <> Enum.join(lines, "\n") <> "\n" + iodata = ["\nVersions filtered by cooldown:\n", Enum.intersperse(lines, "\n"), "\n"] + IO.chardata_to_string(Hex.Shell.format(iodata)) end end - defp package_lines(entries, cutoff, today) do + defp package_lines(entries, cutoff) do {listed, rest} = entries |> Enum.sort_by(fn {_repo, _pkg, vsn, _} -> Version.parse!(vsn) end, {:desc, Version}) @@ -207,10 +206,22 @@ defmodule Hex.Cooldown do lines = Enum.map(listed, fn {_repo, pkg, vsn, published_at} -> - published_date = published_at |> DateTime.from_unix!() |> DateTime.to_date() - days_ago = Date.diff(today, published_date) eligible_date = eligible_on(published_at, cutoff) - " #{pkg} #{vsn} — published #{days_ago} days ago, eligible #{eligible_date}" + + [ + " ", + :bright, + pkg, + :reset, + " ", + :yellow, + vsn, + :reset, + " — eligible ", + :cyan, + to_string(eligible_date), + :reset + ] end) case rest do diff --git a/lib/hex/policy/diagnostics.ex b/lib/hex/policy/diagnostics.ex index 29786368..94eea03b 100644 --- a/lib/hex/policy/diagnostics.ex +++ b/lib/hex/policy/diagnostics.ex @@ -165,6 +165,16 @@ defmodule Hex.Policy.Diagnostics do def format_reason(other), do: inspect(other) + @doc """ + Maps a blocker reason to an ANSI color used to highlight it in output. + """ + @spec reason_color(term()) :: atom() + def reason_color({:advisory, _}), do: :red + def reason_color({:retirement, _}), do: :yellow + def reason_color({:cooldown, _, _}), do: :cyan + def reason_color(:override_deny), do: :red + def reason_color(_other), do: :reset + defp count(1, noun), do: "1 #{noun}" defp count(n, noun), do: "#{n} #{noun}s" end diff --git a/lib/mix/tasks/hex.policy.ex b/lib/mix/tasks/hex.policy.ex index a76d2741..f8c00dc9 100644 --- a/lib/mix/tasks/hex.policy.ex +++ b/lib/mix/tasks/hex.policy.ex @@ -93,9 +93,13 @@ defmodule Mix.Tasks.Hex.Policy do end defp render_show(policy) do - Hex.Shell.info( - "Active policy: #{policy.repository}/#{policy.name} [#{visibility_label(policy.visibility)}]\n" - ) + Hex.Shell.info([ + "Active policy: ", + [:cyan, "#{policy.repository}/#{policy.name}"], + " ", + visibility_tag(policy.visibility), + "\n" + ]) render_repositories(policy) Hex.Shell.info("") @@ -107,7 +111,11 @@ defmodule Mix.Tasks.Hex.Policy do :ok {source, duration} -> - Hex.Shell.info("Effective cooldown: #{duration} (#{cooldown_source(source)})") + Hex.Shell.info([ + "Effective cooldown: ", + [:cyan, duration], + " (#{cooldown_source(source)})" + ]) end end @@ -125,11 +133,11 @@ defmodule Mix.Tasks.Hex.Policy do restriction = Map.get(rp, :restriction) overrides = Map.get(rp, :overrides, []) - Hex.Shell.info(" #{rp.repository}:") - Hex.Shell.info(" Cooldown: #{cooldown_label(restriction)}") - Hex.Shell.info(" Advisory rule: #{advisory_label(restriction)}") - Hex.Shell.info(" Retirement rule: #{retirement_label(restriction)}") - Hex.Shell.info(" Overrides: #{length(overrides)}") + Hex.Shell.info([" ", [:bright, "#{rp.repository}:"]]) + Hex.Shell.info([" Cooldown: ", cooldown_label(restriction)]) + Hex.Shell.info([" Advisory rule: ", advisory_label(restriction)]) + Hex.Shell.info([" Retirement rule: ", retirement_label(restriction)]) + Hex.Shell.info([" Overrides: ", to_string(length(overrides))]) end defp why(arg) do @@ -177,16 +185,16 @@ defmodule Mix.Tasks.Hex.Policy do case Filter.classify(policy, candidate) do :allowed -> - [version, "ALLOWED", ""] + [version, [:green, "ALLOWED"], ""] {:blocked, reasons} -> - reason_text = + reason_cell = reasons - |> Enum.map(&Diagnostics.format_reason/1) |> Enum.uniq() - |> Enum.join(", ") + |> Enum.map(&[Diagnostics.reason_color(&1), Diagnostics.format_reason(&1)]) + |> Enum.intersperse(", ") - [version, "BLOCKED", reason_text] + [version, [:red, "BLOCKED"], reason_cell] end end) @@ -196,27 +204,30 @@ defmodule Mix.Tasks.Hex.Policy do defp cooldown_source(:local), do: "local" defp cooldown_source({repo, name}), do: "#{repo}/#{name}" - defp visibility_label(:VISIBILITY_PUBLIC), do: "public" - defp visibility_label(:VISIBILITY_PRIVATE), do: "private" - defp visibility_label(other), do: to_string(other) + defp visibility_tag(:VISIBILITY_PUBLIC), do: [:green, "[public]"] + defp visibility_tag(:VISIBILITY_PRIVATE), do: [:yellow, "[private]"] + defp visibility_tag(other), do: ["[", to_string(other), "]"] - defp cooldown_label(%{cooldown: duration}) when is_binary(duration), do: duration - defp cooldown_label(_restriction), do: "(none)" + defp cooldown_label(%{cooldown: duration}) when is_binary(duration), do: [:cyan, duration] + defp cooldown_label(_restriction), do: [:faint, "(none)"] - defp advisory_label(%{advisory_min_severity: :SEVERITY_NONE}), do: "block any advisory" + defp advisory_label(%{advisory_min_severity: :SEVERITY_NONE}), do: [:red, "block any advisory"] defp advisory_label(%{advisory_min_severity: severity}) when not is_nil(severity), - do: "block ≥ #{Hex.Utils.advisory_severity(severity)}" + do: [:red, "block ≥ #{Hex.Utils.advisory_severity(severity)}"] - defp advisory_label(_restriction), do: "(disabled)" + defp advisory_label(_restriction), do: [:faint, "(disabled)"] defp retirement_label(%{retirement_reasons: reasons}) when is_list(reasons) and reasons != [] do - reasons - |> Enum.map(fn r -> - r |> Hex.Utils.package_retirement_reason() |> String.upcase() - end) - |> Enum.join(", ") + text = + reasons + |> Enum.map(fn r -> + r |> Hex.Utils.package_retirement_reason() |> String.upcase() + end) + |> Enum.join(", ") + + [:yellow, text] end - defp retirement_label(_restriction), do: "(disabled)" + defp retirement_label(_restriction), do: [:faint, "(disabled)"] end diff --git a/test/hex/cooldown_test.exs b/test/hex/cooldown_test.exs index 33a64abd..e61059b6 100644 --- a/test/hex/cooldown_test.exs +++ b/test/hex/cooldown_test.exs @@ -298,7 +298,6 @@ defmodule Hex.CooldownTest do assert summary =~ "Versions filtered by cooldown:" assert summary =~ "castore 1.0.19" - assert summary =~ "3 days ago" assert summary =~ "eligible #{Cooldown.eligible_on(published_at, cutoff)}" end