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"""

Microprints

Visual fingerprints of source code files. Click a line to highlight it. Click a card to expand and view the source.

<.microprint_legend />
<%= for %{path: path, microprint: microprint, source: source} = item <- @microprints do %> {error = item[:error]}

{path}

<%= if microprint do %> <.microprint microprint={microprint} width={200} max_height={100} clickable={true} file_path={path} highlighted_line={@highlighted_line} /> <%= if @expanded_path == path and source do %> <.source_viewer content={source} highlighted_line={@highlighted_line} language="elixir" /> <% end %> <% else %>
Error: {inspect(error)}
<% end %>
<% end %>
""" 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 defp sort_by_mtime(files) do app_root = Mix.Project.project_file() |> Path.dirname() files |> Enum.map(fn file -> abs_path = Path.join(app_root, file) {file, File.stat!(abs_path).mtime} end) |> Enum.sort_by(fn {_file, mtime} -> mtime end, :desc) |> Enum.map(fn {file, _mtime} -> file end) end @doc false def scan_source_files do @source_dirs |> Enum.flat_map(&collect_elixir_files/1) |> Enum.uniq() |> Enum.sort() |> sort_by_mtime() |> Enum.take(2) end defp resolve_absolute_paths(files) do app_root = Mix.Project.project_file() |> Path.dirname() Enum.map(files, fn path -> Path.expand(Path.join(app_root, path)) 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("<", "<") |> String.replace(">", ">") {: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