{"id":51767164,"url":"https://github.com/arcboxlabs/expo-libghostty","last_synced_at":"2026-07-21T21:02:03.825Z","repository":{"id":371807851,"uuid":"1301484423","full_name":"arcboxlabs/expo-libghostty","owner":"arcboxlabs","description":"Ghostty terminal view for Expo / React Native, powered by libghostty","archived":false,"fork":false,"pushed_at":"2026-07-17T12:45:58.000Z","size":1302,"stargazers_count":15,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-19T19:33:08.683Z","etag":null,"topics":["android","expo","expo-module","ghostty","ios","libghostty","react-native","terminal","terminal-emulator"],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/arcboxlabs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","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-07-15T10:22:24.000Z","updated_at":"2026-07-19T11:57:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/arcboxlabs/expo-libghostty","commit_stats":null,"previous_names":["arcboxlabs/expo-libghostty"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/arcboxlabs/expo-libghostty","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcboxlabs%2Fexpo-libghostty","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcboxlabs%2Fexpo-libghostty/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcboxlabs%2Fexpo-libghostty/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcboxlabs%2Fexpo-libghostty/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/arcboxlabs","download_url":"https://codeload.github.com/arcboxlabs/expo-libghostty/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcboxlabs%2Fexpo-libghostty/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35664169,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-19T02:00:06.923Z","response_time":112,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["android","expo","expo-module","ghostty","ios","libghostty","react-native","terminal","terminal-emulator"],"created_at":"2026-07-19T19:33:12.221Z","updated_at":"2026-07-19T19:33:13.565Z","avatar_url":"https://github.com/arcboxlabs.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# expo-libghostty\n\n[![npm](https://img.shields.io/npm/v/expo-libghostty)](https://www.npmjs.com/package/expo-libghostty)\n[![Release](https://github.com/arcboxlabs/expo-libghostty/actions/workflows/release.yml/badge.svg)](https://github.com/arcboxlabs/expo-libghostty/actions/workflows/release.yml)\n[![license](https://img.shields.io/npm/l/expo-libghostty)](./LICENSE)\n![platforms](https://img.shields.io/badge/platforms-iOS%2016.4%2B%20%7C%20Android-4630EB)\n\nGhostty terminal view for Expo / React Native, powered by\n[libghostty](https://ghostty.org) — on iOS via\n[Lakr233/libghostty-spm](https://github.com/Lakr233/libghostty-spm), on\nAndroid via [libghostty-vt](https://github.com/ghostty-org/ghostty) with a\nCanvas renderer.\n\n- Real VT parsing from ghostty's core on both platforms\n- Theming (background/foreground/cursor/selection + 256-color palette) and\n  font size, with pinch-to-zoom\n- iOS: GPU (Metal) rendering, CJK IME, keyboard accessory bar with sticky\n  modifiers, touch selection\n- Android: libghostty-vt state machine + JNI, dirty-row Canvas rendering\n  (system font fallback covers CJK/emoji; bundled Symbols Nerd Font covers\n  private-use glyphs), IME text input, hardware keys via ghostty's key\n  encoder, keyboard accessory bar with sticky modifiers, touch selection\n  with clipboard, inertial scrollback with indicator, cursor blink\n- Bring-your-own PTY: the view only renders bytes and reports input/resizes —\n  transport and session lifecycle stay on your side\n\n**Platforms:** iOS 16.4+ and Android (arm64-v8a / x86_64; Expo SDK 57).\nWeb is not supported.\n\n## Install\n\n```sh\nnpx expo install expo-libghostty\n```\n\nThe prebuilt `GhosttyKit.xcframework` (~50 MB, iOS) and Android\n`libghostty-vt` static libraries are downloaded checksum-pinned by a\n`postinstall` script. pnpm blocks dependency build scripts by default —\nallow it in `pnpm-workspace.yaml`:\n\n```yaml\nallowBuilds:\n  expo-libghostty: true\n```\n\n## Usage\n\nAll byte payloads are base64 strings.\n\n```tsx\nimport { TerminalView, TerminalViewRef } from 'expo-libghostty';\nimport { useRef } from 'react';\n\nexport function Terminal({ pty }) {\n  const terminal = useRef\u003cTerminalViewRef\u003e(null);\n\n  // PTY output -\u003e grid: terminal.current?.write(base64Chunk)\n  // PTY exited -\u003e terminal.current?.finish(exitCode)\n\n  return (\n    \u003cTerminalView\n      ref={terminal}\n      style={{ flex: 1 }}\n      onInput={({ nativeEvent }) =\u003e pty.write(nativeEvent.data)}\n      onResize={({ nativeEvent }) =\u003e pty.resize(nativeEvent.cols, nativeEvent.rows)}\n    /\u003e\n  );\n}\n```\n\n`example/` contains a runnable local-echo demo (`pnpm --dir example ios` /\n`pnpm --dir example android`).\n\n### API\n\n| `\u003cTerminalView\u003e` prop | Description                                                                                                                         |\n| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |\n| `fontSize`            | Base font size in density-independent units (default 14, clamped 4–64); pinch-to-zoom steps from it. Android applies changes live (the grid reflows); iOS rebuilds the surface on change, resetting the grid — set it before mounting. |\n| `theme`               | Terminal colors: `background`, `foreground`, `cursorColor`, `selectionBackground`, `selectionForeground`, and `palette` (overrides by index, 0–255). Values use ghostty config syntax (hex or X11 names); invalid values are ignored with a warning. Android applies the theme per view; on iOS it is app-wide. |\n| `onInput`             | User keyboard/IME input to forward to the PTY. `nativeEvent.data` is base64 bytes; `nativeEvent.text` is the same decoded as UTF-8. |\n| `onResize`            | Grid resized (layout, rotation, font change). Forward `nativeEvent.cols` / `nativeEvent.rows` to the PTY.                           |\n| `onBell`              | BEL received (0x07).                                                                                                                |\n| `onTitleChange`       | Title set via OSC 0/2; `nativeEvent.title`.                                                                                          |\n| `onDirectoryChange`   | Working directory reported via OSC 7/9/1337; `nativeEvent.path` is passed through as sent (OSC 7 is a `file://` URI). Android only for now — the upstream iOS in-memory surface does not emit pwd actions. |\n| `ref`                 | Imperative handle (`TerminalViewRef`), methods below.                                                                               |\n\n| Ref method         | Description                                            |\n| ------------------ | ------------------------------------------------------ |\n| `write(base64)`    | Feed base64-encoded PTY output into the terminal grid. |\n| `writeText(text)`  | Feed PTY output as UTF-8 text, for string-based wires. |\n| `finish(exitCode)` | Mark the underlying PTY as exited.                     |\n\n### Platform parity\n\nThe JS contract is identical on both platforms; native behavior differs\nwhere the platforms do:\n\n| Behavior                          | iOS (GhosttyKit)         | Android (libghostty-vt + Canvas)              |\n| --------------------------------- | ------------------------ | --------------------------------------------- |\n| Rendering                         | Metal                    | Canvas/Skia, dirty-row bitmap patching         |\n| CJK / emoji                       | bundled font stack       | system font fallback (Minikin)                 |\n| Nerd Font private-use glyphs      | bundled font stack       | bundled Symbols Nerd Font Mono                 |\n| IME text input                    | ✅                        | ✅ (commitText; sticky Ctrl/Alt compose chords) |\n| Keyboard accessory bar            | ✅ sticky modifiers       | ✅ Esc/Ctrl/Alt/Tab/arrows/nav, sticky Ctrl/Alt |\n| Hardware keys (DECCKM/kitty)      | ✅                        | ✅ via `ghostty_key_encoder`                    |\n| Touch selection + clipboard       | ✅                        | ✅ long-press word, drag handles, magnifier, floating Copy/Paste/Select all; bracketed paste with unsafe-paste confirm |\n| Scrollback                        | ✅                        | ✅ inertial fling, fading indicator, jump-to-bottom chip |\n| Cursor blink (DECSCUSR)           | ✅                        | ✅ (holds solid on I/O, honors animations-off)  |\n| Pinch-to-zoom font size           | ✅                        | ✅ (same 0.1-scale → ±1 steps, 4–64 bounds)     |\n| Theme colors (`theme` prop)       | ✅ app-wide (controller config) | ✅ per view (terminal default colors)     |\n| Terminal events (bell/title)      | ✅ (surface delegate)     | ✅ (vt effect callbacks)                        |\n| `onDirectoryChange` (OSC 7/9/1337)| ❌ (surface emits no pwd actions) | ✅                                      |\n\n## Vendoring\n\nExpo autolinking cannot consume Swift packages, so the pure-Swift layers of\nlibghostty-spm (`GhosttyKit`, `GhosttyTerminal`) and `MSDisplayLink` are\nvendored under `ios/vendor/` as CocoaPods mirroring the upstream SPM products\n(all MIT, licenses included). `vendor-manifest.json` pins the upstream tags\nand the XCFramework checksum; `pnpm sync-vendor` re-syncs. A daily\n`vendor-watch` workflow compares every pin against upstream and keeps a\ndrift issue open while any is behind. Once React Native supports SPM\ndependencies this layer disappears in favor of the upstream package.\n\nOn Android, `android/vendor/` holds per-ABI `libghostty-vt.a` static\nlibraries plus the matching C headers, cross-compiled from a pinned ghostty\ncommit (Zig 0.15.2 + NDK r27); `vendor-manifest.json` pins the tarball\nchecksum. A thin JNI shim (`android/src/main/cpp/ghostty_jni.cpp`) exposes\nthe terminal + render-state loop to Kotlin, which paints the grid with\nCanvas/Skia (`GhosttyTerminalView.kt`).\n\n## Troubleshooting\n\n**`pod install` fails on a missing `GhosttyKit.xcframework`, or the Android\nbuild can't find `libghostty-vt.a`.** The `postinstall` script that fetches\nthem didn't run — pnpm blocks dependency build scripts unless\n`expo-libghostty` is in `allowBuilds:` (see Install), and `--ignore-scripts`\nskips it under any package manager. Fix the config and reinstall, or run\n`node node_modules/expo-libghostty/scripts/download-xcframework.mjs` and\n`…/download-android-libs.mjs` by hand.\n\n**Install fails with `download failed: HTTP …` or `checksum mismatch`.**\nThe binaries come from GitHub release assets, so the install machine needs\naccess to `github.com` (and `objects.githubusercontent.com`). A checksum\nmismatch means the download was corrupted or rewritten in transit\n(intercepting proxy) — the script fails loudly rather than installing an\nunverified binary. Both scripts skip the download when the files are already\npresent, so air-gapped setups can pre-seed `ios/vendor/Frameworks/` and\n`android/vendor/{arm64-v8a,x86_64}/`.\n\n**Android crashes with `UnsatisfiedLinkError`.** Only `arm64-v8a` and\n`x86_64` ship (see Platforms); 32-bit ABIs are not supported.\n\n**The terminal stays black in release builds (\u003c 0.8.1).** `write()` issued\nfrom a mount effect used to race native view registration and was silently\ndropped. Fixed in 0.8.1, where imperative calls queue until the view reports\nits first grid size — upgrade.\n\n**iOS: the grid resets when `fontSize` changes after mount.** A font-size\nchange rebuilds the terminal surface on iOS; set it before mounting. Android\napplies it live (the grid reflows in place).\n\n**Contributing: the example iOS build dies with `Could not resolve package\ndependencies`.** Expo SDK 57's `expo-modules-jsi` is a\n`swift-tools-version: 6.2` package — building the example needs the\nXcode 26 toolchain (CI uses `macos-26`).\n\n## License\n\nMIT © [ArcBox, Inc.](./LICENSE) Vendored components (libghostty,\nlibghostty-spm, MSDisplayLink) are MIT-licensed by their respective authors;\ntheir licenses ship alongside the vendored sources.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farcboxlabs%2Fexpo-libghostty","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farcboxlabs%2Fexpo-libghostty","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farcboxlabs%2Fexpo-libghostty/lists"}