Packages
fnord
0.9.39
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/tee.ex
defmodule UI.Tee do
@moduledoc """
Optional transcript writer for `--tee`. When started, every UI output
(Logger messages, stdout puts, direct stderr writes) is mirrored to a
plain-text file with ANSI escape codes stripped.
When not running, `write/1` is a silent no-op, so callers never need to
check whether tee mode is active.
"""
use GenServer
# ---------------------------------------------------------------------------
# ANSI stripping
# ---------------------------------------------------------------------------
# Matches CSI sequences (e.g. \e[31m, \e[0;1;32m, \e[K) and OSC sequences
# (e.g. \e]8;;url\e\\). Covers the full range of terminal escapes that Owl
# and IO.ANSI produce.
@ansi_re ~r/\x1b(?:\[[0-9;]*[a-zA-Z]|\][^\x1b]*\x1b\\|\][^\x07]*\x07)/
@spec strip_ansi(Owl.Data.t() | iodata()) :: binary()
def strip_ansi(data) do
# Two kinds of data arrive here:
# 1. Raw iodata from IO.ANSI.format (improper lists with ANSI escapes)
# 2. Owl.Data.t() with Owl.Data.tag structs (from Owl.Box, etc.)
#
# Owl.Data.to_chardata/1 handles both: it strips Owl.Data.tag structs
# and normalizes improper lists from IO.ANSI.format into valid iodata.
# Raw ANSI escape sequences in strings pass through, so the regex strips
# those from the flattened binary.
data
|> Owl.Data.to_chardata()
|> IO.iodata_to_binary()
|> String.replace(@ansi_re, "")
end
# ---------------------------------------------------------------------------
# Client API
# ---------------------------------------------------------------------------
@spec start_link(Path.t()) :: GenServer.on_start()
def start_link(path) do
with {:ok, pid} <- GenServer.start_link(__MODULE__, path) do
Services.Instance.register(__MODULE__, pid)
{:ok, pid}
end
end
@doc """
Write data to the tee file, stripping ANSI codes. No-op if tee is not
running. Accepts iodata - it will be flattened to a binary before stripping.
"""
@spec write(iodata()) :: :ok
def write(data) do
Services.Instance.cast(__MODULE__, {:write, data})
end
@doc """
Flush and close the tee file, then stop the GenServer.
"""
@spec stop() :: :ok
def stop do
case Services.Instance.whereis(__MODULE__) do
nil -> :ok
pid -> GenServer.stop(pid, :normal)
end
end
# ---------------------------------------------------------------------------
# GenServer callbacks
# ---------------------------------------------------------------------------
@impl true
def init(path) do
case File.open(path, [:write, :utf8]) do
{:ok, fd} -> {:ok, %{fd: fd}}
{:error, reason} -> {:stop, {:file_open_failed, path, reason}}
end
end
@impl true
def handle_cast({:write, data}, %{fd: fd} = state) do
clean = strip_ansi(data)
IO.write(fd, clean)
{:noreply, state}
end
@impl true
def terminate(_reason, %{fd: fd}) do
File.close(fd)
:ok
end
end