Skip to content

Release 1.0.0-rc#21

Merged
pcharbon70 merged 195 commits into
mainfrom
develop
Jan 26, 2026
Merged

Release 1.0.0-rc#21
pcharbon70 merged 195 commits into
mainfrom
develop

Conversation

@pcharbon70

Copy link
Copy Markdown
Owner

Release 1.0.0-rc

This release candidate adds TTY backend rendering support for IEx compatibility and improves developer experience.

Major Changes

  • TTY Backend: New TTY mode for running TermUI applications inside IEx
  • mix termui.run: New mix task for easy application execution
  • Backend Selection: Automatic detection between Raw and TTY backends

Documentation Updates

  • Updated all 26 example READMEs with both Raw Mode and TTY Mode instructions
  • Added "Understanding Backends: Raw vs TTY" section to user guides
  • Updated Terminal Features section in overview

Technical Details

  • Added NodeRenderer.render_to_buffer_direct/3 for direct buffer rendering
  • Fixed do_render/2 to support TTY backend rendering
  • Added IEx-compatible character input via :io.get_chars/2
  • Fixed resize callback registration to check Terminal GenServer status

Test plan

  • Test Raw Mode: mix termui.run from example directory
  • Test TTY Mode: iex -S mix then ExampleApp.run()
  • Verify all examples work with both backends
  • Review documentation builds correctly

pcharbon70 and others added 30 commits December 3, 2025 13:28
Implement Section 1.1 of the multi-renderer plan:
- Define Backend behaviour with 10 callbacks for terminal operations
- Add types: position, size, color, cell, event, state
- Lifecycle callbacks: init/1, shutdown/1
- Query callbacks: size/1
- Cursor callbacks: move_cursor/2, hide_cursor/1, show_cursor/1
- Rendering callbacks: clear/1, draw_cells/2, flush/1
- Input callback: poll_event/2
- Add comprehensive unit tests (24 tests)
- Update phase plan with completed tasks
Create selector module with comprehensive documentation:
- Document "try raw mode first" strategy for backend selection
- Explain why environment heuristics are insufficient
- Define types: selection_result, raw_state, capabilities, color_depth
- Implement select/0 and select/1 with placeholder logic
- Add 18 unit tests for module structure and documentation
- Update phase plan with completed task 1.2.1
Add try_raw_mode/0 and attempt_raw_mode/0 functions:
- Call :shell.start_interactive({:noshell, :raw}) for raw mode detection
- Return {:raw, %{raw_mode_started: true}} on success
- Return {:tty, capabilities} when shell already running
- Wrap in try/rescue for pre-OTP 28 fallback (UndefinedFunctionError)
- Add 10 new tests for selection logic (28 total)
- Update phase plan with completed task 1.2.2
Add detect_capabilities/0 function that detects terminal capabilities
when falling back to TTY mode. Detection includes:

- Color depth via $COLORTERM and $TERM environment variables
- Unicode support via $LC_ALL, $LC_CTYPE, and $LANG locale settings
- Terminal dimensions via :io.columns/0 and :io.rows/0
- Terminal presence via :io.getopts/0 :terminal key

Add 18 new tests for capability detection (46 total).
Task 1.2.4 verified that select/1 explicit selection was already
implemented in task 1.2.1. Mark all subtasks and Section 1.2 as
complete in the phase plan.

Section 1.2 (Backend Selector Module) is now fully implemented:
- 1.2.1 Selector Module Structure
- 1.2.2 Core Selection Logic
- 1.2.3 TTY Capability Detection
- 1.2.4 Explicit Selection
- Unit Tests 1.2
Concerns addressed:
- Add detailed comment explaining defensive error handling for
  unexpected errors from :shell.start_interactive/1
- Remove redundant == true comparison in detect_terminal_presence/0
- Add comprehensive tests for all 13 terminal types in basic_terminal?/1
- Add tests documenting defensive error handling behavior

Improvements implemented:
- Extract test helpers to new SelectorTestHelpers module with with_env/2
  for environment variable isolation and validation helpers
- Simplify nested if expression to use cond for locale priority
- Add @basic_terminals module attribute for maintainability

Test results: 55 tests pass (up from 46, added 9 new tests)
Create State module with struct wrapping backend-specific state
with common metadata for consistent state management across
backend implementations.

Struct fields:
- backend_module (required): The backend implementation module
- backend_state: Backend-specific internal state
- mode (required): Current terminal mode (:raw or :tty)
- capabilities: Detected terminal capabilities (default: %{})
- size: Cached terminal dimensions (default: nil)
- initialized: Whether backend is initialized (default: false)

Includes comprehensive documentation, type specifications,
and 30 unit tests.
Implement constructor functions for creating state structs:
- new/2: General-purpose constructor with backend_module and options
- new_raw/0, new_raw/1: Convenience constructor for raw mode
- new_tty/1, new_tty/2: Convenience constructor for TTY mode

Add 18 new unit tests covering all constructor functions.
Mark task 1.3.2 complete in phase plan.
Implement immutable update functions for state manipulation:
- put_backend_state/2: Updates backend-specific internal state
- put_size/2: Updates cached terminal dimensions
- put_capabilities/2: Replaces capabilities map
- mark_initialized/1: Sets initialized flag to true

Add 23 new unit tests covering all update functions.
Mark task 1.3.3 and section 1.3 complete in phase plan.
Create Config module with functions for reading backend configuration
from the application environment:
- get_backend/0: Backend selection mode (default: :auto)
- get_character_set/0: Preferred character set (default: :unicode)
- get_fallback_character_set/0: Fallback character set (default: :ascii)
- get_tty_opts/0: TTY backend options (default: [line_mode: :full_redraw])
- get_raw_opts/0: Raw backend options (default: [alternate_screen: true])

Add 30 unit tests covering all configuration getters.
Mark task 1.4.1 complete in phase plan.
Add module attributes defining valid configuration values:
- @valid_backends: [:auto, Raw, TTY, Test]
- @valid_character_sets: [:unicode, :ascii]
- @valid_line_modes: [:full_redraw, :incremental]

Add validation functions:
- validate!/0: Validates config, raises ArgumentError with descriptive messages
- valid?/0: Returns boolean without raising

Add 24 unit tests covering all validation scenarios.
Mark task 1.4.2 complete in phase plan.
Implement runtime_config/0 that returns complete configuration as a map:
- Validates configuration before returning (raises on invalid)
- Returns map with backend, character_set, fallback_character_set,
  tty_opts, and raw_opts keys

Add 9 unit tests for runtime_config/0.
Mark task 1.4.3 and section 1.4 complete in phase plan.
- Replace is_list/1 with Keyword.keyword?/1 for stricter validation
- Add alternate_screen boolean type validation in raw_opts
- Add @type config for runtime config map with @TypeDoc
- Add @SPEC declarations to all private validation functions
- Add 10 new tests for stricter validation cases

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Create backend_selection_test.exs with 21 integration tests
- Test backend selection flow from config to selector to state
- Test capability detection and environment variable handling
- Test state management across selection flow
- Complete Phase 1: Backend Selector and Behaviour Definition

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Create Raw backend module implementing TermUI.Backend behaviour
- Add comprehensive documentation for OTP 28+ requirements
- Document raw mode activation flow via Selector
- Alias TermUI.ANSI for escape sequence generation
- Add stub implementations for all 10 behaviour callbacks
- Add 21 unit tests for module structure and stubs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Define defstruct with size, cursor_visible, cursor_position,
  alternate_screen, mouse_mode, and current_style fields
- Add type specifications for mouse_mode, style_state, and t
- Update all callback specs to use t() instead of term()
- Add 10 unit tests for state struct behavior
- Complete Section 2.1: Raw Backend Module Structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
BLOCKER Fixed:
- Add mouse mode naming documentation with mapping table
- Add mouse_mode_to_ansi/1 helper function

CONCERNS Fixed:
- Update Backend.position and Backend.size to use pos_integer()
- Add comprehensive style delta optimization documentation
- Improve test assertions with specific patterns
- Add ExUnit setup block to reduce fixture duplication
- Refactor Terminal module to use ANSI module functions

SUGGESTIONS Implemented:
- Add valid_position?/2 helper function
- Add error handling documentation to callbacks
- Add guard clauses to move_cursor/2
- Clean up task-specific section headers

Test Results: 259 backend tests pass (8 new tests added)
- Implement full init/1 with option parsing:
  - :alternate_screen (default: true)
  - :hide_cursor (default: true)
  - :mouse_tracking (default: :none)
  - :size (auto-detect if not provided)

- Implement terminal setup sequence:
  - Query size via :io.rows/0, :io.columns/0 or env vars
  - Enter alternate screen buffer if enabled
  - Hide cursor if enabled
  - Enable mouse tracking with SGR mode if requested
  - Clear screen and home cursor

- Add private helper functions:
  - get_terminal_size/1 for size detection
  - get_size_from_env/0 for LINES/COLUMNS fallback
  - write_to_terminal/1 with error safety

- Add 12 unit tests for init/1 functionality
- Update existing tests to use explicit size option
- Implement full shutdown/1 with error-safe cleanup sequence:
  - Disable ALL mouse tracking modes defensively
  - Show cursor
  - Reset all text attributes
  - Leave alternate screen if entered
  - Return to cooked mode

- Add error-safe helper functions:
  - safe_write/1 logs errors but continues cleanup
  - safe_cooked_mode/0 handles OTP version differences

- Error handling features:
  - Each step wrapped in try/rescue
  - Errors logged via Logger.warning but don't stop cleanup
  - Special handling for UndefinedFunctionError (pre-OTP 28)
  - Always returns :ok regardless of failures
  - Idempotent - safe to call multiple times

- Add 6 unit tests for shutdown/1 functionality
- Mark Section 2.2 (Initialization Lifecycle) complete
Implements cursor positioning using ANSI escape sequences:
- Generate \e[row;colH sequence via ANSI.cursor_position/2
- Write sequence to terminal via write_to_terminal/1
- Update cursor_position in state
- Return {:ok, updated_state}

Tests:
- 10 new tests covering valid positions, state updates, edge cases,
  guard clause enforcement, and type validation

All 61 backend tests pass.
…Task 2.3.2)

Implements cursor visibility control with idempotent behavior:
- hide_cursor/1: Writes \e[?25l (DECTCEM off), updates cursor_visible to false
- show_cursor/1: Writes \e[?25h (DECTCEM on), updates cursor_visible to true
- Pattern matching on struct fields for idempotent no-ops when already
  in desired state (avoids unnecessary terminal I/O)

Tests:
- 10 new tests covering state updates, idempotent behavior, state
  preservation, and round-trip visibility cycles

All 69 backend tests pass.
Add optimize_cursor option to Raw backend that enables cursor movement
optimization via the existing CursorOptimizer module. When enabled
(default), move_cursor/2 selects the cheapest movement sequence
(absolute vs relative positioning) to reduce cursor movement overhead.

- Add optimize_cursor field to state (default: true)
- Add generate_cursor_sequence/3 private function with pattern matching
- Fall back to absolute positioning when cursor_position is nil
- Add 8 tests for cursor optimization behavior
- Mark Section 2.3 complete in phase plan
Fix all concerns from code review:
- Document bounds checking contract in move_cursor/2
- Add error handling with fallback in generate_cursor_sequence/3
- Document idempotent behavior in hide/show_cursor
- Add bounds checking to CursorOptimizer.advance/2

Implement suggestions:
- Reorder function clauses (most specific to general)
- Consolidate generate_cursor_sequence clauses
- Add assert_state_unchanged_except/3 test helper
- Add cross-reference documentation (See Also sections)
- Add large distance and diagonal move tests

Add @max_cursor_pos constant (9999) to prevent integer overflow.
11 new tests (130 total), all passing.
Add full screen clear functionality:
- Write ESC[2J (clear entire screen) and ESC[1;1H (cursor home)
- Reset current_style to nil (terminal style state unknown after clear)
- Update cursor_position to {1, 1}

Add 6 tests covering return value, state changes, field preservation,
and idempotent behavior. 89 tests total, all passing.
- Enhanced documentation for size/1 with examples and cross-references
- Added 5 unit tests verifying cached dimension retrieval
- Subtasks 2.4.2.3-4 (refresh_size) deferred to Task 2.4.3

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Added refresh_size/1 to re-query terminal dimensions after SIGWINCH
- Returns 3-tuple {:ok, new_size, updated_state} on success
- Reuses get_terminal_size/1 for consistent detection logic
- Comprehensive documentation with SIGWINCH integration example
- Added 9 tests covering success, error, and env fallback cases
- Completes Section 2.4 (Screen Operations)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Concerns fixed:
- Add debug logging to write_to_terminal/1 for troubleshooting
- Add @max_terminal_dimension (9999) with bounds validation

Suggestions implemented:
- Add examples to clear/1 documentation
- Document size/1 typespec rationale (future-proofing comment)
- Refactor get_env_int/1 to use idiomatic with pattern
- Use assert_state_unchanged_except/3 helper consistently
- Extract with_terminal_env/3 test helper for environment setup
- Add draw_cells/2 with cell sorting by position (row, col)
- Implement style delta optimization to minimize escape sequences
- Support all color types: named, 256-color, RGB true color
- Support all 8 text attributes: bold, dim, italic, underline, blink, reverse, hidden, strikethrough
- Build output as iolist for efficient batched I/O
- Track cursor position and style state across cells
- Add 23 comprehensive tests for cell drawing functionality
- Add catch-all clauses to color_sequence/2 with Logger warnings
- Add comprehensive docstrings and @specs to private helpers:
  - normalize_attrs/1: Document MapSet support rationale
  - cursor_move_output/2: Document width assumption, cursor optimization TODO
  - style_delta_output/2: Document SGR duplication TODO
  - build_full_style/1: Document usage context
  - build_style_delta/2: Document delta-only behavior
- Add test for full screen rendering (80x24 = 1920 cells)
- Add test for attribute removal reset path
- Mark Section 2.5 complete in phase plan
Add TermUI.Sanitize module to detect and neutralize terminal escape
sequences that could be injected into user input.

- Detects CSI, OSC, DCS escape sequences
- Three modes: :bracket (safe notation), :remove (strip), :keep
- Max length validation (default: 10_000 chars)
- Control character filtering (allows tab, newline, CR)
- Validates input and returns specific errors
- strip_ansi/1 removes all escapes
- has_ansi?/1 detects presence of escapes

Add 45 security tests validating attack prevention.
Add child_spec/1 function to enable proper supervision:

- TermUI.Runtime - Main runtime orchestrator
- TermUI.Terminal - Terminal management GenServer
- TermUI.Renderer.BufferManager - Double-buffer rendering
- TermUI.ComponentServer - Component lifecycle manager

Child specs enable these processes to be started within a
supervision tree with explicit restart strategies and timeouts.
Add centralized PersistentTerms module for managing persistent_term lifecycle.

Created TermUI.PersistentTerms module to centralize:
- Storage of backend mode, capabilities, and character set
- Query functions for each persistent term
- Cleanup function to erase all TermUI persistent terms

Updated Runtime to:
- Use PersistentTerms instead of direct persistent_term calls
- Call PersistentTerms.cleanup() in terminate/2

Updated App and CharacterSet to use PersistentTerms for queries.

This fixes the OTP blocker about persistent_term cleanup not being called
on shutdown, preventing potential memory leaks from orphaned terms.

17 tests added for PersistentTerms module - all passing.
Replace Process.put/get with state storage in FramerateLimiter.

Updated FramerateLimiter to store the internal dirty atomics reference
in the GenServer state instead of the process dictionary.

Changes:
- Added internal_dirty field to FramerateLimiter state struct
- Store atomics ref in state during init instead of Process.put
- Use state.internal_dirty in handle_call callbacks instead of Process.get

This fixes the OTP blocker about unsafe process dictionary usage in
GenServers, ensuring proper supervisor safety and crash recovery.

22 tests passing.
- Mark Phase 2 (OTP Blockers) as complete in planning doc
- Mark Phase 5 (Planning Document) as complete
- Update multi-renderer Section 6.3 checkboxes
- Create comprehensive summary document
- 117 tests passing across all new modules
Phase 3.1 - Standardize Error Handling:
- Create TermUI.Error module with 15 standardized error types
- Add format/1, error/2, is_error_reason/1, error_type/1 functions
- 20 tests added - all passing

Phase 3.2 - Naming Conventions:
- Rename Backend.State.mode to Backend.State.backend_mode
- Update @type mode to @type backend_mode
- Update Input.Selector documentation
- 71 tests updated in state_test.exs - all passing
- Add naming convention documentation to Backend.State

Total: 137 tests for new/modified modules - all passing
Phase 6 Review Fixes - Complete

All critical security, OTP, and consistency blockers addressed:

Phase 1 - Security Blockers (3/3):
- Command injection fix via TermUI.TermUtils (15 tests)
- Bounded event queue via TermUI.EventQueue (18 tests)
- Escape injection prevention via TermUI.Sanitize (45 tests)

Phase 2 - OTP Blockers (3/3):
- child_spec/1 added to Runtime, Terminal, BufferManager, ComponentServer
- TermUI.PersistentTerms for centralized cleanup (17 tests)
- Process dictionary removed from FramerateLimiter (22 tests)

Phase 3 - Consistency Blockers (2/2):
- TermUI.Error module with standardized error types (20 tests)
- Renamed Backend.State.mode to backend_mode (71 tests)

Phase 4 - Skipped (ANSI modules already exist)

137 tests added for new/modified modules - all passing
Analyze the snake_test project's approach to IEx-compatible input
handling. Research reveals that using :io.get_chars/2 instead of
IO.getn/2 does not solve the IEx input stealing problem, as both
functions use the same IO server (group leader) which IEx controls.

Key findings:
- :io.get_chars/2 with binary: false returns charlists
- Process architecture provides good supervision/cleanup
- Both approaches are affected by IEx input stealing

Recommendations:
- Document IEx as a known limitation (recommended)
- Implement /dev/tty direct access (complex, but works)
- Implement process architecture for structural benefits

Files added:
- notes/features/phase-7.1-iex-research.md - Planning document
- notes/summaries/phase-7.1-research-summary.md - Research findings
- notes/features/test_iex_input.exs - IEx test script
- notes/features/test_io_comparison.exs - IO comparison script
Complete Phase 7.1 research on IEx compatibility.

The research found that the snake_test approach does not solve IEx
input stealing, as both IO.getn/2 and :io.get_chars/2 use the same
IO server controlled by IEx.

Phase 7.2-7.6 implementation is deferred pending decision on:
- Documenting IEx as a known limitation
- Implementing /dev/tty direct access
- Implementing process architecture for structural benefits
User testing confirmed that the snake_test approach works correctly
inside IEx. The initial research conclusion was incorrect.

Key correction:
- snake_test's Snake.start() works perfectly with 'iex -S mix'
- Arrow keys control the snake, input is NOT stolen by IEx
- The combination of :io.get_chars/2 + separate process + receive
  after 0 loop successfully bypasses IEx input interception

Phase 7.2 implementation should proceed with this working approach.
Updated TermUI.Input.TTY to use Erlang's :io.get_chars/2 instead of
Elixir's IO.getn/2. This enables TUI applications to work correctly
inside IEx without having keyboard input stolen by IEx.

Key changes:
- read_char/0 now uses :io.get_chars(~c"", 1) which returns charlists
- Added charlist to binary conversion via :unicode.characters_to_binary/1
- Added setup_io_opts/0 to save original options and set echo/binary modes
- Added restore_io_opts/0 for cleanup
- Added io_opts_set and io_opts_restored fields to state struct
- Increased escape timeout from 50ms to 100ms to match snake_test

Also created TermUI.Input.TTY.Server (archived but not integrated) for
potential future non-blocking I/O implementation.
Updated tests to account for new io_opts_set and io_opts_restored
fields in TTY state struct. Added documentation tests to verify
IEx compatibility is properly documented in moduledoc.

All 47 tests passing.
Added comprehensive planning document and implementation summary
for Phase 7.2 TTY Input Handler IEx Compatibility. Updated
phase-06-integration.md to mark Section 7.2 tasks as complete.
Added a stop/1 callback to the TermUI.Input behaviour to ensure
proper cleanup of input handler resources during Runtime shutdown.

Key changes:
- Input behaviour now requires stop/1 callback
- Raw.stop/1 is a no-op (InputReader managed separately)
- TTY.stop/1 restores IO options (echo, binary mode)
- Runtime.terminate/2 now calls input_handler.stop/1

This ensures TTY handler's IO options are restored when the TUI
application exits, leaving the terminal in a consistent state.
Added tests for the new stop/1 callback in the Input behaviour
and its implementations in Raw and TTY handlers.
Added comprehensive planning document and implementation summary
for Phase 7.3 Runtime Integration. Updated phase-06-integration.md
to mark Section 7.3 tasks as complete.
Added IEx detection capabilities to TermUI for environment awareness
and testing flexibility.

- TermUI.iex_mode?/0 - Detects if running in IEx
- TermUI.running_mode/0 - Returns :iex or :standalone
- Config option :iex_compatible to force mode
- Environment variable TERM_UI_IEX_MODE for override
- Runtime logs execution mode at startup

Override hierarchy: env var > config > auto-detection
Added comprehensive planning document and implementation summary
for Phase 7.4 IEx Detection and Configuration. Updated
phase-06-integration.md to mark Section 7.4 tasks as complete.
- Added IEx compatibility section to README
- Added comprehensive IEx documentation to App module
- Created IEx counter example demonstrating IEx usage
- Added troubleshooting guide for common IEx issues
Added comprehensive planning document and implementation summary
for Phase 7.5 Examples and Documentation. Updated
phase-06-integration.md to mark Section 7.5 tasks as complete.
Implements comprehensive integration tests covering IEx lifecycle
and cross-mode consistency for TermUI applications.

- Add IEx lifecycle integration tests (detection, TTY handler, cleanup)
- Add cross-mode consistency tests (handler modes, stop callback)
- Add planning and summary documentation
Implements TTY backend rendering support for running TermUI applications
from IEx, and updates all documentation with both Raw and TTY mode
instructions.

Runtime changes:
- Fix do_render/2 to allow TTY backend rendering (terminal_started check)
- Add render_to_tty_backend/2 for TTY-specific rendering path
- Add extract_all_cells/1 to get non-empty cells for TTY output
- Add NodeRenderer.render_to_buffer_direct/3 for direct buffer rendering
- Default use_input_handler to true for new Input behaviour
- Fix resize callback registration to check if Terminal GenServer is running

Add mix termui.run task:
- Autodetects module with run/0 from app name
- Supports --module and --function overrides
- Supports --iex flag for IEx-compatible mode

Documentation updates:
- All 26 example READMEs updated with Raw Mode and TTY Mode sections
- User guide 02-getting-started.md adds "Understanding Backends" section
- User guide 01-overview.md updated to reference backend documentation
- Add guides to package files for Hex distribution
- Add TTY backend rendering support for IEx compatibility
- Add mix termui.run task for easy application execution
- Update all example documentation with Raw and TTY mode instructions
- Add backend selection documentation to user guides
@pcharbon70 pcharbon70 merged commit 66aadf5 into main Jan 26, 2026
1 check failed
pcharbon70 added a commit that referenced this pull request Feb 15, 2026
Merge pull request #21 from pcharbon70/develop
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants