"""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]