"""Icon tiers, selected from context.
The honest answer to "can I use Nerd Font glyphs?" depends on where the terminal
actually lives, and guessing wrong produces tofu.
nerd private-use Nerd Font glyphs. Needs a Nerd Font installed *and* a
terminal we can reasonably expect to render it.
unicode geometric Unicode every monospace font carries (default fallback).
ascii dumb terminals, no UTF-8, NO_ICONS.
Every state glyph is exactly one cell wide in every tier, because the layout is a
fixed grid and a two-cell glyph silently breaks every column to its right.
"""
from __future__ import annotations
import functools
import os
import subprocess
import sys
from dataclasses import dataclass
@dataclass(frozen=True)
class Glyphs:
done: str
run: str
wait: str
fail: str
shared: str
warn: str
arrow: str
branch: str
last: str
pipe: str
# nf-fa-check, nf-fa-play, nf-fa-clock_o, nf-fa-times, nf-fa-refresh,
# nf-fa-exclamation_triangle, nf-fa-arrow_right
NERD = Glyphs(
done="\uf00c",
run="\uf04b",
wait="\uf017",
fail="\uf00d",
shared="\uf021",
warn="\uf071",
arrow="\uf061",
branch="├─",
last="└─",
pipe="│",
)
UNICODE = Glyphs(
done="✓",
run="▶",
wait="·",
fail="✗",
shared="↻",
warn="!",
arrow="→",
branch="├─",
last="└─",
pipe="│",
)
ASCII = Glyphs(
done="+",
run=">",
wait=".",
fail="x",
shared="~",
warn="!",
arrow=">",
branch="|-",
last="`-",
pipe="|",
)
TIERS = {"nerd": NERD, "unicode": UNICODE, "ascii": ASCII}
@functools.lru_cache(maxsize=1)
def has_nerd_font() -> bool:
"""Whether any Nerd Font family is installed on *this* host.
A proxy, not proof: it cannot see the font at the far end of an SSH session,
which is why the ssh/tmux checks below take precedence.
"""
try:
out = subprocess.run(
["fc-list"], capture_output=True, text=True, errors="replace", timeout=5, check=False
)
except (OSError, subprocess.SubprocessError):
return False
return "nerd font" in out.stdout.lower()
def detect(
env: dict[str, str] | None = None,
*,
is_tty: bool | None = None,
nerd_font: bool | None = None,
override: str | None = None,
) -> tuple[str, str]:
"""Pick an icon tier. Returns (tier, reason) so --debug can explain itself."""
env = os.environ if env is None else env
is_tty = sys.stdout.isatty() if is_tty is None else is_tty
nerd_font = has_nerd_font() if nerd_font is None else nerd_font
override = override or env.get("UPDATER_ICONS") or None
if override:
if override not in TIERS:
raise ValueError(f"UPDATER_ICONS={override!r}; expected one of {list(TIERS)}")
return override, "explicit override"
if env.get("NO_ICONS"):
return "ascii", "NO_ICONS set"
if not is_tty or env.get("TERM", "") in ("", "dumb"):
return "ascii", "not an interactive terminal"
enc = (getattr(sys.stdout, "encoding", None) or "").lower()
if "utf" not in enc:
return "ascii", f"stdout encoding is {enc or 'unknown'}"
# PUA codepoints are the first thing to break over a link or a multiplexer,
# and neither can tell us what the far-end font is.
if env.get("SSH_CONNECTION") or env.get("SSH_TTY") or env.get("SSH_CLIENT"):
return "unicode", "remote session (ssh)"
if env.get("TMUX") or env.get("STY"):
return "unicode", "multiplexer (tmux/screen)"
if not nerd_font:
return "unicode", "no Nerd Font installed"
return "nerd", "local terminal with Nerd Font installed"
def glyphs(tier: str) -> Glyphs:
return TIERS[tier]