Packages
fnord
0.8.58
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
@moduledoc """
User interface functions for output, logging, and user interaction.
## Context Warnings for Interactive UI
Interactive UI functions (`confirm/1`, `choose/2`, `prompt/1`) can cause deadlocks
when called from certain contexts and must be wrapped appropriately.
### GenServer Callbacks
Use `UI.Queue.run_from_genserver/1` to prevent deadlocks:
def handle_call(:delete_item, _from, state) do
confirmed = UI.Queue.run_from_genserver(fn ->
UI.confirm("Delete this item?")
end)
# ...
end
### Services.Globals.Spawn.async and Spawned Processes
Use `UI.Queue.run_from_task/1` when tasks need to participate in an existing UI interaction:
task = Services.Globals.Spawn.async(fn ->
UI.Queue.run_from_task(fn ->
UI.confirm("Process this item?")
end)
end)
### Creating UI Components
Use `UI.interact/1` to group multiple UI operations into a single atomic component:
def confirm_with_details(item) do
UI.interact(fn ->
UI.info("Item details: \#{item.name}")
UI.puts("Size: #\{item.size}, Modified: #\{item.date}")
UI.confirm("Delete this item?")
end)
end
Non-interactive functions (`info/2`, `warn/2`, `error/2`, `puts/1`, `say/1`) are safe
to call directly from any context.
## Interactive vs Non-Interactive Functions
**Interactive (require context wrappers in GenServer/Task contexts):**
- `confirm/1` - waits for yes/no input
- `choose/2` - waits for selection
- `prompt/1` - waits for text input
**Non-interactive (safe to call directly from any context):**
- `info/2`, `warn/2`, `error/2`, `debug/2` - just output
- `puts/1`, `say/1` - just output
- `interact/1` - groups operations but doesn't itself interact
"""
require Logger
# ----------------------------------------------------------------------------
# Messaging
# ----------------------------------------------------------------------------
@doc """
Execute a function as a single interaction unit. All UI calls within the function
(puts, log, choose, prompt, etc.) will be treated as part of this interaction
and execute immediately without queuing.
This is useful for composite TUI components that combine multiple UI elements.
"""
def interact(fun) when is_function(fun, 0) do
output_module().interact(fun)
end
def say(msg) do
UI.flush()
msg
|> format_detail()
|> output_module().puts()
end
def puts(msg) do
output_module().puts(msg)
end
# ----------------------------------------------------------------------------
# Feedback messages from the LLM
# ----------------------------------------------------------------------------
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
msg =
[
label_codes,
:bright,
"༺ ",
name,
" ༻ ",
:reset,
": ",
:italic,
detail_codes,
msg,
:reset
]
|> IO.ANSI.format(colorize?())
output_module().log(:info, msg)
end
# ----------------------------------------------------------------------------
# Step reporting and logging
# ----------------------------------------------------------------------------
def report_from(nil, msg), do: info(msg)
def report_from(name, msg) do
IO.ANSI.format([:cyan, "⦑ #{name} ⦒ ", :reset, msg], colorize?())
|> info()
end
def report_from(nil, msg, detail), do: info(msg, detail)
def report_from(name, msg, detail) do
output_module().log(
: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) do
IO.ANSI.format([:green, "➤ ", msg, :reset], colorize?())
|> info()
end
def begin_step(msg, detail) do
output_module().log(
:info,
IO.ANSI.format(
[:green, "➤ ", msg, :reset, ": ", :cyan, clean_detail(detail), :reset],
colorize?()
)
)
end
def end_step(msg) do
IO.ANSI.format([:yellow, "✓ ", msg, :reset], colorize?())
|> info()
end
def end_step(msg, detail) do
output_module().log(
:info,
IO.ANSI.format(
[:yellow, "✓ ", msg, :reset, ": ", :cyan, clean_detail(detail), :reset],
colorize?()
)
)
end
# Directly write to ensure visibility even if output is paused.
def printf_debug(item) do
Logger.debug(inspect(item, pretty: true))
Logger.flush()
item
end
def debug(msg) do
msg = IO.ANSI.format([:green, msg, :reset], colorize?())
output_module().log(:debug, msg)
end
def debug(msg, detail) do
msg =
IO.ANSI.format(
[:green, msg, :reset, ": ", :cyan, clean_detail(detail), :reset],
colorize?()
)
output_module().log(:debug, msg)
end
def info(msg) do
output_module().log(:info, IO.ANSI.format([:green, msg, :reset], colorize?()))
end
def info(msg, detail) do
output_module().log(
:info,
IO.ANSI.format(
[:green, msg || "", :reset, ": ", :cyan, clean_detail(detail), :reset],
colorize?()
)
)
end
def warn(msg) do
output_module().log(:warning, IO.ANSI.format([:yellow, msg, :reset], colorize?()))
end
def warn(msg, detail) do
output_module().log(
:warning,
IO.ANSI.format(
[:yellow, msg, :reset, ": ", :cyan, clean_detail(detail), :reset],
colorize?()
)
)
end
def error(msg) do
output_module().log(:error, IO.ANSI.format([:red, msg, :reset], colorize?()))
end
def error(msg, detail) do
output_module().log(
:error,
IO.ANSI.format([:red, msg, :reset, ": ", :cyan, clean_detail(detail), :reset], colorize?())
)
end
# Directly write to ensure visibility even if output is paused.
@spec fatal(binary) :: no_return()
def fatal(msg) do
Logger.error(IO.ANSI.format([:red, msg, :reset], colorize?()))
Logger.flush()
System.halt(1)
end
# Directly write to ensure visibility even if output is paused.
@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
# Directly write to stderr to ensure visibility even if output is paused.
@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
safe_usage = max(usage, 0)
safe_context = max(model.context, 1)
percentage = Float.round(safe_usage / safe_context * 100, 2)
str_usage = Util.format_number(safe_usage)
str_context = Util.format_number(safe_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 UI.is_tty?() && !UI.quiet?() do
output_module().choose(label, options, timeout_ms, default)
else
{:error, :no_tty}
end
end
def choose(label, options) do
if UI.is_tty?() && !UI.quiet?() do
output_module().choose(label, options)
else
{:error, :no_tty}
end
end
def prompt(prompt, owl_opts \\ []) do
if UI.is_tty?() && !UI.quiet?() do
owl_opts = Keyword.put(owl_opts, :label, prompt)
output_module().prompt(prompt, owl_opts)
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
output_module().confirm(msg, default)
end
def newline do
unless UI.quiet?() do
output_module().newline()
end
end
def box(contents, opts) do
unless UI.quiet?() do
output_module().newline()
output_module().box(contents, opts)
end
end
# ----------------------------------------------------------------------------
# Helper functions
# ----------------------------------------------------------------------------
def flush, do: output_module().flush()
defp output_module do
Services.Globals.get_env(:fnord, :ui_output, UI.Output.Production)
end
def quiet?() do
Services.Globals.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