Packages
fnord
0.8.35
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
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