August Was a Great Month for Puzzles

04 Sep, 2025 by Graham Marlow in games

I can't stop thinking about two puzzle games that came out last month. They're both the kind of game that makes me jealous for not having designed it myself, then jealous again for knowing that even with the idea in my hands I couldn't execute it as well. Those two games are Strange Jigsaws and Öoo.

Strange Jigsaws screenshot

Strange Jigsaws is a meta-puzzle exploration built on the humble jigsaw puzzle. If you're not into jigsaws (I don't blame you, they're not particularly puzzling) don't be dissuaded. The puzzles in Strange Jigsaws encompass a wide variety of different logic and themed puzzles, jigsaws only in the narrowest sense.

Thanks to those jigsaws, the game is incredibly tactile. FLEB clearly knows what he's doing when he designs puzzles that emphasize juicy interactions. Moving, rotating, and slotting puzzle pieces is dopamine delivered straight to the brain.

The real triumph of Strange Jigsaws is the breadth of ideas and overall quality of their execution. There are a ton of different puzzle ideas in the game, almost none of which are a plain ol' jigsaw. Each puzzle is a fresh challenge with a consistent difficulty, a bit on the easy side but only so much as to avoid any sense of frustration.

I played through Strange Jigsaws in three, one-hour sessions. It's a game that's very amenable to short sessions since each puzzle is neatly self-contained.

Öoo is likewise a short game. I finished in just under two hours, but that two hours was a single, non-stop playthrough. Öoo is not an easy game to put down.

Öoo screenshot

Design-wise, Öoo is kind of the antithesis of Strange Jigsaws. Where Strange Jigsaws delights by introducing new mechanics with every puzzle, Öoo is a study into the possibility space of a single idea. That single idea grows deeper as the player accumulates the knowledge of how to apply it to the surrounding world.

That means mechanics in Öoo aren't so much introduced as revealed. Every puzzle advances the player's intuition of the mechanical language underlying Öoo, unveiling how that language interacts with the surrounding world and how the player can use that language to solve puzzles. The player character doesn't gain new abilities. Instead, the player learns the world and the world reveals itself as one great puzzle.

It's hard to overstate how much design excellence Öoo squeezes out of its mechanics. Several times a solution left me blurting out in laughter, amazed by how Öoo bent the rules of the world and re-contextualized my expectations. It's a uniquely joyful experience.

It's hard to believe that two of the best puzzle games that I've played in the last two years came out in the same month. Do yourself a favor and check them out.

New Emacs Package: Helix Mode

17 May, 2025 by Graham Marlow in emacs

Short version:

I'm building a new Emacs package: Helix Mode. Helix Mode implements the Helix modal keybindings in Emacs. It's been my daily driver for about a month, and while it still has some bugs, I'm reasonably confident it's in a usable state.

Install Helix Mode in Emacs 30.1:

(use-package helix
  :vc (:url "https://github.com/mgmarlow/helix-mode")
  :config
  (helix-mode))

Long version:

About six months ago I attempted to set up Emacs on a Windows machine and found it to be an immensely frustrating experience. The default Windows Emacs build works well enough if you don't use any third-party packages, but who is using Emacs who isn't also using packages? Diving into the complexity of setting up my entire Emacs config exposed a reliance on Linux CLI tools that I hadn't installed, and attempting to configure my Windows environment to properly export paths with cygwin/w64devkit/whatever was not going well.

Eventually I gave up and swapped over to WSL, the Linux emulation layer for Windows. For the most part WSL is great, provided you use the terminal. Attempting to use GUI Emacs from WSL results in a Frankenstein-like windowing experience. It kinda works but is far from ideal.

With these frustrations top of mind, I decided to drop Emacs altogether and experiment with a terminal-first workflow. I had already been itching for an excuse to try out Helix and this felt like the perfect opportunity.

As it turns out, Helix is an incredibly capable text editor, if a bit light on the tooling. The vim-ish keybinding scheme is magical once you understand the basics, and the automatic configuration settings for tree-sitter and LSP work amazingly well. That said, Helix is not very featureful and expects a lot of supplemental work done in the terminal. It really needs to be paired with a terminal multiplexer like Zellij or tmux to work effectively.

I settled on tmux and set up a light config that emulates Emacs:

# remap prefix from C-b to C-x
unbind C-b
set-option -g prefix C-x
bind-key C-x send-prefix

# split panes
bind 0 kill-pane
bind 1 kill-pane -a
bind 2 split-window -v
bind 3 split-window -h
unbind '"'
unbind %

# zellij-style pane swapping
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R

If you're willing to settle for a minimal text editor that's supplemented with tmux and small scripting languages, I think Helix is incredibly compelling. It remixes the Vim keybindings[1] in a way that makes them far more intuitive.

The principle change is flipping around Vim's verb-object model. In Vim, if you want to delete the next word, you first press d (delete) and followup with w (word). The idea is that you declare your action before your intended target, queuing up what you intend to perform before telling Vim what to perform it on.

Helix is the opposite. First you select your intended target: w (word). Helix automatically selects the word as the cursor navigates to it, clarifying the selection visually. Then you perform your action: d (delete).

It's kind of like Helix is operating in a permanent, automatic visual mode. In Vim, I often found myself resorting to visual mode because I didn't inherently trust my muscle memory to select the appropriate selection before performing a deletion. This is problematic because Vim's visual mode makes everything less efficient. Here's how you'd delete with visual mode:

  1. Press v to enter visual mode.
  2. Press w to navigate word.
  3. Press d to delete.

The funny thing is that visual mode makes Vim function like Helix, but requires an extra keypress for every action. In Helix, the selection is automatic so you don't lose any street cred.

Back to Emacs

Despite enjoying the Helix + tmux workflow, in the last couple months I've come to miss some of the niceties of Emacs:

  • Built-in diffing tools like vc-diff are really nice, even if I prefer the git CLI for most everything else.
  • project.el is unbeatable. Helix doesn't have a concept of workspaces, nor does it allow global search & replace like project-query-replace-regexp.
  • Helix only recently got a proper file navigator but it hasn't yet been released. I doubt that it will be as useful as dired.
  • Emacs remains the king of Lisp editing (shoutout to the 2025 Spring Lisp Game Jam where I'm using Fennel & Love2d).

And so the idea for Helix Mode developed. It's easily my most ambitious Emacs package, both in lines of code and functionality. But it brings all of my favorite pieces of Helix into the Emacs editing experience.

Helix Mode isn't designed to re-implement all of Helix, nor provide the extensibility of the venerable Evil Mode. Instead it's aimed at the subset of Helix keybindings responsible for editing and navigation, leaving everything else to the responsibility of Emacs. That means you'll still be using stuff like M-x replace-string or consult.

What it does offer is the same object-verb navigation as Helix, complete with automatic selections. It also includes some of the Helix sub-modes, like the Goto mode that provides go-to definition (g d) or the Space mode that allows navigation across a project (SPC f). Both of which integrate with project.el and xref.

If I haven't bored you with the details of my text-editor dabblings over the past six months, I encourage you to check out Helix Mode. I have a long list of features and improvements that I'd like to make before the 1.0.0 release, but I think it's currently in a very usable state.


  1. Noteworthy that the object-verb idea isn't Helix's innovation, but Kakoune's. ↩︎

Action Cable + React

02 May, 2025 by Graham Marlow in til

Using Action Cable in React is surprisingly difficult. Every third-party package that I've come across that offers a custom hook makes some fundamental assumptions that only hold for simple applications. The vast majority of tutorials gloss over anything beyond the "log when event is received" boilerplate.

The principle of integrating Action Cable is easy, since it follows a well-known subscribe/unsubscribe pattern with a useEffect. It looks something like this[1]:

import { createConsume } from '@rails/actioncable'

const MyComponent = () => {
  const consumer = useMemo(
    () => createConsumer('ws://localhost:3000/cable'),
    [],
  )

  useEffect(() => {
    const sub = consumer.subscriptions.create('AlertChannel', {
      received(data) {
        console.log(data)
      },
    })

    return () => {
      sub.unsubscribe()
    }
  }, [consumer])

  return <div />
}

When all the React application is doing is logging some data, sure, easy-peasy. But when that component needs to access component state? Now we have a problem.

Let me demonstrate by introducing a stateful counter. It's a contrived example, but it gets the point across that accessing component state is probably useful for Websocket subscribers.

const MyComponent = () => {
  const [count, setCount] = useState(0)

  const consumer = useMemo(
    () => createConsumer('ws://localhost:3000/cable'),
    [],
  )

  useEffect(() => {
    const sub = consumer.subscriptions.create('AlertChannel', {
      received() {
        console.log(count)
      },
    })

    return () => {
      sub.unsubscribe()
    }
  }, [consumer])

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>increment</button>
    </div>
  )
}

This example demonstrates the most obvious flaw: count is missing in the useEffect dependencies, so no matter how many times the increment button is clicked, the value logged will always be 0. We have to make the subscription event handler aware that the count has changed by adding it as a dependency to the effect.

useEffect(() => {
  const sub = consumer.subscriptions.create('AlertChannel', {
    received() {
      console.log(count)
    },
  })

  return () => {
    sub.unsubscribe()
  }
}, [consumer, count])

Now, theoretically this resolves our counter issue. And, for the most part, it does. When we receive an Action Cable event from our server, the received handler logs with the correct value of count. However, in practice this code has another bug: subscriptions are not properly cleaned up, so the client responds to the Action Cable message many more times than expected. In my testing, if I clicked the button 12 times quickly, I would see 6 console logs when the Action Cable event is broadcast.

It seems that Action Cable is not particularly good about cleaning up subscriptions that have the same channel key. That is, when the increment button is clicked multiple times in succession (representing many state updates in our component), Action Cable does not do a good job ensuring that every connection is appropriately unsubscribed between renders. You will actually observe errors in the Rails console, indicating that it’s struggling to keep up:

Could not execute command from ({"command" => "unsubscribe", "identifier" =>
"{\"channel\":\"AlertChannel\"}"}) [RuntimeError - Unable to find subscription
with identifier: {"channel":"AlertChannel"}]

Digging into the Action Cable source code, it's made apparent that the Action Cable library uses a JSON-stringified representation of the channel name as an identifier when storing the subscriber. Here's the relevant code:

export default class Subscription {
  constructor(consumer, params = {}, mixin) {
    this.consumer = consumer
    this.identifier = JSON.stringify(params)
    extend(this, mixin)
  }
  // ...

I can only guess that there's a race condition somewhere in Action Cable involving identical subscription identifiers. I managed to locate a GitHub issue that tracks a similar problem and lends a little extra support to the theory.

One way to resolve this race condition is to simply include the count in the channel identifier, even if it's unused by the channel on the server. That way a unique identifier is created for every re-render caused by count:

useEffect(() => {
  const sub = consumer.subscriptions.create(
    { channel: 'AlertChannel', count },
    {
      received() {
        console.log(count)
      },
    },
  )

  return () => {
    sub.unsubscribe()
  }
}, [consumer, count])

This seems to get the job done. Each Websocket subscriber is given a unique key that can be easily located by the Action Cable library for removal. Note that this only works for serializable data.

I'll note that I also tried passing a reference (via useRef) for the Action Cable callbacks, hoping that a stable object reference might avoid the need for the extra useEffect dependency. However, when the Action Cable JS library creates new subscriptions, it creates an entirely new object, rendering the stable reference moot.

Anyway, all this to say: be careful when creating Action Cable subscriptions that rely on external state. Subscriptions created with the same key will likely not be cleaned up correctly.

Most of the time in React applications this doesn't matter that much, since we can get by with a stable, memoized reference. useQueryClient from tanstack-query is a great example, since it allows us to invalidate our client requests when an event is broadcast from the server:

// e.g. from react-query or tanstack-query
const queryClient = useQueryClient()

const consumer = useMemo(() => createConsumer('ws://localhost:3000/cable'), [])

useEffect(() => {
  if (!consumer || !queryClient) {
    return
  }

  const sub = consumer.subscriptions.create('AlertChannel', {
    received() {
      queryClient.invalidateQueries({
        queryKey: ['alerts'],
      })
    },
  })

  return () => {
    sub.unsubscribe()
  }
}, [consumer, queryClient])

For other purposes, it's likely a good idea to pass serializable data to the Websocket channel parameters.


  1. Note that I'm being careful to only create one consumer for a given component to avoid re-establishing a connection to the Websocket on every re-render. It's also likely that your consumer will need to live in a separate hook for authorization purposes. ↩︎

Kafka on the Shore is My Favorite Murakami Novel

22 Apr, 2025 by Graham Marlow

The following is an email I sent a friend regarding Kafka on the Shore.

I've had a week or two now to digest Kafka on the Shore and put some thoughts together. It's definitely my favorite Murakami novel thus far. By a long shot. The symbolism feels attainable, yet abstract enough that there's still room for reader interpretation. The plot is interesting enough to give weight to the characters, aided by the dual narrative between Kafka and Nakata/Hoshino. It's great.

A couple of ideas stand out to me:

The Oedipus prophecy set upon Kafka isn't necessarily that he literally needs to fulfill the Oedipus contract, but that he needs to carry on the spirit of his father's art. The subtext that I'm picking up is that his father (the cat-murdering, flute-blowing madman sculptor) sacrificed everything for his art, including his relationship with his son. The prophecy that he laid upon Kafka is his own desire for immortality, extending his name and art with Kafka as the vehicle. Thus Kafka feels overwhelming pressure and the impossibility of his own individuality, thus he runs away.

In Miss Saeki, Kafka finds a companion in grief. The two struggle with existing in the real world, caught instead between the threshold of life and death where her 15 year old spirit inhabits memories of the past. To her the past and present are inseparable, the youth that once drove her to compose Kafka on the Shore has long since vanished.

When Kafka ventures into the forest behind the cabin, he grapples with the idea of suicide. He's literally on the precipice of death, peering into the world beyond and the purgatory in-between. Here there's comfort in routine, at the cost of the literal music of life. Back home there's grief and sadness, but also the ability to form new memories shaped from the past.

I'll leave you with one of my favorite quotes near the end of the book,

“Every one of us is losing something precious to us,” he says after the phone stops ringing. “Lost opportunities, lost possibilities, feelings we can never get back again. That’s part of what it means to be alive. But inside our heads—at least that’s where I imagine it—there’s a little room where we store those memories. A room like the stacks in this library. And to understand the workings of our own heart we have to keep on making new reference cards. We have to dust things off every once in a while, let in fresh air, change the water in the flower vases. In other words, you’ll live forever in your own private library.”

How I Organize Email with HEY

18 Apr, 2025 by Graham Marlow

Six months after swapping back over to HEY for email feels like the appropriate time to check in on how it’s going. Here are the ways I use HEY to organize my email; what works and what doesn't.

My workflow

I read every email that finds its way into my inbox. I hate unread emails, and I especially hate the # unread counter that most other email platforms surface within their tab titles. It unnerves me to an unhealthy degree.

That doesn't mean that I categorize every email into a special folder or label to get it out of my inbox. HEY doesn't even support this workflow, it lacks the notion of folders. Instead, read emails that I don't immediately delete simply pile up in the inbox and are covered with an image.

HEY claims that their email client is "countless"[1], in that there are no numbers telling you how many emails are in your inbox or how far you're behind in your organizational duties. And for the most part, that's true, except for one glaring counter that tells you how many unscreened emails are awaiting your approval:

HEY Screener counter

Not exactly "countless" but at least the screener is only relevant for emails from unrecognized senders.

Back on the topic of emails flowing into my inbox, most transactional emails find their way into the Paper Trail automatically. Receipts of this kind are bundled up and kept out of sight, out of mind.

Other emails that I want to draw temporary importance to reside in one of the two inbox drawers, Set Aside or Reply Later. I use Set Aside for shipping notifications, reservations, and other emails that are only relevant for a short period of time. Reply Later is self-evident. The system is very simple and works the way HEY intends.

My favorite HEY feature is easily The Feed, which aggregates newsletters into a single page. In a world where Substack has convinced every blogger that newsletters are the correct way to distribute their thoughts, The Feed is a great platform for aggregation. Shout-out to JavaScript Weekly and Ruby Weekly.

The Feed, Paper Trail, Set Aside, and Reply Later make up the bulk of my daily workflow in HEY. I'm very happy with these tools and while they are largely achievable via application of filters, labels, and rules in other inbox systems, I find the experience in HEY to be an improvement thanks to its email client and UI.

A few other HEY tools fit into more niche use-cases.

Collections are essentially threads of threads. They're similar to labels, but have the added benefit of aggregating attachments to the top of the page. I tend to use them for travel plans because they provide easy access to boarding passes or receipts.

HEY Collections

On the topic of travel, Clips are amazing for Airbnb door codes, addresses, or other key information that often finds itself buried in email marketing fluff. Instead of keeping the email in the Set Aside drawer and digging into it every time you need to retrieve a bit of information, simply highlight the relevant text and save it to a Clip.

HEY for domains, while severely limited in its lack of support for multiple custom domains, at least allows for email extensions. I use [email protected] to automatically tag incoming email with the "reimburse" label so I can later retrieve it for my company's reimbursement systems.

Important missing features

HEY is missing a couple of crucial features that I replace with free alternatives.

The first is allowing multiple custom domains, a feature of Fastmail that I dearly miss. I have a few side projects that live on separate domains and I would prefer those projects to have email contacts matching said domain. If I wanted to achieve this with HEY, I'd have to pay an additional $12/mo per domain which is prohibitively expensive[2].

Instead of creating multiple HEY accounts for multiple domains, I use email forwarding to point my other custom domains towards my single HEY account. Forward Email is one such service, which offers free email forwarding at the cost of denoting the DNS records in plain text (you pay extra for encryption). Another option I haven't investigated is Cloudflare Email Routing, which may be more convenient if Cloudflare doubles as your domain registrar.

It's a bummer that I can't configure email forwarding for custom domains within HEY itself, as I can with Fastmail.

The other big missing feature of HEY is masked email.

Fastmail partners with 1Password to offer randomly-generated email addresses that point to a generic @fastmail domain instead of your personal domain. This is such a useful (and critical) feature for keeping a clean inbox, since many newsletter sign-ups or point-of-sale devices (looking at you, Toast) that collect your email have a tendency to spam without consent. With masked email, you have the guarantee that if your masked email address gets out in the wild it can be trivially destroyed with no link back to your other email addresses.

Luckily, DuckDuckGo has their own masked email service and it’s totally free: DuckDuckGo Email Protection. The trade-off is a one-time download of the DuckDuckGo browser extension that you can remove afterwards.

Both of these features make me wish that HEY was more invested in privacy and security. They have a couple of great features that already veer in that direction, like tracking-pixel elimination and the entire concept of the Screener, but they haven't added any new privacy features since the platform launched.

Problem areas

Generally speaking, the Screener is one of the killer features of HEY. Preventing unknown senders from dropping email directly into your inbox is really nice. It does come with a couple of trade-offs, however.

For one, joining a mailing list means constant triage of Screener requests. Every personal email of every participant on that mailing list must be manually screened. HEY created the Speakeasy code as a pseudo workaround, but it doesn't solve the mailing list issue because it requires a special code in the subject line of an email.

The second problem with the Screener is pollution of your contact list. When you screen an email into your inbox, you add that email address to your contacts. That means your contact list export (which you may create if you migrate email platforms) is cluttered with truckloads of no-reply email addresses, since many services use no-reply senders for OTP or transactional emails.

When I originally migrated off of HEY to Fastmail a few years ago (before coming back) I wrote a script that ran through my contacts archive and removed no-reply domains with regular expressions. Instead, I wish that allowed senders were simply stored in a location separate from my email contacts.

The other pain point is around the HEY pricing structure. HEY is divided into two products: HEY for You, which provides an @hey.com email address, and HEY for Domains, which allows a single custom domain and some extra features. The problem is that these two products are mutually exclusive.

By using HEY for Domains, I do not have access to an @hey.com email address, a HEY World blog, or the ability to take personal notes on email threads. If I wanted these features in addition to a custom domain, I'd need to pay for both HEY products and manage two separate accounts in my email inbox (of which I want to do neither).

The split in pricing is made even worse because the extra features offered by Hey for Domains all revolve around team accounts, e.g. multi-user companies. For a single HEY user, the HEY for You features are more appealing.

This creates an awkward pricing dynamic for a single-user HEY experience. The product that I actually want is HEY for You with a single custom domain that maps both emails to a single account. The @hey.com email address should be a freebie for HEY for Domain users, as it is with alternative email providers.

I still like it though

Since the last two sections have been dwelling a bit on the negatives, I'll end by saying that I still think HEY is a good product. Not every feature is going to resonate with every individual (there's a good amount of fluff), but the features that do resonate makes HEY feel like personally-crafted software.


  1. HEY talks about their general philosophy here. ↩︎

  2. It's worth noting that the HEY for Domains pricing scheme is intended for multiple users. HEY for Domains used to be branded as "HEY for Work", if that's any indication of where the pricing awkwardness comes from. ↩︎