<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>jakeg.dev</title>
    <link rel="self" type="application/atom+xml" href="https://jakeg.dev/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://jakeg.dev"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-11-04T00:00:00+00:00</updated>
    <id>https://jakeg.dev/atom.xml</id>
    <entry xml:lang="en">
        <title>pyZDL: a launcher for GZDoom profiles</title>
        <published>2026-11-04T00:00:00+00:00</published>
        <updated>2026-11-04T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/posts/pyzdl-gzdoom-launcher/"/>
        <id>https://jakeg.dev/posts/pyzdl-gzdoom-launcher/</id>
        
        <content type="html" xml:base="https://jakeg.dev/posts/pyzdl-gzdoom-launcher/">&lt;p&gt;pyZDL is a Python launcher for GZDoom and other Doom source ports.&lt;/p&gt;
&lt;p&gt;Each &lt;strong&gt;profile&lt;/strong&gt; bundles a source port, an IWAD, an ordered mod list, extra arguments, and optional config and save directories. Per-profile directories keep total conversions from overwriting each other&#39;s settings.&lt;/p&gt;
&lt;p&gt;Features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Searchable profile library with cover art and keyboard navigation&lt;/li&gt;
&lt;li&gt;Ordered load list with enable/disable toggles and drag-and-drop&lt;/li&gt;
&lt;li&gt;Automatic &lt;code&gt;-deh&lt;/code&gt; handling for &lt;code&gt;.deh&lt;/code&gt; and &lt;code&gt;.bex&lt;/code&gt; patches&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/pyzdl-profile.png&quot; alt=&quot;pyZDL profile tab&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;splitting-extra-arguments&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#splitting-extra-arguments&quot; aria-label=&quot;Anchor link for: splitting-extra-arguments&quot;&gt;Splitting extra arguments&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Profile arguments are typed as a single string, so the launcher has to split them the way a shell would. On Windows, &lt;code&gt;shlex&lt;/code&gt; leaves surrounding quotes in place, so they are stripped by hand:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt; split_args&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Optional&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;str&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; -&amp;gt;&lt;/span&gt;&lt;span&gt; list&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;str&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;Split an argument string, respecting quotes and multiple spaces.&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;    if&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; not&lt;/span&gt;&lt;span&gt; value&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;        return&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;    try&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        parts&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; =&lt;/span&gt;&lt;span&gt; shlex&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-12&quot;&gt; posix&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;=&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;os&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; !=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;nt&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;    except&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; ValueError&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; as&lt;/span&gt;&lt;span&gt; exc&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;        raise&lt;/span&gt;&lt;span&gt; LoaderError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;Could not parse extra arguments: &lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;{&lt;/span&gt;&lt;span&gt;exc&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; from&lt;/span&gt;&lt;span&gt; exc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;    if&lt;/span&gt;&lt;span&gt; os&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; ==&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;nt&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-3&quot;&gt;        #&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-3&quot;&gt; shlex in non-POSIX mode leaves surrounding quotes in place.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        parts&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                part&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;1&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;1&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;                if&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; len&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;part&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; &amp;gt;=&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; 2&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; and&lt;/span&gt;&lt;span&gt; part&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; ==&lt;/span&gt;&lt;span&gt; part&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;1&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; and&lt;/span&gt;&lt;span&gt; part&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; in&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;\&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;                else&lt;/span&gt;&lt;span&gt; part&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;            for&lt;/span&gt;&lt;span&gt; part&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; in&lt;/span&gt;&lt;span&gt; parts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;    return&lt;/span&gt;&lt;span&gt; parts&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Downloads:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/pyzdl/releases/download/v1.0.2/pyZDL-arm64.dmg&quot;&gt;macOS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/pyzdl/releases/download/v1.0.0/pyZDL-setup.exe&quot;&gt;windows&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/pyzdl&quot;&gt;Source on GitHub&lt;/a&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Building Resumaker, a small encrypted resume app</title>
        <published>2026-11-04T00:00:00+00:00</published>
        <updated>2026-11-04T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/posts/resumaker/"/>
        <id>https://jakeg.dev/posts/resumaker/</id>
        
        <content type="html" xml:base="https://jakeg.dev/posts/resumaker/">&lt;p&gt;Resumaker started as a tiny command-line tool: point it at a JSON file, a Go
template, and a stylesheet, and it produced a clean one-page resume. That was
enough for a while. Then I wanted to publish the code, and the data became the
problem. The resume JSON was sitting in the repository, along with every change
to my contact details and work history.&lt;/p&gt;
&lt;p&gt;I removed it from the git history. The better fix was to stop keeping the resume
in the repository at all. Resumaker became a small Go web app: an account can
hold several resume versions, edit them in the browser, and print them to PDF
without any server-side rendering pipeline.&lt;/p&gt;
&lt;h2 id=&quot;from-a-script-to-a-service&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#from-a-script-to-a-service&quot; aria-label=&quot;Anchor link for: from-a-script-to-a-service&quot;&gt;From a script to a service&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The web app is deliberately unexciting: Go&#39;s standard library for HTTP, &lt;code&gt;pgx&lt;/code&gt; for
Postgres, &lt;code&gt;golang.org/x/oauth2&lt;/code&gt; for GitHub and Google sign-in, and server-rendered
HTML with Bootstrap. There is no SPA and no build step for the frontend.&lt;/p&gt;
&lt;p&gt;Sessions are random tokens stored as hashes in Postgres. OAuth accounts are keyed
by the provider&#39;s user ID rather than email address, so two providers can never
end up sharing an account by accident. Resume content lives in encrypted
revisions, which gives the app a simple history model: every save creates a new
revision, and the latest one is what the editor loads.&lt;/p&gt;
&lt;h2 id=&quot;keeping-the-data-encrypted&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#keeping-the-data-encrypted&quot; aria-label=&quot;Anchor link for: keeping-the-data-encrypted&quot;&gt;Keeping the data encrypted&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The encryption is envelope encryption. Each user gets a random 32-byte data key.
That key is wrapped by the server&#39;s master key and stored with the account. Each
resume revision is encrypted with the user&#39;s data key using AES-256-GCM.&lt;/p&gt;
&lt;p&gt;The revision ID is used as associated data, so a ciphertext only decrypts under
the row it was written to. Moving a revision to another resume, or swapping two
revisions, makes decryption fail. The master key stays in the service environment;
losing it means losing access to every stored resume, which is the trade-off that
makes the design worth it.&lt;/p&gt;
&lt;h2 id=&quot;deleting-the-render-pipeline&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#deleting-the-render-pipeline&quot; aria-label=&quot;Anchor link for: deleting-the-render-pipeline&quot;&gt;Deleting the render pipeline&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The first version of the web app had a render queue. Saving a resume enqueued a
job, a separate worker claimed it, launched headless Chrome, printed the HTML to
PDF, encrypted the PDF, and stored it for download. It worked, and it was a lot
of machinery: a worker process, a job table, Chrome availability, timeouts,
sandboxing, and encrypted artifacts to clean up.&lt;/p&gt;
&lt;p&gt;Then I remembered that every browser already has a print dialog. The preview page
is a self-contained HTML document with a print stylesheet. The user presses the
print button, chooses &quot;Save as PDF&quot;, and gets a better result than the headless
pipeline produced. The whole queue, worker, Chrome dependency, and artifact
storage disappeared. The change deleted about 1,200 lines and added 200.&lt;/p&gt;
&lt;p&gt;The important part is CSS:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;css&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;page&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;  size&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; letter&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;  margin&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; 0.42&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;in&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;media&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; print&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;  .&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;print-toolbar&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;    display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; none&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Page size, margins, and break avoidance live in the same stylesheet as the screen
layout. The browser does the part it is already good at, and the server only has
to render HTML.&lt;/p&gt;
&lt;h2 id=&quot;editing-without-a-json-textarea&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#editing-without-a-json-textarea&quot; aria-label=&quot;Anchor link for: editing-without-a-json-textarea&quot;&gt;Editing without a JSON textarea&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The first editor was just a textarea containing the resume JSON. It was honest,
but it was not a good way to edit a resume. The current editor is a structured
form with sections for contact details, experience, education, and skills. Rows
can be dragged to reorder them, and repeated sections can be added or removed
without a round trip.&lt;/p&gt;
&lt;p&gt;A toggle switches between the form and the raw JSON. The two views stay in sync:
switching to JSON serializes the current form values, and switching back parses
the JSON into rows. If the JSON is invalid or contains fields the form cannot
represent, the editor keeps the JSON view open and shows an error instead of
quietly dropping data. The server treats whichever view was submitted as the
source of truth.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/resumaker-editor.png&quot; alt=&quot;The Resumaker editor with its Form and JSON toggle&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The resume list has the utilities I kept reaching for: preview and print,
duplicate a version to make a variant, and delete with a confirmation dialog.&lt;/p&gt;
&lt;h2 id=&quot;five-ways-to-look-serious&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#five-ways-to-look-serious&quot; aria-label=&quot;Anchor link for: five-ways-to-look-serious&quot;&gt;Five ways to look serious&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Resume templates are where the project gets to have some fun. Resumaker ships
with five styles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Classic&lt;/strong&gt; is the original LaTeX-inspired one-pager.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modern&lt;/strong&gt; is a clean sans-serif layout with a teal accent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sidebar&lt;/strong&gt; puts skills and education in a tinted rail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Comic&lt;/strong&gt; is a wholehearted Comic Sans homage, complete with a HIRE ME sticker.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dingbats&lt;/strong&gt; is the Wingdings-adjacent option: black section bars and symbol
decorations, but the words stay readable.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Classic&lt;/th&gt;&lt;th&gt;Comic&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;/images/projects/resumaker-classic.png&quot; alt=&quot;Classic resume&quot; /&gt;&lt;/td&gt;&lt;td&gt;&lt;img src=&quot;/images/projects/resumaker-comic.png&quot; alt=&quot;Comic resume&quot; /&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The rest are on the &lt;a href=&quot;/projects/resumaker/&quot;&gt;project page&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;deployment&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#deployment&quot; aria-label=&quot;Anchor link for: deployment&quot;&gt;Deployment&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The app is deployed with an Ansible playbook. It cross-compiles the Go binary on
the controller, installs PostgreSQL and nginx on the host, creates the database
and role, deploys a hardened systemd unit, and obtains a Let&#39;s Encrypt certificate
through the webroot challenge. Updating is a re-run of the playbook.&lt;/p&gt;
&lt;p&gt;That is a lot of words for &quot;a small app&quot;, but most of it is one-time setup. The
service itself is a static binary, a database, and a reverse proxy.&lt;/p&gt;
&lt;h2 id=&quot;what-i-d-add-next&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-i-d-add-next&quot; aria-label=&quot;Anchor link for: what-i-d-add-next&quot;&gt;What I&#39;d add next&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The obvious next features are custom template uploads, key rotation, and cleanup
jobs for old revisions. Custom templates are the ones I actually want, but they
need a real sandbox before I am comfortable running arbitrary template code next
to other users&#39; data.&lt;/p&gt;
&lt;p&gt;For now, Resumaker does the small thing it was supposed to do: keep a few versions
of a resume, keep them encrypted, and make printing one painless.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/resumaker&quot;&gt;Source on GitHub&lt;/a&gt; ·
&lt;a rel=&quot;external&quot; href=&quot;https://resumaker.skinet.org&quot;&gt;Live site&lt;/a&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>NGage FPGA</title>
        <published>2026-11-04T00:00:00+00:00</published>
        <updated>2026-11-04T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/ngage-fpga/"/>
        <id>https://jakeg.dev/projects/ngage-fpga/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/ngage-fpga/">&lt;p&gt;A parody product site for the fictional NEXGEN-GAGE NX-1, a reconfigurable handheld whose hardware becomes a different console each time you load a new core. It is written as a late-90s/early-2000s style page, complete with guestbook, visitor counter, and &quot;best viewed in&quot; badges.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://drproteus.github.io/ngage-fpga-site/&quot;&gt;drproteus.github.io/ngage-fpga-site&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/ngage-fpga.png&quot; alt=&quot;NX-1 handheld mockup&quot; /&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Pokémon TCG (GBC) Recomp</title>
        <published>2026-11-04T00:00:00+00:00</published>
        <updated>2026-11-04T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/pokemon-tcg-gbc-recomp/"/>
        <id>https://jakeg.dev/projects/pokemon-tcg-gbc-recomp/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/pokemon-tcg-gbc-recomp/">&lt;p&gt;A cross-platform recompilation of the Game Boy Color &lt;em&gt;Pokémon Trading Card Game&lt;/em&gt;. The project builds the game from the &lt;a rel=&quot;external&quot; href=&quot;https://github.com/pret/poketcg&quot;&gt;pret/poketcg&lt;/a&gt; disassembly, recompiles it into portable C with &lt;a rel=&quot;external&quot; href=&quot;https://github.com/arcanite24/gb-recompiled&quot;&gt;gb-recompiled&lt;/a&gt;, and runs it on an SDL2 runtime for macOS, Linux, and Windows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/pokemon-tcg-gbc-recomp&quot;&gt;drproteus/pokemon-tcg-gbc-recomp&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/pokemon-tcg-gbc-recomp.jpg&quot; alt=&quot;Pokémon Trading Card Game box art&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The longer-term goals include widescreen and other quality-of-life improvements, plus a data pipeline that lets card sets be edited as CSV or JSON and levels be edited in &lt;a rel=&quot;external&quot; href=&quot;https://www.mapeditor.org/&quot;&gt;Tiled&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;status&quot;&gt;Status&lt;/h2&gt;
&lt;p&gt;This is early-stage work. The native runtime boots and renders the title screen, and the card data tools work. Map extraction and widescreen patches are planned but not yet done.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reference ROM build (RGBDS, byte-identical): done&lt;/li&gt;
&lt;li&gt;Native recompiled runtime: boots and renders the title screen&lt;/li&gt;
&lt;li&gt;Native-patch SDK: spike proven&lt;/li&gt;
&lt;li&gt;Card extractor, compiler, and CSV/JSON converters: done, with a byte-exact round trip&lt;/li&gt;
&lt;li&gt;Card art mapping and new-card scaffold generator: done&lt;/li&gt;
&lt;li&gt;Map extractor (to Tiled) and widescreen/QoL patches: planned&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;/h2&gt;
&lt;p&gt;The project separates three concerns so each can be swapped independently:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ground truth.&lt;/strong&gt; The pret/poketcg disassembly, assembled with RGBDS, is the reference implementation and the source of truth for data formats.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recompiled runtime.&lt;/strong&gt; gb-recompiled turns the ROM into a portable C project with an SDL2 runtime. Native patches and host presentation live here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data pipeline.&lt;/strong&gt; Cards, decks, boosters, maps, scripts, and text are extracted into external formats (CSV/JSON, Tiled TMX/TSX) and compiled back into the game&#39;s native formats.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;legal&quot;&gt;Legal&lt;/h2&gt;
&lt;p&gt;This repository contains no ROM images and no extracted game data. You need your own ROM that you are legally entitled to use. The build tools check it against a known hash before producing any derived output. The extracted card data is generated locally and git-ignored.&lt;/p&gt;
&lt;p&gt;Pokémon is a trademark of Nintendo, The Pokémon Company, Creatures, and Game Freak. This project is not affiliated with or endorsed by any of them.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>pyZDL</title>
        <published>2026-11-04T00:00:00+00:00</published>
        <updated>2026-11-04T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/pyzdl/"/>
        <id>https://jakeg.dev/projects/pyzdl/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/pyzdl/">&lt;p&gt;A launcher for GZDoom and other Doom source ports. Profiles bundle a source port, IWAD, ordered mod list, and per-profile config and save directories.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/pyzdl&quot;&gt;drproteus/pyzdl&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Downloads:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/pyzdl/releases/download/v1.0.2/pyZDL-arm64.dmg&quot;&gt;macOS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/pyzdl/releases/download/v1.0.0/pyZDL-setup.exe&quot;&gt;windows&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/pyzdl-profile.png&quot; alt=&quot;pyZDL profile tab&quot; /&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Resumaker</title>
        <published>2026-11-04T00:00:00+00:00</published>
        <updated>2026-11-04T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/resumaker/"/>
        <id>https://jakeg.dev/projects/resumaker/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/resumaker/">&lt;p&gt;Resumaker is a small Go web app for keeping several versions of a resume, editing
them as a structured form or raw JSON, and printing a clean PDF from the browser.
It started as a command-line JSON-to-HTML renderer and grew into a hosted app when
I wanted to open-source the code without publishing my actual resume.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/resumaker-editor.png&quot; alt=&quot;Resumaker editor&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Each account signs in with GitHub or Google OAuth. Resume content is encrypted at
rest with AES-256-GCM, using a per-user data key wrapped by the server&#39;s master key.
There is no separate render worker and no headless browser: the print stylesheet
handles page size, margins and breaks, and the browser&#39;s print dialog produces the
PDF.&lt;/p&gt;
&lt;p&gt;Features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Structured editor with drag-and-drop reordering and a synced raw JSON view&lt;/li&gt;
&lt;li&gt;Duplicate and delete actions, with delete confirmation&lt;/li&gt;
&lt;li&gt;Five print styles: Classic, Modern, Sidebar, Comic and Dingbats&lt;/li&gt;
&lt;li&gt;PostgreSQL storage with encrypted revisions&lt;/li&gt;
&lt;li&gt;Ansible deployment with nginx and Let&#39;s Encrypt&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/resumaker-list.png&quot; alt=&quot;Resumaker resume list&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;styles&quot;&gt;Styles&lt;/h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Classic&lt;/th&gt;&lt;th&gt;Modern&lt;/th&gt;&lt;th&gt;Sidebar&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;/images/projects/resumaker-classic.png&quot; alt=&quot;Classic resume&quot; /&gt;&lt;/td&gt;&lt;td&gt;&lt;img src=&quot;/images/projects/resumaker-modern.png&quot; alt=&quot;Modern resume&quot; /&gt;&lt;/td&gt;&lt;td&gt;&lt;img src=&quot;/images/projects/resumaker-sidebar.png&quot; alt=&quot;Sidebar resume&quot; /&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Comic&lt;/th&gt;&lt;th&gt;Dingbats&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;/images/projects/resumaker-comic.png&quot; alt=&quot;Comic resume&quot; /&gt;&lt;/td&gt;&lt;td&gt;&lt;img src=&quot;/images/projects/resumaker-dingbats.png&quot; alt=&quot;Dingbats resume&quot; /&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/resumaker&quot;&gt;drproteus/resumaker&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://resumaker.skinet.org&quot;&gt;resumaker.skinet.org&lt;/a&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Soul Blade Recomp</title>
        <published>2026-11-04T00:00:00+00:00</published>
        <updated>2026-11-04T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/soul-blade-recomp/"/>
        <id>https://jakeg.dev/projects/soul-blade-recomp/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/soul-blade-recomp/">&lt;p&gt;A static recompilation of the PlayStation 1 game &lt;em&gt;Soul Blade&lt;/em&gt;. The game&#39;s MIPS code is translated into C ahead of time and built into a native executable. It is built on &lt;a rel=&quot;external&quot; href=&quot;https://github.com/mstan/psxrecomp&quot;&gt;psxrecomp&lt;/a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https://github.com/RetroPortingToolKit/recomp-ui&quot;&gt;recomp-ui&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/soul-blade-recomp&quot;&gt;drproteus/soul-blade-recomp&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Downloads:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/soul-blade-recomp/releases/tag/v0.0.2&quot;&gt;v0.0.2 for Windows (x64)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You need your own copy of the game to use this project. The disc image is not included and is never committed to the repository. Releases run on the bundled MIT-licensed OpenBIOS, so no retail BIOS is needed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/soul-blade-title.png&quot; alt=&quot;Soul Blade title screen&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;retro-launcher&quot;&gt;Retro Launcher&lt;/h2&gt;
&lt;p&gt;You can run the game standalone by downloading a release zip and pointing it at your disc. Alternatively, you can manage installs, updates, and disc and BIOS setup with &lt;a rel=&quot;external&quot; href=&quot;https://github.com/RetroPortingToolKit/Retro-Launcher&quot;&gt;Retro Launcher&lt;/a&gt;, a hub for self-compiling recomps.&lt;/p&gt;
&lt;h2 id=&quot;building&quot;&gt;Building&lt;/h2&gt;
&lt;p&gt;The generated C for the game is committed to &lt;code&gt;generated/&lt;/code&gt;, so you can build from the repository without regenerating it. To regenerate it from a disc image, initialize the submodules and run the psxrecomp CLI:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;git submodule update --init --recursive&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;./psxrecomp/tools/ci/build_emitters.sh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;python3 psxrecomp/psxrecomp_cli.py generate \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  --config game.toml --project-root . --disc disc/&amp;lt;your&amp;gt;.cue&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then configure and build with &lt;code&gt;psxrecomp_cli.py rebuild&lt;/code&gt;. The README in the repository covers the full build and release process.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Cataloguing PlayStation discs with psx-tools</title>
        <published>2026-10-17T00:00:00+00:00</published>
        <updated>2026-10-17T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/posts/psx-tools/"/>
        <id>https://jakeg.dev/posts/psx-tools/</id>
        
        <content type="html" xml:base="https://jakeg.dev/posts/psx-tools/">&lt;p&gt;Multi-disc PS1 games are easy to lose track of. &lt;code&gt;psx-tools&lt;/code&gt; is a set of small Python scripts for working with PSX ROMs.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;detect-title&lt;/code&gt; reads the Title ID from a disc image.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lookup-disc&lt;/code&gt; finds the game by Title ID using &lt;a rel=&quot;external&quot; href=&quot;https://serialstation.com/&quot;&gt;SerialStation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;m3u_gen&lt;/code&gt; writes &lt;code&gt;.m3u&lt;/code&gt; playlists for every disc set in a folder.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;finding-the-title-id&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#finding-the-title-id&quot; aria-label=&quot;Anchor link for: finding-the-title-id&quot;&gt;Finding the Title ID&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The scanner skips the first 32 KiB of the image, then reads the rest in 4 KiB chunks until a regex matches a Title ID such as &lt;code&gt;SLUS_012.51&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt; get_psx_id&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;file_path&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; str&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; -&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; str&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; |&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; None&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;    try&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;        with&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; open&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;file_path&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; as&lt;/span&gt;&lt;span&gt; fin&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            _&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; =&lt;/span&gt;&lt;span&gt; fin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;seek&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;32768&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-3&quot;&gt;  #&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-3&quot;&gt; Skip the first 32768 bytes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;            while&lt;/span&gt;&lt;span&gt; buffer&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; :=&lt;/span&gt;&lt;span&gt; fin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;read&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;4096&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                buffered&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; =&lt;/span&gt;&lt;span&gt; buffer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;decode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;ISO-8859-1&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-6&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                matches&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; =&lt;/span&gt;&lt;span&gt; title_re&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;search&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;buffered&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;                if&lt;/span&gt;&lt;span&gt; matches&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;                    return&lt;/span&gt;&lt;span&gt; matches&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;group&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;    except&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; Exception&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;        pass&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/psx-tools&quot;&gt;Source on GitHub&lt;/a&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>psx-tools</title>
        <published>2026-10-17T00:00:00+00:00</published>
        <updated>2026-10-17T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/psx-tools/"/>
        <id>https://jakeg.dev/projects/psx-tools/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/psx-tools/">&lt;p&gt;Python scripts for PSX ROMs: read Title IDs from disc images, look up games on SerialStation, and generate &lt;code&gt;.m3u&lt;/code&gt; playlists.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/psx-tools&quot;&gt;drproteus/psx-tools&lt;/a&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SirtetJS: Game Boy Tetris in Phaser</title>
        <published>2026-07-30T00:00:00+00:00</published>
        <updated>2026-07-30T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/posts/sirtetjs/"/>
        <id>https://jakeg.dev/posts/sirtetjs/</id>
        
        <content type="html" xml:base="https://jakeg.dev/posts/sirtetjs/">&lt;p&gt;SirtetJS is a Game Boy Tetris clone written in JavaScript with &lt;a rel=&quot;external&quot; href=&quot;https://phaser.io/&quot;&gt;Phaser&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rotate clockwise with &lt;code&gt;C&lt;/code&gt;, counterclockwise with &lt;code&gt;X&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Choose 1 Player on the menu and press &lt;code&gt;space&lt;/code&gt; to start. The 2-player option is not supported yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/sirtetjs-menu-3x.png&quot; alt=&quot;SirtetJS menu&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/sirtetjs-field-3x.png&quot; alt=&quot;SirtetJS playfield&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;rotation&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#rotation&quot; aria-label=&quot;Anchor link for: rotation&quot;&gt;Rotation&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Rotation is an index into a table of precomputed shapes, following the &lt;a rel=&quot;external&quot; href=&quot;https://tetris.wiki/Nintendo_Rotation_System&quot;&gt;Nintendo Rotation System&lt;/a&gt;. Each piece stores its orientations, and rotating only moves the index:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;getCWIndex&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;    return&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;index&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; +&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; %&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;rotations&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;length&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;getCCWIndex&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;    let&lt;/span&gt;&lt;span&gt; i&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;index&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; %&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;rotations&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;length&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;    if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; 0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; i&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; =&lt;/span&gt;&lt;span&gt; i&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; +&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;rotations&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;length&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;    return&lt;/span&gt;&lt;span&gt; i&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;rotateCW&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;    this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;index&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;getCWIndex&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It builds with Bun and runs from any static file server. Try the &lt;a rel=&quot;external&quot; href=&quot;https://drproteus.github.io/sirtetjs/&quot;&gt;live demo&lt;/a&gt;, or see the &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/sirtetjs&quot;&gt;source&lt;/a&gt;.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SirtetJS</title>
        <published>2026-07-30T00:00:00+00:00</published>
        <updated>2026-07-30T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/sirtetjs/"/>
        <id>https://jakeg.dev/projects/sirtetjs/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/sirtetjs/">&lt;p&gt;A Game Boy Tetris clone implemented in Phaser, built with Bun.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/sirtetjs&quot;&gt;drproteus/sirtetjs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/sirtetjs-menu-3x.png&quot; alt=&quot;SirtetJS menu&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/sirtetjs-field-3x.png&quot; alt=&quot;SirtetJS playfield&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://drproteus.github.io/sirtetjs/&quot;&gt;drproteus.github.io/sirtetjs&lt;/a&gt;&lt;/p&gt;
&lt;iframe src=&quot;https://drproteus.github.io/sirtetjs/&quot; width=&quot;100%&quot; height=&quot;500&quot; loading=&quot;lazy&quot; title=&quot;SirtetJS demo&quot;&gt;&lt;/iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>ebaysearch</title>
        <published>2026-07-05T00:00:00+00:00</published>
        <updated>2026-07-05T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/ebaysearch/"/>
        <id>https://jakeg.dev/projects/ebaysearch/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/ebaysearch/">&lt;p&gt;A work-in-progress command-line tool that scrapes eBay completed listings, so you can see what items actually sold for before pricing your own.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/ebaysearch&quot;&gt;drproteus/ebaysearch&lt;/a&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Solving Protohackers in Go</title>
        <published>2026-07-01T00:00:00+00:00</published>
        <updated>2026-07-01T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/posts/protohackers/"/>
        <id>https://jakeg.dev/posts/protohackers/</id>
        
        <content type="html" xml:base="https://jakeg.dev/posts/protohackers/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https://protohackers.com/&quot;&gt;Protohackers&lt;/a&gt; is a set of network programming challenges. Each one asks you to build a server that speaks a specific protocol correctly under load.&lt;/p&gt;
&lt;p&gt;I&#39;ve completed the first six challenges in Go. Each solved README includes its test output as proof. The code is in the &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/protohackers&quot;&gt;protohackers repo&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-challenges&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-challenges&quot; aria-label=&quot;Anchor link for: the-challenges&quot;&gt;The challenges&lt;/a&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/protohackers/tree/main/0-smoke-test&quot;&gt;Smoke Test&lt;/a&gt;&lt;/strong&gt;: a TCP echo server that handles several clients at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/protohackers/tree/main/1-prime-time&quot;&gt;Prime Time&lt;/a&gt;&lt;/strong&gt;: a JSON-over-TCP server that answers whether a number is prime.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/protohackers/tree/main/2-means-to-an-end&quot;&gt;Means to an End&lt;/a&gt;&lt;/strong&gt;: a binary protocol for storing prices by timestamp and returning their mean.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/protohackers/tree/main/3-budget-chat&quot;&gt;Budget Chat&lt;/a&gt;&lt;/strong&gt;: a multi-user chat room over TCP.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/protohackers/tree/main/4-unusual-database&quot;&gt;Unusual Database Program&lt;/a&gt;&lt;/strong&gt;: a key-value store reached over UDP.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/protohackers/tree/main/5-mob-in-the-middle&quot;&gt;Mob in the Middle&lt;/a&gt;&lt;/strong&gt;: a proxy that rewrites Boguscoin addresses in chat messages.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;prime-time&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#prime-time&quot; aria-label=&quot;Anchor link for: prime-time&quot;&gt;Prime Time&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Non-integers are never prime, and &lt;code&gt;big.ProbablyPrime(0)&lt;/code&gt; gives an exact answer for values that fit in 64 bits:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;go&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;num&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; _&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; :=&lt;/span&gt;&lt;span&gt; request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Number&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;Get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;var&lt;/span&gt;&lt;span&gt; prime&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; bool&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;if&lt;/span&gt;&lt;span&gt; math&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;Trunc&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;num&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; !=&lt;/span&gt;&lt;span&gt; num&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-3&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-3&quot;&gt; Non-integers are automatically non-prime&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    prime&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt; false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    prime&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt; =&lt;/span&gt;&lt;span&gt; big&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;NewInt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;int64&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;num&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;ProbablyPrime&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Protohackers</title>
        <published>2026-07-01T00:00:00+00:00</published>
        <updated>2026-07-01T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/protohackers/"/>
        <id>https://jakeg.dev/projects/protohackers/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/protohackers/">&lt;p&gt;Solutions to the &lt;a rel=&quot;external&quot; href=&quot;https://protohackers.com&quot;&gt;Protohackers&lt;/a&gt; network programming challenges, written mostly in Go. Each problem directory includes its own README with the problem statement and, once solved, the test output as proof.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/protohackers&quot;&gt;drproteus/protohackers&lt;/a&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Bun Phaser Template</title>
        <published>2026-06-20T00:00:00+00:00</published>
        <updated>2026-06-20T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/bun-phaser-template/"/>
        <id>https://jakeg.dev/projects/bun-phaser-template/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/bun-phaser-template/">&lt;p&gt;A starter Phaser 3 game project built with Bun. Use it as the base for a new browser game.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/bun-phaser-template&quot;&gt;drproteus/bun-phaser-template&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;bun install&lt;/code&gt;, then &lt;code&gt;bun run build&lt;/code&gt; and &lt;code&gt;bun run serve&lt;/code&gt;. The dev server runs at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/bun-phaser-template.png&quot; alt=&quot;Bun Phaser template&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://drproteus.github.io/bun-phaser-template/&quot;&gt;drproteus.github.io/bun-phaser-template&lt;/a&gt;&lt;/p&gt;
&lt;iframe src=&quot;https://drproteus.github.io/bun-phaser-template/&quot; width=&quot;100%&quot; height=&quot;500&quot; loading=&quot;lazy&quot; title=&quot;Bun Phaser Template demo&quot;&gt;&lt;/iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Bun Web App Template</title>
        <published>2026-01-28T00:00:00+00:00</published>
        <updated>2026-01-28T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/bun-app-template/"/>
        <id>https://jakeg.dev/projects/bun-app-template/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/bun-app-template/">&lt;p&gt;A starter web app built with Bun. It includes Backbone, lodash, jQuery, Normalize CSS, and Sakura CSS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/bun-app-template&quot;&gt;drproteus/bun-app-template&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;bun install&lt;/code&gt;, then &lt;code&gt;bun run build&lt;/code&gt; and &lt;code&gt;bun run serve&lt;/code&gt;. The dev server runs at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The demo uses the arrow keys to move a cat around the page.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/bun-app-template-demo.gif&quot; alt=&quot;Bun app template demo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://drproteus.github.io/bun-app-template/&quot;&gt;drproteus.github.io/bun-app-template&lt;/a&gt;&lt;/p&gt;
&lt;iframe src=&quot;https://drproteus.github.io/bun-app-template/&quot; width=&quot;100%&quot; height=&quot;500&quot; loading=&quot;lazy&quot; title=&quot;Bun Web App Template demo&quot;&gt;&lt;/iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>gthumb</title>
        <published>2026-01-28T00:00:00+00:00</published>
        <updated>2026-01-28T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/gthumb/"/>
        <id>https://jakeg.dev/projects/gthumb/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/gthumb/">&lt;p&gt;A Go tool that scans a directory for JPEG photos, applies their EXIF orientation, writes square thumbnails and display-size copies, and produces a single-page static gallery (&lt;code&gt;index.html&lt;/code&gt;) that can be hosted anywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/gthumb&quot;&gt;drproteus/gthumb&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/gthumb-metro.png&quot; alt=&quot;gthumb Metro theme&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;go run . -src examples -out public&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each directory under &lt;code&gt;-src&lt;/code&gt; (including &lt;code&gt;-src&lt;/code&gt; itself) that contains JPEG files becomes an album. The output looks like this:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;public/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  index.html&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  thumbs/&amp;lt;album&amp;gt;/&amp;lt;photo&amp;gt;.jpg   square thumbnails (center-cropped)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  photos/&amp;lt;album&amp;gt;/&amp;lt;photo&amp;gt;.jpg   display-size copies (aspect ratio kept)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main flags are &lt;code&gt;-src&lt;/code&gt;, &lt;code&gt;-out&lt;/code&gt;, &lt;code&gt;-templates&lt;/code&gt;, &lt;code&gt;-title&lt;/code&gt;, &lt;code&gt;-thumb&lt;/code&gt; (default 400px), &lt;code&gt;-max&lt;/code&gt; (default 2048px), and &lt;code&gt;-quality&lt;/code&gt; (default 80).&lt;/p&gt;
&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Orientation:&lt;/strong&gt; the EXIF &lt;code&gt;Orientation&lt;/code&gt; tag is applied, so phone photos shot in portrait come out upright.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metadata:&lt;/strong&gt; camera model and capture date are shown in each photo&#39;s caption when present. Files without EXIF data are still processed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thumbnails:&lt;/strong&gt; scaled so the shorter edge matches &lt;code&gt;-thumb&lt;/code&gt;, then center-cropped to a square.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Display images:&lt;/strong&gt; scaled to fit within &lt;code&gt;-max&lt;/code&gt; on the longest edge. Images are never upscaled.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Template:&lt;/strong&gt; &lt;code&gt;templates/index.gohtml&lt;/code&gt; is a plain &lt;code&gt;html/template&lt;/code&gt;, so the layout can be changed without rebuilding.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It uses &lt;a rel=&quot;external&quot; href=&quot;https://github.com/anthonynsimon/bild&quot;&gt;bild&lt;/a&gt; for image work and &lt;a rel=&quot;external&quot; href=&quot;https://github.com/dsoprea/go-exif&quot;&gt;go-exif&lt;/a&gt; for EXIF parsing.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>channel2600</title>
        <published>2026-01-07T00:00:00+00:00</published>
        <updated>2026-01-07T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/channel2600/"/>
        <id>https://jakeg.dev/projects/channel2600/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/channel2600/">&lt;p&gt;A 2600chan-style imageboard written in Python. Threads and images are stored through Cloudinary, with ReCaptcha on posting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/channel2600&quot;&gt;drproteus/channel2600&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/channel2600.png&quot; alt=&quot;channel2600 thread&quot; /&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Earthman64</title>
        <published>2025-12-12T00:00:00+00:00</published>
        <updated>2025-12-12T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/earthman64/"/>
        <id>https://jakeg.dev/projects/earthman64/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/earthman64/">&lt;p&gt;A Commodore 64 memorial demo written in 6502 assembly. It shows a bitmap image with a cover of Coldplay&#39;s &quot;Clocks&quot; playing from the SID chip.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/earthman64&quot;&gt;drproteus/earthman64&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/earthman64.gif&quot; alt=&quot;Earthman64 demo&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;building&quot;&gt;Building&lt;/h2&gt;
&lt;p&gt;Assemble with &lt;a rel=&quot;external&quot; href=&quot;https://dasm-assembler.github.io/&quot;&gt;dasm&lt;/a&gt;, then run the &lt;code&gt;.prg&lt;/code&gt; in &lt;a rel=&quot;external&quot; href=&quot;https://vice-emu.sourceforge.io/&quot;&gt;VICE&lt;/a&gt;. After loading, enter &lt;code&gt;sys 2064&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dasm main.asm -oearthman.prg&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;music-and-timing&quot;&gt;Music and timing&lt;/h2&gt;
&lt;p&gt;The music plays from a raster interrupt. The code points the IRQ vector at &lt;code&gt;$0314&lt;/code&gt; to &lt;code&gt;irq&lt;/code&gt;, then fires it at scanline &lt;code&gt;$7e&lt;/code&gt;. Each interrupt calls the SID player and acknowledges the VIC:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;asm&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	lda #&amp;lt;irq&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	ldx #&amp;gt;irq&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	ldy #&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;$7e&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	sta &lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;$0314&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	stx &lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;$0315&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	sty &lt;/span&gt;&lt;span&gt;$d012&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;irq&lt;/span&gt;&lt;span class=&quot;z-l-6 z-d-7&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	jsr &lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-4&quot;&gt;$1003&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	asl &lt;/span&gt;&lt;span&gt;$d019&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-10&quot;&gt;	jmp&lt;/span&gt;&lt;span&gt; $ea81&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The demo is still in progress. Scrolling text is on the to-do list.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Bun Postgres and S3 Template</title>
        <published>2025-08-30T00:00:00+00:00</published>
        <updated>2025-08-30T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/bun-pg-s3-backend/"/>
        <id>https://jakeg.dev/projects/bun-pg-s3-backend/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/bun-pg-s3-backend/">&lt;p&gt;A working prototype for storing collections of items with image attachments, using Bun with Postgres for records and an S3-compatible store for media.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/bun-pg-s3-backend&quot;&gt;drproteus/bun-pg-s3-backend&lt;/a&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>lastfm-grid</title>
        <published>2024-10-16T00:00:00+00:00</published>
        <updated>2024-10-16T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/lastfm-grid/"/>
        <id>https://jakeg.dev/projects/lastfm-grid/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/lastfm-grid/">&lt;p&gt;A Flask server that builds a cover-art grid from a Last.fm user&#39;s top albums, served as a single image.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/lastfm-grid&quot;&gt;drproteus/lastfm-grid&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://lastfm-grid.skinet.org&quot;&gt;lastfm-grid.skinet.org&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/lastfm-grid.png&quot; alt=&quot;lastfm-grid output&quot; /&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SIRTET</title>
        <published>2024-10-12T00:00:00+00:00</published>
        <updated>2024-10-12T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/sirtet/"/>
        <id>https://jakeg.dev/projects/sirtet/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/sirtet/">&lt;p&gt;A Tetris clone written in Lua for the LÖVE framework, started in 2014.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/sirtet&quot;&gt;drproteus/sirtet&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/sirtet.png&quot; alt=&quot;SIRTET screenshot&quot; /&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Cellular Automaton</title>
        <published>2023-05-31T00:00:00+00:00</published>
        <updated>2023-05-31T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/cellular-automaton/"/>
        <id>https://jakeg.dev/projects/cellular-automaton/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/cellular-automaton/">&lt;p&gt;Conway&#39;s Game of Life in vanilla JavaScript with no dependencies. An early experiment, with plans for wrapping edges and a cube-face variant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/cellular-automaton&quot;&gt;drproteus/cellular-automaton&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://static.skinet.org/cellular-automata/&quot;&gt;static.skinet.org/cellular-automata&lt;/a&gt;&lt;/p&gt;
&lt;iframe src=&quot;https://static.skinet.org/cellular-automata/&quot; width=&quot;100%&quot; height=&quot;500&quot; loading=&quot;lazy&quot; title=&quot;Cellular Automaton demo&quot;&gt;&lt;/iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>pyCHIP-8</title>
        <published>2022-11-12T00:00:00+00:00</published>
        <updated>2022-11-12T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/pychip8/"/>
        <id>https://jakeg.dev/projects/pychip8/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/pychip8/">&lt;p&gt;A CHIP-8 virtual machine interpreter written in Python.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/pychip8&quot;&gt;drproteus/pychip8&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/pychip8.png&quot; alt=&quot;pyCHIP-8 screenshot&quot; /&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>YACA</title>
        <published>2022-10-13T00:00:00+00:00</published>
        <updated>2022-10-13T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/yaca/"/>
        <id>https://jakeg.dev/projects/yaca/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/yaca/">&lt;p&gt;Yet Another Caffeine App: a small Python app that keeps your Mac awake. Packaged as a macOS &lt;code&gt;.app&lt;/code&gt; with &lt;code&gt;make build&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/yaca&quot;&gt;drproteus/yaca&lt;/a&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>bodyport</title>
        <published>2022-03-14T00:00:00+00:00</published>
        <updated>2022-03-14T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/bodyport/"/>
        <id>https://jakeg.dev/projects/bodyport/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/bodyport/">&lt;p&gt;Research into lossless compression for numerical sequences. Since no single scheme works for every data shape, this project parses the samples into 3-byte signed integers, plots them, and tests a compression approach against the shape of the data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/bodyport&quot;&gt;drproteus/bodyport&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/projects/bodyport.png&quot; alt=&quot;bodyport figure&quot; /&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>EndOS</title>
        <published>2017-01-06T00:00:00+00:00</published>
        <updated>2017-01-06T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jakeg.dev/projects/endos/"/>
        <id>https://jakeg.dev/projects/endos/</id>
        
        <content type="html" xml:base="https://jakeg.dev/projects/endos/">&lt;p&gt;A live Linux distribution for when you need a comfortable system to develop on. Built with archiso on Arch Linux and bundled with XFCE, preconfigured with a dark Adwaita theme.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/drproteus/endos&quot;&gt;drproteus/endos&lt;/a&gt;&lt;/p&gt;
</content>
        
    </entry>
</feed>
