From 806a31054bce6ec4faaeff8801c8f611423c905d Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 24 Sep 2026 05:31:30 +0000 Subject: [PATCH 1/2] Icons: OpenIcon built in and on by default, icon("mail") draws the best glyph icon(name) returns the Nerd Font glyph, the Unicode symbol or the ASCII spelling of an OpenIcon icon, whichever this terminal can draw: the app's setIconMode, then OPENICON_GLYPHS / HQTUI_ICONS, then NERD_FONT=1, then the existing Unicode detection. A Nerd Font is never assumed. Aliases resolve, a missing Nerd glyph falls back to Unicode, unknown names draw nothing, and useIconPack(iconPackFrom(json)) swaps in another set. 370 icons, generated by scripts/generate-icons.ts from github.com/profullstack/openicon into the TypeScript library and the Go, Python and Rust ports, each with the same API and tests. Docs section on hqtui.com/docs#icons and in the README. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/web/app/docs/page.tsx | 38 ++ packages/hqtui/README.md | 22 + packages/hqtui/scripts/generate-icons.ts | 150 +++++++ packages/hqtui/src/icons-data.ts | 538 ++++++++++++++++++++++ packages/hqtui/src/icons.ts | 124 ++++++ packages/hqtui/src/index.ts | 6 + packages/hqtui/test/icons.test.ts | 78 ++++ ports/go/icons.go | 119 +++++ ports/go/icons_data.go | 541 +++++++++++++++++++++++ ports/go/icons_test.go | 78 ++++ ports/python/hqtui/__init__.py | 2 + ports/python/hqtui/icons.py | 79 ++++ ports/python/hqtui/icons_data.py | 538 ++++++++++++++++++++++ ports/python/tests/test_icons.py | 42 ++ ports/rust/src/capabilities.rs | 2 +- ports/rust/src/icons.rs | 149 +++++++ ports/rust/src/icons_data.rs | 541 +++++++++++++++++++++++ ports/rust/src/lib.rs | 3 + 18 files changed, 3049 insertions(+), 1 deletion(-) create mode 100644 packages/hqtui/scripts/generate-icons.ts create mode 100644 packages/hqtui/src/icons-data.ts create mode 100644 packages/hqtui/src/icons.ts create mode 100644 packages/hqtui/test/icons.test.ts create mode 100644 ports/go/icons.go create mode 100644 ports/go/icons_data.go create mode 100644 ports/go/icons_test.go create mode 100644 ports/python/hqtui/icons.py create mode 100644 ports/python/hqtui/icons_data.py create mode 100644 ports/python/tests/test_icons.py create mode 100644 ports/rust/src/icons.rs create mode 100644 ports/rust/src/icons_data.rs diff --git a/apps/web/app/docs/page.tsx b/apps/web/app/docs/page.tsx index 85a2ee6..2eb5b06 100644 --- a/apps/web/app/docs/page.tsx +++ b/apps/web/app/docs/page.tsx @@ -22,6 +22,7 @@ const SECTIONS = [ { id: "widgets", label: "Widgets" }, { id: "graphics", label: "Graphics" }, { id: "themes", label: "Themes" }, + { id: "icons", label: "Icons" }, { id: "input", label: "Input" }, { id: "testing", label: "Testing" }, { id: "escape-hatches", label: "Escape hatches" }, @@ -323,6 +324,43 @@ const app = await createApp({ theme: brand }); app.setTheme(themes.nord); // switch at runtime`} /> +

Icons

+

+ The OpenIcon pack is built in and + on by default: 370 icons, from mail and{" "} + git-branch to{" "} + github and{" "} + bluesky. Each has three glyphs, and{" "} + icon() returns the best one this terminal can draw: a + Nerd Font glyph, a Unicode symbol, or ASCII. Aliases work too, so{" "} + icon("email") is{" "} + icon("mail"). +

+ +

+ A Nerd Font cannot be detected from inside a terminal, so it is never assumed. Set{" "} + NERD_FONT=1, or{" "} + OPENICON_GLYPHS=nerd|unicode|ascii, or call{" "} + setIconMode(). Otherwise you get Unicode where the + terminal draws it and ASCII where it does not. An icon Nerd Fonts has no glyph for falls back to Unicode, + and an unknown name draws nothing. Swap in another OpenIcon set with{" "} + useIconPack(iconPackFrom(json)). The Rust, Go and Python + ports carry the same table: icon("mail"),{" "} + hqtui.Icon("mail"),{" "} + hqtui.icon("mail"). +

+

Input

Keys arrive normalized — "ctrl+c",{" "} diff --git a/packages/hqtui/README.md b/packages/hqtui/README.md index f557540..67666e5 100644 --- a/packages/hqtui/README.md +++ b/packages/hqtui/README.md @@ -112,6 +112,28 @@ activity, sessions, services and the full widget catalogue. | **Input** | normalized keys with modifiers, SGR mouse (click, drag, scroll, move), bracketed paste, focus events, Tab focus traversal | | **Testing** | headless renderer: `renderToText`, `renderToScreen`, `renderToAnsi`, `renderToHtml` — no TTY required | +## Icons + +The [OpenIcon](https://logicsrc.com/openicon) pack is built in and on by +default: 370 icons, each with a Nerd Font glyph, a Unicode symbol and an ASCII +spelling. `icon()` returns the best one this terminal can draw. + +```ts +import { icon, setIconMode } from "@profullstack/hqtui"; + +ui.text(`${icon("mail")} Inbox ${icon("git-branch")} main`); +// 󰇰 Inbox main with a Nerd Font +// ✉ Inbox ⎇ main in a UTF-8 terminal +// @ Inbox Y main anywhere else +``` + +A Nerd Font is never assumed, because it cannot be detected from inside the +terminal: set `NERD_FONT=1`, `OPENICON_GLYPHS=nerd|unicode|ascii`, or call +`setIconMode()`. Aliases resolve (`icon("email")`), an icon Nerd Fonts lacks +falls back to Unicode, and an unknown name draws nothing. `useIconPack()` swaps +in any other OpenIcon set. The table is generated from the set by +`scripts/generate-icons.ts`, for this library and the Rust, Go and Python ports. + ## Testing your TUI Terminal apps are usually untestable. Here they are not: diff --git a/packages/hqtui/scripts/generate-icons.ts b/packages/hqtui/scripts/generate-icons.ts new file mode 100644 index 0000000..75fd8cd --- /dev/null +++ b/packages/hqtui/scripts/generate-icons.ts @@ -0,0 +1,150 @@ +/** + * Regenerate the built-in icon pack from an OpenIcon set. + * + * node packages/hqtui/scripts/generate-icons.ts [openicon.json | URL] + * + * Defaults to the reference set, github.com/profullstack/openicon. Writes the + * same table for the TypeScript library and the Go, Python and Rust ports, so + * `icon("mail")` is the same glyph in every language. Only the terminal + * glyphs are kept (Nerd Font, Unicode, ASCII) plus aliases; the SVGs are for + * other surfaces and never ship in a terminal library. + */ + +import { spawnSync } from "node:child_process"; +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const DEFAULT = "https://raw.githubusercontent.com/profullstack/openicon/main/openicon.json"; +const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); + +interface Entry { + key: string; + aliases?: string[]; + tui: { nerd?: string; unicode: string; ascii: string }; +} +interface Set { + openicon: string; + name: string; + version: string; + icons: Entry[]; +} + +/** Byte order, not locale order: the Rust port binary-searches these tables. */ +const byteOrder = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0); + +const source = process.argv[2] ?? DEFAULT; +const set: Set = source.startsWith("http") + ? await (await fetch(source)).json() + : JSON.parse(readFileSync(source, "utf8")); + +const rows = set.icons + .map((i) => [i.key, i.tui.nerd ?? "", i.tui.unicode, i.tui.ascii] as const) + .sort((a, b) => byteOrder(a[0], b[0])); +const aliases = set.icons + .flatMap((i) => (i.aliases ?? []).map((a) => [a, i.key] as const)) + .sort((a, b) => byteOrder(a[0], b[0])); +const stamp = `${set.name} ${set.version} (OpenIcon ${set.openicon}), ${rows.length} icons`; + +/** A string literal every one of the four languages reads the same way. */ +const lit = (s: string) => + `"${[...s] + .map((ch) => { + const cp = ch.codePointAt(0)!; + if (ch === '"' || ch === "\\") return `\\${ch}`; + return cp >= 0x20 && cp < 0x7f ? ch : null; + }) + .map((out, i) => out ?? escapeFor([...s][i]!)) + .join("")}"`; +let escapeFor = (ch: string) => `\\u{${ch.codePointAt(0)!.toString(16)}}`; + +function write(rel: string, text: string) { + writeFileSync(join(root, rel), text); + console.log(`wrote ${rel}`); +} + +// TypeScript: \u{...} escapes. +escapeFor = (ch) => `\\u{${ch.codePointAt(0)!.toString(16)}}`; +write( + "packages/hqtui/src/icons-data.ts", + `// Generated by scripts/generate-icons.ts from ${stamp}. Do not edit. +// [key, nerd, unicode, ascii]; an empty nerd means Nerd Fonts has no glyph for it. + +export const OPENICON_VERSION = ${lit(set.version)}; + +export const OPENICON_GLYPHS: ReadonlyArray = [ +${rows.map((r) => ` [${r.map(lit).join(", ")}],`).join("\n")} +]; + +export const OPENICON_ALIASES: ReadonlyArray = [ +${aliases.map((a) => ` [${a.map(lit).join(", ")}],`).join("\n")} +]; +`, +); + +// Go: \U00XXXXXX escapes. +escapeFor = (ch) => `\\U${ch.codePointAt(0)!.toString(16).padStart(8, "0")}`; +write( + "ports/go/icons_data.go", + `// Code generated by packages/hqtui/scripts/generate-icons.ts from ${stamp}. DO NOT EDIT. + +package hqtui + +// OpenIconVersion is the OpenIcon set the built-in pack was generated from. +const OpenIconVersion = ${lit(set.version)} + +// {key, nerd, unicode, ascii}; an empty nerd means Nerd Fonts has no glyph for it. +var openIconGlyphs = [][4]string{ +${rows.map((r) => `\t{${r.map(lit).join(", ")}},`).join("\n")} +} + +var openIconAliases = map[string]string{ +${aliases.map(([a, k]) => `\t${lit(a)}: ${lit(k)},`).join("\n")} +} +`, +); + +// gofmt aligns the alias map; run it when Go is installed, so the file is +// already in the shape CI's gofmt check expects. +spawnSync("gofmt", ["-w", join(root, "ports/go/icons_data.go")], { stdio: "ignore" }); + +// Python: \UXXXXXXXX escapes. +escapeFor = (ch) => `\\U${ch.codePointAt(0)!.toString(16).padStart(8, "0")}`; +write( + "ports/python/hqtui/icons_data.py", + `# Generated by packages/hqtui/scripts/generate-icons.ts from ${stamp}. Do not edit. +# (key, nerd, unicode, ascii); an empty nerd means Nerd Fonts has no glyph for it. + +OPENICON_VERSION = ${lit(set.version)} + +OPENICON_GLYPHS = ( +${rows.map((r) => ` (${r.map(lit).join(", ")}),`).join("\n")} +) + +OPENICON_ALIASES = { +${aliases.map(([a, k]) => ` ${lit(a)}: ${lit(k)},`).join("\n")} +} +`, +); + +// Rust: \u{...} escapes. +escapeFor = (ch) => `\\u{${ch.codePointAt(0)!.toString(16)}}`; +write( + "ports/rust/src/icons_data.rs", + `// Generated by packages/hqtui/scripts/generate-icons.ts from ${stamp}. Do not edit. +// (key, nerd, unicode, ascii); an empty nerd means Nerd Fonts has no glyph for it. + +/// The OpenIcon set the built-in pack was generated from. +pub const OPENICON_VERSION: &str = ${lit(set.version)}; + +/// Sorted by key, so lookups can binary-search. +pub static OPENICON_GLYPHS: &[(&str, &str, &str, &str)] = &[ +${rows.map((r) => ` (${r.map(lit).join(", ")}),`).join("\n")} +]; + +/// Sorted by alias. +pub static OPENICON_ALIASES: &[(&str, &str)] = &[ +${aliases.map((a) => ` (${a.map(lit).join(", ")}),`).join("\n")} +]; +`, +); diff --git a/packages/hqtui/src/icons-data.ts b/packages/hqtui/src/icons-data.ts new file mode 100644 index 0000000..63f10d3 --- /dev/null +++ b/packages/hqtui/src/icons-data.ts @@ -0,0 +1,538 @@ +// Generated by scripts/generate-icons.ts from OpenIcon 2026-09-24 (OpenIcon 0.1), 370 icons. Do not edit. +// [key, nerd, unicode, ascii]; an empty nerd means Nerd Fonts has no glyph for it. + +export const OPENICON_VERSION = "2026-09-24"; + +export const OPENICON_GLYPHS: ReadonlyArray = [ + ["accessibility", "\u{f02e6}", "\u{267f}", "a11y"], + ["activity", "\u{f0430}", "\u{1f4c8}", "/\\/"], + ["add", "\u{f0415}", "+", "+"], + ["alarm", "\u{f0020}", "\u{23f0}", "(!)"], + ["align-center", "\u{f0260}", "\u{2261}", "="], + ["align-left", "\u{f0262}", "\u{2af7}", "|="], + ["align-right", "\u{f0263}", "\u{2af8}", "=|"], + ["amazon", "\u{f270}", "\u{24d0}", "amz"], + ["anchor", "\u{f0031}", "\u{2693}", "t"], + ["android", "\u{f0032}", "\u{1f916}", "and"], + ["anthropic", "", "\u{24b6}", "ant"], + ["api", "\u{f109b}", "\u{2699}", "api"], + ["apple", "\u{f0035}", "\u{1f34e}", "mac"], + ["apple-music", "\u{f2eb}", "\u{1f3b5}", "am"], + ["archive", "\u{f120e}", "\u{1f5c4}", "[_]"], + ["arrow-down", "\u{f0045}", "\u{2193}", "v"], + ["arrow-left", "\u{f004d}", "\u{2190}", "<-"], + ["arrow-right", "\u{f0054}", "\u{2192}", "->"], + ["arrow-up", "\u{f005d}", "\u{2191}", "^"], + ["arrow-up-right", "\u{f005c}", "\u{2197}", "/^"], + ["at", "\u{f0065}", "@", "@"], + ["award", "\u{f1326}", "\u{1f3c5}", "(*)"], + ["bag", "\u{f11d5}", "\u{1f6cd}", "[u]"], + ["ban", "\u{f073a}", "\u{1f6ab}", "(/)"], + ["bandcamp", "\u{f2d5}", "\u{25e2}", "bc"], + ["barcode", "\u{f0071}", "\u{25a5}", "|||"], + ["battery", "\u{f008e}", "\u{1f50b}", "[=="], + ["battery-charging", "\u{f0084}", "\u{1f50c}", "[=~"], + ["behance", "\u{f1b4}", "B\u{113}", "be"], + ["bell", "\u{f009c}", "\u{1f514}", "(!)"], + ["bell-off", "\u{f0a91}", "\u{1f515}", "(x)"], + ["bitbucket", "\u{f00a8}", "\u{1faa3}", "bb"], + ["bitcoin", "\u{f0813}", "\u{20bf}", "btc"], + ["bluesky", "\u{f1589}", "\u{1f98b}", "bsky"], + ["bluetooth", "\u{f00af}", "\u{16d2}", "B"], + ["bold", "\u{f0264}", "\u{1d401}", "B"], + ["book", "\u{f0b64}", "\u{1f4d5}", "[B]"], + ["book-open", "\u{f0b63}", "\u{1f4d6}", "[]"], + ["bookmark", "\u{f00c3}", "\u{1f516}", "[]>"], + ["braces", "\u{f0169}", "{}", "{}"], + ["brave", "\u{f0499}", "\u{1f981}", "brv"], + ["briefcase", "\u{f0814}", "\u{1f4bc}", "[b]"], + ["brush", "\u{f00e3}", "\u{1f58c}", "/~"], + ["bug", "\u{f0a30}", "\u{1f41b}", "bug"], + ["building", "\u{f151f}", "\u{1f3e2}", "[#]"], + ["bun", "\u{e76f}", "\u{1f95f}", "bun"], + ["buy-me-a-coffee", "\u{f0176}", "\u{2615}", "bmc"], + ["calendar", "\u{f0b66}", "\u{1f4c5}", "[=]"], + ["calendar-check", "\u{f0c44}", "\u{1f4c5}", "[v]"], + ["calendar-plus", "\u{f00f3}", "\u{1f4c5}", "[+]"], + ["camera", "\u{f0d5d}", "\u{1f4f7}", "[o]"], + ["cart", "\u{f0111}", "\u{1f6d2}", "\\_/"], + ["cast", "\u{f0118}", "\u{1f4e1}", "))"], + ["cell-signal", "\u{f04a2}", "\u{1f4f6}", ".:|"], + ["chat", "\u{f0ede}", "\u{1f4ac}", "()"], + ["chat-dots", "\u{f12ca}", "\u{1f4ac}", "(..)"], + ["check", "\u{f012c}", "\u{2713}", "v"], + ["check-circle", "\u{f05e1}", "\u{2705}", "(v)"], + ["checkbox", "\u{f0135}", "\u{2611}", "[x]"], + ["checkbox-empty", "\u{f0131}", "\u{2610}", "[ ]"], + ["chevron-down", "\u{f0140}", "\u{2304}", "v"], + ["chevron-left", "\u{f0141}", "\u{2039}", "<"], + ["chevron-right", "\u{f0142}", "\u{203a}", ">"], + ["chevron-up", "\u{f0143}", "\u{2303}", "^"], + ["chevrons-left", "\u{f013d}", "\u{ab}", "<<"], + ["chevrons-right", "\u{f013e}", "\u{bb}", ">>"], + ["chrome", "\u{f02af}", "\u{25c9}", "chr"], + ["claude", "", "\u{2733}", "cl"], + ["clipboard", "\u{f014c}", "\u{1f4cb}", "[=]"], + ["clipboard-check", "\u{f08a8}", "\u{1f4cb}", "[v]"], + ["clock", "\u{f0150}", "\u{1f552}", "(t)"], + ["close", "\u{f0156}", "\u{2715}", "x"], + ["cloud", "\u{f0163}", "\u{2601}", "(~)"], + ["cloud-download", "\u{f0b7d}", "\u{2601}", "(v)"], + ["cloud-upload", "\u{f0b7e}", "\u{2601}", "(^)"], + ["cloudflare", "\u{e792}", "\u{2601}", "cf"], + ["code", "\u{f0174}", "\u{27e8}\u{27e9}", ""], + ["codeberg", "\u{f330}", "\u{26f0}", "cb"], + ["codepen", "\u{f0175}", "\u{2b21}", "cpn"], + ["coffee", "\u{f06ca}", "\u{2615}", "c[_]"], + ["coins", "\u{f1890}", "\u{1fa99}", "(o)"], + ["command", "\u{f0633}", "\u{2318}", "cmd"], + ["compass", "\u{f018c}", "\u{1f9ed}", "(N)"], + ["contact", "\u{f0dab}", "\u{1f4c7}", "[@]"], + ["container", "\u{f01a7}", "\u{1f4e6}", "[c]"], + ["copy", "\u{f018f}", "\u{29c9}", "cp"], + ["cpu", "\u{f061a}", "\u{1f532}", "[#]"], + ["credit-card", "\u{f019b}", "\u{1f4b3}", "[=]"], + ["crop", "\u{f019e}", "\u{2317}", "[_"], + ["crosshair", "\u{f01a3}", "\u{2316}", "-+-"], + ["cut", "\u{f0190}", "\u{2702}", "8<"], + ["database", "\u{f1632}", "\u{1f6e2}", "[=]"], + ["debian", "\u{f08da}", "\u{1f300}", "deb"], + ["delete", "\u{f0a7a}", "\u{1f5d1}", "del"], + ["deno", "\u{e7c0}", "\u{1f995}", "deno"], + ["dev-to", "\u{eef4}", "DEV", "dev"], + ["discord", "\u{f066f}", "\u{1f3ae}", "dc"], + ["docker", "\u{f0868}", "\u{1f433}", "dkr"], + ["dollar", "\u{f01c1}", "$", "$"], + ["download", "\u{f01da}", "\u{2913}", "v_"], + ["dribbble", "\u{f17d}", "\u{1f3c0}", "drb"], + ["droplet", "\u{f0e0a}", "\u{1f4a7}", "o"], + ["ebay", "\u{edbe}", "\u{24d4}", "ebay"], + ["edit", "\u{f03eb}", "\u{270e}", "/e"], + ["element", "\u{f0628}", "\u{24ba}", "el"], + ["enter", "\u{f0311}", "\u{21b5}", "<-|"], + ["error", "\u{f05d6}", "\u{26d4}", "!!"], + ["ethereum", "\u{f086a}", "\u{39e}", "eth"], + ["etsy", "\u{f2d7}", "\u{24ba}", "etsy"], + ["external-link", "\u{f03cc}", "\u{2197}", "->]"], + ["eye", "\u{f06d0}", "\u{1f441}", "o"], + ["eye-off", "\u{f06d1}", "\u{1f648}", "-o-"], + ["facebook", "\u{f020c}", "\u{24d5}", "fb"], + ["farcaster", "", "\u{26e9}", "fc"], + ["fast-forward", "\u{f0211}", "\u{23e9}", ">>"], + ["figma", "\u{ef47}", "\u{1f3a8}", "fig"], + ["file", "\u{f0224}", "\u{1f4c4}", "[f]"], + ["file-code", "\u{f102b}", "\u{1f4c4}", "[<>]"], + ["file-plus", "\u{f0eed}", "\u{1f4c4}", "[+]"], + ["file-text", "\u{f09ee}", "\u{1f4c4}", "[t]"], + ["film", "\u{f0230}", "\u{1f39e}", "[#]"], + ["filter", "\u{f0233}", "\u{23f7}", "Y"], + ["fingerprint", "\u{f0237}", "\u{1fac6}", "(@)"], + ["firefox", "\u{f0239}", "\u{1f98a}", "ff"], + ["flag", "\u{f023d}", "\u{2691}", "|>"], + ["flask", "\u{f0096}", "\u{2697}", "/_\\"], + ["folder", "\u{f0256}", "\u{1f4c1}", "[d]"], + ["folder-open", "\u{f0dcf}", "\u{1f4c2}", "[d]"], + ["folder-plus", "\u{f0b9d}", "\u{1f4c1}", "[d+]"], + ["forward", "\u{f0496}", "\u{21aa}", "->"], + ["frown", "\u{f01f8}", "\u{1f641}", ":("], + ["gauge", "\u{f029a}", "\u{23f2}", "(/)"], + ["ghost", "\u{f02a0}", "\u{1f47b}", "gst"], + ["gift", "\u{f02a1}", "\u{1f381}", "[+]"], + ["git-branch", "\u{f418}", "\u{2387}", "Y"], + ["git-commit", "\u{f417}", "\u{22b8}", "-o-"], + ["git-merge", "\u{f419}", "\u{2442}", ">-"], + ["git-pull-request", "\u{f407}", "\u{21c4}", "PR"], + ["gitea", "\u{f339}", "\u{1f375}", "gt"], + ["github", "\u{f02a4}", "\u{1f419}", "gh"], + ["gitlab", "\u{f0ba0}", "\u{1f98a}", "gl"], + ["globe", "\u{f059f}", "\u{1f310}", "(#)"], + ["gmail", "\u{f02ab}", "\u{2709}", "gm"], + ["go", "\u{f07d3}", "\u{1f439}", "go"], + ["google", "\u{f02ad}", "\u{24bc}", "g"], + ["google-meet", "\u{f0bdc}", "\u{1f4f9}", "meet"], + ["grid", "\u{f11d9}", "\u{25a6}", "::"], + ["grip", "\u{f01db}", "\u{283f}", "::"], + ["hacker-news", "\u{f1d4}", "\u{24ce}", "hn"], + ["hard-drive", "\u{f02ca}", "\u{1f5b4}", "[o]"], + ["hash", "\u{f0423}", "#", "#"], + ["hashnode", "", "#", "hn#"], + ["heading", "\u{f0274}", "H", "H"], + ["headphones", "\u{f02cb}", "\u{1f3a7}", "hp"], + ["heart", "\u{f02d5}", "\u{2665}", "<3"], + ["help", "\u{f0625}", "\u{2753}", "?"], + ["history", "\u{f02da}", "\u{1f558}", "<(t)"], + ["home", "\u{f06a1}", "\u{1f3e0}", "~"], + ["hourglass", "\u{f051f}", "\u{23f3}", "8"], + ["id-card", "\u{f0dab}", "\u{1faaa}", "[id]"], + ["image", "\u{f0976}", "\u{1f5bc}", "[^]"], + ["image-plus", "\u{f087c}", "\u{1f5bc}", "[+]"], + ["inbox", "\u{f0687}", "\u{1f4e5}", "[v]"], + ["info", "\u{f02fd}", "\u{2139}", "i"], + ["instagram", "\u{f02fe}", "\u{1f4f7}", "ig"], + ["italic", "\u{f0277}", "\u{1d43c}", "/"], + ["javascript", "\u{f031e}", "JS", "js"], + ["jira", "\u{f0303}", "\u{25c8}", "jira"], + ["key", "\u{f0dd6}", "\u{1f511}", "o-"], + ["keybase", "\u{edbf}", "\u{1f511}", "kb"], + ["keyboard", "\u{f097b}", "\u{2328}", "[kb]"], + ["kick", "", "\u{24c0}", "kick"], + ["ko-fi", "\u{f0176}", "\u{2615}", "kofi"], + ["kubernetes", "\u{f10fe}", "\u{2638}", "k8s"], + ["laptop", "\u{f0322}", "\u{1f4bb}", "[_]"], + ["layers", "\u{f09fe}", "\u{2630}", "="], + ["leaf", "\u{f032a}", "\u{1f343}", "~"], + ["lemmy", "", "\u{1f42d}", "lmy"], + ["lightbulb", "\u{f0336}", "\u{1f4a1}", "i"], + ["line", "\u{f2fb}", "\u{1f4ac}", "line"], + ["link", "\u{f0339}", "\u{1f517}", "~"], + ["linkedin", "\u{f033b}", "\u{24d8}", "in"], + ["linux", "\u{f033d}", "\u{1f427}", "lnx"], + ["list", "\u{f0279}", "\u{2637}", "-="], + ["list-bullet", "\u{f0279}", "\u{2022}", "*"], + ["list-ordered", "\u{f027b}", "\u{2488}", "1."], + ["loader", "\u{f0772}", "\u{25cc}", "..."], + ["lock", "\u{f0341}", "\u{1f512}", "[#]"], + ["log-in", "\u{f0342}", "\u{21e5}", "->|"], + ["log-out", "\u{f0343}", "\u{21e4}", "|->"], + ["mail", "\u{f01f0}", "\u{2709}", "@"], + ["mail-open", "\u{f05ef}", "\u{1f4e8}", "@"], + ["map", "\u{f0982}", "\u{1f5fa}", "[#]"], + ["map-pin", "\u{f07d9}", "\u{1f4cd}", "@"], + ["mastodon", "\u{f0ad1}", "\u{1f418}", "mdn"], + ["matrix", "\u{f0628}", "\u{24c2}", "[m]"], + ["mattermost", "", "\u{24c2}", "mm"], + ["maximize", "\u{f0293}", "\u{26f6}", "[ ]"], + ["medium", "\u{f23a}", "\u{24c2}", "md"], + ["megaphone", "\u{f0b23}", "\u{1f4e3}", "<|"], + ["menu", "\u{f035c}", "\u{2630}", "="], + ["messenger", "\u{f020e}", "\u{1f4ac}", "msg"], + ["mic", "\u{f036e}", "\u{1f3a4}", "mic"], + ["mic-off", "\u{f036d}", "\u{1f507}", "x-m"], + ["microsoft", "\u{f0372}", "\u{229e}", "ms"], + ["minimize", "\u{f0294}", "\u{22a1}", "]["], + ["minus", "\u{f0374}", "\u{2212}", "-"], + ["minus-circle", "\u{f0377}", "\u{2296}", "(-)"], + ["misskey", "", "\u{24c2}", "mk"], + ["monitor", "\u{f0379}", "\u{1f5a5}", "[ ]"], + ["moon", "\u{f0594}", "\u{1f319}", "C"], + ["more-horizontal", "\u{f01d8}", "\u{22ef}", "..."], + ["more-vertical", "\u{f01d9}", "\u{22ee}", ":"], + ["mouse", "\u{f037d}", "\u{1f5b1}", "(|)"], + ["move", "\u{f01be}", "\u{2725}", "+"], + ["music", "\u{f0387}", "\u{1f3b5}", "#"], + ["navigation", "\u{f18f1}", "\u{27b6}", ">"], + ["netlify", "\u{e83c}", "\u{25c6}", "ntl"], + ["newspaper", "\u{f1004}", "\u{1f4f0}", "[n]"], + ["nodejs", "\u{f0399}", "\u{2b22}", "node"], + ["notion", "\u{e848}", "\u{24c3}", "ntn"], + ["npm", "\u{f06f7}", "\u{1f4e6}", "npm"], + ["online", "\u{f0aa5}", "\u{1f7e2}", "(o)"], + ["openai", "", "\u{273a}", "oai"], + ["package", "\u{f03d7}", "\u{1f4e6}", "[#]"], + ["palette", "\u{f0e0c}", "\u{1f3a8}", "(:)"], + ["paperclip", "\u{f03e2}", "\u{1f4ce}", "0/"], + ["patreon", "\u{f0882}", "\u{24c5}", "pat"], + ["pause", "\u{f03e4}", "\u{23f8}", "||"], + ["paypal", "\u{f1ed}", "\u{24c5}", "pp"], + ["peertube", "", "\u{25b6}", "pt"], + ["pen-tool", "\u{f0d13}", "\u{2712}", "_/"], + ["percent", "\u{f03f0}", "%", "%"], + ["phone", "\u{f0df0}", "\u{260e}", "tel"], + ["phone-call", "\u{f1182}", "\u{1f4de}", "tel"], + ["phone-off", "\u{f11a6}", "\u{1f4f5}", "x-t"], + ["pin", "\u{f0931}", "\u{1f4cc}", "-|"], + ["pinterest", "\u{f0407}", "\u{1f4cc}", "pin"], + ["pixelfed", "", "\u{1f5bc}", "pxf"], + ["play", "\u{f040a}", "\u{25b6}", ">"], + ["plug", "\u{f1425}", "\u{1f50c}", "-["], + ["plus-circle", "\u{f0419}", "\u{2295}", "(+)"], + ["power", "\u{f0425}", "\u{23fb}", "(|)"], + ["print", "\u{f1786}", "\u{1f5a8}", "prn"], + ["product-hunt", "\u{f288}", "\u{24c5}", "ph"], + ["proton-mail", "\u{f01f1}", "\u{2709}", "pm"], + ["puzzle", "\u{f0a66}", "\u{1f9e9}", "[+]"], + ["python", "\u{f0320}", "\u{1f40d}", "py"], + ["qr-code", "\u{f0432}", "\u{25a6}", "[#]"], + ["quote", "\u{f0757}", "\u{275d}", "\""], + ["radio", "\u{f0003}", "\u{1f4fb}", "(o)"], + ["radio-off", "\u{f111}", "\u{25cb}", "( )"], + ["radio-on", "\u{f043e}", "\u{25c9}", "(*)"], + ["railway", "\u{e883}", "\u{1f686}", "rly"], + ["receipt", "\u{f0449}", "\u{1f9fe}", "[$]"], + ["reddit", "\u{f044d}", "\u{1f47d}", "rd"], + ["redo", "\u{f044e}", "\u{21b7}", "->"], + ["refresh", "\u{f0450}", "\u{27f3}", "@"], + ["repeat", "\u{f0456}", "\u{1f501}", "<->"], + ["reply", "\u{f045a}", "\u{21a9}", "<-"], + ["rewind", "\u{f045f}", "\u{23ea}", "<<"], + ["rocket", "\u{f14df}", "\u{1f680}", "^"], + ["rocketchat", "\u{ed20}", "\u{1f680}", "rc"], + ["rss", "\u{f046b}", "\u{1f4e1}", "rss"], + ["ruler", "\u{f046d}", "\u{1f4cf}", "|-|"], + ["rust", "\u{f1617}", "\u{1f980}", "rs"], + ["safari", "\u{f0039}", "\u{1f9ed}", "saf"], + ["save", "\u{f0818}", "\u{1f4be}", "[s]"], + ["search", "\u{f0349}", "\u{1f50d}", "?"], + ["send", "\u{f1165}", "\u{27a4}", ">>"], + ["server", "\u{f048b}", "\u{1f5a5}", "[:]"], + ["settings", "\u{f0493}", "\u{2699}", "*"], + ["share", "\u{f1514}", "\u{2934}", "<"], + ["share-out", "\u{f0b93}", "\u{21ea}", "^"], + ["shield", "\u{f0499}", "\u{1f6e1}", "[S]"], + ["shield-check", "\u{f0cc8}", "\u{1f6e1}", "[v]"], + ["shopify", "\u{f049a}", "\u{1f6cd}", "shp"], + ["shuffle", "\u{f049f}", "\u{1f500}", "><"], + ["sidebar", "\u{f10aa}", "\u{25a5}", "|="], + ["signal", "\u{f116d}", "\u{1f4ac}", "sig"], + ["skip-back", "\u{f04ae}", "\u{23ee}", "|<"], + ["skip-forward", "\u{f04ad}", "\u{23ed}", ">|"], + ["skype", "\u{f04af}", "\u{24c8}", "sky"], + ["slack", "\u{f04b1}", "#", "slk"], + ["sliders", "\u{f1542}", "\u{1f39a}", "=|="], + ["smartphone", "\u{f011c}", "\u{1f4f1}", "[.]"], + ["smile", "\u{f01f5}", "\u{1f642}", ":)"], + ["sms", "\u{f1170}", "\u{1f4ac}", "sms"], + ["snapchat", "\u{f04b6}", "\u{1f47b}", "snap"], + ["sort", "\u{f04ba}", "\u{21c5}", "^v"], + ["sort-asc", "\u{f04bc}", "\u{2191}", "a-z"], + ["sort-desc", "\u{f04bd}", "\u{2193}", "z-a"], + ["soundcloud", "\u{f04c0}", "\u{2601}", "sc"], + ["sourcehut", "", "\u{25ef}", "srht"], + ["sparkles", "\u{f0674}", "\u{2728}", "*+"], + ["spotify", "\u{f04c7}", "\u{1f3b5}", "spot"], + ["stack-overflow", "\u{f04cc}", "\u{1f4da}", "so"], + ["star", "\u{f04d2}", "\u{2605}", "*"], + ["stop", "\u{f04db}", "\u{23f9}", "[]"], + ["store", "\u{f10c1}", "\u{1f3ea}", "[S]"], + ["strikethrough", "\u{f0280}", "S\u{336}", "-s-"], + ["stripe", "\u{ed53}", "\u{24c8}", "str"], + ["substack", "\u{f0fb1}", "\u{2709}", "ss"], + ["sun", "\u{f05a8}", "\u{2600}", "*"], + ["supabase", "\u{e8b6}", "\u{26a1}", "sb"], + ["table", "\u{f04eb}", "\u{25a6}", "[#]"], + ["tablet", "\u{f04f6}", "\u{1f4f1}", "[..]"], + ["tag", "\u{f04fc}", "\u{1f3f7}", "#"], + ["target", "\u{f04fe}", "\u{1f3af}", "(o)"], + ["telegram", "\u{f2c6}", "\u{2708}", "tg"], + ["terminal", "\u{f018d}", "\u{2328}", ">_"], + ["theme", "\u{f050e}", "\u{25d0}", "(|)"], + ["thermometer", "\u{f050f}", "\u{1f321}", "|o"], + ["threads", "\u{f0065}", "@", "th"], + ["thumbs-down", "\u{f0512}", "\u{1f44e}", "-1"], + ["thumbs-up", "\u{f0514}", "\u{1f44d}", "+1"], + ["tiktok", "\u{f0387}", "\u{266a}", "tt"], + ["timer", "\u{f051b}", "\u{23f1}", "(:)"], + ["toggle-off", "\u{f0a19}", "\u{1f518}", "[o=]"], + ["toggle-on", "\u{f0521}", "\u{1f518}", "[=o]"], + ["tor", "\u{f371}", "\u{1f9c5}", "tor"], + ["translate", "\u{f05ca}", "\u{1f310}", "A/a"], + ["trello", "\u{f0532}", "\u{25a4}", "tr"], + ["trophy", "\u{f053a}", "\u{1f3c6}", "\\_/"], + ["truck", "\u{f129d}", "\u{1f69a}", "[=o"], + ["tumblr", "\u{f173}", "\u{24e3}", "tb"], + ["tv", "\u{f0502}", "\u{1f4fa}", "[_]"], + ["twitch", "\u{f0543}", "\u{1f4fa}", "ttv"], + ["type", "\u{f0284}", "T", "T"], + ["typescript", "\u{f06e6}", "TS", "ts"], + ["ubuntu", "\u{f0548}", "\u{25ce}", "ubu"], + ["umbrella", "\u{f054b}", "\u{2602}", "T"], + ["underline", "\u{f0287}", "U\u{332}", "_"], + ["undo", "\u{f054c}", "\u{21b6}", "<-"], + ["unlink", "\u{f033a}", "\u{26d3}", "~/"], + ["unlock", "\u{f0fc7}", "\u{1f513}", "[ ]"], + ["upload", "\u{f0552}", "\u{2912}", "^_"], + ["user", "\u{f0013}", "\u{1f464}", "@"], + ["user-check", "\u{f0be2}", "\u{1f464}", "@v"], + ["user-circle", "\u{f0b55}", "\u{1f464}", "(@)"], + ["user-minus", "\u{f0aec}", "\u{1f464}", "@-"], + ["user-plus", "\u{f0801}", "\u{1f464}", "@+"], + ["users", "\u{f000f}", "\u{1f465}", "@@"], + ["vercel", "\u{f0536}", "\u{25b2}", "vc"], + ["video", "\u{f0bdc}", "\u{1f4f9}", "[>"], + ["vimeo", "\u{f0577}", "\u{24e5}", "vm"], + ["voicemail", "\u{f057d}", "\u{2328}", "oo"], + ["volume", "\u{f057e}", "\u{1f50a}", "<))"], + ["volume-low", "\u{f0580}", "\u{1f509}", "<)"], + ["volume-off", "\u{f0581}", "\u{1f507}", ""], + ["wechat", "\u{f0611}", "\u{1f4ac}", "wx"], + ["whatsapp", "\u{f05a3}", "\u{1f4de}", "wa"], + ["wifi", "\u{f05a9}", "\u{1f4f6}", "((."], + ["wifi-off", "\u{f05aa}", "\u{1f4f5}", "x(("], + ["windows", "\u{f05b3}", "\u{229e}", "win"], + ["wordpress", "\u{f05b4}", "\u{24cc}", "wp"], + ["x", "\u{f0b05}", "\u{1d54f}", "x"], + ["x-circle", "\u{f015a}", "\u{274e}", "(x)"], + ["xmpp", "\u{f07ff}", "\u{1f4ac}", "xmpp"], + ["y-combinator", "\u{f23b}", "\u{24ce}", "yc"], + ["youtube", "\u{f05c3}", "\u{25b6}", "yt"], + ["zap", "\u{f140c}", "\u{26a1}", "/"], + ["zoom", "\u{f0567}", "\u{1f4f9}", "zm"], + ["zoom-in", "\u{f06ed}", "\u{1f50e}", "+?"], + ["zoom-out", "\u{f06ec}", "\u{1f50e}", "-?"], + ["zulip", "", "\u{24cf}", "zl"], +]; + +export const OPENICON_ALIASES: ReadonlyArray = [ + ["a11y", "accessibility"], + ["account", "user"], + ["add-user", "user-plus"], + ["address-book", "contact"], + ["ai", "sparkles"], + ["alarm-clock", "alarm"], + ["alert-circle", "error"], + ["alert-triangle", "warning"], + ["announce", "megaphone"], + ["at-sign", "at"], + ["attach", "paperclip"], + ["attachment", "paperclip"], + ["bin", "delete"], + ["biometric", "fingerprint"], + ["blocked", "ban"], + ["bolt", "zap"], + ["box", "package"], + ["bullhorn", "megaphone"], + ["bullseye", "target"], + ["call", "phone"], + ["card", "credit-card"], + ["caution", "warning"], + ["cellphone", "smartphone"], + ["check-square", "checkbox"], + ["chip", "cpu"], + ["cli", "terminal"], + ["cmd", "command"], + ["cog", "settings"], + ["color", "palette"], + ["comment", "chat"], + ["company", "building"], + ["computer", "laptop"], + ["console", "terminal"], + ["contrast", "theme"], + ["curly-braces", "braces"], + ["currency", "dollar"], + ["dark-mode", "moon"], + ["date", "calendar"], + ["day", "sun"], + ["db", "database"], + ["delivery", "truck"], + ["deploy", "rocket"], + ["desktop", "monitor"], + ["directory", "folder"], + ["discount", "percent"], + ["dislike", "thumbs-down"], + ["display", "monitor"], + ["document", "file"], + ["duplicate", "copy"], + ["ellipsis", "more-horizontal"], + ["email", "mail"], + ["envelope", "mail"], + ["exit-fullscreen", "minimize"], + ["experiment", "flask"], + ["extension", "puzzle"], + ["faq", "help"], + ["favorite", "star"], + ["find", "search"], + ["floppy", "save"], + ["forbidden", "ban"], + ["fullscreen", "maximize"], + ["gear", "settings"], + ["goal", "target"], + ["golang", "go"], + ["hamburger", "menu"], + ["happy", "smile"], + ["hashtag", "hash"], + ["hide", "eye-off"], + ["house", "home"], + ["i18n", "translate"], + ["idea", "lightbulb"], + ["identity", "id-card"], + ["information", "info"], + ["integration", "plug"], + ["invite", "user-plus"], + ["invoice", "receipt"], + ["job", "briefcase"], + ["json", "braces"], + ["kebab", "more-vertical"], + ["lab", "flask"], + ["label", "tag"], + ["language", "translate"], + ["light-mode", "sun"], + ["lightning", "zap"], + ["like", "thumbs-up"], + ["loading", "loader"], + ["location", "map-pin"], + ["login", "log-in"], + ["logout", "log-out"], + ["love", "heart"], + ["magic", "sparkles"], + ["magnify", "search"], + ["marker", "map-pin"], + ["mention", "at"], + ["merge-request", "git-pull-request"], + ["message", "chat"], + ["microphone", "mic"], + ["mobile", "smartphone"], + ["movie", "film"], + ["mute", "volume-off"], + ["night", "moon"], + ["node", "nodejs"], + ["notification", "bell"], + ["office", "building"], + ["open-in-new", "external-link"], + ["paper-plane", "send"], + ["payment", "credit-card"], + ["pencil", "edit"], + ["pending", "hourglass"], + ["people", "users"], + ["person", "user"], + ["photo", "image"], + ["photo-camera", "camera"], + ["picture", "image"], + ["plugin", "puzzle"], + ["plus", "add"], + ["preferences", "settings"], + ["present", "gift"], + ["printer", "print"], + ["prize", "trophy"], + ["processor", "cpu"], + ["profile", "user"], + ["pull-request", "git-pull-request"], + ["qr", "qr-code"], + ["question", "help"], + ["recent", "history"], + ["reception", "cell-signal"], + ["reload", "refresh"], + ["scissors", "cut"], + ["shell", "terminal"], + ["shipping", "truck"], + ["shopping-bag", "bag"], + ["shopping-cart", "cart"], + ["show", "eye"], + ["sign-in", "log-in"], + ["sign-out", "log-out"], + ["song", "music"], + ["sound", "volume"], + ["source-code", "code"], + ["speaker", "volume"], + ["spinner", "loader"], + ["square", "checkbox-empty"], + ["stopwatch", "timer"], + ["storefront", "store"], + ["sync", "refresh"], + ["team", "users"], + ["telephone", "phone"], + ["television", "tv"], + ["thumbtack", "pin"], + ["times", "close"], + ["trash", "delete"], + ["twitter", "x"], + ["usd", "dollar"], + ["view", "eye"], + ["web", "globe"], + ["work", "briefcase"], + ["world", "globe"], + ["x-mark", "close"], +]; diff --git a/packages/hqtui/src/icons.ts b/packages/hqtui/src/icons.ts new file mode 100644 index 0000000..1a94f57 --- /dev/null +++ b/packages/hqtui/src/icons.ts @@ -0,0 +1,124 @@ +/** + * Icons for terminals: `icon("mail")` is the best glyph this terminal can draw. + * + * import { icon } from "@profullstack/hqtui"; + * ui.text(`${icon("mail")} Inbox`); // 󰇰 Inbox, ✉ Inbox, or @ Inbox + * + * The built-in pack is OpenIcon (https://logicsrc.com/openicon), on by + * default: 370 icons, each with a Nerd Font glyph, a Unicode symbol and an + * ASCII spelling. Which of the three you get: + * + * 1. setIconMode("nerd" | "unicode" | "ascii"), if the app chose; + * 2. OPENICON_GLYPHS or HQTUI_ICONS in the environment; + * 3. NERD_FONT=1 means Nerd Font glyphs; + * 4. otherwise Unicode where the terminal draws it (the same detection the + * rest of hqtui uses), and ASCII where it does not. + * + * A Nerd Font is never assumed, because it cannot be detected from inside the + * terminal: the font is the emulator's business, and a Nerd codepoint in a + * plain font is a box. Say so with NERD_FONT=1 or setIconMode("nerd"). + * + * An icon Nerd Fonts has no glyph for falls back to its Unicode symbol, and an + * unknown name draws nothing rather than throwing mid-frame. + */ + +import { detectCapabilities } from "./capabilities.ts"; +import { OPENICON_ALIASES, OPENICON_GLYPHS, OPENICON_VERSION } from "./icons-data.ts"; + +export type IconMode = "nerd" | "unicode" | "ascii"; + +export interface IconGlyphs { + /** The Nerd Font character; empty when Nerd Fonts has none for this icon. */ + nerd: string; + unicode: string; + ascii: string; +} + +/** An icon pack: glyphs by key, plus aliases that resolve to a key. */ +export interface IconPack { + name: string; + version: string; + icons: ReadonlyMap; + aliases: ReadonlyMap; +} + +/** Build a pack from an OpenIcon `openicon.json`, for a set other than the built-in one. */ +export function iconPackFrom(set: { + name: string; + version: string; + icons: ReadonlyArray<{ key: string; aliases?: readonly string[]; tui: { nerd?: string; unicode: string; ascii: string } }>; +}): IconPack { + const icons = new Map(); + const aliases = new Map(); + for (const i of set.icons) { + icons.set(i.key, { nerd: i.tui.nerd ?? "", unicode: i.tui.unicode, ascii: i.tui.ascii }); + for (const a of i.aliases ?? []) aliases.set(a, i.key); + } + return { name: set.name, version: set.version, icons, aliases }; +} + +/** The built-in pack: OpenIcon. */ +export const openIcon: IconPack = { + name: "OpenIcon", + version: OPENICON_VERSION, + icons: new Map(OPENICON_GLYPHS.map(([key, nerd, unicode, ascii]) => [key, { nerd, unicode, ascii }])), + aliases: new Map(OPENICON_ALIASES), +}; + +let pack: IconPack = openIcon; +let chosen: IconMode | undefined; +let detected: IconMode | undefined; + +/** Swap the icon pack for the whole app. `useIconPack(openIcon)` restores the default. */ +export function useIconPack(next: IconPack): void { + pack = next; +} + +export function currentIconPack(): IconPack { + return pack; +} + +/** Pin the glyph family for the whole app, or pass undefined to detect again. */ +export function setIconMode(mode: IconMode | undefined): void { + chosen = mode; + detected = undefined; +} + +const isMode = (value: string | undefined): value is IconMode => + value === "nerd" || value === "unicode" || value === "ascii"; + +/** Which glyph family this environment gets, following the order at the top of this file. */ +export function iconMode(env: NodeJS.ProcessEnv = process.env): IconMode { + if (chosen) return chosen; + const named = env.OPENICON_GLYPHS || env.HQTUI_ICONS; + if (isMode(named)) return named; + if (env.NERD_FONT === "1" || env.NERD_FONTS === "1") return "nerd"; + return detectCapabilities({}, env).unicode ? "unicode" : "ascii"; +} + +/** The three glyphs of an icon, by key or alias, or undefined if the pack has no such icon. */ +export function iconGlyphs(name: string): IconGlyphs | undefined { + return pack.icons.get(name) ?? pack.icons.get(pack.aliases.get(name) ?? ""); +} + +export interface IconOptions { + /** Override the app-wide mode for this one call. */ + mode?: IconMode; +} + +/** + * The best glyph for an icon: `icon("mail")`, `icon("email")` (an alias), or + * `icon("github", { mode: "ascii" })`. Unknown names return "". + */ +export function icon(name: string, options: IconOptions = {}): string { + const glyphs = iconGlyphs(name); + if (!glyphs) return ""; + const mode = options.mode ?? chosen ?? (detected ??= iconMode()); + if (mode === "nerd") return glyphs.nerd || glyphs.unicode; + return mode === "unicode" ? glyphs.unicode : glyphs.ascii; +} + +/** Every key in the current pack, sorted. */ +export function iconNames(): string[] { + return [...pack.icons.keys()].sort(); +} diff --git a/packages/hqtui/src/index.ts b/packages/hqtui/src/index.ts index bddc7c5..a22cdf7 100644 --- a/packages/hqtui/src/index.ts +++ b/packages/hqtui/src/index.ts @@ -67,6 +67,12 @@ export { type Span, type SpanLine, type RichText, } from "./richtext.ts"; +// Icons: the OpenIcon pack, on by default +export { + icon, iconGlyphs, iconMode, setIconMode, useIconPack, currentIconPack, iconPackFrom, iconNames, openIcon, + type IconMode, type IconGlyphs, type IconPack, type IconOptions, +} from "./icons.ts"; + // Graphics export { BrailleCanvas } from "./graphics/braille.ts"; export { diff --git a/packages/hqtui/test/icons.test.ts b/packages/hqtui/test/icons.test.ts new file mode 100644 index 0000000..f893a52 --- /dev/null +++ b/packages/hqtui/test/icons.test.ts @@ -0,0 +1,78 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + icon, iconGlyphs, iconMode, iconNames, iconPackFrom, openIcon, setIconMode, useIconPack, +} from "../src/icons.ts"; +import { OPENICON_GLYPHS } from "../src/icons-data.ts"; +import { renderToText } from "../src/testing.ts"; + +test("the OpenIcon pack is built in and on by default", () => { + assert.ok(iconNames().length >= 300); + for (const key of ["mail", "phone", "link", "search", "settings", "terminal", "git-branch", "github", "x", "slack"]) { + assert.ok(iconGlyphs(key), key); + } +}); + +test("each mode draws its own glyph, and aliases find the same icon", () => { + assert.equal(icon("mail", { mode: "nerd" }), "\u{f01f0}"); + assert.equal(icon("mail", { mode: "unicode" }), "✉"); + assert.equal(icon("mail", { mode: "ascii" }), "@"); + assert.equal(icon("email", { mode: "ascii" }), "@"); + assert.equal(icon("twitter", { mode: "ascii" }), "x"); +}); + +test("an icon Nerd Fonts lacks falls back to Unicode, and an unknown name draws nothing", () => { + const noNerd = OPENICON_GLYPHS.find(([, nerd]) => nerd === ""); + assert.ok(noNerd, "the set has icons without a Nerd glyph"); + assert.equal(icon(noNerd[0], { mode: "nerd" }), noNerd[2]); + assert.equal(icon("no-such-icon"), ""); +}); + +test("the mode comes from the app, then the environment, then the terminal", () => { + assert.equal(iconMode({ OPENICON_GLYPHS: "ascii", NERD_FONT: "1", LANG: "en_US.UTF-8" }), "ascii"); + assert.equal(iconMode({ HQTUI_ICONS: "nerd" }), "nerd"); + assert.equal(iconMode({ NERD_FONT: "1" }), "nerd"); + assert.equal(iconMode({ LANG: "en_US.UTF-8", TERM: "xterm-256color" }), "unicode"); + assert.equal(iconMode({ TERM: "dumb" }), "ascii"); + setIconMode("ascii"); + try { + assert.equal(iconMode({ NERD_FONT: "1" }), "ascii"); + assert.equal(icon("phone"), "tel"); + } finally { + setIconMode(undefined); + } +}); + +test("a pack can be swapped for another OpenIcon set", () => { + const custom = iconPackFrom({ + name: "Tiny", version: "1", + icons: [{ key: "mail", aliases: ["post"], tui: { unicode: "📮", ascii: "M" } }], + }); + useIconPack(custom); + try { + assert.equal(icon("post", { mode: "ascii" }), "M"); + assert.equal(icon("mail", { mode: "nerd" }), "📮"); + assert.equal(icon("phone"), ""); + } finally { + useIconPack(openIcon); + } +}); + +test("icons render inside widgets like any text", () => { + setIconMode("ascii"); + try { + const text = renderToText(({ ui }) => { ui.text(`${icon("mail")} Inbox ${icon("terminal")}`); }, { width: 20, height: 1 }); + assert.ok(text.startsWith("@ Inbox >_"), text); + } finally { + setIconMode(undefined); + } +}); + +test("the generated table is sorted and complete", () => { + const keys = OPENICON_GLYPHS.map(([k]) => k); + assert.deepEqual(keys, [...keys].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))); + for (const [key, , unicode, ascii] of OPENICON_GLYPHS) { + assert.ok(unicode.length > 0, key); + assert.match(ascii, /^[\x21-\x7e]([\x20-\x7e]{0,2}[\x21-\x7e])?$/, key); + } +}); diff --git a/ports/go/icons.go b/ports/go/icons.go new file mode 100644 index 0000000..4af47c4 --- /dev/null +++ b/ports/go/icons.go @@ -0,0 +1,119 @@ +package hqtui + +// Icons for terminals: Icon("mail") is the best glyph this terminal can draw. +// +// The built-in pack is OpenIcon (https://logicsrc.com/openicon), on by +// default, generated into icons_data.go from the same set as the TypeScript +// reference, so an icon is the same glyph in every port. Which of its three +// glyphs you get: SetIconMode if the app chose; OPENICON_GLYPHS or +// HQTUI_ICONS; NERD_FONT=1 for Nerd Font glyphs; otherwise Unicode where the +// terminal draws it and ASCII where it does not. A Nerd Font is never assumed: +// it cannot be detected from inside the terminal. + +import "sync" + +// IconMode picks one of an icon's three glyphs. +type IconMode string + +const ( + IconNerd IconMode = "nerd" + IconUnicode IconMode = "unicode" + IconASCII IconMode = "ascii" +) + +// IconGlyphs are the three spellings of one icon. Nerd is empty when Nerd +// Fonts has no glyph for it. +type IconGlyphs struct { + Nerd, Unicode, ASCII string +} + +var ( + iconMu sync.RWMutex + iconChosen IconMode + iconByKey map[string]IconGlyphs + iconsLoaded sync.Once +) + +func loadIcons() { + iconsLoaded.Do(func() { + iconByKey = make(map[string]IconGlyphs, len(openIconGlyphs)) + for _, row := range openIconGlyphs { + iconByKey[row[0]] = IconGlyphs{Nerd: row[1], Unicode: row[2], ASCII: row[3]} + } + }) +} + +// SetIconMode pins the glyph family for the whole app; "" detects again. +func SetIconMode(mode IconMode) { + iconMu.Lock() + iconChosen = mode + iconMu.Unlock() +} + +// IconModeIn is the glyph family an environment gets, in the order above. +func IconModeIn(env Env, windows bool) IconMode { + iconMu.RLock() + chosen := iconChosen + iconMu.RUnlock() + if chosen != "" { + return chosen + } + named := env.get("OPENICON_GLYPHS") + if named == "" { + named = env.get("HQTUI_ICONS") + } + switch IconMode(named) { + case IconNerd, IconUnicode, IconASCII: + return IconMode(named) + } + if env.get("NERD_FONT") == "1" || env.get("NERD_FONTS") == "1" { + return IconNerd + } + if detectUnicode(env, windows) { + return IconUnicode + } + return IconASCII +} + +// IconGlyphsOf returns an icon's glyphs by key or alias. +func IconGlyphsOf(name string) (IconGlyphs, bool) { + loadIcons() + if g, ok := iconByKey[name]; ok { + return g, true + } + g, ok := iconByKey[openIconAliases[name]] + return g, ok +} + +// IconIn is an icon's glyph in a given mode. Unknown names return "". +func IconIn(name string, mode IconMode) string { + g, ok := IconGlyphsOf(name) + if !ok { + return "" + } + switch mode { + case IconNerd: + if g.Nerd != "" { + return g.Nerd + } + return g.Unicode + case IconUnicode: + return g.Unicode + default: + return g.ASCII + } +} + +// Icon is the best glyph for an icon in this process's terminal. +func Icon(name string) string { + return IconIn(name, IconModeIn(ProcessEnv(), runningOnWindows())) +} + +// IconNames lists every key in the built-in pack, sorted. +func IconNames() []string { + names := make([]string, len(openIconGlyphs)) + for i, row := range openIconGlyphs { + names[i] = row[0] + } + return names +} diff --git a/ports/go/icons_data.go b/ports/go/icons_data.go new file mode 100644 index 0000000..0be253a --- /dev/null +++ b/ports/go/icons_data.go @@ -0,0 +1,541 @@ +// Code generated by packages/hqtui/scripts/generate-icons.ts from OpenIcon 2026-09-24 (OpenIcon 0.1), 370 icons. DO NOT EDIT. + +package hqtui + +// OpenIconVersion is the OpenIcon set the built-in pack was generated from. +const OpenIconVersion = "2026-09-24" + +// {key, nerd, unicode, ascii}; an empty nerd means Nerd Fonts has no glyph for it. +var openIconGlyphs = [][4]string{ + {"accessibility", "\U000f02e6", "\U0000267f", "a11y"}, + {"activity", "\U000f0430", "\U0001f4c8", "/\\/"}, + {"add", "\U000f0415", "+", "+"}, + {"alarm", "\U000f0020", "\U000023f0", "(!)"}, + {"align-center", "\U000f0260", "\U00002261", "="}, + {"align-left", "\U000f0262", "\U00002af7", "|="}, + {"align-right", "\U000f0263", "\U00002af8", "=|"}, + {"amazon", "\U0000f270", "\U000024d0", "amz"}, + {"anchor", "\U000f0031", "\U00002693", "t"}, + {"android", "\U000f0032", "\U0001f916", "and"}, + {"anthropic", "", "\U000024b6", "ant"}, + {"api", "\U000f109b", "\U00002699", "api"}, + {"apple", "\U000f0035", "\U0001f34e", "mac"}, + {"apple-music", "\U0000f2eb", "\U0001f3b5", "am"}, + {"archive", "\U000f120e", "\U0001f5c4", "[_]"}, + {"arrow-down", "\U000f0045", "\U00002193", "v"}, + {"arrow-left", "\U000f004d", "\U00002190", "<-"}, + {"arrow-right", "\U000f0054", "\U00002192", "->"}, + {"arrow-up", "\U000f005d", "\U00002191", "^"}, + {"arrow-up-right", "\U000f005c", "\U00002197", "/^"}, + {"at", "\U000f0065", "@", "@"}, + {"award", "\U000f1326", "\U0001f3c5", "(*)"}, + {"bag", "\U000f11d5", "\U0001f6cd", "[u]"}, + {"ban", "\U000f073a", "\U0001f6ab", "(/)"}, + {"bandcamp", "\U0000f2d5", "\U000025e2", "bc"}, + {"barcode", "\U000f0071", "\U000025a5", "|||"}, + {"battery", "\U000f008e", "\U0001f50b", "[=="}, + {"battery-charging", "\U000f0084", "\U0001f50c", "[=~"}, + {"behance", "\U0000f1b4", "B\U00000113", "be"}, + {"bell", "\U000f009c", "\U0001f514", "(!)"}, + {"bell-off", "\U000f0a91", "\U0001f515", "(x)"}, + {"bitbucket", "\U000f00a8", "\U0001faa3", "bb"}, + {"bitcoin", "\U000f0813", "\U000020bf", "btc"}, + {"bluesky", "\U000f1589", "\U0001f98b", "bsky"}, + {"bluetooth", "\U000f00af", "\U000016d2", "B"}, + {"bold", "\U000f0264", "\U0001d401", "B"}, + {"book", "\U000f0b64", "\U0001f4d5", "[B]"}, + {"book-open", "\U000f0b63", "\U0001f4d6", "[]"}, + {"bookmark", "\U000f00c3", "\U0001f516", "[]>"}, + {"braces", "\U000f0169", "{}", "{}"}, + {"brave", "\U000f0499", "\U0001f981", "brv"}, + {"briefcase", "\U000f0814", "\U0001f4bc", "[b]"}, + {"brush", "\U000f00e3", "\U0001f58c", "/~"}, + {"bug", "\U000f0a30", "\U0001f41b", "bug"}, + {"building", "\U000f151f", "\U0001f3e2", "[#]"}, + {"bun", "\U0000e76f", "\U0001f95f", "bun"}, + {"buy-me-a-coffee", "\U000f0176", "\U00002615", "bmc"}, + {"calendar", "\U000f0b66", "\U0001f4c5", "[=]"}, + {"calendar-check", "\U000f0c44", "\U0001f4c5", "[v]"}, + {"calendar-plus", "\U000f00f3", "\U0001f4c5", "[+]"}, + {"camera", "\U000f0d5d", "\U0001f4f7", "[o]"}, + {"cart", "\U000f0111", "\U0001f6d2", "\\_/"}, + {"cast", "\U000f0118", "\U0001f4e1", "))"}, + {"cell-signal", "\U000f04a2", "\U0001f4f6", ".:|"}, + {"chat", "\U000f0ede", "\U0001f4ac", "()"}, + {"chat-dots", "\U000f12ca", "\U0001f4ac", "(..)"}, + {"check", "\U000f012c", "\U00002713", "v"}, + {"check-circle", "\U000f05e1", "\U00002705", "(v)"}, + {"checkbox", "\U000f0135", "\U00002611", "[x]"}, + {"checkbox-empty", "\U000f0131", "\U00002610", "[ ]"}, + {"chevron-down", "\U000f0140", "\U00002304", "v"}, + {"chevron-left", "\U000f0141", "\U00002039", "<"}, + {"chevron-right", "\U000f0142", "\U0000203a", ">"}, + {"chevron-up", "\U000f0143", "\U00002303", "^"}, + {"chevrons-left", "\U000f013d", "\U000000ab", "<<"}, + {"chevrons-right", "\U000f013e", "\U000000bb", ">>"}, + {"chrome", "\U000f02af", "\U000025c9", "chr"}, + {"claude", "", "\U00002733", "cl"}, + {"clipboard", "\U000f014c", "\U0001f4cb", "[=]"}, + {"clipboard-check", "\U000f08a8", "\U0001f4cb", "[v]"}, + {"clock", "\U000f0150", "\U0001f552", "(t)"}, + {"close", "\U000f0156", "\U00002715", "x"}, + {"cloud", "\U000f0163", "\U00002601", "(~)"}, + {"cloud-download", "\U000f0b7d", "\U00002601", "(v)"}, + {"cloud-upload", "\U000f0b7e", "\U00002601", "(^)"}, + {"cloudflare", "\U0000e792", "\U00002601", "cf"}, + {"code", "\U000f0174", "\U000027e8\U000027e9", ""}, + {"codeberg", "\U0000f330", "\U000026f0", "cb"}, + {"codepen", "\U000f0175", "\U00002b21", "cpn"}, + {"coffee", "\U000f06ca", "\U00002615", "c[_]"}, + {"coins", "\U000f1890", "\U0001fa99", "(o)"}, + {"command", "\U000f0633", "\U00002318", "cmd"}, + {"compass", "\U000f018c", "\U0001f9ed", "(N)"}, + {"contact", "\U000f0dab", "\U0001f4c7", "[@]"}, + {"container", "\U000f01a7", "\U0001f4e6", "[c]"}, + {"copy", "\U000f018f", "\U000029c9", "cp"}, + {"cpu", "\U000f061a", "\U0001f532", "[#]"}, + {"credit-card", "\U000f019b", "\U0001f4b3", "[=]"}, + {"crop", "\U000f019e", "\U00002317", "[_"}, + {"crosshair", "\U000f01a3", "\U00002316", "-+-"}, + {"cut", "\U000f0190", "\U00002702", "8<"}, + {"database", "\U000f1632", "\U0001f6e2", "[=]"}, + {"debian", "\U000f08da", "\U0001f300", "deb"}, + {"delete", "\U000f0a7a", "\U0001f5d1", "del"}, + {"deno", "\U0000e7c0", "\U0001f995", "deno"}, + {"dev-to", "\U0000eef4", "DEV", "dev"}, + {"discord", "\U000f066f", "\U0001f3ae", "dc"}, + {"docker", "\U000f0868", "\U0001f433", "dkr"}, + {"dollar", "\U000f01c1", "$", "$"}, + {"download", "\U000f01da", "\U00002913", "v_"}, + {"dribbble", "\U0000f17d", "\U0001f3c0", "drb"}, + {"droplet", "\U000f0e0a", "\U0001f4a7", "o"}, + {"ebay", "\U0000edbe", "\U000024d4", "ebay"}, + {"edit", "\U000f03eb", "\U0000270e", "/e"}, + {"element", "\U000f0628", "\U000024ba", "el"}, + {"enter", "\U000f0311", "\U000021b5", "<-|"}, + {"error", "\U000f05d6", "\U000026d4", "!!"}, + {"ethereum", "\U000f086a", "\U0000039e", "eth"}, + {"etsy", "\U0000f2d7", "\U000024ba", "etsy"}, + {"external-link", "\U000f03cc", "\U00002197", "->]"}, + {"eye", "\U000f06d0", "\U0001f441", "o"}, + {"eye-off", "\U000f06d1", "\U0001f648", "-o-"}, + {"facebook", "\U000f020c", "\U000024d5", "fb"}, + {"farcaster", "", "\U000026e9", "fc"}, + {"fast-forward", "\U000f0211", "\U000023e9", ">>"}, + {"figma", "\U0000ef47", "\U0001f3a8", "fig"}, + {"file", "\U000f0224", "\U0001f4c4", "[f]"}, + {"file-code", "\U000f102b", "\U0001f4c4", "[<>]"}, + {"file-plus", "\U000f0eed", "\U0001f4c4", "[+]"}, + {"file-text", "\U000f09ee", "\U0001f4c4", "[t]"}, + {"film", "\U000f0230", "\U0001f39e", "[#]"}, + {"filter", "\U000f0233", "\U000023f7", "Y"}, + {"fingerprint", "\U000f0237", "\U0001fac6", "(@)"}, + {"firefox", "\U000f0239", "\U0001f98a", "ff"}, + {"flag", "\U000f023d", "\U00002691", "|>"}, + {"flask", "\U000f0096", "\U00002697", "/_\\"}, + {"folder", "\U000f0256", "\U0001f4c1", "[d]"}, + {"folder-open", "\U000f0dcf", "\U0001f4c2", "[d]"}, + {"folder-plus", "\U000f0b9d", "\U0001f4c1", "[d+]"}, + {"forward", "\U000f0496", "\U000021aa", "->"}, + {"frown", "\U000f01f8", "\U0001f641", ":("}, + {"gauge", "\U000f029a", "\U000023f2", "(/)"}, + {"ghost", "\U000f02a0", "\U0001f47b", "gst"}, + {"gift", "\U000f02a1", "\U0001f381", "[+]"}, + {"git-branch", "\U0000f418", "\U00002387", "Y"}, + {"git-commit", "\U0000f417", "\U000022b8", "-o-"}, + {"git-merge", "\U0000f419", "\U00002442", ">-"}, + {"git-pull-request", "\U0000f407", "\U000021c4", "PR"}, + {"gitea", "\U0000f339", "\U0001f375", "gt"}, + {"github", "\U000f02a4", "\U0001f419", "gh"}, + {"gitlab", "\U000f0ba0", "\U0001f98a", "gl"}, + {"globe", "\U000f059f", "\U0001f310", "(#)"}, + {"gmail", "\U000f02ab", "\U00002709", "gm"}, + {"go", "\U000f07d3", "\U0001f439", "go"}, + {"google", "\U000f02ad", "\U000024bc", "g"}, + {"google-meet", "\U000f0bdc", "\U0001f4f9", "meet"}, + {"grid", "\U000f11d9", "\U000025a6", "::"}, + {"grip", "\U000f01db", "\U0000283f", "::"}, + {"hacker-news", "\U0000f1d4", "\U000024ce", "hn"}, + {"hard-drive", "\U000f02ca", "\U0001f5b4", "[o]"}, + {"hash", "\U000f0423", "#", "#"}, + {"hashnode", "", "#", "hn#"}, + {"heading", "\U000f0274", "H", "H"}, + {"headphones", "\U000f02cb", "\U0001f3a7", "hp"}, + {"heart", "\U000f02d5", "\U00002665", "<3"}, + {"help", "\U000f0625", "\U00002753", "?"}, + {"history", "\U000f02da", "\U0001f558", "<(t)"}, + {"home", "\U000f06a1", "\U0001f3e0", "~"}, + {"hourglass", "\U000f051f", "\U000023f3", "8"}, + {"id-card", "\U000f0dab", "\U0001faaa", "[id]"}, + {"image", "\U000f0976", "\U0001f5bc", "[^]"}, + {"image-plus", "\U000f087c", "\U0001f5bc", "[+]"}, + {"inbox", "\U000f0687", "\U0001f4e5", "[v]"}, + {"info", "\U000f02fd", "\U00002139", "i"}, + {"instagram", "\U000f02fe", "\U0001f4f7", "ig"}, + {"italic", "\U000f0277", "\U0001d43c", "/"}, + {"javascript", "\U000f031e", "JS", "js"}, + {"jira", "\U000f0303", "\U000025c8", "jira"}, + {"key", "\U000f0dd6", "\U0001f511", "o-"}, + {"keybase", "\U0000edbf", "\U0001f511", "kb"}, + {"keyboard", "\U000f097b", "\U00002328", "[kb]"}, + {"kick", "", "\U000024c0", "kick"}, + {"ko-fi", "\U000f0176", "\U00002615", "kofi"}, + {"kubernetes", "\U000f10fe", "\U00002638", "k8s"}, + {"laptop", "\U000f0322", "\U0001f4bb", "[_]"}, + {"layers", "\U000f09fe", "\U00002630", "="}, + {"leaf", "\U000f032a", "\U0001f343", "~"}, + {"lemmy", "", "\U0001f42d", "lmy"}, + {"lightbulb", "\U000f0336", "\U0001f4a1", "i"}, + {"line", "\U0000f2fb", "\U0001f4ac", "line"}, + {"link", "\U000f0339", "\U0001f517", "~"}, + {"linkedin", "\U000f033b", "\U000024d8", "in"}, + {"linux", "\U000f033d", "\U0001f427", "lnx"}, + {"list", "\U000f0279", "\U00002637", "-="}, + {"list-bullet", "\U000f0279", "\U00002022", "*"}, + {"list-ordered", "\U000f027b", "\U00002488", "1."}, + {"loader", "\U000f0772", "\U000025cc", "..."}, + {"lock", "\U000f0341", "\U0001f512", "[#]"}, + {"log-in", "\U000f0342", "\U000021e5", "->|"}, + {"log-out", "\U000f0343", "\U000021e4", "|->"}, + {"mail", "\U000f01f0", "\U00002709", "@"}, + {"mail-open", "\U000f05ef", "\U0001f4e8", "@"}, + {"map", "\U000f0982", "\U0001f5fa", "[#]"}, + {"map-pin", "\U000f07d9", "\U0001f4cd", "@"}, + {"mastodon", "\U000f0ad1", "\U0001f418", "mdn"}, + {"matrix", "\U000f0628", "\U000024c2", "[m]"}, + {"mattermost", "", "\U000024c2", "mm"}, + {"maximize", "\U000f0293", "\U000026f6", "[ ]"}, + {"medium", "\U0000f23a", "\U000024c2", "md"}, + {"megaphone", "\U000f0b23", "\U0001f4e3", "<|"}, + {"menu", "\U000f035c", "\U00002630", "="}, + {"messenger", "\U000f020e", "\U0001f4ac", "msg"}, + {"mic", "\U000f036e", "\U0001f3a4", "mic"}, + {"mic-off", "\U000f036d", "\U0001f507", "x-m"}, + {"microsoft", "\U000f0372", "\U0000229e", "ms"}, + {"minimize", "\U000f0294", "\U000022a1", "]["}, + {"minus", "\U000f0374", "\U00002212", "-"}, + {"minus-circle", "\U000f0377", "\U00002296", "(-)"}, + {"misskey", "", "\U000024c2", "mk"}, + {"monitor", "\U000f0379", "\U0001f5a5", "[ ]"}, + {"moon", "\U000f0594", "\U0001f319", "C"}, + {"more-horizontal", "\U000f01d8", "\U000022ef", "..."}, + {"more-vertical", "\U000f01d9", "\U000022ee", ":"}, + {"mouse", "\U000f037d", "\U0001f5b1", "(|)"}, + {"move", "\U000f01be", "\U00002725", "+"}, + {"music", "\U000f0387", "\U0001f3b5", "#"}, + {"navigation", "\U000f18f1", "\U000027b6", ">"}, + {"netlify", "\U0000e83c", "\U000025c6", "ntl"}, + {"newspaper", "\U000f1004", "\U0001f4f0", "[n]"}, + {"nodejs", "\U000f0399", "\U00002b22", "node"}, + {"notion", "\U0000e848", "\U000024c3", "ntn"}, + {"npm", "\U000f06f7", "\U0001f4e6", "npm"}, + {"online", "\U000f0aa5", "\U0001f7e2", "(o)"}, + {"openai", "", "\U0000273a", "oai"}, + {"package", "\U000f03d7", "\U0001f4e6", "[#]"}, + {"palette", "\U000f0e0c", "\U0001f3a8", "(:)"}, + {"paperclip", "\U000f03e2", "\U0001f4ce", "0/"}, + {"patreon", "\U000f0882", "\U000024c5", "pat"}, + {"pause", "\U000f03e4", "\U000023f8", "||"}, + {"paypal", "\U0000f1ed", "\U000024c5", "pp"}, + {"peertube", "", "\U000025b6", "pt"}, + {"pen-tool", "\U000f0d13", "\U00002712", "_/"}, + {"percent", "\U000f03f0", "%", "%"}, + {"phone", "\U000f0df0", "\U0000260e", "tel"}, + {"phone-call", "\U000f1182", "\U0001f4de", "tel"}, + {"phone-off", "\U000f11a6", "\U0001f4f5", "x-t"}, + {"pin", "\U000f0931", "\U0001f4cc", "-|"}, + {"pinterest", "\U000f0407", "\U0001f4cc", "pin"}, + {"pixelfed", "", "\U0001f5bc", "pxf"}, + {"play", "\U000f040a", "\U000025b6", ">"}, + {"plug", "\U000f1425", "\U0001f50c", "-["}, + {"plus-circle", "\U000f0419", "\U00002295", "(+)"}, + {"power", "\U000f0425", "\U000023fb", "(|)"}, + {"print", "\U000f1786", "\U0001f5a8", "prn"}, + {"product-hunt", "\U0000f288", "\U000024c5", "ph"}, + {"proton-mail", "\U000f01f1", "\U00002709", "pm"}, + {"puzzle", "\U000f0a66", "\U0001f9e9", "[+]"}, + {"python", "\U000f0320", "\U0001f40d", "py"}, + {"qr-code", "\U000f0432", "\U000025a6", "[#]"}, + {"quote", "\U000f0757", "\U0000275d", "\""}, + {"radio", "\U000f0003", "\U0001f4fb", "(o)"}, + {"radio-off", "\U0000f111", "\U000025cb", "( )"}, + {"radio-on", "\U000f043e", "\U000025c9", "(*)"}, + {"railway", "\U0000e883", "\U0001f686", "rly"}, + {"receipt", "\U000f0449", "\U0001f9fe", "[$]"}, + {"reddit", "\U000f044d", "\U0001f47d", "rd"}, + {"redo", "\U000f044e", "\U000021b7", "->"}, + {"refresh", "\U000f0450", "\U000027f3", "@"}, + {"repeat", "\U000f0456", "\U0001f501", "<->"}, + {"reply", "\U000f045a", "\U000021a9", "<-"}, + {"rewind", "\U000f045f", "\U000023ea", "<<"}, + {"rocket", "\U000f14df", "\U0001f680", "^"}, + {"rocketchat", "\U0000ed20", "\U0001f680", "rc"}, + {"rss", "\U000f046b", "\U0001f4e1", "rss"}, + {"ruler", "\U000f046d", "\U0001f4cf", "|-|"}, + {"rust", "\U000f1617", "\U0001f980", "rs"}, + {"safari", "\U000f0039", "\U0001f9ed", "saf"}, + {"save", "\U000f0818", "\U0001f4be", "[s]"}, + {"search", "\U000f0349", "\U0001f50d", "?"}, + {"send", "\U000f1165", "\U000027a4", ">>"}, + {"server", "\U000f048b", "\U0001f5a5", "[:]"}, + {"settings", "\U000f0493", "\U00002699", "*"}, + {"share", "\U000f1514", "\U00002934", "<"}, + {"share-out", "\U000f0b93", "\U000021ea", "^"}, + {"shield", "\U000f0499", "\U0001f6e1", "[S]"}, + {"shield-check", "\U000f0cc8", "\U0001f6e1", "[v]"}, + {"shopify", "\U000f049a", "\U0001f6cd", "shp"}, + {"shuffle", "\U000f049f", "\U0001f500", "><"}, + {"sidebar", "\U000f10aa", "\U000025a5", "|="}, + {"signal", "\U000f116d", "\U0001f4ac", "sig"}, + {"skip-back", "\U000f04ae", "\U000023ee", "|<"}, + {"skip-forward", "\U000f04ad", "\U000023ed", ">|"}, + {"skype", "\U000f04af", "\U000024c8", "sky"}, + {"slack", "\U000f04b1", "#", "slk"}, + {"sliders", "\U000f1542", "\U0001f39a", "=|="}, + {"smartphone", "\U000f011c", "\U0001f4f1", "[.]"}, + {"smile", "\U000f01f5", "\U0001f642", ":)"}, + {"sms", "\U000f1170", "\U0001f4ac", "sms"}, + {"snapchat", "\U000f04b6", "\U0001f47b", "snap"}, + {"sort", "\U000f04ba", "\U000021c5", "^v"}, + {"sort-asc", "\U000f04bc", "\U00002191", "a-z"}, + {"sort-desc", "\U000f04bd", "\U00002193", "z-a"}, + {"soundcloud", "\U000f04c0", "\U00002601", "sc"}, + {"sourcehut", "", "\U000025ef", "srht"}, + {"sparkles", "\U000f0674", "\U00002728", "*+"}, + {"spotify", "\U000f04c7", "\U0001f3b5", "spot"}, + {"stack-overflow", "\U000f04cc", "\U0001f4da", "so"}, + {"star", "\U000f04d2", "\U00002605", "*"}, + {"stop", "\U000f04db", "\U000023f9", "[]"}, + {"store", "\U000f10c1", "\U0001f3ea", "[S]"}, + {"strikethrough", "\U000f0280", "S\U00000336", "-s-"}, + {"stripe", "\U0000ed53", "\U000024c8", "str"}, + {"substack", "\U000f0fb1", "\U00002709", "ss"}, + {"sun", "\U000f05a8", "\U00002600", "*"}, + {"supabase", "\U0000e8b6", "\U000026a1", "sb"}, + {"table", "\U000f04eb", "\U000025a6", "[#]"}, + {"tablet", "\U000f04f6", "\U0001f4f1", "[..]"}, + {"tag", "\U000f04fc", "\U0001f3f7", "#"}, + {"target", "\U000f04fe", "\U0001f3af", "(o)"}, + {"telegram", "\U0000f2c6", "\U00002708", "tg"}, + {"terminal", "\U000f018d", "\U00002328", ">_"}, + {"theme", "\U000f050e", "\U000025d0", "(|)"}, + {"thermometer", "\U000f050f", "\U0001f321", "|o"}, + {"threads", "\U000f0065", "@", "th"}, + {"thumbs-down", "\U000f0512", "\U0001f44e", "-1"}, + {"thumbs-up", "\U000f0514", "\U0001f44d", "+1"}, + {"tiktok", "\U000f0387", "\U0000266a", "tt"}, + {"timer", "\U000f051b", "\U000023f1", "(:)"}, + {"toggle-off", "\U000f0a19", "\U0001f518", "[o=]"}, + {"toggle-on", "\U000f0521", "\U0001f518", "[=o]"}, + {"tor", "\U0000f371", "\U0001f9c5", "tor"}, + {"translate", "\U000f05ca", "\U0001f310", "A/a"}, + {"trello", "\U000f0532", "\U000025a4", "tr"}, + {"trophy", "\U000f053a", "\U0001f3c6", "\\_/"}, + {"truck", "\U000f129d", "\U0001f69a", "[=o"}, + {"tumblr", "\U0000f173", "\U000024e3", "tb"}, + {"tv", "\U000f0502", "\U0001f4fa", "[_]"}, + {"twitch", "\U000f0543", "\U0001f4fa", "ttv"}, + {"type", "\U000f0284", "T", "T"}, + {"typescript", "\U000f06e6", "TS", "ts"}, + {"ubuntu", "\U000f0548", "\U000025ce", "ubu"}, + {"umbrella", "\U000f054b", "\U00002602", "T"}, + {"underline", "\U000f0287", "U\U00000332", "_"}, + {"undo", "\U000f054c", "\U000021b6", "<-"}, + {"unlink", "\U000f033a", "\U000026d3", "~/"}, + {"unlock", "\U000f0fc7", "\U0001f513", "[ ]"}, + {"upload", "\U000f0552", "\U00002912", "^_"}, + {"user", "\U000f0013", "\U0001f464", "@"}, + {"user-check", "\U000f0be2", "\U0001f464", "@v"}, + {"user-circle", "\U000f0b55", "\U0001f464", "(@)"}, + {"user-minus", "\U000f0aec", "\U0001f464", "@-"}, + {"user-plus", "\U000f0801", "\U0001f464", "@+"}, + {"users", "\U000f000f", "\U0001f465", "@@"}, + {"vercel", "\U000f0536", "\U000025b2", "vc"}, + {"video", "\U000f0bdc", "\U0001f4f9", "[>"}, + {"vimeo", "\U000f0577", "\U000024e5", "vm"}, + {"voicemail", "\U000f057d", "\U00002328", "oo"}, + {"volume", "\U000f057e", "\U0001f50a", "<))"}, + {"volume-low", "\U000f0580", "\U0001f509", "<)"}, + {"volume-off", "\U000f0581", "\U0001f507", ""}, + {"wechat", "\U000f0611", "\U0001f4ac", "wx"}, + {"whatsapp", "\U000f05a3", "\U0001f4de", "wa"}, + {"wifi", "\U000f05a9", "\U0001f4f6", "((."}, + {"wifi-off", "\U000f05aa", "\U0001f4f5", "x(("}, + {"windows", "\U000f05b3", "\U0000229e", "win"}, + {"wordpress", "\U000f05b4", "\U000024cc", "wp"}, + {"x", "\U000f0b05", "\U0001d54f", "x"}, + {"x-circle", "\U000f015a", "\U0000274e", "(x)"}, + {"xmpp", "\U000f07ff", "\U0001f4ac", "xmpp"}, + {"y-combinator", "\U0000f23b", "\U000024ce", "yc"}, + {"youtube", "\U000f05c3", "\U000025b6", "yt"}, + {"zap", "\U000f140c", "\U000026a1", "/"}, + {"zoom", "\U000f0567", "\U0001f4f9", "zm"}, + {"zoom-in", "\U000f06ed", "\U0001f50e", "+?"}, + {"zoom-out", "\U000f06ec", "\U0001f50e", "-?"}, + {"zulip", "", "\U000024cf", "zl"}, +} + +var openIconAliases = map[string]string{ + "a11y": "accessibility", + "account": "user", + "add-user": "user-plus", + "address-book": "contact", + "ai": "sparkles", + "alarm-clock": "alarm", + "alert-circle": "error", + "alert-triangle": "warning", + "announce": "megaphone", + "at-sign": "at", + "attach": "paperclip", + "attachment": "paperclip", + "bin": "delete", + "biometric": "fingerprint", + "blocked": "ban", + "bolt": "zap", + "box": "package", + "bullhorn": "megaphone", + "bullseye": "target", + "call": "phone", + "card": "credit-card", + "caution": "warning", + "cellphone": "smartphone", + "check-square": "checkbox", + "chip": "cpu", + "cli": "terminal", + "cmd": "command", + "cog": "settings", + "color": "palette", + "comment": "chat", + "company": "building", + "computer": "laptop", + "console": "terminal", + "contrast": "theme", + "curly-braces": "braces", + "currency": "dollar", + "dark-mode": "moon", + "date": "calendar", + "day": "sun", + "db": "database", + "delivery": "truck", + "deploy": "rocket", + "desktop": "monitor", + "directory": "folder", + "discount": "percent", + "dislike": "thumbs-down", + "display": "monitor", + "document": "file", + "duplicate": "copy", + "ellipsis": "more-horizontal", + "email": "mail", + "envelope": "mail", + "exit-fullscreen": "minimize", + "experiment": "flask", + "extension": "puzzle", + "faq": "help", + "favorite": "star", + "find": "search", + "floppy": "save", + "forbidden": "ban", + "fullscreen": "maximize", + "gear": "settings", + "goal": "target", + "golang": "go", + "hamburger": "menu", + "happy": "smile", + "hashtag": "hash", + "hide": "eye-off", + "house": "home", + "i18n": "translate", + "idea": "lightbulb", + "identity": "id-card", + "information": "info", + "integration": "plug", + "invite": "user-plus", + "invoice": "receipt", + "job": "briefcase", + "json": "braces", + "kebab": "more-vertical", + "lab": "flask", + "label": "tag", + "language": "translate", + "light-mode": "sun", + "lightning": "zap", + "like": "thumbs-up", + "loading": "loader", + "location": "map-pin", + "login": "log-in", + "logout": "log-out", + "love": "heart", + "magic": "sparkles", + "magnify": "search", + "marker": "map-pin", + "mention": "at", + "merge-request": "git-pull-request", + "message": "chat", + "microphone": "mic", + "mobile": "smartphone", + "movie": "film", + "mute": "volume-off", + "night": "moon", + "node": "nodejs", + "notification": "bell", + "office": "building", + "open-in-new": "external-link", + "paper-plane": "send", + "payment": "credit-card", + "pencil": "edit", + "pending": "hourglass", + "people": "users", + "person": "user", + "photo": "image", + "photo-camera": "camera", + "picture": "image", + "plugin": "puzzle", + "plus": "add", + "preferences": "settings", + "present": "gift", + "printer": "print", + "prize": "trophy", + "processor": "cpu", + "profile": "user", + "pull-request": "git-pull-request", + "qr": "qr-code", + "question": "help", + "recent": "history", + "reception": "cell-signal", + "reload": "refresh", + "scissors": "cut", + "shell": "terminal", + "shipping": "truck", + "shopping-bag": "bag", + "shopping-cart": "cart", + "show": "eye", + "sign-in": "log-in", + "sign-out": "log-out", + "song": "music", + "sound": "volume", + "source-code": "code", + "speaker": "volume", + "spinner": "loader", + "square": "checkbox-empty", + "stopwatch": "timer", + "storefront": "store", + "sync": "refresh", + "team": "users", + "telephone": "phone", + "television": "tv", + "thumbtack": "pin", + "times": "close", + "trash": "delete", + "twitter": "x", + "usd": "dollar", + "view": "eye", + "web": "globe", + "work": "briefcase", + "world": "globe", + "x-mark": "close", +} diff --git a/ports/go/icons_test.go b/ports/go/icons_test.go new file mode 100644 index 0000000..8d2b3dd --- /dev/null +++ b/ports/go/icons_test.go @@ -0,0 +1,78 @@ +package hqtui + +// The same cases as packages/hqtui/test/icons.test.ts. + +import ( + "sort" + "testing" +) + +func TestIconModesAndAliases(t *testing.T) { + cases := []struct { + name string + mode IconMode + want string + }{ + {"mail", IconNerd, "\U000f01f0"}, + {"mail", IconUnicode, "✉"}, + {"mail", IconASCII, "@"}, + {"email", IconASCII, "@"}, + {"twitter", IconASCII, "x"}, + {"no-such-icon", IconUnicode, ""}, + } + for _, c := range cases { + if got := IconIn(c.name, c.mode); got != c.want { + t.Errorf("IconIn(%q, %s) = %q, want %q", c.name, c.mode, got, c.want) + } + } +} + +func TestIconNerdFallsBackToUnicode(t *testing.T) { + for _, row := range openIconGlyphs { + if row[1] == "" { + if got := IconIn(row[0], IconNerd); got != row[2] { + t.Fatalf("%s: nerd fallback = %q, want %q", row[0], got, row[2]) + } + return + } + } + t.Fatal("expected at least one icon without a Nerd glyph") +} + +func TestIconModeOrder(t *testing.T) { + checks := []struct { + env Env + want IconMode + }{ + {Env{"OPENICON_GLYPHS": "ascii", "NERD_FONT": "1", "LANG": "en_US.UTF-8"}, IconASCII}, + {Env{"HQTUI_ICONS": "nerd"}, IconNerd}, + {Env{"NERD_FONT": "1"}, IconNerd}, + {Env{"LANG": "en_US.UTF-8", "TERM": "xterm-256color"}, IconUnicode}, + {Env{"TERM": "dumb"}, IconASCII}, + } + for _, c := range checks { + if got := IconModeIn(c.env, false); got != c.want { + t.Errorf("IconModeIn(%v) = %s, want %s", c.env, got, c.want) + } + } + SetIconMode(IconASCII) + defer SetIconMode("") + if got := IconModeIn(Env{"NERD_FONT": "1"}, false); got != IconASCII { + t.Errorf("SetIconMode should win, got %s", got) + } +} + +func TestIconTableSortedAndComplete(t *testing.T) { + names := IconNames() + if len(names) < 300 { + t.Fatalf("only %d icons", len(names)) + } + if !sort.StringsAreSorted(names) { + t.Fatal("icons_data.go is not sorted by key") + } + for _, row := range openIconGlyphs { + if row[2] == "" || row[3] == "" { + t.Errorf("%s has an empty fallback", row[0]) + } + } +} diff --git a/ports/python/hqtui/__init__.py b/ports/python/hqtui/__init__.py index 0fa66d8..28953d2 100644 --- a/ports/python/hqtui/__init__.py +++ b/ports/python/hqtui/__init__.py @@ -22,6 +22,7 @@ from .color import Color, DEFAULT_COLOR, Gradient, from_256 from .diff import EncodeResult, Encoder, encode_full from .graphics import BrailleCanvas, FillMode +from .icons import IconGlyphs, icon, icon_glyphs, icon_mode, icon_names, set_icon_mode from .input import InputParser, KeyEvent, MouseAction, MouseEvent, match_key from .layout import Constraint, Direction, Rect, solve, stack from .surface import BorderStyle, BoxOptions, Surface, TextOptions @@ -49,6 +50,7 @@ "ScrollHandlers", "Terminal", "TerminalOptions", "TerminalSize", "emergency_restore", "match_key", "render_to_ansi", "render_to_html", "render_to_screen", "render_to_text", "widgets", + "IconGlyphs", "icon", "icon_glyphs", "icon_mode", "icon_names", "set_icon_mode", ] from . import widgets # noqa: E402 (re-exported for `hqtui.widgets.*`) diff --git a/ports/python/hqtui/icons.py b/ports/python/hqtui/icons.py new file mode 100644 index 0000000..9fcd599 --- /dev/null +++ b/ports/python/hqtui/icons.py @@ -0,0 +1,79 @@ +"""Icons for terminals: ``icon("mail")`` is the best glyph this terminal can draw. + +The built-in pack is OpenIcon (https://logicsrc.com/openicon), on by default, +generated into ``icons_data.py`` from the same set as the TypeScript reference, +so an icon is the same glyph in every port. Which of its three glyphs you get: +``set_icon_mode`` if the app chose; ``OPENICON_GLYPHS`` or ``HQTUI_ICONS``; +``NERD_FONT=1`` for Nerd Font glyphs; otherwise Unicode where the terminal +draws it and ASCII where it does not. A Nerd Font is never assumed: it cannot +be detected from inside the terminal. +""" +from __future__ import annotations + +import os +import sys +from collections.abc import Mapping +from typing import Literal, NamedTuple + +from .capabilities import _detect_unicode +from .icons_data import OPENICON_ALIASES, OPENICON_GLYPHS, OPENICON_VERSION + +IconMode = Literal["nerd", "unicode", "ascii"] +_MODES = ("nerd", "unicode", "ascii") + + +class IconGlyphs(NamedTuple): + """The three spellings of one icon; ``nerd`` is empty when Nerd Fonts has none.""" + + nerd: str + unicode: str + ascii: str + + +_BY_KEY = {key: IconGlyphs(nerd, uni, asc) for key, nerd, uni, asc in OPENICON_GLYPHS} +_chosen: IconMode | None = None + + +def set_icon_mode(mode: IconMode | None) -> None: + """Pin the glyph family for the whole app, or ``None`` to detect again.""" + global _chosen + if mode is not None and mode not in _MODES: + raise ValueError(f"icon mode must be one of {_MODES}, got {mode!r}") + _chosen = mode + + +def icon_mode(env: Mapping[str, str] | None = None) -> IconMode: + """The glyph family an environment gets, in the order in this module's docstring.""" + if _chosen is not None: + return _chosen + env = os.environ if env is None else env + named = env.get("OPENICON_GLYPHS") or env.get("HQTUI_ICONS") or "" + if named in _MODES: + return named # type: ignore[return-value] + if env.get("NERD_FONT") == "1" or env.get("NERD_FONTS") == "1": + return "nerd" + return "unicode" if _detect_unicode(env, sys.platform == "win32") else "ascii" + + +def icon_glyphs(name: str) -> IconGlyphs | None: + """An icon's glyphs by key or alias, or ``None``.""" + return _BY_KEY.get(name) or _BY_KEY.get(OPENICON_ALIASES.get(name, "")) + + +def icon(name: str, mode: IconMode | None = None) -> str: + """The best glyph for an icon. Unknown names return ``""``.""" + glyphs = icon_glyphs(name) + if glyphs is None: + return "" + mode = mode or icon_mode() + if mode == "nerd": + return glyphs.nerd or glyphs.unicode + return glyphs.unicode if mode == "unicode" else glyphs.ascii + + +def icon_names() -> list[str]: + """Every key in the built-in pack, sorted.""" + return [row[0] for row in OPENICON_GLYPHS] + + +__all__ = ["IconGlyphs", "IconMode", "OPENICON_VERSION", "icon", "icon_glyphs", "icon_mode", "icon_names", "set_icon_mode"] diff --git a/ports/python/hqtui/icons_data.py b/ports/python/hqtui/icons_data.py new file mode 100644 index 0000000..f9cc31c --- /dev/null +++ b/ports/python/hqtui/icons_data.py @@ -0,0 +1,538 @@ +# Generated by packages/hqtui/scripts/generate-icons.ts from OpenIcon 2026-09-24 (OpenIcon 0.1), 370 icons. Do not edit. +# (key, nerd, unicode, ascii); an empty nerd means Nerd Fonts has no glyph for it. + +OPENICON_VERSION = "2026-09-24" + +OPENICON_GLYPHS = ( + ("accessibility", "\U000f02e6", "\U0000267f", "a11y"), + ("activity", "\U000f0430", "\U0001f4c8", "/\\/"), + ("add", "\U000f0415", "+", "+"), + ("alarm", "\U000f0020", "\U000023f0", "(!)"), + ("align-center", "\U000f0260", "\U00002261", "="), + ("align-left", "\U000f0262", "\U00002af7", "|="), + ("align-right", "\U000f0263", "\U00002af8", "=|"), + ("amazon", "\U0000f270", "\U000024d0", "amz"), + ("anchor", "\U000f0031", "\U00002693", "t"), + ("android", "\U000f0032", "\U0001f916", "and"), + ("anthropic", "", "\U000024b6", "ant"), + ("api", "\U000f109b", "\U00002699", "api"), + ("apple", "\U000f0035", "\U0001f34e", "mac"), + ("apple-music", "\U0000f2eb", "\U0001f3b5", "am"), + ("archive", "\U000f120e", "\U0001f5c4", "[_]"), + ("arrow-down", "\U000f0045", "\U00002193", "v"), + ("arrow-left", "\U000f004d", "\U00002190", "<-"), + ("arrow-right", "\U000f0054", "\U00002192", "->"), + ("arrow-up", "\U000f005d", "\U00002191", "^"), + ("arrow-up-right", "\U000f005c", "\U00002197", "/^"), + ("at", "\U000f0065", "@", "@"), + ("award", "\U000f1326", "\U0001f3c5", "(*)"), + ("bag", "\U000f11d5", "\U0001f6cd", "[u]"), + ("ban", "\U000f073a", "\U0001f6ab", "(/)"), + ("bandcamp", "\U0000f2d5", "\U000025e2", "bc"), + ("barcode", "\U000f0071", "\U000025a5", "|||"), + ("battery", "\U000f008e", "\U0001f50b", "[=="), + ("battery-charging", "\U000f0084", "\U0001f50c", "[=~"), + ("behance", "\U0000f1b4", "B\U00000113", "be"), + ("bell", "\U000f009c", "\U0001f514", "(!)"), + ("bell-off", "\U000f0a91", "\U0001f515", "(x)"), + ("bitbucket", "\U000f00a8", "\U0001faa3", "bb"), + ("bitcoin", "\U000f0813", "\U000020bf", "btc"), + ("bluesky", "\U000f1589", "\U0001f98b", "bsky"), + ("bluetooth", "\U000f00af", "\U000016d2", "B"), + ("bold", "\U000f0264", "\U0001d401", "B"), + ("book", "\U000f0b64", "\U0001f4d5", "[B]"), + ("book-open", "\U000f0b63", "\U0001f4d6", "[]"), + ("bookmark", "\U000f00c3", "\U0001f516", "[]>"), + ("braces", "\U000f0169", "{}", "{}"), + ("brave", "\U000f0499", "\U0001f981", "brv"), + ("briefcase", "\U000f0814", "\U0001f4bc", "[b]"), + ("brush", "\U000f00e3", "\U0001f58c", "/~"), + ("bug", "\U000f0a30", "\U0001f41b", "bug"), + ("building", "\U000f151f", "\U0001f3e2", "[#]"), + ("bun", "\U0000e76f", "\U0001f95f", "bun"), + ("buy-me-a-coffee", "\U000f0176", "\U00002615", "bmc"), + ("calendar", "\U000f0b66", "\U0001f4c5", "[=]"), + ("calendar-check", "\U000f0c44", "\U0001f4c5", "[v]"), + ("calendar-plus", "\U000f00f3", "\U0001f4c5", "[+]"), + ("camera", "\U000f0d5d", "\U0001f4f7", "[o]"), + ("cart", "\U000f0111", "\U0001f6d2", "\\_/"), + ("cast", "\U000f0118", "\U0001f4e1", "))"), + ("cell-signal", "\U000f04a2", "\U0001f4f6", ".:|"), + ("chat", "\U000f0ede", "\U0001f4ac", "()"), + ("chat-dots", "\U000f12ca", "\U0001f4ac", "(..)"), + ("check", "\U000f012c", "\U00002713", "v"), + ("check-circle", "\U000f05e1", "\U00002705", "(v)"), + ("checkbox", "\U000f0135", "\U00002611", "[x]"), + ("checkbox-empty", "\U000f0131", "\U00002610", "[ ]"), + ("chevron-down", "\U000f0140", "\U00002304", "v"), + ("chevron-left", "\U000f0141", "\U00002039", "<"), + ("chevron-right", "\U000f0142", "\U0000203a", ">"), + ("chevron-up", "\U000f0143", "\U00002303", "^"), + ("chevrons-left", "\U000f013d", "\U000000ab", "<<"), + ("chevrons-right", "\U000f013e", "\U000000bb", ">>"), + ("chrome", "\U000f02af", "\U000025c9", "chr"), + ("claude", "", "\U00002733", "cl"), + ("clipboard", "\U000f014c", "\U0001f4cb", "[=]"), + ("clipboard-check", "\U000f08a8", "\U0001f4cb", "[v]"), + ("clock", "\U000f0150", "\U0001f552", "(t)"), + ("close", "\U000f0156", "\U00002715", "x"), + ("cloud", "\U000f0163", "\U00002601", "(~)"), + ("cloud-download", "\U000f0b7d", "\U00002601", "(v)"), + ("cloud-upload", "\U000f0b7e", "\U00002601", "(^)"), + ("cloudflare", "\U0000e792", "\U00002601", "cf"), + ("code", "\U000f0174", "\U000027e8\U000027e9", ""), + ("codeberg", "\U0000f330", "\U000026f0", "cb"), + ("codepen", "\U000f0175", "\U00002b21", "cpn"), + ("coffee", "\U000f06ca", "\U00002615", "c[_]"), + ("coins", "\U000f1890", "\U0001fa99", "(o)"), + ("command", "\U000f0633", "\U00002318", "cmd"), + ("compass", "\U000f018c", "\U0001f9ed", "(N)"), + ("contact", "\U000f0dab", "\U0001f4c7", "[@]"), + ("container", "\U000f01a7", "\U0001f4e6", "[c]"), + ("copy", "\U000f018f", "\U000029c9", "cp"), + ("cpu", "\U000f061a", "\U0001f532", "[#]"), + ("credit-card", "\U000f019b", "\U0001f4b3", "[=]"), + ("crop", "\U000f019e", "\U00002317", "[_"), + ("crosshair", "\U000f01a3", "\U00002316", "-+-"), + ("cut", "\U000f0190", "\U00002702", "8<"), + ("database", "\U000f1632", "\U0001f6e2", "[=]"), + ("debian", "\U000f08da", "\U0001f300", "deb"), + ("delete", "\U000f0a7a", "\U0001f5d1", "del"), + ("deno", "\U0000e7c0", "\U0001f995", "deno"), + ("dev-to", "\U0000eef4", "DEV", "dev"), + ("discord", "\U000f066f", "\U0001f3ae", "dc"), + ("docker", "\U000f0868", "\U0001f433", "dkr"), + ("dollar", "\U000f01c1", "$", "$"), + ("download", "\U000f01da", "\U00002913", "v_"), + ("dribbble", "\U0000f17d", "\U0001f3c0", "drb"), + ("droplet", "\U000f0e0a", "\U0001f4a7", "o"), + ("ebay", "\U0000edbe", "\U000024d4", "ebay"), + ("edit", "\U000f03eb", "\U0000270e", "/e"), + ("element", "\U000f0628", "\U000024ba", "el"), + ("enter", "\U000f0311", "\U000021b5", "<-|"), + ("error", "\U000f05d6", "\U000026d4", "!!"), + ("ethereum", "\U000f086a", "\U0000039e", "eth"), + ("etsy", "\U0000f2d7", "\U000024ba", "etsy"), + ("external-link", "\U000f03cc", "\U00002197", "->]"), + ("eye", "\U000f06d0", "\U0001f441", "o"), + ("eye-off", "\U000f06d1", "\U0001f648", "-o-"), + ("facebook", "\U000f020c", "\U000024d5", "fb"), + ("farcaster", "", "\U000026e9", "fc"), + ("fast-forward", "\U000f0211", "\U000023e9", ">>"), + ("figma", "\U0000ef47", "\U0001f3a8", "fig"), + ("file", "\U000f0224", "\U0001f4c4", "[f]"), + ("file-code", "\U000f102b", "\U0001f4c4", "[<>]"), + ("file-plus", "\U000f0eed", "\U0001f4c4", "[+]"), + ("file-text", "\U000f09ee", "\U0001f4c4", "[t]"), + ("film", "\U000f0230", "\U0001f39e", "[#]"), + ("filter", "\U000f0233", "\U000023f7", "Y"), + ("fingerprint", "\U000f0237", "\U0001fac6", "(@)"), + ("firefox", "\U000f0239", "\U0001f98a", "ff"), + ("flag", "\U000f023d", "\U00002691", "|>"), + ("flask", "\U000f0096", "\U00002697", "/_\\"), + ("folder", "\U000f0256", "\U0001f4c1", "[d]"), + ("folder-open", "\U000f0dcf", "\U0001f4c2", "[d]"), + ("folder-plus", "\U000f0b9d", "\U0001f4c1", "[d+]"), + ("forward", "\U000f0496", "\U000021aa", "->"), + ("frown", "\U000f01f8", "\U0001f641", ":("), + ("gauge", "\U000f029a", "\U000023f2", "(/)"), + ("ghost", "\U000f02a0", "\U0001f47b", "gst"), + ("gift", "\U000f02a1", "\U0001f381", "[+]"), + ("git-branch", "\U0000f418", "\U00002387", "Y"), + ("git-commit", "\U0000f417", "\U000022b8", "-o-"), + ("git-merge", "\U0000f419", "\U00002442", ">-"), + ("git-pull-request", "\U0000f407", "\U000021c4", "PR"), + ("gitea", "\U0000f339", "\U0001f375", "gt"), + ("github", "\U000f02a4", "\U0001f419", "gh"), + ("gitlab", "\U000f0ba0", "\U0001f98a", "gl"), + ("globe", "\U000f059f", "\U0001f310", "(#)"), + ("gmail", "\U000f02ab", "\U00002709", "gm"), + ("go", "\U000f07d3", "\U0001f439", "go"), + ("google", "\U000f02ad", "\U000024bc", "g"), + ("google-meet", "\U000f0bdc", "\U0001f4f9", "meet"), + ("grid", "\U000f11d9", "\U000025a6", "::"), + ("grip", "\U000f01db", "\U0000283f", "::"), + ("hacker-news", "\U0000f1d4", "\U000024ce", "hn"), + ("hard-drive", "\U000f02ca", "\U0001f5b4", "[o]"), + ("hash", "\U000f0423", "#", "#"), + ("hashnode", "", "#", "hn#"), + ("heading", "\U000f0274", "H", "H"), + ("headphones", "\U000f02cb", "\U0001f3a7", "hp"), + ("heart", "\U000f02d5", "\U00002665", "<3"), + ("help", "\U000f0625", "\U00002753", "?"), + ("history", "\U000f02da", "\U0001f558", "<(t)"), + ("home", "\U000f06a1", "\U0001f3e0", "~"), + ("hourglass", "\U000f051f", "\U000023f3", "8"), + ("id-card", "\U000f0dab", "\U0001faaa", "[id]"), + ("image", "\U000f0976", "\U0001f5bc", "[^]"), + ("image-plus", "\U000f087c", "\U0001f5bc", "[+]"), + ("inbox", "\U000f0687", "\U0001f4e5", "[v]"), + ("info", "\U000f02fd", "\U00002139", "i"), + ("instagram", "\U000f02fe", "\U0001f4f7", "ig"), + ("italic", "\U000f0277", "\U0001d43c", "/"), + ("javascript", "\U000f031e", "JS", "js"), + ("jira", "\U000f0303", "\U000025c8", "jira"), + ("key", "\U000f0dd6", "\U0001f511", "o-"), + ("keybase", "\U0000edbf", "\U0001f511", "kb"), + ("keyboard", "\U000f097b", "\U00002328", "[kb]"), + ("kick", "", "\U000024c0", "kick"), + ("ko-fi", "\U000f0176", "\U00002615", "kofi"), + ("kubernetes", "\U000f10fe", "\U00002638", "k8s"), + ("laptop", "\U000f0322", "\U0001f4bb", "[_]"), + ("layers", "\U000f09fe", "\U00002630", "="), + ("leaf", "\U000f032a", "\U0001f343", "~"), + ("lemmy", "", "\U0001f42d", "lmy"), + ("lightbulb", "\U000f0336", "\U0001f4a1", "i"), + ("line", "\U0000f2fb", "\U0001f4ac", "line"), + ("link", "\U000f0339", "\U0001f517", "~"), + ("linkedin", "\U000f033b", "\U000024d8", "in"), + ("linux", "\U000f033d", "\U0001f427", "lnx"), + ("list", "\U000f0279", "\U00002637", "-="), + ("list-bullet", "\U000f0279", "\U00002022", "*"), + ("list-ordered", "\U000f027b", "\U00002488", "1."), + ("loader", "\U000f0772", "\U000025cc", "..."), + ("lock", "\U000f0341", "\U0001f512", "[#]"), + ("log-in", "\U000f0342", "\U000021e5", "->|"), + ("log-out", "\U000f0343", "\U000021e4", "|->"), + ("mail", "\U000f01f0", "\U00002709", "@"), + ("mail-open", "\U000f05ef", "\U0001f4e8", "@"), + ("map", "\U000f0982", "\U0001f5fa", "[#]"), + ("map-pin", "\U000f07d9", "\U0001f4cd", "@"), + ("mastodon", "\U000f0ad1", "\U0001f418", "mdn"), + ("matrix", "\U000f0628", "\U000024c2", "[m]"), + ("mattermost", "", "\U000024c2", "mm"), + ("maximize", "\U000f0293", "\U000026f6", "[ ]"), + ("medium", "\U0000f23a", "\U000024c2", "md"), + ("megaphone", "\U000f0b23", "\U0001f4e3", "<|"), + ("menu", "\U000f035c", "\U00002630", "="), + ("messenger", "\U000f020e", "\U0001f4ac", "msg"), + ("mic", "\U000f036e", "\U0001f3a4", "mic"), + ("mic-off", "\U000f036d", "\U0001f507", "x-m"), + ("microsoft", "\U000f0372", "\U0000229e", "ms"), + ("minimize", "\U000f0294", "\U000022a1", "]["), + ("minus", "\U000f0374", "\U00002212", "-"), + ("minus-circle", "\U000f0377", "\U00002296", "(-)"), + ("misskey", "", "\U000024c2", "mk"), + ("monitor", "\U000f0379", "\U0001f5a5", "[ ]"), + ("moon", "\U000f0594", "\U0001f319", "C"), + ("more-horizontal", "\U000f01d8", "\U000022ef", "..."), + ("more-vertical", "\U000f01d9", "\U000022ee", ":"), + ("mouse", "\U000f037d", "\U0001f5b1", "(|)"), + ("move", "\U000f01be", "\U00002725", "+"), + ("music", "\U000f0387", "\U0001f3b5", "#"), + ("navigation", "\U000f18f1", "\U000027b6", ">"), + ("netlify", "\U0000e83c", "\U000025c6", "ntl"), + ("newspaper", "\U000f1004", "\U0001f4f0", "[n]"), + ("nodejs", "\U000f0399", "\U00002b22", "node"), + ("notion", "\U0000e848", "\U000024c3", "ntn"), + ("npm", "\U000f06f7", "\U0001f4e6", "npm"), + ("online", "\U000f0aa5", "\U0001f7e2", "(o)"), + ("openai", "", "\U0000273a", "oai"), + ("package", "\U000f03d7", "\U0001f4e6", "[#]"), + ("palette", "\U000f0e0c", "\U0001f3a8", "(:)"), + ("paperclip", "\U000f03e2", "\U0001f4ce", "0/"), + ("patreon", "\U000f0882", "\U000024c5", "pat"), + ("pause", "\U000f03e4", "\U000023f8", "||"), + ("paypal", "\U0000f1ed", "\U000024c5", "pp"), + ("peertube", "", "\U000025b6", "pt"), + ("pen-tool", "\U000f0d13", "\U00002712", "_/"), + ("percent", "\U000f03f0", "%", "%"), + ("phone", "\U000f0df0", "\U0000260e", "tel"), + ("phone-call", "\U000f1182", "\U0001f4de", "tel"), + ("phone-off", "\U000f11a6", "\U0001f4f5", "x-t"), + ("pin", "\U000f0931", "\U0001f4cc", "-|"), + ("pinterest", "\U000f0407", "\U0001f4cc", "pin"), + ("pixelfed", "", "\U0001f5bc", "pxf"), + ("play", "\U000f040a", "\U000025b6", ">"), + ("plug", "\U000f1425", "\U0001f50c", "-["), + ("plus-circle", "\U000f0419", "\U00002295", "(+)"), + ("power", "\U000f0425", "\U000023fb", "(|)"), + ("print", "\U000f1786", "\U0001f5a8", "prn"), + ("product-hunt", "\U0000f288", "\U000024c5", "ph"), + ("proton-mail", "\U000f01f1", "\U00002709", "pm"), + ("puzzle", "\U000f0a66", "\U0001f9e9", "[+]"), + ("python", "\U000f0320", "\U0001f40d", "py"), + ("qr-code", "\U000f0432", "\U000025a6", "[#]"), + ("quote", "\U000f0757", "\U0000275d", "\""), + ("radio", "\U000f0003", "\U0001f4fb", "(o)"), + ("radio-off", "\U0000f111", "\U000025cb", "( )"), + ("radio-on", "\U000f043e", "\U000025c9", "(*)"), + ("railway", "\U0000e883", "\U0001f686", "rly"), + ("receipt", "\U000f0449", "\U0001f9fe", "[$]"), + ("reddit", "\U000f044d", "\U0001f47d", "rd"), + ("redo", "\U000f044e", "\U000021b7", "->"), + ("refresh", "\U000f0450", "\U000027f3", "@"), + ("repeat", "\U000f0456", "\U0001f501", "<->"), + ("reply", "\U000f045a", "\U000021a9", "<-"), + ("rewind", "\U000f045f", "\U000023ea", "<<"), + ("rocket", "\U000f14df", "\U0001f680", "^"), + ("rocketchat", "\U0000ed20", "\U0001f680", "rc"), + ("rss", "\U000f046b", "\U0001f4e1", "rss"), + ("ruler", "\U000f046d", "\U0001f4cf", "|-|"), + ("rust", "\U000f1617", "\U0001f980", "rs"), + ("safari", "\U000f0039", "\U0001f9ed", "saf"), + ("save", "\U000f0818", "\U0001f4be", "[s]"), + ("search", "\U000f0349", "\U0001f50d", "?"), + ("send", "\U000f1165", "\U000027a4", ">>"), + ("server", "\U000f048b", "\U0001f5a5", "[:]"), + ("settings", "\U000f0493", "\U00002699", "*"), + ("share", "\U000f1514", "\U00002934", "<"), + ("share-out", "\U000f0b93", "\U000021ea", "^"), + ("shield", "\U000f0499", "\U0001f6e1", "[S]"), + ("shield-check", "\U000f0cc8", "\U0001f6e1", "[v]"), + ("shopify", "\U000f049a", "\U0001f6cd", "shp"), + ("shuffle", "\U000f049f", "\U0001f500", "><"), + ("sidebar", "\U000f10aa", "\U000025a5", "|="), + ("signal", "\U000f116d", "\U0001f4ac", "sig"), + ("skip-back", "\U000f04ae", "\U000023ee", "|<"), + ("skip-forward", "\U000f04ad", "\U000023ed", ">|"), + ("skype", "\U000f04af", "\U000024c8", "sky"), + ("slack", "\U000f04b1", "#", "slk"), + ("sliders", "\U000f1542", "\U0001f39a", "=|="), + ("smartphone", "\U000f011c", "\U0001f4f1", "[.]"), + ("smile", "\U000f01f5", "\U0001f642", ":)"), + ("sms", "\U000f1170", "\U0001f4ac", "sms"), + ("snapchat", "\U000f04b6", "\U0001f47b", "snap"), + ("sort", "\U000f04ba", "\U000021c5", "^v"), + ("sort-asc", "\U000f04bc", "\U00002191", "a-z"), + ("sort-desc", "\U000f04bd", "\U00002193", "z-a"), + ("soundcloud", "\U000f04c0", "\U00002601", "sc"), + ("sourcehut", "", "\U000025ef", "srht"), + ("sparkles", "\U000f0674", "\U00002728", "*+"), + ("spotify", "\U000f04c7", "\U0001f3b5", "spot"), + ("stack-overflow", "\U000f04cc", "\U0001f4da", "so"), + ("star", "\U000f04d2", "\U00002605", "*"), + ("stop", "\U000f04db", "\U000023f9", "[]"), + ("store", "\U000f10c1", "\U0001f3ea", "[S]"), + ("strikethrough", "\U000f0280", "S\U00000336", "-s-"), + ("stripe", "\U0000ed53", "\U000024c8", "str"), + ("substack", "\U000f0fb1", "\U00002709", "ss"), + ("sun", "\U000f05a8", "\U00002600", "*"), + ("supabase", "\U0000e8b6", "\U000026a1", "sb"), + ("table", "\U000f04eb", "\U000025a6", "[#]"), + ("tablet", "\U000f04f6", "\U0001f4f1", "[..]"), + ("tag", "\U000f04fc", "\U0001f3f7", "#"), + ("target", "\U000f04fe", "\U0001f3af", "(o)"), + ("telegram", "\U0000f2c6", "\U00002708", "tg"), + ("terminal", "\U000f018d", "\U00002328", ">_"), + ("theme", "\U000f050e", "\U000025d0", "(|)"), + ("thermometer", "\U000f050f", "\U0001f321", "|o"), + ("threads", "\U000f0065", "@", "th"), + ("thumbs-down", "\U000f0512", "\U0001f44e", "-1"), + ("thumbs-up", "\U000f0514", "\U0001f44d", "+1"), + ("tiktok", "\U000f0387", "\U0000266a", "tt"), + ("timer", "\U000f051b", "\U000023f1", "(:)"), + ("toggle-off", "\U000f0a19", "\U0001f518", "[o=]"), + ("toggle-on", "\U000f0521", "\U0001f518", "[=o]"), + ("tor", "\U0000f371", "\U0001f9c5", "tor"), + ("translate", "\U000f05ca", "\U0001f310", "A/a"), + ("trello", "\U000f0532", "\U000025a4", "tr"), + ("trophy", "\U000f053a", "\U0001f3c6", "\\_/"), + ("truck", "\U000f129d", "\U0001f69a", "[=o"), + ("tumblr", "\U0000f173", "\U000024e3", "tb"), + ("tv", "\U000f0502", "\U0001f4fa", "[_]"), + ("twitch", "\U000f0543", "\U0001f4fa", "ttv"), + ("type", "\U000f0284", "T", "T"), + ("typescript", "\U000f06e6", "TS", "ts"), + ("ubuntu", "\U000f0548", "\U000025ce", "ubu"), + ("umbrella", "\U000f054b", "\U00002602", "T"), + ("underline", "\U000f0287", "U\U00000332", "_"), + ("undo", "\U000f054c", "\U000021b6", "<-"), + ("unlink", "\U000f033a", "\U000026d3", "~/"), + ("unlock", "\U000f0fc7", "\U0001f513", "[ ]"), + ("upload", "\U000f0552", "\U00002912", "^_"), + ("user", "\U000f0013", "\U0001f464", "@"), + ("user-check", "\U000f0be2", "\U0001f464", "@v"), + ("user-circle", "\U000f0b55", "\U0001f464", "(@)"), + ("user-minus", "\U000f0aec", "\U0001f464", "@-"), + ("user-plus", "\U000f0801", "\U0001f464", "@+"), + ("users", "\U000f000f", "\U0001f465", "@@"), + ("vercel", "\U000f0536", "\U000025b2", "vc"), + ("video", "\U000f0bdc", "\U0001f4f9", "[>"), + ("vimeo", "\U000f0577", "\U000024e5", "vm"), + ("voicemail", "\U000f057d", "\U00002328", "oo"), + ("volume", "\U000f057e", "\U0001f50a", "<))"), + ("volume-low", "\U000f0580", "\U0001f509", "<)"), + ("volume-off", "\U000f0581", "\U0001f507", ""), + ("wechat", "\U000f0611", "\U0001f4ac", "wx"), + ("whatsapp", "\U000f05a3", "\U0001f4de", "wa"), + ("wifi", "\U000f05a9", "\U0001f4f6", "((."), + ("wifi-off", "\U000f05aa", "\U0001f4f5", "x(("), + ("windows", "\U000f05b3", "\U0000229e", "win"), + ("wordpress", "\U000f05b4", "\U000024cc", "wp"), + ("x", "\U000f0b05", "\U0001d54f", "x"), + ("x-circle", "\U000f015a", "\U0000274e", "(x)"), + ("xmpp", "\U000f07ff", "\U0001f4ac", "xmpp"), + ("y-combinator", "\U0000f23b", "\U000024ce", "yc"), + ("youtube", "\U000f05c3", "\U000025b6", "yt"), + ("zap", "\U000f140c", "\U000026a1", "/"), + ("zoom", "\U000f0567", "\U0001f4f9", "zm"), + ("zoom-in", "\U000f06ed", "\U0001f50e", "+?"), + ("zoom-out", "\U000f06ec", "\U0001f50e", "-?"), + ("zulip", "", "\U000024cf", "zl"), +) + +OPENICON_ALIASES = { + "a11y": "accessibility", + "account": "user", + "add-user": "user-plus", + "address-book": "contact", + "ai": "sparkles", + "alarm-clock": "alarm", + "alert-circle": "error", + "alert-triangle": "warning", + "announce": "megaphone", + "at-sign": "at", + "attach": "paperclip", + "attachment": "paperclip", + "bin": "delete", + "biometric": "fingerprint", + "blocked": "ban", + "bolt": "zap", + "box": "package", + "bullhorn": "megaphone", + "bullseye": "target", + "call": "phone", + "card": "credit-card", + "caution": "warning", + "cellphone": "smartphone", + "check-square": "checkbox", + "chip": "cpu", + "cli": "terminal", + "cmd": "command", + "cog": "settings", + "color": "palette", + "comment": "chat", + "company": "building", + "computer": "laptop", + "console": "terminal", + "contrast": "theme", + "curly-braces": "braces", + "currency": "dollar", + "dark-mode": "moon", + "date": "calendar", + "day": "sun", + "db": "database", + "delivery": "truck", + "deploy": "rocket", + "desktop": "monitor", + "directory": "folder", + "discount": "percent", + "dislike": "thumbs-down", + "display": "monitor", + "document": "file", + "duplicate": "copy", + "ellipsis": "more-horizontal", + "email": "mail", + "envelope": "mail", + "exit-fullscreen": "minimize", + "experiment": "flask", + "extension": "puzzle", + "faq": "help", + "favorite": "star", + "find": "search", + "floppy": "save", + "forbidden": "ban", + "fullscreen": "maximize", + "gear": "settings", + "goal": "target", + "golang": "go", + "hamburger": "menu", + "happy": "smile", + "hashtag": "hash", + "hide": "eye-off", + "house": "home", + "i18n": "translate", + "idea": "lightbulb", + "identity": "id-card", + "information": "info", + "integration": "plug", + "invite": "user-plus", + "invoice": "receipt", + "job": "briefcase", + "json": "braces", + "kebab": "more-vertical", + "lab": "flask", + "label": "tag", + "language": "translate", + "light-mode": "sun", + "lightning": "zap", + "like": "thumbs-up", + "loading": "loader", + "location": "map-pin", + "login": "log-in", + "logout": "log-out", + "love": "heart", + "magic": "sparkles", + "magnify": "search", + "marker": "map-pin", + "mention": "at", + "merge-request": "git-pull-request", + "message": "chat", + "microphone": "mic", + "mobile": "smartphone", + "movie": "film", + "mute": "volume-off", + "night": "moon", + "node": "nodejs", + "notification": "bell", + "office": "building", + "open-in-new": "external-link", + "paper-plane": "send", + "payment": "credit-card", + "pencil": "edit", + "pending": "hourglass", + "people": "users", + "person": "user", + "photo": "image", + "photo-camera": "camera", + "picture": "image", + "plugin": "puzzle", + "plus": "add", + "preferences": "settings", + "present": "gift", + "printer": "print", + "prize": "trophy", + "processor": "cpu", + "profile": "user", + "pull-request": "git-pull-request", + "qr": "qr-code", + "question": "help", + "recent": "history", + "reception": "cell-signal", + "reload": "refresh", + "scissors": "cut", + "shell": "terminal", + "shipping": "truck", + "shopping-bag": "bag", + "shopping-cart": "cart", + "show": "eye", + "sign-in": "log-in", + "sign-out": "log-out", + "song": "music", + "sound": "volume", + "source-code": "code", + "speaker": "volume", + "spinner": "loader", + "square": "checkbox-empty", + "stopwatch": "timer", + "storefront": "store", + "sync": "refresh", + "team": "users", + "telephone": "phone", + "television": "tv", + "thumbtack": "pin", + "times": "close", + "trash": "delete", + "twitter": "x", + "usd": "dollar", + "view": "eye", + "web": "globe", + "work": "briefcase", + "world": "globe", + "x-mark": "close", +} diff --git a/ports/python/tests/test_icons.py b/ports/python/tests/test_icons.py new file mode 100644 index 0000000..0c26103 --- /dev/null +++ b/ports/python/tests/test_icons.py @@ -0,0 +1,42 @@ +"""The same cases as packages/hqtui/test/icons.test.ts.""" +import unittest + +from hqtui import icon, icon_glyphs, icon_mode, icon_names, set_icon_mode +from hqtui.icons_data import OPENICON_GLYPHS + + +class IconTests(unittest.TestCase): + def test_modes_and_aliases(self): + self.assertEqual(icon("mail", "nerd"), "\U000f01f0") + self.assertEqual(icon("mail", "unicode"), "✉") + self.assertEqual(icon("mail", "ascii"), "@") + self.assertEqual(icon("email", "ascii"), "@") + self.assertEqual(icon("twitter", "ascii"), "x") + self.assertEqual(icon("no-such-icon"), "") + + def test_nerd_falls_back_to_unicode(self): + key, _, uni, _ = next(row for row in OPENICON_GLYPHS if row[1] == "") + self.assertEqual(icon(key, "nerd"), uni) + + def test_mode_order(self): + self.assertEqual(icon_mode({"OPENICON_GLYPHS": "ascii", "NERD_FONT": "1", "LANG": "en_US.UTF-8"}), "ascii") + self.assertEqual(icon_mode({"HQTUI_ICONS": "nerd"}), "nerd") + self.assertEqual(icon_mode({"NERD_FONT": "1"}), "nerd") + self.assertEqual(icon_mode({"LANG": "en_US.UTF-8", "TERM": "xterm-256color"}), "unicode") + self.assertEqual(icon_mode({"TERM": "dumb"}), "ascii") + set_icon_mode("ascii") + try: + self.assertEqual(icon_mode({"NERD_FONT": "1"}), "ascii") + self.assertEqual(icon("phone"), "tel") + finally: + set_icon_mode(None) + + def test_table(self): + names = icon_names() + self.assertGreaterEqual(len(names), 300) + self.assertEqual(names, sorted(names)) + self.assertIsNotNone(icon_glyphs("github")) + + +if __name__ == "__main__": + unittest.main() diff --git a/ports/rust/src/capabilities.rs b/ports/rust/src/capabilities.rs index 77bd3fd..b64924d 100644 --- a/ports/rust/src/capabilities.rs +++ b/ports/rust/src/capabilities.rs @@ -180,7 +180,7 @@ fn detect_colors(env: &Env, tty: bool) -> ColorDepth { ColorDepth::Ansi16 } -fn detect_unicode(env: &Env) -> bool { +pub(crate) fn detect_unicode(env: &Env) -> bool { // A dumb terminal has no glyph repertoire to speak of. The Linux console is // not in that category — its default font draws box and block elements // perfectly well — so only Braille is withheld from it, below. diff --git a/ports/rust/src/icons.rs b/ports/rust/src/icons.rs new file mode 100644 index 0000000..0c4ea52 --- /dev/null +++ b/ports/rust/src/icons.rs @@ -0,0 +1,149 @@ +//! Icons for terminals: `icon("mail")` is the best glyph this terminal can draw. +//! +//! The built-in pack is OpenIcon (), on by +//! default, generated into `icons_data.rs` from the same set as the +//! TypeScript reference, so an icon is the same glyph in every port. Which of +//! its three glyphs you get: [`set_icon_mode`] if the app chose; +//! `OPENICON_GLYPHS` or `HQTUI_ICONS`; `NERD_FONT=1` for Nerd Font glyphs; +//! otherwise Unicode where the terminal draws it and ASCII where it does not. +//! A Nerd Font is never assumed: it cannot be detected from inside the terminal. + +use std::sync::RwLock; + +use crate::capabilities::{detect_unicode, process_env, Env}; +use crate::icons_data::{OPENICON_ALIASES, OPENICON_GLYPHS}; +pub use crate::icons_data::OPENICON_VERSION; + +/// One of an icon's three glyph families. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum IconMode { + Nerd, + Unicode, + Ascii, +} + +impl IconMode { + fn parse(value: &str) -> Option { + match value { + "nerd" => Some(Self::Nerd), + "unicode" => Some(Self::Unicode), + "ascii" => Some(Self::Ascii), + _ => None, + } + } +} + +/// The three spellings of one icon; `nerd` is empty when Nerd Fonts has none. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct IconGlyphs { + pub nerd: &'static str, + pub unicode: &'static str, + pub ascii: &'static str, +} + +static CHOSEN: RwLock> = RwLock::new(None); + +/// Pin the glyph family for the whole app, or `None` to detect again. +pub fn set_icon_mode(mode: Option) { + *CHOSEN.write().unwrap_or_else(|e| e.into_inner()) = mode; +} + +/// The glyph family an environment gets, in the order in this module's docs. +pub fn icon_mode_in(env: &Env) -> IconMode { + if let Some(mode) = *CHOSEN.read().unwrap_or_else(|e| e.into_inner()) { + return mode; + } + let named = env + .get("OPENICON_GLYPHS") + .filter(|v| !v.is_empty()) + .or_else(|| env.get("HQTUI_ICONS")) + .map(String::as_str) + .unwrap_or(""); + if let Some(mode) = IconMode::parse(named) { + return mode; + } + if env.get("NERD_FONT").map(String::as_str) == Some("1") || env.get("NERD_FONTS").map(String::as_str) == Some("1") { + return IconMode::Nerd; + } + if detect_unicode(env) { + IconMode::Unicode + } else { + IconMode::Ascii + } +} + +/// An icon's glyphs by key or alias. +pub fn icon_glyphs(name: &str) -> Option { + let key = match OPENICON_GLYPHS.binary_search_by(|row| row.0.cmp(name)) { + Ok(_) => name, + Err(_) => { + let i = OPENICON_ALIASES.binary_search_by(|row| row.0.cmp(name)).ok()?; + OPENICON_ALIASES[i].1 + } + }; + let i = OPENICON_GLYPHS.binary_search_by(|row| row.0.cmp(key)).ok()?; + let (_, nerd, unicode, ascii) = OPENICON_GLYPHS[i]; + Some(IconGlyphs { nerd, unicode, ascii }) +} + +/// An icon's glyph in a given mode. Unknown names return `""`. +pub fn icon_in(name: &str, mode: IconMode) -> &'static str { + let Some(g) = icon_glyphs(name) else { return "" }; + match mode { + IconMode::Nerd if !g.nerd.is_empty() => g.nerd, + IconMode::Nerd | IconMode::Unicode => g.unicode, + IconMode::Ascii => g.ascii, + } +} + +/// The best glyph for an icon in this process's terminal. +pub fn icon(name: &str) -> &'static str { + icon_in(name, icon_mode_in(&process_env())) +} + +/// Every key in the built-in pack, sorted. +pub fn icon_names() -> impl Iterator { + OPENICON_GLYPHS.iter().map(|row| row.0) +} + +#[cfg(test)] +mod tests { + // The same cases as packages/hqtui/test/icons.test.ts. + use super::*; + + fn env(pairs: &[(&str, &str)]) -> Env { + pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect() + } + + #[test] + fn modes_and_aliases() { + assert_eq!(icon_in("mail", IconMode::Nerd), "\u{f01f0}"); + assert_eq!(icon_in("mail", IconMode::Unicode), "✉"); + assert_eq!(icon_in("mail", IconMode::Ascii), "@"); + assert_eq!(icon_in("email", IconMode::Ascii), "@"); + assert_eq!(icon_in("twitter", IconMode::Ascii), "x"); + assert_eq!(icon_in("no-such-icon", IconMode::Unicode), ""); + } + + #[test] + fn nerd_falls_back_to_unicode() { + let row = OPENICON_GLYPHS.iter().find(|r| r.1.is_empty()).expect("an icon without a Nerd glyph"); + assert_eq!(icon_in(row.0, IconMode::Nerd), row.2); + } + + #[test] + fn mode_order() { + assert_eq!(icon_mode_in(&env(&[("OPENICON_GLYPHS", "ascii"), ("NERD_FONT", "1"), ("LANG", "en_US.UTF-8")])), IconMode::Ascii); + assert_eq!(icon_mode_in(&env(&[("HQTUI_ICONS", "nerd")])), IconMode::Nerd); + assert_eq!(icon_mode_in(&env(&[("NERD_FONT", "1")])), IconMode::Nerd); + assert_eq!(icon_mode_in(&env(&[("LANG", "en_US.UTF-8"), ("TERM", "xterm-256color")])), IconMode::Unicode); + assert_eq!(icon_mode_in(&env(&[("TERM", "dumb")])), IconMode::Ascii); + } + + #[test] + fn tables_are_sorted_for_binary_search() { + assert!(OPENICON_GLYPHS.windows(2).all(|w| w[0].0 < w[1].0)); + assert!(OPENICON_ALIASES.windows(2).all(|w| w[0].0 < w[1].0)); + assert!(icon_names().count() >= 300); + } +} diff --git a/ports/rust/src/icons_data.rs b/ports/rust/src/icons_data.rs new file mode 100644 index 0000000..2624965 --- /dev/null +++ b/ports/rust/src/icons_data.rs @@ -0,0 +1,541 @@ +// Generated by packages/hqtui/scripts/generate-icons.ts from OpenIcon 2026-09-24 (OpenIcon 0.1), 370 icons. Do not edit. +// (key, nerd, unicode, ascii); an empty nerd means Nerd Fonts has no glyph for it. + +/// The OpenIcon set the built-in pack was generated from. +pub const OPENICON_VERSION: &str = "2026-09-24"; + +/// Sorted by key, so lookups can binary-search. +pub static OPENICON_GLYPHS: &[(&str, &str, &str, &str)] = &[ + ("accessibility", "\u{f02e6}", "\u{267f}", "a11y"), + ("activity", "\u{f0430}", "\u{1f4c8}", "/\\/"), + ("add", "\u{f0415}", "+", "+"), + ("alarm", "\u{f0020}", "\u{23f0}", "(!)"), + ("align-center", "\u{f0260}", "\u{2261}", "="), + ("align-left", "\u{f0262}", "\u{2af7}", "|="), + ("align-right", "\u{f0263}", "\u{2af8}", "=|"), + ("amazon", "\u{f270}", "\u{24d0}", "amz"), + ("anchor", "\u{f0031}", "\u{2693}", "t"), + ("android", "\u{f0032}", "\u{1f916}", "and"), + ("anthropic", "", "\u{24b6}", "ant"), + ("api", "\u{f109b}", "\u{2699}", "api"), + ("apple", "\u{f0035}", "\u{1f34e}", "mac"), + ("apple-music", "\u{f2eb}", "\u{1f3b5}", "am"), + ("archive", "\u{f120e}", "\u{1f5c4}", "[_]"), + ("arrow-down", "\u{f0045}", "\u{2193}", "v"), + ("arrow-left", "\u{f004d}", "\u{2190}", "<-"), + ("arrow-right", "\u{f0054}", "\u{2192}", "->"), + ("arrow-up", "\u{f005d}", "\u{2191}", "^"), + ("arrow-up-right", "\u{f005c}", "\u{2197}", "/^"), + ("at", "\u{f0065}", "@", "@"), + ("award", "\u{f1326}", "\u{1f3c5}", "(*)"), + ("bag", "\u{f11d5}", "\u{1f6cd}", "[u]"), + ("ban", "\u{f073a}", "\u{1f6ab}", "(/)"), + ("bandcamp", "\u{f2d5}", "\u{25e2}", "bc"), + ("barcode", "\u{f0071}", "\u{25a5}", "|||"), + ("battery", "\u{f008e}", "\u{1f50b}", "[=="), + ("battery-charging", "\u{f0084}", "\u{1f50c}", "[=~"), + ("behance", "\u{f1b4}", "B\u{113}", "be"), + ("bell", "\u{f009c}", "\u{1f514}", "(!)"), + ("bell-off", "\u{f0a91}", "\u{1f515}", "(x)"), + ("bitbucket", "\u{f00a8}", "\u{1faa3}", "bb"), + ("bitcoin", "\u{f0813}", "\u{20bf}", "btc"), + ("bluesky", "\u{f1589}", "\u{1f98b}", "bsky"), + ("bluetooth", "\u{f00af}", "\u{16d2}", "B"), + ("bold", "\u{f0264}", "\u{1d401}", "B"), + ("book", "\u{f0b64}", "\u{1f4d5}", "[B]"), + ("book-open", "\u{f0b63}", "\u{1f4d6}", "[]"), + ("bookmark", "\u{f00c3}", "\u{1f516}", "[]>"), + ("braces", "\u{f0169}", "{}", "{}"), + ("brave", "\u{f0499}", "\u{1f981}", "brv"), + ("briefcase", "\u{f0814}", "\u{1f4bc}", "[b]"), + ("brush", "\u{f00e3}", "\u{1f58c}", "/~"), + ("bug", "\u{f0a30}", "\u{1f41b}", "bug"), + ("building", "\u{f151f}", "\u{1f3e2}", "[#]"), + ("bun", "\u{e76f}", "\u{1f95f}", "bun"), + ("buy-me-a-coffee", "\u{f0176}", "\u{2615}", "bmc"), + ("calendar", "\u{f0b66}", "\u{1f4c5}", "[=]"), + ("calendar-check", "\u{f0c44}", "\u{1f4c5}", "[v]"), + ("calendar-plus", "\u{f00f3}", "\u{1f4c5}", "[+]"), + ("camera", "\u{f0d5d}", "\u{1f4f7}", "[o]"), + ("cart", "\u{f0111}", "\u{1f6d2}", "\\_/"), + ("cast", "\u{f0118}", "\u{1f4e1}", "))"), + ("cell-signal", "\u{f04a2}", "\u{1f4f6}", ".:|"), + ("chat", "\u{f0ede}", "\u{1f4ac}", "()"), + ("chat-dots", "\u{f12ca}", "\u{1f4ac}", "(..)"), + ("check", "\u{f012c}", "\u{2713}", "v"), + ("check-circle", "\u{f05e1}", "\u{2705}", "(v)"), + ("checkbox", "\u{f0135}", "\u{2611}", "[x]"), + ("checkbox-empty", "\u{f0131}", "\u{2610}", "[ ]"), + ("chevron-down", "\u{f0140}", "\u{2304}", "v"), + ("chevron-left", "\u{f0141}", "\u{2039}", "<"), + ("chevron-right", "\u{f0142}", "\u{203a}", ">"), + ("chevron-up", "\u{f0143}", "\u{2303}", "^"), + ("chevrons-left", "\u{f013d}", "\u{ab}", "<<"), + ("chevrons-right", "\u{f013e}", "\u{bb}", ">>"), + ("chrome", "\u{f02af}", "\u{25c9}", "chr"), + ("claude", "", "\u{2733}", "cl"), + ("clipboard", "\u{f014c}", "\u{1f4cb}", "[=]"), + ("clipboard-check", "\u{f08a8}", "\u{1f4cb}", "[v]"), + ("clock", "\u{f0150}", "\u{1f552}", "(t)"), + ("close", "\u{f0156}", "\u{2715}", "x"), + ("cloud", "\u{f0163}", "\u{2601}", "(~)"), + ("cloud-download", "\u{f0b7d}", "\u{2601}", "(v)"), + ("cloud-upload", "\u{f0b7e}", "\u{2601}", "(^)"), + ("cloudflare", "\u{e792}", "\u{2601}", "cf"), + ("code", "\u{f0174}", "\u{27e8}\u{27e9}", ""), + ("codeberg", "\u{f330}", "\u{26f0}", "cb"), + ("codepen", "\u{f0175}", "\u{2b21}", "cpn"), + ("coffee", "\u{f06ca}", "\u{2615}", "c[_]"), + ("coins", "\u{f1890}", "\u{1fa99}", "(o)"), + ("command", "\u{f0633}", "\u{2318}", "cmd"), + ("compass", "\u{f018c}", "\u{1f9ed}", "(N)"), + ("contact", "\u{f0dab}", "\u{1f4c7}", "[@]"), + ("container", "\u{f01a7}", "\u{1f4e6}", "[c]"), + ("copy", "\u{f018f}", "\u{29c9}", "cp"), + ("cpu", "\u{f061a}", "\u{1f532}", "[#]"), + ("credit-card", "\u{f019b}", "\u{1f4b3}", "[=]"), + ("crop", "\u{f019e}", "\u{2317}", "[_"), + ("crosshair", "\u{f01a3}", "\u{2316}", "-+-"), + ("cut", "\u{f0190}", "\u{2702}", "8<"), + ("database", "\u{f1632}", "\u{1f6e2}", "[=]"), + ("debian", "\u{f08da}", "\u{1f300}", "deb"), + ("delete", "\u{f0a7a}", "\u{1f5d1}", "del"), + ("deno", "\u{e7c0}", "\u{1f995}", "deno"), + ("dev-to", "\u{eef4}", "DEV", "dev"), + ("discord", "\u{f066f}", "\u{1f3ae}", "dc"), + ("docker", "\u{f0868}", "\u{1f433}", "dkr"), + ("dollar", "\u{f01c1}", "$", "$"), + ("download", "\u{f01da}", "\u{2913}", "v_"), + ("dribbble", "\u{f17d}", "\u{1f3c0}", "drb"), + ("droplet", "\u{f0e0a}", "\u{1f4a7}", "o"), + ("ebay", "\u{edbe}", "\u{24d4}", "ebay"), + ("edit", "\u{f03eb}", "\u{270e}", "/e"), + ("element", "\u{f0628}", "\u{24ba}", "el"), + ("enter", "\u{f0311}", "\u{21b5}", "<-|"), + ("error", "\u{f05d6}", "\u{26d4}", "!!"), + ("ethereum", "\u{f086a}", "\u{39e}", "eth"), + ("etsy", "\u{f2d7}", "\u{24ba}", "etsy"), + ("external-link", "\u{f03cc}", "\u{2197}", "->]"), + ("eye", "\u{f06d0}", "\u{1f441}", "o"), + ("eye-off", "\u{f06d1}", "\u{1f648}", "-o-"), + ("facebook", "\u{f020c}", "\u{24d5}", "fb"), + ("farcaster", "", "\u{26e9}", "fc"), + ("fast-forward", "\u{f0211}", "\u{23e9}", ">>"), + ("figma", "\u{ef47}", "\u{1f3a8}", "fig"), + ("file", "\u{f0224}", "\u{1f4c4}", "[f]"), + ("file-code", "\u{f102b}", "\u{1f4c4}", "[<>]"), + ("file-plus", "\u{f0eed}", "\u{1f4c4}", "[+]"), + ("file-text", "\u{f09ee}", "\u{1f4c4}", "[t]"), + ("film", "\u{f0230}", "\u{1f39e}", "[#]"), + ("filter", "\u{f0233}", "\u{23f7}", "Y"), + ("fingerprint", "\u{f0237}", "\u{1fac6}", "(@)"), + ("firefox", "\u{f0239}", "\u{1f98a}", "ff"), + ("flag", "\u{f023d}", "\u{2691}", "|>"), + ("flask", "\u{f0096}", "\u{2697}", "/_\\"), + ("folder", "\u{f0256}", "\u{1f4c1}", "[d]"), + ("folder-open", "\u{f0dcf}", "\u{1f4c2}", "[d]"), + ("folder-plus", "\u{f0b9d}", "\u{1f4c1}", "[d+]"), + ("forward", "\u{f0496}", "\u{21aa}", "->"), + ("frown", "\u{f01f8}", "\u{1f641}", ":("), + ("gauge", "\u{f029a}", "\u{23f2}", "(/)"), + ("ghost", "\u{f02a0}", "\u{1f47b}", "gst"), + ("gift", "\u{f02a1}", "\u{1f381}", "[+]"), + ("git-branch", "\u{f418}", "\u{2387}", "Y"), + ("git-commit", "\u{f417}", "\u{22b8}", "-o-"), + ("git-merge", "\u{f419}", "\u{2442}", ">-"), + ("git-pull-request", "\u{f407}", "\u{21c4}", "PR"), + ("gitea", "\u{f339}", "\u{1f375}", "gt"), + ("github", "\u{f02a4}", "\u{1f419}", "gh"), + ("gitlab", "\u{f0ba0}", "\u{1f98a}", "gl"), + ("globe", "\u{f059f}", "\u{1f310}", "(#)"), + ("gmail", "\u{f02ab}", "\u{2709}", "gm"), + ("go", "\u{f07d3}", "\u{1f439}", "go"), + ("google", "\u{f02ad}", "\u{24bc}", "g"), + ("google-meet", "\u{f0bdc}", "\u{1f4f9}", "meet"), + ("grid", "\u{f11d9}", "\u{25a6}", "::"), + ("grip", "\u{f01db}", "\u{283f}", "::"), + ("hacker-news", "\u{f1d4}", "\u{24ce}", "hn"), + ("hard-drive", "\u{f02ca}", "\u{1f5b4}", "[o]"), + ("hash", "\u{f0423}", "#", "#"), + ("hashnode", "", "#", "hn#"), + ("heading", "\u{f0274}", "H", "H"), + ("headphones", "\u{f02cb}", "\u{1f3a7}", "hp"), + ("heart", "\u{f02d5}", "\u{2665}", "<3"), + ("help", "\u{f0625}", "\u{2753}", "?"), + ("history", "\u{f02da}", "\u{1f558}", "<(t)"), + ("home", "\u{f06a1}", "\u{1f3e0}", "~"), + ("hourglass", "\u{f051f}", "\u{23f3}", "8"), + ("id-card", "\u{f0dab}", "\u{1faaa}", "[id]"), + ("image", "\u{f0976}", "\u{1f5bc}", "[^]"), + ("image-plus", "\u{f087c}", "\u{1f5bc}", "[+]"), + ("inbox", "\u{f0687}", "\u{1f4e5}", "[v]"), + ("info", "\u{f02fd}", "\u{2139}", "i"), + ("instagram", "\u{f02fe}", "\u{1f4f7}", "ig"), + ("italic", "\u{f0277}", "\u{1d43c}", "/"), + ("javascript", "\u{f031e}", "JS", "js"), + ("jira", "\u{f0303}", "\u{25c8}", "jira"), + ("key", "\u{f0dd6}", "\u{1f511}", "o-"), + ("keybase", "\u{edbf}", "\u{1f511}", "kb"), + ("keyboard", "\u{f097b}", "\u{2328}", "[kb]"), + ("kick", "", "\u{24c0}", "kick"), + ("ko-fi", "\u{f0176}", "\u{2615}", "kofi"), + ("kubernetes", "\u{f10fe}", "\u{2638}", "k8s"), + ("laptop", "\u{f0322}", "\u{1f4bb}", "[_]"), + ("layers", "\u{f09fe}", "\u{2630}", "="), + ("leaf", "\u{f032a}", "\u{1f343}", "~"), + ("lemmy", "", "\u{1f42d}", "lmy"), + ("lightbulb", "\u{f0336}", "\u{1f4a1}", "i"), + ("line", "\u{f2fb}", "\u{1f4ac}", "line"), + ("link", "\u{f0339}", "\u{1f517}", "~"), + ("linkedin", "\u{f033b}", "\u{24d8}", "in"), + ("linux", "\u{f033d}", "\u{1f427}", "lnx"), + ("list", "\u{f0279}", "\u{2637}", "-="), + ("list-bullet", "\u{f0279}", "\u{2022}", "*"), + ("list-ordered", "\u{f027b}", "\u{2488}", "1."), + ("loader", "\u{f0772}", "\u{25cc}", "..."), + ("lock", "\u{f0341}", "\u{1f512}", "[#]"), + ("log-in", "\u{f0342}", "\u{21e5}", "->|"), + ("log-out", "\u{f0343}", "\u{21e4}", "|->"), + ("mail", "\u{f01f0}", "\u{2709}", "@"), + ("mail-open", "\u{f05ef}", "\u{1f4e8}", "@"), + ("map", "\u{f0982}", "\u{1f5fa}", "[#]"), + ("map-pin", "\u{f07d9}", "\u{1f4cd}", "@"), + ("mastodon", "\u{f0ad1}", "\u{1f418}", "mdn"), + ("matrix", "\u{f0628}", "\u{24c2}", "[m]"), + ("mattermost", "", "\u{24c2}", "mm"), + ("maximize", "\u{f0293}", "\u{26f6}", "[ ]"), + ("medium", "\u{f23a}", "\u{24c2}", "md"), + ("megaphone", "\u{f0b23}", "\u{1f4e3}", "<|"), + ("menu", "\u{f035c}", "\u{2630}", "="), + ("messenger", "\u{f020e}", "\u{1f4ac}", "msg"), + ("mic", "\u{f036e}", "\u{1f3a4}", "mic"), + ("mic-off", "\u{f036d}", "\u{1f507}", "x-m"), + ("microsoft", "\u{f0372}", "\u{229e}", "ms"), + ("minimize", "\u{f0294}", "\u{22a1}", "]["), + ("minus", "\u{f0374}", "\u{2212}", "-"), + ("minus-circle", "\u{f0377}", "\u{2296}", "(-)"), + ("misskey", "", "\u{24c2}", "mk"), + ("monitor", "\u{f0379}", "\u{1f5a5}", "[ ]"), + ("moon", "\u{f0594}", "\u{1f319}", "C"), + ("more-horizontal", "\u{f01d8}", "\u{22ef}", "..."), + ("more-vertical", "\u{f01d9}", "\u{22ee}", ":"), + ("mouse", "\u{f037d}", "\u{1f5b1}", "(|)"), + ("move", "\u{f01be}", "\u{2725}", "+"), + ("music", "\u{f0387}", "\u{1f3b5}", "#"), + ("navigation", "\u{f18f1}", "\u{27b6}", ">"), + ("netlify", "\u{e83c}", "\u{25c6}", "ntl"), + ("newspaper", "\u{f1004}", "\u{1f4f0}", "[n]"), + ("nodejs", "\u{f0399}", "\u{2b22}", "node"), + ("notion", "\u{e848}", "\u{24c3}", "ntn"), + ("npm", "\u{f06f7}", "\u{1f4e6}", "npm"), + ("online", "\u{f0aa5}", "\u{1f7e2}", "(o)"), + ("openai", "", "\u{273a}", "oai"), + ("package", "\u{f03d7}", "\u{1f4e6}", "[#]"), + ("palette", "\u{f0e0c}", "\u{1f3a8}", "(:)"), + ("paperclip", "\u{f03e2}", "\u{1f4ce}", "0/"), + ("patreon", "\u{f0882}", "\u{24c5}", "pat"), + ("pause", "\u{f03e4}", "\u{23f8}", "||"), + ("paypal", "\u{f1ed}", "\u{24c5}", "pp"), + ("peertube", "", "\u{25b6}", "pt"), + ("pen-tool", "\u{f0d13}", "\u{2712}", "_/"), + ("percent", "\u{f03f0}", "%", "%"), + ("phone", "\u{f0df0}", "\u{260e}", "tel"), + ("phone-call", "\u{f1182}", "\u{1f4de}", "tel"), + ("phone-off", "\u{f11a6}", "\u{1f4f5}", "x-t"), + ("pin", "\u{f0931}", "\u{1f4cc}", "-|"), + ("pinterest", "\u{f0407}", "\u{1f4cc}", "pin"), + ("pixelfed", "", "\u{1f5bc}", "pxf"), + ("play", "\u{f040a}", "\u{25b6}", ">"), + ("plug", "\u{f1425}", "\u{1f50c}", "-["), + ("plus-circle", "\u{f0419}", "\u{2295}", "(+)"), + ("power", "\u{f0425}", "\u{23fb}", "(|)"), + ("print", "\u{f1786}", "\u{1f5a8}", "prn"), + ("product-hunt", "\u{f288}", "\u{24c5}", "ph"), + ("proton-mail", "\u{f01f1}", "\u{2709}", "pm"), + ("puzzle", "\u{f0a66}", "\u{1f9e9}", "[+]"), + ("python", "\u{f0320}", "\u{1f40d}", "py"), + ("qr-code", "\u{f0432}", "\u{25a6}", "[#]"), + ("quote", "\u{f0757}", "\u{275d}", "\""), + ("radio", "\u{f0003}", "\u{1f4fb}", "(o)"), + ("radio-off", "\u{f111}", "\u{25cb}", "( )"), + ("radio-on", "\u{f043e}", "\u{25c9}", "(*)"), + ("railway", "\u{e883}", "\u{1f686}", "rly"), + ("receipt", "\u{f0449}", "\u{1f9fe}", "[$]"), + ("reddit", "\u{f044d}", "\u{1f47d}", "rd"), + ("redo", "\u{f044e}", "\u{21b7}", "->"), + ("refresh", "\u{f0450}", "\u{27f3}", "@"), + ("repeat", "\u{f0456}", "\u{1f501}", "<->"), + ("reply", "\u{f045a}", "\u{21a9}", "<-"), + ("rewind", "\u{f045f}", "\u{23ea}", "<<"), + ("rocket", "\u{f14df}", "\u{1f680}", "^"), + ("rocketchat", "\u{ed20}", "\u{1f680}", "rc"), + ("rss", "\u{f046b}", "\u{1f4e1}", "rss"), + ("ruler", "\u{f046d}", "\u{1f4cf}", "|-|"), + ("rust", "\u{f1617}", "\u{1f980}", "rs"), + ("safari", "\u{f0039}", "\u{1f9ed}", "saf"), + ("save", "\u{f0818}", "\u{1f4be}", "[s]"), + ("search", "\u{f0349}", "\u{1f50d}", "?"), + ("send", "\u{f1165}", "\u{27a4}", ">>"), + ("server", "\u{f048b}", "\u{1f5a5}", "[:]"), + ("settings", "\u{f0493}", "\u{2699}", "*"), + ("share", "\u{f1514}", "\u{2934}", "<"), + ("share-out", "\u{f0b93}", "\u{21ea}", "^"), + ("shield", "\u{f0499}", "\u{1f6e1}", "[S]"), + ("shield-check", "\u{f0cc8}", "\u{1f6e1}", "[v]"), + ("shopify", "\u{f049a}", "\u{1f6cd}", "shp"), + ("shuffle", "\u{f049f}", "\u{1f500}", "><"), + ("sidebar", "\u{f10aa}", "\u{25a5}", "|="), + ("signal", "\u{f116d}", "\u{1f4ac}", "sig"), + ("skip-back", "\u{f04ae}", "\u{23ee}", "|<"), + ("skip-forward", "\u{f04ad}", "\u{23ed}", ">|"), + ("skype", "\u{f04af}", "\u{24c8}", "sky"), + ("slack", "\u{f04b1}", "#", "slk"), + ("sliders", "\u{f1542}", "\u{1f39a}", "=|="), + ("smartphone", "\u{f011c}", "\u{1f4f1}", "[.]"), + ("smile", "\u{f01f5}", "\u{1f642}", ":)"), + ("sms", "\u{f1170}", "\u{1f4ac}", "sms"), + ("snapchat", "\u{f04b6}", "\u{1f47b}", "snap"), + ("sort", "\u{f04ba}", "\u{21c5}", "^v"), + ("sort-asc", "\u{f04bc}", "\u{2191}", "a-z"), + ("sort-desc", "\u{f04bd}", "\u{2193}", "z-a"), + ("soundcloud", "\u{f04c0}", "\u{2601}", "sc"), + ("sourcehut", "", "\u{25ef}", "srht"), + ("sparkles", "\u{f0674}", "\u{2728}", "*+"), + ("spotify", "\u{f04c7}", "\u{1f3b5}", "spot"), + ("stack-overflow", "\u{f04cc}", "\u{1f4da}", "so"), + ("star", "\u{f04d2}", "\u{2605}", "*"), + ("stop", "\u{f04db}", "\u{23f9}", "[]"), + ("store", "\u{f10c1}", "\u{1f3ea}", "[S]"), + ("strikethrough", "\u{f0280}", "S\u{336}", "-s-"), + ("stripe", "\u{ed53}", "\u{24c8}", "str"), + ("substack", "\u{f0fb1}", "\u{2709}", "ss"), + ("sun", "\u{f05a8}", "\u{2600}", "*"), + ("supabase", "\u{e8b6}", "\u{26a1}", "sb"), + ("table", "\u{f04eb}", "\u{25a6}", "[#]"), + ("tablet", "\u{f04f6}", "\u{1f4f1}", "[..]"), + ("tag", "\u{f04fc}", "\u{1f3f7}", "#"), + ("target", "\u{f04fe}", "\u{1f3af}", "(o)"), + ("telegram", "\u{f2c6}", "\u{2708}", "tg"), + ("terminal", "\u{f018d}", "\u{2328}", ">_"), + ("theme", "\u{f050e}", "\u{25d0}", "(|)"), + ("thermometer", "\u{f050f}", "\u{1f321}", "|o"), + ("threads", "\u{f0065}", "@", "th"), + ("thumbs-down", "\u{f0512}", "\u{1f44e}", "-1"), + ("thumbs-up", "\u{f0514}", "\u{1f44d}", "+1"), + ("tiktok", "\u{f0387}", "\u{266a}", "tt"), + ("timer", "\u{f051b}", "\u{23f1}", "(:)"), + ("toggle-off", "\u{f0a19}", "\u{1f518}", "[o=]"), + ("toggle-on", "\u{f0521}", "\u{1f518}", "[=o]"), + ("tor", "\u{f371}", "\u{1f9c5}", "tor"), + ("translate", "\u{f05ca}", "\u{1f310}", "A/a"), + ("trello", "\u{f0532}", "\u{25a4}", "tr"), + ("trophy", "\u{f053a}", "\u{1f3c6}", "\\_/"), + ("truck", "\u{f129d}", "\u{1f69a}", "[=o"), + ("tumblr", "\u{f173}", "\u{24e3}", "tb"), + ("tv", "\u{f0502}", "\u{1f4fa}", "[_]"), + ("twitch", "\u{f0543}", "\u{1f4fa}", "ttv"), + ("type", "\u{f0284}", "T", "T"), + ("typescript", "\u{f06e6}", "TS", "ts"), + ("ubuntu", "\u{f0548}", "\u{25ce}", "ubu"), + ("umbrella", "\u{f054b}", "\u{2602}", "T"), + ("underline", "\u{f0287}", "U\u{332}", "_"), + ("undo", "\u{f054c}", "\u{21b6}", "<-"), + ("unlink", "\u{f033a}", "\u{26d3}", "~/"), + ("unlock", "\u{f0fc7}", "\u{1f513}", "[ ]"), + ("upload", "\u{f0552}", "\u{2912}", "^_"), + ("user", "\u{f0013}", "\u{1f464}", "@"), + ("user-check", "\u{f0be2}", "\u{1f464}", "@v"), + ("user-circle", "\u{f0b55}", "\u{1f464}", "(@)"), + ("user-minus", "\u{f0aec}", "\u{1f464}", "@-"), + ("user-plus", "\u{f0801}", "\u{1f464}", "@+"), + ("users", "\u{f000f}", "\u{1f465}", "@@"), + ("vercel", "\u{f0536}", "\u{25b2}", "vc"), + ("video", "\u{f0bdc}", "\u{1f4f9}", "[>"), + ("vimeo", "\u{f0577}", "\u{24e5}", "vm"), + ("voicemail", "\u{f057d}", "\u{2328}", "oo"), + ("volume", "\u{f057e}", "\u{1f50a}", "<))"), + ("volume-low", "\u{f0580}", "\u{1f509}", "<)"), + ("volume-off", "\u{f0581}", "\u{1f507}", ""), + ("wechat", "\u{f0611}", "\u{1f4ac}", "wx"), + ("whatsapp", "\u{f05a3}", "\u{1f4de}", "wa"), + ("wifi", "\u{f05a9}", "\u{1f4f6}", "((."), + ("wifi-off", "\u{f05aa}", "\u{1f4f5}", "x(("), + ("windows", "\u{f05b3}", "\u{229e}", "win"), + ("wordpress", "\u{f05b4}", "\u{24cc}", "wp"), + ("x", "\u{f0b05}", "\u{1d54f}", "x"), + ("x-circle", "\u{f015a}", "\u{274e}", "(x)"), + ("xmpp", "\u{f07ff}", "\u{1f4ac}", "xmpp"), + ("y-combinator", "\u{f23b}", "\u{24ce}", "yc"), + ("youtube", "\u{f05c3}", "\u{25b6}", "yt"), + ("zap", "\u{f140c}", "\u{26a1}", "/"), + ("zoom", "\u{f0567}", "\u{1f4f9}", "zm"), + ("zoom-in", "\u{f06ed}", "\u{1f50e}", "+?"), + ("zoom-out", "\u{f06ec}", "\u{1f50e}", "-?"), + ("zulip", "", "\u{24cf}", "zl"), +]; + +/// Sorted by alias. +pub static OPENICON_ALIASES: &[(&str, &str)] = &[ + ("a11y", "accessibility"), + ("account", "user"), + ("add-user", "user-plus"), + ("address-book", "contact"), + ("ai", "sparkles"), + ("alarm-clock", "alarm"), + ("alert-circle", "error"), + ("alert-triangle", "warning"), + ("announce", "megaphone"), + ("at-sign", "at"), + ("attach", "paperclip"), + ("attachment", "paperclip"), + ("bin", "delete"), + ("biometric", "fingerprint"), + ("blocked", "ban"), + ("bolt", "zap"), + ("box", "package"), + ("bullhorn", "megaphone"), + ("bullseye", "target"), + ("call", "phone"), + ("card", "credit-card"), + ("caution", "warning"), + ("cellphone", "smartphone"), + ("check-square", "checkbox"), + ("chip", "cpu"), + ("cli", "terminal"), + ("cmd", "command"), + ("cog", "settings"), + ("color", "palette"), + ("comment", "chat"), + ("company", "building"), + ("computer", "laptop"), + ("console", "terminal"), + ("contrast", "theme"), + ("curly-braces", "braces"), + ("currency", "dollar"), + ("dark-mode", "moon"), + ("date", "calendar"), + ("day", "sun"), + ("db", "database"), + ("delivery", "truck"), + ("deploy", "rocket"), + ("desktop", "monitor"), + ("directory", "folder"), + ("discount", "percent"), + ("dislike", "thumbs-down"), + ("display", "monitor"), + ("document", "file"), + ("duplicate", "copy"), + ("ellipsis", "more-horizontal"), + ("email", "mail"), + ("envelope", "mail"), + ("exit-fullscreen", "minimize"), + ("experiment", "flask"), + ("extension", "puzzle"), + ("faq", "help"), + ("favorite", "star"), + ("find", "search"), + ("floppy", "save"), + ("forbidden", "ban"), + ("fullscreen", "maximize"), + ("gear", "settings"), + ("goal", "target"), + ("golang", "go"), + ("hamburger", "menu"), + ("happy", "smile"), + ("hashtag", "hash"), + ("hide", "eye-off"), + ("house", "home"), + ("i18n", "translate"), + ("idea", "lightbulb"), + ("identity", "id-card"), + ("information", "info"), + ("integration", "plug"), + ("invite", "user-plus"), + ("invoice", "receipt"), + ("job", "briefcase"), + ("json", "braces"), + ("kebab", "more-vertical"), + ("lab", "flask"), + ("label", "tag"), + ("language", "translate"), + ("light-mode", "sun"), + ("lightning", "zap"), + ("like", "thumbs-up"), + ("loading", "loader"), + ("location", "map-pin"), + ("login", "log-in"), + ("logout", "log-out"), + ("love", "heart"), + ("magic", "sparkles"), + ("magnify", "search"), + ("marker", "map-pin"), + ("mention", "at"), + ("merge-request", "git-pull-request"), + ("message", "chat"), + ("microphone", "mic"), + ("mobile", "smartphone"), + ("movie", "film"), + ("mute", "volume-off"), + ("night", "moon"), + ("node", "nodejs"), + ("notification", "bell"), + ("office", "building"), + ("open-in-new", "external-link"), + ("paper-plane", "send"), + ("payment", "credit-card"), + ("pencil", "edit"), + ("pending", "hourglass"), + ("people", "users"), + ("person", "user"), + ("photo", "image"), + ("photo-camera", "camera"), + ("picture", "image"), + ("plugin", "puzzle"), + ("plus", "add"), + ("preferences", "settings"), + ("present", "gift"), + ("printer", "print"), + ("prize", "trophy"), + ("processor", "cpu"), + ("profile", "user"), + ("pull-request", "git-pull-request"), + ("qr", "qr-code"), + ("question", "help"), + ("recent", "history"), + ("reception", "cell-signal"), + ("reload", "refresh"), + ("scissors", "cut"), + ("shell", "terminal"), + ("shipping", "truck"), + ("shopping-bag", "bag"), + ("shopping-cart", "cart"), + ("show", "eye"), + ("sign-in", "log-in"), + ("sign-out", "log-out"), + ("song", "music"), + ("sound", "volume"), + ("source-code", "code"), + ("speaker", "volume"), + ("spinner", "loader"), + ("square", "checkbox-empty"), + ("stopwatch", "timer"), + ("storefront", "store"), + ("sync", "refresh"), + ("team", "users"), + ("telephone", "phone"), + ("television", "tv"), + ("thumbtack", "pin"), + ("times", "close"), + ("trash", "delete"), + ("twitter", "x"), + ("usd", "dollar"), + ("view", "eye"), + ("web", "globe"), + ("work", "briefcase"), + ("world", "globe"), + ("x-mark", "close"), +]; diff --git a/ports/rust/src/lib.rs b/ports/rust/src/lib.rs index ff535fa..dfddfee 100644 --- a/ports/rust/src/lib.rs +++ b/ports/rust/src/lib.rs @@ -26,6 +26,8 @@ pub mod capabilities; pub mod color; pub mod diff; pub mod graphics; +pub mod icons; +mod icons_data; pub mod input; pub mod layout; pub mod surface; @@ -44,6 +46,7 @@ pub use capabilities::{Capabilities, CapabilityOverrides, ColorDepth}; pub use color::{Color, Gradient}; pub use diff::{encode_full, EncodeResult, Encoder, EncoderOptions}; pub use graphics::BrailleCanvas; +pub use icons::{icon, icon_glyphs, icon_in, icon_mode_in, icon_names, set_icon_mode, IconGlyphs, IconMode}; pub use input::{FocusEvent, InputEvent, InputParser, KeyEvent, MouseAction, MouseButton, MouseEvent, PasteEvent}; pub use layout::{solve, stack, Constraint, Direction, Padding, Rect, Size}; pub use surface::{BorderStyle, Surface}; From 758bd3d459346aabec5db3524dfc41865163b392 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 24 Sep 2026 07:19:34 +0000 Subject: [PATCH 2/2] ci(bindings): pin FFI::Platypus 2.12; CPAN's index no longer lists 2.11 cpanm --mirror-only resolves from 02packages, which now carries only 2.12, so the exact @2.11 pin fails with "Found FFI::Platypus 2.12 which doesn't satisfy == 2.11" on every branch, main included. Makefile.PL and Hqtui.pm keep 2.11 as the minimum. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/bindings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bindings.yml b/.github/workflows/bindings.yml index 48c436e..69341ed 100644 --- a/.github/workflows/bindings.yml +++ b/.github/workflows/bindings.yml @@ -23,7 +23,7 @@ jobs: - name: Install only binding test toolchains run: mise install ruby@4.0.6 conda:php@8.5.9 perl@5.44.0.0 cmake@4.4.3 python@3.12.13 - name: Install private Perl dependencies - run: mise exec perl@5.44.0.0 -- cpanm --local-lib-contained "${{ runner.temp }}/hqtui-perl" --notest --mirror https://cpan.metacpan.org --mirror-only FFI::Platypus@2.11 + run: mise exec perl@5.44.0.0 -- cpanm --local-lib-contained "${{ runner.temp }}/hqtui-perl" --notest --mirror https://cpan.metacpan.org --mirror-only FFI::Platypus@2.12 - name: Build optimized binding library and PHP adapter shell: bash run: |