-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, <>}]; false -> [{write, <>}] 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.