{"id":43807332,"url":"https://github.com/nick-skriabin/glyph","last_synced_at":"2026-02-12T07:01:41.876Z","repository":{"id":336648712,"uuid":"1150558523","full_name":"nick-skriabin/glyph","owner":"nick-skriabin","description":"A React-based framework for building modern, composable terminal UIs.","archived":false,"fork":false,"pushed_at":"2026-02-10T03:44:54.000Z","size":78843,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-10T09:13:23.715Z","etag":null,"topics":["cli","flexbox","react","react-reconciler","terminal","tui","typescript","yoga"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nick-skriabin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-05T12:25:35.000Z","updated_at":"2026-02-10T03:44:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nick-skriabin/glyph","commit_stats":null,"previous_names":["nick-skriabin/glyph"],"tags_count":49,"template":false,"template_full_name":null,"purl":"pkg:github/nick-skriabin/glyph","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nick-skriabin%2Fglyph","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nick-skriabin%2Fglyph/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nick-skriabin%2Fglyph/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nick-skriabin%2Fglyph/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nick-skriabin","download_url":"https://codeload.github.com/nick-skriabin/glyph/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nick-skriabin%2Fglyph/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29327809,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-11T03:52:29.695Z","status":"ssl_error","status_checked_at":"2026-02-11T03:52:23.094Z","response_time":97,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","flexbox","react","react-reconciler","terminal","tui","typescript","yoga"],"created_at":"2026-02-05T23:03:08.153Z","updated_at":"2026-02-11T06:00:50.515Z","avatar_url":"https://github.com/nick-skriabin.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://em-content.zobj.net/source/apple/391/crystal-ball_1f52e.png\" width=\"120\" height=\"120\" alt=\"Glyph\"\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eGlyph\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eReact renderer for terminal UIs\u003c/strong\u003e\u003cbr\u003e\n  \u003cem\u003eFlexbox layout. Keyboard-driven. Zero compromises.\u003c/em\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#quick-start\"\u003eQuick Start\u003c/a\u003e \u0026bull;\n  \u003ca href=\"#components\"\u003eComponents\u003c/a\u003e \u0026bull;\n  \u003ca href=\"#hooks\"\u003eHooks\u003c/a\u003e \u0026bull;\n  \u003ca href=\"#styling\"\u003eStyling\u003c/a\u003e \u0026bull;\n  \u003ca href=\"#examples\"\u003eExamples\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@nick-skriabin/glyph\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/@nick-skriabin/glyph?color=crimson\u0026logo=npm\" alt=\"npm version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/nick-skriabin/glyph/actions/workflows/test.yml\"\u003e\u003cimg src=\"https://github.com/nick-skriabin/glyph/actions/workflows/test.yml/badge.svg\" alt=\"Tests\"\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/badge/React-18%2B-61dafb?logo=react\u0026logoColor=white\" alt=\"React 18+\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Yoga-Flexbox-mediumpurple?logo=meta\u0026logoColor=white\" alt=\"Yoga Flexbox\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/TypeScript-First-3178c6?logo=typescript\u0026logoColor=white\" alt=\"TypeScript\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/License-MIT-blue\" alt=\"MIT License\"\u003e\n\u003c/p\u003e\n\n---\n\nBuild real terminal applications with React. Glyph provides a full component model with flexbox layout (powered by Yoga), focus management, keyboard input, and efficient diff-based rendering. Write TUIs the same way you write web apps.\n\n|  | | |\n|---|---|---|\n| ![Glyph](./screehshots/glyph-main.jpg) | ![Glyph List](./screehshots/glyph-list.jpg) |![Glyph Edit](./screehshots/glyph-edit.jpg) |\n\n\n### Features\n\n| | |\n|---|---|\n| **Flexbox Layout** | Full CSS-like flexbox via Yoga \u0026mdash; rows, columns, wrapping, alignment, gaps, padding |\n| **Rich Components** | Box, Text, Input, Button, Checkbox, Radio, Select, ScrollView, List, Menu, Progress, Spinner, Toasts, Dialogs, Portal, JumpNav |\n| **Focus System** | Tab navigation, focus scopes, focus trapping for modals, JumpNav quick-jump hints |\n| **Keyboard Input** | `useInput` hook, declarative `\u003cKeybind\u003e` component, vim-style bindings |\n| **Smart Rendering** | Double-buffered framebuffer with character-level diffing \u0026mdash; only changed cells are written |\n| **True Colors** | Named colors, hex, RGB, 256-palette. Auto-contrast text on colored backgrounds |\n| **Borders** | Single, double, rounded, and ASCII border styles |\n| **TypeScript** | Full type coverage. Every prop, style, and hook is typed |\n\n---\n\n## Installation\n\n```bash\n# npm\nnpm install @nick-skriabin/glyph react\n\n# pnpm\npnpm add @nick-skriabin/glyph react\n\n# bun\nbun add @nick-skriabin/glyph react\n```\n\n---\n\n## Quick Start\n\n```tsx\nimport React from \"react\";\nimport { render, Box, Text, Keybind, useApp } from \"@nick-skriabin/glyph\";\n\nfunction App() {\n  const { exit } = useApp();\n\n  return (\n    \u003cBox style={{ border: \"round\", borderColor: \"cyan\", padding: 1 }}\u003e\n      \u003cText style={{ bold: true, color: \"green\" }}\u003eHello, Glyph!\u003c/Text\u003e\n      \u003cKeybind keypress=\"q\" onPress={() =\u003e exit()} /\u003e\n    \u003c/Box\u003e\n  );\n}\n\nrender(\u003cApp /\u003e);\n```\n\nRun it:\n\n```bash\nnpx tsx app.tsx\n```\n\n---\n\n## Components\n\n### `\u003cBox\u003e`\n\nFlexbox container. The fundamental building block.\n\n```tsx\n\u003cBox style={{ flexDirection: \"row\", gap: 2, border: \"single\", padding: 1 }}\u003e\n  \u003cBox style={{ flexGrow: 1, bg: \"blue\" }}\u003e\n    \u003cText\u003eLeft\u003c/Text\u003e\n  \u003c/Box\u003e\n  \u003cBox style={{ flexGrow: 1, bg: \"red\" }}\u003e\n    \u003cText\u003eRight\u003c/Text\u003e\n  \u003c/Box\u003e\n\u003c/Box\u003e\n```\n\n### `\u003cText\u003e`\n\nStyled text content. Supports wrapping, alignment, bold, dim, italic, underline.\n\n```tsx\n\u003cText style={{ color: \"yellowBright\", bold: true, textAlign: \"center\" }}\u003e\n  Warning: something happened\n\u003c/Text\u003e\n```\n\n### `\u003cInput\u003e`\n\nText input field with cursor and placeholder support.\n\n```tsx\n\u003cInput\n  value={text}\n  onChange={setText}\n  placeholder=\"Type here...\"\n  style={{ bg: \"blackBright\", paddingX: 1 }}\n  focusedStyle={{ bg: \"white\", color: \"black\" }}\n/\u003e\n```\n\nSupports `multiline` for multi-line editing, `autoFocus` for automatic focus on mount. The cursor is always visible when focused.\n\n**Input types** for validation:\n\n```tsx\n// Text input (default) - accepts any character\n\u003cInput type=\"text\" value={name} onChange={setName} /\u003e\n\n// Number input - only accepts digits, decimal point, minus sign\n\u003cInput type=\"number\" value={age} onChange={setAge} placeholder=\"0\" /\u003e\n```\n\n**Input masking** with `onBeforeChange` for validation/formatting:\n\n```tsx\nimport { createMask, masks } from \"@nick-skriabin/glyph\";\n\n// Pre-built masks\n\u003cInput onBeforeChange={masks.usPhone} placeholder=\"(___) ___-____\" /\u003e\n\u003cInput onBeforeChange={masks.creditCard} placeholder=\"____ ____ ____ ____\" /\u003e\n\n// Custom masks: 9=digit, a=letter, *=alphanumeric\nconst licensePlate = createMask(\"aaa-9999\");\n\u003cInput onBeforeChange={licensePlate} placeholder=\"___-____\" /\u003e\n```\n\nAvailable masks: `usPhone`, `intlPhone`, `creditCard`, `dateUS`, `dateEU`, `dateISO`, `time`, `timeFull`, `ssn`, `zip`, `zipPlus4`, `ipv4`, `mac`.\n\n### `\u003cButton\u003e`\n\nFocusable button with press handling and visual feedback.\n\n```tsx\n\u003cButton\n  onPress={() =\u003e console.log(\"clicked\")}\n  style={{ border: \"single\", borderColor: \"cyan\", paddingX: 2 }}\n  focusedStyle={{ borderColor: \"yellowBright\", bold: true }}\n\u003e\n  \u003cText\u003eSubmit\u003c/Text\u003e\n\u003c/Button\u003e\n```\n\nButtons participate in the focus system automatically. Press `Enter` or `Space` to activate.\n\n### `\u003cCheckbox\u003e`\n\nToggle checkbox with label support.\n\n```tsx\nconst [agreed, setAgreed] = useState(false);\n\n\u003cCheckbox\n  checked={agreed}\n  onChange={setAgreed}\n  label=\"I agree to the terms\"\n  focusedStyle={{ color: \"cyan\" }}\n/\u003e\n```\n\nFocusable. Press `Enter` or `Space` to toggle. Supports custom `checkedChar` and `uncheckedChar` props.\n\n### `\u003cRadio\u003e`\n\nRadio button group for single selection from multiple options.\n\n```tsx\nconst [theme, setTheme] = useState\u003cstring\u003e(\"dark\");\n\n\u003cRadio\n  items={[\n    { label: \"Light\", value: \"light\" },\n    { label: \"Dark\", value: \"dark\" },\n    { label: \"System\", value: \"system\" },\n  ]}\n  value={theme}\n  onChange={setTheme}\n  focusedItemStyle={{ color: \"cyan\" }}\n  selectedItemStyle={{ bold: true }}\n/\u003e\n```\n\nFocusable. Navigate with `Up`/`Down`/`Left`/`Right`/`Tab`/`Shift+Tab`, select with `Enter`/`Space`. Supports `direction` prop (`\"column\"` or `\"row\"`), custom `selectedChar` and `unselectedChar`.\n\n### `\u003cScrollView\u003e`\n\nScrollable container with keyboard navigation and clipping.\n\n```tsx\n\u003cScrollView style={{ flexGrow: 1, border: \"single\" }}\u003e\n  {items.map((item, i) =\u003e (\n    \u003cBox key={i}\u003e\n      \u003cText\u003e{item}\u003c/Text\u003e\n    \u003c/Box\u003e\n  ))}\n\u003c/ScrollView\u003e\n```\n\n**Keyboard:** `PageUp`/`PageDown`, `Ctrl+d`/`Ctrl+u` (half-page), `Ctrl+f`/`Ctrl+b` (full page).\n\nShows a scrollbar when content exceeds viewport (disable with `showScrollbar={false}`). Supports controlled mode with `scrollOffset` and `onScroll` props.\n\n**Focus-aware scrolling:** ScrollView is focusable by default and responds to scroll keys when focused (or when it contains the focused element). This prevents multiple ScrollViews from scrolling simultaneously — only the one with focus responds.\n\nSet `focusable={false}` if you want the ScrollView to only scroll when a child element has focus:\n\n```tsx\n\u003cScrollView focusable={false} style={{ flexGrow: 1 }}\u003e\n  \u003cInput ... /\u003e  {/* ScrollView scrolls only when Input is focused */}\n\u003c/ScrollView\u003e\n```\n\n### `\u003cList\u003e`\n\nKeyboard-navigable selection list with a render callback.\n\n```tsx\n\u003cList\n  count={items.length}\n  onSelect={(index) =\u003e handleSelect(items[index])}\n  disabledIndices={new Set([2, 5])}\n  renderItem={({ index, selected, focused }) =\u003e (\n    \u003cBox style={selected \u0026\u0026 focused ? { bg: \"cyan\" } : {}}\u003e\n      \u003cText style={selected ? { bold: true } : {}}\u003e\n        {selected ? \"\u003e \" : \"  \"}{items[index]}\n      \u003c/Text\u003e\n    \u003c/Box\u003e\n  )}\n/\u003e\n```\n\nFocusable. `Up`/`Down`/`j`/`k` to navigate, `G` to jump to bottom, `gg` to jump to top, `Enter` to select. Disabled indices are skipped.\n\n### `\u003cMenu\u003e`\n\nStyled menu built on `\u003cList\u003e`. Accepts structured items with labels, values, and disabled state.\n\n```tsx\n\u003cMenu\n  items={[\n    { label: \"New File\", value: \"new\" },\n    { label: \"Open File\", value: \"open\" },\n    { label: \"Export\", value: \"export\", disabled: true },\n    { label: \"Quit\", value: \"quit\" },\n  ]}\n  onSelect={(value) =\u003e handleAction(value)}\n  highlightColor=\"yellow\"\n/\u003e\n```\n\n### `\u003cSelect\u003e`\n\nDropdown select with keyboard navigation and type-to-filter search.\n\n```tsx\nconst [lang, setLang] = useState\u003cstring | undefined\u003e();\n\n\u003cSelect\n  items={[\n    { label: \"TypeScript\", value: \"ts\" },\n    { label: \"JavaScript\", value: \"js\" },\n    { label: \"Rust\", value: \"rust\" },\n    { label: \"Go\", value: \"go\" },\n    { label: \"COBOL\", value: \"cobol\", disabled: true },\n  ]}\n  value={lang}\n  onChange={setLang}\n  placeholder=\"Pick a language...\"\n  maxVisible={6}\n  highlightColor=\"yellow\"\n/\u003e\n```\n\nFocusable. `Enter`/`Space`/`Down` to open, `Up`/`Down` to navigate, `Enter` to confirm, `Escape` to close. Type characters to filter items when open. Disabled items are skipped.\n\nProps: `items`, `value`, `onChange`, `placeholder`, `maxVisible`, `highlightColor`, `searchable`, `style`, `focusedStyle`, `dropdownStyle`, `disabled`.\n\n### `\u003cFocusScope\u003e`\n\nFocus trapping for modals and overlays.\n\n```tsx\n\u003cFocusScope trap\u003e\n  \u003cInput value={v} onChange={setV} /\u003e\n  \u003cButton onPress={submit}\u003e\n    \u003cText\u003eOK\u003c/Text\u003e\n  \u003c/Button\u003e\n\u003c/FocusScope\u003e\n```\n\n### `\u003cPortal\u003e`\n\nRenders children in a fullscreen absolute overlay. Useful for modals and dialogs.\n\n```tsx\n\u003cPortal\u003e\n  \u003cBox style={{ width: \"100%\", height: \"100%\", justifyContent: \"center\", alignItems: \"center\" }}\u003e\n    \u003cBox style={{ width: 40, border: \"double\", bg: \"black\", padding: 1 }}\u003e\n      \u003cText\u003eModal content\u003c/Text\u003e\n    \u003c/Box\u003e\n  \u003c/Box\u003e\n\u003c/Portal\u003e\n```\n\n### `\u003cJumpNav\u003e`\n\nQuick keyboard navigation to any focusable element. Press an activation key to show hint labels on all focusable elements, then type the hint to jump directly to that element. Similar to Vim's EasyMotion or browser extensions like Vimium.\n\n```tsx\nfunction App() {\n  return (\n    \u003cJumpNav activationKey=\"ctrl+o\"\u003e\n      \u003cBox style={{ flexDirection: \"column\", gap: 1 }}\u003e\n        \u003cInput placeholder=\"Name\" /\u003e\n        \u003cInput placeholder=\"Email\" /\u003e\n        \u003cSelect items={countries} /\u003e\n        \u003cButton onPress={submit}\u003eSubmit\u003c/Button\u003e\n      \u003c/Box\u003e\n    \u003c/JumpNav\u003e\n  );\n}\n```\n\n**How it works:**\n1. Press `Ctrl+O` (or custom `activationKey`) to activate\n2. Hint labels (a, s, d, f...) appear next to each focusable element\n3. Type a hint to instantly focus that element\n4. Press `Escape` to cancel\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `activationKey` | `string` | `\"ctrl+o\"` | Key to activate jump mode |\n| `hintChars` | `string` | `\"asdfghjkl...\"` | Characters used for hints |\n| `hintBg` | `Color` | `\"yellow\"` | Hint label background |\n| `hintFg` | `Color` | `\"black\"` | Hint label text color |\n| `hintStyle` | `Style` | `{}` | Additional hint label styling |\n| `enabled` | `boolean` | `true` | Enable/disable JumpNav |\n\n**Focus scope aware:** JumpNav automatically respects `\u003cFocusScope trap\u003e`. When a modal with a focus trap is open, only elements inside that trap will show hints.\n\n### `\u003cKeybind\u003e`\n\nDeclarative keyboard shortcut. Renders nothing.\n\n```tsx\n\u003cKeybind keypress=\"ctrl+s\" onPress={save} /\u003e\n\u003cKeybind keypress=\"escape\" onPress={close} /\u003e\n\u003cKeybind keypress=\"q\" onPress={() =\u003e exit()} /\u003e\n```\n\n**Modifiers:** `ctrl`, `alt`, `shift`, `meta` (Cmd/Super). Combine with `+`: `\"ctrl+shift+p\"`, `\"alt+return\"`.\n\n**Priority keybinds:** Use `priority` prop to run BEFORE focused input handlers. Useful for keybinds that should work even when an Input is focused:\n\n```tsx\n\u003cKeybind keypress=\"ctrl+return\" onPress={submit} priority /\u003e\n\u003cKeybind keypress=\"alt+return\" onPress={submit} priority /\u003e\n```\n\n**Terminal configuration:** Some keybinds like `ctrl+return` require terminal support:\n\n| Terminal | Configuration |\n|----------|---------------|\n| **Ghostty** | Add to `~/.config/ghostty/config`: `keybind = ctrl+enter=text:\\x1b[13;5~` |\n| **iTerm2** | Profiles → Keys → General → Enable \"CSI u\" mode |\n| **Kitty/WezTerm** | Works out of the box |\n\n`alt+return` works universally without configuration.\n\n### `\u003cProgress\u003e`\n\nDeterminate or indeterminate progress bar. Uses `useLayout` to measure actual width and renders block characters.\n\n```tsx\n\u003cProgress value={0.65} showPercent /\u003e\n\u003cProgress indeterminate label=\"Loading\" /\u003e\n```\n\nProps: `value` (0..1), `indeterminate`, `width`, `label`, `showPercent`, `filled`/`empty` (characters).\n\n### `\u003cSpinner\u003e`\n\nAnimated spinner with configurable frames. Cleans up timers on unmount.\n\n```tsx\n\u003cSpinner label=\"Loading...\" style={{ color: \"green\" }} /\u003e\n\u003cSpinner frames={[\"|\", \"/\", \"-\", \"\\\\\"]} intervalMs={100} /\u003e\n```\n\n### `\u003cToastHost\u003e` + `useToast()`\n\nLightweight toast notifications rendered via Portal. Wrap your app in `\u003cToastHost\u003e`, then push toasts from anywhere with `useToast()`.\n\n```tsx\nfunction App() {\n  const toast = useToast();\n  return \u003cKeybind keypress=\"t\" onPress={() =\u003e\n    toast({ message: \"Saved!\", variant: \"success\" })\n  } /\u003e;\n}\n\nrender(\u003cToastHost position=\"top-right\"\u003e\u003cApp /\u003e\u003c/ToastHost\u003e);\n```\n\nVariants: `\"info\"`, `\"success\"`, `\"warning\"`, `\"error\"`. Auto-dismiss after `durationMs` (default 3000).\n\n### `\u003cDialogHost\u003e` + `useDialog()`\n\nImperative `alert()` and `confirm()` dialogs, similar to browser APIs. Wrap your app in `\u003cDialogHost\u003e`, then show dialogs from anywhere.\n\n```tsx\nfunction App() {\n  const { alert, confirm } = useDialog();\n\n  const handleDelete = async () =\u003e {\n    const ok = await confirm(\"Delete this item?\", {\n      okText: \"Delete\",\n      cancelText: \"Keep\"\n    });\n    if (ok) {\n      // delete the item\n    }\n  };\n\n  const handleSave = async () =\u003e {\n    await saveData();\n    await alert(\"Saved successfully!\");\n  };\n\n  return \u003cButton onPress={handleDelete}\u003e\u003cText\u003eDelete\u003c/Text\u003e\u003c/Button\u003e;\n}\n\nrender(\u003cDialogHost\u003e\u003cApp /\u003e\u003c/DialogHost\u003e);\n```\n\n**Rich content** — pass React elements instead of strings:\n\n```tsx\nawait alert(\n  \u003cBox style={{ flexDirection: \"column\" }}\u003e\n    \u003cText style={{ bold: true, color: \"green\" }}\u003e✓ Success!\u003c/Text\u003e\n    \u003cText\u003eYour changes have been saved.\u003c/Text\u003e\n  \u003c/Box\u003e,\n  { okText: \"Got it!\" }\n);\n```\n\n**Keyboard:** Tab/Shift+Tab or arrows to switch buttons, Enter/Space to select, Escape to cancel.\n\n**Chained dialogs** work naturally with async/await — each dialog waits for the previous to close.\n\n### `\u003cSpacer\u003e`\n\nFlexible space filler. Pushes siblings apart.\n\n```tsx\n\u003cBox style={{ flexDirection: \"row\" }}\u003e\n  \u003cText\u003eLeft\u003c/Text\u003e\n  \u003cSpacer /\u003e\n  \u003cText\u003eRight\u003c/Text\u003e\n\u003c/Box\u003e\n```\n\n---\n\n## Hooks\n\n### `useInput(handler)`\n\nListen for all keyboard events.\n\n```tsx\nuseInput((key) =\u003e {\n  if (key.name === \"escape\") close();\n  if (key.ctrl \u0026\u0026 key.name === \"s\") save();\n});\n```\n\n### `useFocus(nodeRef)`\n\nGet focus state for a node.\n\n```tsx\nconst ref = useRef(null);\nconst { focused, focus } = useFocus(ref);\n\n\u003cBox ref={ref} focusable\u003e\n  \u003cText style={focused ? { color: \"cyan\" } : {}}\u003e\n    {focused ? \"* focused *\" : \"not focused\"}\n  \u003c/Text\u003e\n\u003c/Box\u003e\n```\n\n### `useFocusable(options)`\n\nMake any element focusable with full keyboard support. Perfect for building custom interactive components.\n\n```tsx\nimport { useFocusable, Box, Text } from \"@nick-skriabin/glyph\";\n\nfunction CustomPicker({ items, onSelect }) {\n  const [selected, setSelected] = useState(0);\n  \n  const { ref, isFocused } = useFocusable({\n    onKeyPress: (key) =\u003e {\n      if (key.name === \"up\") {\n        setSelected(s =\u003e Math.max(0, s - 1));\n        return true; // Consume the key\n      }\n      if (key.name === \"down\") {\n        setSelected(s =\u003e Math.min(items.length - 1, s + 1));\n        return true;\n      }\n      if (key.name === \"return\") {\n        onSelect(items[selected]);\n        return true;\n      }\n      return false; // Let other handlers process\n    },\n    onFocus: () =\u003e console.log(\"Picker focused\"),\n    onBlur: () =\u003e console.log(\"Picker blurred\"),\n    disabled: false, // Set to true to skip in tab order\n  });\n\n  return (\n    \u003cBox\n      ref={ref}\n      focusable\n      style={{ \n        border: \"round\",\n        borderColor: isFocused ? \"cyan\" : \"gray\",\n        padding: 1,\n      }}\n    \u003e\n      {items.map((item, i) =\u003e (\n        \u003cText key={i} style={{ inverse: i === selected }}\u003e\n          {i === selected ? \"\u003e \" : \"  \"}{item}\n        \u003c/Text\u003e\n      ))}\n    \u003c/Box\u003e\n  );\n}\n```\n\nReturns `{ ref, isFocused, focus, focusId }`. The `ref` must be attached to an element with `focusable` prop.\n\n### `useLayout(nodeRef)`\n\nSubscribe to a node's computed layout.\n\n```tsx\nconst ref = useRef(null);\nconst layout = useLayout(ref);\n\n// layout: { x, y, width, height, innerX, innerY, innerWidth, innerHeight }\n```\n\n### `useApp()`\n\nAccess app-level utilities.\n\n```tsx\nconst { exit, columns, rows } = useApp();\n```\n\n---\n\n## Styling\n\nAll components accept a `style` prop. Glyph uses Yoga for flexbox layout, so the model is familiar if you've used CSS flexbox or React Native.\n\n### Layout\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `width`, `height` | `number \\| \"${n}%\"` | Dimensions |\n| `minWidth`, `minHeight` | `number` | Minimum dimensions |\n| `maxWidth`, `maxHeight` | `number` | Maximum dimensions |\n| `padding` | `number` | Padding on all sides |\n| `paddingX`, `paddingY` | `number` | Horizontal / vertical padding |\n| `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft` | `number` | Individual sides |\n| `gap` | `number` | Gap between flex children |\n\n### Flexbox\n\n| Property | Type | Default |\n|----------|------|---------|\n| `flexDirection` | `\"row\" \\| \"column\"` | `\"column\"` |\n| `flexWrap` | `\"nowrap\" \\| \"wrap\"` | `\"nowrap\"` |\n| `justifyContent` | `\"flex-start\" \\| \"center\" \\| \"flex-end\" \\| \"space-between\" \\| \"space-around\"` | `\"flex-start\"` |\n| `alignItems` | `\"flex-start\" \\| \"center\" \\| \"flex-end\" \\| \"stretch\"` | `\"stretch\"` |\n| `flexGrow` | `number` | `0` |\n| `flexShrink` | `number` | `0` |\n\n### Positioning\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `position` | `\"relative\" \\| \"absolute\"` | Positioning mode |\n| `top`, `right`, `bottom`, `left` | `number \\| \"${n}%\"` | Offsets |\n| `inset` | `number \\| \"${n}%\"` | Shorthand for all four edges |\n| `zIndex` | `number` | Stacking order |\n\n### Visual\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `bg` | `Color` | Background color |\n| `border` | `\"none\" \\| \"single\" \\| \"double\" \\| \"round\" \\| \"ascii\"` | Border style |\n| `borderColor` | `Color` | Border color |\n| `clip` | `boolean` | Clip overflowing children |\n\n### Text\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `color` | `Color` | Text color |\n| `bold` | `boolean` | Bold text |\n| `dim` | `boolean` | Dimmed text |\n| `italic` | `boolean` | Italic text |\n| `underline` | `boolean` | Underlined text |\n| `wrap` | `\"wrap\" \\| \"truncate\" \\| \"ellipsis\" \\| \"none\"` | Text wrapping mode |\n| `textAlign` | `\"left\" \\| \"center\" \\| \"right\"` | Text alignment |\n\n### Colors\n\nColors can be specified as:\n\n- **Named:** `\"red\"`, `\"green\"`, `\"blueBright\"`, `\"whiteBright\"`, etc.\n- **Hex:** `\"#ff0000\"`, `\"#1a1a2e\"`\n- **RGB:** `{ r: 255, g: 0, b: 0 }`\n- **256-palette:** `0`\u0026ndash;`255`\n\nText on colored backgrounds automatically picks black or white for contrast when no explicit color is set.\n\n---\n\n## `render(element, options?)`\n\nMount a React element to the terminal.\n\n```tsx\nconst app = render(\u003cApp /\u003e, {\n  stdout: process.stdout,\n  stdin: process.stdin,\n  debug: false,\n  useNativeCursor: true, // Use terminal's native cursor (default: true)\n});\n\napp.unmount(); // Tear down\napp.exit();    // Unmount and exit process\n```\n\n### Options\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `stdout` | `NodeJS.WriteStream` | `process.stdout` | Output stream |\n| `stdin` | `NodeJS.ReadStream` | `process.stdin` | Input stream |\n| `debug` | `boolean` | `false` | Enable debug logging |\n| `useNativeCursor` | `boolean` | `true` | Use terminal's native cursor instead of simulated one |\n\n### Native Cursor\n\nBy default, Glyph uses the terminal's native cursor, which enables:\n\n- **Cursor shaders** in terminals that support them (e.g., Ghostty)\n- **Custom cursor shapes** (block, beam, underline) from terminal settings\n- **Cursor animations** and blinking behavior\n\nThe native cursor is automatically shown when an input is focused and hidden otherwise.\n\nTo use the simulated cursor instead (inverted colors, no shader support):\n\n```tsx\nrender(\u003cApp /\u003e, { useNativeCursor: false });\n```\n\n---\n\n## Examples\n\nInteractive examples are included in the repo. Each demonstrates different components and patterns:\n\n| Example | Description | Source |\n|---------|-------------|--------|\n| **basic-layout** | Flexbox layout fundamentals | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/basic-layout) |\n| **modal-input** | Modal dialogs, input focus trapping | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/modal-input) |\n| **scrollview-demo** | Scrollable content with keyboard navigation | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/scrollview-demo) |\n| **list-demo** | Keyboard-navigable lists | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/list-demo) |\n| **menu-demo** | Styled menus with icons | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/menu-demo) |\n| **select-demo** | Dropdown select with search | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/select-demo) |\n| **forms-demo** | Checkbox and Radio inputs | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/forms-demo) |\n| **masked-input** | Input masks (phone, credit card, SSN) | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/masked-input) |\n| **dialog-demo** | Alert and Confirm dialogs | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/dialog-demo) |\n| **jump-nav** | Quick navigation with keyboard hints | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/jump-nav) |\n| **showcase** | Progress bars, Spinners, Toasts | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/showcase) |\n| **dashboard** | Full task manager (all components) | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/dashboard) |\n\n### Running Examples Locally\n\n```bash\n# Clone and install\ngit clone https://github.com/nick-skriabin/glyph.git \u0026\u0026 cd glyph\nbun install \u0026\u0026 bun run build\n\n# Run any example\nbun run --filter \u003cexample-name\u003e dev\n\n# e.g.\nbun run --filter dashboard dev\nbun run --filter jump-nav dev\n```\n\n---\n\n## Who Uses Glyph\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\"\u003e\n      \u003ca href=\"https://github.com/nick-skriabin/aion\"\u003e\n        \u003cstrong\u003eAion\u003c/strong\u003e\n      \u003c/a\u003e\n      \u003cbr\u003e\n      \u003csub\u003eCalendar \u0026 time management TUI\u003c/sub\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n\u003csub\u003eUsing Glyph in your project? \u003ca href=\"https://github.com/nick-skriabin/glyph/issues\"\u003eLet us know!\u003c/a\u003e\u003c/sub\u003e\n\n---\n\n## Architecture\n\n```\nsrc/\n├── reconciler/    React reconciler (host config + GlyphNode tree)\n├── layout/        Yoga-based flexbox + text measurement\n├── paint/         Framebuffer, character diffing, borders, colors\n├── runtime/       Terminal raw mode, key parsing, OSC handling\n├── components/    Box, Text, Input, Button, ScrollView, List, Menu, ...\n├── hooks/         useInput, useFocus, useLayout, useApp\n└── render.ts      Entry point tying it all together\n```\n\n**Render pipeline:** React reconciler builds a GlyphNode tree \u0026rarr; Yoga computes flexbox layout \u0026rarr; painter rasterizes to a framebuffer \u0026rarr; diff engine writes only changed cells to stdout.\n\n---\n\n## License\n\nMIT\n\n---\n\n\u003cp align=\"center\"\u003e\n  \u003csub\u003eBuilt with React \u0026bull; Yoga \u0026bull; a lot of ANSI escape codes\u003c/sub\u003e\n\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnick-skriabin%2Fglyph","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnick-skriabin%2Fglyph","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnick-skriabin%2Fglyph/lists"}