firehose/app/lib/firehose_web/live/microprints_live.ex
Willem van den Ende 7a50f2d4e7 WIP microprints source now showing
also added go to install showboat and rodney for the next steps.

microprints work in qwan tracker, but not in firehose. some of the
rendering is in the project, maybe the library should provide sample
webpages.
2026-05-14 11:15:37 +01:00

229 lines
5.9 KiB
Elixir

defmodule FirehoseWeb.MicroprintsLive do
use FirehoseWeb, :live_view
alias Microprints.MicroprintCache
alias Microprints.MicroprintComponent
@source_dirs ["app", "blogex"]
@impl true
def mount(_params, _session, socket) do
files = scan_source_files()
microprints =
files
|> Enum.map(&process_file/1)
{:ok,
socket
|> assign(:page_title, "Microprints")
|> assign(:microprints, microprints)
|> assign(:expanded_path, nil)
|> assign(:highlighted_path, nil)
|> assign(:highlighted_line, nil)}
rescue
e ->
{:ok,
socket
|> assign(:page_title, "Microprints")
|> assign(:microprints, [])
|> assign(:expanded_path, nil)
|> assign(:highlighted_path, nil)
|> assign(:highlighted_line, nil)
|> put_flash(:error, "Error loading microprints: #{inspect(e)}")}
end
@impl true
def render(assigns) do
~H"""
<div class="max-w-6xl mx-auto">
<h1 class="text-2xl font-bold mb-2">Microprints</h1>
<p class="text-sm text-zinc-500 mb-6">
Visual fingerprints of source code files. Click a line to highlight it.
Click a card to expand and view the source.
</p>
<.microprint_legend />
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-6">
<%= for %{path: path, microprint: microprint, source: source} = item <- @microprints do %>
{error = item[:error]}
<div class="card bg-base-100 shadow-sm border border-zinc-200">
<div class="card-body p-4">
<h3 class="text-sm font-mono font-medium truncate" title={path}>
{path}
</h3>
<%= if microprint do %>
<.microprint
microprint={microprint}
width={200}
max_height={100}
clickable={true}
file_path={path}
highlighted_line={@highlighted_line}
/>
<button
phx-click="toggle_expand"
phx-value-path={path}
class="btn btn-xs btn-ghost mt-2 w-full"
>
<%= if @expanded_path == path do %>
Collapse
<% else %>
Expand
<% end %>
</button>
<%= if @expanded_path == path and source do %>
<.source_viewer
content={source}
highlighted_line={@highlighted_line}
language="elixir"
/>
<% end %>
<% else %>
<div class="text-xs text-red-500 mt-1">
Error: {inspect(error)}
</div>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
"""
end
@impl true
def handle_event("highlight_line", %{"line" => line, "path" => path}, socket) do
highlighted =
case socket.assigns.highlighted_path do
^path -> nil
_ -> String.to_integer(line)
end
{:noreply,
socket
|> assign(:highlighted_path, path)
|> assign(:highlighted_line, highlighted)}
end
@impl true
def handle_event("toggle_expand", %{"path" => path}, socket) do
expanded =
case socket.assigns.expanded_path do
^path -> nil
_ -> path
end
{:noreply, assign(socket, :expanded_path, expanded)}
end
# Private helpers
@doc false
def scan_source_files do
@source_dirs
|> Enum.flat_map(&collect_elixir_files/1)
|> Enum.uniq()
|> Enum.sort()
end
defp resolve_absolute_paths(files) do
app_root = Mix.Project.project_file() |> Path.dirname()
Enum.map(files, fn path ->
case Path.split(path) do
[".." | _rest] ->
Path.join(app_root, path)
_ ->
Path.expand(path)
end
end)
end
defp excluded_path?(path) do
path =~ "/_build/" or
path =~ "/deps/" or
path =~ "/examples/" or
path =~ "/test/" or
path =~ "/lib_dev/"
end
defp collect_elixir_files(dir) do
app_root = Mix.Project.project_file() |> Path.dirname()
monorepo_root = app_root |> Path.dirname()
base = Path.join(monorepo_root, dir)
case File.dir?(base) do
true ->
base
|> Path.join("**/*.ex")
|> Path.wildcard()
|> Enum.filter(&File.regular?(&1))
|> Enum.reject(&excluded_path?/1)
|> Enum.map(&format_path(&1, monorepo_root, dir))
false ->
[]
end
end
defp format_path(path, monorepo_root, dir) do
relative = Path.relative_to(path, monorepo_root)
case dir do
"app" ->
String.replace_prefix(relative, "app/", "")
_ ->
"../" <> relative
end
end
defp process_file(rel_path) do
abs_path = resolve_absolute_paths([rel_path]) |> List.first()
case MicroprintCache.get_microprint(abs_path) do
{:ok, microprint} ->
# Add line numbers to each line for highlighting
lines_with_numbers =
microprint.lines
|> Enum.with_index(1)
|> Enum.map(fn {line, num} -> Map.put(line, :line_number, num) end)
# Read source code for expand/contract
source = read_source(abs_path)
%{
path: rel_path,
microprint: Map.put(microprint, :lines, lines_with_numbers),
source: source
}
{:error, reason} ->
%{path: rel_path, microprint: nil, error: reason}
end
end
defp read_source(abs_path) do
case File.read(abs_path) do
{:ok, content} ->
content
|> String.replace("<", "&lt;")
|> String.replace(">", "&gt;")
{:error, _} ->
nil
end
end
# Delegate to MicroprintComponent
defdelegate microprint(assigns), to: MicroprintComponent
defdelegate microprint_legend(assigns), to: MicroprintComponent
defdelegate source_viewer(assigns), to: MicroprintComponent
end