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

+

Input

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