README.md

# Drafter

An Elixir Terminal User Interface framework inspired by Python's Textual. Build rich, interactive terminal applications with a declarative API similar to Phoenix LiveView.

## Features

- **Declarative API** - Phoenix LiveView-inspired component model
- **Rich Widget Library** - 30+ widgets including DataTable, Tree, Charts, Inputs
- **Event-Driven Architecture** - Keyboard, mouse, and custom events
- **Flexible Layout System** - Vertical, horizontal, grid, and scrollable layouts
- **Multi-Screen Navigation** - Push/pop screens, modals, toasts, panels
- **Theming System** - Built-in themes with customization support
- **Animation Support** - Smooth property animations with easing functions
- **Zero Runtime Dependencies** - Pure Elixir implementation

## Requirements

- Elixir ~> 1.18
- Erlang/OTP 28 or later

Drafter relies on OTP 28's raw terminal mode (`-noshell` raw input), improved ANSI escape sequence handling, and lazy input reading. Earlier OTP versions will not handle keyboard input or screen updates correctly.

## Installation

Add `drafter` to your `mix.exs`:

```elixir
def deps do
  [
    {:drafter, path: "../drafter"}
  ]
end
```

## Quick Start

```elixir
defmodule MyApp do
  use Drafter.App

  def mount(_props) do
    %{counter: 0}
  end

  def render(state) do
    vertical([
      header("My App"),
      label("Counter: #{state.counter}"),
      horizontal([
        button("Decrement", on_click: :decrement),
        button("Increment", on_click: :increment)
      ], gap: 2),
      footer(bindings: [{"q", "Quit"}])
    ])
  end

  def handle_event(:increment, _data, state) do
    {:ok, %{state | counter: state.counter + 1}}
  end

  def handle_event(:decrement, _data, state) do
    {:ok, %{state | counter: state.counter - 1}}
  end

  def handle_event({:key, :q}, _state), do: {:stop, :normal}
  def handle_event({:key, :c, [:ctrl]}, _state), do: {:stop, :normal}
  def handle_event(_event, state), do: {:noreply, state}
end
```

Run your app:

```bash
mix run -e "Drafter.run(MyApp)"
```

## Core Concepts

### Application Structure

Every TUI application implements the `Drafter.App` behaviour:

```elixir
defmodule MyApp do
  use Drafter.App

  @callback mount(props :: map()) :: state :: map()
  @callback render(state :: map()) :: component_tree :: tuple()
  @callback handle_event(event :: term(), state :: map()) :: result :: term()
  @callback on_ready(state :: map()) :: state :: map()
  @callback on_timer(timer_id :: atom(), state :: map()) :: state :: map()
end
```

### Widget Types

#### Display Widgets
- `label(text, opts)` - Text display
- `markdown(content, opts)` - Markdown rendering
- `digits(value, opts)` - Large ASCII art numbers
- `sparkline(data, opts)` - Mini inline charts
- `chart(data, opts)` - Full charts (line, bar, pie)
- `progress_bar(opts)` - Progress indication
- `loading_indicator(opts)` - Animated spinner
- `rule(opts)` - Horizontal/vertical dividers

#### Input Widgets
- `button(text, opts)` - Clickable button
- `text_input(opts)` - Single-line text input
- `text_area(opts)` - Multi-line text editor
- `checkbox(label, opts)` - Boolean toggle
- `switch(opts)` - On/off switch
- `radio_set(options, opts)` - Mutually exclusive options
- `selection_list(options, opts)` - Multi-select list
- `option_list(items, opts)` - Single-select list
- `masked_input(opts)` - Formatted input (phone, date, etc.)

#### Data Widgets
- `data_table(opts)` - Full-featured table with sorting, selection
- `tree(opts)` - Hierarchical data display
- `directory_tree(opts)` - File system browser

#### Layout Widgets
- `vertical(children, opts)` - Vertical stack
- `horizontal(children, opts)` - Horizontal row
- `container(children, opts)` - Generic container
- `scrollable(children, opts)` - Scrollable area
- `grid(children, opts)` - CSS Grid-like layout
- `sidebar(left, right, opts)` - Two-column layout

#### Container Widgets
- `card(children, opts)` - Bordered card
- `box(children, opts)` - Simple box
- `collapsible(title, content, opts)` - Expandable section
- `tabbed_content(tabs, opts)` - Tab navigation
- `header(title, opts)` - App header
- `footer(opts)` - App footer with keybindings

### Event Handling

Events are handled in the `handle_event/2` callback:

```elixir
def handle_event(:button_clicked, _data, state) do
  {:ok, %{state | clicked: true}}
end

def handle_event({:key, :enter}, state) do
  {:ok, state}
end

def handle_event({:key, :q}, _state) do
  {:stop, :normal}
end

def handle_event({:key, :c, [:ctrl]}, _state) do
  {:stop, :normal}
end
```

#### Event Return Values

- `{:ok, new_state}` - Update state and re-render
- `{:noreply, state}` - No re-render needed
- `{:stop, reason}` - Exit the application
- `{:show_modal, module, props, opts}` - Display a modal
- `{:show_toast, message, opts}` - Show a toast notification
- `{:push, module, props, opts}` - Push a new screen
- `{:pop, result}` - Pop current screen

### Screens and Navigation

Create multi-screen applications with modals and toasts:

```elixir
defmodule MainScreen do
  use Drafter.Screen

  def mount(_props), do: %{items: []}

  def render(state) do
    vertical([
      label("Main Screen"),
      button("Open Modal", on_click: :open_modal),
      button("Show Toast", on_click: :show_toast)
    ])
  end

  def handle_event(:open_modal, _state) do
    {:show_modal, MyModal, %{title: "Info"}, [width: 50, height: 15]}
  end

  def handle_event(:show_toast, _state) do
    {:show_toast, "Hello!", [variant: :success]}
  end
end

defmodule MyModal do
  use Drafter.Screen

  def mount(props), do: %{title: props.title}

  def render(state) do
    vertical([
      label(state.title),
      button("Close", on_click: :close)
    ])
  end

  def handle_event(:close, _state), do: {:pop, :closed}
  def handle_event({:key, :escape}, _state), do: {:pop, :dismissed}
end
```

### Screen Types

- **Default** - Full-screen content
- **Modal** - Centered dialog with overlay
- **Popover** - Anchored popup
- **Panel** - Side panel
- **Toast** - Auto-dismissing notification

### Toast Variants

```elixir
{:show_toast, "Info message", [variant: :info]}
{:show_toast, "Success!", [variant: :success]}
{:show_toast, "Warning!", [variant: :warning]}
{:show_toast, "Error!", [variant: :error]}
```

Toast positions: `:top_left`, `:top_center`, `:top_right`, `:middle_left`, `:middle_center`, `:middle_right`, `:bottom_left`, `:bottom_center`, `:bottom_right`

### Widget State Binding

Bind widget values directly to app state:

```elixir
def mount(_props) do
  %{username: "", remember: false}
end

def render(state) do
  vertical([
    text_input(placeholder: "Username", bind: :username),
    checkbox("Remember me", bind: :remember),
    button("Submit", on_click: :submit)
  ])
end

def handle_event(:submit, _data, state) do
  IO.puts("Username: #{state.username}")
  {:ok, state}
end
```

### Accessing Widget State

```elixir
Drafter.get_widget_value(:my_input)
Drafter.get_widget_state(:my_checkbox)
Drafter.query_one("#submit")
Drafter.query_all("Button")
```

### Timers

```elixir
def on_ready(state) do
  Drafter.set_interval(1000, :tick)
  state
end

def on_timer(:tick, state) do
  %{state | seconds: state.seconds + 1}
end
```

### Animations

```elixir
Drafter.animate(:my_widget, :opacity, 0.5, duration: 500, easing: :ease_out)
Drafter.animate(:my_label, :background, {255, 0, 0}, duration: 1000)
```

Available easing functions: `:linear`, `:ease`, `:ease_in`, `:ease_out`, `:ease_in_out`, `:ease_in_quad`, `:ease_out_quad`, `:ease_in_cubic`, `:ease_out_cubic`, `:ease_in_elastic`, `:ease_out_elastic`, `:ease_in_bounce`, `:ease_out_bounce`

## Complete Example

```elixir
defmodule TodoApp do
  use Drafter.App

  def mount(_props) do
    %{
      todos: ["Learn Drafter", "Build awesome CLI apps"],
      new_todo: ""
    }
  end

  def render(state) do
    todo_items = Enum.map(state.todos, fn todo ->
      label("  • #{todo}")
    end)

    vertical([
      header("Todo App"),
      scrollable(todo_items, flex: 1),
      horizontal([
        text_input(placeholder: "Add todo...", bind: :new_todo, flex: 1),
        button("Add", on_click: :add_todo)
      ], gap: 1),
      footer(bindings: [{"q", "Quit"}, {"Enter", "Add"}])
    ])
  end

  def handle_event(:add_todo, _data, state) do
    if String.trim(state.new_todo) != "" do
      {:ok, %{state | todos: state.todos ++ [state.new_todo], new_todo: ""}}
    else
      {:noreply, state}
    end
  end

  def handle_event({:key, :q}, _state), do: {:stop, :normal}
  def handle_event(_event, state), do: {:noreply, state}
end
```

## Syntax Highlighting

Drafter supports syntax highlighting via the [`tree-sitter`](https://tree-sitter.github.io/tree-sitter/) CLI. This is entirely optional — if you don't need it, no setup is required.

### If you already have tree-sitter installed

Nothing to do. Pass `syntax_highlighting: true` when starting your app:

```elixir
Drafter.run(MyApp, syntax_highlighting: true)
```

Then use `code_view` with a file path:

```elixir
code_view(path: "/path/to/file.rs", show_line_numbers: true, flex: 1)
```

Language is detected automatically from the file extension. Highlighting quality depends on which grammars you have installed in your tree-sitter environment.

### If you don't have tree-sitter

Skip `syntax_highlighting: true` (or don't pass it). The `code_view` widget will still work — Elixir files get built-in highlighting, all other files render as plain text.

### Installing tree-sitter

```bash
# macOS
brew install tree-sitter

# Or via npm
npm install -g tree-sitter-cli
```

After installing, set up grammars for the languages you want to highlight by following the [tree-sitter getting started guide](https://tree-sitter.github.io/tree-sitter/). The more grammars you have installed, the more languages `code_view` will highlight.

### Supported in code_view

```elixir
code_view(
  path: state.selected_file,   # preferred — tree-sitter reads the file directly
  show_line_numbers: true,
  flex: 1
)

code_view(
  source: some_string,         # also works — uses a temp file under the hood
  language: :python,
  flex: 1
)
```

When `path:` is given, tree-sitter reads the file directly (one system call, no temp file). When only `source:` is given, a temp file is created, highlighted, then deleted.

## Running Examples

Standalone scripts in the `examples/` directory can be run directly with `elixir`:

```bash
elixir examples/hello_world.exs
elixir examples/counter.exs
elixir examples/animation.exs
elixir examples/clock.exs
elixir examples/calculator.exs
elixir examples/charts.exs
elixir examples/widgets.exs
elixir examples/theme_sandbox.exs
elixir examples/themes.exs
elixir examples/hsl_colors.exs
elixir examples/data_table.exs
elixir examples/screens.exs
elixir examples/key_inspector.exs
elixir examples/code_browser.exs
elixir examples/syntax_highlight.exs
elixir examples/custom_loop.exs
```

Examples that are compiled into the library can be run via `mix run`:

```bash
mix run -e "Drafter.run(Drafter.Examples.ScreenDemo)"
mix run -e "Drafter.run(Drafter.Examples.DeclarativeSandbox)"
mix run -e "Drafter.run(Drafter.Examples.ThemeSandbox)"
mix run -e "Drafter.run(Drafter.Examples.ChartDemo)"
```

## Keyboard Shortcuts

- `Ctrl+C` or `Ctrl+Q` - Quit application
- `Tab` - Next focusable widget
- `Shift+Tab` - Previous focusable widget
- Arrow keys - Navigate within widgets
- `Enter` - Activate/confirm
- `Escape` - Dismiss modals

## License

MIT