<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.2.2">Jekyll</generator><link href="https://parkerhiggins.net/feed.xml" rel="self" type="application/atom+xml" /><link href="https://parkerhiggins.net/" rel="alternate" type="text/html" /><updated>2026-03-27T20:52:25-04:00</updated><id>https://parkerhiggins.net/feed.xml</id><title type="html">parker higgins dot net</title><subtitle>parker higgins is an artist and activist in brooklyn, new york</subtitle><author><name>Parker Higgins</name></author><entry><title type="html">Crossword: “Rain sounds”</title><link href="https://parkerhiggins.net/2025/10/crossword-rain-sounds/" rel="alternate" type="text/html" title="Crossword: “Rain sounds”" /><published>2025-10-27T00:00:00-04:00</published><updated>2025-10-27T00:00:00-04:00</updated><id>https://parkerhiggins.net/2025/10/crossword-rain-sounds</id><content type="html" xml:base="https://parkerhiggins.net/2025/10/crossword-rain-sounds/"><![CDATA[<p>I’m publishing a crossword on this blog for the first time — I hope you enjoy it. I’ve added some notes below the puzzle, offset with some space to avoid any spoilers.</p>

<p><br /></p>

<iframe height="700px" width="100%" allow="web-share; fullscreen" style="border:none; width: 100% !important; position: static;display: block !important; margin: 0 !important;" src="https://puzzleme.amuselabs.com/pmm/crossword?id=2a2a3482&amp;set=1a7a5fb6891fe61d7f2f9efa85707d976a08e2f5cf89711981cc5db6a8c3e809&amp;embed=1" aria-label="Puzzle Me Game"> </iframe>

<p>S</p>

<p>P</p>

<p>O</p>

<p>I</p>

<p>L</p>

<p>E</p>

<p>R</p>

<p>S</p>

<p>below!</p>

<p>I’ve wanted to make this puzzle for a very long time, but I couldn’t figure out any way to make the theme something that could run in a mainstream publcation. Finally I decided that this was how I wanted to do it and I’d just share it myself, which feels a bit exciting. I’ve long admired the crossword bloggers of the world; maybe now I can start to count myself among them.</p>

<p><a href="https://en.wikipedia.org/wiki/Waters_of_March">The song at the heart of this puzzle</a> is very beautiful, and if you haven’t heard it I recommend listening in both <a href="https://www.youtube.com/watch?v=mcERxtlRPQo">English</a> and <a href="https://www.youtube.com/watch?v=wBEesrdaRog">Portuguese</a>. I’ve always loved the <a href="https://www.youtube.com/watch?v=3A3W5v1dNNI">version by Art Garfunkel</a> and had a very special experience one time seeing <a href="https://www.youtube.com/watch?v=giaL9u0eDG0">Sondre Lerche perform it live</a>. (That’s not the exact performance I saw but I think it was the same tour.) For a laugh, the <a href="https://www.youtube.com/watch?v=ryVsU-5z3aE">1980s Coca-Cola jingle version</a> of it is charmingly insane.</p>

<p>Making the verse into a theme felt like a small act of translation, and it’s always been interesting to me that Jobim wrote both sets of lyrics himself. It’s especially interesting that his translation was also a localization: the English lyrics are written for audiences in the northen hemisphere, who may associate March with rebirth and springtime. In English, it’s about the end of frost and new life, and the images take on a new meaning.</p>

<p>I’ve had occasion to think a lot this year about endings and beginnings, and it felt good to engage with a work that addresses them so beautifully. I hope you enjoyed the puzzle.</p>]]></content><author><name>Parker Higgins</name></author><category term="crossword" /><summary type="html"><![CDATA[I’m publishing a crossword on this blog for the first time — I hope you enjoy it. I’ve added some notes below the puzzle, offset with some space to avoid any spoilers.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://parkerhiggins.net/assets/images/crossword-beat-drops.png" /><media:content medium="image" url="https://parkerhiggins.net/assets/images/crossword-beat-drops.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Fill Harmonics, the crossword puzzle music machine</title><link href="https://parkerhiggins.net/2025/09/fill-harmonics-crossword-music-machine/" rel="alternate" type="text/html" title="Fill Harmonics, the crossword puzzle music machine" /><published>2025-09-09T00:00:00-04:00</published><updated>2025-09-09T00:00:00-04:00</updated><id>https://parkerhiggins.net/2025/09/fill-harmonics-crossword-music-machine</id><content type="html" xml:base="https://parkerhiggins.net/2025/09/fill-harmonics-crossword-music-machine/"><![CDATA[<p>Today I’m releasing <a href="https://fillharmonics.com/">Fill Harmonics</a>, a music machine that takes its rules and styling from the world of crossword puzzles. The name is a pun: in crossword lingo, “fill” is the stuff that goes into the grid. This project is a collaboration with Natan Last, one of my cruciverbal inspirations and the author of “Across The Universe,” a very fun upcoming book on the history and culture of crosswords (<a href="https://www.penguinrandomhouse.com/books/723796/across-the-universe-by-natan-last/">available for pre-order now!</a>).</p>

<p>See an example grid creation:</p>

<video controls="" width="100%">
	<source src="/assets/images/fill-harmonics/simple-creation.mp4" type="video/mp4" />
</video>

<p>We got started on Fill Harmonics towards <a href="/2025/06/recurse-center-return-statement/">the end of my batch at Recurse Center</a>. From a tech perspective, it was a fun opportunity to continue building in Javascript, and combine some recent areas of interest: in particular, the CSS grid data structure work I’d done for <a href="/2025/06/new-website-for-malaika/">Malaika’s personal page</a> and the web audio libraries I’d explored for my <a href="/2025/06/generating-infinite-sea-shanty-in-the-browser/">infinite sea shanty</a>. Similarly, this one is written entirely in vanilla Javascript, though I think at some point it exceeded the complexity where that was a good idea.</p>

<p>Natan and I are both fans of <a href="https://10kdrummachines.com/">Max Neely-Cohen’s 10,000 Drum Machines project</a>, and we wanted to build something that could be a part of it. Our shared interest in crosswords was a natural starting point, and we pretty quickly landed on the overlap between sequencing instruments and crossword aesthetics.</p>

<p>As we continued to build, we took that early concept and pushed it out in some fun directions. I think the combination of synthesis for melody and sampling for the drums works really well and allows for pretty elaborate musical phrases to emerge from simple components.</p>

<p>Perhaps the most fun addition was “word play mode,” which changes the sequencer loop from operating simultaneously across each row to instead looping over every “across entry” in the grid. (It makes a little more sense when you see it in action, but it’s still a little brain-bendy.) That mode emerged out of a conversation with <a href="https://www.gagekrause.com/">fellow Recurser Gage Krause</a>, who is working on some <a href="https://www.gagekrause.com/projects/sealion">extremely cool and polished drum machine stuff</a> right now.</p>

<p>The musical effect of word mode is called <a href="https://www.ethanhein.com/wp/2023/polymeter-vs-polyrhythm/">polymeter, a term that lives in the shadow of its more famous cousin polyrhythm</a>. Polyrhythm takes a measure and divides it with two or more sets of even subdivisions; polymeter, by contrast, keeps each beat the same size but loops them across measures of different lengths. The neat thing about it is that you can easily create phased patterns that only repeat at their least common multiple number of beats.</p>

<p>Keeping the tempo the same across grid mode and word mode means that you can alternate between the two for a neat musical effect:</p>

<video controls="" width="100%">
	<source src="/assets/images/fill-harmonics/switch-between-playback-modes.mp4" type="video/mp4" />
</video>

<p>One other early musical decision that paid off nicely was making the grid always square and configurable between 8 and 16 units wide. That means that the biggest and smallest grids feel like pretty comfortable 4/4 patterns that are easy to alternate between.</p>

<p>Because the grid is always square, and expanding it adds lower notes to the bottom, you can grab the slider for a cool effect that cuts the loop in half and drops the bass out entirely.</p>

<video controls="" width="100%">
	<source src="/assets/images/fill-harmonics/switch-between-sizes.mp4" type="video/mp4" />
</video>

<p>Please send along any sounds you make with <a href="https://fillharmonics.com">Fill Harmonics</a>! We’ve had so much fun making this thing and I can’t wait to see it in other people’s hands.</p>]]></content><author><name>Parker Higgins</name></author><category term="programming" /><category term="crossword" /><summary type="html"><![CDATA[Today I’m releasing Fill Harmonics, a music machine that takes its rules and styling from the world of crossword puzzles. The name is a pun: in crossword lingo, “fill” is the stuff that goes into the grid. This project is a collaboration with Natan Last, one of my cruciverbal inspirations and the author of “Across The Universe,” a very fun upcoming book on the history and culture of crosswords (available for pre-order now!).]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://parkerhiggins.net/assets/images/fill-harmonics/fill-harmonics.png" /><media:content medium="image" url="https://parkerhiggins.net/assets/images/fill-harmonics/fill-harmonics.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Cascading GitHub Action workflows for rebuilding static sites</title><link href="https://parkerhiggins.net/2025/07/cascading-github-action-workflows-for-static-sites/" rel="alternate" type="text/html" title="Cascading GitHub Action workflows for rebuilding static sites" /><published>2025-07-01T00:00:00-04:00</published><updated>2025-07-01T00:00:00-04:00</updated><id>https://parkerhiggins.net/2025/07/cascading-github-action-workflows-for-static-sites</id><content type="html" xml:base="https://parkerhiggins.net/2025/07/cascading-github-action-workflows-for-static-sites/"><![CDATA[<p>A common pattern for building static sites on GitHub Pages (or similar) is to just trigger a new build every time there’s a change in the repository. This is very straightforward and well-tested, and works until you want to also trigger builds when something changes <em>outside</em> the repo. Then you run into some issues — but the solution, which took me some time to hunt down, turns out to be simple. The trick is specifying the right <code class="language-plaintext highlighter-rouge">ref</code> in your build workflow, and I explain why below.</p>

<p>In my case, there’s a <code class="language-plaintext highlighter-rouge">build-and-deploy</code> workflow that’s triggered on every new push to <code class="language-plaintext highlighter-rouge">main</code>. Then there’s also a <code class="language-plaintext highlighter-rouge">fetch-new-data</code> workflow that runs on a cron schedule and checks an external data source for updates. (Conveniently, <a href="https://parkerhiggins.net/2025/05/google-sheets-database-for-projects/">that external data source can be a Google Sheet</a>.) If there is new data, it’s formatted correctly and committed to the right place in the repo.</p>

<p>At first I thought that would be the whole story, because the new commit would automatically kick off a new build process. That didn’t happen. It turns out GitHub Actions has a <a href="https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow#triggering-a-workflow-from-a-workflow">(reasonable!) security feature</a> that prevents tasks performed using the repository’s <code class="language-plaintext highlighter-rouge">GITHUB_TOKEN</code> from setting off new workflow runs, in order to prevent accidental infinite loops. My <code class="language-plaintext highlighter-rouge">fetch-new-data</code> workflow included a very default set-up of <a href="https://github.com/actions/checkout">the <code class="language-plaintext highlighter-rouge">actions/checkout</code> Action</a>, which uses that default repository token, so I was affected.</p>

<p>As far as I can tell, there are two approaches to fixing this. One would be to reconfigure the <code class="language-plaintext highlighter-rouge">fetch-new-data</code> workflow to <em>not</em> use the default token. Generate a <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens">personal access token</a> with the appropriate permissions, and tell the <code class="language-plaintext highlighter-rouge">checkout</code> action to use that instead. I didn’t go that route, but I think that’s what <a href="https://masteringlaravel.io/daily/2024-11-20-have-one-github-action-trigger-another">this tutorial describes</a>.</p>

<p>I opted instead to have the <code class="language-plaintext highlighter-rouge">fetch-new-data</code> workflow explicitly call the <code class="language-plaintext highlighter-rouge">build-and-deploy</code> workflow if there was a change. I <a href="https://docs.github.com/en/actions/how-tos/sharing-automations/reusing-workflows">reconfigured the <code class="language-plaintext highlighter-rouge">build-and-deploy</code> workflow to be reusable</a> with an <code class="language-plaintext highlighter-rouge">on: workflow_call:</code>, added the <code class="language-plaintext highlighter-rouge">uses</code> block to my calling workflow, and sat back and waited for it to work.</p>

<p>Once again, it did not. I could get it to trigger the build step, but the deployed site did not reflect the latest change. How annoying! I thought maybe it was some kind of race condition where the new commit hadn’t fully propagated, or there was some kind of caching involved. I tried a lot of silly and complicated things to address those two perceived problem areas.</p>

<p>As it turns out, I was close, and the answer is simple. In my case, the <code class="language-plaintext highlighter-rouge">build-and-deploy</code> workflow was also using a very default set-up of the <code class="language-plaintext highlighter-rouge">checkout</code> Action. If you don’t provide an explicit <code class="language-plaintext highlighter-rouge">ref</code> there, it falls back to whatever the current <code class="language-plaintext highlighter-rouge">GITHUB_SHA</code> value is. In the case of a <code class="language-plaintext highlighter-rouge">workflow_dispatch</code>/<code class="language-plaintext highlighter-rouge">workflow_call</code> setup, the called workflow <a href="https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_call">uses the <code class="language-plaintext highlighter-rouge">GITHUB_SHA</code> of the calling workflow</a>. The calling workflow, in turn, uses the <code class="language-plaintext highlighter-rouge">ref</code> of the state of the repo when it ran, which did not include the commit it performed.</p>

<p>So the fix was to set an explicit value for <code class="language-plaintext highlighter-rouge">ref</code> in the <code class="language-plaintext highlighter-rouge">checkout</code> step of the called workflow. I think hard-coding <code class="language-plaintext highlighter-rouge">main</code> probably works in almost all cases, but <a href="https://docs.github.com/en/actions/reference/accessing-contextual-information-about-workflow-runs#github-context">I used <code class="language-plaintext highlighter-rouge">github.ref_name</code></a> for just a touch more class.</p>

<p>I’m using this in two places now. One is in the repo for this very site, where a script that fetches my most recent movies, books, and concerts from various data sources can update a file here that gets incorporated into a Jekyll include. That info is now on my home page, but the “embed” here should also be live data, which is fun.</p>

<div class="recently">
    <div class="movies"><h3>Recently watched</h3>
    <ul>
        <li><em>Gaea Girls</em> (2000)</li><li><em>Hail, Caesar!</em> (2016)</li><li><em>The Secret Agent</em> (2025)</li>
    </ul></div>
    <div class="books"><h3>Recently read</h3>
    <ul>
        <li><em>Speedboat</em> by Renata Adler</li><li><em>The Sirens' Call</em> by Christopher L. Hayes</li><li><em>On the Calculation of Volume II</em> by Solvej Balle</li>
    </ul></div>
    <div class="concerts"><h3>Recently attended</h3>
    <ul>
        <li>Grace Bowers at Night Club 101</li><li>Unbroken Chain at The Capitol Theatre</li><li>John Medeski, Stanton Moore, Nels Cline, and Skerik at Brooklyn Bowl</li>
    </ul></div>
</div>

<p>The data sources for those are my Letterboxd account’s RSS feed, my Goodreads account’s RSS feed, and a Google Doc of concerts that I maintain, but it’s neat to know that I could easily point the script somewhere else if I ever wanted to move away from those sites.</p>

<p>The other is on <a href="https://malaikahanda.com/">Malaika’s personal site</a>, where changes in her spreadsheet trigger new builds with updated calendar data.</p>

<p>Writing this out, I realize that there are a zillion ways to solve this problem. Most of them I discarded not because they wouldn’t work, but because they felt inelegant. I could’ve just used a personal access token, but that felt dicey on the least privilege principle. I could’ve just copied and pasted the build workflow steps into the fetch workflow, but that felt like it was unnecessarily repetitive. I’m happy to have landed on a solution where everything works like it feels like it should.</p>]]></content><author><name>Parker Higgins</name></author><category term="programming" /><summary type="html"><![CDATA[A common pattern for building static sites on GitHub Pages (or similar) is to just trigger a new build every time there’s a change in the repository. This is very straightforward and well-tested, and works until you want to also trigger builds when something changes outside the repo. Then you run into some issues — but the solution, which took me some time to hunt down, turns out to be simple. The trick is specifying the right ref in your build workflow, and I explain why below.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://parkerhiggins.net/assets/images/ithaca-waterfall.jpg" /><media:content medium="image" url="https://parkerhiggins.net/assets/images/ithaca-waterfall.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Recurse Center return statement, or What I did on my coding vacation</title><link href="https://parkerhiggins.net/2025/06/recurse-center-return-statement/" rel="alternate" type="text/html" title="Recurse Center return statement, or What I did on my coding vacation" /><published>2025-06-30T00:00:00-04:00</published><updated>2025-06-30T00:00:00-04:00</updated><id>https://parkerhiggins.net/2025/06/recurse-center-return-statement</id><content type="html" xml:base="https://parkerhiggins.net/2025/06/recurse-center-return-statement/"><![CDATA[<p>I’ve just wrapped a 12-week batch at <a href="https://www.recurse.com">Recurse Center</a>, a self-directed coding retreat that provides an environment for people to become better programmers. I’m a huge fan of RC, and previously did a full batch in the summer of 2017 and a week-long minibatch at the start of 2019. It’s common for people finishing some time at Recurse Center to write up some notes afterwards, which are cutely called “return statements,” and that’s what I’m going to do here.</p>

<p>I had a good conversation with some fellow Recursers at about the halfway point of my batch, where we lamented how easy it is to feel like you’re “not getting enough done,” especially because the opportunity to focus on your own projects for 6 or 12 weeks is rare and feels precious. That is true, but it’s missing some critical context. In my opinion, the main project I’ve worked on for each of my two full batches was <em>myself</em>, as a programmer, cultivating my expertise and preferences and experiences.</p>

<p>It’s hard to account for that project, but any reckoning that doesn’t include it is incomplete. Sometimes it presents in more straightforward ways—like in my first batch I committed to using a real text editor, which was new to me and slowed things down, but has paid dividends so many times over—but often it’s intangible improvements to how I think about technology and problem-solving.</p>

<p>That said, I did ship some projects that I’d like to share!</p>

<h3 id="signal-bot-for-puzzmo-leaderboards">Signal bot for Puzzmo leaderboards</h3>

<p>I wanted to write some Go during my batch, which helped shape <a href="https://parkerhiggins.net/2025/04/webhooks-to-signal-groups-tailscale-puzzmo/">this first project</a>: a server that processes Puzzmo webhooks containing daily leaderboard updates and sends them to a Signal group. It was fun to dip in to a project that used Tailscale’s <code class="language-plaintext highlighter-rouge">tsnet</code> library, and it was also nice to start immediately getting daily benefits of code in production.</p>

<p>At first I felt a little self-conscious that this was not really packaged for public consumption, but that faded. I came across another Recurser’s blog post about <a href="https://hannahilea.com/blog/houseplant-programming/">“houseplant programming”</a>, a concept that I have come to really love. This was a real houseplant programming job for me, and it made my life a little better while improving my understanding of things like webhooks and Go and Tailscale and <a href="https://parkerhiggins.net/2025/04/signal-messenger-api-tailnet-docker-compose/">Docker and Signal and sysadmin stuff</a>. Hopefully it <a href="https://rygoldstein.com/posts/introducing-mirror-darkly">serves as</a> “an ad to write tiny software just for yourself.”</p>

<h3 id="surfing-the-bluesky-jetstream-with-helping-friendly-bot">Surfing the Bluesky Jetstream with Helping Friendly Bot</h3>

<p>When I did my first Recurse Center batch in 2017, social media bots—which mostly just meant Twitter bots—were a primary medium for me. In the years since, I’ve watched Twitter (and its API) get destroyed. It’s forced me to think about how to continue to apply the lessons I’d learned and the practices I’d developed in a new context.</p>

<p>So it was especially fun to work on <a href="https://parkerhiggins.net/2025/04/realtime-bluesky-events-jetstream-for-helping-friendly-bot/">a Bluesky project that would not have been possible with Twitter</a>. This project involved expanding my pre-existing Helping Friendly Bot, which posts context data about Phish songs as the band plays them on-stage, to listen for real-time Bluesky events and do a bunch of finicky replying and reposting stuff.</p>

<p>Beyond the Bluesky and <a href="https://atproto.com/">AT Protocol</a> specific parts, I learned a lot about websockets, shell-scripting, <a href="https://www.redhat.com/en/blog/linux-at-command">job scheduling with the <code class="language-plaintext highlighter-rouge">at</code> utility</a>, and more sysadmin stuff.</p>

<h3 id="personal-sites-for-friends-calendar-of-song-and-malaikas-crossword-chart">Personal sites for friends: Calendar of Song and Malaika’s crossword chart</h3>

<p>I knew about myself that I love websites and I love helping people publish websites that speak to them. These two projects really worked in that vein. For Michael, I <a href="https://parkerhiggins.net/2025/06/michael-atkins-calendar-of-song/">updated a long-running project</a> to each day <a href="https://allthingsatkins.com/calendar">refresh a page</a> on the personal site I’d previously built for him. For Malaika, I built her a <a href="https://malaikahanda.com/">brand new portfolio page</a> that displays her (many, many) publications on <a href="https://parkerhiggins.net/2025/06/new-website-for-malaika/">a GitHub-style contributions graph</a>.</p>

<p>It’s fun working to a friend’s specifications, and it enforces some discipline about where you won’t cut corners. I think I built things a little nicer for my friends than I would have for myself, and I appreciate the opportunity to show some craftsmanship. I also discovered about myself that I like writing Javascript that runs in the browser! That has not always been the case, but I’ve grown and so has the language and it’s gotten me started on a few projects that will ship in the future.</p>

<h3 id="maintenance-on-xword-dl-and-others">Maintenance on <code class="language-plaintext highlighter-rouge">xword-dl</code> and others</h3>

<p>I had told myself coming in to this batch that I would not let myself fall back on Python throughout. I’ve spent much more time writing in Python than in any other language, and it’s fast and comfortable for me. But there are definite benefits to embracing a diversity of languages and approaches, and building the skills and confidence to be able to choose the right tool, not just the one closest at hand.</p>

<p>So it was with some trepidation that I undertook some maintenance on my <a href="https://github.com/thisisparker/xword-dl/"><code class="language-plaintext highlighter-rouge">xword-dl</code> crossword scraping utility</a>—would I be learning, or just doing chores?</p>

<p>That fear was misplaced. There’s so much to learn, and there is a distinct value to continuing to work on a codebase over the course of years. I’ve now watched this project grow and develop as I have grown and developed, and I learned a lot about newer Python features, development best practices, and packaging stuff.</p>

<h3 id="infinite-sea-shanty-in-the-browser">Infinite sea shanty in the browser</h3>

<p>A corner of a larger project that I’m still working on, this <a href="https://parkerhiggins.net/2025/06/generating-infinite-sea-shanty-in-the-browser/">generative sea shanty</a> was a fun opportunity to catch up on web audio and dip my toe back into the world of generative composition and music theory. I used to be a musician and I have always loved theory, and to be honest I kind of forget that it is a body of knowledge I have spent thousands of hours working on.</p>

<p>(Do you ever do that? Write off skills you have as trivial to fixate on the skills that you don’t?)</p>

<p>So in that way, this was a fun exercise on the first order, and then it was also fun to figure out how to embed it into a (very lightly) interactive blog post, which was new for me. I also found a bug in the library I was using, reported it, and got it fixed! Between the original composition and the post about it, I learned a lot about Javascript, Tone.js, web audio, git and GitHub, and even some music theory. I also got to listen to a lot of shanties.</p>

<h3 id="you-should-probably-go-to-recurse-center">You should probably go to Recurse Center</h3>

<p>This list of projects isn’t complete, because there are a few things I’m still working on and want to keep cooking for a little bit longer. I’ve come to appreciate that Recurse Center is something of a mindset, and that I can chase down those projects and the curiosity that inspires them even as I re-enter the world.</p>

<p>This has been a really hard year for me, in a lot of ways. That has forced me into introspection, and trying to figure out what parts of myself are load-bearing, and how I want to build around them.</p>

<p>Alongside that project, I have been so grateful to have a space and a community like Recurse Center that encourages the pursuit of self-improvement. Obviously at RC, the focus is becoming a better programmer. Developing the volitional skills and the discipline to learn through building at the edge of your abilities, though, helps across the board.</p>

<p>Hands-down, though, the best thing about Recurse Center is the community of Recursers. People that I’ve met during batches have been hugely helpful and inspirational to me, and the alumni network is a constant source of wonder. I am grateful to have met so many of these people, and I want to meet more of them, and I want to learn generously and inspire others the way I’ve been inspired.</p>

<p>I know it is an incredible privilege to be able to focus on that for months at a time. But if you can swing it, I <a href="https://www.recurse.com/apply">encourage you to apply</a> to do a batch.</p>]]></content><author><name>Parker Higgins</name></author><category term="programming" /><summary type="html"><![CDATA[I’ve just wrapped a 12-week batch at Recurse Center, a self-directed coding retreat that provides an environment for people to become better programmers. I’m a huge fan of RC, and previously did a full batch in the summer of 2017 and a week-long minibatch at the start of 2019. It’s common for people finishing some time at Recurse Center to write up some notes afterwards, which are cutely called “return statements,” and that’s what I’m going to do here.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://parkerhiggins.net/assets/images/recurse-lab.jpg" /><media:content medium="image" url="https://parkerhiggins.net/assets/images/recurse-lab.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Generating an infinite sea shanty in the browser</title><link href="https://parkerhiggins.net/2025/06/generating-infinite-sea-shanty-in-the-browser/" rel="alternate" type="text/html" title="Generating an infinite sea shanty in the browser" /><published>2025-06-25T00:00:00-04:00</published><updated>2025-06-25T00:00:00-04:00</updated><id>https://parkerhiggins.net/2025/06/generating-infinite-sea-shanty-in-the-browser</id><content type="html" xml:base="https://parkerhiggins.net/2025/06/generating-infinite-sea-shanty-in-the-browser/"><![CDATA[<p>For a project I’m working on, I wanted to generate an endless looping soundtrack that felt like a sea shanty continuing indefinitely without a fixed melody. This proved to be a fun exercise for both musical and technical reasons, so I thought I’d write up some thoughts on how that came together.</p>

<p>I’ve had a longstanding interest in sea shanties, which made their sudden spike in popularity in 2021 first exciting and then somewhat annoying. One nice side effect, though, was for a few weeks there everybody who writes or makes videos about music stuff <a href="https://www.youtube.com/watch?v=m1ovAB4vKzw">had to talk about shanties</a>, and it produced a lot of good tutorials and discussions that helped me in this effort.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>

<p>With generative music, the success or failure of the “composition” really comes down to the actual rules you put in place. There’s a narrow path that’s fun and interesting, and danger on both sides. If the rules are too strict, it doesn’t <em>feel</em> generative. Sometimes this looks like overfitting to a particular creative model, which can make variations feel meaningless, leading to <a href="https://emshort.blog/2016/09/21/bowls-of-oatmeal-and-text-generation/">an oatmeal problem</a>. But if the rules are too lax, it doesn’t feel composed. You end up making a wind chime. Great if that’s what you’re aiming for, but not what I wanted.</p>

<p>Similarly, when composing in a genre you encounter some of the same overfit/underfit dynamic. If you lean too much into the <a href="https://en.wikipedia.org/wiki/Sc%C3%A8nes_%C3%A0_faire">scène à faire</a> elements, the result feels derivative; but too little and it doesn’t successfully evoke the genre at all.</p>

<p>With that in mind, embedded below is a working draft of my infinite shanty, and <a href="https://github.com/thisisparker/sea-minor-embed">here’s the code that powers it</a>. You can toggle each component individually if you want to hear a given instrument’s part more clearly.</p>

<iframe width="100%" height="120px" frameborder="0" src="https://thisisparker.github.io/sea-minor-embed/"></iframe>

<h3 id="musical-thoughts">Musical thoughts</h3>

<p>In order to make a thing that sounded like a shanty, I outlined some musical constraints that I could represent in the code.</p>

<h4 id="time-considerations">Time considerations</h4>

<p>Because of the strong historical connection between shanties and repetitive, rhythmic maritime labor tasks, time plays a major role in creating that distinctive sound. There’s a heavy emphasis on the downbeat, usually some “swing” (which means that the emphasized notes have a little longer duration), and often there are grace notes leading in to the start of phrases.</p>

<p>A lot of writing online connects shanties with 6/8 time in particular. (6/8 means that you can count six beats in a measure, usually as 1-2-3 1-2-3.) And there are definitely some notable shanties in 6: <a href="https://en.wikipedia.org/wiki/Blow_the_Man_Down">Blow The Man Down</a> is a traditional one, or <a href="https://www.youtube.com/watch?v=EVKPN5yVyNw">Fathoms Below</a> from the Little Mermaid is a modern example. I think it’s a bit of an oversimplification because lots and lots of shanties are not in 6, but in any case it is the sound I wanted here, so this one is in 6.</p>

<p>And to get more specific: many shanties have a musical connection to Irish traditional music, and can be described as a “jig” (which is usually in 6/8) or “hornpipe” (which is usually in 4/4). I wanted the jig sound, and in particular I wanted a “double jig,” which is characterized by notes on each of the 1-2-3 beats. Maybe the most famous traditional Irish double jig is <a href="https://en.wikipedia.org/wiki/The_Irish_Washerwoman">The Irish Washerwoman</a>, which is not a shanty but  has some melodic properties I wanted to emulate.</p>

<p>To get that effect, the timing of the melody I wrote looks like this in the (pretty intuitive!) notation format of <a href="https://scribbletune.com/">the library I used, Scribbletune</a>:</p>

<p><code class="language-plaintext highlighter-rouge">[xxx][x-x] [xxx][x-x] [xxx][x-x] [xxx][x__]</code></p>

<h4 id="melody-and-sound">Melody and sound</h4>

<p>That is the rhythm of the melody, and the note values vary with each loop. The Scribbletune library allows me to specify particular notes for some beats, and to pull randomly from a specified set for others. In order to give it a classic shanty feel, I used some preset phrases and pull the random notes from a scale in <a href="https://en.wikipedia.org/wiki/Dorian_mode#Modern_Dorian_mode">the Dorian mode</a>.</p>

<p>Musical modes are a super interesting way to evoke a particular vibe, and I feel like they’re not well understood by non-musicians. A very simple description is that they determine which notes are available to use in relation to the “tonic” note. For example, this piece uses the D Dorian scale, which, like C Major, is composed of the white piano keys: no sharps or flats. To distinguish this melody from one in C Major we have to establish that the D is our home base. Here, I did that with a droning bassline that just sits on a D note, except for a little flourish every 8 bars, and by populating the melody with some phrases that resolve to D.</p>

<p>Aesthetically, I think this piece is more interesting with synthesized instruments instead of sampled ones, so that’s what I’ve used. I’d initially wanted the melody to be on some kind of wind instrument, but I couldn’t get it to sound good, so I switched to a synthesized steelpan as a nod to some Caribbean sound. Shanties are typically a cappella or close to it, and they have lyrics, so these decisions were really just me vibing. Beyond the melody, there’s the simple bass, and two more percussive instruments that really just keep the rhythm going.</p>

<h3 id="technical-details">Technical details</h3>

<p>It’s possible to do web audio stuff using just vanilla javascript, but it pretty quickly becomes a headache to do anything musical. There’s <a href="https://tonejs.github.io/">a framework called Tone.js</a> that is hugely helpful in giving you abstractions like “notes” and “measures” and “instruments,” and my first draft of this used Tone.js directly.</p>

<p>But I found that I was still missing some useful abstractions related to composition, and so I turned to <a href="https://scribbletune.com/">Scribbletune, a javascript library</a> geared towards musical ideation (that uses Tone.js for its browser support). I think in practice that’s mostly for electronic music — it’s possible this is the first Scribbletune shanty. Beyond that, I think my usage pattern, where the Scribbletune playback in the browser is the actual end product, is a little unusual.</p>

<p>So I hit some rough edges of the library but I’m glad I persevered. For one thing, it has no configurable time signature, so I’ve had to sort of fake it by grouping my beats into triplets. This is where I think its roots in electronic music come through: not a lot of EDM uses anything but 4/4.</p>

<p>One thing I love about music theory is that it’s very empirical but still requires a lot of judgment calls. The time signature of a given piece, especially a traditional one, can be subject to some debate, and there are usually multiple ways of representing the same underlying information that differ mostly in terms of what mental model is being applied. In that way, it kind of reminds me of <a href="https://en.wikipedia.org/wiki/Duck_typing">duck typing</a>. It is useful to be able to say: okay, the software may not have a concept of “time signature,” but the <em>property</em> I’m looking for is two groupings of three beats, which I was able to achieve with the triplet function.</p>

<p>Beyond that, the randomness is not very configurable, and <a href="https://github.com/scribbletune/scribbletune/issues/192">I think I found a bug how it’s documented</a>. I think it accidentally made the piece more interesting, so I can’t be too mad.</p>

<p>My goal when starting this project was to make something interesting enough to give your attention for a bit, but ambient enough to fade into the background as necessary. I think I’ve done that, and I’ve learned a good deal about both song composition and javascript along the way.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>That video in particular, from the great Adam Neely, draws a distinction between the “shanty” and the “folk sea song.” Like many people online, I am eliding those two terms throughout this post, but if the difference matters to you, his description of it is a good one. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Parker Higgins</name></author><category term="programming" /><category term="sea shanties" /><category term="javascript" /><summary type="html"><![CDATA[For a project I’m working on, I wanted to generate an endless looping soundtrack that felt like a sea shanty continuing indefinitely without a fixed melody. This proved to be a fun exercise for both musical and technical reasons, so I thought I’d write up some thoughts on how that came together.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://parkerhiggins.net/assets/images/pirate-drawings/pirateship.png" /><media:content medium="image" url="https://parkerhiggins.net/assets/images/pirate-drawings/pirateship.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">You can’t fork your own GitHub repo (but you can get pretty close)</title><link href="https://parkerhiggins.net/2025/06/you-cant-fork-your-own-github-repo/" rel="alternate" type="text/html" title="You can’t fork your own GitHub repo (but you can get pretty close)" /><published>2025-06-25T00:00:00-04:00</published><updated>2025-06-25T00:00:00-04:00</updated><id>https://parkerhiggins.net/2025/06/you-cant-fork-your-own-github-repo</id><content type="html" xml:base="https://parkerhiggins.net/2025/06/you-cant-fork-your-own-github-repo/"><![CDATA[<p>I wanted to fork my own GitHub repo the other day, and it turns out you can’t. Instead, I got most of what I needed by duplicating the repo, and adding the original <a href="https://docs.github.com/en/get-started/git-basics/managing-remote-repositories">as a second remote</a>.</p>

<p>Why would you even want to fork your own repo? Here’s my situation. In my <a href="/2025/06/generating-infinite-sea-shanty-in-the-browser/">last blog post</a>, I embedded a small page with some javascript to play the audio I was describing. I’m actively working on that code, and to be able to look at it remotely or share it with friends, I’ve got a small Action that builds the single page and pushes to GitHub Pages.</p>

<p>I could’ve just embedded the resulting page, but it’s under active development and I don’t want to accidentally break something in the blog post. Really, I want a snapshot — but I also want to be able to deliberately pull in changes to that snapshot as necessary.</p>

<p>Usually that would mean you could just make a branch. But in this case I wanted a Pages environment, which is allotted at the repo level.</p>

<p>So instead, I cloned the repository, pushed it to a new GitHub repo, and then used <code class="language-plaintext highlighter-rouge">git remote add</code> to add back the Git URL of the original repo as a second remote. That preserves the shared history, and I can even commingle commits. You don’t quite get the interface sugar of pull requests across the two repos, but you can pull upstream changes into a downstream branch and open a PR that way.</p>

<p>One other option I considered: You <em>can</em> fork your own repo into one owned by an <a href="https://docs.github.com/en/organizations/collaborating-with-groups-in-organizations/about-organizations">organization</a>, so if I were doing this workflow a lot, maybe I’d make an organization that just owns blog-embedding-related repos. For now, though, the duplicated repo did the trick.</p>]]></content><author><name>Parker Higgins</name></author><category term="programming" /><category term="github" /><summary type="html"><![CDATA[I wanted to fork my own GitHub repo the other day, and it turns out you can’t. Instead, I got most of what I needed by duplicating the repo, and adding the original as a second remote.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://parkerhiggins.net/assets/images/default.jpg" /><media:content medium="image" url="https://parkerhiggins.net/assets/images/default.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">A new website for Malaika, tracking crossword publications</title><link href="https://parkerhiggins.net/2025/06/new-website-for-malaika/" rel="alternate" type="text/html" title="A new website for Malaika, tracking crossword publications" /><published>2025-06-06T00:00:00-04:00</published><updated>2025-06-06T00:00:00-04:00</updated><id>https://parkerhiggins.net/2025/06/new-website-for-malaika</id><content type="html" xml:base="https://parkerhiggins.net/2025/06/new-website-for-malaika/"><![CDATA[<p>A few weeks ago, my friend <a href="https://malaikahanda.com">Malaika Handa</a> nerd-sniped me with an interesting request. She is a very prolific crossword constructor (think: multiple publications a week) <em>and</em> she keeps comprehensive records of those publications in a spreadsheet that is always up to date.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> She wanted a site to display that history in a clean and browsable way.</p>

<p>I <a href="https://malaikahanda.com">built her that site</a>, and we published it yesterday. I’m really proud of how it turned out, and want to share some thoughts about how it came together.</p>

<figure class="image">
    <img src="/assets/images/malaika-site-screenshot.png" alt="" title="" />
    <figcaption>A screenshot from Malaika's site on the publication date of this post.</figcaption>
</figure>

<p>Programmers in the audience will likely recognize the inspiration immediately. She wanted to display her data in the style of <a href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/managing-contribution-settings-on-your-profile/viewing-contributions-on-your-profile#contributions-calendar">GitHub’s “contributions calendar,”</a> which is displayed by default on every user’s profile. (<a href="https://github.com/thisisparker/">See my profile</a>, for an example.)</p>

<p>That chart displays days as cells in a 7x53 grid, and colors each cell in a shade of green that depends on how many “contributions” the user made on that day. As a format for visualizing software development data, it has been wildly influential, and that influence has come with <a href="https://github.com/isaacs/github/issues/627">lots of backlash</a>. It’s kind of a classic GitHub issue: something they thought was a straightforward measure of some objective metric turned out to be deeply opinionated, and even political, in terms of the values it promotes and actions it encourages. Ah, well.</p>

<p>Some of the criticism boils down to its flattening tendency. By first creating a limited definition of a “contribution,” and then equating all of them in a quantity-only measure, it strips the character of some contributions and outright ignores others. (I can pretend to be above it, but this project began life as a major pull request before being split out into its own repo. The commits for the former were not shown on my graph, and I was annoyed!)</p>

<p>Part of our design spec aimed to avoid that flattening. We wanted to create a customizable set of rules for cell colors, beyond just buckets of counts: Being able to visually distinguish days with entries in a certain category — such as “full-size” 15×15 puzzles — was much more important to us than highlighting days with two or three puzzles published.</p>

<p>The means that, despite the visual similarity, <a href="https://github.com/malihanda/malaikahanda.com">our implementation</a> diverged meaningfully from the inspiration. The general term for the kind of chart GitHub makes is a “calendar heatmap,” and there are some <a href="https://cal-heatmap.com">robust libraries</a> for creating them. But given that divergence, and because it would be more fun to build this from scratch, I didn’t use any of them. Instead, the calendar is rendered with a dependency-free vanilla javascript module.</p>

<p>In fact, the first version was one big blob of javascript that did everything by itself. When I shared it with some other folks at the Recurse Center, I got the good feedback that it might be fun to generalize a bit for possible reuse. (I got maybe halfway there in time for launch.) I did realize that moving more configuration from hardcoded properties to initialization parameters would make it easier for Malaika to update and change it in the future, so the calendar, the details panel, and the filters are each separately defined and given specifications in the page’s script tag. Those specifications include the coloring rules, the format of the tooltip presentation, the columns to filter by, that kind of thing.</p>

<p>That pattern of moving configuration up a level of abstraction is one I repeated throughout the development, and eventually I moved a bunch of parameters into a repo-level configuration file. The text on the page, the site’s metadata, and even the theme colors are defined in one file, so she can make updates without having to get into the weeds of the site. Once I’d done that, I was able to ship a bunch of little quality-of-life improvements: the site’s favicon and Open Graph image are generated at build time using the theme colors, for example.</p>

<p>Keen-eyed readers may have put together that Malaika is the “sophisticated programmer” I mentioned in <a href="/2025/05/google-sheets-database-for-projects/">my post last week about Google Sheets as a database</a>. The source of truth for the site is a Sheet she has kept updated for years, which she maintains with a combination of manual data entry and <a href="https://developers.google.com/apps-script">Apps Script</a> automation. Every few hours, a GitHub Action pulls the live data from the sheet, commits if there have been any changes, and then kicks off a new site build. GitHub Actions are famously sort of finicky, and I ran into an issue with chaining workflows, but otherwise it was pretty smooth sailing.</p>

<p>What she didn’t realize when she mentioned the project to me was that this format was a little personal. Years ago, before I had started my programming journey, I had received a cache of time series data in response to a public records request. It showed that a particular surveillance practice had steadily become much more frequent over the period in question. I had this gut feeling that a calendar heatmap would be the best way to display it, but I didn’t know how — and before I could figure it out, the data had become stale and we never did anything with it.</p>

<p>I regret that I never got to do that project, but I’m glad to have gotten a chance to implement something similar. And <a href="https://malaikahanda.com/">the site</a> is, I think, quite beautiful.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>You may know about her <a href="https://www.7xwords.com/">7xwords project</a>, which published crosswords for all the possible 7×7 grids over the course of a year, with 6 posts a week. I made <a href="https://www.7xwords.com/daily/04/04-15.html">two</a> of <a href="https://www.7xwords.com/daily/09/09-08.html">them</a>! <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Parker Higgins</name></author><category term="programming" /><category term="crosswords" /><category term="malaika handa" /><category term="javascript" /><summary type="html"><![CDATA[A few weeks ago, my friend Malaika Handa nerd-sniped me with an interesting request. She is a very prolific crossword constructor (think: multiple publications a week) and she keeps comprehensive records of those publications in a spreadsheet that is always up to date.1 She wanted a site to display that history in a clean and browsable way. You may know about her 7xwords project, which published crosswords for all the possible 7×7 grids over the course of a year, with 6 posts a week. I made two of them! &#8617;]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://parkerhiggins.net/assets/images/blue-malaika-image.png" /><media:content medium="image" url="https://parkerhiggins.net/assets/images/blue-malaika-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Calendar of Song, by Michael Atkins</title><link href="https://parkerhiggins.net/2025/06/michael-atkins-calendar-of-song/" rel="alternate" type="text/html" title="The Calendar of Song, by Michael Atkins" /><published>2025-06-02T00:00:00-04:00</published><updated>2025-06-02T00:00:00-04:00</updated><id>https://parkerhiggins.net/2025/06/michael-atkins-calendar-of-song</id><content type="html" xml:base="https://parkerhiggins.net/2025/06/michael-atkins-calendar-of-song/"><![CDATA[<p>Over at Michael Atkins’ blog, he’s released a <a href="https://allthingsatkins.com/blog/2025/06/01/calendar-of-song-statement/">lovely artist’s statement on Calendar of Song</a>, a new (and old) project I’ve helped to get working over the years. Through various tech stacks, the basic premise has stayed the same: Michael picks a song of the day, usually highlighting some lyrical relevance or historical coincidence, and some Parker tech “publishes” it.</p>

<p>I’ve helped Michael build and maintain his site over the years because I love reading his writing, and the short captions that accompany these songs are no different. And besides, he pulls from such a wide variety of music, the playlist itself is a great tour across genres, styles, and traditions.</p>

<p>Since we deployed the <a href="https://allthingsatkins.com/calendar">Calendar of Song</a>, I’ve added it to my phone’s home screen, and tune in each day when I’ve got a quiet moment. It’s been nice.</p>

<p>His statement really captures the project better than I can, but I want to drill down just a bit on his use of the word “site-specific,” which is (both) a great pun and a pithy distillation of the some of the motivation behind my implementation decisions.</p>

<p>The first iteration of the Calendar of Song was (what else?) a Twitter bot, where its atomic unit was an individual post, in a format designed to obliterate context. For the viewer, a Calendar of Song update in that era appeared under a headline, over a shitpost, between selfies and memes and reaction gifs. As the algorithmic timeline gained purchase, it lost even the excuse of simultaneity.</p>

<p>Today’s Calendar of Song is a different beast altogether. Like a tear-off desk calendar, the only context is: today. There is no “previous post” or “see more” or “index,” just the song Michael has picked, and a few words about it. It was fun to build to that spec, especially within the constraints of his (static) site and the delightfully janky CMS I’ve set up for him.</p>

<p>You might describe it as eschewing the promise of the massively parallel for a more serial experience. In that way, from a user experience perspective, it’s as influenced by Wordle as it is the more erudite art antecedents that Michael highlights.</p>

<p>Anyway, <a href="https://allthingsatkins.com/calendar">go give it a listen</a>. And if you like what you hear, see you again tomorrow.</p>]]></content><author><name>Parker Higgins</name></author><category term="programming" /><category term="art" /><category term="michael atkins" /><summary type="html"><![CDATA[Over at Michael Atkins’ blog, he’s released a lovely artist’s statement on Calendar of Song, a new (and old) project I’ve helped to get working over the years. Through various tech stacks, the basic premise has stayed the same: Michael picks a song of the day, usually highlighting some lyrical relevance or historical coincidence, and some Parker tech “publishes” it.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://parkerhiggins.net/assets/images/IMG_5028.jpg" /><media:content medium="image" url="https://parkerhiggins.net/assets/images/IMG_5028.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Using Google Sheets as a database for projects</title><link href="https://parkerhiggins.net/2025/05/google-sheets-database-for-projects/" rel="alternate" type="text/html" title="Using Google Sheets as a database for projects" /><published>2025-05-27T00:00:00-04:00</published><updated>2025-05-27T00:00:00-04:00</updated><id>https://parkerhiggins.net/2025/05/google-sheets-database-for-projects</id><content type="html" xml:base="https://parkerhiggins.net/2025/05/google-sheets-database-for-projects/"><![CDATA[<p>As a software developer who enjoys working on hobby projects, especially with less technical collaborators, I have found myself using Google Sheets as a good-enough database in a lot of circumstances. I have lots of qualms about having the biggest of Big Tech in a load-bearing spot on these projects, but the practical benefits and the existence of a reasonable exit route have helped to quiet them.</p>

<p>One good example project is <a href="https://dailycrosswordlinks.com/">Daily Crossword Links</a>, a newsletter started by Matthew Gritzmacher to compile, as its name suggests, links to each day’s published crosswords. The newsletter always  includes mainstream and independent outlets (down to individual bloggers). When Matthew and I first spoke about it, he was putting it together each day by hand, checking and copying text from dozens of sites while referring to a Google Sheet of puzzle sources that he’d made. We started from that Google Sheet, and I was able to automate a lot of that process, ultimately producing a draft each day that he (and eventually other editors) could clean up for publication—and to this day, the source of truth for what goes into those drafts is a Sheet.</p>

<p>It’s a recipe I’ve followed a bunch of times now, and when I’m discussing it with collaborators, I usually come back to the same list of benefits.</p>

<ul>
  <li>Everybody already has an account, and they are comfortable with the collaboration workflows. That doesn’t mean they’re the ideal workflows for any given situation, or even that they’re very good, but at least they’re familiar. So when the Daily Crossword Links project expanded from one editor to a handful of volunteers, they were able to manage this through invitations to the spreadsheet, without me having to build any new auth or permissions model. Besides collaboration, they also have pretty robust version controls, and other mechanisms in place to prevent data loss.</li>
  <li>Spreadsheets themselves are really powerful tools, including for automation. The crossword newsletter crew now has a bunch of functions to fill in dynamic dates and titles and things like that, and they don’t need “developer time” to expand those. On another project I’m working on, the proprietor (who is a much more sophisticated programmer than I am!) wrote a Google App Script to update her data. And even for less technical users, I can write formulas that are less intimidating—and provide real-time feedback—than the equivalent Python would be.</li>
  <li>There are clients for every device, which means editing is always going to be more-or-less reasonable. Editing a spreadsheet from a cell phone is not great, but it’s not an experience I’m shoving down their throat. Similarly, people know how to <em>read</em> a spreadsheet, and it’s much more inviting than an equivalent table of data.</li>
  <li>Spreadsheets also enforce some data discipline in ways that don’t feel overbearing. With most projects I’m immediately converting the data into JSON or YAML, which to their credit are mostly readable (if intimidating). Creating a new object in either format, though, requires cutting and pasting and sometimes interacting with the vagaries of the specs. On the other hand, a new row in a spreadsheet has labeled columns and example data right next to it. Not to mention, conditional formatting can even highlight missing or malformed data if necessary. All of this makes the upgrade path out of a Sheet clear, if need be; and in most cases, a CSV export would work as a drop-in replacement, in a pinch.</li>
  <li>Combining those last two points: One pattern I’ve used successfully is rigging up a Google Sheets “database” to a Form, which is a totally familiar interface that can do all kinds of validation and provide default values.</li>
</ul>

<p>There are of course drawbacks. To make one obvious point, a spreadsheet is not really the right data structure for some kinds of projects. Text formatting is a bit of a bear to deal with, and if you need more than two dimensions of data (including nesting or any kind of relational component), things get very hacky fast. (It helps that you can have multiple <a href="https://support.google.com/docs/answer/1218656">sheets</a> within a Sheet, and they can refer to each other in formulas.)</p>

<p>And of course, a lot of these advantages would be moot if there weren’t good tooling. For my Python projects, I’ve had a great experience with <a href="https://docs.gspread.org">gspread</a> for both reading and writing data. I haven’t really had to look into what the ecosystem is like elsewhere, but I expect that this many years into Google Sheets there’s pretty good support everywhere.</p>

<p>From a practical standpoint, the existence of good tooling like that means that my exposure to Google Sheets is pretty minimal: I hit the API to download live data, and in some cases to make updates, but the data otherwise feels pretty native. If I ever had to migrate a particular project away, it would be disruptive for my collaborators learning a new workflow, but it would be pretty easy for me to swap out gspread for a regular parser of a standard data format.</p>]]></content><author><name>Parker Higgins</name></author><category term="programming" /><category term="google" /><category term="spreadsheets" /><summary type="html"><![CDATA[As a software developer who enjoys working on hobby projects, especially with less technical collaborators, I have found myself using Google Sheets as a good-enough database in a lot of circumstances. I have lots of qualms about having the biggest of Big Tech in a load-bearing spot on these projects, but the practical benefits and the existence of a reasonable exit route have helped to quiet them.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://parkerhiggins.net/assets/images/google-sheets-screenshot.png" /><media:content medium="image" url="https://parkerhiggins.net/assets/images/google-sheets-screenshot.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">A Signal messenger API on your tailnet with Docker Compose</title><link href="https://parkerhiggins.net/2025/04/signal-messenger-api-tailnet-docker-compose/" rel="alternate" type="text/html" title="A Signal messenger API on your tailnet with Docker Compose" /><published>2025-04-23T00:00:00-04:00</published><updated>2025-04-23T00:00:00-04:00</updated><id>https://parkerhiggins.net/2025/04/signal-messenger-api-tailnet-docker-compose</id><content type="html" xml:base="https://parkerhiggins.net/2025/04/signal-messenger-api-tailnet-docker-compose/"><![CDATA[<p>I’ve got a new configuration for sending Signal messages on the command line, and it’s powerful and flexible and finally gives me a nice ergonomic interface to use from other programs. Even cooler, it is accessible over Tailscale from any of my devices, which means I can securely reach it from multiple machines without going through the rigmarole of configuring and maintaining multiple copies of <code class="language-plaintext highlighter-rouge">signal-cli</code>.</p>

<p>In a nutshell, I’m using Docker Compose to spin up <a href="https://github.com/bbernhard/signal-cli-rest-api">this containerized version of <code class="language-plaintext highlighter-rouge">signal-cli</code></a> that exposes a REST API, with Tailscale in a sidecar container for its network mode. From the (cloud) machine where it’s running, the API is accessible at <code class="language-plaintext highlighter-rouge">127.0.0.1:8080</code>, and from my other tailnet devices, it’s at the host <code class="language-plaintext highlighter-rouge">signal</code>. From a security perspective this is especially neat, because its ensures that all of the otherwise unencrypted requests are going through the Wireguard tunnels that Tailscale sets up.</p>

<p>The end result is I can send Signal messages with <code class="language-plaintext highlighter-rouge">curl</code> or <code class="language-plaintext highlighter-rouge">requests</code> or whatever, from any of my devices. If I want to be really slick, I can use just the hostname, for a command like:</p>

<p><code class="language-plaintext highlighter-rouge">curl -X 'GET' 'signal/v1/accounts'</code></p>

<p>The container offering the API appears as a “machine” in my Tailscale admin console, and access is controlled with my ACLs.</p>

<p>Here’s my full Docker Compose file. A lot of this is pretty <a href="https://tailscale.com/blog/docker-tailscale-guide">standard for a Tailscale sidecar, as seen in the big guide blog post</a>, but I will get into some fun parts below.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">ts-signal</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">tailscale/tailscale:latest</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">ts-signal</span>
    <span class="na">hostname</span><span class="pi">:</span> <span class="s">signal</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">TS_AUTHKEY=tskey-client-n0tr3aLLy474115c413k3y101?ephemeral=false</span>
      <span class="pi">-</span> <span class="s">TS_EXTRA_ARGS=--advertise-tags=tag:server</span>
      <span class="pi">-</span> <span class="s">TS_STATE_DIR=/var/lib/tailscale</span>
      <span class="pi">-</span> <span class="s">TS_AUTH_ONCE=true</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">${PWD}/state:/var/lib/tailscale</span>
      <span class="pi">-</span> <span class="s">${PWD}/config:/config</span>
    <span class="na">devices</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/dev/net/tun:/dev/net/tun</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">127.0.0.1:8080:80</span>
    <span class="na">cap_add</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">net_admin</span>
      <span class="pi">-</span> <span class="s">sys_module</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
  <span class="na">signal-api</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">bbernhard/signal-cli-rest-api</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">signal-api</span>
    <span class="na">network_mode</span><span class="pi">:</span> <span class="s">service:ts-signal</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">ts-signal</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/path/to/your/.local/share/signal-cli:/home/.local/share/signal-cli</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">MODE=native</span>
      <span class="pi">-</span> <span class="s">PORT=80</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">signal-api</span><span class="pi">:</span>
    <span class="na">driver</span><span class="pi">:</span> <span class="s">local</span>
  <span class="na">ts-signal</span><span class="pi">:</span>
    <span class="na">driver</span><span class="pi">:</span> <span class="s">local</span>
</code></pre></div></div>

<p>Before we go through the fun parts, let me lay out the end state I was working towards:</p>

<ul>
  <li>for compatibility reasons, I want the API to be available from the host machine itself at <code class="language-plaintext highlighter-rouge">localhost:8080</code></li>
  <li>for external access, I want the API to be available over Tailscale at a memorable domain (without requiring a port number)</li>
  <li>I want the API to “play nice” with an existing <code class="language-plaintext highlighter-rouge">signal-cli</code> install. I almost didn’t dare to dream that I’d be able to use my existing config, but (spoiler alert) I think that’s possible</li>
</ul>

<p>I was able to get all those things. Stepping through the interesting portions:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="pi">-</span> <span class="s">TS_AUTHKEY=tskey-client-n0tr3aLLy474115c413k3y101?ephemeral=false</span>
      <span class="pi">-</span> <span class="s">TS_EXTRA_ARGS=--advertise-tags=tag:server</span>
      <span class="pi">-</span> <span class="s">TS_STATE_DIR=/var/lib/tailscale</span>
      <span class="pi">-</span> <span class="s">TS_AUTH_ONCE=true</span>
</code></pre></div></div>

<p>These are all magic Tailscale environment variables. <del>The one thing I’ll note is that I’m using <a href="https://tailscale.com/kb/1282/docker#ts_authkey">an OAuth client secret</a> here instead of an authkey so I don’t have to worry about cycling it. That in turn requires the <code class="language-plaintext highlighter-rouge">--advertise-tags</code> bit is passed to the <code class="language-plaintext highlighter-rouge">TS_EXTRA_ARGS</code>.</del></p>

<p>(<strong>Update 2025-04-25</strong>: It turns out I had a misunderstanding about the relationship between Tailscale OAuth clients and auth keys and the struck-out portion above is not exactly correct. The <code class="language-plaintext highlighter-rouge">TS_AUTHKEY</code> value is used when creating the node and on initial authorization, but then the machine’s state is preserved and its expiry can be disabled. That’s why we map the container’s state directory to the local machine, and as long as we keep it there, it’s fine to just use an auth key as the <code class="language-plaintext highlighter-rouge">AUTHKEY</code>. If you do use a client secret though, it is still the case that you have to provide the tags argument.)</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">127.0.0.1:8080:80</span>
<span class="c1">#and later, under the signal-api service:</span>
<span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">MODE=native</span>
      <span class="pi">-</span> <span class="s">PORT=80</span>
</code></pre></div></div>

<p>This was my tricky approach to making the API available locally at <code class="language-plaintext highlighter-rouge">:8080</code> and remotely without a port number. The <code class="language-plaintext highlighter-rouge">PORT</code> variable for <code class="language-plaintext highlighter-rouge">signal-api</code> makes it available at <code class="language-plaintext highlighter-rouge">:80</code>, which is the standard unencrypted HTTP port; the Tailscale <code class="language-plaintext highlighter-rouge">network_mode</code> makes it so that you can hit that container, and its port 80, through normal Tailscale addressing. And because the container is just a machine on the tailnet, you can grant or restrict access to it through your normal ACL configuration.</p>

<p>I thought I was going to use <a href="https://tailscale.com/kb/1242/tailscale-serve">tailscale serve</a> for this, but it kept not working exactly the way I wanted. I think it has to do with the fact that the Signal API container serves (a 404) on port 80 even when it’s using a higher port for the main stuff, but that’s kind of speculation. In any case, this ended up working great. If you skipped the Tailscale container ports line, the API is just available at the Tailscale address, which is not a bad outcome either.</p>

<p>One other note on the <code class="language-plaintext highlighter-rouge">ports</code> field: a lot of docs I saw for the Signal API container suggested configurations like <code class="language-plaintext highlighter-rouge">8080:8080</code>, which maps the container’s <code class="language-plaintext highlighter-rouge">8080</code> to the host machine’s <code class="language-plaintext highlighter-rouge">8080</code>. I would guess most users actually want to bind that to a specific IP address! Without an additional specification, it is set to <code class="language-plaintext highlighter-rouge">0.0.0.0</code>: available on all interfaces. Maybe fine for a network of only trusted devices but I don’t know, man. Bind your ports.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/path/to/your/.local/share/signal-cli:/home/.local/share/signal-cli</span>
</code></pre></div></div>

<p>This one surprised me at first, but: as far as I can tell, you can really point a host and container <code class="language-plaintext highlighter-rouge">signal-cli</code> at the same config directory and they’ll share state. So if you’re already configured, this plays nicely and just gives you more powers. If you want to keep them separate, then point this volume elsewhere. One small gotcha: by default the config directory is at <code class="language-plaintext highlighter-rouge">$HOME/.local/share/signal-cli</code>, but Docker Compose doesn’t expand the <code class="language-plaintext highlighter-rouge">$HOME</code>, so you may want to hard-code the path in there.</p>

<p>With this set-up, I can keep the Signal factors separate from my other deployment considerations. For little things like my <a href="https://parkerhiggins.net/2025/04/webhooks-to-signal-groups-tailscale-puzzmo/">server to send Puzzmo results to a Signal group</a>, or the <a href="https://parkerhiggins.net/2025/04/realtime-bluesky-events-jetstream-for-helping-friendly-bot/">personalized messages from my Helping Friendly Bot</a>, it’s so much simpler to be able to put those anywhere I can make a network request, and manage all of my Signal stuff in one place.</p>]]></content><author><name>Parker Higgins</name></author><category term="programming" /><category term="signal" /><category term="tailscale" /><category term="docker" /><summary type="html"><![CDATA[I’ve got a new configuration for sending Signal messages on the command line, and it’s powerful and flexible and finally gives me a nice ergonomic interface to use from other programs. Even cooler, it is accessible over Tailscale from any of my devices, which means I can securely reach it from multiple machines without going through the rigmarole of configuring and maintaining multiple copies of signal-cli.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://parkerhiggins.net/assets/images/signal-from-python.png" /><media:content medium="image" url="https://parkerhiggins.net/assets/images/signal-from-python.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>