{"id":47734772,"url":"https://github.com/dannote/ghostty_ex","last_synced_at":"2026-04-24T23:01:00.085Z","repository":{"id":347876315,"uuid":"1195505588","full_name":"dannote/ghostty_ex","owner":"dannote","description":"Terminal emulator library for the BEAM — libghostty-vt NIFs with OTP integration","archived":false,"fork":false,"pushed_at":"2026-04-24T21:31:47.000Z","size":253,"stargazers_count":90,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2026-04-24T21:35:13.991Z","etag":null,"topics":["elixir","genserver","ghostty","nif","simd","terminal-emulator","vt-parser"],"latest_commit_sha":null,"homepage":null,"language":"Elixir","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/dannote.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":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-29T18:42:55.000Z","updated_at":"2026-04-24T21:31:48.000Z","dependencies_parsed_at":null,"dependency_job_id":"4a2fd9cd-2072-4835-91d8-15b3656ae061","html_url":"https://github.com/dannote/ghostty_ex","commit_stats":null,"previous_names":["dannote/ghostty_ex"],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/dannote/ghostty_ex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fghostty_ex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fghostty_ex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fghostty_ex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fghostty_ex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dannote","download_url":"https://codeload.github.com/dannote/ghostty_ex/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fghostty_ex/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32243803,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-24T13:21:15.438Z","status":"ssl_error","status_checked_at":"2026-04-24T13:21:15.005Z","response_time":64,"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":["elixir","genserver","ghostty","nif","simd","terminal-emulator","vt-parser"],"created_at":"2026-04-02T22:19:19.712Z","updated_at":"2026-04-24T23:01:00.074Z","avatar_url":"https://github.com/dannote.png","language":"Elixir","funding_links":[],"categories":["Core \u0026 Libraries"],"sub_categories":[],"readme":"# Ghostty\n\nTerminal emulator library for the BEAM.\n\nWraps [libghostty-vt](https://ghostty.org) — the virtual terminal extracted from\n[Ghostty](https://github.com/ghostty-org/ghostty). SIMD-optimized VT parsing,\nfull Unicode, 24-bit color, scrollback with text reflow. Terminals are GenServers.\n\n## Installation\n\n```elixir\ndef deps do\n  [{:ghostty, \"~\u003e 0.4\"}]\nend\n```\n\nPrecompiled terminal and PTY NIF binaries are downloaded automatically for\nx86_64 Linux, aarch64 Linux, and aarch64 macOS.\n\n## Usage\n\n```elixir\n{:ok, term} = Ghostty.Terminal.start_link(cols: 120, rows: 40)\n\nGhostty.Terminal.write(term, \"Hello, \\e[1;32mworld\\e[0m!\\r\\n\")\n\n{:ok, text} = Ghostty.Terminal.snapshot(term)\n# =\u003e \"Hello, world!\"\n\n{:ok, html} = Ghostty.Terminal.snapshot(term, :html)\n# =\u003e HTML with inline color styles\n\n{col, row} = Ghostty.Terminal.cursor(term)\n# =\u003e {0, 1}\n```\n\n## Supervision\n\n```elixir\nchildren = [\n  {Ghostty.Terminal, name: :console, cols: 120, rows: 40},\n  {Ghostty.Terminal, name: :logs, id: :logs, cols: 200, rows: 100,\n   max_scrollback: 100_000}\n]\n\nSupervisor.start_link(children, strategy: :one_for_one)\n\nGhostty.Terminal.write(:console, data)\n{:ok, html} = Ghostty.Terminal.snapshot(:console, :html)\n```\n\n## Messages\n\n### Terminal effects\n\nEffect messages are sent to the process that called `start_link/1`:\n\n| Message | Trigger |\n|---|---|\n| `{:pty_write, binary}` | Query responses to write back to the PTY |\n| `:bell` | BEL character (`\\a`) |\n| `:title_changed` | Title change via OSC 2 |\n\n### Subprocess output\n\n`Ghostty.PTY` sends messages to the process that called `start_link/1`:\n\n| Message | Trigger |\n|---|---|\n| `{:data, binary}` | Subprocess stdout/stderr |\n| `{:exit, status}` | Subprocess exit |\n\n## Resize with reflow\n\n```elixir\nGhostty.Terminal.write(term, String.duplicate(\"x\", 120) \u003c\u003e \"\\r\\n\")\nGhostty.Terminal.resize(term, 40, 24)\n{:ok, text} = Ghostty.Terminal.snapshot(term)\n# Long line is now wrapped across 3 lines\n```\n\n## Strip ANSI\n\n```elixir\n{:ok, term} = Ghostty.Terminal.start_link(cols: 200, rows: 500)\nGhostty.Terminal.write(term, ansi_output)\n{:ok, plain} = Ghostty.Terminal.snapshot(term)\n```\n\n## Render state\n\nRead the screen as a grid of cells for building custom renderers\n(LiveView, Scenic, etc.):\n\n```elixir\nrows = Ghostty.Terminal.cells(term)\n\nfor row \u003c- rows do\n  for {grapheme, fg, bg, flags} \u003c- row do\n    if Ghostty.Terminal.Cell.bold?({grapheme, fg, bg, flags}) do\n      IO.write(IO.ANSI.bright())\n    end\n    IO.write(grapheme)\n  end\n  IO.puts(\"\")\nend\n```\n\n## Current terminal TTY\n\nUse `Ghostty.TTY` for local terminal applications that need raw keyboard input\nfrom the terminal running the BEAM process:\n\n```elixir\n{:ok, tty} = Ghostty.TTY.start_link()\nGhostty.TTY.write(tty, [IO.ANSI.clear(), IO.ANSI.home(), \"Ready\"])\n\nreceive do\n  {Ghostty.TTY, ^tty, {:key, %Ghostty.KeyEvent{key: :enter}}} -\u003e :submitted\n  {Ghostty.TTY, ^tty, {:resize, cols, rows}} -\u003e {cols, rows}\nend\n```\n\n`Ghostty.TTY` complements `Ghostty.PTY`: TTY is the current terminal; PTY is for\nchild pseudo-terminals. Raw terminal bytes are decoded by `Ghostty.KeyDecoder`.\nSee `examples/tty_keys.exs` for an interactive smoke test.\n\n## PTY\n\nRun interactive programs in a real pseudo-terminal:\n\n```elixir\n{:ok, term} = Ghostty.Terminal.start_link(cols: 80, rows: 24)\n{:ok, pty} = Ghostty.PTY.start_link(cmd: \"/bin/bash\", cols: 80, rows: 24)\n\n# PTY output arrives as messages\nreceive do\n  {:data, data} -\u003e Ghostty.Terminal.write(term, data)\nend\n\n# Send keyboard input\nGhostty.PTY.write(pty, \"ls --color\\n\")\n\n# Resize the PTY (reflows in the terminal too)\nGhostty.PTY.resize(pty, 120, 40)\nGhostty.Terminal.resize(term, 120, 40)\n```\n\n## LiveView\n\nInstall the LiveView hook into a Phoenix app with:\n\n```bash\nmix igniter.install ghostty\n```\n\nThen drop in a terminal with `Ghostty.LiveTerminal.Component` — it handles\nkeyboard events internally so your LiveView only manages the terminal\nand PTY lifecycle:\n\n```elixir\ndefmodule MyAppWeb.TerminalLive do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, term} = Ghostty.Terminal.start_link(cols: 80, rows: 24)\n    {:ok, assign(socket, term: term, pty: nil)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    \u003c.live_component\n      module={Ghostty.LiveTerminal.Component}\n      id=\"term\"\n      term={@term}\n      pty={@pty}\n      fit={true}\n      autofocus={true}\n    /\u003e\n    \"\"\"\n  end\n\n  def handle_info({:terminal_ready, \"term\", cols, rows}, socket) do\n    {:ok, pty} = Ghostty.PTY.start_link(cmd: \"/bin/bash\", cols: cols, rows: rows)\n    {:noreply, assign(socket, pty: pty)}\n  end\n\n  def handle_info({:data, data}, socket) do\n    Ghostty.Terminal.write(socket.assigns.term, data)\n    send_update(Ghostty.LiveTerminal.Component, id: \"term\", refresh: true)\n    {:noreply, socket}\n  end\n\n  def handle_info({:exit, _status}, socket), do: {:noreply, socket}\nend\n```\n\n### Component assigns\n\n| Assign | Default | Description |\n|---|---|---|\n| `:term` | required | `Ghostty.Terminal` pid |\n| `:pty` | `nil` | `Ghostty.PTY` pid; key input is written here when present |\n| `:fit` | `false` | Auto-fit terminal size to the rendered container |\n| `:autofocus` | `false` | Focus the hidden terminal input on mount |\n| `:class` | `\"\"` | CSS class for the container div |\n\nWhen `fit` is enabled, the hook measures the container and sends a `\"ready\"`\nevent with the computed `cols` and `rows`. The component resizes the terminal\nand notifies the parent with `{:terminal_ready, id, cols, rows}` —\nuse this to defer PTY startup until the real container size is known.\n\n### Low-level helpers\n\nFor full control, use the helpers directly:\n\n```elixir\nGhostty.LiveTerminal.key_event_from_params(params)       # parse browser key event\nGhostty.LiveTerminal.handle_key(term, params)             # parse + encode\nGhostty.LiveTerminal.push_render(socket, \"term-id\", term) # push cells to client\n```\n\n### Asset bundling\n\n`mix igniter.install ghostty` vendors `ghostty.js` into your app assets and wires\n`GhosttyTerminal` into `assets/js/app.js` automatically.\n\nTypeScript source lives in `priv/ts/` and is bundled at compile time via\n[OXC](https://hex.pm/packages/oxc) — no Node.js or Bun required for end users.\n\nContributors can run TypeScript quality checks:\n\n```bash\nbun install \u0026\u0026 bun run lint \u0026\u0026 bun run format:check\n```\n\n### Demo app\n\nSee [`examples/live_terminal/`](https://github.com/dannote/ghostty_ex/tree/master/examples/live_terminal)\nfor a complete runnable app with Playwright browser tests. It includes a control\npanel with preset commands, fit/banner toggles, and sets `TERM=xterm-256color`\nfor colorized shell output.\n\n## ExUnit helpers\n\n`Ghostty.Test` provides concise test helpers without expanding the core\n`Ghostty.Terminal` API:\n\n```elixir\ndefmodule MyTerminalTest do\n  use ExUnit.Case, async: true\n\n  import Ghostty.Test\n\n  test \"renders output\" do\n    {:ok, terminal} = term(cols: 80, rows: 24)\n\n    terminal\n    |\u003e lines([\"Hello\", IO.ANSI.red(), \"red\", IO.ANSI.reset()])\n    |\u003e assert_text(\"Hello\")\n    |\u003e refute_text(\"missing\")\n    |\u003e assert_snap(\"test/fixtures/terminal/basic.txt\")\n  end\nend\n```\n\nSet `UPDATE_GHOSTTY_SNAPSHOTS=1` to rewrite snapshot fixture files.\n\n## Examples\n\nSee the [`examples/`](https://github.com/dannote/ghostty_ex/tree/master/examples) directory:\n\n| Example | What it does |\n|---|---|\n| [`hello.exs`](https://github.com/dannote/ghostty_ex/blob/master/examples/hello.exs) | Write with colors, read back plain + HTML |\n| [`ansi_stripper.exs`](https://github.com/dannote/ghostty_ex/blob/master/examples/ansi_stripper.exs) | Pipe stdin, strip ANSI codes |\n| [`html_recorder.exs`](https://github.com/dannote/ghostty_ex/blob/master/examples/html_recorder.exs) | Capture command output as styled HTML |\n| [`progress_bar.exs`](https://github.com/dannote/ghostty_ex/blob/master/examples/progress_bar.exs) | `\\r` overwrites → final screen state only |\n| [`reflow.exs`](https://github.com/dannote/ghostty_ex/blob/master/examples/reflow.exs) | Text reflow on resize |\n| [`supervised.exs`](https://github.com/dannote/ghostty_ex/blob/master/examples/supervised.exs) | Named terminals in a supervision tree |\n| [`diff.exs`](https://github.com/dannote/ghostty_ex/blob/master/examples/diff.exs) | Terminal-aware Myers diff |\n| [`expect.exs`](https://github.com/dannote/ghostty_ex/blob/master/examples/expect.exs) | Expect-like automation with pattern matching |\n| [`pool.exs`](https://github.com/dannote/ghostty_ex/blob/master/examples/pool.exs) | Reusable terminal pool for concurrent processing |\n| [`live_terminal/`](https://github.com/dannote/ghostty_ex/tree/master/examples/live_terminal) | Phoenix LiveView terminal renderer with Playwright browser tests |\n\n## Development\n\n[Zig 0.15+](https://ziglang.org) is only required for source builds.\n\n```bash\ngit clone https://github.com/dannote/ghostty_ex\ncd ghostty_ex\nmix deps.get\nmix ghostty.setup            # clones Ghostty, builds libghostty-vt\nGHOSTTY_BUILD=1 mix test\n```\n\nTo use an existing Ghostty checkout:\n\n```bash\nGHOSTTY_SOURCE_DIR=~/code/ghostty mix ghostty.setup\n```\n\n### Troubleshooting\n\n**Xcode 26.4 breaks Zig builds on macOS.** Downgrade to Xcode 26.3 CLT:\n\n```bash\n# Download from https://developer.apple.com/download/all/?q=Command+Line+Tools+for+Xcode+26.3\nsudo xcode-select --switch /Library/Developer/CommandLineTools\n```\n\nSee [ziglang/zig#31658](https://codeberg.org/ziglang/zig/issues/31658) for details.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdannote%2Fghostty_ex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdannote%2Fghostty_ex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdannote%2Fghostty_ex/lists"}