add in new post and missing files
Bobby Grayson 2 weeks ago 4 files (+346, -0)
@@ -0,0 +1,112 @@+defmodule BlogWeb.KeyloggerLive do+ use BlogWeb, :live_view+ import BlogWeb.CoreComponents+ require Logger+ @meta_attrs [+ %{name: "title", content: "See what key you are pressing, and have it remembered"},+ %{name: "description", content: "See what key you are pressing. It also will keep what you type on hand to print if you want"},+ %{property: "og:title", content: "See what key you are pressing, and have it remembered"},+ %{property: "og:description", content: "See what key you are pressing. It also will keep what you type on hand to print if you want"},+ %{property: "og:type", content: "website"}+ ]++ def mount(_params, _session, socket) do+ {:ok,+ assign(socket,+ pressed_key: "",+ pressed_keys: "",+ show_modal: true,+ page_title: "Experiment - sorta typewriter",+ meta_attrs: @meta_attrs+ )}+ end++ def handle_event("keydown", %{"key" => key}, socket) do+ Logger.info("Key pressed: #{key}")+ pressed_keys =+ case key do+ "Enter" ->+ socket.assigns.pressed_keys <> "\r\n"+ _ -> socket.assigns.pressed_keys <> key+ end+ {:noreply, assign(socket, pressed_key: key, pressed_keys: pressed_keys) |> assign(show_modal: false)}+ end++ def handle_event("toggle_modal", %{"value" => _}, socket) do+ {:noreply, assign(socket, show_modal: !socket.assigns.show_modal)}+ end+ def handle_event("toggle_modal", _, socket) do+ {:noreply, assign(socket, show_modal: !socket.assigns.show_modal)}+ end++ def render(assigns) do+ ~H"""+ <style>+ @media print {+ /* Hide everything by default */+ body * {+ visibility: hidden;+ }++ /* Only show the content we want to print */+ #content-of-letter,+ #content-of-letter * {+ visibility: visible;+ }++ /* Position the content at the top of the page */+ #content-of-letter {+ position: absolute;+ left: 0;+ top: 0;+ width: 100%;+ text-align: left;+ white-space: pre-wrap;+ font-family: "Courier New", Courier, monospace;+ font-size: 14px;+ line-height: 1.5;+ color: #333;+ padding: 2rem;+ }+ }++ .cursor {+ display: inline-block;+ width: 2px;+ height: 1em;+ background-color: #333;+ margin-left: 1px;+ animation: blink 1s step-end infinite;+ }+ </style>++ <.head_tags meta_attrs={@meta_attrs} page_title={@page_title} />+ <h1 class="text-[75px]">Pressing: <%= @pressed_key %></h1>+ <%= if @show_modal do %>+ <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" phx-click="toggle_modal">+ <div class="bg-white p-8 rounded-lg shadow-lg max-w-lg" phx-click-away="toggle_modal">+ <div class="prose">+ <p class="font-mono text-gray-800">+ Simulate a typewriter. We server this up so that when you type out a letter its as if it were on a typewriter, and we package it up to print for whoever you would like if you press the right key shortcut or button+ </p>+ </div>+ <div class="mt-6 flex justify-end">+ <button+ phx-click="toggle_modal"+ class="px-4 py-2 bg-gray-800 text-gray-100 rounded hover:bg-gray-700 font-mono"+ >+ Close+ </button>+ </div>+ </div>+ </div>+ <% end %>+ <div id="content-of-letter" class="mt-4 text-gray-500" phx-window-keydown="keydown">+ THIS COPY IS PROVIDED WITH NO COPY AND PASTE AND IS ALL HAND WRITTEN BY YOUR COMMON HUMAN FRIEND+ <br>+ <div class="whitespace-pre-wrap"><%= @pressed_keys %></div>+ </div>+ """+ end++end
@@ -0,0 +1,64 @@+defmodule BlogWeb.VimTweetsLive do+ use BlogWeb, :live_view+ import BlogWeb.CoreComponents+ @meta_attrs [+ %{name: "title", content: "Browse Bluesky: In Vim Mode!"},+ %{name: "description", content: "Use j and k to navigate skeets"},+ %{property: "og:title", content: "Browse Bluesky: In Vim Mode!"},+ %{property: "og:description", content: "Use j and k to navigate skeets"},+ %{property: "og:type", content: "website"}+ ]++ @window_size 25+ def mount(_params, _session, socket) do+ tweets = Blog.Skeets.Sampler.sample(100) |> Enum.map(& &1.skeet)++ socket = socket+ |> assign(+ cursor: 0,+ tweets: tweets,+ visible_tweets: Enum.take(tweets, @window_size),+ page_title: "Thoughts and Tidbits Blog: Bobby Experiment - vim navigation",+ meta_attrs: @meta_attrs+ )++ {:ok, socket}+ end++ def handle_event("keydown", %{"key" => "j"}, socket) do+ new_cursor = min(socket.assigns.cursor + 1, length(socket.assigns.tweets) - 1)+ visible_tweets = get_visible_tweets(socket.assigns.tweets, new_cursor)+ {:noreply, assign(socket, cursor: new_cursor, visible_tweets: visible_tweets)}+ end++ def handle_event("keydown", %{"key" => "k"}, socket) do+ new_cursor = max(socket.assigns.cursor - 1, 0)+ visible_tweets = get_visible_tweets(socket.assigns.tweets, new_cursor)+ {:noreply, assign(socket, cursor: new_cursor, visible_tweets: visible_tweets)}+ end++ def handle_event("keydown", _key, socket), do: {:noreply, socket}++ defp get_visible_tweets(tweets, cursor) do+ start_idx = max(0, cursor - 2)+ Enum.slice(tweets, start_idx, @window_size)+ end++ def render(assigns) do+ ~H"""+ <.head_tags meta_attrs={@meta_attrs} page_title={@page_title} />+ <div class="mt-4 text-gray-500">+ Cursor position: <%= @cursor %>+ </div>+ <div class="p-4" phx-window-keydown="keydown">+ <div class="space-y-4">+ <%= for {tweet, index} <- Enum.with_index(@visible_tweets) do %>+ <div class={"p-4 border rounded #{if index == 2, do: 'bg-blue-100'}"}>+ <%= tweet %>+ </div>+ <% end %>+ </div>+ </div>+ """+ end+end
@@ -0,0 +1,7 @@+defmodule Blog.Repo.Migrations.AddUniqueIndexToSkeets do+ use Ecto.Migration++ def change do+ create unique_index(:skeets, [:skeet])+ end+end
@@ -0,0 +1,163 @@+tags: programming,elixir,fun++# A Quick Typewriter-Set Letter Project/Experiment++I decided to start off by coding up a vim-style scroller for live skeets.++Let's say I have a context called `Social`, with a function `sample/1` that returns lists of skeets.++Who cares where they come from, its a simple enough API that we can use as the basis here.++I wanted to have a simple setup where we would scroll through a list of posts with `j` and `k`.+I make that database table of skeets get populated live.+I ingest the entire network, and every second I save about 1 of the 60ish skeets coming over the network.+So this page is always fresh, and you will want to quickly mindlessly scroll.++I started off with a pretty simpe LiveView:++```elixir+defmodule BlogWeb.VimTweetsLive do+ use BlogWeb, :live_view++ @window_size 25++ def mount(_params, _session, socket) do+ tweets = Social.sample(100) |> Enum.map(& &1.skeet)++ socket = socket+ |> assign(+ cursor: 0,+ tweets: tweets,+ visible_tweets: Enum.take(tweets, @window_size),+ page_title: "Thoughts and Tidbits Blog: Bobby Experiment - vim navigation",+ meta_attrs: @meta_attrs+ )++ {:ok, socket}+ end++ def handle_event("keydown", %{"key" => "j"}, socket) do+ new_cursor = min(socket.assigns.cursor + 1, length(socket.assigns.tweets) - 1)+ visible_tweets = get_visible_tweets(socket.assigns.tweets, new_cursor)+ {:noreply, assign(socket, cursor: new_cursor, visible_tweets: visible_tweets)}+ end++ def handle_event("keydown", %{"key" => "k"}, socket) do+ new_cursor = max(socket.assigns.cursor - 1, 0)+ visible_tweets = get_visible_tweets(socket.assigns.tweets, new_cursor)+ {:noreply, assign(socket, cursor: new_cursor, visible_tweets: visible_tweets)}+ end++ def handle_event("keydown", _key, socket), do: {:noreply, socket}++ defp get_visible_tweets(tweets, cursor) do+ start_idx = max(0, cursor - 2)+ Enum.slice(tweets, start_idx, @window_size)+ end++ def render(assigns) do+ ~H"""+ <.head_tags meta_attrs={@meta_attrs} page_title={@page_title} />+ <div class="mt-4 text-gray-500">+ Cursor position: <%= @cursor %>+ </div>+ <div class="p-4" phx-window-keydown="keydown">+ <div class="space-y-4">+ <%= for {tweet, index} <- Enum.with_index(@visible_tweets) do %>+ <div class={"p-4 border rounded #{if index == 2, do: 'bg-blue-100'}"}>+ <%= tweet %>+ </div>+ <% end %>+ </div>+ </div>+ """+ end+end+```++Let's break this down into pieces.++### Key Events+We can really easily intercept key events in Phoenix/LiveView.++Let's take a look at the core of how that works:++```elixir+ # liveview definition with mount++ require Logger++ def handle_event("keydown", _key, socket), do: {:noreply, socket}+ Logger.info("Pressed: #{key}")+ {:noreply, socket}+ end++ def render(assigns)+ ~H"""+ <div class="p-4" phx-window-keydown="keydown">+ hi+ </div>+ """+ end+```++Now, this starts off with `phx-window-keydown` which is set to `"keydown"`.++We can get [key events](https://hexdocs.pm/phoenix_live_view/bindings.html#key-events) from the provided APIs using this.++```+The onkeydown, and onkeyup events are supported via the phx-keydown, and phx-keyup bindings.+Each binding supports a phx-key attribute, which triggers the event for the specific key press.+If no phx-key is provided, the event is triggered for any key press.+When pushed, the value sent to the server will contain the "key" that was pressed, plus any user-defined metadata.+For example, pressing the Escape key looks like this:++%{"key" => "Escape"}+```++Great, so with this, we are now logging what we are pressing.++So now, we can wire into `j` and `k` and make it so the "visible" batch of skeets is offset by the change in index.++With that change, we make a new "cursor" which is just an index position, and a new batch of "visible skeets" that are simply the ones from the batch we have deemed currenty viewable.++This all is quite simple and elegant, in my opinion.++If we go and look at the HTML we can see how this ties together so simply:++```elixir+ def render(assigns) do+ ~H"""+ <.head_tags meta_attrs={@meta_attrs} page_title={@page_title} />+ <div class="mt-4 text-gray-500">+ Cursor position: <%= @cursor %>+ </div>+ <div class="p-4" phx-window-keydown="keydown">+ <div class="space-y-4">+ <%= for {tweet, index} <- Enum.with_index(@visible_tweets) do %>+ <div class={"p-4 border rounded #{if index == 2, do: 'bg-blue-100'}"}>+ <%= tweet %>+ </div>+ <% end %>+ </div>+ </div>+ """+ end+```++We are handling keydown and for the indexed tweet, highlighting its coor.+Then if we key up or down, we redefine whats viewable here, and the rest are just displayed.++What we end up with is beautifully simple looking.++You can check it out [here](https://thoughts-and-tidbits.fly.dev/vim).++## Making this+This all was fun, and inspired something else:++A letter writer.+Where you cannot copy and paste.+You must take the time to write.+You must be truly original and keep your mistakes except for backspace being allowed.++That project is partially shipped and I will write more about it later.