Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
5fe4f56
feat: harden runtime for Jido Console
mikehostetler Aug 20, 2026
4314b83
feat: complete TermUI 1.0 runtime and widgets
mikehostetler Aug 20, 2026
053d3a4
fix: harden runtime and terminal cleanup
mikehostetler Aug 22, 2026
89f5f58
ci: harden release candidate delivery
mikehostetler Aug 22, 2026
b79aec7
ci: use Node 24 action runtimes
mikehostetler Aug 22, 2026
68b8a83
fix: preserve UI acronym in run task
mikehostetler Aug 22, 2026
f709051
ci: adopt Jido package quality platform
mikehostetler Aug 22, 2026
8b56def
test: add release matrix coverage margin
mikehostetler Aug 22, 2026
c2fc464
feat: add bounded stream delivery
mikehostetler Aug 22, 2026
f25725b
feat: parse streaming markdown incrementally
mikehostetler Aug 22, 2026
58c9db7
feat: add reusable advanced UI state
mikehostetler Aug 22, 2026
04bc739
docs: update advanced feature parity
mikehostetler Aug 22, 2026
f6bf6bd
deps: update CI compatibility packages
mikehostetler Aug 22, 2026
1f5cfa9
fix: suppress logger during full-screen sessions
mikehostetler Aug 23, 2026
e994f75
deps: refresh dependency lock
mikehostetler Aug 23, 2026
4e14665
fix: preserve terminal control input in raw mode
mikehostetler Aug 25, 2026
ff2b46b
fix: harden core runtime boundaries
mikehostetler Aug 25, 2026
cf6f6c5
feat: add interactive TermUI showcase
mikehostetler Aug 25, 2026
5f2906a
feat: add live data to showcase
mikehostetler Aug 25, 2026
976d56e
fix: harden runtime and widget behavior
mikehostetler Aug 25, 2026
d894a03
test: raise TermUI coverage above 90%
mikehostetler Aug 26, 2026
f3eb1c9
feat(widgets): add TUIStudio parity controls
mikehostetler Aug 26, 2026
5f5de9a
refactor(structs): limit Zoi to data boundaries
mikehostetler Aug 26, 2026
c2574c8
chore: sync upstream develop
mikehostetler Aug 31, 2026
4514c93
feat(ssh): restore v2 remote sessions (#48)
mikehostetler Aug 31, 2026
7621d7a
feat(app): add v2 compatibility facade (#49)
mikehostetler Aug 31, 2026
d205e22
feat(runtime): add safe v1 aliases (#50)
mikehostetler Aug 31, 2026
6210f63
feat(config): adapt v1 runtime settings (#51)
mikehostetler Aug 31, 2026
aa57f95
feat(widget): add parent-owned routing helpers (#52)
mikehostetler Aug 31, 2026
0f10a86
feat(test): publish deterministic v2 backend (#53)
mikehostetler Aug 31, 2026
a23133e
feat(layout): add pure constraint helpers (#54)
mikehostetler Aug 31, 2026
091f71f
feat(build): make TTY NIF optional (#55)
mikehostetler Aug 31, 2026
1960c99
feat(widget): add honest compatibility facade (#56)
mikehostetler Aug 31, 2026
8e7f439
feat(table): restore sorting and multi-select (#57)
mikehostetler Aug 31, 2026
0f91466
feat(input): add v2 event normalization helpers (#58)
mikehostetler Aug 31, 2026
67cdc17
feat(form): restore validation error flow (#59)
mikehostetler Aug 31, 2026
b2c2664
feat(menu): restore nested navigation (#60)
mikehostetler Aug 31, 2026
a49dbd5
feat(markdown): add optional syntax highlighting (#61)
mikehostetler Aug 31, 2026
2790c79
feat(split-pane): add versioned state restore (#62)
mikehostetler Aug 31, 2026
6aa4c80
feat(snapshot): add optional system providers (#63)
mikehostetler Aug 31, 2026
43ba77e
feat(toast): add explicit expiry commands (#64)
mikehostetler Aug 31, 2026
f4e8a5c
ci(runtime): verify supported baseline (#65)
mikehostetler Aug 31, 2026
9bc2674
docs(architecture): record UI context decision (#66)
mikehostetler Aug 31, 2026
80e5da6
fix(ci): compile cleanly on Elixir 1.20 (#65)
mikehostetler Aug 31, 2026
93eb1a0
fix(build): repair cached native output path (#65)
mikehostetler Aug 31, 2026
048ae09
chore: prepare next v2 branch
mikehostetler Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .dialyzer_ignore.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
# MDEx 0.13.5 refers to the type of its optional Lumis dependency.
{"lib/mdex/document.ex", "Unknown type: Lumis.options/0."}
]
9 changes: 8 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
inputs: [
"{mix,.formatter}.exs",
"{config,lib,test}/**/*.{ex,exs}",
"examples/iex_counter/{mix,run}.exs",
"examples/iex_counter/{lib,test}/**/*.{ex,exs}",
"examples/showcase/{mix,run,.formatter}.exs",
"examples/showcase/{lib,test}/**/*.{ex,exs}"
]
]
25 changes: 25 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
version: 2
updates:
- package-ecosystem: "mix"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "elixir"
commit-message:
prefix: "deps"
include: "scope"

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "deps"
include: "scope"
108 changes: 72 additions & 36 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,53 +1,89 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]
merge_group:
push:
branches:
- develop
- next/v2

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
MIX_ENV: test
permissions:
actions: read
contents: read

jobs:
test:
name: Test (Elixir ${{ matrix.elixir }} / OTP ${{ matrix.otp }})
runs-on: ubuntu-latest
ci:
name: CI
uses: agentjido/github-actions/.github/workflows/jido-ci.yml@v5
secrets: inherit
with:
test_matrix: '[{"otp":"28","elixir":"1.18.4"},{"otp":"28","elixir":"1.19"},{"otp":"28","elixir":"1.20"},{"otp":"29","elixir":"1.20"}]'
format_command: mix do format --check-formatted, xref graph --format cycles --fail-above 0
docs_command: mix docs --warnings-as-errors -f html && mix doctor --raise
test_command: mix coveralls --warnings-as-errors

tty-nif-policy:
name: TTY NIF (${{ matrix.mode }}, OTP ${{ matrix.otp }}, Elixir ${{ matrix.elixir }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- elixir: "1.19"
- mode: source
otp: "28"

elixir: "1.18.4"
- mode: source
otp: "29"
elixir: "1.20"
- mode: disabled
otp: "29"
elixir: "1.20"
env:
TERM_UI_TTY_NIF: ${{ matrix.mode }}
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Elixir
uses: erlef/setup-beam@v1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with:
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}
elixir-version: ${{ matrix.elixir }}
- name: Compile selected native policy
run: |
mix deps.get
mix compile --warnings-as-errors
- name: Verify selected native policy
run: |
if [ "$TERM_UI_TTY_NIF" = "source" ]; then
mix run --no-start --no-compile -e \
'unless TermUI.Terminal.TtyNif.ensure_loaded() == :ok and TermUI.Terminal.TtyNif.loaded?(), do: raise("TTY NIF did not load")'
else
test ! -e _build/dev/lib/term_ui/priv/term_ui_tty_nif.so
mix run --no-start --no-compile test/support/no_nif_backend_probe.exs
fi
- name: Test selected native policy
run: mix test --warnings-as-errors

- name: Cache dependencies
uses: actions/cache@v4
examples:
name: Example (${{ matrix.directory }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
directory: [iex_counter, showcase]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}-

- name: Install dependencies
run: mix deps.get

- name: Check formatting
run: mix format --check-formatted

- name: Compile with warnings as errors
run: mix compile --warnings-as-errors

- name: Run tests
run: mix test
otp-version: "29"
elixir-version: "1.20"
- name: Compile and test example
working-directory: examples/${{ matrix.directory }}
run: |
mix deps.get
mix deps.unlock --check-unused
git diff --exit-code -- mix.lock
mix test --warnings-as-errors
59 changes: 59 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Release

on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
operation:
description: "Release operation: auto, prepare, or publish"
required: false
type: choice
default: auto
options:
- auto
- prepare
- publish
tag_name:
description: "Optional v-prefixed tag for publish simulation"
required: false
type: string
default: ""
dry_run:
description: "Dry run (no git push, no tag, no GitHub release, no Hex publish)"
required: false
type: boolean
default: false
hex_dry_run:
description: "Hex dry run only (run all git/release steps, but skip actual Hex publish)"
required: false
type: boolean
default: false
skip_tests:
description: "Skip tests before release"
required: false
type: boolean
default: false
version_override:
description: "Optional bare SemVer override (for example 1.2.3, not v1.2.3)"
required: false
type: string
default: ""

permissions:
actions: write
contents: write

jobs:
release:
name: Release
uses: agentjido/github-actions/.github/workflows/jido-release.yml@v5
with:
operation: ${{ inputs.operation || 'auto' }}
tag_name: ${{ inputs.tag_name || '' }}
dry_run: ${{ inputs.dry_run || false }}
hex_dry_run: ${{ inputs.hex_dry_run || false }}
skip_tests: ${{ inputs.skip_tests || false }}
version_override: ${{ inputs.version_override || '' }}
secrets: inherit
22 changes: 22 additions & 0 deletions .github/workflows/review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Jido Review

on:
pull_request:
branches:
- develop
- next/v2

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
actions: read
contents: read
issues: write
pull-requests: write

jobs:
review:
name: Jido Review
uses: agentjido/github-actions/.github/workflows/jido-review.yml@v5
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# TermUI Agent Instructions

TermUI is a small Elm-style terminal runtime for Elixir and the BEAM.

Keep these runtime boundaries:

- One runtime process owns application state and update order.
- Application views return one complete `TermUI.Frame`.
- One backend owner controls terminal input, output, size, capabilities, and cleanup.
- Widgets are pure. The parent application owns widget state and effects.
- Commands are data values. Do not run effects in widget code.

Use `next/v2` as the pull request target while the v2 runtime is under
development. Do not merge v2 work into `develop` until the project makes a
separate release decision. Preserve the public `TermUI` namespace and the Jido
Console runtime contract. Run `mix quality` and `mix coveralls` before a commit.
Terminal lifecycle changes also need a real terminal check.

Use Conventional Commits. Never mention an AI assistant in a commit message.
41 changes: 18 additions & 23 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

TermUI is a direct-mode Terminal UI framework for Elixir/BEAM, currently in the research and design phase. The goal is to build a world-class TUI framework that leverages BEAM's unique strengths (fault tolerance, actor model, hot code reloading, distribution) while adopting proven patterns from modern TUI frameworks like BubbleTea (Go) and Ratatui (Rust).
TermUI is a small Elm-style terminal runtime for Elixir and the BEAM.

## Target Architecture

The framework uses The Elm Architecture adapted for OTP with three abstraction layers:
The runtime has three clear boundaries:

1. **Port layer** - Low-level terminal interface (raw mode, escape sequences, capability detection)
2. **Renderer layer** - Virtual screen buffer with differential updates (ETS-based double buffering)
3. **Widget layer** - OTP-based component system with supervision
1. **Application** - One runtime process owns application state and serializes updates.
2. **Frame** - Pure views return one canonical `TermUI.Frame`.
3. **Backend** - One backend owner serializes input, output, size, capabilities, and cleanup.

### Key Design Decisions

- **OTP 28+ only** - Uses native raw mode via `shell.start_interactive({:noshell, :raw})`
- **Process-per-component** for interactive widgets, shared state for static display elements
- **Framerate-limited rendering** (60 FPS default) with intelligent diffing
- **Cassowary constraint solver** for layouts with LRU caching
- **Commands pattern** for side effects (async operations return messages to update loop)
- **Pure widgets** with state owned by the parent application
- **Coalesced frame scheduling** with one final meaningful render
- **One normalized event model** for keys, text, paste, mouse, resize, and focus
- **Data commands** for messages, timers, asynchronous work, and shutdown

### Platform Targets

Expand All @@ -30,21 +30,16 @@ The framework uses The Elm Architecture adapted for OTP with three abstraction l

## Project Status

Currently in research phase. The `notes/research/state_of_tui.md` contains comprehensive analysis of:
- Historical terminal architecture (terminfo, curses, VT100)
- Modern TUI frameworks (BubbleTea, Ratatui, Textual, FTXUI, etc.)
- BEAM-specific patterns (GenServer, Supervisors, GenStage, Ports vs NIFs)
- Direct mode programming requirements
- Proposed architecture and implementation roadmap
The `1.0.0-rc` design is implemented. Files under `notes/` are historical research and are not current architecture guidance.

## Development Notes

When implementation begins, follow these patterns:
Follow these patterns:

- Use **GenServer** for stateful widgets with clear message-based APIs
- Use **Supervisors** to mirror UI component hierarchies for fault isolation
- Prefer **Ports over NIFs** for terminal I/O (crash isolation)
- Use **ETS tables** for render buffers (`:screen_current`, `:screen_previous`)
- Implement **cursor optimization** (compare cost of absolute vs relative positioning)
- Support graceful degradation for terminal features (true color → 256 → 16 → mono)
- IMPORTANT you must NEVER mention Claude or any AI assistant in your commit messages!
- Keep application and widget transitions pure.
- Keep terminal implementation logic in backends.
- Return `TermUI.Frame` directly from application views.
- Keep backend callback state under one serialized owner.
- Use `TermUI.Command` values for effects.
- Preserve graceful degradation for terminal features.
- IMPORTANT you must NEVER mention Claude or any AI assistant in your commit messages!
32 changes: 32 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Contributing to TermUI

Use the `develop` branch as the pull request target. Keep changes focused and
include tests for behavior changes.

Before you submit a pull request, run:

```bash
mix deps.get
mix quality
mix coveralls
mix deps.unlock --check-unused
mix hex.audit
mix docs --warnings-as-errors -f html
HEX_API_KEY=dry-run mix hex.publish --dry-run --yes
```

TermUI uses the shared v5 Jido CI, review, and release workflows. Dependabot
checks Mix and GitHub Actions dependencies each week. Use Conventional Commits.
Do not edit `CHANGELOG.md` in a normal pull request. `git_ops` creates release
notes from commit history during release preparation.

Terminal lifecycle changes also need a manual check in a real terminal. Verify
normal exit, application failure, backend failure, and forced process exit.
After each case, confirm that cooked input, the cursor, style, paste mode,
focus events, mouse tracking, and the active screen are restored.

The supported runtime matrix is Elixir 1.18.4 or later on OTP 28 or later. CI
tests Elixir 1.18.4, 1.19, and 1.20 on OTP 28, and Elixir 1.20 on OTP 29.

See [Package quality](guides/package-quality.md) for the Jido standard and the
documented compatibility exceptions.
Loading
Loading