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).
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:
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.toScriptYou 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.elmThe 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.
Tui.Programprogram 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.
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.
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.toScriptSee also programWithCliOptions and
programOrScript.
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.toScriptRun 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
}
)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.
Which path should a programOrScript take: the
interactive TUI, or the plain BackendTask fallback?
type Mode
= Tui
| CliReturned from mode : BackendTask FatalError Mode in programOrScript.
Use isInteractive for the standard heuristic, or build
your own BackendTask to decide.
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.
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.isInteractivePassed to your view function. Use colorProfile to adapt themes for
different terminal capabilities.
Read-only terminal context provided to view.
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
An opaque TUI program value. Construct one with
programand finalize it withtoScript. Future releases will add pipeable modifiers that work on this type without changing its public shape.