-module(dream_test@reporters@progress). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/dream_test/reporters/progress.gleam"). -export([new/0, render/2, handle_event/2]). -export_type([progress_reporter/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Live progress bar reporter.\n" "\n" " This module renders a single-line progress bar that updates in-place using\n" " carriage returns, and adapts to the current terminal width.\n" "\n" " Because it uses `\\r` (carriage return) to rewrite the current line, it is\n" " best suited for interactive terminals. In CI logs (or when other output is\n" " printed concurrently), the output may be less readable.\n" "\n" " ## Terminal width\n" "\n" " Width is detected via Erlang `io:columns/0` with fallbacks:\n" "\n" " - if `io:columns/0` fails, it reads the `COLUMNS` environment variable\n" " - if that is missing/invalid, it defaults to 80 columns\n" "\n" " It is designed to be driven by `dream_test/reporters/types.ReporterEvent`,\n" " but most users should not call it directly. Prefer attaching it via\n" " `runner.progress_reporter(progress.new())` and letting the runner drive events.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " pub fn main() {\n" " runner.new([tests()])\n" " |> runner.progress_reporter(progress.new())\n" " |> runner.exit_on_failure()\n" " |> runner.run()\n" " }\n" " ```\n" ). -opaque progress_reporter() :: progress_reporter. -file("src/dream_test/reporters/progress.gleam", 48). ?DOC(" Create a new progress reporter.\n"). -spec new() -> progress_reporter(). new() -> progress_reporter. -file("src/dream_test/reporters/progress.gleam", 202). -spec format_result_name(dream_test@types:test_result()) -> binary(). format_result_name(Result) -> case lists:reverse(erlang:element(3, Result)) of [] -> erlang:element(2, Result); [Leaf] -> Leaf; [Leaf@1, Parent | _] -> <<<>/binary, Leaf@1/binary>> end. -file("src/dream_test/reporters/progress.gleam", 211). -spec pad_or_truncate(binary(), integer()) -> binary(). pad_or_truncate(Text, Width) -> Graphemes = gleam@string:to_graphemes(Text), Len = erlang:length(Graphemes), case Len =:= Width of true -> Text; false -> case Len < Width of true -> <>, Width - Len))/binary>>; false -> gleam@string:join( gleam@list:take(Graphemes, Width), <<""/utf8>> ) end end. -file("src/dream_test/reporters/progress.gleam", 224). -spec clamp_min(integer(), integer()) -> integer(). clamp_min(N, Min) -> case N < Min of true -> Min; false -> N end. -file("src/dream_test/reporters/progress.gleam", 231). -spec clamp_range(integer(), integer(), integer()) -> integer(). clamp_range(N, Min, Max) -> case N < Min of true -> Min; false -> case N > Max of true -> Max; false -> N end end. -file("src/dream_test/reporters/progress.gleam", 196). -spec render_bar(integer(), integer()) -> binary(). render_bar(Width, Percent) -> Filled = (Width * clamp_range(Percent, 0, 100)) div 100, Empty = Width - Filled, <<(gleam@string:repeat(<<"█"/utf8>>, Filled))/binary, (gleam@string:repeat(<<"░"/utf8>>, Empty))/binary>>. -file("src/dream_test/reporters/progress.gleam", 163). -spec render_line(integer(), integer(), integer(), binary()) -> binary(). render_line(Columns, Completed, Total, Label) -> Safe_total = case Total =< 0 of true -> 1; false -> Total end, Safe_completed = clamp_range(Completed, 0, Safe_total), Percent = case Safe_total of 0 -> 0; Gleam@denominator -> Safe_completed * 100 div Gleam@denominator end, Counter = <<<<(erlang:integer_to_binary(Safe_completed))/binary, "/"/utf8>>/binary, (erlang:integer_to_binary(Safe_total))/binary>>, Percent_text = <<(erlang:integer_to_binary(Percent))/binary, "%"/utf8>>, Prefix = <>, Suffix = case Label of <<""/utf8>> -> <<" "/utf8, Percent_text/binary>>; _ -> <<<<<<" "/utf8, Percent_text/binary>>/binary, " "/utf8>>/binary, Label/binary>> end, Fixed = (string:length(Prefix) + 3) + string:length(Suffix), Bar_width = clamp_range(Columns - Fixed, 10, Columns), Bar = <<<<"["/utf8, (render_bar(Bar_width, Percent))/binary>>/binary, "]"/utf8>>, Raw = <<<>/binary, Suffix/binary>>, pad_or_truncate(Raw, Columns). -file("src/dream_test/reporters/progress.gleam", 138). ?DOC( " Render a progress bar line for a given terminal width.\n" "\n" " This is pure and is intended for testing and for custom reporter work.\n" "\n" " Width is measured in **graphemes** (user-visible characters), so Unicode\n" " stays aligned. The width is clamped to a minimum of 20 so the bar remains\n" " readable.\n" "\n" " Note: The `columns` input is typically a *terminal column count* (from\n" " `io:columns/0`), but rendering is done by grapheme count so we can safely\n" " pad/truncate Unicode.\n" "\n" " ## Parameters\n" "\n" " - `columns`: terminal width (in columns)\n" " - `event`: event to render\n" "\n" " ## Returns\n" "\n" " A single line of text **exactly** `columns` graphemes wide (after clamping).\n" "\n" " ## Example\n" "\n" " ```gleam\n" " progress.render(30, reporter_types.RunStarted(total: 10))\n" " |> should\n" " |> match_snapshot(\"./test/snapshots/progress_render_run_started.snap\")\n" " |> or_fail_with(\"expected render output snapshot match\")\n" " ```\n" ). -spec render(integer(), dream_test@reporters@types:reporter_event()) -> binary(). render(Columns, Event) -> Cols = clamp_min(Columns, 20), case Event of {run_started, Total} -> render_line(Cols, 0, Total, <<""/utf8>>); {test_finished, Completed, Total@1, Result} -> render_line(Cols, Completed, Total@1, format_result_name(Result)); {run_finished, Completed@1, Total@2, _} -> render_line(Cols, Completed@1, Total@2, <<"done"/utf8>>); _ -> render_line(Cols, 0, 1, <<""/utf8>>) end. -file("src/dream_test/reporters/progress.gleam", 93). ?DOC( " Handle a single reporter event by writing an in-place progress bar line.\n" "\n" " - For `RunStarted`, prints an initial 0% bar.\n" " - For `TestFinished`, prints an updated bar using the included counts.\n" " - For `RunFinished`, prints a final 100% bar and a newline.\n" "\n" " This function ignores hook events (`HookStarted` / `HookFinished`) so hook\n" " chatter doesn’t scramble the single-line UI.\n" "\n" " ## Parameters\n" "\n" " - `event`: a `ReporterEvent` emitted by the runner\n" " - `write`: an output sink (typically `io.print`). For best results this\n" " should write **without adding extra newlines**.\n" "\n" " ## When should I use this?\n" "\n" " Usually you shouldn’t call it directly—prefer attaching it via\n" " `runner.progress_reporter(progress.new())` and letting the runner drive events.\n" "\n" " You may call it directly only if you are building your own reporter/driver\n" " and you are already receiving `ReporterEvent`s.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " progress.handle_event(\n" " reporter_types.RunStarted(total: 10),\n" " write_progress_line_to_file,\n" " )\n" "\n" " use text <- result.try(\n" " file.read(\"test/tmp/progress_handle_event.txt\")\n" " |> result.map_error(file.error_to_string),\n" " )\n" "\n" " text\n" " |> should\n" " |> match_snapshot(\"./test/snapshots/progress_handle_event_run_started.snap\")\n" " |> or_fail_with(\"expected handle_event output snapshot match\")\n" " ```\n" ). -spec handle_event( progress_reporter(), dream_test@reporters@types:reporter_event() ) -> gleam@option:option(binary()). handle_event(Reporter, Event) -> _ = Reporter, Cols = dream_test_reporter_progress_ffi:columns(), Line = render(Cols, Event), case Event of {hook_started, _, _, _} -> none; {hook_finished, _, _, _, _} -> none; {run_finished, _, _, _} -> {some, <<<<"\r"/utf8, Line/binary>>/binary, "\n"/utf8>>}; _ -> {some, <<"\r"/utf8, Line/binary>>} end.