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: |
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").
+
+
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};