{"id":50235384,"url":"https://github.com/elixir-vibe/cringe","last_synced_at":"2026-06-01T00:00:40.800Z","repository":{"id":360458255,"uuid":"1249100773","full_name":"elixir-vibe/cringe","owner":"elixir-vibe","description":"OTP-native terminal UI toolkit for Elixir","archived":false,"fork":false,"pushed_at":"2026-05-27T07:41:22.000Z","size":141,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-27T20:22:38.852Z","etag":null,"topics":["ansi","beam","cli","developer-tools","elixir","otp","terminal","terminal-ui","tui"],"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/elixir-vibe.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-05-25T10:57:29.000Z","updated_at":"2026-05-27T07:41:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/elixir-vibe/cringe","commit_stats":null,"previous_names":["elixir-vibe/cringe"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/elixir-vibe/cringe","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-vibe%2Fcringe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-vibe%2Fcringe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-vibe%2Fcringe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-vibe%2Fcringe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/elixir-vibe","download_url":"https://codeload.github.com/elixir-vibe/cringe/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-vibe%2Fcringe/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33624216,"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-05-28T02:00:06.440Z","response_time":99,"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":["ansi","beam","cli","developer-tools","elixir","otp","terminal","terminal-ui","tui"],"created_at":"2026-05-26T19:00:55.496Z","updated_at":"2026-05-28T21:00:32.940Z","avatar_url":"https://github.com/elixir-vibe.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Cringe\n\nOTP-native terminal UI toolkit for Elixir.\n\nCringe helps you build terminal interfaces with plain Elixir data, supervised processes, semantic input events, and ExUnit-friendly rendering. The name is a joke; the goal is serious terminal UI ergonomics for the BEAM.\n\n```elixir\nuse Cringe\n\nbox padding: 1 do\n  column gap: 1 do\n    text(\"Cringe\", color: :green, bold: true)\n    text(\"Terminal UI for Elixir\")\n    progress(value: 0.42, width: 16, label: \"Build\")\n  end\nend\n|\u003e render(ansi: true)\n|\u003e IO.puts()\n```\n\n## Status\n\nCringe is early alpha. It is useful for experiments, demos, small tools, and for exploring terminal UI design on the BEAM. APIs may change before `1.0`.\n\n## Why Cringe?\n\n- **Plain Elixir documents** — compose text, rows, columns, boxes, and widgets without a template language.\n- **OTP-native runtime** — apps are regular supervised processes with explicit state and event handling.\n- **Ghostty-backed terminal input** — keyboard decoding and current-terminal integration use the `ghostty` package instead of hand-rolled TTY parsing.\n- **Semantic events** — apps handle `%Cringe.Event.Key{}`, `%Cringe.Event.Text{}`, `%Cringe.Event.Resize{}`, and `%Cringe.Event.Tick{}`.\n- **Testable rendering** — assert terminal output with normal ExUnit heredocs.\n- **Small widget layer** — render inputs, selects, progress bars, and spinners while keeping app state explicit.\n- **Canvas + painter pipeline** — render fixed-size frames and repaint changed lines efficiently.\n\n## Installation\n\nAdd `cringe` to your dependencies:\n\n```elixir\ndef deps do\n  [\n    {:cringe, \"~\u003e 0.4.0\"}\n  ]\nend\n```\n\nDocumentation: \u003chttps://hexdocs.pm/cringe\u003e\n\n## Documents\n\nImport the DSL with `use Cringe` or `import Cringe`:\n\n```elixir\nuse Cringe\n\ncolumn gap: 1 do\n  text(\"Deploy\", color: :green, bold: true)\n  text(\"Building assets\")\n  progress(value: 0.7, width: 20)\nend\n|\u003e render(ansi: true)\n```\n\nCore building blocks:\n\n```elixir\ntext(\"hello\", color: :green, bold: true)\nrow([text(\"left\"), text(\"right\")], gap: 2)\ncolumn([text(\"one\"), text(\"two\")], gap: 1)\nbox(text(\"inside\"), padding: 1)\n```\n\nBlock syntax is available for containers:\n\n```elixir\nbox padding: 1 do\n  column gap: 1 do\n    text(\"Title\")\n    text(\"Body\")\n  end\nend\n```\n\n## Widgets\n\nWidgets are render-only by default. You keep state in your app and pass it in explicitly.\n\n```elixir\ncolumn gap: 1 do\n  spinner(frame: 2, label: \"Loading\")\n  progress(value: 0.42, width: 16, label: \"Build\")\n  input(value: \"cringe\", focused: true, width: 24)\n  select(options: [\"Dashboard\", \"Logs\", \"Settings\"], selected: 1, focused: true)\nend\n```\n\nCursor-aware input state is available when you need editing behavior:\n\n```elixir\nalias Cringe.Widgets.Input\nalias Cringe.Widgets.Input.State\n\nstate = State.new(\"hello\", cursor: 5)\n{:ok, state} = Input.update(state, Cringe.Event.text(\"!\"))\n```\n\nSelects expose the same explicit update style:\n\n```elixir\nalias Cringe.Widgets.Select\n\n{:ok, selected} = Select.update(0, Cringe.Event.key(:down), [\"one\", \"two\"])\n```\n\n## Interactive apps\n\nCringe apps are modules that use `Cringe.App`:\n\n```elixir\ndefmodule Counter do\n  use Cringe.App\n\n  def init(_opts), do: {:ok, %{count: 0}}\n\n  def handle_event(%Cringe.Event.Key{key: :up}, state),\n    do: {:noreply, %{state | count: state.count + 1}}\n\n  def handle_event(%Cringe.Event.Key{key: :down}, state),\n    do: {:noreply, %{state | count: state.count - 1}}\n\n  def handle_event(%Cringe.Event.Text{text: \"q\"}, _state),\n    do: {:stop, :normal}\n\n  def render(state) do\n    box padding: 1 do\n      column gap: 1 do\n        text(\"Counter\", color: :green, bold: true)\n        text(\"Count: #{state.count}\")\n        text(\"Use arrows, q quits\", color: :bright_black)\n      end\n    end\n  end\nend\n\n{:ok, app} =\n  Cringe.run(Counter,\n    backend: {Cringe.Runtime.Backend.Terminal, alternate_screen: true},\n    ansi: true\n  )\n\nCringe.Runtime.paint(app)\n```\n\nThe terminal backend uses `Ghostty.TTY` for current-terminal input when running against `:stdio`.\n\nFor OTP trees, start the runtime under its supervisor:\n\n```elixir\n{:ok, supervisor} = Cringe.run_supervised(Counter, ansi: true)\napp = Cringe.Runtime.Supervisor.runtime(supervisor)\n```\n\n## Layout regions and focus\n\nLayout nodes preserve document IDs, roles, focusability, and coordinates:\n\n```elixir\nlayout =\n  box padding: 1 do\n    input(id: :name, value: \"Dan\")\n  end\n  |\u003e Cringe.Layout.Engine.layout()\n\nCringe.Layout.find(layout, :name)\nCringe.Layout.at(layout, 2, 2)\nCringe.Layout.path_at(layout, 2, 2)\nCringe.Layout.focusable(layout)\n```\n\n`Cringe.Focus` is a tiny deterministic focus ring:\n\n```elixir\nfocus = Cringe.Focus.new([:name, :email, :role])\nfocus = Cringe.Focus.next(focus)\nCringe.Focus.focused?(focus, :email)\n```\n\nThe form example shows this with inputs and selects.\n\n## Architecture\n\nCringe keeps each terminal UI stage explicit:\n\n```text\nDocument -\u003e Layout.Node tree -\u003e Draw/Canvas -\u003e Frame -\u003e Painter -\u003e Backend\n```\n\n- Documents are plain Elixir structs built with functions or the DSL.\n- Layout computes positioned nodes, sizes, content rectangles, cursors, focus metadata, and hit regions.\n- Draw turns the layout tree into a fixed-size canvas and frame.\n- The painter compares frames and emits terminal updates.\n- Backends write updates to tests, IO devices, or the current terminal.\n\nThis split keeps app state semantic and makes rendering deterministic in tests.\n\n## Testing\n\nCringe test helpers keep expected terminal output readable in normal ExUnit assertions:\n\n```elixir\ndefmodule MyUITest do\n  use ExUnit.Case, async: true\n\n  use Cringe.Case\n\n  test \"renders a box\" do\n    assert_render box(text(\"hi\"), padding: 1), \"\"\"\n    ╭────╮\n    │    │\n    │ hi │\n    │    │\n    ╰────╯\n    \"\"\"\n  end\nend\n```\n\nFor apps:\n\n```elixir\n{:ok, app} = Cringe.Driver.start(Counter)\nCringe.Driver.keys(app, [:up, :up])\n\nassert Cringe.Driver.await_state(app, \u0026(\u00261.count == 2))\nassert_app_text(app, \"...\")\n```\n\n`Cringe.Driver.await_frame/3` is useful when testing async terminal input, resize, or tick-driven repaint behavior.\n\n## Examples\n\nRun examples locally:\n\n```sh\nmix run examples/hello.exs\nmix run examples/dashboard.exs\nmix run examples/layout.exs\nmix run examples/dsl.exs\nmix run examples/widgets.exs\nmix run examples/counter.exs\nmix run examples/interactive_counter.exs\nmix run examples/interactive_input.exs\nmix run examples/form.exs\nmix run examples/ticking_spinner.exs\n```\n\nThe interactive examples use the terminal backend. `q` or Ctrl+C exits where supported.\n\n## Benchmarks\n\nCringe includes local Benchee benchmarks for render, canvas, painter, and input paths:\n\n```sh\nmix bench\n```\n\nBenchmarks are for local regression checks and are not part of CI.\n\n## Development\n\n```sh\nmix deps.get\nmix ci\n```\n\n## License\n\nMIT © 2026 Danila Poyarkov\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felixir-vibe%2Fcringe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Felixir-vibe%2Fcringe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felixir-vibe%2Fcringe/lists"}