refactor: change tooling

This commit is contained in:
sloane 2024-11-08 08:34:44 -05:00
parent 9927cac9ae
commit e59536446c
Signed by: sloanelybutsurely
SSH key fingerprint: SHA256:8SBnwhl+RY3oEyQxy1a9wByPzxWM0x+/Ejc+sIlY5qQ
50 changed files with 59 additions and 1262 deletions

View file

@ -1,5 +0,0 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
import_deps: [:typed_struct, :plug]
]

37
.gitignore vendored
View file

@ -1,32 +1,7 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
sloane_sh-*.tar
# Temporary files, for example, from tests.
/tmp/
/.elixir_ls/
/priv/output/
.DS_Store .DS_Store
compiled/
/build/
*.html
*.css

2
.mise.toml Normal file
View file

@ -0,0 +1,2 @@
[tools]
racket = "8.15"

View file

@ -1,3 +0,0 @@
erlang 26.2.2
elixir 1.16.1-otp-26
nodejs 20.11.1

View file

@ -1,10 +1,22 @@
# SloaneSH # sloane.sh
_Sloane's personal static site generator powering [sloane.sh](https://sloane.sh)._
## Setup ## Setup
1. Clone the repo ```sh
2. Install Erlang and Elixir: `asdf install` brew install --cask racket
3. Install dependencies: `mix deps.get` raco pkg install pollen
```
## Working on the site
```sh
raco pollen start site
```
## Building the site
```sh
mkdir -p build
raco pollen render site
raco pollen publish site build
```

View file

@ -1,17 +0,0 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
body {
--pagefind-ui-border-width: 1px;
}
@media (prefers-color-scheme: dark) {
#search {
--pagefind-ui-primary: #eeeeee;
--pagefind-ui-text: #eeeeee;
--pagefind-ui-background: rgb(64, 64, 64);
--pagefind-ui-border: rgb(163, 163, 163);
--pagefind-ui-tag: #152028;
}
}

View file

View file

@ -1,28 +0,0 @@
// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration
let plugin = require('tailwindcss/plugin')
module.exports = {
content: [
'./js/**/*.js',
'../lib/sloane_sh/layouts/*.ex',
'../priv/site/**/*.*ex',
'../priv/site/**/*.md',
'../priv/site/**/*.html',
],
theme: {
container: {
center: true,
},
extend: {},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
plugin(({addVariant}) => addVariant('phx-no-feedback', ['&.phx-no-feedback', '.phx-no-feedback &'])),
plugin(({addVariant}) => addVariant('phx-click-loading', ['&.phx-click-loading', '.phx-click-loading &'])),
plugin(({addVariant}) => addVariant('phx-submit-loading', ['&.phx-submit-loading', '.phx-submit-loading &'])),
plugin(({addVariant}) => addVariant('phx-change-loading', ['&.phx-change-loading', '.phx-change-loading &']))
]
}

View file

@ -1,22 +0,0 @@
import Config
config :logger, :default_formatter, format: "$time $metadata[$level] $message\n"
config :tailwind,
version: "3.4.1",
default: [
args: ~w(
--config=tailwind.config.js
--input=css/app.css
--output=../priv/output/assets/css/app.css
),
cd: Path.expand("../assets", __DIR__)
]
config :esbuild,
version: "0.20.1",
default: [
args: ~w(js/app.js --bundle --outdir=../priv/output/assets/js),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]

View file

@ -1,15 +0,0 @@
defmodule Mix.Tasks.Site.Build do
@moduledoc "Build and output the site as HTML"
@shortdoc "build the site"
use Mix.Task
require Logger
alias SloaneSH.Format
@impl Mix.Task
def run(_args) do
Mix.Task.run("app.start")
{micro, :ok} = :timer.tc(&SloaneSH.build/0)
Logger.info("Built site in #{Format.time(micro)}")
end
end

View file

@ -1,37 +0,0 @@
defmodule Mix.Tasks.Site.Dev do
@moduledoc "Build the site, watch for changes, and serve the built site"
@shortdoc "run site.watch and site.serve"
use Mix.Task
@impl Mix.Task
def run(_args) do
Mix.Task.run("app.start")
{:ok, watch_pid} =
Task.start_link(fn ->
Mix.Task.run("site.watch")
end)
{:ok, serve_pid} =
Task.start_link(fn ->
Mix.Task.run("site.serve")
end)
unless iex_running?() do
watch_ref = Process.monitor(watch_pid)
serve_ref = Process.monitor(serve_pid)
receive do
{:DOWN, ^watch_ref, _, _, _} ->
:ok
{:DOWN, ^serve_ref, _, _, _} ->
:ok
end
end
end
defp iex_running? do
Code.ensure_loaded?(IEx) and IEx.started?()
end
end

View file

@ -1,28 +0,0 @@
defmodule Mix.Tasks.Site.Serve do
@moduledoc "Serve the built site. Only for use in development"
@shortdoc "serve the built site"
use Mix.Task
require Logger
@impl Mix.Task
def run(_args) do
Mix.Task.run("app.start")
Logger.info("Starting development server...")
{:ok, pid} = SloaneSH.serve()
unless iex_running?() do
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, _, _, _} ->
Logger.info("Development server terminated")
:ok
end
end
end
defp iex_running? do
Code.ensure_loaded?(IEx) and IEx.started?()
end
end

View file

@ -1,28 +0,0 @@
defmodule Mix.Tasks.Site.Watch do
@moduledoc "Build and output the site as HTML watching for changes"
@shortdoc "build the site and watch for changes"
use Mix.Task
require Logger
@impl Mix.Task
def run(_args) do
Mix.Task.run("app.start")
Logger.info("Starting site.watch...")
{:ok, pid} = SloaneSH.watch()
unless iex_running?() do
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, _, _, _} ->
Logger.info("site.watch terminated")
:ok
end
end
end
defp iex_running? do
Code.ensure_loaded?(IEx) and IEx.started?()
end
end

View file

@ -1,31 +0,0 @@
defmodule SloaneSH do
@moduledoc """
Sloane's personal static site generator powering [sloane.sh](https://sloane.sh).
"""
alias SloaneSH.Build
alias SloaneSH.Context
alias SloaneSH.Serve
alias SloaneSH.Watch
def build(_opts \\ []) do
context()
|> Build.run()
:ok
end
def watch(_opts \\ []) do
context()
|> Watch.start_link()
end
def serve do
context()
|> Serve.start_link()
end
def context do
Context.new()
end
end

View file

@ -1,26 +0,0 @@
defmodule SloaneSH.Asset do
use TypedStruct
alias SloaneSH.Config
alias SloaneSH.Context
typedstruct do
field :mod, module(), enforce: true
field :src, String.t(), enforce: true
field :src_contents, binary(), enforce: true
field :attrs, map(), enforce: true
end
@callback extensions(cfg :: Config.t()) :: [String.t()]
@callback attrs(cfg :: Config.t(), path :: String.t(), data :: binary()) ::
{:ok, map(), without_attrs :: term()} | {:ok, map()} | :error | {:error, term()}
@callback render(
cfg :: Config.t(),
ctx :: Context.t(),
path :: String.t(),
data :: binary(),
attrs :: map()
) ::
{:ok, [{dest :: String.t(), binary()}]} | :error | {:error, term()}
end

View file

@ -1,42 +0,0 @@
defmodule SloaneSH.Assets.Image do
alias SloaneSH.Asset
alias SloaneSH.OutputDirs
@behaviour Asset
@impl Asset
def extensions(_cfg), do: ~w[.jpg .jpeg .png .webp .gif]
@impl Asset
def attrs(_cfg, _path, data) do
{:ok, image} = Image.from_binary(data)
aspect = Image.aspect(image)
{width, height, _} = Image.shape(image)
{:ok, %{aspect: aspect, width: width, height: height}, image}
end
@impl Asset
def render(cfg, _ctx, path, data, _attrs) do
formats = ~w[.webp .png .jpg]
outputs =
for format <- formats do
format_path = OutputDirs.replace_ext(path, format)
output_path = OutputDirs.image(cfg, format_path)
{:ok, converted} =
Image.write(data, :memory,
suffix: format,
strip_metadata: true,
minimize_file_size: true,
quality: 80
)
{output_path, converted}
end
{:ok, outputs}
end
end

View file

@ -1,88 +0,0 @@
defmodule SloaneSH.Assets.Markdown do
@moduledoc """
Helper to define markdown, html, and eex templating for pages and posts
"""
defmacro __using__(opts) do
type = Keyword.fetch!(opts, :type)
quote do
import SloaneSH.Layouts.Helpers, warn: false
alias SloaneSH.Asset
alias SloaneSH.FrontMatter
alias SloaneSH.Layouts
alias SloaneSH.OutputDirs
@behaviour Asset
@impl Asset
def extensions(_cfg), do: ~w[.md .html .md.eex .html.eex]
@impl Asset
def attrs(cfg, path, data) do
{:ok, attrs, without_attrs} = FrontMatter.parse(data)
attrs =
Map.put_new_lazy(attrs, :permalink, fn ->
output = apply(OutputDirs, unquote(type), [cfg, path])
permalink = OutputDirs.to_permalink(cfg, output)
end)
attrs = handle_attrs(cfg, path, without_attrs, attrs)
{:ok, attrs, without_attrs}
end
@impl Asset
def render(cfg, ctx, path, data, attrs) do
output_path =
if attrs[:permalink] do
OutputDirs.from_permalink(cfg, attrs[:permalink])
else
apply(OutputDirs, unquote(type), [cfg, path])
end
output = {output_path, do_render(ctx, path, data, attrs)}
{:ok, [output]}
end
defp do_render(ctx, path, data, attrs) when is_binary(path) do
do_render(ctx, base_and_ext(path), data, attrs)
end
defp do_render(ctx, {path, ".eex"}, data, attrs) do
eexed = eval_eex(data, "#{path}.eex", ctx, attrs)
do_render(ctx, base_and_ext(path), eexed, attrs)
end
defp do_render(ctx, {path, ".md"}, data, attrs) do
html = Earmark.as_html!(data)
do_render(ctx, {path, ".html"}, html, attrs)
end
defp do_render(ctx, {_path, ".html"}, data, attrs) do
apply(Layouts, unquote(type), [data, ctx, attrs])
end
defp base_and_ext(path) do
ext = Path.extname(path)
base = Path.basename(path, ext)
{base, ext}
end
defp eval_eex(template, file, ctx, attrs) do
{result, _binding} =
template
|> EEx.compile_string(file: file)
|> Code.eval_quoted([ctx: ctx, attrs: attrs], __ENV__)
result
end
def handle_attrs(cfg, path, data, attrs) do
attrs
end
defoverridable handle_attrs: 4
end
end
end

View file

@ -1,3 +0,0 @@
defmodule SloaneSH.Assets.Page do
use SloaneSH.Assets.Markdown, type: :page
end

View file

@ -1,12 +0,0 @@
defmodule SloaneSH.Assets.Post do
use SloaneSH.Assets.Markdown, type: :post
require Logger
def handle_attrs(_cfg, path, _data, attrs) do
unless Map.has_key?(attrs, :date) do
Logger.warning("Post missing date property: #{inspect(path)}")
end
attrs
end
end

View file

@ -1,31 +0,0 @@
defmodule SloaneSH.Build do
require Logger
alias SloaneSH.Context
def run(%Context{} = ctx) do
assets = ctx.posts ++ ctx.pages ++ ctx.images
File.mkdir_p!(ctx.config.output_dir)
for asset <- assets do
case asset.mod.render(ctx.config, ctx, asset.src, asset.src_contents, asset.attrs) do
{:ok, output_files} ->
for {dest, content} <- output_files do
with :ok <- dest |> Path.dirname() |> File.mkdir_p(),
:ok <- File.write(dest, content) do
Logger.info("Wrote #{inspect(dest)}.")
else
{:error, err} ->
Logger.error("Failed to write #{inspect(dest)}, #{inspect(err)}")
end
end
err ->
Logger.error("Failed to render #{inspect(asset.src)}, #{inspect(err)}")
end
end
:ok
end
end

View file

@ -1,37 +0,0 @@
defmodule SloaneSH.Config do
@moduledoc """
SloaneSH configuration
"""
use TypedStruct
alias __MODULE__
typedstruct do
field :pages_dir, String.t(), enforce: true
field :posts_dir, String.t(), enforce: true
field :images_dir, String.t(), enforce: true
field :output_dir, String.t(), enforce: true
end
def default do
priv = :code.priv_dir(:sloane_sh) |> resolve_link()
%Config{
pages_dir: Path.join(priv, "site/pages"),
posts_dir: Path.join(priv, "site/posts"),
images_dir: Path.join(priv, "site/images"),
output_dir: Path.join(priv, "output")
}
end
defp resolve_link(path) do
case File.read_link(path) do
{:ok, link} ->
dir = Path.dirname(path)
Path.expand(link, dir)
_ ->
path
end
end
end

View file

@ -1,59 +0,0 @@
defmodule SloaneSH.Context do
@moduledoc """
A SloaneSH build context containing configuration and reference to content
files.
"""
use TypedStruct
require Logger
alias SloaneSH.Config
alias SloaneSH.Asset
alias SloaneSH.Assets.Page
alias SloaneSH.Assets.Post
alias SloaneSH.Assets.Image
alias __MODULE__
typedstruct do
field :config, Config.t(), enforce: true
field :pages, [Asset.t()], default: []
field :posts, [Asset.t()], default: []
field :images, [Asset.t()], default: []
end
def new(cfg \\ Config.default()) do
pages = load_assets(cfg, Page, cfg.pages_dir)
posts = load_assets(cfg, Post, cfg.posts_dir)
images = load_assets(cfg, Image, cfg.images_dir)
%Context{config: cfg, pages: pages, posts: posts, images: images}
end
defp load_assets(cfg, mod, src_dir) do
exts = mod.extensions(cfg)
for src <- collect_src_files(src_dir, exts) do
contents = File.read!(src)
case mod.attrs(cfg, src, contents) do
{:ok, attrs, src_contents} ->
%Asset{mod: mod, src: src, src_contents: src_contents, attrs: attrs}
{:ok, attrs} ->
%Asset{mod: mod, src: src, src_contents: contents, attrs: attrs}
_ ->
Logger.warning("Failed to parse attrs for #{inspect(src)}")
%Asset{mod: mod, src: src, src_contents: contents, attrs: %{}}
end
end
end
defp collect_src_files(src_dir, exts) do
files = src_dir |> File.ls!() |> Enum.map(&Path.join(src_dir, &1))
{src_files, rest} = Enum.split_with(files, &String.ends_with?(&1, exts))
other_dirs = Enum.filter(rest, &File.dir?/1)
src_files ++ Enum.flat_map(other_dirs, &collect_src_files(&1, exts))
end
end

View file

@ -1,34 +0,0 @@
defmodule SloaneSH.Format do
@moduledoc """
Functions to format various literals into human readable forms.
"""
@millisecond 1000
@second @millisecond ** 2
@minute 60 * @second
@hour 60 * @minute
@max_depth 2
def time(micro, depth \\ 0)
def time(_micro, depth) when depth >= @max_depth, do: []
time_units = [
{@hour, "h"},
{@minute, "m"},
{@second, "s"},
{@millisecond, "ms"}
]
for {division, unit} <- time_units do
def time(micro, depth) when micro >= unquote(division) do
count = "#{div(micro, unquote(division))}#{unquote(unit)}"
rem = rem(micro, unquote(division))
[count, time(rem, depth + 1)]
end
end
def time(micro, _depth) do
"#{micro}μs"
end
end

View file

@ -1,17 +0,0 @@
defmodule SloaneSH.FrontMatter do
@moduledoc """
Parses TOML front matter out put files
"""
def parse("+++" <> rest) do
[toml, body] = String.split(rest, ["+++\n", "+++\r\n"], parts: 2)
with {:ok, attrs} <- Toml.decode(toml, keys: :atoms) do
{:ok, attrs, body}
end
end
def parse(body) do
{:ok, %{}, body}
end
end

View file

@ -1,43 +0,0 @@
defmodule SloaneSH.Layouts do
@moduledoc """
`EEx` based layouts
"""
require EEx
import SloaneSH.Layouts.Partials, warn: false
import SloaneSH.Layouts.Helpers, warn: false
@layouts_dir Path.join(:code.priv_dir(:sloane_sh), "site/layouts")
EEx.function_from_file(:def, :root, Path.join(@layouts_dir, "root.html.eex"), [
:inner_content,
:ctx,
:attrs
])
EEx.function_from_file(:defp, :page_layout, Path.join(@layouts_dir, "page.html.eex"), [
:inner_content,
:ctx,
:attrs
])
EEx.function_from_file(:defp, :post_layout, Path.join(@layouts_dir, "post.html.eex"), [
:inner_content,
:ctx,
:attrs
])
def page(inner_content, ctx, attrs) do
inner_content
|> page_layout(ctx, attrs)
|> root(ctx, attrs)
end
def post(inner_content, ctx, attrs) do
inner_content
|> post_layout(ctx, attrs)
|> root(ctx, attrs)
end
defp prefix_title(prefix, nil), do: prefix
defp prefix_title(prefix, page_title), do: [prefix, " | ", page_title]
end

View file

@ -1,67 +0,0 @@
defmodule SloaneSH.Layouts.Helpers do
require Logger
alias SloaneSH.Context
alias SloaneSH.OutputDirs
def cx(classes) do
classes
|> Enum.map(fn
{_, _} = t -> t
c -> {c, true}
end)
|> Enum.filter(fn {_, v} -> !!v end)
|> Enum.map_join(" ", fn {class, _} -> class end)
end
def sorted_post_attrs(%Context{} = ctx) do
{drafts, others} =
ctx.posts
|> Enum.map(& &1.attrs)
|> Enum.split_with(&is_nil(&1[:date]))
others = Enum.sort_by(others, & &1[:date], {:desc, Date})
drafts ++ others
end
def fmt_date(nil), do: "Draft"
def fmt_date(date), do: Timex.format!(date, "{Mfull} {D}, {YYYY}")
def picture(ctx, src, alt \\ "", class \\ "") do
image =
Enum.find(ctx.images, fn i ->
output = OutputDirs.image(ctx.config, i.src)
src == OutputDirs.to_permalink(ctx.config, output)
end)
if is_nil(image) do
Logger.warning("Could not find #{inspect(src)} to make picture element")
~s|<img src="#{src}" alt="#{alt}" class="#{class}">|
else
[{_, src} | srcsets] =
[
{"image/jpg", ".jpg"},
{"image/webp", ".webp"},
{"image/png", ".png"}
]
|> Enum.map(fn {type, ext} ->
{type, OutputDirs.replace_ext(src, ext)}
end)
EEx.eval_string(
~S"""
<picture class="<%= class %>">
<%= for {type, srcset} <- srcsets do %>
<source srcset="<%= srcset %>" type="<%= type %>" />
<% end %>
<img src="<%= src %>" alt="<%= alt %>" />
</picture>
""",
src: src,
srcsets: srcsets,
alt: alt,
class: class
)
end
end
end

View file

@ -1,36 +0,0 @@
defmodule SloaneSH.Layouts.Partials do
@moduledoc """
HTML partials for use in HTML layouts
"""
require EEx
import SloaneSH.Layouts.Helpers, warn: false
EEx.function_from_string(
:def,
:header,
~S"""
<header class="flex flex-row justify-between gap-2 pb-4 mb-2" data-pagefind-ignore>
<div class="flex flex-col justify-end">
<%= if attrs[:title] do %>
<h1 class="text-3xl font-extrabold"><%= attrs[:title] %></h1>
<% end %>
<%= if attrs[:date] do %>
<small><%= fmt_date(attrs[:date]) %></small>
<% end %>
</div>
<div class="flex flex-col gap-2 items-end">
<div class="flex flex-row gap-2 items-center">
<a href="/"><span class="text-lg font-bold">sloane.sh</span></a>
<%= picture(ctx, "/assets/images/heart.png", "a purple heart emoji", "w-4 h-4") %>
</div>
<nav class="flex flex-row gap-2">
<a href="/" class="<%= cx(underline: attrs[:permalink] == "/") %>">home</a>
<a href="/posts" class="<%= cx(underline: Map.get(attrs, :permalink, "") =~ ~r[^/posts]) %>">posts</a>
<a href="/search" class="<%= cx(underline: attrs[:permalink] == "/search") %>">search</a>
</nav>
</div>
</header>
""",
[:ctx, :attrs]
)
end

View file

@ -1,55 +0,0 @@
defmodule SloaneSH.OutputDirs do
def page(cfg, src) do
path = Path.relative_to(src, cfg.pages_dir)
cfg.output_dir |> Path.join(path) |> prettify_html_path()
end
def post(cfg, src) do
path = Path.relative_to(src, cfg.posts_dir)
path = Path.join("post", path)
cfg.output_dir |> Path.join(path) |> prettify_html_path()
end
def image(cfg, src) do
path = Path.relative_to(src, cfg.images_dir)
path = Path.join("assets/images", path)
cfg.output_dir |> Path.join(path)
end
def prettify_html_path(path) do
file = Path.basename(path)
[without_extension | _] = String.split(file, ".", parts: 2)
suffix =
if without_extension == "index" do
"index.html"
else
Path.join(without_extension, "index.html")
end
String.replace_suffix(path, file, suffix)
end
def to_permalink(cfg, output_path) do
output_path
|> Path.relative_to(cfg.output_dir)
|> String.trim_trailing("index.html")
|> String.replace_prefix("", "/")
|> String.trim_trailing("/")
end
def from_permalink(cfg, permalink) do
Path.join([cfg.output_dir, permalink, "/index.html"])
end
def replace_ext(path, new_ext) do
ext = Path.extname(path)
base = Path.basename(path, ext)
dir = Path.dirname(path)
Path.join(dir, base <> new_ext)
end
end

View file

@ -1,21 +0,0 @@
defmodule SloaneSH.Serve do
@moduledoc """
Task to use `Bandit` to start `SloaneSH.Serve.Plug`
"""
use Supervisor
alias __MODULE__
def start_link(ctx) do
Supervisor.start_link(__MODULE__, ctx, name: __MODULE__)
end
@impl Supervisor
def init(_ctx) do
children = [
{Bandit, plug: Serve.Plug}
]
Supervisor.init(children, strategy: :one_for_one)
end
end

View file

@ -1,29 +0,0 @@
defmodule SloaneSH.Serve.Plug do
@moduledoc """
Basic HTTP server for testing and local development.
Inspired by https://github.com/mbuhot/plug_static_index_html
"""
use Plug.Builder
plug Plug.Logger
plug :rewrite_for_index_html
plug Plug.Static, at: "/", from: {:sloane_sh, "priv/output"}
plug :not_found
def rewrite_for_index_html(conn, _) do
if String.match?(conn.request_path, ~r[.+\..+$]) do
conn
else
%{
conn
| request_path: Path.join(conn.request_path, "index.html"),
path_info: conn.path_info ++ ["index.html"]
}
end
end
def not_found(conn, _) do
send_resp(conn, 404, "Not found")
end
end

View file

@ -1,81 +0,0 @@
defmodule SloaneSH.Watch do
use GenServer
use TypedStruct
require Logger
alias SloaneSH.Build
alias SloaneSH.Context
alias SloaneSH.Layouts
typedstruct do
field :ctx, Context.t(), enforce: true
field :watcher_pid, pid(), enforce: true
end
def start_link(%Context{} = ctx, opts \\ []) do
GenServer.start_link(__MODULE__, ctx, opts)
end
@impl GenServer
def init(%Context{} = ctx) do
{:ok, watcher_pid} =
FileSystem.start_link(
dirs: [
# ctx.config.layouts_dir,
"priv/site/layouts",
"lib/sloane_sh/layouts",
ctx.config.pages_dir,
ctx.config.posts_dir
]
)
:ok = FileSystem.subscribe(watcher_pid)
Task.start_link(fn ->
Tailwind.install_and_run(:default, ~w[--watch])
end)
Task.start_link(fn ->
Esbuild.install_and_run(:default, ~w[--watch])
end)
state = %__MODULE__{ctx: ctx, watcher_pid: watcher_pid}
{:ok, state, {:continue, :build}}
end
@impl GenServer
def handle_continue(:build, %{ctx: ctx} = state) do
Build.run(ctx)
{:noreply, state}
end
@impl GenServer
def handle_info({:file_event, pid, {path, _events}}, %{watcher_pid: pid} = state) do
if String.match?(path, ~r/layouts/) do
recompile_layouts()
end
ctx = Context.new()
{:noreply, %{state | ctx: ctx}, {:continue, :build}}
end
@impl GenServer
def handle_info({:file_event, pid, :stop}, %{watcher_pid: pid}) do
Logger.warning("File watcher stopped")
{:stop, :watcher_stopped, pid}
end
defp recompile_layouts do
helpers_source = Layouts.Helpers.module_info(:compile)[:source] |> List.to_string()
partials_source = Layouts.Partials.module_info(:compile)[:source] |> List.to_string()
layouts_source = Layouts.module_info(:compile)[:source] |> List.to_string()
{:ok, _, _} =
Kernel.ParallelCompiler.compile([helpers_source, partials_source, layouts_source])
:ok
end
end

53
mix.exs
View file

@ -1,53 +0,0 @@
defmodule SloaneSH.MixProject do
use Mix.Project
def project do
[
app: :sloane_sh,
version: "0.1.0",
elixir: "~> 1.15",
start_permanent: Mix.env() == :prod,
deps: deps(),
aliases: aliases()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:file_system, "~> 1.0.0"},
{:typed_struct, "~> 0.3.0"},
{:earmark, "~> 1.4"},
{:earmark_parser, "~> 1.4"},
{:plug, "~> 1.15"},
{:bandit, "~> 1.2"},
{:tailwind, "~> 0.2"},
{:toml, "~> 0.7"},
{:esbuild, "~> 0.8"},
{:timex, "~> 3.7"},
{:image, "~> 0.42"}
]
end
defp aliases do
[
"assets.deploy": [
"tailwind default --minify",
"esbuild default --minify --sourcemap --target=chrome58,firefox57,safari11,edge16"
],
"site.index": "cmd npx -y pagefind --site priv/output/",
"site.deploy": [
"site.build",
"site.index",
"assets.deploy"
]
]
end
end

View file

@ -1,39 +0,0 @@
%{
"bandit": {:hex, :bandit, "1.2.2", "569fe5d0efb107c9af37a1e37e25ce2ceec293101a2d4bc512876fc3207192b5", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "2f89adb7281c78d4e75733e0a9e1b24f46f84d2993963d6fa57d0eafadec5f03"},
"castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"},
"esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
"expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"floki": {:hex, :floki, "0.35.4", "cc947b446024732c07274ac656600c5c4dc014caa1f8fb2dfff93d275b83890d", [:mix], [], "hexpm", "27fa185d3469bd8fc5947ef0f8d5c4e47f0af02eb6b070b63c868f69e3af0204"},
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"image": {:hex, :image, "0.42.0", "aa561f15b53c40ac571e7880083cecf1419ff405fc45dc95675c58aa308eaa22", [:mix], [{:bumblebee, "~> 0.3", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.5", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.23", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "19972043abadc40e2d77dc38fc57f52382859791f89a962b0f1425ae64262f7d"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix_html": {:hex, :phoenix_html, "4.0.0", "4857ec2edaccd0934a923c2b0ba526c44a173c86b847e8db725172e9e51d11d6", [:mix], [], "hexpm", "cee794a052f243291d92fa3ccabcb4c29bb8d236f655fb03bcbdc3a8214b8d13"},
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"},
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"thousand_island": {:hex, :thousand_island, "1.3.2", "bc27f9afba6e1a676dd36507d42e429935a142cf5ee69b8e3f90bff1383943cd", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e085b93012cd1057b378fce40cbfbf381ff6d957a382bfdd5eca1a98eec2535"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"vix": {:hex, :vix, "0.26.0", "027f10b6969b759318be84bd0bd8c88af877445e4e41cf96a0460392cea5399c", [:make, :mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "71b0a79ae7f199cacfc8e679b0e4ba25ee47dc02e182c5b9097efb29fbe14efd"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 889 B

View file

@ -1,4 +0,0 @@
<%= header(ctx, attrs) %>
<main class="prose-neutral prose max-w-none dark:prose-invert" data-pagefind-body>
<%= inner_content %>
</main>

View file

@ -1,5 +0,0 @@
<%= header(ctx, attrs) %>
<article class="prose-neutral prose max-w-none dark:prose-invert" data-pagefind-body>
<%= inner_content %>
</article>

View file

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html class="m-4 flex flex-row justify-center" lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" sizes="32x32" href="/assets/images/heart.webp">
<title><%= prefix_title("sloane.sh", attrs[:page_title]) %></title>
<link rel="stylesheet" href="/assets/css/app.css" />
</head>
<body class="w-full max-w-3xl bg-white text-neutral-900 dark:bg-neutral-900 dark:text-neutral-200 pb-8">
<%= inner_content %>
<script defer src="/assets/js/app.js"></script>
<!-- generated by sloane_sh at <%= DateTime.utc_now() |> DateTime.to_iso8601() %> -->
</body>
</html>

View file

@ -1,29 +0,0 @@
+++
page_title = "home"
permalink = "/"
+++
hey, i'm sloane! i'm a professional software engineer and an amateur musician, photographer, wife, chef, and pet mom.
### around the web
<ul>
<li><a rel="me" href="https://tech.lgbt/@sloane" target="_blank">@sloane</a> on the fediverse</li>
<li><a href="https://github.com/sloanelybutsurely" target="_blank">@sloanelybutsurely</a> on github</li>
<li><a href="https://instagram.com/sloane_of_arc" target="_blank">@sloane_of_arc</a> on instagram</li>
</ul>
### writing
i write more than i share but here are some things that have made it out of my drafts folder recently
<%= for post <- Enum.take(sorted_post_attrs(ctx), 3) do %>
- <%= fmt_date(post[:date]) %>: [<%= post[:title] %>](<%= post[:permalink] %>)
<% end %>
### projects
<h4>🌱 <a href="https://screen.garden" target="_blank">screen.garden</a></h4>
real-time collaboration across obsidian and the web. currently in closed beta.

View file

@ -1,11 +0,0 @@
+++
page_title = "posts"
+++
<ul data-pagefind-ignore>
<%= for post <- sorted_post_attrs(ctx) do %>
<li>
<%= fmt_date(post[:date]) %>: <a href="<%= post[:permalink] %>"><%= post[:title] %></a>
</li>
<% end %>
</ul>

View file

@ -1,16 +0,0 @@
</main>
<div class="mt-6 mx-auto" id="search"></div>
<link rel="stylesheet" href="/pagefind/pagefind-ui.css" />
<script src="/pagefind/pagefind-ui.js"></script>
<script>
window.addEventListener('DOMContentLoaded', () => {
new PagefindUI({
element: '#search',
showSubResults: true,
showImages: false,
});
});
</script>
<main>

View file

@ -1,33 +0,0 @@
+++
date = 2023-07-23
title = "obsidianrc - a programmer's approach to obsidian"
page_title = "obsidian"
+++
my biggest hangup with obsidian has always been that it isn't vim. as a longtime vim user i'm used to being able to fully customize and extend my editing experience.
there are many existing community plugins that make it easy to customize some parts of obsidian but whenever i would have an idea or a desired workflow that didn't fit into one of those existing boxes i was stuck.
i had three options, roughly in order of immediate work required:
1. adapt my own workflows to fit the existing options
1. contribute to an existing community plugin and hope that the original author has time to review, merge in, and publish my change
1. build my own plugin to do exactly what i want
so today i had an idea for another, hopefully easier option: obsidianrc
an obsidianrc is a personal, custom plugin that resembles an "[rc](https://superuser.com/questions/144339/vimrc-screenrc-bashrc-kshrc-etc-what-does-the-rc-mean)" file like a `.vimrc`. it has no one purpose but instead is a grab-bag of whatever the author needed at the time. an obsidian user's obsidianrc evolves with her as she comes up with new workflows or changes her existing workflows.
the freedom of the obsidianrc is that it never needs to be used by anyone but its author. it's a workbench covered in jigs. if any bit of functionality ever sticks out as generally useful the code is easily (famous last words perhaps...) extracted into its own plugin and published in the community plugin repo.
## create your own obsidianrc
1. use the [obsidian-sample-plugin](https://github.com/obsidianmd/obsidian-sample-plugin) template
1. name your repo `obsidianrc`
1. install your obsidianrc in your vault
1. fill your obsidianrc with whatever need whenever you need it
1. (optional) use [BRAT](https://github.com/TfTHacker/obsidian42-brat) to install your obsidianrc and keep it up-to-date
## what's in my obsidianrc?
well at time of writing, not much. but you can take a look for yourself: [sloanelybutsurely/obsidianrc](https://github.com/sloanelybutsurely/obsidianrc)

View file

@ -1,19 +0,0 @@
+++
date = 2023-12-28
title = "in review: 2023"
page_title = "in review: 2023"
+++
- **january**: visited family in florida
- **february**: switched to injections (dolls, i implore you to do the same)
- **march**: back in florida for an elastic on-site (and an evening at disney world)
- **april**: saw caroline polachek live
- **may**: visited a friend in kentucky
- **june**: was in a friends wedding, had brunch with friends passing through town, performed with the columbus gay mens chorus, pride!! 🏳️‍🌈
- **july**: vacationed in northern michigan, went to a minor league baseball game, saw an airshow (hayleys first!), started a new job
- **august**: went to the state fair
- **september**: just enjoyed the last bits of summer
- **october**: saw kim petras live in DC, started working towards my private pilot certificate
- **november**: celebrated my 30th birthday
- **december**: celebrated the holidays and rang in the new year with family and friends

View file

@ -1,60 +0,0 @@
+++
title = "\"Uses This\""
page_title = "uses"
permalink = "/uses"
date = 2024-03-09
+++
> **Have you considered adding a `/uses/` page to your own site, answering the same questions?**
> \- [Daniel](https://wafer.baby/@d) of [usesthis.com](https://usesthis.com/)
## Who are you, and what do you do?
I'm a professional software engineer and once engineering manager. I work a day job but I'm also working with a good friend and former boss to bring [screen.garden](https://screen.garden), a real-time collaboration tool for PKMs and the web, to life.
In my free time I sing with a local queer TTBB chorus, play table-top RPGs, watch Formula 1, and play video games.
## What hardware do you use?
I work atop a sit-stand desk I bought when I first started working remotely in 2017. It stays in the "sit" position 99% of the time. For work I use whatever machine my employer provides. Right now that's a 14in M3 MacBook Pro. Personally, I have an M1 MacBook Air which I love. A single thunderbolt cable runs from either of those machines to a [CalDigit TS4](https://www.caldigit.com/thunderbolt-station-4/) which connects it to power, ethernet, a USB hub, and my display.
I use just the one display, a [GIGABYTE M32U](https://www.gigabyte.com/Monitor/M32U), which is a 32 inch, 4k, 144Hz monitor. Whenever someone is talking about replacing their monitor I always bring up refresh rate. It's one of those things that sounds like you wouldn't notice but it actually makes looking at a screen for most of your day a lot more pleasant. I've sat a no-name-brand monitor light and a Logitech webcam atop it.
I have a collection of mechanical keyboards ([ErgoDox EZ](https://ergodox-ez.com/), [Keyboardio Atreus](https://shop.keyboard.io/products/keyboardio-atreus), to name a couple) which all live in a drawer while I type away on my [Glorious GMMK Pro](https://www.gloriousgaming.com/products/glorious-gmmk-pro-75-barebone-black) with [Glorious Panda tactile switches](https://www.gloriousgaming.com/products/glorious-panda-mechanical-switches?variant=37691905933487). I think Glorious's branding is a bit "cringe" to say the least but they were the only custom keyboard option I could get same-day at the nearby Micro Center when I needed to replace my [Pok3r](https://drop.com/buy/vortex-poker-iii-compact-keyboard) following a coffee spill incident.
I talk to my coworkers and friends through a [Blue Yeti mic](https://www.bestbuy.com/site/blue-microphones-blue-yeti-professional-multi-pattern-usb-condenser-microphone/9737441.p?skuId=9737441) that I bought when a former employer gave everyone a couple hundred dollars for work-from-home equipment in early 2020 (despite my having already worked from my home my entire tenure there).
At the edges of my desk are piles of scrunchies, a couple hair clips, my AirPods Pros, a pair of Sennheiser HD 600s, my iPhone 14 Pro Max (I always go "Max" or "Plus" for the extra battery life), an [Aquaphor lip balm stick](https://www.aquaphorus.com/products/lip-care/lip-repair-stick), some hand lotion, and a nice candle.
Away from my desk I have a collection of cameras but the one I use the most is my Leica M6 which I usually shoot with a Voigtlander Nokton Classic 35mm f/1.4. I digitize my negatives with a beat-up Sony a6000, a cheap macro lens, and a [Valoi easy35](https://www.valoi.co/easy35).
I have a couple Apple TV 4ks to stream content from the cloud and also the Plex Media Server running on a Synology NAS. I have a couple TVs in different rooms but the Xbox Series X stays connected to the 65 inch LG C1 OLED (once again which a high refresh rate).
Finally, currently sitting on my nightstand wrapped in some FiiO IEMs is a 5th iPod Classic (aka an iPod Video) whose hard disk I've [replaced with a 512gb microSD card](https://www.iflash.xyz/). It's really incredible how well it still works.
## And what software?
These days I'm macOS all-the-way. I'm fully integrated into the ecosystem and the ergonomics and reliability of development on the platform is unparalleled in my opinion. Obviously I use a ton of software so I'll limit (mostly) to things I keep pinned to my dock (although most of the time I'm launching things from [Alfred](https://www.alfredapp.com/)):
- [Firefox](https://www.mozilla.org/en-US/firefox/new/) to browse the web
- [Fantastical](https://flexibits.com/fantastical) to manage several calendar accounts. I could just use Calendar.app but there are few features (like travel time and automatic event merging) that keep me renewing my subscription.
- Mail.app for emails...
- [Things 3](https://culturedcode.com/things/) makes sure I get things done. I switched from an Android phone to an iPhone many years ago just so I could use Things while I was away from my computer.
- [Kitty](https://sw.kovidgoyal.net/kitty/) to run all of my command line apps. I always work within a `tmux` session so my terminal emulator doesn't really matter all that much (because I'm never using tabs or splits or whatever) but Kitty is quick and the [alternative icon](https://github.com/DinkDonk/kitty-icon) I use for it is really cute. I'm a vim user (neovim really) and have been since 2015. My neovim setup could be its own post...
- Music - I switch between two libraries: 1. My local library which I sync with my iPod and 2. My iCloud, Apple Music backed library
- [Dash 6](https://kapeli.com/dash) (usually via Alfred) to quickly reference documentation. Elixir / Hex package docs support is incredible
- [Obsidian](https://obsidian.md) for personal, work, and TTRPG notes. I keep my plugins list slip with just Templater, DataView, Tasks, Periodic Notes, and of course screen.garden.
- [Readwise Reader](https://readwise.io/read) as my read-later service
I have to shout-out Lightroom with [Negative Lab Pro](https://www.negativelabpro.com/) for converting scans/photos of film negatives.
## What would be your dream setup?
I've obviously spoiled myself already so I'd keep most things the same but...
I'd love a thunderbolt KVM of some kind that would let me swap _quickly_ between machines at the press of a button. I also feel like I'd benefit from a larger desk.
I think about replacing my webcam with the Sony a6000 and replacing that with a newer, higher resolution mirrorless camera.
I'm really hoping the ARM desktop / server market continues to become more accessible to the consumer market because the Synology NAS is looking a little worse-for-wear these days. I've thought about replacing it with a custom build x64 machine but the additional power consumption and heat keep me from doing it (I'm spoiled by these Apple ARM machines...).

3
site/index.html.pm Normal file
View file

@ -0,0 +1,3 @@
#lang pollen
hey, i'm sloane! i'm a professional software engineer and an amateur musician, photographer, wife, chef, and pet mom.

3
site/index.ptree Normal file
View file

@ -0,0 +1,3 @@
#lang pollen
index.html

17
site/pollen.rkt Normal file
View file

@ -0,0 +1,17 @@
#lang racket/base
(require pollen/decode pollen/misc/tutorial txexpr)
(provide (all-defined-out))
(define site-name "sloane.sh")
(define email "sloane@fastmail.com")
(define txexpr-elements-proc decode-paragraphs)
(define string-proc (compose1 smart-quotes smart-dashes))
(define (root . elements)
(txexpr 'root empty (decode-elements elements
#:txexpr-elements-proc txexpr-elements-proc
#:string-proc string-proc)))
(module setup racket/base
(provide (all-defined-out)))

10
site/template.html.p Normal file
View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>|site-name|</title>
</head>
<body>
(->html doc)
</body>
</html>

View file

@ -1,42 +0,0 @@
defmodule SloaneSH.FrontMatterTest do
use ExUnit.Case
alias SloaneSH.FrontMatter
test "parses TOML front matter" do
document = ~S"""
+++
foo = "bar"
+++
# Hello, World!
This is a document with front matter.
"""
assert {:ok, %{foo: "bar"}, "# Hello, World!" <> _} = FrontMatter.parse(document, %{})
end
test "returns an empty map is the document doesn't have front matter" do
document = ~S"""
# Hello, World!
This is a document with front matter.
"""
assert {:ok, %{} = map, document} = FrontMatter.parse(document, %{})
assert %{} = map
end
test "errors in TOML front matter produce an error" do
document = ~S"""
+++
foo = "bar
+++
# Hello, World!
This is a document with front matter.
"""
assert {:error, _} = FrontMatter.parse(document, %{})
end
end

View file

@ -1,3 +0,0 @@
defmodule SloaneSHTest do
use ExUnit.Case
end

View file

@ -1 +0,0 @@
ExUnit.start()