Longer version (templated with Claude Sonnet using the github commit):
I've been thinking about getting a MIDI controller to use as a Stream Deck type
of device — not to do any music stuff, but use the midi messages to
automate workflows. Not having found anything compact, robust and with cool
twiddly knobs, I thought to beta test my actual interest by loaning a Novation
Launchpad Mk3 from a friend.
I started by asking ChatGPT if we can control this thing with MIDI and the
answer was yes. It even suggested a Python test script to try it out. I obliged
and ran it, and it didn't work, but a few rounds of discussion revealed the
culprit to be two input and output devices, and the script had defaulted on the
wrong one.
Now Chrome nowadays has all these awesome local USB capabilities, so what about
MIDI? Turns out, you can skip all Python package dependencies and just write a
single HTML page that can connect to the launchpad and program it using the Web
MIDI API!
The Launchpad Mk3 uses MIDI messages also to control the LEDs, so you can not
just receive input, but actually control the 8x8 LED matrix with 127 colors
(ChatGPT said first half is static and the other half pulsing colors, but I was
not quite sure how this worked).
Snake and Blob Games
Once I had the basic LED control working and a HTML debug console that showed
the MIDI messages flowing, I couldn't resist adding some games. Snake is the
classic — use the arrow pads to guide the snake around the 8x8 grid. The
Blob game is a variant I asked Claude Code to whip up for me, or actually my
daughter, who still lacks the dexterity to play Snake, but might be happy with
some animation if she succeeds in hitting the blob on the grid.
I've been doing some housekeeping on my website, moving various small tools and experiments that were scattered in subfolders to a proper home at tools.codeandlife.com. While at it, I used Claude Code to give most of them a UI refresh while keeping the core functionality intact.
Simple (PWA) arcade game for the Christmas holidays
Most of these are simple single-file HTML+JS apps that I've accumulated over the years. Nothing fancy, but occasionally useful. The full list is at tools.codeandlife.com.
You might have noticed things look a bit different around here. I finally got around to refreshing the blog design with the aid of Claude Code. Thanks for drafting this post also go the same way.
What's New
Dark and light mode — The site now respects your system preference. No more blinding white pages at 2 AM.
Retro-inspired styling — I went for a look that nods to the old-school web while staying readable. Monospace headers, clean lines, nothing fancy.
Search — There's now a search box in the header! Finding that one post about Arduino timers from 2014 should be much easier now. Try it out and see if Claude just hallucinated that joke!
Related posts — Single post pages now show related articles in the sidebar (desktop) and at the bottom (mobile). Hopefully this surfaces some older posts that might still be useful.
The underlying tech is still Eleventy with Liquid templates, but I did upgrade to newer version of 11ty with some AI help — Opus 4.5 did most of the work and even advised against upgrading to a fancier framework, which I appreciated greatly.
Very small cross-advertisement: I finally got around to revamp my personal website, eliminating a lot of autobiographical jabber. Such level of detail felt very needed in 1998ish when I first did my homepage and I had followed the tradition, but now we're in a more streamlined mode. Having said that, it's still pretty verbose.
Looks are now very clean, though smell AI generated (and rightly so), but it's not a heavy traffic site and most readers will miss the point, so I'll let it stand. It's not like the previous version was better, so I will take the improvement and spend twiddly-time elsewhere. :)
My first tool idea: a browser-based zip image viewer. I just updated JZipView (a native C app), and thought a pure HTML/JS version would be a nice complement.
Two AI-Generated Viewers
I had both Claude Opus 4.5 and ChatGPT 5.2 Pro create their own versions from the same basic prompt. The results:
Both needed some polishing with Claude Code (using Sonnet) to get them working similarly — handling drag-and-drop, keyboard navigation, and the usual edge cases. But the core functionality came out surprisingly well from both models.
You can drag a .zip file over, and it will use import { unzip } from 'https://esm.sh/fflate@0.8.2'; to decompress it and show a gallery view
Left click on an image opens it in "fit to view" mode
Another left click will open 1:1 mode and mouse gets captures so you can just mouse move around the image
Right click goes back to "fit to view" and again to grid mode
Arrow keys and scroll wheel allow to browse through the images
I released the JUnzip library back in 2014 as a minimalistic C library for reading ZIP files without heavy dependencies. It worked well for simple cases, but had a long-standing limitation: it couldn't handle "streaming" ZIP files where file sizes weren't known upfront. These ZIPs set bit 3 in the general purpose flag and store sizes in a "data descriptor" after the file data instead of the header. I was not aware of this exact nature of the problem until last week, just that many zips, annoyingly those including Google Photos generated ones, just failed when I tried to view them with JZipView that uses my JUnzip library as well.
Fast forward to December 2025, and with help from ChatGPT 5.2 and OpenAI Codex, I finally fixed this issue. It feels great to breathe new life into a project that's been dormant for so long!
What Changed in JUnzip
The core fix was improving support for ZIP data descriptors. When a ZIP entry has bit 3 set in generalPurposeBitFlag, the local file header contains zeros for CRC, compressed size, and uncompressed size. The actual values come in a data descriptor that follows the file data. My library was assuming both local headers and end file data contain all needed data, and thus failed when encountering these zips.
The key changes:
A new jzReadDataDescriptor() function that reads the optional descriptor (handling both signature-present and signature-absent variants)
Modified jzReadLocalFileHeader() to preserve sizes from the central directory when the local header has zeros
Updated jzReadData() to automatically consume the data descriptor when present
This was an API-breaking change — the callback signature for jzReadCentralDirectory() now includes a void *user_data parameter. Additionally, the library uses a JZFile abstraction instead of raw FILE* pointers.