Packages

fnord

0.8.42

AI code archaeology

Current section

Files

Jump to
fnord lib ui.ex
Raw

lib/ui.ex

defmodule UI do
require Logger
@notification_timeout_ms 60_000
# ----------------------------------------------------------------------------
# Messaging
# ----------------------------------------------------------------------------
def say(msg) do
UI.flush()
msg
|> format_detail()
|> Owl.IO.puts()
end
def puts(msg) do
Owl.IO.puts(msg)
end
# ----------------------------------------------------------------------------
# Inversion of the `detail` for notifications from Fnord Prefect itself
# ----------------------------------------------------------------------------
def feedback(:info, name, msg) do
feedback(name, msg, :green_background, :green)
end
def feedback(:warn, name, msg) do
feedback(name, msg, :yellow_background, :yellow)
end
def feedback(:error, name, msg) do
feedback(name, msg, :red_background, :red)
end
def feedback(:debug, name, msg) do
feedback(name, msg, :cyan_background, :cyan)
end
defp feedback(name, msg, label_codes, detail_codes) do
[
label_codes,
:bright,
"༺ ",
name,
" ༻ ",
:reset,
": ",
:italic,
detail_codes,
msg,
:reset
]
|> IO.ANSI.format(colorize?())
|> Logger.info()
end
# ----------------------------------------------------------------------------
# Step reporting and logging
# ----------------------------------------------------------------------------
def report_from(nil, msg), do: info(msg)
def report_from(name, msg) do
Logger.info(
IO.ANSI.format(
[
:cyan,
"⦑ #{name} ⦒ ",
:reset,
:green,
msg,
:reset
],
colorize?()
)
)
end
def report_from(nil, msg, detail), do: info(msg, detail)
def report_from(name, msg, detail) do
Logger.info(
IO.ANSI.format(
[
:cyan,
"⦑ #{name} ⦒ ",
:reset,
:green,
msg,
:reset,
": ",
:light_black,
clean_detail(detail),
:reset
],
colorize?()
)
)
end
def report_step(msg), do: info(msg)
def report_step(msg, detail), do: info(msg, detail)
def begin_step(msg, detail \\ nil) do
if is_nil(detail) do
Logger.info(IO.ANSI.format([:green, msg, :reset], colorize?()))
else
Logger.info(
IO.ANSI.format(
[:green, msg, :reset, ": ", :cyan, clean_detail(detail), :reset],
colorize?()
)
)
end
end
def end_step(msg, detail \\ nil) do
if is_nil(detail) do
Logger.info(IO.ANSI.format([:yellow, msg, :reset], colorize?()))
else
Logger.info(
IO.ANSI.format(
[:yellow, msg, :reset, ": ", :cyan, clean_detail(detail), :reset],
colorize?()
)
)
end
end
def printf_debug(item) do
Logger.debug(inspect(item, pretty: true))
Logger.flush()
item
end
def debug(msg) do
Logger.debug(IO.ANSI.format([:green, msg, :reset], colorize?()))
end
def debug(msg, detail) do
Logger.debug(
IO.ANSI.format(
[:green, msg, :reset, ": ", :cyan, clean_detail(detail), :reset],
colorize?()
)
)
end
def info(msg) do
Logger.info(IO.ANSI.format([:green, msg, :reset], colorize?()))
end
def info(msg, detail) do
msg = msg || ""
Logger.info(
IO.ANSI.format(
[:green, msg, :reset, ": ", :cyan, clean_detail(detail), :reset],
colorize?()
)
)
end
def warn(msg) do
Logger.warning(IO.ANSI.format([:yellow, msg, :reset], colorize?()))
end
def warn(msg, detail) do
Logger.warning(
IO.ANSI.format(
[:yellow, msg, :reset, ": ", :cyan, clean_detail(detail), :reset],
colorize?()
)
)
end
def error(msg) do
Logger.error(IO.ANSI.format([:red, msg, :reset], colorize?()))
end
def error(msg, detail) do
Logger.error(
IO.ANSI.format([:red, msg, :reset, ": ", :cyan, clean_detail(detail), :reset], colorize?())
)
end
@spec fatal(binary) :: no_return()
def fatal(msg) do
Logger.error(IO.ANSI.format([:red, msg, :reset], colorize?()))
Logger.flush()
System.halt(1)
end
@spec fatal(binary, binary) :: no_return()
def fatal(msg, detail) do
Logger.error(
IO.ANSI.format([:red, msg, :reset, ": ", :cyan, clean_detail(detail), :reset], colorize?())
)
Logger.flush()
System.halt(1)
end
@spec warning_banner(binary) :: :ok
def warning_banner(msg) do
IO.puts(
:stderr,
IO.ANSI.format(
[
:red_background,
:black,
" <<< WARNING >>> #{msg} <<< WARNING >>> ",
:reset
],
colorize?()
)
)
end
@spec log_usage(AI.Model.t(), non_neg_integer | map) :: :ok
def log_usage(model, usage) when is_integer(usage) do
percentage = Float.round(usage / model.context * 100, 2)
str_usage = Util.format_number(usage)
str_context = Util.format_number(model.context)
info("Context window usage", "#{percentage}% (#{str_usage} / #{str_context} tokens)")
end
def log_usage(model, %{"total_tokens" => total_tokens} = _usage) do
log_usage(model, total_tokens)
end
def log_usage(model, %{total_tokens: total_tokens} = _usage) do
log_usage(model, total_tokens)
end
@spec italicize(binary) :: iodata
def italicize(text) do
IO.ANSI.format([:italic, text, :reset], colorize?())
end
# ----------------------------------------------------------------------------
# TUI/Animated elements
# ----------------------------------------------------------------------------
def spin(processing, func) do
if quiet?() do
begin_step(processing)
{msg, result} = func.()
end_step(msg)
result
else
Spinner.run(func, processing)
end
end
def progress_bar_start(name, label, total) do
if !quiet?() do
Owl.ProgressBar.start(
id: name,
label: label,
total: total,
timer: true,
absolute_values: true
)
end
end
def progress_bar_update(name) do
if !quiet?() do
Owl.ProgressBar.inc(id: name)
Owl.LiveScreen.await_render()
end
end
def async_stream(enumerable, fun, label \\ "Working", options \\ []) do
progress_bar_start(:async_stream, label, Enum.count(enumerable))
enumerable
|> Util.async_stream(
fn item ->
result = fun.(item)
progress_bar_update(:async_stream)
result
end,
options
)
end
# ----------------------------------------------------------------------------
# Interactive prompts
# ----------------------------------------------------------------------------
def choose(label, options, timeout_ms, default) do
if is_tty?() && !quiet?() do
# Display the selection prompt
task =
Task.async(fn ->
UI.flush()
Owl.IO.select(options, label: label)
end)
# Phase A: wait for notification threshold
case Task.yield(task, @notification_timeout_ms) do
{:ok, selection} ->
selection
nil ->
# Send OS notification and continue waiting
Notifier.notify("Fnord", "Fnord is waiting for your selection: #{label}", [])
# Phase B: wait for auto window
case Task.yield(task, timeout_ms) do
{:ok, selection} ->
Notifier.dismiss("Fnord")
selection
nil ->
Task.shutdown(task, :brutal_kill)
Notifier.dismiss("Fnord")
UI.debug("Auto-selection after user-specified timeout", "#{label}: #{default}")
default
end
end
else
{:error, :no_tty}
end
end
def choose(label, options) do
if is_tty?() && !quiet?() do
with_notification_timeout(
fn ->
UI.flush()
Owl.IO.select(options, label: label)
end,
"Fnord is waiting for your selection: #{label}"
)
else
{:error, :no_tty}
end
end
def prompt(prompt, owl_opts \\ []) do
if is_tty?() && !quiet?() do
owl_opts = Keyword.put(owl_opts, :label, prompt)
with_notification_timeout(
fn ->
UI.flush()
Owl.IO.input(owl_opts)
end,
"Fnord is waiting for your input: #{String.slice(prompt, 0..50)}"
)
else
{:error, :no_tty}
end
end
@spec confirm(binary) :: boolean
def confirm(msg), do: confirm(msg, false)
@spec confirm(binary, boolean) :: boolean
def confirm(msg, default) do
has_default = is_boolean(default)
cond do
is_tty?() ->
yes = if default == true, do: "Y", else: "y"
no = if default == false, do: "N", else: "n"
with_notification_timeout(
fn ->
flush()
IO.write(:stderr, UI.Formatter.format_output("#{msg} (#{yes}/#{no}) "))
case IO.gets("") do
"y\n" -> true
"Y\n" -> true
_ -> default
end
end,
"Fnord is waiting for your response to: #{msg}"
)
has_default ->
default
true ->
Logger.warning(
"Confirmation requested without default, but session is not connected to a TTY."
)
false
end
end
def newline do
unless UI.quiet?() do
IO.puts("")
end
end
def box(contents, opts) do
unless UI.quiet?() do
Owl.IO.puts("")
contents
|> Owl.Box.new(opts)
|> Owl.IO.puts()
end
end
# ----------------------------------------------------------------------------
# Notification timeout for interactive prompts
# ----------------------------------------------------------------------------
@doc """
Executes a function with a notification timeout.
If the function takes longer than the specified timeout (default 60 seconds),
a system notification is sent to alert the user, but the function continues
to run. The notification is cancelled if the function completes before the
timeout.
"""
@spec with_notification_timeout((-> any), binary) :: any
def with_notification_timeout(func, notification_message) do
with_notification_timeout(func, notification_message, @notification_timeout_ms)
end
@spec with_notification_timeout((-> any), binary, non_neg_integer) :: any
def with_notification_timeout(func, notification_message, timeout_ms) do
# Start a task to execute the function
task = Task.async(func)
# Start a timer for the notification
timer_ref =
Process.send_after(self(), {:notification_timeout, notification_message}, timeout_ms)
# Wait for the task to complete while handling timeout messages
result = wait_for_task_with_timeout(task, timer_ref)
# Clean up: cancel timer if still active
Process.cancel_timer(timer_ref)
result
end
@spec wait_for_task_with_timeout(Task.t(), reference) :: any
defp wait_for_task_with_timeout(task, timer_ref) do
wait_for_task_with_timeout(task, timer_ref, false)
end
@spec wait_for_task_with_timeout(Task.t(), reference, boolean) :: any
defp wait_for_task_with_timeout(task, timer_ref, notification_sent?) do
receive do
{:notification_timeout, message} ->
# Send notification but continue waiting for the task
Notifier.notify("Fnord", message, [])
wait_for_task_with_timeout(task, timer_ref, true)
after
100 ->
# Check if task completed
case Task.yield(task, 0) do
{:ok, result} ->
# If we sent a notification, try to dismiss it
if notification_sent? do
Notifier.dismiss("Fnord")
end
result
nil ->
# Keep waiting
wait_for_task_with_timeout(task, timer_ref, notification_sent?)
end
end
end
# ----------------------------------------------------------------------------
# Helper functions
# ----------------------------------------------------------------------------
def flush, do: Logger.flush()
def quiet?() do
Application.get_env(:fnord, :quiet)
end
def is_tty? do
:prim_tty.isatty(:stderr)
|> case do
true -> true
_ -> false
end
end
def colorize?, do: is_tty?() && !quiet?()
defp format_detail(content) when is_binary(content) do
content |> UI.Formatter.format_output()
end
defp format_detail(content) when is_list(content) do
content |> IO.ANSI.format(colorize?())
end
defp format_detail(content) do
# Fallback for other types - convert to string and treat as markdown
content |> to_string() |> UI.Formatter.format_output()
end
def clean_detail(nil), do: ""
def clean_detail(detail) do
if iodata?(detail) do
detail
else
inspect(detail, pretty: true, limit: :infinity)
end
|> IO.ANSI.format(colorize?())
|> IO.iodata_to_binary()
|> String.trim()
|> then(fn str ->
if String.contains?(str, "\n") do
# If there are multiple lines, prefix with an empty line to ensure the
# string is displayed correctly.
"\n" <> str
else
str
end
end)
end
def iodata?(term) when is_binary(term), do: true
def iodata?(term) when is_integer(term) and term in 0..255, do: true
def iodata?([]), do: true
def iodata?([head | tail]), do: iodata?(head) and iodata_tail?(tail)
def iodata?(_), do: false
defp iodata_tail?(tail) when is_list(tail), do: iodata?(tail)
defp iodata_tail?(tail) when is_binary(tail), do: true
defp iodata_tail?(_), do: false
end