-module(etui@app).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/etui/app.gleam").
-export([run/6, run_buffered/6, run_animated/6, run_buffered_cursor/6]).
-export_type([app_result/1, loop_step/2]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type app_result(EIJ) :: {success, EIJ} | {error, binary()}.
-type loop_step(EIK, EIL) :: {step_quit, EIK, EIL} |
{step_continue, etui@backend:input_event(), EIK, EIL}.
-file("src/etui/app.gleam", 100).
-spec step(
etui@backend:backend(EIS),
EIS,
EIU,
list(etui@backend:render_op()),
fun((etui@backend:input_event(), EIU) -> EIU),
fun((EIU) -> boolean()),
integer()
) -> loop_step(EIU, EIS).
step(B, Bs, State, Ops, On_event, Should_quit, Poll_timeout_ms) ->
case (erlang:element(3, B))(Bs, Ops) of
{ok, Bs2} ->
case (erlang:element(4, B))(Bs2, Poll_timeout_ms) of
{ok, {Event, Bs3}} ->
Next = On_event(Event, State),
case Should_quit(Next) of
true ->
{step_quit, Next, Bs3};
false ->
{step_continue, Event, Next, Bs3}
end;
_ ->
{step_quit, State, Bs2}
end;
_ ->
{step_quit, State, Bs}
end.
-file("src/etui/app.gleam", 126).
-spec loop(
etui@backend:backend(EIY),
EIY,
EJA,
fun((EJA) -> list(etui@backend:render_op())),
fun((etui@backend:input_event(), EJA) -> EJA),
fun((EJA) -> boolean()),
integer()
) -> {EJA, EIY}.
loop(B, Bs, State, Render, On_event, Should_quit, Poll_timeout_ms) ->
case step(
B,
Bs,
State,
Render(State),
On_event,
Should_quit,
Poll_timeout_ms
) of
{step_quit, S, Final_bs} ->
{S, Final_bs};
{step_continue, _, Next, Bs3} ->
loop(B, Bs3, Next, Render, On_event, Should_quit, Poll_timeout_ms)
end.
-file("src/etui/app.gleam", 56).
?DOC(
" Run the app loop.\n"
"\n"
" Lifecycle:\n"
" 1. `b.init()`, enter raw mode, alt screen.\n"
" 2. Loop: `render(state)` → emit ops → `b.poll()` → `on_event()`.\n"
" 3. Exit when `should_quit(state)` returns `True`.\n"
" 4. `b.cleanup()`, always runs, even on panic.\n"
"\n"
" ```gleam\n"
" app.run(\n"
" default.new(),\n"
" Model(count: 0),\n"
" fn(m) { [Write(int.to_string(m.count))] },\n"
" fn(ev, m) { case ev { KeyPress(\"q\") -> m KeyPress(_) -> Model(count: m.count + 1) _ -> m } },\n"
" fn(m) { m.count >= 10 },\n"
" 16,\n"
" )\n"
" ```\n"
).
-spec run(
etui@backend:backend(any()),
EIP,
fun((EIP) -> list(etui@backend:render_op())),
fun((etui@backend:input_event(), EIP) -> EIP),
fun((EIP) -> boolean()),
integer()
) -> app_result(EIP).
run(B, Init_state, Render, On_event, Should_quit, Poll_timeout_ms) ->
case (erlang:element(2, B))() of
{ok, Bs} ->
etui_run_ffi:with_cleanup(
fun() ->
{Final_state, Final_bs} = loop(
B,
Bs,
Init_state,
Render,
On_event,
Should_quit,
Poll_timeout_ms
),
(erlang:element(6, B))(Final_bs),
{success, Final_state}
end,
fun() -> (erlang:element(6, B))(Bs) end
);
_ ->
{error, <<"Terminal init failed"/utf8>>}
end.
-file("src/etui/app.gleam", 217).
-spec loop_buffered(
etui@backend:backend(EJG),
EJG,
EJI,
fun((EJI, etui@geometry:rect()) -> etui@buffer:buffer()),
fun((etui@backend:input_event(), EJI) -> EJI),
fun((EJI) -> boolean()),
integer(),
etui@buffer:buffer(),
boolean()
) -> {EJI, EJG}.
loop_buffered(
B,
Bs,
State,
Render,
On_event,
Should_quit,
Poll_timeout_ms,
Prev_buf,
First_frame
) ->
Screen = etui@buffer:area(Prev_buf),
Curr_buf = Render(State, Screen),
Ansi = case First_frame of
true ->
etui@buffer:to_ansi(Curr_buf);
false ->
etui@buffer:diff_to_ansi(Prev_buf, Curr_buf)
end,
Ops = case Ansi of
<<""/utf8>> ->
[];
_ ->
case First_frame of
true ->
[clear_screen, {move_cursor, 0, 0}, {write, Ansi}];
false ->
[{write, Ansi}]
end
end,
case step(B, Bs, State, Ops, On_event, Should_quit, Poll_timeout_ms) of
{step_quit, S, Final_bs} ->
{S, Final_bs};
{step_continue, Event, Next, Bs3} ->
{New_prev, Is_first} = case Event of
{resize, W, H} ->
New_screen = etui@geometry:rect_new(0, 0, W, H),
{etui@buffer:buffer_new(New_screen), true};
_ ->
{Curr_buf, false}
end,
loop_buffered(
B,
Bs3,
Next,
Render,
On_event,
Should_quit,
Poll_timeout_ms,
New_prev,
Is_first
)
end.
-file("src/etui/app.gleam", 167).
?DOC(
" High-level app loop. The render function produces a `Buffer`; the loop\n"
" diffs it against the previous frame and emits only the changed cells.\n"
"\n"
" First frame: full `to_ansi` (clean slate). Subsequent frames: `diff_to_ansi`.\n"
" On `Resize`: full re-render at new size.\n"
"\n"
" ```gleam\n"
" app.run_buffered(\n"
" default.new(),\n"
" Model(count: 0),\n"
" fn(m, screen) {\n"
" buffer.buffer_new(screen)\n"
" |> paragraph.render(screen, paragraph.paragraph_new(int.to_string(m.count)))\n"
" },\n"
" fn(ev, m) { case ev { KeyPress(\"q\") -> m _ -> m } },\n"
" fn(m) { m.quit },\n"
" 16,\n"
" )\n"
" ```\n"
).
-spec run_buffered(
etui@backend:backend(any()),
EJE,
fun((EJE, etui@geometry:rect()) -> etui@buffer:buffer()),
fun((etui@backend:input_event(), EJE) -> EJE),
fun((EJE) -> boolean()),
integer()
) -> app_result(EJE).
run_buffered(B, Init_state, Render, On_event, Should_quit, Poll_timeout_ms) ->
case (erlang:element(2, B))() of
{ok, Bs} ->
etui_run_ffi:with_cleanup(
fun() ->
_ = (erlang:element(3, B))(
Bs,
[{write, etui@cursor:hide()}]
),
{Size, Bs2} = case (erlang:element(5, B))(Bs) of
{ok, {Sz, Bs1}} ->
{Sz, Bs1};
_ ->
{{terminal_size, 80, 24}, Bs}
end,
Screen = etui@geometry:rect_new(
0,
0,
erlang:element(2, Size),
erlang:element(3, Size)
),
Blank = etui@buffer:buffer_new(Screen),
Init_state@1 = On_event(
{resize,
erlang:element(2, Size),
erlang:element(3, Size)},
Init_state
),
{Final_state, Final_bs} = loop_buffered(
B,
Bs2,
Init_state@1,
Render,
On_event,
Should_quit,
Poll_timeout_ms,
Blank,
true
),
_ = (erlang:element(3, B))(
Final_bs,
[{write, etui@cursor:show()}]
),
(erlang:element(6, B))(Final_bs),
{success, Final_state}
end,
fun() ->
_ = (erlang:element(3, B))(
Bs,
[{write, etui@cursor:show()}]
),
(erlang:element(6, B))(Bs)
end
);
_ ->
{error, <<"Terminal init failed"/utf8>>}
end.
-file("src/etui/app.gleam", 341).
-spec loop_animated(
etui@backend:backend(EJN),
EJN,
EJP,
fun((EJP, etui@geometry:rect(), etui@anim:anim_state()) -> etui@buffer:buffer()),
fun((etui@backend:input_event(), EJP) -> EJP),
fun((EJP) -> boolean()),
integer(),
etui@buffer:buffer(),
boolean(),
etui@anim:anim_state()
) -> {EJP, EJN}.
loop_animated(
B,
Bs,
State,
Render,
On_event,
Should_quit,
Poll_timeout_ms,
Prev_buf,
First_frame,
Anim_state
) ->
Screen = etui@buffer:area(Prev_buf),
Curr_buf = Render(State, Screen, Anim_state),
Ansi = case First_frame of
true ->
etui@buffer:to_ansi(Curr_buf);
false ->
etui@buffer:diff_to_ansi(Prev_buf, Curr_buf)
end,
Ops = case Ansi of
<<""/utf8>> ->
[];
_ ->
case First_frame of
true ->
[clear_screen, {move_cursor, 0, 0}, {write, Ansi}];
false ->
[{write, Ansi}]
end
end,
Next_anim = etui@anim:tick(Anim_state),
case step(B, Bs, State, Ops, On_event, Should_quit, Poll_timeout_ms) of
{step_quit, S, Final_bs} ->
{S, Final_bs};
{step_continue, Event, Next, Bs3} ->
{New_prev, Is_first} = case Event of
{resize, W, H} ->
New_screen = etui@geometry:rect_new(0, 0, W, H),
{etui@buffer:buffer_new(New_screen), true};
_ ->
{Curr_buf, false}
end,
loop_animated(
B,
Bs3,
Next,
Render,
On_event,
Should_quit,
Poll_timeout_ms,
New_prev,
Is_first,
Next_anim
)
end.
-file("src/etui/app.gleam", 293).
?DOC(
" Like `run_buffered` but passes an `anim.AnimState` to the render function,\n"
" auto-ticked every frame. Use when your UI has spinners, blinking widgets,\n"
" marquees, or any frame-dependent animation, no manual tick needed.\n"
"\n"
" ```gleam\n"
" app.run_animated(\n"
" default.new(),\n"
" Model(quit: False),\n"
" fn(m, screen, anim_state) {\n"
" let frame = anim_state.frame\n"
" buffer.buffer_new(screen)\n"
" |> spinner.render(area, spinner.spinner_new() |> spinner.with_frame(frame))\n"
" },\n"
" fn(ev, m) { case ev { backend.KeyPress(\"q\") -> Model(quit: True) _ -> m } },\n"
" fn(m) { m.quit },\n"
" 16,\n"
" )\n"
" ```\n"
).
-spec run_animated(
etui@backend:backend(any()),
EJL,
fun((EJL, etui@geometry:rect(), etui@anim:anim_state()) -> etui@buffer:buffer()),
fun((etui@backend:input_event(), EJL) -> EJL),
fun((EJL) -> boolean()),
integer()
) -> app_result(EJL).
run_animated(B, Init_state, Render, On_event, Should_quit, Poll_timeout_ms) ->
case (erlang:element(2, B))() of
{ok, Bs} ->
etui_run_ffi:with_cleanup(
fun() ->
_ = (erlang:element(3, B))(
Bs,
[{write, etui@cursor:hide()}]
),
{Size, Bs2} = case (erlang:element(5, B))(Bs) of
{ok, {Sz, Bs1}} ->
{Sz, Bs1};
_ ->
{{terminal_size, 80, 24}, Bs}
end,
Screen = etui@geometry:rect_new(
0,
0,
erlang:element(2, Size),
erlang:element(3, Size)
),
Blank = etui@buffer:buffer_new(Screen),
Init_state@1 = On_event(
{resize,
erlang:element(2, Size),
erlang:element(3, Size)},
Init_state
),
{Final_state, Final_bs} = loop_animated(
B,
Bs2,
Init_state@1,
Render,
On_event,
Should_quit,
Poll_timeout_ms,
Blank,
true,
etui@anim:anim_new()
),
_ = (erlang:element(3, B))(
Final_bs,
[{write, etui@cursor:show()}]
),
(erlang:element(6, B))(Final_bs),
{success, Final_state}
end,
fun() ->
_ = (erlang:element(3, B))(
Bs,
[{write, etui@cursor:show()}]
),
(erlang:element(6, B))(Bs)
end
);
_ ->
{error, <<"Terminal init failed"/utf8>>}
end.
-file("src/etui/app.gleam", 475).
-spec loop_buffered_cursor(
etui@backend:backend(EJW),
EJW,
EJY,
fun((EJY, etui@geometry:rect()) -> {etui@buffer:buffer(),
{ok, etui@geometry:position()} | {error, nil}}),
fun((etui@backend:input_event(), EJY) -> EJY),
fun((EJY) -> boolean()),
integer(),
etui@buffer:buffer(),
boolean()
) -> {EJY, EJW}.
loop_buffered_cursor(
B,
Bs,
State,
Render,
On_event,
Should_quit,
Poll_timeout_ms,
Prev_buf,
First_frame
) ->
Screen = etui@buffer:area(Prev_buf),
{Curr_buf, Cursor_pos} = Render(State, Screen),
Ansi = case First_frame of
true ->
etui@buffer:to_ansi(Curr_buf);
false ->
etui@buffer:diff_to_ansi(Prev_buf, Curr_buf)
end,
Cursor_ansi = case Cursor_pos of
{ok, Pos} ->
<<<<(etui@cursor:hide())/binary,
(etui@cursor:move_to(
erlang:element(3, Pos) + 1,
erlang:element(2, Pos) + 1
))/binary>>/binary,
(etui@cursor:show())/binary>>;
_ ->
etui@cursor:hide()
end,
Ops = case Ansi of
<<""/utf8>> ->
[{write, Cursor_ansi}];
_ ->
case First_frame of
true ->
[clear_screen,
{move_cursor, 0, 0},
{write, <<Ansi/binary, Cursor_ansi/binary>>}];
false ->
[{write, <<Ansi/binary, Cursor_ansi/binary>>}]
end
end,
case step(B, Bs, State, Ops, On_event, Should_quit, Poll_timeout_ms) of
{step_quit, S, Final_bs} ->
{S, Final_bs};
{step_continue, Event, Next, Bs3} ->
{New_prev, Is_first} = case Event of
{resize, W, H} ->
New_screen = etui@geometry:rect_new(0, 0, W, H),
{etui@buffer:buffer_new(New_screen), true};
_ ->
{Curr_buf, false}
end,
loop_buffered_cursor(
B,
Bs3,
Next,
Render,
On_event,
Should_quit,
Poll_timeout_ms,
New_prev,
Is_first
)
end.
-file("src/etui/app.gleam", 425).
?DOC(
" Like `run_buffered` but the render function also returns an optional cursor\n"
" position as `Result(geometry.Position, Nil)`.\n"
"\n"
" - `Ok(pos)`, shows the cursor at `pos` (0-based). Use for text inputs and\n"
" text areas where the user needs to see the insertion point.\n"
" - `Error(Nil)`, hides the cursor. Use for read-only views.\n"
"\n"
" The cursor is hidden automatically on init and restored on exit.\n"
"\n"
" ```gleam\n"
" app.run_buffered_cursor(\n"
" default.new(),\n"
" Model(text: \"\", cursor: 0),\n"
" fn(m, screen) {\n"
" let buf = buffer.buffer_new(screen) |> input.render(area, w, input_state)\n"
" let cursor_pos = geometry.Position(x: area.x + input_state.cursor_x + 1, y: area.y)\n"
" #(buf, Ok(cursor_pos))\n"
" },\n"
" on_event,\n"
" fn(m) { m.quit },\n"
" 16,\n"
" )\n"
" ```\n"
).
-spec run_buffered_cursor(
etui@backend:backend(any()),
EJS,
fun((EJS, etui@geometry:rect()) -> {etui@buffer:buffer(),
{ok, etui@geometry:position()} | {error, nil}}),
fun((etui@backend:input_event(), EJS) -> EJS),
fun((EJS) -> boolean()),
integer()
) -> app_result(EJS).
run_buffered_cursor(
B,
Init_state,
Render,
On_event,
Should_quit,
Poll_timeout_ms
) ->
case (erlang:element(2, B))() of
{ok, Bs} ->
etui_run_ffi:with_cleanup(
fun() ->
{Size, Bs2} = case (erlang:element(5, B))(Bs) of
{ok, {Sz, Bs1}} ->
{Sz, Bs1};
_ ->
{{terminal_size, 80, 24}, Bs}
end,
Screen = etui@geometry:rect_new(
0,
0,
erlang:element(2, Size),
erlang:element(3, Size)
),
Blank = etui@buffer:buffer_new(Screen),
_ = (erlang:element(3, B))(
Bs2,
[{write, etui@cursor:hide()}]
),
Init_state@1 = On_event(
{resize,
erlang:element(2, Size),
erlang:element(3, Size)},
Init_state
),
{Final_state, Final_bs} = loop_buffered_cursor(
B,
Bs2,
Init_state@1,
Render,
On_event,
Should_quit,
Poll_timeout_ms,
Blank,
true
),
_ = (erlang:element(3, B))(
Final_bs,
[{write, etui@cursor:show()}]
),
(erlang:element(6, B))(Final_bs),
{success, Final_state}
end,
fun() ->
_ = (erlang:element(3, B))(
Bs,
[{write, etui@cursor:show()}]
),
(erlang:element(6, B))(Bs)
end
);
_ ->
{error, <<"Terminal init failed"/utf8>>}
end.