Side-effect descriptors returned from init/1 and update/2.
An app's update/2 can return {new_model, cmd} to request side-effects.
The runtime dispatches the cmd to a Task.Supervisor and re-enters the
TEA loop; results arrive later as ordinary {:harlock_event, result}
messages that update/2 handles like any other event.
def update(:fetch, model) do
cmd = Cmd.from(fn -> HTTPoison.get!("https://example.com") end)
{model, cmd}
end
def update({:ok, %{body: body}}, model), do: {%{model | body: body}, Cmd.none()}Constructors:
none/0— no side-effect. Equivalent to returning just the model.from/1— run a 0-arity function in a task; deliver its return value.exec/3— hand the terminal to another program until it exits.suspend/0— stop the app for the shell's job control, as Ctrl-Z does.batch/1— dispatch a list of cmds concurrently; no ordering guarantee.map/2— tag/transform the result of an inner cmd before delivery.
Running another program
exec/3 suspends the app's use of the terminal, runs a program with it, and
resumes when the program exits — how an editor, pager, or git commit is run
from a terminal UI:
def update({:harlock_submit, :edit}, m) do
{m, Cmd.exec("vim", [m.path]) |> Cmd.map(&{:edited, &1})}
end
def update({:edited, {:ok, 0}}, m), do: reload(m)
def update({:edited, _failed}, m), do: %{m | status: "editor failed"}While the program runs it owns the terminal completely: the app's alternate
screen is left, the terminal settings the shell had before the app started
are restored, and keystrokes, Ctrl-C and window resizes go to the program.
The app keeps running — subscriptions and other cmd results still reach
update/2 — but nothing is drawn. On exit the app redraws at the current
size and receives the result:
{:ok, exit_status}— the program exited, with any status.{:error, {:signal, n}}— it was killed by signaln(2 for Ctrl-C).{:error, {:exec, reason}}— it could not be started, e.g.:enoentwhenprogramis not onPATH.{:error, {:chdir, reason}}— the:cddirectory could not be entered.{:error, :killed}— the program was killed with the app, which ended while it ran.{:error, :busy}— another program is already running.{:error, :not_foreground}— the app does not hold the terminal's foreground, so it cannot hand it over.- other
{:error, {stage, reason}}pairs for failures setting the program up (opening the terminal, a pipe, starting it), which are rare. {:error, :no_terminal}— the app is not attached to a terminal, as under the test backend without an:execstub (seeHarlock.Test.start_app/3).
exec/3 needs the app to own its terminal: run it with mix run, not from an
IEx prompt, whose own terminal driver competes for input.
Suspending
In raw mode Ctrl-Z is an ordinary key, {:key, {:char, ?z}, [:ctrl]}, not a
suspend: the terminal driver no longer turns it into a signal. An app that
wants the usual job-control behaviour — Ctrl-Z to the shell, fg to come
back — binds it to suspend/0:
def update({:key, {:char, ?z}, [:ctrl]}, m), do: {m, Cmd.suspend()}The terminal is handed back to the shell as for exec/3, the app stops, and
after fg it redraws at the current size and receives {:ok, :resumed}.
It is not bound by default because apps use Ctrl-Z for other things, undo
among them.
Suspending needs a shell with job control to resume the app. Without one —
the app started by a script, by exec, or under script(1) — it returns
{:error, :no_job_control} and leaves the terminal as it was, rather than
stopping with nothing to resume it. Other results: {:error, :busy} while an
exec/3 program runs, {:error, :no_terminal} under the test backend
without a :suspend stub, and {:error, :not_stopped} if the stop was
requested but did not happen, in which case the app simply carries on.
Task lifecycle: from/1 tasks run under a task supervisor in the app's
supervision tree, started before the runtime. They end with the app: quitting
stops the tree, and so does a crash. A crash inside a task is caught at the
task body and delivered as {:cmd_error, reason}; it never propagates to the
runtime. exec/3 and suspend/0 are not tasks — the runtime carries them
out itself.
Summary
Functions
Run program with the terminal until it exits. See "Running another program"
above.
Stop the app for the shell's job control, as Ctrl-Z would. See "Suspending" above.
Types
Functions
Run program with the terminal until it exits. See "Running another program"
above.
program is looked up on PATH unless it contains a /. args are passed
as-is, with no shell in between — use exec("sh", ["-c", script]) when a
shell is wanted.
Options:
:cd— directory to run the program in.:env— environment variables to set for the program, as{name, value}pairs or a map, on top of the current environment. Anilvalue unsets the variable.
@spec none() :: t()
@spec suspend() :: t()
Stop the app for the shell's job control, as Ctrl-Z would. See "Suspending" above.