dillonkearns/elm-pages/12.2.1

NOTE: the latest version is 12.3.0

Tui

What It Is

Build a TUI (Text-Based User Interface) as an elm-pages script. An elm-pages CLI, defined by Pages.Script, lets you parse CLI options and then execute a single BackendTask (no TEA init/update). For our purposes, we'll use the term TUI to mean interactive (like vim), and CLI to mean a more static command (like grep or ls).

What You Can Do

A Tui.Program lets you build an interactive Elm app that renders its view as text in the terminal and lets you init and update your Model in response to events:

You can also fire off a BackendTask and get back a Msg:

Example

Similar to in elm-pages Route Modules, the data function resolves a BackendTask prior to init

-- script/src/Counter.elm




module Counter exposing (run)


import Tui
import Tui.Screen as Screen
import Tui.Sub


run : Script
run =
    Tui.program
        { data = BackendTask.succeed ()
        , init = \() -> ( { count = 0 }, Effect.none )
        , update = update
        , view =
            \_ model ->
                Screen.text ("Count: " ++ String.fromInt model.count)
        , subscriptions = \_ -> Tui.Sub.onKeyPress KeyPressed
        }
        |> Tui.toScript

You run a TUI like you run any other Pages.Script.Script, see the elm-pages scripts docs page

cd script && npx elm-pages run src/Counter.elm

The data field resolves before init runs, so you can cleanly initialize your Model with data from reading files, fetching HTTP data, or running shell commands. Resolving to a FatalError terminates the program with a non-zero exit code and prints an error message.

For a full interactive example that uses data to seed the initial repo, Tui.Input to edit it, and BackendTask.Http.getJson to refetch stars, see examples/end-to-end/script/src/TuiStars.elm. That behavior is covered by examples/end-to-end/script/tests/TuiStarsTests.elm.

Building a Tui.Program

program builds an opaque Program value; toScript finalizes it into a runnable Pages.Script.Script. Keeping the intermediate Program as its own type lets future releases add pipeable modifiers (themes, key bindings, etc.) without changing the shape of any existing user code.

type Program data model msg

An opaque TUI program value. Construct one with program and finalize it with toScript. Future releases will add pipeable modifiers that work on this type without changing its public shape.

type alias ProgramConfig data model msg =
{ data : BackendTask FatalError data
, init : data -> ( model, Effect msg )
, update : msg -> model -> ( model, Effect msg )
, view : Context -> model -> Screen
, subscriptions : model -> Sub msg
}

The record shape that program accepts, and also the shape that Test.Tui.start accepts so a single config value can be shared between a production script and its tests.

The field list is intentionally locked for the v12 series: future TUI capabilities will be added as pipeable modifiers on Program rather than as new required fields.

program : ProgramConfig data model msg -> Program data model msg

Build an interactive TUI from a config record.

run : Script
run =
    Tui.program
        { data = BackendTask.succeed ()
        , init = \() -> ( { count = 0 }, Effect.none )
        , update = update
        , view = view
        , subscriptions = subscriptions
        }
        |> Tui.toScript

See also programWithCliOptions and programOrScript.

toScript : Program data model msg -> Script

Turn a Program into a runnable Pages.Script.Script. This is the terminal step of a TUI pipeline. Placing it at the end leaves room for future pipeable modifiers (e.g. |> Tui.withTheme ...) without forcing every user to restructure.

run : Script
run =
    Tui.program { ... } |> Tui.toScript
programWithCliOptions : Config cliOptions -> (cliOptions -> Program data model msg) -> Script

Run a TUI as a Script, with CLI option parsing.

run : Script
run =
    Tui.programWithCliOptions
        (Program.config
            |> Program.add
                (OptionsParser.build identity
                    |> OptionsParser.with
                        (Option.optionalKeywordArg "dir"
                            |> Option.withDefault "."
                        )
                )
        )
        (\dir ->
            Tui.program
                { data = loadFiles dir
                , init = init
                , update = update
                , view = view
                , subscriptions = subscriptions
                }
        )

TUI or CLI

For programs that make sense both interactively and non-interactively (an agent piping output, a CI run), use programOrScript to provide a script branch alongside the tui. At runtime, mode decides which path to take. isInteractive is the standard heuristic (isatty + CI + NO_COLOR) for common use.

type Mode
= Tui
| Cli

Which path should a programOrScript take: the interactive TUI, or the plain BackendTask fallback?

type Mode
    = Tui
    | Cli

Returned from mode : BackendTask FatalError Mode in programOrScript. Use isInteractive for the standard heuristic, or build your own BackendTask to decide.

Config cliOptions
-> ( cliOptions
-> { tui : Program data model msg
, script : BackendTask FatalError ()
, mode : BackendTask FatalError Mode
}
)
-> Script

Run a TUI when the terminal is interactive, fall back to a non-interactive script otherwise. The mode BackendTask decides which path to take. Pass isInteractive for the standard isatty + CI + NO_COLOR heuristic, or your own BackendTask FatalError Mode for custom detection.

run : Script
run =
    Tui.programOrScript
        (Program.config |> Program.add ...)
        (\flags ->
            { tui =
                { data = loadCommits
                , init = init
                , update = update
                , view = view
                , subscriptions = subscriptions
                }
            , script =
                loadCommits
                    |> BackendTask.andThen
                        (\commits -> Script.log (summarize commits))
            , mode = Tui.isInteractive
            }
        )

The tui's data only runs on the TUI path; the script path runs independently so it can do its own loading or be a trivial no-op.

isInteractive : BackendTask FatalError Mode

The standard interactive-terminal heuristic: returns Tui if stdout and stdin are both TTYs and CI, NO_COLOR, and TERM=dumb are all unset. Otherwise returns Cli.

Use this as the mode field of programOrScript. If you need different rules (for example, always TUI regardless of pipes), supply your own BackendTask FatalError Mode instead.

-- Default:
mode =
    Tui.isInteractive


-- Force TUI:
mode =
    BackendTask.succeed Tui.Tui


-- Opt out of TUI when a flag is set, otherwise use the default:
mode =
    if cliOptions.plain then
        BackendTask.succeed Tui.Cli


    else
        Tui.isInteractive

Terminal Context

Passed to your view function. Use colorProfile to adapt themes for different terminal capabilities.

type alias Context =
{ width : Int
, height : Int
, colorProfile : ColorProfile
}

Read-only terminal context provided to view.

= TrueColor
| Color256
| Color16
| Mono

Terminal color capability, detected at init from environment variables. Follows charmbracelet/colorprofile's detection precedence: $NO_COLOR -> $COLORTERM -> known terminals -> $TERM suffix -> default.

The renderer automatically degrades colors based on the profile, so the Elm app can always use the highest fidelity colors and they'll be converted. This field lets apps adapt themes (e.g., use different palettes for 16-color).

view ctx model =
    case ctx.colorProfile of
        Tui.TrueColor ->
            richColorView model


        _ ->
            basicColorView model