diff --git a/lib/mix/tasks/build.ex b/lib/mix/tasks/build.ex index 382908c..04bf95b 100644 --- a/lib/mix/tasks/build.ex +++ b/lib/mix/tasks/build.ex @@ -1,5 +1,6 @@ defmodule Mix.Tasks.Build do use Mix.Task + @impl Mix.Task def run(_args) do {micro, :ok} = diff --git a/lib/personal.ex b/lib/personal.ex index 0810fc6..c5cd51d 100644 --- a/lib/personal.ex +++ b/lib/personal.ex @@ -56,12 +56,12 @@ defmodule Personal do
-

andy@andyleclair.dev$>_

+

andy@andyleclair.dev$>_

<%= render_slot(@inner_block) %>
-
diff --git a/lib/personal/application.ex b/lib/personal/application.ex index ec4c3e4..765e190 100644 --- a/lib/personal/application.ex +++ b/lib/personal/application.ex @@ -8,8 +8,8 @@ defmodule Personal.Application do @impl true def start(_type, _args) do children = [ - # Starts a worker by calling: Personal.Worker.start_link(arg) - # {Personal.Worker, arg} + {Bandit, plug: Personal.DevServer}, + {Personal.Watcher, dirs: ["./lib", "./posts"]} ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/personal/dev_server.ex b/lib/personal/dev_server.ex new file mode 100644 index 0000000..7b37c95 --- /dev/null +++ b/lib/personal/dev_server.ex @@ -0,0 +1,25 @@ +defmodule Personal.Router do + use Plug.Router + plug :match + plug :dispatch + + get "/drafts" do + send_resp(conn, 200, "drafts") + end + + match _ do + send_resp(conn, 404, "oops") + end +end + +defmodule Personal.DevServer do + use Plug.Builder + + plug Plug.Static.IndexHtml, at: "/" + + plug Plug.Static, + at: "/", + from: "./output" + + plug Personal.Router +end diff --git a/lib/personal/watcher.ex b/lib/personal/watcher.ex new file mode 100644 index 0000000..e22fb1f --- /dev/null +++ b/lib/personal/watcher.ex @@ -0,0 +1,35 @@ +defmodule Personal.Watcher do + require Logger + use GenServer + + def start_link(args) do + GenServer.start_link(__MODULE__, args) + end + + def init(args) do + {:ok, watcher_pid} = FileSystem.start_link(args) + FileSystem.subscribe(watcher_pid) + {:ok, %{watcher_pid: watcher_pid}} + end + + # These emit a LOT of events, [:modified] is fine + def handle_info( + {:file_event, watcher_pid, {path, [:modified]}}, + %{watcher_pid: watcher_pid} = state + ) do + Mix.shell().info(["File modified: #{path}"]) + Mix.shell().info(["Site Rebuilding"]) + # Currently, we're just recompiling the entire site + # TODO: make this fancier based on the path + System.cmd("mix", ["site.build"]) + Mix.shell().info([:green, "Site rebuilt"]) + {:noreply, state} + end + + def handle_info( + {:file_event, watcher_pid, {_path, _events}}, + %{watcher_pid: watcher_pid} = state + ) do + {:noreply, state} + end +end diff --git a/mix.exs b/mix.exs index e1447bb..eb4d90a 100644 --- a/mix.exs +++ b/mix.exs @@ -28,9 +28,12 @@ defmodule Personal.MixProject do {:makeup_erlang, ">= 0.0.0"}, {:nimble_publisher, "~> 1.1.0"}, {:phoenix, "~> 1.7"}, + {:bandit, "~> 1.0"}, {:phoenix_live_view, "~> 1.0"}, {:tailwind, "~> 0.2"}, - {:tzdata, "~> 1.1"} + {:tzdata, "~> 1.1"}, + {:plug_static_index_html, "~> 1.0"}, + {:file_system, "~> 1.0", only: :dev} ] end diff --git a/mix.lock b/mix.lock index 3f05b68..8669953 100644 --- a/mix.lock +++ b/mix.lock @@ -1,9 +1,12 @@ %{ + "bandit": {:hex, :bandit, "1.6.1", "9e01b93d72ddc21d8c576a704949e86ee6cde7d11270a1d3073787876527a48f", [:mix], [{:hpax, "~> 1.0", [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", "5a904bf010ea24b67979835e0507688e31ac873d4ffc8ed0e5413e8d77455031"}, "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"}, "esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "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, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, @@ -22,9 +25,11 @@ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [: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", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "thousand_island": {:hex, :thousand_island, "1.3.7", "1da7598c0f4f5f50562c097a3f8af308ded48cd35139f0e6f17d9443e4d0c9c5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0139335079953de41d381a6134d8b618d53d084f558c734f2662d1a72818dd12"}, "tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, diff --git a/output/assets/app.css b/output/assets/app.css index 51c75f8..ce147f5 100644 --- a/output/assets/app.css +++ b/output/assets/app.css @@ -1 +1 @@ -/*! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where([class~=lead]):not(:where([class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-bottom:1.2em;margin-top:1.2em}.prose :where(a):not(:where([class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose] *)){list-style-type:decimal;margin-bottom:1.25em;margin-top:1.25em;padding-left:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose] *)){list-style-type:disc;margin-bottom:1.25em;margin-top:1.25em;padding-left:1.625em}.prose :where(ol>li):not(:where([class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(hr):not(:where([class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-bottom:3em;margin-top:3em}.prose :where(blockquote):not(:where([class~=not-prose] *)){border-left-color:var(--tw-prose-quote-borders);border-left-width:.25rem;color:var(--tw-prose-quotes);font-style:italic;font-weight:500;margin-bottom:1.6em;margin-top:1.6em;padding-left:1em;quotes:"\201C""\201D""\2018""\2019"}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-size:2.25em;font-weight:800;line-height:1.1111111;margin-bottom:.8888889em;margin-top:0}.prose :where(h1 strong):not(:where([class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.5em;font-weight:700;line-height:1.3333333;margin-bottom:1em;margin-top:2em}.prose :where(h2 strong):not(:where([class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.25em;font-weight:600;line-height:1.6;margin-bottom:.6em;margin-top:1.6em}.prose :where(h3 strong):not(:where([class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;line-height:1.5;margin-bottom:.5em;margin-top:1.5em}.prose :where(h4 strong):not(:where([class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(figure>*):not(:where([class~=not-prose] *)){margin-bottom:0;margin-top:0}.prose :where(figcaption):not(:where([class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose :where(code):not(:where([class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose] *)){background-color:var(--tw-prose-pre-bg);border-radius:.375rem;color:var(--tw-prose-pre-code);font-size:.875em;font-weight:400;line-height:1.7142857;margin-bottom:1.7142857em;margin-top:1.7142857em;overflow-x:auto;padding:.8571429em 1.1428571em}.prose :where(pre code):not(:where([class~=not-prose] *)){background-color:initial;border-radius:0;border-width:0;color:inherit;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit;padding:0}.prose :where(pre code):not(:where([class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose] *)){font-size:.875em;line-height:1.7142857;margin-bottom:2em;margin-top:2em;table-layout:auto;text-align:left;width:100%}.prose :where(thead):not(:where([class~=not-prose] *)){border-bottom-color:var(--tw-prose-th-borders);border-bottom-width:1px}.prose :where(thead th):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;padding-bottom:.5714286em;padding-left:.5714286em;padding-right:.5714286em;vertical-align:bottom}.prose :where(tbody tr):not(:where([class~=not-prose] *)){border-bottom-color:var(--tw-prose-td-borders);border-bottom-width:1px}.prose :where(tbody tr:last-child):not(:where([class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose] *)){vertical-align:initial}.prose :where(tfoot):not(:where([class~=not-prose] *)){border-top-color:var(--tw-prose-th-borders);border-top-width:1px}.prose :where(tfoot td):not(:where([class~=not-prose] *)){vertical-align:top}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(p):not(:where([class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em}.prose :where(video):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(figure):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(li):not(:where([class~=not-prose] *)){margin-bottom:.5em;margin-top:.5em}.prose :where(ol>li):not(:where([class~=not-prose] *)){padding-left:.375em}.prose :where(ul>li):not(:where([class~=not-prose] *)){padding-left:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(hr+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose :where(thead th:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose] *)){padding:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.prose-sm :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.5714286em;margin-top:.5714286em}.prose-sm :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose-sm :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.prose-base :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose-base :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose-base :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose-base :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose-base :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose-base :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose-base :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.prose-lg :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.8888889em;margin-top:.8888889em}.prose-lg :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.3333333em}.prose-lg :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.3333333em}.prose-lg :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.3333333em}.prose-lg :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.3333333em}.prose-lg :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose-lg :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.prose-xl :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.8em;margin-top:.8em}.prose-xl :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.2em}.prose-xl :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.2em}.prose-xl :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.2em}.prose-xl :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.2em}.prose-xl :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose-xl :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.prose-2xl :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.8333333em;margin-top:.8333333em}.prose-2xl :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.3333333em}.prose-2xl :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.3333333em}.prose-2xl :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.3333333em}.prose-2xl :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.3333333em}.prose-2xl :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose-2xl :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.absolute{position:absolute}.relative{position:relative}.my-4{margin-bottom:1rem;margin-top:1rem}.flex{display:flex}.h-60{height:15rem}.min-h-screen{min-height:100vh}.flex-1{flex:1 1 0%}.grow{flex-grow:1}.flex-col{flex-direction:column}.items-center{align-items:center}.bg-bludacris{--tw-bg-opacity:1;background-color:rgb(44 180 255/var(--tw-bg-opacity))}.bg-nor-easter{--tw-bg-opacity:1;background-color:rgb(245 247 250/var(--tw-bg-opacity))}.p-10{padding:2.5rem}.p-4{padding:1rem}.text-center{text-align:center}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-smurf-blood{--tw-text-opacity:1;color:rgb(11 26 56/var(--tw-text-opacity))}.blink{animation:blinker 1s step-start infinite}@keyframes blinker{50%{opacity:0}}@font-face{font-family:Server Mono;font-style:normal;font-weight:400;src:url(/assets/fonts/ServerMono-Regular.otf) format("opentype");src:url(/assets/fonts/ServerMono-Regular.woff) format("woff");src:url(/assets/fonts/ServerMono-Regular.woff2) format("woff2")}@font-face{font-family:Server Mono;font-style:italic;font-weight:400;src:url(/assets/fonts/ServerMono-Regular-Italic.otf) format("opentype");src:url(/assets/fonts/ServerMono-Regular-Italic.woff) format("woff");src:url(/assets/fonts/ServerMono-Regular-Italic.woff2) format("woff2")}body{font-family:Server Mono,monospace;font-size:1rem}.makeup .hll{background-color:#4f424c}.makeup{background-color:#2f1e2e;color:#e7e9db}.makeup .unselectable{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.makeup .bp{color:#e7e9db}.makeup .c,.makeup .c1,.makeup .ch,.makeup .cm,.makeup .cp,.makeup .cpf,.makeup .cs{color:#776e71}.makeup .dl{color:#48b685}.makeup .err{color:#ef6155}.makeup .fm{color:#06b6ef}.makeup .gd{color:#ef6155}.makeup .ge{font-style:italic}.makeup .gh{color:#e7e9db;font-weight:700}.makeup .gi{color:#48b685}.makeup .gp{color:#776e71}.makeup .gp,.makeup .gs,.makeup .gu{font-weight:700}.makeup .gu{color:#5bc4bf}.makeup .il{color:#f99b15}.makeup .k,.makeup .kc,.makeup .kd{color:#815ba4}.makeup .kn{color:#5bc4bf}.makeup .kp,.makeup .kr{color:#815ba4}.makeup .kt{color:#fec418}.makeup .l,.makeup .ld,.makeup .m,.makeup .mb,.makeup .mf,.makeup .mh,.makeup .mi,.makeup .mo{color:#f99b15}.makeup .n{color:#e7e9db}.makeup .na{color:#06b6ef}.makeup .nb{color:#e7e9db}.makeup .nc{color:#fec418}.makeup .nd{color:#5bc4bf}.makeup .ne{color:#ef6155}.makeup .nf{color:#06b6ef}.makeup .ni,.makeup .nl{color:#e7e9db}.makeup .nn{color:#fec418}.makeup .no{color:#ef6155}.makeup .nt{color:#5bc4bf}.makeup .nv{color:#ef6155}.makeup .nx{color:#06b6ef}.makeup .o,.makeup .ow{color:#5bc4bf}.makeup .p,.makeup .py{color:#e7e9db}.makeup .s,.makeup .s1,.makeup .s2,.makeup .sa,.makeup .sb{color:#48b685}.makeup .sc{color:#e7e9db}.makeup .sd{color:#776e71}.makeup .se{color:#f99b15}.makeup .sh{color:#48b685}.makeup .si{color:#f99b15}.makeup .sr,.makeup .ss,.makeup .sx{color:#48b685}.makeup .vc,.makeup .vg,.makeup .vi,.makeup .vm{color:#ef6155}@media (min-width:1024px){.lg\:prose-xl{font-size:1.25rem;line-height:1.8}.lg\:prose-xl :where(p):not(:where([class~=not-prose] *)){margin-bottom:1.2em;margin-top:1.2em}.lg\:prose-xl :where([class~=lead]):not(:where([class~=not-prose] *)){font-size:1.2em;line-height:1.5;margin-bottom:1em;margin-top:1em}.lg\:prose-xl :where(blockquote):not(:where([class~=not-prose] *)){margin-bottom:1.6em;margin-top:1.6em;padding-left:1.0666667em}.lg\:prose-xl :where(h1):not(:where([class~=not-prose] *)){font-size:2.8em;line-height:1;margin-bottom:.8571429em;margin-top:0}.lg\:prose-xl :where(h2):not(:where([class~=not-prose] *)){font-size:1.8em;line-height:1.1111111;margin-bottom:.8888889em;margin-top:1.5555556em}.lg\:prose-xl :where(h3):not(:where([class~=not-prose] *)){font-size:1.5em;line-height:1.3333333;margin-bottom:.6666667em;margin-top:1.6em}.lg\:prose-xl :where(h4):not(:where([class~=not-prose] *)){line-height:1.6;margin-bottom:.6em;margin-top:1.8em}.lg\:prose-xl :where(img):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.lg\:prose-xl :where(video):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.lg\:prose-xl :where(figure):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.lg\:prose-xl :where(figure>*):not(:where([class~=not-prose] *)){margin-bottom:0;margin-top:0}.lg\:prose-xl :where(figcaption):not(:where([class~=not-prose] *)){font-size:.9em;line-height:1.5555556;margin-top:1em}.lg\:prose-xl :where(code):not(:where([class~=not-prose] *)){font-size:.9em}.lg\:prose-xl :where(h2 code):not(:where([class~=not-prose] *)){font-size:.8611111em}.lg\:prose-xl :where(h3 code):not(:where([class~=not-prose] *)){font-size:.9em}.lg\:prose-xl :where(pre):not(:where([class~=not-prose] *)){border-radius:.5rem;font-size:.9em;line-height:1.7777778;margin-bottom:2em;margin-top:2em;padding:1.1111111em 1.3333333em}.lg\:prose-xl :where(ol):not(:where([class~=not-prose] *)){margin-bottom:1.2em;margin-top:1.2em;padding-left:1.6em}.lg\:prose-xl :where(ul):not(:where([class~=not-prose] *)){margin-bottom:1.2em;margin-top:1.2em;padding-left:1.6em}.lg\:prose-xl :where(li):not(:where([class~=not-prose] *)){margin-bottom:.6em;margin-top:.6em}.lg\:prose-xl :where(ol>li):not(:where([class~=not-prose] *)){padding-left:.4em}.lg\:prose-xl :where(ul>li):not(:where([class~=not-prose] *)){padding-left:.4em}.lg\:prose-xl :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.8em;margin-top:.8em}.lg\:prose-xl :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.2em}.lg\:prose-xl :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.2em}.lg\:prose-xl :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.2em}.lg\:prose-xl :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.2em}.lg\:prose-xl :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose] *)){margin-bottom:.8em;margin-top:.8em}.lg\:prose-xl :where(hr):not(:where([class~=not-prose] *)){margin-bottom:2.8em;margin-top:2.8em}.lg\:prose-xl :where(hr+*):not(:where([class~=not-prose] *)){margin-top:0}.lg\:prose-xl :where(h2+*):not(:where([class~=not-prose] *)){margin-top:0}.lg\:prose-xl :where(h3+*):not(:where([class~=not-prose] *)){margin-top:0}.lg\:prose-xl :where(h4+*):not(:where([class~=not-prose] *)){margin-top:0}.lg\:prose-xl :where(table):not(:where([class~=not-prose] *)){font-size:.9em;line-height:1.5555556}.lg\:prose-xl :where(thead th):not(:where([class~=not-prose] *)){padding-bottom:.8888889em;padding-left:.6666667em;padding-right:.6666667em}.lg\:prose-xl :where(thead th:first-child):not(:where([class~=not-prose] *)){padding-left:0}.lg\:prose-xl :where(thead th:last-child):not(:where([class~=not-prose] *)){padding-right:0}.lg\:prose-xl :where(tbody td,tfoot td):not(:where([class~=not-prose] *)){padding:.8888889em .6666667em}.lg\:prose-xl :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose] *)){padding-left:0}.lg\:prose-xl :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose] *)){padding-right:0}.lg\:prose-xl :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.lg\:prose-xl :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}}.prose-pre\:bg-codebg :is(:where(pre):not(:where([class~=not-prose] *))){--tw-bg-opacity:1;background-color:rgb(47 30 46/var(--tw-bg-opacity))}@media (min-width:1024px){.lg\:mb-14{margin-bottom:3.5rem}.lg\:mt-10{margin-top:2.5rem}} \ No newline at end of file +/*! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where([class~=lead]):not(:where([class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-bottom:1.2em;margin-top:1.2em}.prose :where(a):not(:where([class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose] *)){list-style-type:decimal;margin-bottom:1.25em;margin-top:1.25em;padding-left:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose] *)){list-style-type:disc;margin-bottom:1.25em;margin-top:1.25em;padding-left:1.625em}.prose :where(ol>li):not(:where([class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(hr):not(:where([class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-bottom:3em;margin-top:3em}.prose :where(blockquote):not(:where([class~=not-prose] *)){border-left-color:var(--tw-prose-quote-borders);border-left-width:.25rem;color:var(--tw-prose-quotes);font-style:italic;font-weight:500;margin-bottom:1.6em;margin-top:1.6em;padding-left:1em;quotes:"\201C""\201D""\2018""\2019"}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-size:2.25em;font-weight:800;line-height:1.1111111;margin-bottom:.8888889em;margin-top:0}.prose :where(h1 strong):not(:where([class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.5em;font-weight:700;line-height:1.3333333;margin-bottom:1em;margin-top:2em}.prose :where(h2 strong):not(:where([class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.25em;font-weight:600;line-height:1.6;margin-bottom:.6em;margin-top:1.6em}.prose :where(h3 strong):not(:where([class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;line-height:1.5;margin-bottom:.5em;margin-top:1.5em}.prose :where(h4 strong):not(:where([class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(figure>*):not(:where([class~=not-prose] *)){margin-bottom:0;margin-top:0}.prose :where(figcaption):not(:where([class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose :where(code):not(:where([class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose] *)){background-color:var(--tw-prose-pre-bg);border-radius:.375rem;color:var(--tw-prose-pre-code);font-size:.875em;font-weight:400;line-height:1.7142857;margin-bottom:1.7142857em;margin-top:1.7142857em;overflow-x:auto;padding:.8571429em 1.1428571em}.prose :where(pre code):not(:where([class~=not-prose] *)){background-color:initial;border-radius:0;border-width:0;color:inherit;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit;padding:0}.prose :where(pre code):not(:where([class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose] *)){font-size:.875em;line-height:1.7142857;margin-bottom:2em;margin-top:2em;table-layout:auto;text-align:left;width:100%}.prose :where(thead):not(:where([class~=not-prose] *)){border-bottom-color:var(--tw-prose-th-borders);border-bottom-width:1px}.prose :where(thead th):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;padding-bottom:.5714286em;padding-left:.5714286em;padding-right:.5714286em;vertical-align:bottom}.prose :where(tbody tr):not(:where([class~=not-prose] *)){border-bottom-color:var(--tw-prose-td-borders);border-bottom-width:1px}.prose :where(tbody tr:last-child):not(:where([class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose] *)){vertical-align:initial}.prose :where(tfoot):not(:where([class~=not-prose] *)){border-top-color:var(--tw-prose-th-borders);border-top-width:1px}.prose :where(tfoot td):not(:where([class~=not-prose] *)){vertical-align:top}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(p):not(:where([class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em}.prose :where(video):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(figure):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(li):not(:where([class~=not-prose] *)){margin-bottom:.5em;margin-top:.5em}.prose :where(ol>li):not(:where([class~=not-prose] *)){padding-left:.375em}.prose :where(ul>li):not(:where([class~=not-prose] *)){padding-left:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(hr+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose :where(thead th:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose] *)){padding:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.prose-sm :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.5714286em;margin-top:.5714286em}.prose-sm :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose-sm :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.prose-base :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose-base :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose-base :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose-base :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose-base :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose-base :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose-base :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.prose-lg :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.8888889em;margin-top:.8888889em}.prose-lg :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.3333333em}.prose-lg :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.3333333em}.prose-lg :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.3333333em}.prose-lg :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.3333333em}.prose-lg :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose-lg :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.prose-xl :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.8em;margin-top:.8em}.prose-xl :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.2em}.prose-xl :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.2em}.prose-xl :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.2em}.prose-xl :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.2em}.prose-xl :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose-xl :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.prose-2xl :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.8333333em;margin-top:.8333333em}.prose-2xl :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.3333333em}.prose-2xl :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.3333333em}.prose-2xl :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.3333333em}.prose-2xl :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.3333333em}.prose-2xl :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose-2xl :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.absolute{position:absolute}.relative{position:relative}.my-4{margin-bottom:1rem;margin-top:1rem}.mt-24{margin-top:6rem}.flex{display:flex}.h-60{height:15rem}.min-h-screen{min-height:100vh}.flex-1{flex:1 1 0%}.grow{flex-grow:1}.flex-col{flex-direction:column}.items-center{align-items:center}.bg-bludacris{--tw-bg-opacity:1;background-color:rgb(44 180 255/var(--tw-bg-opacity))}.bg-nor-easter{--tw-bg-opacity:1;background-color:rgb(245 247 250/var(--tw-bg-opacity))}.p-10{padding:2.5rem}.p-4{padding:1rem}.text-center{text-align:center}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-smurf-blood{--tw-text-opacity:1;color:rgb(11 26 56/var(--tw-text-opacity))}.blink{animation:blinker 1s step-start infinite}@keyframes blinker{50%{opacity:0}}@font-face{font-family:Server Mono;font-style:normal;font-weight:400;src:url(/assets/fonts/ServerMono-Regular.otf) format("opentype");src:url(/assets/fonts/ServerMono-Regular.woff) format("woff");src:url(/assets/fonts/ServerMono-Regular.woff2) format("woff2")}@font-face{font-family:Server Mono;font-style:italic;font-weight:400;src:url(/assets/fonts/ServerMono-Regular-Italic.otf) format("opentype");src:url(/assets/fonts/ServerMono-Regular-Italic.woff) format("woff");src:url(/assets/fonts/ServerMono-Regular-Italic.woff2) format("woff2")}body{font-family:Server Mono,monospace;font-size:1rem}.makeup .hll{background-color:#4f424c}.makeup{background-color:#2f1e2e;color:#e7e9db}.makeup .unselectable{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.makeup .bp{color:#e7e9db}.makeup .c,.makeup .c1,.makeup .ch,.makeup .cm,.makeup .cp,.makeup .cpf,.makeup .cs{color:#776e71}.makeup .dl{color:#48b685}.makeup .err{color:#ef6155}.makeup .fm{color:#06b6ef}.makeup .gd{color:#ef6155}.makeup .ge{font-style:italic}.makeup .gh{color:#e7e9db;font-weight:700}.makeup .gi{color:#48b685}.makeup .gp{color:#776e71}.makeup .gp,.makeup .gs,.makeup .gu{font-weight:700}.makeup .gu{color:#5bc4bf}.makeup .il{color:#f99b15}.makeup .k,.makeup .kc,.makeup .kd{color:#815ba4}.makeup .kn{color:#5bc4bf}.makeup .kp,.makeup .kr{color:#815ba4}.makeup .kt{color:#fec418}.makeup .l,.makeup .ld,.makeup .m,.makeup .mb,.makeup .mf,.makeup .mh,.makeup .mi,.makeup .mo{color:#f99b15}.makeup .n{color:#e7e9db}.makeup .na{color:#06b6ef}.makeup .nb{color:#e7e9db}.makeup .nc{color:#fec418}.makeup .nd{color:#5bc4bf}.makeup .ne{color:#ef6155}.makeup .nf{color:#06b6ef}.makeup .ni,.makeup .nl{color:#e7e9db}.makeup .nn{color:#fec418}.makeup .no{color:#ef6155}.makeup .nt{color:#5bc4bf}.makeup .nv{color:#ef6155}.makeup .nx{color:#06b6ef}.makeup .o,.makeup .ow{color:#5bc4bf}.makeup .p,.makeup .py{color:#e7e9db}.makeup .s,.makeup .s1,.makeup .s2,.makeup .sa,.makeup .sb{color:#48b685}.makeup .sc{color:#e7e9db}.makeup .sd{color:#776e71}.makeup .se{color:#f99b15}.makeup .sh{color:#48b685}.makeup .si{color:#f99b15}.makeup .sr,.makeup .ss,.makeup .sx{color:#48b685}.makeup .vc,.makeup .vg,.makeup .vi,.makeup .vm{color:#ef6155}@media (min-width:1024px){.lg\:prose-xl{font-size:1.25rem;line-height:1.8}.lg\:prose-xl :where(p):not(:where([class~=not-prose] *)){margin-bottom:1.2em;margin-top:1.2em}.lg\:prose-xl :where([class~=lead]):not(:where([class~=not-prose] *)){font-size:1.2em;line-height:1.5;margin-bottom:1em;margin-top:1em}.lg\:prose-xl :where(blockquote):not(:where([class~=not-prose] *)){margin-bottom:1.6em;margin-top:1.6em;padding-left:1.0666667em}.lg\:prose-xl :where(h1):not(:where([class~=not-prose] *)){font-size:2.8em;line-height:1;margin-bottom:.8571429em;margin-top:0}.lg\:prose-xl :where(h2):not(:where([class~=not-prose] *)){font-size:1.8em;line-height:1.1111111;margin-bottom:.8888889em;margin-top:1.5555556em}.lg\:prose-xl :where(h3):not(:where([class~=not-prose] *)){font-size:1.5em;line-height:1.3333333;margin-bottom:.6666667em;margin-top:1.6em}.lg\:prose-xl :where(h4):not(:where([class~=not-prose] *)){line-height:1.6;margin-bottom:.6em;margin-top:1.8em}.lg\:prose-xl :where(img):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.lg\:prose-xl :where(video):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.lg\:prose-xl :where(figure):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.lg\:prose-xl :where(figure>*):not(:where([class~=not-prose] *)){margin-bottom:0;margin-top:0}.lg\:prose-xl :where(figcaption):not(:where([class~=not-prose] *)){font-size:.9em;line-height:1.5555556;margin-top:1em}.lg\:prose-xl :where(code):not(:where([class~=not-prose] *)){font-size:.9em}.lg\:prose-xl :where(h2 code):not(:where([class~=not-prose] *)){font-size:.8611111em}.lg\:prose-xl :where(h3 code):not(:where([class~=not-prose] *)){font-size:.9em}.lg\:prose-xl :where(pre):not(:where([class~=not-prose] *)){border-radius:.5rem;font-size:.9em;line-height:1.7777778;margin-bottom:2em;margin-top:2em;padding:1.1111111em 1.3333333em}.lg\:prose-xl :where(ol):not(:where([class~=not-prose] *)){margin-bottom:1.2em;margin-top:1.2em;padding-left:1.6em}.lg\:prose-xl :where(ul):not(:where([class~=not-prose] *)){margin-bottom:1.2em;margin-top:1.2em;padding-left:1.6em}.lg\:prose-xl :where(li):not(:where([class~=not-prose] *)){margin-bottom:.6em;margin-top:.6em}.lg\:prose-xl :where(ol>li):not(:where([class~=not-prose] *)){padding-left:.4em}.lg\:prose-xl :where(ul>li):not(:where([class~=not-prose] *)){padding-left:.4em}.lg\:prose-xl :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.8em;margin-top:.8em}.lg\:prose-xl :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.2em}.lg\:prose-xl :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.2em}.lg\:prose-xl :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.2em}.lg\:prose-xl :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.2em}.lg\:prose-xl :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose] *)){margin-bottom:.8em;margin-top:.8em}.lg\:prose-xl :where(hr):not(:where([class~=not-prose] *)){margin-bottom:2.8em;margin-top:2.8em}.lg\:prose-xl :where(hr+*):not(:where([class~=not-prose] *)){margin-top:0}.lg\:prose-xl :where(h2+*):not(:where([class~=not-prose] *)){margin-top:0}.lg\:prose-xl :where(h3+*):not(:where([class~=not-prose] *)){margin-top:0}.lg\:prose-xl :where(h4+*):not(:where([class~=not-prose] *)){margin-top:0}.lg\:prose-xl :where(table):not(:where([class~=not-prose] *)){font-size:.9em;line-height:1.5555556}.lg\:prose-xl :where(thead th):not(:where([class~=not-prose] *)){padding-bottom:.8888889em;padding-left:.6666667em;padding-right:.6666667em}.lg\:prose-xl :where(thead th:first-child):not(:where([class~=not-prose] *)){padding-left:0}.lg\:prose-xl :where(thead th:last-child):not(:where([class~=not-prose] *)){padding-right:0}.lg\:prose-xl :where(tbody td,tfoot td):not(:where([class~=not-prose] *)){padding:.8888889em .6666667em}.lg\:prose-xl :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose] *)){padding-left:0}.lg\:prose-xl :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose] *)){padding-right:0}.lg\:prose-xl :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.lg\:prose-xl :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}}.prose-pre\:bg-codebg :is(:where(pre):not(:where([class~=not-prose] *))){--tw-bg-opacity:1;background-color:rgb(47 30 46/var(--tw-bg-opacity))}@media (min-width:1024px){.lg\:mb-14{margin-bottom:3.5rem}.lg\:mt-10{margin-top:2.5rem}} \ No newline at end of file diff --git a/output/atom.xml b/output/atom.xml index a58df05..1517ca1 100644 --- a/output/atom.xml +++ b/output/atom.xml @@ -3,13 +3,20 @@ andyleclair.dev - 2024-12-30T00:00:00-05:00 + 2024-12-31T00:00:00-05:00 Andy LeClair https://andyleclair.dev/ + Dev Server + + https://andyleclair.dev/posts/2024/12-31-dev-server.html + 2024-12-31T00:00:00-05:00 + Adding a development server + + Adding RSS to a static site https://andyleclair.dev/posts/2024/12-30-adding-rss-to-a-static-site.html diff --git a/output/feed.xml b/output/feed.xml index 25440c3..d17ca31 100644 --- a/output/feed.xml +++ b/output/feed.xml @@ -6,6 +6,13 @@ https://andyleclair.dev/ + Dev Server + Adding a development server + Tue, 31 Dec 2024 00:00:00 EST + https://andyleclair.dev/posts/2024/12-31-dev-server.html + https://andyleclair.dev/posts/2024/12-31-dev-server.html + + Adding RSS to a static site Adding RSS and Atom feeds to this site Mon, 30 Dec 2024 00:00:00 EST diff --git a/output/index.html b/output/index.html index 903aa66..02d3b26 100644 --- a/output/index.html +++ b/output/index.html @@ -14,13 +14,15 @@
-

andy@andyleclair.dev$>_

+

andy@andyleclair.dev$>_

Blog!

-
+
© Andy LeClair 2024 | Atom | RSS
diff --git a/output/posts/2024/08-27-hello-world.html b/output/posts/2024/08-27-hello-world.html index 4e8402d..2f7ef77 100644 --- a/output/posts/2024/08-27-hello-world.html +++ b/output/posts/2024/08-27-hello-world.html @@ -14,7 +14,7 @@
-

andy@andyleclair.dev$>_

+

andy@andyleclair.dev$>_

@@ -36,7 +36,7 @@

Related Listening<

-
+
© Andy LeClair 2024 | Atom | RSS
diff --git a/output/posts/2024/08-28-readme.html b/output/posts/2024/08-28-readme.html index 801038f..2220a05 100644 --- a/output/posts/2024/08-28-readme.html +++ b/output/posts/2024/08-28-readme.html @@ -14,7 +14,7 @@
-

andy@andyleclair.dev$>_

+

andy@andyleclair.dev$>_

@@ -45,7 +45,7 @@

Related Listening<

-
+
© Andy LeClair 2024 | Atom | RSS
diff --git a/output/posts/2024/09-09-gltest.html b/output/posts/2024/09-09-gltest.html index 62f01df..327e56b 100644 --- a/output/posts/2024/09-09-gltest.html +++ b/output/posts/2024/09-09-gltest.html @@ -14,7 +14,7 @@
-

andy@andyleclair.dev$>_

+

andy@andyleclair.dev$>_

@@ -34,229 +34,229 @@

Related Listening<

These files are lifted directly from here

%% src/gl_const.erl
--module(gl_const).
--compile(nowarn_export_all).
--compile(export_all).
+-module(gl_const).
+-compile(nowarn_export_all).
+-compile(export_all).
 
--include_lib("wx/include/gl.hrl").
-
-gl_depth_test() -> ?GL_DEPTH_TEST.
-
-gl_lequal() -> ?GL_LEQUAL.
-gl_color_buffer_bit() -> ?GL_COLOR_BUFFER_BIT.
-
-gl_depth_buffer_bit() -> ?GL_DEPTH_BUFFER_BIT.
-
-gl_triangles() -> ?GL_TRIANGLES.
-gl_array_buffer() -> ?GL_ARRAY_BUFFER.
-gl_element_array_buffer() -> ?GL_ELEMENT_ARRAY_BUFFER.
-gl_static_draw() -> ?GL_STATIC_DRAW.
-
-gl_vertex_shader() -> ?GL_VERTEX_SHADER.
-gl_fragment_shader() -> ?GL_FRAGMENT_SHADER.
-
-gl_compile_status() -> ?GL_COMPILE_STATUS.
-gl_link_status() -> ?GL_LINK_STATUS.
-
-gl_float() -> ?GL_FLOAT.
-gl_false() -> ?GL_FALSE.
-gl_true() -> ?GL_TRUE.
-gl_unsigned_int() -> ?GL_UNSIGNED_INT.
-gl_unsigned_byte() -> ?GL_UNSIGNED_BYTE.
-
-gl_front_and_back() -> ?GL_FRONT_AND_BACK.
-gl_line() -> ?GL_LINE.
-gl_fill() -> ?GL_FILL.
-gl_debug_output() -> ?GL_DEBUG_OUTPUT.
-gl_texture_2d() -> ?GL_TEXTURE_2D.
-gl_texture_wrap_s() -> ?GL_TEXTURE_WRAP_S.
-gl_texture_wrap_t() -> ?GL_TEXTURE_WRAP_T.
-gl_texture_min_filter() -> ?GL_TEXTURE_MIN_FILTER.
-gl_texture_mag_filter() -> ?GL_TEXTURE_MAG_FILTER.
-gl_rgb() -> ?GL_RGB.
-gl_rgba() -> ?GL_RGBA.
-gl_multisample() -> ?GL_MULTISAMPLE.
-gl_luminance() -> ?GL_LUMINANCE.
-
-gl_texture0() -> ?GL_TEXTURE0.
-
-gl_cull_face() -> ?GL_CULL_FACE.
-gl_back() -> ?GL_BACK.
-gl_front() -> ?GL_FRONT.
-gl_ccw() -> ?GL_CCW.
-gl_cw() -> ?GL_CW.
-
-gl_info_log_length() -> ?GL_INFO_LOG_LENGTH.
-
-gl_blend() -> ?GL_BLEND.
-gl_src_alpha() -> ?GL_SRC_ALPHA.
-gl_one() -> ?GL_ONE.
-gl_one_minus_src_alpha() -> ?GL_ONE_MINUS_SRC_ALPHA.
-
-gl_repeat() -> ?GL_REPEAT.
-gl_linear() -> ?GL_LINEAR.
-gl_nearest() -> ?GL_NEAREST.
-
-gl_framebuffer() -> ?GL_FRAMEBUFFER.
-gl_renderbuffer() -> ?GL_RENDERBUFFER.
-gl_color_attachment0() -> ?GL_COLOR_ATTACHMENT0.
-gl_framebuffer_complete() -> ?GL_FRAMEBUFFER_COMPLETE.
-gl_read_framebuffer() -> ?GL_READ_FRAMEBUFFER.
-gl_draw_framebuffer() -> ?GL_DRAW_FRAMEBUFFER.
-
-gl_texture_env() -> ?GL_TEXTURE_ENV.
-gl_texture_env_mode() -> ?GL_TEXTURE_ENV_MODE.
-gl_replace() -> ?GL_REPLACE.
+-include_lib("wx/include/gl.hrl"). + +gl_depth_test() -> ?GL_DEPTH_TEST. + +gl_lequal() -> ?GL_LEQUAL. +gl_color_buffer_bit() -> ?GL_COLOR_BUFFER_BIT. + +gl_depth_buffer_bit() -> ?GL_DEPTH_BUFFER_BIT. + +gl_triangles() -> ?GL_TRIANGLES. +gl_array_buffer() -> ?GL_ARRAY_BUFFER. +gl_element_array_buffer() -> ?GL_ELEMENT_ARRAY_BUFFER. +gl_static_draw() -> ?GL_STATIC_DRAW. + +gl_vertex_shader() -> ?GL_VERTEX_SHADER. +gl_fragment_shader() -> ?GL_FRAGMENT_SHADER. + +gl_compile_status() -> ?GL_COMPILE_STATUS. +gl_link_status() -> ?GL_LINK_STATUS. + +gl_float() -> ?GL_FLOAT. +gl_false() -> ?GL_FALSE. +gl_true() -> ?GL_TRUE. +gl_unsigned_int() -> ?GL_UNSIGNED_INT. +gl_unsigned_byte() -> ?GL_UNSIGNED_BYTE. + +gl_front_and_back() -> ?GL_FRONT_AND_BACK. +gl_line() -> ?GL_LINE. +gl_fill() -> ?GL_FILL. +gl_debug_output() -> ?GL_DEBUG_OUTPUT. +gl_texture_2d() -> ?GL_TEXTURE_2D. +gl_texture_wrap_s() -> ?GL_TEXTURE_WRAP_S. +gl_texture_wrap_t() -> ?GL_TEXTURE_WRAP_T. +gl_texture_min_filter() -> ?GL_TEXTURE_MIN_FILTER. +gl_texture_mag_filter() -> ?GL_TEXTURE_MAG_FILTER. +gl_rgb() -> ?GL_RGB. +gl_rgba() -> ?GL_RGBA. +gl_multisample() -> ?GL_MULTISAMPLE. +gl_luminance() -> ?GL_LUMINANCE. + +gl_texture0() -> ?GL_TEXTURE0. + +gl_cull_face() -> ?GL_CULL_FACE. +gl_back() -> ?GL_BACK. +gl_front() -> ?GL_FRONT. +gl_ccw() -> ?GL_CCW. +gl_cw() -> ?GL_CW. + +gl_info_log_length() -> ?GL_INFO_LOG_LENGTH. + +gl_blend() -> ?GL_BLEND. +gl_src_alpha() -> ?GL_SRC_ALPHA. +gl_one() -> ?GL_ONE. +gl_one_minus_src_alpha() -> ?GL_ONE_MINUS_SRC_ALPHA. + +gl_repeat() -> ?GL_REPEAT. +gl_linear() -> ?GL_LINEAR. +gl_nearest() -> ?GL_NEAREST. + +gl_framebuffer() -> ?GL_FRAMEBUFFER. +gl_renderbuffer() -> ?GL_RENDERBUFFER. +gl_color_attachment0() -> ?GL_COLOR_ATTACHMENT0. +gl_framebuffer_complete() -> ?GL_FRAMEBUFFER_COMPLETE. +gl_read_framebuffer() -> ?GL_READ_FRAMEBUFFER. +gl_draw_framebuffer() -> ?GL_DRAW_FRAMEBUFFER. + +gl_texture_env() -> ?GL_TEXTURE_ENV. +gl_texture_env_mode() -> ?GL_TEXTURE_ENV_MODE. +gl_replace() -> ?GL_REPLACE.

 %% src/wx_const.erl
--module(wx_const).
--compile(nowarn_export_all).
--compile(export_all).
+-module(wx_const).
+-compile(nowarn_export_all).
+-compile(export_all).
 
--include_lib("wx/include/wx.hrl").
+-include_lib("wx/include/wx.hrl").
 
-wx_id_any() -> ?wxID_ANY.
-wx_gl_rgba() -> ?WX_GL_RGBA.
+wx_id_any() -> ?wxID_ANY.
+wx_gl_rgba() -> ?WX_GL_RGBA.
 
-wx_gl_doublebuffer() -> ?WX_GL_DOUBLEBUFFER.
-wx_gl_depth_size() -> ?WX_GL_DEPTH_SIZE.
-wx_gl_forward_compat() -> ?WX_GL_FORWARD_COMPAT.
+wx_gl_doublebuffer() -> ?WX_GL_DOUBLEBUFFER.
+wx_gl_depth_size() -> ?WX_GL_DEPTH_SIZE.
+wx_gl_forward_compat() -> ?WX_GL_FORWARD_COMPAT.
 
-wxk_left() -> ?WXK_LEFT.
-wxk_right() -> ?WXK_RIGHT.
-wxk_up() -> ?WXK_UP.
-wxk_down() -> ?WXK_DOWN.
-wxk_space() -> ?WXK_SPACE.
-wxk_raw_control() -> ?WXK_RAW_CONTROL.
+wxk_left() -> ?WXK_LEFT.
+wxk_right() -> ?WXK_RIGHT.
+wxk_up() -> ?WXK_UP.
+wxk_down() -> ?WXK_DOWN.
+wxk_space() -> ?WXK_SPACE.
+wxk_raw_control() -> ?WXK_RAW_CONTROL.
 
-wx_gl_major_version() -> ?WX_GL_MAJOR_VERSION.
+wx_gl_major_version() -> ?WX_GL_MAJOR_VERSION.
 
-wx_gl_minor_version() -> ?WX_GL_MINOR_VERSION.
+wx_gl_minor_version() -> ?WX_GL_MINOR_VERSION.
 
-wx_gl_core_profile() -> ?WX_GL_CORE_PROFILE.
-wx_gl_sample_buffers() -> ?WX_GL_SAMPLE_BUFFERS.
+wx_gl_core_profile() -> ?WX_GL_CORE_PROFILE.
+wx_gl_sample_buffers() -> ?WX_GL_SAMPLE_BUFFERS.
 
-wx_gl_samples() -> ?WX_GL_SAMPLES.
+wx_gl_samples() -> ?WX_GL_SAMPLES.
 
-wx_null_cursor() -> ?wxNullCursor.
-wx_cursor_blank() -> ?wxCURSOR_BLANK.
-wx_cursor_cross() -> ?wxCURSOR_CROSS.
+wx_null_cursor() -> ?wxNullCursor.
+wx_cursor_blank() -> ?wxCURSOR_BLANK.
+wx_cursor_cross() -> ?wxCURSOR_CROSS.
 
-wx_fontfamily_default() -> ?wxFONTFAMILY_DEFAULT.
-wx_fontfamily_teletype() -> ?wxFONTFAMILY_TELETYPE.
-wx_normal() -> ?wxNORMAL.
-wx_fontstyle_normal() -> ?wxFONTSTYLE_NORMAL.
-wx_fontweight_bold() -> ?wxFONTWEIGHT_BOLD.
-wx_fontweight_normal() -> ?wxFONTWEIGHT_NORMAL.
+wx_fontfamily_default() -> ?wxFONTFAMILY_DEFAULT. +wx_fontfamily_teletype() -> ?wxFONTFAMILY_TELETYPE. +wx_normal() -> ?wxNORMAL. +wx_fontstyle_normal() -> ?wxFONTSTYLE_NORMAL. +wx_fontweight_bold() -> ?wxFONTWEIGHT_BOLD. +wx_fontweight_normal() -> ?wxFONTWEIGHT_NORMAL.

Also add wx to extra_applications in mix.exs:

-
  def application do
-    [
-      extra_applications: [:logger, :wx]
-    ]
-  end
+
  def application do
+    [
+      extra_applications: [:logger, :wx]
+    ]
+  end

For reference, this is the link to the Erlang WX documentation: https://www.erlang.org/doc/apps/wx/chapter.html

Following along here with Ian’s Triangle post, we will also add the records from wx.hrl to our project:

-
defmodule WxRecords do
+
defmodule WxRecords do
   require Record
 
-  for {type, record} <- Record.extract_all(from_lib: "wx/include/wx.hrl") do
-    Record.defrecord(type, record)
-  end
-end
+
for {type, record} <- Record.extract_all(from_lib: "wx/include/wx.hrl") do + Record.defrecord(type, record) + end +end

Finally, we can add a module to render our window!

-
defmodule GlTest.Window do
+
defmodule GlTest.Window do
   import WxRecords
 
   @behaviour :wx_object
 
-  def start_link(_) do
-    :wx_object.start_link(__MODULE__, [], [])
-    {:ok, self()}
-  end
+  def start_link(_) do
+    :wx_object.start_link(__MODULE__, [], [])
+    {:ok, self()}
+  end
 
   @impl :wx_object
-  def init(_) do
-    opts = [size: {800, 600}]
-    wx = :wx.new()
-    frame = :wxFrame.new(wx, :wx_const.wx_id_any(), ~c"Hello", opts)
-    :wxWindow.connect(frame, :close_window)
-
-    :wxFrame.show(frame)
-
-    gl_attrib = [
-      attribList: [
-        :wx_const.wx_gl_core_profile(),
-        :wx_const.wx_gl_major_version(),
+  def init(_) do
+    opts = [size: {800, 600}]
+    wx = :wx.new()
+    frame = :wxFrame.new(wx, :wx_const.wx_id_any(), ~c"Hello", opts)
+    :wxWindow.connect(frame, :close_window)
+
+    :wxFrame.show(frame)
+
+    gl_attrib = [
+      attribList: [
+        :wx_const.wx_gl_core_profile(),
+        :wx_const.wx_gl_major_version(),
         3,
-        :wx_const.wx_gl_minor_version(),
+        :wx_const.wx_gl_minor_version(),
         3,
-        :wx_const.wx_gl_doublebuffer(),
+        :wx_const.wx_gl_doublebuffer(),
         0
-      ]
-    ]
+      ]
+    ]
 
-    canvas = :wxGLCanvas.new(frame, opts ++ gl_attrib)
-    ctx = :wxGLContext.new(canvas)
-    :wxGLCanvas.setCurrent(canvas, ctx)
+    canvas = :wxGLCanvas.new(frame, opts ++ gl_attrib)
+    ctx = :wxGLContext.new(canvas)
+    :wxGLCanvas.setCurrent(canvas, ctx)
 
-    send(self(), :update)
+    send(self(), :update)
 
-    {frame,
-     %{
+    {frame,
+     %{
        frame: frame,
        canvas: canvas
-     }}
-  end
+     }}
+  end
 
   @impl :wx_object
-  def handle_event(wx(event: wxClose()), state) do
-    {:stop, :normal, state}
-  end
+  def handle_event(wx(event: wxClose()), state) do
+    {:stop, :normal, state}
+  end
 
   @impl :wx_object
-  def handle_info(:stop, %{canvas: canvas} = state) do
-    :wxGLCanvas.destroy(canvas)
+  def handle_info(:stop, %{canvas: canvas} = state) do
+    :wxGLCanvas.destroy(canvas)
 
-    {:stop, :normal, state}
-  end
+    {:stop, :normal, state}
+  end
 
   @impl :wx_object
-  def handle_info(:update, state) do
-    render(state)
+  def handle_info(:update, state) do
+    render(state)
 
-    {:noreply, state}
-  end
+    {:noreply, state}
+  end
 
-  defp render(%{canvas: canvas} = state) do
-    draw(state)
-    :wxGLCanvas.swapBuffers(canvas)
-    send(self(), :update)
+  defp render(%{canvas: canvas} = state) do
+    draw(state)
+    :wxGLCanvas.swapBuffers(canvas)
+    send(self(), :update)
 
     :ok
-  end
+  end
 
-  defp draw(%{canvas: _canvas} = _state) do
-    :gl.clearColor(0.2, 0.1, 0.3, 1.0)
-    :gl.clear(:gl_const.gl_color_buffer_bit())
+  defp draw(%{canvas: _canvas} = _state) do
+    :gl.clearColor(0.2, 0.1, 0.3, 1.0)
+    :gl.clear(:gl_const.gl_color_buffer_bit())
 
     :ok
-  end
+  end
 
-  def child_spec(opts) do
-    %{
+  def child_spec(opts) do
+    %{
       id: __MODULE__,
-      start: {__MODULE__, :start_link, [opts]},
+      start: {__MODULE__, :start_link, [opts]},
       restart: :permanent
-    }
-  end
-end
+
} + end +end

And then we can add it to our application.ex and fire up iex -S mix:

-
defmodule Gltest.Application do
+
defmodule Gltest.Application do
   # See https://hexdocs.pm/elixir/Application.html
   # for more information on OTP Applications
   @moduledoc false
@@ -264,17 +264,17 @@ 

Related Listening< use Application @impl true - def start(_type, _args) do - children = [ + def start(_type, _args) do + children = [ GlTest.Window - ] + ] # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options - opts = [strategy: :one_for_one, name: Gltest.Supervisor] - Supervisor.start_link(children, opts) - end -end

+
opts = [strategy: :one_for_one, name: Gltest.Supervisor] + Supervisor.start_link(children, opts) + end +end

It’s important to note that we’re returning {:ok, self()} from start_link/1, because if you just return the value of :wx_object.start_link/3 the process will crash, because what that @@ -290,7 +290,7 @@

Related Listening<

-
+
© Andy LeClair 2024 | Atom | RSS
diff --git a/output/posts/2024/09-10-opengl-part-2.html b/output/posts/2024/09-10-opengl-part-2.html index a95cf0e..46701c3 100644 --- a/output/posts/2024/09-10-opengl-part-2.html +++ b/output/posts/2024/09-10-opengl-part-2.html @@ -14,7 +14,7 @@
-

andy@andyleclair.dev$>_

+

andy@andyleclair.dev$>_

@@ -29,53 +29,53 @@

Related Listening< The journey of a thousand triangles begins with a single triangle.

This post is basically porting the C++ from this learnopengl.com chapter to Elixir.

-
defmodule GlTest.Window do
+
defmodule GlTest.Window do
   import WxRecords
 
   @behaviour :wx_object
 
-  def start_link(_) do
-    :wx_object.start_link(__MODULE__, [], [])
-    {:ok, self()}
-  end
+  def start_link(_) do
+    :wx_object.start_link(__MODULE__, [], [])
+    {:ok, self()}
+  end
 
   @impl :wx_object
-  def init(_) do
-    opts = [size: {800, 600}]
-    wx = :wx.new()
-    frame = :wxFrame.new(wx, :wx_const.wx_id_any(), ~c"Hello", opts)
-    :wxWindow.connect(frame, :close_window)
-
-    :wxFrame.show(frame)
-
-    gl_attrib = [
-      attribList: [
-        :wx_const.wx_gl_core_profile(),
-        :wx_const.wx_gl_major_version(),
+  def init(_) do
+    opts = [size: {800, 600}]
+    wx = :wx.new()
+    frame = :wxFrame.new(wx, :wx_const.wx_id_any(), ~c"Hello", opts)
+    :wxWindow.connect(frame, :close_window)
+
+    :wxFrame.show(frame)
+
+    gl_attrib = [
+      attribList: [
+        :wx_const.wx_gl_core_profile(),
+        :wx_const.wx_gl_major_version(),
         3,
-        :wx_const.wx_gl_minor_version(),
+        :wx_const.wx_gl_minor_version(),
         3,
-        :wx_const.wx_gl_doublebuffer(),
+        :wx_const.wx_gl_doublebuffer(),
         0
-      ]
-    ]
+      ]
+    ]
 
-    canvas = :wxGLCanvas.new(frame, opts ++ gl_attrib)
-    ctx = :wxGLContext.new(canvas)
-    :wxGLCanvas.setCurrent(canvas, ctx)
+    canvas = :wxGLCanvas.new(frame, opts ++ gl_attrib)
+    ctx = :wxGLContext.new(canvas)
+    :wxGLCanvas.setCurrent(canvas, ctx)
 
-    {shader_program, vao} = init_opengl()
+    {shader_program, vao} = init_opengl()
 
-    send(self(), :update)
+    send(self(), :update)
 
-    {frame,
-     %{
+    {frame,
+     %{
        frame: frame,
        canvas: canvas,
        shader_program: shader_program,
        vao: vao
-     }}
-  end
+     }}
+  end
 
   @vertex_source """
                  #version 330 core
@@ -84,7 +84,7 @@ 

Related Listening< gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); }\0 """ - |> String.to_charlist() + |> String.to_charlist() @fragment_source """ #version 330 core @@ -93,27 +93,27 @@

Related Listening< FragColor = vec4(0.44f, 0.35f, 0.5f, 1.0f); }\0 """ - |> String.to_charlist() + |> String.to_charlist() - def init_opengl() do - vertex_shader = :gl.createShader(:gl_const.gl_vertex_shader()) - :gl.shaderSource(vertex_shader, [@vertex_source]) - :gl.compileShader(vertex_shader) + def init_opengl() do + vertex_shader = :gl.createShader(:gl_const.gl_vertex_shader()) + :gl.shaderSource(vertex_shader, [@vertex_source]) + :gl.compileShader(vertex_shader) - fragment_shader = :gl.createShader(:gl_const.gl_fragment_shader()) - :gl.shaderSource(fragment_shader, [@fragment_source]) - :gl.compileShader(fragment_shader) + fragment_shader = :gl.createShader(:gl_const.gl_fragment_shader()) + :gl.shaderSource(fragment_shader, [@fragment_source]) + :gl.compileShader(fragment_shader) - shader_program = :gl.createProgram() - :gl.attachShader(shader_program, vertex_shader) - :gl.attachShader(shader_program, fragment_shader) - :gl.linkProgram(shader_program) + shader_program = :gl.createProgram() + :gl.attachShader(shader_program, vertex_shader) + :gl.attachShader(shader_program, fragment_shader) + :gl.linkProgram(shader_program) - :gl.deleteShader(vertex_shader) - :gl.deleteShader(fragment_shader) + :gl.deleteShader(vertex_shader) + :gl.deleteShader(fragment_shader) vertices = - [ + [ -0.5, -0.5, 0.0, @@ -123,88 +123,88 @@

Related Listening< 0.0, 0.5, 0.0 - ] - |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(32)>> end) + ] + |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(32)>> end) - [vao] = :gl.genVertexArrays(1) - [vbo] = :gl.genBuffers(1) + [vao] = :gl.genVertexArrays(1) + [vbo] = :gl.genBuffers(1) - :gl.bindVertexArray(vao) + :gl.bindVertexArray(vao) - :gl.bindBuffer(:gl_const.gl_array_buffer(), vbo) + :gl.bindBuffer(:gl_const.gl_array_buffer(), vbo) - :gl.bufferData( - :gl_const.gl_array_buffer(), - byte_size(vertices), + :gl.bufferData( + :gl_const.gl_array_buffer(), + byte_size(vertices), vertices, - :gl_const.gl_static_draw() - ) + :gl_const.gl_static_draw() + ) - :gl.vertexAttribPointer( + :gl.vertexAttribPointer( 0, 3, - :gl_const.gl_float(), - :gl_const.gl_false(), - 3 * byte_size(<<0.0::float-size(32)>>), + :gl_const.gl_float(), + :gl_const.gl_false(), + 3 * byte_size(<<0.0::float-size(32)>>), 0 - ) + ) - :gl.enableVertexAttribArray(0) + :gl.enableVertexAttribArray(0) - :gl.bindBuffer(:gl_const.gl_array_buffer(), 0) + :gl.bindBuffer(:gl_const.gl_array_buffer(), 0) - :gl.bindVertexArray(0) + :gl.bindVertexArray(0) - {shader_program, vao} - end + {shader_program, vao} + end @impl :wx_object - def handle_event(wx(event: wxClose()), state) do - {:stop, :normal, state} - end + def handle_event(wx(event: wxClose()), state) do + {:stop, :normal, state} + end @impl :wx_object - def handle_info(:stop, %{canvas: canvas} = state) do - :wxGLCanvas.destroy(canvas) + def handle_info(:stop, %{canvas: canvas} = state) do + :wxGLCanvas.destroy(canvas) - {:stop, :normal, state} - end + {:stop, :normal, state} + end @impl :wx_object - def handle_info(:update, state) do - render(state) + def handle_info(:update, state) do + render(state) - {:noreply, state} - end + {:noreply, state} + end - defp render(%{canvas: canvas} = state) do - draw(state) - :wxGLCanvas.swapBuffers(canvas) - send(self(), :update) + defp render(%{canvas: canvas} = state) do + draw(state) + :wxGLCanvas.swapBuffers(canvas) + send(self(), :update) :ok - end + end - defp draw(%{canvas: _canvas} = state) do - :gl.clearColor(0.2, 0.1, 0.3, 1.0) - :gl.clear(:gl_const.gl_color_buffer_bit()) + defp draw(%{canvas: _canvas} = state) do + :gl.clearColor(0.2, 0.1, 0.3, 1.0) + :gl.clear(:gl_const.gl_color_buffer_bit()) - :gl.useProgram(state.shader_program) + :gl.useProgram(state.shader_program) - :gl.bindVertexArray(state.vao) - :gl.drawArrays(:gl_const.gl_triangles(), 0, 3) + :gl.bindVertexArray(state.vao) + :gl.drawArrays(:gl_const.gl_triangles(), 0, 3) :ok - end + end - def child_spec(opts) do - %{ + def child_spec(opts) do + %{ id: __MODULE__, - start: {__MODULE__, :start_link, [opts]}, + start: {__MODULE__, :start_link, [opts]}, restart: :permanent - } - end -end

+
} + end +end

Boy, that is a LOT just to render a dang ol triangle. OpenGL doesn’t let you get anything for cheap, huh. Regardless, it’s pretty cool that I can get this to render on my machine with no @@ -218,7 +218,7 @@

Related Listening<

-
+
© Andy LeClair 2024 | Atom | RSS
diff --git a/output/posts/2024/09-11-opengl-part-3.html b/output/posts/2024/09-11-opengl-part-3.html index 7b63a39..14794b2 100644 --- a/output/posts/2024/09-11-opengl-part-3.html +++ b/output/posts/2024/09-11-opengl-part-3.html @@ -14,7 +14,7 @@
-

andy@andyleclair.dev$>_

+

andy@andyleclair.dev$>_

@@ -25,49 +25,49 @@

Related Listening<

Posted on 2024-09-11

Continuing on, I wanted to try the exercises at the bottom of this chapter, and this is what I came up with.

-
defmodule GlTest.Window do
+
defmodule GlTest.Window do
   import WxRecords
 
   @behaviour :wx_object
 
-  def start_link(_) do
-    :wx_object.start_link(__MODULE__, [], [])
-    {:ok, self()}
-  end
+  def start_link(_) do
+    :wx_object.start_link(__MODULE__, [], [])
+    {:ok, self()}
+  end
 
   @impl :wx_object
-  def init(_) do
-    opts = [size: {800, 600}]
-    wx = :wx.new()
-    frame = :wxFrame.new(wx, :wx_const.wx_id_any(), ~c"Hello", opts)
-
-    :wxWindow.connect(frame, :close_window)
-    :wxFrame.show(frame)
-
-    gl_attrib = [
-      attribList: [
-        :wx_const.wx_gl_core_profile(),
-        :wx_const.wx_gl_major_version(),
+  def init(_) do
+    opts = [size: {800, 600}]
+    wx = :wx.new()
+    frame = :wxFrame.new(wx, :wx_const.wx_id_any(), ~c"Hello", opts)
+
+    :wxWindow.connect(frame, :close_window)
+    :wxFrame.show(frame)
+
+    gl_attrib = [
+      attribList: [
+        :wx_const.wx_gl_core_profile(),
+        :wx_const.wx_gl_major_version(),
         3,
-        :wx_const.wx_gl_minor_version(),
+        :wx_const.wx_gl_minor_version(),
         3,
-        :wx_const.wx_gl_doublebuffer(),
+        :wx_const.wx_gl_doublebuffer(),
         0
-      ]
-    ]
+      ]
+    ]
 
-    canvas = :wxGLCanvas.new(frame, opts ++ gl_attrib)
-    ctx = :wxGLContext.new(canvas)
-    :wxGLCanvas.setCurrent(canvas, ctx)
+    canvas = :wxGLCanvas.new(frame, opts ++ gl_attrib)
+    ctx = :wxGLContext.new(canvas)
+    :wxGLCanvas.setCurrent(canvas, ctx)
 
-    {shader_program, vao1, vao2, rect_vao} = init_opengl()
-    frame_counter = :counters.new(1, [:atomics])
+    {shader_program, vao1, vao2, rect_vao} = init_opengl()
+    frame_counter = :counters.new(1, [:atomics])
 
-    send(self(), :update)
-    now = System.monotonic_time(:millisecond)
+    send(self(), :update)
+    now = System.monotonic_time(:millisecond)
 
-    {frame,
-     %{
+    {frame,
+     %{
        last_time: now,
        frame: frame,
        frame_counter: frame_counter,
@@ -77,8 +77,8 @@ 

Related Listening< vao1: vao1, vao2: vao2, rect_vao: rect_vao - }} - end + }} + end @vertex_source """ #version 330 core @@ -87,7 +87,7 @@

Related Listening< gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); }\0 """ - |> String.to_charlist() + |> String.to_charlist() @fragment_source """ #version 330 core @@ -96,216 +96,216 @@

Related Listening< FragColor = vec4(0.44f, 0.35f, 0.5f, 1.0f); }\0 """ - |> String.to_charlist() + |> String.to_charlist() - def init_opengl() do - vertex_shader = :gl.createShader(:gl_const.gl_vertex_shader()) - :gl.shaderSource(vertex_shader, [@vertex_source]) - :gl.compileShader(vertex_shader) + def init_opengl() do + vertex_shader = :gl.createShader(:gl_const.gl_vertex_shader()) + :gl.shaderSource(vertex_shader, [@vertex_source]) + :gl.compileShader(vertex_shader) - fragment_shader = :gl.createShader(:gl_const.gl_fragment_shader()) - :gl.shaderSource(fragment_shader, [@fragment_source]) - :gl.compileShader(fragment_shader) + fragment_shader = :gl.createShader(:gl_const.gl_fragment_shader()) + :gl.shaderSource(fragment_shader, [@fragment_source]) + :gl.compileShader(fragment_shader) - shader_program = :gl.createProgram() - :gl.attachShader(shader_program, vertex_shader) - :gl.attachShader(shader_program, fragment_shader) - :gl.linkProgram(shader_program) + shader_program = :gl.createProgram() + :gl.attachShader(shader_program, vertex_shader) + :gl.attachShader(shader_program, fragment_shader) + :gl.linkProgram(shader_program) - :gl.deleteShader(vertex_shader) - :gl.deleteShader(fragment_shader) + :gl.deleteShader(vertex_shader) + :gl.deleteShader(fragment_shader) - vertices = triangle_vertices() - vertices_2 = triangle_vertices_2() + vertices = triangle_vertices() + vertices_2 = triangle_vertices_2() - [vao1, vao2, rect_vao] = :gl.genVertexArrays(3) - [vbo1, vbo2, rect_vbo, ebo] = :gl.genBuffers(4) + [vao1, vao2, rect_vao] = :gl.genVertexArrays(3) + [vbo1, vbo2, rect_vbo, ebo] = :gl.genBuffers(4) - for {vertex_array, vertex_buffer, vertices} <- [ - {vao1, vbo1, vertices}, - {vao2, vbo2, vertices_2} - ] do - :gl.bindVertexArray(vertex_array) + for {vertex_array, vertex_buffer, vertices} <- [ + {vao1, vbo1, vertices}, + {vao2, vbo2, vertices_2} + ] do + :gl.bindVertexArray(vertex_array) - :gl.bindBuffer(:gl_const.gl_array_buffer(), vertex_buffer) + :gl.bindBuffer(:gl_const.gl_array_buffer(), vertex_buffer) - :gl.bufferData( - :gl_const.gl_array_buffer(), - byte_size(vertices), + :gl.bufferData( + :gl_const.gl_array_buffer(), + byte_size(vertices), vertices, - :gl_const.gl_static_draw() - ) + :gl_const.gl_static_draw() + ) - :gl.vertexAttribPointer( + :gl.vertexAttribPointer( 0, 3, - :gl_const.gl_float(), - :gl_const.gl_false(), - 3 * byte_size(<<0.0::float-size(32)>>), + :gl_const.gl_float(), + :gl_const.gl_false(), + 3 * byte_size(<<0.0::float-size(32)>>), 0 - ) + ) - :gl.enableVertexAttribArray(0) + :gl.enableVertexAttribArray(0) - :gl.bindBuffer(:gl_const.gl_array_buffer(), 0) + :gl.bindBuffer(:gl_const.gl_array_buffer(), 0) - :gl.bindVertexArray(0) - end + :gl.bindVertexArray(0) + end - rect_vertices = rectangle_vertices() - rect_indices = rectangle_indices() + rect_vertices = rectangle_vertices() + rect_indices = rectangle_indices() - :gl.bindVertexArray(rect_vao) - :gl.bindBuffer(:gl_const.gl_array_buffer(), rect_vbo) + :gl.bindVertexArray(rect_vao) + :gl.bindBuffer(:gl_const.gl_array_buffer(), rect_vbo) - :gl.bufferData( - :gl_const.gl_array_buffer(), - byte_size(rect_vertices), + :gl.bufferData( + :gl_const.gl_array_buffer(), + byte_size(rect_vertices), rect_vertices, - :gl_const.gl_static_draw() - ) + :gl_const.gl_static_draw() + ) - :gl.bindBuffer(:gl_const.gl_element_array_buffer(), ebo) + :gl.bindBuffer(:gl_const.gl_element_array_buffer(), ebo) - :gl.bufferData( - :gl_const.gl_element_array_buffer(), - byte_size(rect_indices), + :gl.bufferData( + :gl_const.gl_element_array_buffer(), + byte_size(rect_indices), rect_indices, - :gl_const.gl_static_draw() - ) + :gl_const.gl_static_draw() + ) - :gl.vertexAttribPointer( + :gl.vertexAttribPointer( 0, 3, - :gl_const.gl_float(), - :gl_const.gl_false(), - 3 * byte_size(<<0.0::float-size(32)>>), + :gl_const.gl_float(), + :gl_const.gl_false(), + 3 * byte_size(<<0.0::float-size(32)>>), 0 - ) - - :gl.enableVertexAttribArray(0) - {shader_program, vao1, vao2, rect_vao} - end - - @triangle_vertices [ - [0.0, 1.0, 0.0], - [1.0, 0.0, 0.0], - [1.0, 1.0, 0.0] - ] - |> List.flatten() - |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(32)>> end) - def triangle_vertices do + ) + + :gl.enableVertexAttribArray(0) + {shader_program, vao1, vao2, rect_vao} + end + + @triangle_vertices [ + [0.0, 1.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 1.0, 0.0] + ] + |> List.flatten() + |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(32)>> end) + def triangle_vertices do @triangle_vertices - end - - @triangle_vertices_2 [ - [-0.5, -0.5, 0.0], - [0.5, -0.5, 0.0], - [0.0, 0.5, 0.0] - ] - |> List.flatten() - |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(32)>> end) - def triangle_vertices_2 do + end + + @triangle_vertices_2 [ + [-0.5, -0.5, 0.0], + [0.5, -0.5, 0.0], + [0.0, 0.5, 0.0] + ] + |> List.flatten() + |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(32)>> end) + def triangle_vertices_2 do @triangle_vertices_2 - end - - @rectangle_vertices [ - [0.5, 0.5, 0.0], - [0.5, -0.5, 0.0], - [-0.5, -0.5, 0.0], - [-0.5, 0.5, 0.0] - ] - |> List.flatten() - |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(32)>> end) - - def rectangle_vertices do + end + + @rectangle_vertices [ + [0.5, 0.5, 0.0], + [0.5, -0.5, 0.0], + [-0.5, -0.5, 0.0], + [-0.5, 0.5, 0.0] + ] + |> List.flatten() + |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(32)>> end) + + def rectangle_vertices do @rectangle_vertices - end + end - @rectangle_indices [[0, 1, 3], [1, 2, 3]] - |> List.flatten() - |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::native-size(32)>> end) - def rectangle_indices do + @rectangle_indices [[0, 1, 3], [1, 2, 3]] + |> List.flatten() + |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::native-size(32)>> end) + def rectangle_indices do @rectangle_indices - end + end @impl :wx_object - def handle_event(wx(event: wxClose()), state) do - {:stop, :normal, state} - end + def handle_event(wx(event: wxClose()), state) do + {:stop, :normal, state} + end @impl :wx_object - def handle_info(:stop, %{canvas: canvas, fps_counter_label: fps_counter_label} = state) do - :wxGLCanvas.destroy(canvas) - :wxStaticText.destroy(fps_counter_label) + def handle_info(:stop, %{canvas: canvas, fps_counter_label: fps_counter_label} = state) do + :wxGLCanvas.destroy(canvas) + :wxStaticText.destroy(fps_counter_label) - {:stop, :normal, state} - end + {:stop, :normal, state} + end @impl :wx_object - def handle_info(:update, state) do - state = render(state) + def handle_info(:update, state) do + state = render(state) - {:noreply, state} - end + {:noreply, state} + end - defp render(%{canvas: canvas} = state) do + defp render(%{canvas: canvas} = state) do state = state - |> update_frame_counter() - |> draw() + |> update_frame_counter() + |> draw() - :wxGLCanvas.swapBuffers(canvas) - send(self(), :update) + :wxGLCanvas.swapBuffers(canvas) + send(self(), :update) state - end + end - defp draw(%{frame: frame} = state) do - :gl.clearColor(0.2, 0.1, 0.3, 1.0) - :gl.clear(:gl_const.gl_color_buffer_bit()) + defp draw(%{frame: frame} = state) do + :gl.clearColor(0.2, 0.1, 0.3, 1.0) + :gl.clear(:gl_const.gl_color_buffer_bit()) - :gl.useProgram(state.shader_program) + :gl.useProgram(state.shader_program) - :gl.bindVertexArray(state.vao1) - :gl.drawArrays(:gl_const.gl_triangles(), 0, 3) + :gl.bindVertexArray(state.vao1) + :gl.drawArrays(:gl_const.gl_triangles(), 0, 3) - :gl.bindVertexArray(state.vao2) - :gl.drawArrays(:gl_const.gl_triangles(), 0, 3) + :gl.bindVertexArray(state.vao2) + :gl.drawArrays(:gl_const.gl_triangles(), 0, 3) - :gl.polygonMode(:gl_const.gl_front_and_back(), :gl_const.gl_line()) - :gl.bindVertexArray(state.rect_vao) - :gl.drawElements(:gl_const.gl_triangles(), 6, :gl_const.gl_unsigned_int(), 0) - :gl.polygonMode(:gl_const.gl_front_and_back(), :gl_const.gl_fill()) + :gl.polygonMode(:gl_const.gl_front_and_back(), :gl_const.gl_line()) + :gl.bindVertexArray(state.rect_vao) + :gl.drawElements(:gl_const.gl_triangles(), 6, :gl_const.gl_unsigned_int(), 0) + :gl.polygonMode(:gl_const.gl_front_and_back(), :gl_const.gl_fill()) - :wxWindow.setLabel(frame, ~c"FPS: #{state.fps}") + :wxWindow.setLabel(frame, ~c"FPS: #{state.fps}") state - end + end - def update_frame_counter(%{last_time: last_time, frame_counter: frame_counter} = state) do - now = System.monotonic_time(:millisecond) + def update_frame_counter(%{last_time: last_time, frame_counter: frame_counter} = state) do + now = System.monotonic_time(:millisecond) elapsed = now - last_time - if elapsed > 100 do - frames = :counters.get(frame_counter, 1) - fps = (frames / elapsed * 1000) |> round() - :counters.put(frame_counter, 1, 0) - Map.merge(state, %{fps: fps, last_time: now}) - else - :counters.add(frame_counter, 1, 1) + if elapsed > 100 do + frames = :counters.get(frame_counter, 1) + fps = (frames / elapsed * 1000) |> round() + :counters.put(frame_counter, 1, 0) + Map.merge(state, %{fps: fps, last_time: now}) + else + :counters.add(frame_counter, 1, 1) state - end - end + end + end - def child_spec(opts) do - %{ + def child_spec(opts) do + %{ id: __MODULE__, - start: {__MODULE__, :start_link, [opts]}, + start: {__MODULE__, :start_link, [opts]}, restart: :permanent - } - end -end

+
} + end +end

I was reading some of the related links and I saw this article with a bit about adding a FPS counter, and I thought that would be a fun exercise to try.

@@ -321,7 +321,7 @@

Related Listening<

-
+
© Andy LeClair 2024 | Atom | RSS
diff --git a/output/posts/2024/10-28-live-fridge.html b/output/posts/2024/10-28-live-fridge.html index fdfa8ba..d6e2729 100644 --- a/output/posts/2024/10-28-live-fridge.html +++ b/output/posts/2024/10-28-live-fridge.html @@ -14,7 +14,7 @@
-

andy@andyleclair.dev$>_

+

andy@andyleclair.dev$>_

@@ -42,7 +42,7 @@

+

diff --git a/output/posts/2024/11-15-how-to-do-a-preload-good.html b/output/posts/2024/11-15-how-to-do-a-preload-good.html index b8ba784..dc053d3 100644 --- a/output/posts/2024/11-15-how-to-do-a-preload-good.html +++ b/output/posts/2024/11-15-how-to-do-a-preload-good.html @@ -14,7 +14,7 @@
-

andy@andyleclair.dev$>_

+

andy@andyleclair.dev$>_

@@ -36,51 +36,51 @@

How do you solve it? More functions!

The Solution

-
def get_thing(id, opts \\ []) do
-  from(t in Thing, where: t.id == ^id)
-  |> preload(opts[:preload])
-  |> Repo.one()
-end
+
def get_thing(id, opts \\ []) do
+  from(t in Thing, where: t.id == ^id)
+  |> preload(opts[:preload])
+  |> Repo.one()
+end
 
-defp preload(query), do: preload(query, true)
-defp preload(query, nil), do: query
+defp preload(query), do: preload(query, true)
+defp preload(query, nil), do: query
 
-defp preload(query, true) do
+defp preload(query, true) do
   from q in query, 
-    left_join: t in assoc(q, :thing),
-    left_join: s in assoc(t, :sub_thing),
-    preload: [
-      thing: {t, [sub_thing: s]}
-    ],
-    order_by: [asc: t.index]
+    left_join: t in assoc(q, :thing),
+    left_join: s in assoc(t, :sub_thing),
+    preload: [
+      thing: {t, [sub_thing: s]}
+    ],
+    order_by: [asc: t.index]
 ]
-end
+end
 
-defp preload(query, preloads) do
+defp preload(query, preloads) do
   from q in query, preload: ^preloads
-end
+
end

Edit: I made a mistake here originally. Thanks to @AtomKirk for pointing it out!

What I had originally was:

-
def preload(query, true) do
-  from q in query, preload: [
+
def preload(query, true) do
+  from q in query, preload: [
     :association,
-    other_assoc: [:sub_assoc]
-  ]
-end
+
other_assoc: [:sub_assoc] + ] +end

However, this is incorrect. The correct way to do this is to use the left_join with assoc functions.

What I described originally was how Ash does it. In Ash, you’d do something like:

-
Ash.get!(Thing, id, load: [:association, other_assoc: [:sub_assoc]])
+
Ash.get!(Thing, id, load: [:association, other_assoc: [:sub_assoc]])

For a more explicit example, check out this code here

-
+
© Andy LeClair 2024 | Atom | RSS
diff --git a/output/posts/2024/12-30-adding-rss-to-a-static-site.html b/output/posts/2024/12-30-adding-rss-to-a-static-site.html index 51ff531..8b49b64 100644 --- a/output/posts/2024/12-30-adding-rss-to-a-static-site.html +++ b/output/posts/2024/12-30-adding-rss-to-a-static-site.html @@ -14,7 +14,7 @@
-

andy@andyleclair.dev$>_

+

andy@andyleclair.dev$>_

@@ -39,7 +39,7 @@

Related Listening<

-
+
© Andy LeClair 2024 | Atom | RSS
diff --git a/output/posts/2024/12-31-dev-server.html b/output/posts/2024/12-31-dev-server.html new file mode 100644 index 0000000..eafaaf0 --- /dev/null +++ b/output/posts/2024/12-31-dev-server.html @@ -0,0 +1,145 @@ + + + + + + + + + Dev Server + + + + + +
+
+

andy@andyleclair.dev$>_

+
+
+ +
+

Dev Server

+

Adding a development server

+

Related Listening

+

Posted on 2024-12-31

+

+A minor annoyance while developing this site is that I’ve not really had a good way to iterate on the markup of the pages, nor a good way to preview how a post will look before I publish it. Annoying!

+

+What I’ve been doing is just a string of fish commands to rebuild the whole site and then start a python server in the /output directory. Let’s do better!

+

+First, we’ll pull in Bandit to serve the stuff, and then we’ll pull in FileSystem to watch for changes.

+
{:bandit, "~> 1.0"},
+{:plug_static_index_html, "~> 1.0"},
+{:file_system, "~> 1.0", only: :dev}
+

+I’m using plug_static_index_html but it hasn’t been updated in years, and it’s one file, I could just pull it in. It needs to be updated and emits a warning. Maybe the author will take a PR?

+

+I’ve added this Plug which handles serving the static files and the index.html file. It would be nice if Plug.Static could do that automatically. There have been closed PRs that would do it, but apparently it’s not wanted? Fair enough.

+
defmodule Personal.Router do
+  use Plug.Router
+  plug :match
+  plug :dispatch
+
+  get "/drafts" do
+    send_resp(conn, 200, "drafts")
+  end
+
+  match _ do
+    send_resp(conn, 404, "oops")
+  end
+end
+
+defmodule Personal.DevServer do
+  use Plug.Builder
+
+  plug Plug.Static.IndexHtml, at: "/"
+
+  plug Plug.Static,
+    at: "/",
+    from: "./output"
+
+  plug Personal.Router
+end
+
+

+There’s a stub for serving drafts from the /drafts folder, but I can live without it for now. Perfect is the enemy of good, after all.

+

+Then we add the Filesystem watcher:

+
defmodule Personal.Watcher do
+  require Logger
+  use GenServer
+
+  def start_link(args) do
+    GenServer.start_link(__MODULE__, args)
+  end
+
+  def init(args) do
+    {:ok, watcher_pid} = FileSystem.start_link(args)
+    FileSystem.subscribe(watcher_pid)
+    {:ok, %{watcher_pid: watcher_pid}}
+  end
+
+  # These emit a LOT of events, [:modified] is fine
+  def handle_info(
+        {:file_event, watcher_pid, {path, [:modified]}},
+        %{watcher_pid: watcher_pid} = state
+      ) do
+    Mix.shell().info(["File modified: #{path}"])
+    Mix.shell().info(["Site Rebuilding"])
+    # Currently, we're just recompiling the entire site
+    # TODO: make this fancier based on the path
+    System.cmd("mix", ["site.build"])
+    Mix.shell().info([:green, "Site rebuilt"])
+    {:noreply, state}
+    {:noreply, state}
+  end
+
+  def handle_info(
+        {:file_event, watcher_pid, {_path, _events}},
+        %{watcher_pid: watcher_pid} = state
+      ) do
+    {:noreply, state}
+  end
+end
+
+

+I’m planning to get a bit fancier with this, currently I’m just rebuilding the whole site when any file changes. That’s fine for now, but I’d like to get to a point where I could rebuild just one post at a time, for example.

+

+Something else I ran into, you can’t use Mix.Task.run in the watcher, because the posts are stored in a module attribute and I don’t know how to force a recompile of the module. I think I could use Code.compile_file but that seems like a problem for another day.

+

+Lastly, we need to start the watcher and the server:

+
defmodule Personal.Application do
+  # See https://hexdocs.pm/elixir/Application.html
+  # for more information on OTP Applications
+  @moduledoc false
+
+  use Application
+
+  @impl true
+  def start(_type, _args) do
+    children = [
+      {Bandit, plug: Personal.DevServer},
+      {Personal.Watcher, dirs: ["./lib", "./posts"]}
+    ]
+
+    # See https://hexdocs.pm/elixir/Supervisor.html
+    # for other strategies and supported options
+    opts = [strategy: :one_for_one, name: Personal.Supervisor]
+    Supervisor.start_link(children, opts)
+  end
+end
+

+That’s it! Now I can run iex -S mix and have a dev server that watches the filesystem for changes.

+

+Feel free to check out the PR here

+ +
+ +
+ +
+ + \ No newline at end of file diff --git a/posts/2024/12-31-dev-server.md b/posts/2024/12-31-dev-server.md new file mode 100644 index 0000000..10c7294 --- /dev/null +++ b/posts/2024/12-31-dev-server.md @@ -0,0 +1,130 @@ +%{ + title: "Dev Server", + description: "Adding a development server", + author: "Andy LeClair", + tags: ["elixir", "meta"], + related_listening: "https://www.youtube.com/watch?v=SJnfdEX0QWk", +} +--- + +A minor annoyance while developing this site is that I've not really had a good way to iterate on the markup of the pages, nor a good way to preview how a post will look before I publish it. Annoying! + +What I've been doing is just a string of `fish` commands to rebuild the whole site and then start a python server in the `/output` directory. Let's do better! + +First, we'll pull in [Bandit](https://github.com/mtrudel/bandit) to serve the stuff, and then we'll pull in [FileSystem](https://github.com/falood/file_system) to watch for changes. + +```elixir +{:bandit, "~> 1.0"}, +{:plug_static_index_html, "~> 1.0"}, +{:file_system, "~> 1.0", only: :dev} +``` + +I'm using `plug_static_index_html` but it hasn't been updated in years, and it's one file, I could just pull it in. It needs to be updated and emits a warning. Maybe the author will take a PR? + +I've added this `Plug` which handles serving the static files and the `index.html` file. It would be nice if `Plug.Static` could do that automatically. There have been closed PRs that would do it, but apparently it's not wanted? Fair enough. + +```elixir +defmodule Personal.Router do + use Plug.Router + plug :match + plug :dispatch + + get "/drafts" do + send_resp(conn, 200, "drafts") + end + + match _ do + send_resp(conn, 404, "oops") + end +end + +defmodule Personal.DevServer do + use Plug.Builder + + plug Plug.Static.IndexHtml, at: "/" + + plug Plug.Static, + at: "/", + from: "./output" + + plug Personal.Router +end + +``` + +There's a stub for serving drafts from the `/drafts` folder, but I can live without it for now. Perfect is the enemy of good, after all. + +Then we add the Filesystem watcher: + +```elixir +defmodule Personal.Watcher do + require Logger + use GenServer + + def start_link(args) do + GenServer.start_link(__MODULE__, args) + end + + def init(args) do + {:ok, watcher_pid} = FileSystem.start_link(args) + FileSystem.subscribe(watcher_pid) + {:ok, %{watcher_pid: watcher_pid}} + end + + # These emit a LOT of events, [:modified] is fine + def handle_info( + {:file_event, watcher_pid, {path, [:modified]}}, + %{watcher_pid: watcher_pid} = state + ) do + Mix.shell().info(["File modified: #{path}"]) + Mix.shell().info(["Site Rebuilding"]) + # Currently, we're just recompiling the entire site + # TODO: make this fancier based on the path + System.cmd("mix", ["site.build"]) + Mix.shell().info([:green, "Site rebuilt"]) + {:noreply, state} + {:noreply, state} + end + + def handle_info( + {:file_event, watcher_pid, {_path, _events}}, + %{watcher_pid: watcher_pid} = state + ) do + {:noreply, state} + end +end + +``` + +I'm planning to get a bit fancier with this, currently I'm just rebuilding the whole site when any file changes. That's fine for now, but I'd like to get to a point where I could rebuild just one post at a time, for example. + +Something else I ran into, you can't use `Mix.Task.run` in the watcher, because the posts are stored in a module attribute and I don't know how to force a recompile of the module. I think I could use `Code.compile_file` but that seems like a problem for another day. + +Lastly, we need to start the watcher and the server: + +```elixir +defmodule Personal.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + {Bandit, plug: Personal.DevServer}, + {Personal.Watcher, dirs: ["./lib", "./posts"]} + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: Personal.Supervisor] + Supervisor.start_link(children, opts) + end +end +``` + +That's it! Now I can run `iex -S mix` and have a dev server that watches the filesystem for changes. + +Feel free to check out the PR [here]()