


{
  "feed_url": "https://jbmorley.co.uk/feed/",
  "home_page_url": "https://jbmorley.co.uk",
  "items": [
    {
      "content_html": "<p>Continuing my Psion connectivity themed March <a href=\"/december-adventure\">December Adventure</a>, I persisted with the process of addressing legacy threading issues in plptools&mdash;it&rsquo;s important that we have a stable foundation on which we can build future functionality, and it&rsquo;s still my intuition it&rsquo;s better to build on top of what we have than start again.</p> \n<h1><a id=\"foundational-work\"></a>Foundational Work</h1> \n<p>The plptools architecture centralises all the complexity in <code>ncpd</code>, the daemon responsible for implementing the <a href=\"https://thoukydides.github.io/riscos-psifs/plp.html\">Psion Link Protocol</a> (PLP) and exposing different Psion-side server end-points to PC-side TCP clients. This makes the clients relatively simple at the cost of some gnarly internal code which is, unsurprisingly, where all our problems lie.</p> \n<p>An overly simplified overview of that looks something like this:</p> \n<pre><code class=\"language-mermaid\">---\nconfig:\n  class:\n      hideEmptyMembersBox: true\n---\nclassDiagram\ndirection TB\n\nclass NCPSession\nclass NCP\nclass Link\nclass DataLink\nclass LinkChannel\nclass SocketChannel\n\nNCPSession \"1\" --&gt; \"1\" NCP : ncp_\nNCP \"1\" --&gt; \"1\" Link : link_\nNCP \"1\" --&gt; \"n\" SocketChannel : channelPtr\nNCP \"1\" --&gt; \"1\" LinkChannel : lChan\nLink \"1\" --&gt; \"1\" DataLink : dataLink_\n</code></pre> \n<noscript> \n <p class=\"caption\">Enable JavaScript to view the diagram.</p> \n</noscript> \n<p>On first blush, this seems complex, but there&rsquo;s quite a bit that needs to happen and the functionality is relatively well compartmentalized:</p> \n<ul> \n <li><code>NCPSession</code>&mdash;provides APIs to manage the full daemon life cycle</li> \n <li><code>NCP</code>&mdash;multiplexes channels to Psion-side servers, pairing them to connected PC-side TCP clients \n  <ul> \n   <li><code>LinkChannel</code>&mdash;a special channel for communicating with the Psion to manage the overall connection</li> \n   <li><code>SocketChannel</code>&mdash;PC-side TCP client end-point</li> \n  </ul></li> \n <li><code>Link</code>&mdash;manages connection establishment and packet sequencing, transmission, and retransmission</li> \n <li><code>DataLink</code>&mdash;frames and un-frames messages, and writes and reads the serial port</li> \n</ul> \n<p>Most of my effort over the last few days has been focused on <a href=\"https://plptools.github.io/plptools/classDataLink.html\"><code>DataLink</code></a>&mdash;the goal is to ensure we have a robust core before focusing on other aspects of the architecture. Looking at the existing code, my theory is that this was originally written to be single-threaded and threading was added after-the-fact, with no locking at all. This has given me a wonderful opportunity to refresh my memory of C++&rsquo;s take on <a href=\"https://en.cppreference.com/w/cpp/thread/mutex.html\">mutexes</a>, <a href=\"https://en.cppreference.com/w/cpp/thread/lock.html\">locks</a>, and <a href=\"https://en.cppreference.com/w/cpp/thread/condition_variable.html\">condition variables</a>. The introduction of locks has a significant impact on how we shut down <code>ncpd</code>: the various worker threads were relying on <code>PTHREAD_CANCEL_ASYNCHRONOUS</code> which can stop them at any point during their execution, potentially leaving locks held and resulting in deadlock. With locks, we have to explicitly manage thread cancellation. My <a href=\"https://github.com/plptools/plptools/pull/118\">PR</a> for these changes has evolved over the past few days, and I think it&rsquo;s nearly there. Thanks must go to <a href=\"https://oldbytes.space/@thelastpsion\">Alex</a> and <a href=\"https://github.com/kapfab\">Fabrice</a>, the other plptools maintainers, for stoically reviewing and testing my many changes, and keeping me honest during the process.</p> \n<h1><a id=\"self-hosted-daemons\"></a>Self-Hosted Daemons</h1> \n<p>With the various thread-safety improvements in place, I took another crack at adding a self-hosted daemon to <code>plpftp</code> to allow it to be run without necessitating a separate <code>ncpd</code> instance. This involves moving the daemon classes into &lsquo;libplp&rsquo; to allow all the plptools commands to share them and, unfortunately, doing so still resulted in a near-immediate crash in that looked like a double-free in <code>BufferStore</code>, our buffer convenience wrapper.</p> \n<p>Fairly sure I wasn&rsquo;t seeing a multi-threading issue this time around, I kept digging and, after altogether too long, realized the crash was a result of the <code>free</code> function itself being <code>NULL</code>. Unsurprisingly, when runtime fundamentals like this are missing, you get some incredibly strange and misleading crashes and call stacks. Thankfully the fix was easy: I had failed to link <a href=\"https://www.gnu.org/software/gnulib/\">libgnu</a>; a failure that macOS silently ignores, leaving all function pointers <code>NULL</code>. 🤦</p> \n<p>With everything finally working as expected, I was able to make the change I&rsquo;d intended three days prior, adding a simple call to conditionally start a daemon (<code>NCPSession</code>) instance if the serial port was passed as an argument to <code>plpftp</code>:</p> \n<pre><code class=\"language-cpp\">Semaphore *sem = new Semaphore();\nNCPSession *session = nullptr;\nif (serialDevice) {\n    session = new NCPSession(\n        sockNum,\n        115200,\n        host,\n        serialDevice,\n        false,\n        0,\n        [](void *context, bool connected, int version) {\n            static_cast&lt;Semaphore *&gt;(context)-&gt;signal();\n        },\n        sem);\n    session-&gt;start();\n    sem-&gt;wait();\n}\n</code></pre> \n<p>As is always the way in software, this &lsquo;feature&rsquo; work proved the easiest bit and, with the foundations in place, this worked first time:</p> \n<p>\n <body>   \n  <img src=\"/posts/2026-03-19-december-adventure-march-17-18/plpftp@2x/400.gif\" width=\"400\" height=\"264\" x-srcset=\"/posts/2026-03-19-december-adventure-march-17-18/plpftp@2x/400.gif 400 264,/posts/2026-03-19-december-adventure-march-17-18/plpftp@2x/800.gif 800 528,/posts/2026-03-19-december-adventure-march-17-18/plpftp@2x/1200.gif 1200 792,/posts/2026-03-19-december-adventure-march-17-18/plpftp@2x/1600.gif 1600 1056,\" />  \n </body></p>",
      "date_published": "2026-03-19T13:32:10-07:00",
      "id": "https://jbmorley.co.uk/posts/2026-03-19-december-adventure-march-17-18/",
      "title": "December Adventure' March 17-18",
      "url": "https://jbmorley.co.uk/posts/2026-03-19-december-adventure-march-17-18/"
    },
    {
      "content_html": "<p>I started the week with grand plans of adding a small feature a day to one of <a href=\"https://github.com/plptools/plptools\">plptools</a> or <a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a>&mdash;perhaps a backup command, TCP support for connecting to MAME, improvements to the file transfer, or incremental backups in Reconnect. One day in, it&rsquo;s clear I&rsquo;m certainly not going to manage one a day.</p> \n<h1><a id=\"usability-improvements\"></a>Usability Improvements</h1> \n<p>plptools adheres to a very Linux mindset, comprising multiple command line apps, each providing a distinct piece of functionality. These all rely on a central daemon&mdash;<code>ncpd</code>&mdash;for managing the serial port and connections to a Psion. While it&rsquo;s a great system that allows you to run multiple tools in parallel (e.g., installing a program and managing your files at the same time), it does require users to run <code>ncpd</code> in addition to whatever tool they wish to use. For example, I run the following two commands to connect to my Series 3mx:</p> \n<ol> \n <li><p>Start <code>ncpd</code> to connect to the Psion:</p> <pre><code class=\"language-shell\">ncpd -s /dev/cu.usbserial-A9DA9DOF -d\n</code></pre></li> \n <li><p>Run <code>plpftp</code> to browse and transfer files:</p> <pre><code class=\"language-shell\">plpftp\n</code></pre></li> \n</ol> \n<p>Inspite of the clear benefits of the architecture, unless you&rsquo;re planning to run the daemon all the time (which Reconnect does but we don&rsquo;t have a great story for yet in plptools), this can feel pretty heavyweight just to copy a file. I also have a theory that having to understand the plptools architecture sufficiently to know to run <code>ncpd</code> is a pretty big barrier to getting started. With that in mind, I plan to allow the different plptools apps to self-host the daemon if you specify a serial port. This will reduce the above to:</p> \n<pre><code class=\"language-shell\">plpftp -s /dev/cu.usbserial-A9DA9DOF\n</code></pre> \n<p>Doing this means using reusing the <code>NCPSession</code> class from <code>ncpd</code> in <code>plpftp</code>, necessitating moving it into <code>libplp</code> (the library that&rsquo;s shared between all the plptools CLI apps). This should be simple, but moving it left me with an app that segfaults as soon as the Psion connects. My working theory is that I&rsquo;ve subtly changed the timing or object life cycle in a way that exposes existing race conditions. With that, I returned to the exercise of gently tidying the codebase and the work of <a href=\"https://github.com/plptools/plptools/pull/118\">adding thread-safety to ncpd</a>&mdash;an incredibly nuanced process with a codebase as long-in-the-tooth as plptools.</p>",
      "date_published": "2026-03-17T13:38:40-07:00",
      "id": "https://jbmorley.co.uk/posts/2026-03-17-december-adventure-march-16/",
      "title": "December Adventure' March 16",
      "url": "https://jbmorley.co.uk/posts/2026-03-17-december-adventure-march-16/"
    },
    {
      "content_html": "<p>Inspired by <a href=\"https://eli.li/december-adventure-march-2026\">Eli&rsquo;s post</a>, I&rsquo;m spending the next week doing some <a href=\"/december-adventure\">not-quite-December adventuring</a>. It&rsquo;s hard to believe we&rsquo;re nearly &frac14; of the way through 2026, but here we are, and I suspect many of us already feel we need a break from it all. Time for an adventure!</p> \n<p>As I&rsquo;ve only got a week, I&rsquo;m planning to keep things simple by focusing on improving the Psion connectivity story with <a href=\"https://github.com/plptools/plptools\">plptools</a> and <a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a>. If I can, I&rsquo;d like to make one meaningful improvement each day&mdash;I think there&rsquo;s room for a few small changes to each that will make things easier for folks using Psions in 2026.</p>",
      "date_published": "2026-03-16T12:34:15-07:00",
      "id": "https://jbmorley.co.uk/posts/2026-03-16-adventure-time/",
      "title": "Adventure Time",
      "url": "https://jbmorley.co.uk/posts/2026-03-16-adventure-time/"
    },
    {
      "content_html": "<p>Yesterday, I finally got around to publishing my OPL support for <a href=\"https://highlightjs.org\">highlight.js</a>. I&rsquo;ve been using it here since I wrote it <a href=\"/posts/2025-12-06-december-adventure-day-05/\">a couple of months ago</a>, but now it&rsquo;s available on <a href=\"https://github.com/jbmorley/highlightjs-opl\">GitHub</a> and <a href=\"https://www.npmjs.com/package/highlightjs-opl\">NPM</a> for others to use.</p> \n<pre><code class=\"language-opl\">PROC hello:\n  PRINT \"OPL is amazing!\"\n  GET\nENDP\n</code></pre> \n<p>Just like highlight.js itself, it&rsquo;s incredibly easy to use, and there are a few different options.</p> \n<p>You can use the module directly (what I do on this website):</p> \n<pre><code class=\"language-javascript\">import hljs from '../highlight.js/es/highlight.js';\nimport opl from './highlightjs-opl/src/languages/opl.js';\nhljs.registerLanguage('opl', opl);\nhljs.highlightAll();\n</code></pre> \n<p>Or add the minified self-registering version from the UNPKG CDN to your site&rsquo;s <code>head</code> (how I&rsquo;m using it on the <a href=\"https://opolua.org\">OpoLua</a> website):</p> \n<pre><code class=\"language-html\">&lt;link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css\"&gt;\n&lt;script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js\"&gt;&lt;/script&gt;\n&lt;script type=\"text/javascript\" src=\"https://unpkg.com/highlightjs-opl/dist/opl.min.js\"&gt;&lt;/script&gt;\n&lt;script type=\"text/javascript\"&gt;\n    hljs.highlightAll();\n&lt;/script&gt;\n</code></pre> \n<p>I&rsquo;m looking forward to seeing all your OPL projects!</p>",
      "date_published": "2026-03-01T11:35:42-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-03-01-syntax-highlighting/",
      "title": "Syntax Highlighting",
      "url": "https://jbmorley.co.uk/posts/2026-03-01-syntax-highlighting/"
    },
    {
      "content_html": "<p>Ever since <a href=\"https://github.com/tomsci\">Tom</a> decided to bring <a href=\"https://opolua.org\">OpoLua</a>&mdash;our modern runtime for <a href=\"https://en.wikipedia.org/wiki/Open_Programming_Language\">OPL</a>&mdash;to Linux with the introduction of a <a href=\"https://en.wikipedia.org/wiki/Qt_(software)\">Qt</a>-based desktop app, I&rsquo;ve wanted to make it as easy to install and update as the macOS and iOS apps. This week I did that for Debian and Ubuntu by setting up a <a href=\"https://releases.jbmorley.co.uk\">self-hosted apt repository</a> for the software we publish&mdash;if you&rsquo;re using a Debian-based system and you&rsquo;ve not yet tried out OpoLua, you can get started with the following simple commands:</p> \n<pre><code class=\"language-shell\">curl -fsSL https://releases.jbmorley.co.uk/apt/public.asc | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/jbmorley.gpg\necho \"deb https://releases.jbmorley.co.uk/apt $(lsb_release -sc) main\" | sudo tee /etc/apt/sources.list.d/jbmorley.list\nsudo apt update\nsudo apt install opolua\n</code></pre> \n<p>I&rsquo;ve successfully installed OpoLua on Debian Trixie, and Ubuntu Noble and Questing and, thanks to <a href=\"https://www.colinhoad.com\">Colin</a>, we have evidence that it even works on Kubuntu:</p> \n<p>\n <body>   \n  <img src=\"/posts/2026-02-28-publishing-opolua-for-debian-and-ubuntu/kubuntu-screenshot/400.png\" width=\"400\" height=\"209\" x-srcset=\"/posts/2026-02-28-publishing-opolua-for-debian-and-ubuntu/kubuntu-screenshot/400.png 400 209,/posts/2026-02-28-publishing-opolua-for-debian-and-ubuntu/kubuntu-screenshot/800.png 800 419,/posts/2026-02-28-publishing-opolua-for-debian-and-ubuntu/kubuntu-screenshot/1200.png 1200 629,/posts/2026-02-28-publishing-opolua-for-debian-and-ubuntu/kubuntu-screenshot/1600.png 1600 839,\" />  \n </body></p> \n<p>The new apt repositories are built on top of our existing GitHub CI: releases on GitHub now include manifest files that contain metadata about which platforms and architectures binaries and packages support, and these files are then used to determine what to include in the apt repository. Unlike many other GitHub-based solutions I&rsquo;ve seen, I plan to host historical builds of OpoLua (and other projects) to ensure folks can access builds for their OS for as long as possible, even if we&rsquo;ve had to drop it to enable new features. (Though thankfully that shouldn&rsquo;t be necessary with OpoLua as Qt seems to have an amazing backwards compatibility story.)</p> \n<p>In addition to the new repository, there have been significant developments in OpoLua over the past few months, with Tom landing a huge feature last week in the form of an interactive decompiler and debugger, complete with breakpoints and live updates:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2026-02-28-publishing-opolua-for-debian-and-ubuntu/dragonfell-debugger-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2026-02-28-publishing-opolua-for-debian-and-ubuntu/dragonfell-debugger@2x/1600.png\"\n             width=\"800.0\"\n             height=\"519.5\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Peeking at the inner workings of <a href=\"https://cyningstan.itch.io\">Cyningstan</a>&rsquo;s recent release for the Series 3, <a href=\"https://cyningstan.itch.io/dragonfell-psion-3\">Dragonfell</a>.</p> \n<hr /> \n<p>And finally, OpoLua wasn&rsquo;t the only software to receive a release this week: the new apt repository also contains Debian and Ubuntu builds of <a href=\"https://github.com/inseven/reporter\">Reporter</a>, my lightweight file change report generator.</p>",
      "date_published": "2026-02-28T11:22:50-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-02-28-publishing-opolua-for-debian-and-ubuntu/",
      "title": "Publishing OpoLua for Debian and Ubuntu",
      "url": "https://jbmorley.co.uk/posts/2026-02-28-publishing-opolua-for-debian-and-ubuntu/"
    },
    {
      "content_html": "<p>Coming off the back of my <a href=\"/december-adventure\">December Adventure</a>, I was incredibly keen to continue a practice of daily writing&mdash;I found the process deeply rewarding and a wonderful opportunity to reflect on my work, share my process, and be more deliberate about how I approach my projects. Now, nearly a month since I last wrote, it&rsquo;s clear that, while this works well in the context of adventuring, it doesn&rsquo;t translate to my typical way of working. I&rsquo;d like to reflect on why.</p> \n<p>During my December Adventure, I deliberately selected relatively short, self-contained tasks that were fun or immediately rewarding&mdash;things I felt others in the Psion community might enjoy reading about&mdash;and, consequently, that made the process of writing a joy. The work of exploring and supporting older computers is also wonderfully broad, offers a wide variety of problems, and is charmingly pure when contrasted with the myriad cultural and political shifts happening in the industry and world at large. Simply put: it is something I am happy and excited to engage in and share.</p> \n<p>As I&rsquo;ve transitioned back to more work-like tasks, I find I&rsquo;m taking on bigger projects in more contemporary languages that are, frankly, less fun&mdash;what I&rsquo;m doing now is driven more by the outcome than the joy of the journey. Specifically, I&rsquo;ve been focused on infrastructural work in both <a href=\"https://folders.jbmorley.co.uk\">Folders</a>, my Photos-like file manager for macOS, and <a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a>, my Psion connectivity suite for macOS. This work has necessitated spending many days bashing my head against Swift and SwiftUI, and it&rsquo;s not fun to write about the endless process of working around Apple&rsquo;s phoned-in APIs and absentee-parent approach to language design. It&rsquo;s also much harder, working on modern platforms, to ignore the spectre of AI and how it&rsquo;s fundamentally changing the culture of creation&mdash;a shift that only seems to have accelerated in the past couple of months, and one that quells the deep enthusiasm and optimism I usually hold for engineering.</p> \n<p>With all this in mind, I&rsquo;m going to go back to more ad-hoc writing, highlighting some of the larger pieces of work when I complete them, and documenting my little side-quests as and when they occur. I also hope to take some time to sit down and write about my evolving feelings around AI&mdash;specifically how it continues a decades-long trend of abdicating responsibility in product and infrastructure design.</p> \n<hr /> \n<p><em>(I note that <a href=\"https://eli.li\">Eli</a> has <a href=\"https://eli.li/december-adventure-march-2026\">designated the Ides of March a week of adventuring</a>, something I suspect many of us desperately need. I have plans.)</em></p>",
      "date_published": "2026-02-22T12:24:15-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-02-22-rethinking-writing/",
      "title": "Rethinking Writing",
      "url": "https://jbmorley.co.uk/posts/2026-02-22-rethinking-writing/"
    },
    {
      "content_html": "<p>The week got off to a slow start as I found myself spending much of Monday writing up the remainder of week 3. Beyond that, I spent time putting a final base coat of paint on <a href=\"/posts/2026-01-19-week-3-tuesday-onwards/#signage\">the sign for our friend&rsquo;s coffee shop</a>, briefly revisited the world of <a href=\"#psion-roms\">Psion emulation</a>, and set my intentions for the week ahead.</p> \n<h1><a id=\"psion-roms\"></a>Psion ROMs</h1> \n<p>Nigel, the <a href=\"htps://psion.community\">Psion community</a>&rsquo;s resident <a href=\"https://www.mamedev.org/\">MAME</a> expert, has been on a renewed push to get folks to dig out their Psions (not something that needs much encouragement), check their ROM versions, and dump them if they&rsquo;re not already on record.</p> \n<p>Much to my surprise, neither of the builds on my <a href=\"/computers/psion/revo-plus/\">Revo Plus</a> or <a href=\"/computers/psion/series-7/\">Series 7</a> had been dumped, so I broke out <a href=\"https://github.com/explit28/Psion-ROM/tree/main/Tools/PsiROM\">PsiROMx</a> and set about rectifying this travesty. Fortunately, the process of dumping an EPOC32 ROM is easy: simply select &lsquo;Save ROM&rsquo; and specify the output location. (There seem to be some issues dumping Series 5mx Pro devices, but there are still many earlier devices we need to archive, and PsiROMx serves us well here.)</p> \n<p>\n <body>   \n  <img src=\"/posts/2026-01-21-week-4-monday/psiromx/400.png\" width=\"400\" height=\"300\" x-srcset=\"/posts/2026-01-21-week-4-monday/psiromx/400.png 400 300,/posts/2026-01-21-week-4-monday/psiromx/800.png 640 480,/posts/2026-01-21-week-4-monday/psiromx/1200.png 640 480,/posts/2026-01-21-week-4-monday/psiromx/1600.png 640 480,\" />  \n </body></p> \n<p class=\"caption\">PsiROMx&rsquo;s interface is wonderfully simple</p> \n<p>I uploaded these two new ROMs to the <a href=\"https://github.com/explit7/Psion-ROM\">Psion-ROM</a> archive on GitHub and was rewarded not long after by the following screenshots of my Revo Plus ROM running in MAME:</p> \n<p>\n <body>   \n  <img src=\"/posts/2026-01-21-week-4-monday/revo-boot/400.png\" width=\"400\" height=\"157\" x-srcset=\"/posts/2026-01-21-week-4-monday/revo-boot/400.png 400 157,/posts/2026-01-21-week-4-monday/revo-boot/800.png 527 208,/posts/2026-01-21-week-4-monday/revo-boot/1200.png 527 208,/posts/2026-01-21-week-4-monday/revo-boot/1600.png 527 208,\" />  \n </body></p> \n<p>\n <body>   \n  <img src=\"/posts/2026-01-21-week-4-monday/revo-system/400.png\" width=\"400\" height=\"157\" x-srcset=\"/posts/2026-01-21-week-4-monday/revo-system/400.png 400 157,/posts/2026-01-21-week-4-monday/revo-system/800.png 527 208,/posts/2026-01-21-week-4-monday/revo-system/1200.png 527 208,/posts/2026-01-21-week-4-monday/revo-system/1600.png 527 208,\" />  \n </body></p> \n<p>Since I was already poking around in the Psion-ROM repository, I also took a few minutes to set up <a href=\"https://github.com/explit28/Psion-ROM/actions/workflows/build.yaml\">automated builds</a> that package ROMs for use with MAME&mdash;I&rsquo;d love to a establish a single source of ROMs for MAME-based Psion emulation and use this in <a href=\"https://codeberg.org/psion/psiemu\">PsiEmu</a> to make it easy for folks to get started.</p> \n<h1><a id=\"next-steps\"></a>Next Steps</h1> \n<p>Having seen some indication that the hanging issues I&rsquo;ve been seeing with my website builds might be a bug in <a href=\"https://incontext.jbmorley.co.uk\">InContext</a>&mdash;my static site builder&mdash;I&rsquo;ve decided to focus on that for the week. Beyond debugging the hang, I have a growing list of fixes and improvements to make and, if time permits, I&rsquo;d love to flesh out the Linux support.</p>",
      "date_published": "2026-01-21T11:38:00-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-01-21-week-4-monday/",
      "title": "Week 4&mdash;Monday",
      "url": "https://jbmorley.co.uk/posts/2026-01-21-week-4-monday/"
    },
    {
      "content_html": "<p>Keen to make forward progress in spite of other (mostly administrative) distractions, I found myself spending the rest of the week 3 bouncing between infrastructural tasks that I hope will help lay the foundations for future work: <a href=\"#trying-portainer\">trying Portainer</a>, <a href=\"#self-hosting-forgejo\">self-hosting Forgejo</a>, and <a href=\"#installing-freshrss\">installing FreshRSS</a>. I also took a little time out to continue with some <a href=\"#signage\">real world maintenance</a>.</p> \n<h1><a id=\"trying-portainer\"></a>Trying Portainer</h1> \n<p>While I&rsquo;ve been running home infrastructure for a little while, I&rsquo;m fairly new to the whole thing and I&rsquo;ve yet to establish my own preferences and best practices. Using <a href=\"https://www.docker.com/\">Docker</a> to run services, for example, still makes me deeply uncomfortable: I&rsquo;ve been using <a href=\"https://docs.docker.com/compose/\">Docker Compose</a>&mdash;<code>docker-compose.yml</code> files are easy to version using a combination git Git and Ansible&mdash;but I find updates and container life cycles hard to manage. With that in mind, I decided to take a shot at using <a href=\"https://www.portainer.io/\">Portainer</a>.</p> \n<p>Bootstrapping Portainer proved incredibly easy using Docker Compose:</p> \n<pre><code class=\"language-yaml\">services:\n  portainer:\n    container_name: portainer\n    image: portainer/portainer-ce:lts\n    restart: always\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n      - /storage/services/portainer/data:/data\n    ports:\n      - 9443:9443\n      - 8000:8000\n\nnetworks:\n  default:\n    name: portainer_network\n</code></pre> \n<p>(This deviates very slightly from the <a href=\"https://docs.portainer.io/start/install-ce/server/docker/linux#deployment\">off-the-shelf configuration</a>, mapping <code>/data</code> to <code>/storage/services/portainer/data</code> to ensure it&rsquo;s stored on my ZFS pool and easy to back up.)</p> \n<p>Once it was up and running (<code>docker compose up -d</code>), I found Portainer offers a comprehensive management interface, allowing you to create new container &lsquo;stacks&rsquo; using Docker Compose files:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2026-01-19-week-3-tuesday-onwards/portainer-stacks-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2026-01-19-week-3-tuesday-onwards/portainer-stacks@2x/1600.png\"\n             width=\"800.0\"\n             height=\"672.0\"\n             />\n    </picture>\n</div>\n</p> \n<p>Given how easy it was to set up, I wish I&rsquo;d tried Portainer earlier: I&rsquo;m already finding its container and image management very convenient, and it feels like a great low-effort tool for trying out new services. I may, however, still turn to manually managed compose files for services that I choose to keep around.</p> \n<h1><a id=\"self-hosting-forgejo\"></a>Self-Hosting Forgejo</h1> \n<p>Over the past couple of weeks, I&rsquo;ve noticed an uptick in hangs with my website builds using GitHub Actions. This has got in the way of writing, and I decided to see what I could do to improve things. Thinking the issue might be related to a longstanding issue in .NET process management and something that&rsquo;s unlikely to get fixed any time soon (that the GitHub Actions runner might be failing to notice my build script terminating successfully), I decided to make the most of the opportunity to try out <a href=\"https://forgejo.org\">Forgejo</a> to store and build my site.</p> \n<p>With Portainer ready to go, installing Forgejo provided incredibly easy&mdash;I just used the Docker Compose file from their <a href=\"https://forgejo.org/docs/latest/admin/installation/docker/#docker\">installation instructions</a> (again mapping the data volume to my ZFS storage):</p> \n<pre><code class=\"language-yaml\">services:\n  forgejo:\n    image: codeberg.org/forgejo/forgejo:13\n    container_name: forgejo\n    restart: always\n    environment:\n      - USER_UID=1000\n      - USER_GID=1000\n    volumes:\n      - /storage/services/forgejo/data:/data\n      - /etc/timezone:/etc/timezone:ro\n      - /etc/localtime:/etc/localtime:ro\n    ports:\n      - \"3000:3000\"\n      - \"222:22\"\n</code></pre> \n<p>Everything just worked and, armed with a working Forgejo instance, I pushed my website&mdash;including 50GB of LFS files. Much to my surprise, this also worked (once I remembered to run <code>git lfs fetch --all origin main</code> to ensure I had a local copy of all LFS data)&mdash;I had been fully expecting Git LFS to require further setup, but it seems it&rsquo;s provisioned out of the box with Forgejo.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2026-01-19-week-3-tuesday-onwards/forgejo-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2026-01-19-week-3-tuesday-onwards/forgejo@2x/1600.png\"\n             width=\"800.0\"\n             height=\"672.0\"\n             />\n    </picture>\n</div>\n</p> \n<p>Configuring automated builds using <a href=\"https://forgejo.org/docs/next/user/actions/reference/\">Forgejo Actions</a> proved a little more nuanced however: the Forgejo runner doesn&rsquo;t support macOS, so you have to use <a href=\"https://gitea.com\">Gitea</a>&rsquo;s <a href=\"https://docs.gitea.com/usage/actions/act-runner\">Act Runner</a> instead. I also encountered real problems with Git LFS checkouts on the runner, necessitating a manual&mdash;and worryingly precarious&mdash;checkout step<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>:</p> \n<pre><code class=\"language-yaml\">- name: Checkout source\n  shell: bash\n  env:\n    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  run: |\n    set -euo pipefail\n    URL_BASE=\"${{ github.server_url }}\"\n\n    mkdir -p \"$CHECKOUT_ROOT\"\n    if [ ! -d \"$CHECKOUT_ROOT/.git\" ]; then\n      git clone --origin origin --no-checkout \"https://${URL_BASE#https://}/${{ github.repository }}.git\" \"$CHECKOUT_ROOT\"\n    fi\n\n    cd \"$CHECKOUT_ROOT\"\n    git clean -fdx\n    git config url.\"https://${GITHUB_TOKEN}@${URL_BASE#https://}/\".insteadOf \"https://${URL_BASE#https://}/\"\n    git config --unset-all http.${{ github.server_url }}/.extraheader || true  # Unhappy LFS workaround.\n    git fetch --prune --tags origin\n    git checkout \"${{ github.ref_name }}\"\n    git reset --hard \"origin/${{ github.ref_name }}\"\n</code></pre> \n<p>From <a href=\"https://codeberg.org/forgejo/forgejo/issues/7264\">the discussion</a> on Forgejo&rsquo;s issue tracker, it seems the LFS issue might be related to my running Forgejo behind an nginx proxy. I&rsquo;d like to investigate this but, for the time being, I have an approach that works.</p> \n<p>With the checkout issues resolved, the rest of the build worked with no changes, lifted verbatim from my GitHub Actions workflow&mdash;it&rsquo;s really quite impressive and fills me with hope for migrating other projects. I think I might still be seeing infrequent hangs in my builds though, so I wonder if there&rsquo;s a race condition buried deep in <a href=\"https://incontext.jbmorley.co.uk\">InContext</a>&rsquo;s asynchronous code. Something to keep an eye on. 👀</p> \n<h1><a id=\"installing-freshrss\"></a>Installing FreshRSS</h1> \n<p>Having written about the process of installing two services already this week, I&rsquo;ll not go into much detail about <a href=\"https://freshrss.org\">FreshRSS</a>. Suffice to say, I used Docker, Portainer, and nginx as a reverse proxy. It went smoothly.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2026-01-19-week-3-tuesday-onwards/freshrss-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2026-01-19-week-3-tuesday-onwards/freshrss@2x/1600.png\"\n             width=\"800.0\"\n             height=\"731.0\"\n             />\n    </picture>\n</div>\n</p> \n<p>It&rsquo;s nice to finally have a self-hosted feed reader again after nearly 20 years (though I still long for Shaun Inman&rsquo;s Fever feed reader), and I&rsquo;ve been pleasantly surprised to discover it&rsquo;s well supported by client apps like <a href=\"https://netnewswire.com/\">NetNewsWire</a>. I&rsquo;m hopeful the switch will unlock syncing with various retro computers in the future.</p> \n<h1><a id=\"signage\"></a>Signage</h1> \n<p>The week also brought an analogue pursuit in the form of some maintenance work on the sign for <a href=\"https://melemelebakery.com/\">Mele Mele</a>, our friend&rsquo;s coffee shop. The Hawaiian climate is brutal and will destroy near-everything, so I took the opportunity of Sarah repainting the sign to reinforce it in the hope it&rsquo;ll last another few years.</p> \n<p><ul class=\"photos\">\n                                <li style=\"flex: 13.333333333333;\">\n            <a href=\"/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9221/\"\n               style=\"padding-bottom: 75.0%;\"\n               >\n                                                        \n        <img src=\"/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9221/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9221/400.jpeg 400 300,/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9221/800.jpeg 800 600,/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9221/1200.jpeg 1200 900,/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9221/1600.jpeg 1600 1200,\" />\n                            </a>\n        </li>\n                            <li style=\"flex: 13.333333333333;\">\n            <a href=\"/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9222/\"\n               style=\"padding-bottom: 75.0%;\"\n               >\n                                                        \n        <img src=\"/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9222/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9222/400.jpeg 400 300,/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9222/800.jpeg 800 600,/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9222/1200.jpeg 1200 900,/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9222/1600.jpeg 1600 1200,\" />\n                            </a>\n        </li>\n                            <li style=\"flex: 13.333333333333;\">\n            <a href=\"/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9225/\"\n               style=\"padding-bottom: 75.0%;\"\n               >\n                                                        \n        <img src=\"/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9225/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9225/400.jpeg 400 300,/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9225/800.jpeg 800 600,/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9225/1200.jpeg 1200 900,/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9225/1600.jpeg 1600 1200,\" />\n                            </a>\n        </li>\n                            <li style=\"flex: 7.5046904315197;\">\n            <a href=\"/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9229/\"\n               style=\"padding-bottom: 133.25%;\"\n               >\n                                                        \n        <img src=\"/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9229/400.jpeg\" width=\"400\" height=\"533\" x-srcset=\"/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9229/400.jpeg 400 533,/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9229/800.jpeg 800 1066,/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9229/1200.jpeg 1200 1600,/posts/2026-01-19-week-3-tuesday-onwards/mele-mele-sign/IMG_9229/1600.jpeg 1600 2133,\" />\n                            </a>\n        </li>\n    </ul>\n</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>This step is even more complex than it might be as I want to cache checkouts between builds to avoid having to download all 50GB of my site on every build&mdash;the Act Runner differs from the GitHub Actions runner in that it doesn&rsquo;t use stable checkout locations, necessitating some contortions.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2026-01-19T17:41:06-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-01-19-week-3-tuesday-onwards/",
      "title": "Week 3—Tuesday Onwards",
      "url": "https://jbmorley.co.uk/posts/2026-01-19-week-3-tuesday-onwards/"
    },
    {
      "content_html": "<p>Week 3 of the year continues the concerted (if somewhat piecemeal) process of project spring cleaning. Shipping the Qt version of <a href=\"#opolua\">OpoLua</a> remains a priority and I spent much of the day on release adjacent tasks. I also found my thoughts turning to grand ideas of finally packaging things like <a href=\"https://github.com/inseven/reporter\">Reporter</a>, <a href=\"https://github.com/jbmorley/changes\">changes</a>, and <a href=\"https://incontext.jbmorley.co.uk/\">InContext</a>, but I must resist! (For the time being, at least.) That said, I did take a little time out to work on the endless task that is <a href=\"#home-infrastructure\">home infrastructure</a>.</p> \n<h1><a id=\"opolua\"></a>OpoLua</h1> \n<p><a href=\"/posts/2026-01-08-week-2-wednesday/\">Last week</a>, before getting distracted by <a href=\"/posts/2026-01-12-week-2-thursday/\">packaging all the things</a>, I identified a few remaining tasks for shipping <a href=\"https://oppolua.org\">OpoLua</a>, our modern <a href=\"https://en.wikipedia.org/wiki/Open_Programming_Language\">OPL</a> runtime:</p> \n<ul> \n <li>download links</li> \n <li>screenshots</li> \n <li>documentation</li> \n <li>consolidate shared resources in the source tree</li> \n</ul> \n<p>Of these, the highest priorities seemed to be &lsquo;<a href=\"#download-links\">download links</a>&rsquo;, and &lsquo;<a href=\"#documentation\">documentation</a>&rsquo;. Both of these will help users install the app: links to downloadable packages; and , for Linux, instructions for how to add our (as yet non-existent) package repositories.</p> \n<h2><a id=\"download-links\"></a>Download Links</h2> \n<p>The OpoLua website is a <a href=\"https://jekyllrb.com/\">Jekyll</a> static site that lives the <code>docs</code> folder of the source tree, and is built as part of our <a href=\"https://github.com/inseven/opolua/actions/workflows/build.yaml\">GitHub Actions build workflow</a>. This makes it really easy to inject details like the current version number into the site build and, armed with the <a href=\"https://github.com/iBug/jekyll-environment-variables\"><code>jekyll-environment-variables</code></a> plugin, it was easy to update the template to add a link to the latest GitHub release:</p> \n<pre><code class=\"language-html\">&lt;p class=\"download-links\"&gt;\n\n    &lt;a href=\"https://apps.apple.com/app/opolua/id1604029880\"&gt;\n      &lt;img src=\"images/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg\" /&gt;\n    &lt;/a&gt;\n\n    &lt;br/&gt;\n\n    &lt;a href=\"https://github.com/inseven/opolua/releases/tag/{{ site.env.VERSION_NUMBER }}\"&gt;\n      Download for macOS, Windows, and Linux\n    &lt;/a&gt;\n    \n&lt;/p&gt;\n</code></pre> \n<p>While I&rsquo;d like to increase the prominence of these new platforms (the App Store link dominates somewhat), something is better than nothing, and I&rsquo;m pretty pleased with the result:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2026-01-13-week-3-monday/opolua-download-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2026-01-13-week-3-monday/opolua-download@2x/1600.png\"\n             width=\"800.0\"\n             height=\"845.0\"\n             />\n    </picture>\n</div>\n</p> \n<h2><a id=\"documentation\"></a>Documentation</h2> \n<p>As OpoLua has grown to target multiple platforms, support different OPL versions, and include a suite of CLI utilities, it&rsquo;s become clear that short FAQ we created when we first shipped was insufficient. Keen to make room for growth, I added a <a href=\"https://opolua.org/docs\">documentation section</a>, styled after <a href=\"https://just-the-docs.com/\">Just The Docs</a>:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2026-01-13-week-3-monday/opolua-documentation-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2026-01-13-week-3-monday/opolua-documentation@2x/1600.png\"\n             width=\"800.0\"\n             height=\"770.5\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Our documentation is sparse but now there&rsquo;s somewhere to put it</p> \n<p>To complement these structural changes, I also added <a href=\"https://highlightjs.org/\">highlight.js</a>, <a href=\"https://mermaid.js.org/\">Mermaid</a>, and <a href=\"https://github.com/Helveg/jekyll-gfm-admonitions/\">jekyll-gfm-admonitions</a>, to provide us with conveniences for syntax highlighting, diagramming, and <a href=\"https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts\">GitHub-style admonitions</a> respectively.</p> \n<h1><a id=\"home-infrastructure\"></a>Home Infrastructure</h1> \n<p>Over the past few days, I&rsquo;ve been slowly pushing on with my home infrastructure setup. I finally resigned myself to using <a href=\"https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts\">Ansible</a> for managing my NAS/server&mdash;hopefully this will make it easier to keep the configuration in source control and increase my confidence as I continue to move away from Big Tech hosted services.</p> \n<p>I also took delivery of a new 1U rack shelf and spent a little while moving my <a href=\"https://www.lincplustech.com/products/lincstation-n2-network-attached-storage\">LincStation N2</a> NAS/server into the rack, indulging in the prerequisite CAD and 3D printing to mount the network switch alongside it:</p> \n<p>\n <body>   \n  <img src=\"/posts/2026-01-13-week-3-monday/rack/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2026-01-13-week-3-monday/rack/400.jpeg 400 300,/posts/2026-01-13-week-3-monday/rack/800.jpeg 800 600,/posts/2026-01-13-week-3-monday/rack/1200.jpeg 1200 900,/posts/2026-01-13-week-3-monday/rack/1600.jpeg 1600 1200,\" />  \n </body></p>",
      "date_published": "2026-01-13T12:13:24-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-01-13-week-3-monday/",
      "title": "Week 3&mdash;Monday",
      "url": "https://jbmorley.co.uk/posts/2026-01-13-week-3-monday/"
    },
    {
      "content_html": "<p>Having spent an <a href=\"/posts/2026-01-12-week-2-thursday/\">overly long day</a> working on packaging OpoLua, Friday proved a slow one, with little visible progress: I took some time to plan out the remaining work to ship <a href=\"https://opolua.org\">OpoLua</a>; added Thursday&rsquo;s binary Arch builds to the releases<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>; and tidied up the repository a little. Good work, but unexciting.</p> \n<p>Without a doubt, the highlight of the day was this screenshot from <a href=\"https://oldbytes.space/@thelastpsion\">Alex</a>, host of the Psion Discord and maintainer of <a href=\"https://www.haiku-os.org/\">Haiku</a>&rsquo;s MAME package:</p> \n<p>\n <body>   \n  <img src=\"/posts/2026-01-12-week-2-friday/opolua-haiku/400.jpeg\" width=\"400\" height=\"250\" x-srcset=\"/posts/2026-01-12-week-2-friday/opolua-haiku/400.jpeg 400 250,/posts/2026-01-12-week-2-friday/opolua-haiku/800.jpeg 800 500,/posts/2026-01-12-week-2-friday/opolua-haiku/1200.jpeg 1200 750,/posts/2026-01-12-week-2-friday/opolua-haiku/1600.jpeg 1600 1000,\" />  \n </body></p> \n<p>I continue to be amazed by how portable Qt is, and there&rsquo;s little better in life than a Haiku screenshot.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>I plan to follow up with a PKGBUILD distribution for those who prefer source builds, but something is better than nothing.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2026-01-12T11:24:09-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-01-12-week-2-friday/",
      "title": "Week 2&mdash;Friday",
      "url": "https://jbmorley.co.uk/posts/2026-01-12-week-2-friday/"
    },
    {
      "content_html": "<p>Continuing with my goal of trying to ship the <a href=\"https://en.wikipedia.org/wiki/Qt_(software)\">Qt</a> variant of <a href=\"https://opolua.org\">OpoLua</a>&mdash;our modern OPL interpreter&mdash;for macOS, Windows, and Linux, I spent much of the day diving into the ugly world of <a href=\"#opolua\">Linux packaging</a>. To make things a little more palatable, I also treated myself to a <a href=\"#reconnect-menu\">small update</a> to <a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a>, my Psion connectivity suite for macOS.</p> \n<h1><a id=\"opolua\"></a>OpoLua</h1> \n<p>My primary focus for the day was to set up Linux smoke-test builds to ensure that we don&rsquo;t unintentionally break builds on the platform. Having <a href=\"/posts/2026-01-08-week-2-wednesday/#re-licensing-opolua\">already figured out how to build on Ubuntu</a>, setting up automated builds proved easy&mdash;a matter of adding a <a href=\"https://github.com/inseven/opolua/blob/793be8ccb5974eb22a2e6fba8b19f8c51a69eee5/.github/workflows/build.yaml#L108\">GitHub Actions job</a> to the workflow, and crafting a <a href=\"https://github.com/inseven/opolua/blob/793be8ccb5974eb22a2e6fba8b19f8c51a69eee5/scripts/build-qt-linux.sh\">lightweight build script</a>. I continue to be impressed by Qt&rsquo;s tooling, which reduces builds on almost all platforms to:</p> \n<pre><code class=\"language-sh\">qmake6\nmake\n</code></pre> \n<p>Spurred on by a quick win, and lulled into a false sense of security, I decided to try tackling packaging for Ubuntu. When I&rsquo;ve built Debian packages in the past using the standard tooling, I&rsquo;ve been frustrated by the need for distro-specific directory structures so, to avoid polluting our source tree, I decided to look for an alternative. This took me to <a href=\"https://fpm.readthedocs.io/en/latest/\"><code>fpm</code></a> or &lsquo;Effing Package Management&rsquo;, a multi-distro packaging tool which proved incredibly powerful, allowing me to generate a .deb from the directory output of Qt&rsquo;s <code>make install</code> using a single command:</p> \n<pre><code class=\"language-shi\">ARCHITECTURE=`dpkg --print-architecture`\nfpm \\\n    -s dir \\\n    -t deb \\\n    -p \"opolua-ubuntu-24.04-$ARCHITECTURE-$VERSION_NUMBER-$BUILD_NUMBER.deb\" \\\n    --name \"opolua\" \\\n    --version $VERSION_NUMBER \\\n    --architecture \"$ARCHITECTURE\" \\\n    --description \"Runtime and viewer for EPOC programs and files.\" \\\n    --url \"https://opolua.org\" \\\n    --maintainer \"Jason Morley &lt;support@jbmorley.co.uk&gt;\" \\\n    --depends libqt6core6 \\\n    --depends libqt6gui6 \\\n    --depends libqt6widgets6 \\\n    --depends libqt6multimedia6 \\\n    --depends libqt6core5compat6 \\\n    --chdir \"$INSTALL_DIRECTORY\" \\\n    .\n</code></pre> \n<p>(You&rsquo;ll notice I&rsquo;m getting the architecture with <code>dpkg --print-architecture</code>&mdash;using ARM and Intel runners with GitHub Actions <a href=\"https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/run-job-variations\">matrix builds</a>, the build script can output releases for both architectures.)</p> \n<p>I shared the builds with a few members of the <a href=\"https://psion.community\">Psion Discord</a>, and <a href=\"https://colinhoad.com\">Colin</a> kindly sent me a couple of screenshots of OpoLua running on <a href=\"https://kubuntu.org/\">Kubuntu</a>&mdash;it&rsquo;s a real reward to see others using our creation.</p> \n<p>\n <body>   \n  <img src=\"/posts/2026-01-12-week-2-thursday/kubuntu-welcome/400.png\" width=\"400\" height=\"277\" x-srcset=\"/posts/2026-01-12-week-2-thursday/kubuntu-welcome/400.png 400 277,/posts/2026-01-12-week-2-thursday/kubuntu-welcome/800.png 800 554,/posts/2026-01-12-week-2-thursday/kubuntu-welcome/1200.png 1200 831,/posts/2026-01-12-week-2-thursday/kubuntu-welcome/1600.png 1600 1108,\" />  \n </body></p> \n<p>\n <body>   \n  <img src=\"/posts/2026-01-12-week-2-thursday/kubuntu-jumpy/400.png\" width=\"400\" height=\"250\" x-srcset=\"/posts/2026-01-12-week-2-thursday/kubuntu-jumpy/400.png 400 250,/posts/2026-01-12-week-2-thursday/kubuntu-jumpy/800.png 800 500,/posts/2026-01-12-week-2-thursday/kubuntu-jumpy/1200.png 1200 750,/posts/2026-01-12-week-2-thursday/kubuntu-jumpy/1600.png 1600 1000,\" />  \n </body></p> \n<p class=\"caption\"><a href=\"https://software.psion.community/programs/uid/0x1000131a/\">Jumpy!</a> is a wonderful showcase for OPL</p> \n<p>While there are still many different Linux distributions and versions to add to the build script (and I&rsquo;d like to set up an apt repository to ensure folks can get automatic updates), I&rsquo;m pretty pleased with this progress.</p> \n<p>&hellip; But I couldn&rsquo;t leave it there. I decided to try tackling <a href=\"https://archlinux.org/\">Arch Linux</a>&mdash;a distro I&rsquo;ve little experience of. Starting this at 10pm was perhaps a poor choice. <code>fpm</code> did its job well, and I able to change a couple of flags and output a <a href=\"https://wiki.archlinux.org/title/Pacman\">pacman</a> binary package, but <a href=\"https://psion.community/\">Alex</a> quickly dissuaded me of the notion that this was good enough: binary packages are (understandably) frowned upon in Archland and <a href=\"https://wiki.archlinux.org/title/PKGBUILD\">PKGBUILD</a> source builds (distributed via <a href=\"https://aur.archlinux.org/\">AUR</a>) are preferable as they increase transparency.</p> \n<p>PKGBUILD configuration files seem mostly well designed and, with Alex&rsquo;s help, I was able to craft <a href=\"PKGBUILD\">something that worked</a>. The big challenge was how <a href=\"https://git-scm.com/book/en/v2/Git-Tools-Submodules\">Git submodules</a> are (or aren&rsquo;t) handled: while there&rsquo;s good support for Git, there&rsquo;s absolutely no support for submodules&mdash;you have to manually recreate these in your <code>prepare</code>. For a project like OpoLua that relies heavily on recursive submodules, this is a miserable experience. <a href=\"https://wiki.archlinux.org/title/Makepkg\"><code>makepkg</code></a> (which processes <code>PKGBUILD</code> files) will check out each submodule at the top-level, and you have to rewrite all the submodule urls from the bottom up:</p> \n<pre><code class=\"language-plaintext\">prepare() {\n\n    cd \"$srcdir/opolua\"\n    git submodule init\n    git config submodule.LuaSwift.url \"$srcdir/LuaSwift\"\n    git config submodule.diligence.url \"$srcdir/diligence\"\n    git config submodule.scripts/changes.url \"$srcdir/changes\"\n    git config submodule.scripts/build-tools.url \"$srcdir/build-tools\"\n    git -c protocol.file.allow=always submodule update --recursive\n\n    cd \"$srcdir/opolua/dependencies/LuaSwift\"\n    git submodule init\n    git config submodule.Sources/CLua/lua.url \"$srcdir/lua\"\n    git -c protocol.file.allow=always submodule update\n\n}\n</code></pre> \n<p>I&rsquo;ve yet to work out how to synthesize per-release versions of the PKGBUILD file, or how to get those into AUR, but it&rsquo;s good to have broken the back of this.</p> \n<h1><a id=\"reconnect-menu\"></a>Reconnect Menu</h1> \n<p>While Apple&rsquo;s <a href=\"https://en.wikipedia.org/wiki/Liquid_Glass\">Liquid Glass</a> is incredibly divisive and widely derided (I personally dislike it), I&rsquo;m keen for my apps to feel at home on the system&mdash;I don&rsquo;t want to add to users' cognitive burden by rejecting platform choices. With that in mind, I&rsquo;ve been slowly adopting the new menu icons, and I took a little time to add them to the Reconnect menu bar menu:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2026-01-12-week-2-thursday/reconnect-menu-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2026-01-12-week-2-thursday/reconnect-menu@2x/1600.png\"\n             width=\"603.5\"\n             height=\"341.5\"\n             />\n    </picture>\n</div>\n</p> \n<p>It&rsquo;s not clear to me that this is an improvement&mdash;there&rsquo;s a recent piece over at <a href=\"https://tonsky.me/blog/tahoe-icons/\">tonsky.me</a> that captures many of the issues&mdash;but it&rsquo;s where things are going. Perhaps I&rsquo;ll use <a href=\"https://indieweb.social/@brentsimmons/115846213935605782\">this investigation</a> by Brent Simmons to give users a choice.</p>",
      "date_published": "2026-01-12T11:03:19-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-01-12-week-2-thursday/",
      "title": "Week 2&mdash;Thursday",
      "url": "https://jbmorley.co.uk/posts/2026-01-12-week-2-thursday/"
    },
    {
      "content_html": "<p>Having spent the first couple of days of the week bashing my head against Windows and GitHub Actions workflows, I decided to take a &lsquo;break&rsquo; to focus on nailing down the licensing for <a href=\"https://opolua.org\">OpoLua</a>&mdash;it might not be glamorous, but it&rsquo;s a necessity when shipping software in today&rsquo;s world. Thankfully that didn&rsquo;t take the whole day, so I was able to try out OpoLua Qt on Linux for the first time, and even squeeze in a little Psion-related fun. I also found myself returning&mdash;however briefly&mdash;to <a href=\"https://gameplaycolor.com\">Game Play Color</a>, my semi-retired JavaScript Game Boy emulator.</p> \n<h2><a id=\"re-licensing-opolua\"></a>Re-licensing OpoLua</h2> \n<p><a href=\"https://en.wikipedia.org/wiki/Qt_(software)\">Qt</a>&rsquo;s licensing mandates that any software linking the Qt libraries be licensed <a href=\"https://www.gnu.org/licenses/old-licenses/gpl-2.0.html#SEC1\">GPLv2 or Later</a> for non-commercial access to the library. Keen to keep the more permissive <a href=\"https://en.wikipedia.org/wiki/MIT_License\">MIT License</a> where possible, I&rsquo;ve re-licensed just the Qt app under GPL2 or Later and updated our documentation to be far more explicit about the project&rsquo;s components and licenses: the Lua code which provides the OPL runtime remains MIT licensed, as does the iOS app. You can read all the gory details on the <a href=\"https://opolua.org/license/\">website</a>. This represents a significant milestone in officially shipping OpoLua Qt: we can now do so legally. 🥳</p> \n<p>Desperate for a change of scene, I also spent a little time setting up an Ubuntu VM and getting OpoLua Qt building on Linux&mdash;Ubuntu&rsquo;s status as the only GitHub Actions Linux runner makes it a natural first choice for automated builds. Once I realized <a href=\"https://doc.qt.io/qt-6/qmake-manual.html\"><code>qmake</code></a> has been renamed to <code>qmake6</code> on Ubuntu, I was happy to find everything just worked:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2026-01-08-week-2-wednesday/opolua-ubuntu-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2026-01-08-week-2-wednesday/opolua-ubuntu@2x/1600.png\"\n             width=\"800.0\"\n             height=\"613.5\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">It&rsquo;s incredibly exciting to see OPL running in so many places</p> \n<h2><a id=\"psion-ebooks\"></a>Psion eBooks</h2> \n<p>After a morning of licenses and administration, I treated myself to a side-quest back into the world of Psions to explore another way that my Psion might offer daily utility: since letting my Kindle go (I&rsquo;m exhausted by the modern trend of personal devices becoming marketing vehicles and storefronts), I&rsquo;ve not had any digital reading solution and, recalling using a reader program for the Series 3c many years ago, I decided to fix that. Thanks to some help from the <a href=\"https://psion.community\">Psion Discord</a>, I rediscovered <a href=\"https://software.psion.community/programs/sha/dc5de7bf5f327ef9de40111a99ad5ea732b26deb7e6899bccc8e8899edb68fe5/\">eTxtReader</a>:</p> \n<p>\n <body>   \n  <img src=\"/posts/2026-01-08-week-2-wednesday/reader-unicode/400.jpeg\" width=\"400\" height=\"533\" x-srcset=\"/posts/2026-01-08-week-2-wednesday/reader-unicode/400.jpeg 400 533,/posts/2026-01-08-week-2-wednesday/reader-unicode/800.jpeg 800 1066,/posts/2026-01-08-week-2-wednesday/reader-unicode/1200.jpeg 1200 1600,/posts/2026-01-08-week-2-wednesday/reader-unicode/1600.jpeg 1600 2133,\" />  \n </body></p> \n<p class=\"caption\">Haruki Murakami&rsquo;s <a href=\"https://en.wikipedia.org/wiki/Birthday_Girl_(short_story)\">Birthday Girl</a> in eTxtReader on a Series 3mx</p> \n<p>I used <a href=\"https://calibre-ebook.com/\">Calibre</a> to convert some existing books to text files. They required some further massaging to translate from <a href=\"https://en.wikipedia.org/wiki/Unicode\">Unicode</a> to <a href=\"https://en.wikipedia.org/wiki/ASCII\">ASCII</a> (you&rsquo;ll see some strange characters if you look closely at the photo above) and, with this secondary pass, everything worked perfectly:</p> \n<p>\n <body>   \n  <img src=\"/posts/2026-01-08-week-2-wednesday/reader-ascii/400.jpeg\" width=\"400\" height=\"533\" x-srcset=\"/posts/2026-01-08-week-2-wednesday/reader-ascii/400.jpeg 400 533,/posts/2026-01-08-week-2-wednesday/reader-ascii/800.jpeg 800 1066,/posts/2026-01-08-week-2-wednesday/reader-ascii/1200.jpeg 1200 1600,/posts/2026-01-08-week-2-wednesday/reader-ascii/1600.jpeg 1600 2133,\" />  \n </body></p> \n<p>eTxtReader is a really impressive program, barely breaking a sweat when viewing multi-megabyte files on a device with at most 2MB RAM&mdash;a must for the modern Psion user.</p> \n<h2><a id=\"game-play-color\"></a>Game Play Color</h2> \n<p>Earlier in the week I received very short notice that <a href=\"https://www.netlify.com/\">Netlify</a> is ending the platform I&rsquo;ve been using to host Game Play Color. Not ready to let it go just yet, I decided to move to <a href=\"https://docs.github.com/en/pages\">GitHub Pages</a>. Fortunately, as a static site, it was a pretty easy process, and I was able to integrate it into the existing GitHub Actions workflows. (You can see the change <a href=\"https://github.com/gameplaycolor/gameplaycolor/pull/257/changes\">here</a> if you&rsquo;re interested in the details.)</p> \n<p>\n <body>   \n  <img src=\"/posts/2026-01-08-week-2-wednesday/crystalis@3x/400.png\" width=\"400\" height=\"867\" x-srcset=\"/posts/2026-01-08-week-2-wednesday/crystalis@3x/400.png 400 867,/posts/2026-01-08-week-2-wednesday/crystalis@3x/800.png 800 1734,/posts/2026-01-08-week-2-wednesday/crystalis@3x/1200.png 1179 2556,/posts/2026-01-08-week-2-wednesday/crystalis@3x/1600.png 1179 2556,\" />  \n </body></p> \n<p class=\"caption\">Game Play Color up and running with different hosting</p> \n<p>At some point, I imagine I&rsquo;ll let <code>gameplaycolor.com</code> go and continue my consolidation on <code>jbmorley.co.uk</code> subdomains&mdash;it just doesn&rsquo;t make sense to continue paying for so many domain names.</p> \n<hr /> \n<p>With OpoLua Qt macOS and Windows builds complete and available for folks to download, I now have to decide how many of the remaining tasks need tackling before moving onto other projects (and perhaps even feature work). The remaining OpoLua Qt housekeeping I have in mind is:</p> \n<ul> \n <li>Update website: \n  <ul> \n   <li>download links</li> \n   <li>screenshots</li> \n   <li>documentation</li> \n  </ul></li> \n <li>Set up Linux smoke test CI builds</li> \n <li>Consolidate shared resources in the source tree</li> \n</ul> \n<p>This feels like more than I&rsquo;ll manage in the remaining coupe of days this week (which are also being filled with various bits of immigration paperwork), so I&rsquo;ll do what I can. Providing a clear download link for OpoLua Qt on the website feels like the highest priority, followed by a Linux smoke-test build to ensure we don&rsquo;t unintentionally break those builds during development.</p> \n<p>Since this kind of work doesn&rsquo;t light up my creative and coding brain that much, I might also allow myself a small segue into tidying up a few aspects of <a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a>, my Psion connectivity suite&mdash;it&rsquo;s a tool I use daily, and small quality of life improvements there have an outsize positive impact&mdash;self-care for the retro software engineer if you will.</p>",
      "date_published": "2026-01-08T12:43:14-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-01-08-week-2-wednesday/",
      "title": "Week 2&mdash;Wednesday",
      "url": "https://jbmorley.co.uk/posts/2026-01-08-week-2-wednesday/"
    },
    {
      "content_html": "<p>Continuing my week of project spring cleaning (I&rsquo;m keen to clear the decks before launching into new things), I pushed on with the process of preparing <a href=\"https://opolua.org\">OpoLua Qt</a>&mdash;our OPL runtime for modern platforms&mdash;for release.</p> \n<h1><a id=\"opolua-qt-windows-builds\"></a>OpoLua Qt Windows Builds</h1> \n<p>While I finished <a href=\"/posts/2026-01-06-week-2-monday/\">Monday</a> with a building proof-of-concept Windows CI branch, there was still a lot of polish required&mdash;where possible, I like to avoid putting build commands directly in GitHub Actions workflows to ensure builds can be reproduced manually if necessary. I created a new build script (<a href=\"https://github.com/inseven/opolua/blob/cd505adb0b00538b32eb16e3d3a4447b666465f3/scripts/build-qt-windows.sh\"><code>build-qt-windows.sh</code></a>) which we can run locally in a pinch (or migrate more easily to other build systems when GitHub inevitably becomes untenable)&mdash;I prefer separate scripts for each build task as I&rsquo;ve found this helps ensure it&rsquo;s clear where functionality lives and reduces complexity in the scripts themselves. For example, our scripts directory contains the following, hopefully self-explanatory, utilities:</p> \n<ul> \n <li><code>build-ios.sh</code></li> \n <li><code>build-package.sh</code></li> \n <li><code>build-qt-mac.sh</code></li> \n <li><code>build-qt-windows.sh</code></li> \n <li><code>build-website.sh</code></li> \n <li><code>install-dependencies.sh</code></li> \n <li><code>install-qt.sh</code></li> \n <li><code>release.sh</code></li> \n <li><code>test.sh</code></li> \n <li><code>update-release-notes.sh</code></li> \n <li><code>upload-and-publish-release.sh</code></li> \n</ul> \n<p>Since we&rsquo;re now building on multiple platforms with OpoLua (macOS) and OpoLua Qt (macOS, Windows, and Linux), our GitHub Actions needed additional work to ensure version and build numbers were shared across all builds, and that artifacts were aggregated at the end for release. To do this, I created two new jobs: <code>generate-version-number</code> and <code>publish-release</code>. These top and tail the pipeline:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2026-01-07-week-2-tuesday/pipeline-dark/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2026-01-07-week-2-tuesday/pipeline/1600.png\"\n             width=\"1600\"\n             height=\"244\"\n             />\n    </picture>\n</div>\n</p> \n<p>The core of <code>generate-version-number</code> is as follows:</p> \n<pre><code class=\"language-yaml\">generate-version-number:\n  runs-on: ubuntu-latest\n\n  outputs:\n    version_number: ${{ steps.version.outputs.version_number }}\n    build_number: ${{ steps.version.outputs.build_number }}\n\n  steps:\n\n  - name: Checkout repository\n    uses: actions/checkout@v4\n    with:\n        fetch-depth: 0\n\n    # ...\n\n  - name: Generate version and build numbers\n    id: version\n    run: |\n      source scripts/environment.sh\n      VERSION_NUMBER=`scripts/changes/changes version`\n      BUILD_NUMBER=`build-tools generate-build-number`\n      echo \"version_number=$VERSION_NUMBER\" &gt;&gt; $GITHUB_OUTPUT\n      echo \"build_number=$BUILD_NUMBER\" &gt;&gt; $GITHUB_OUTPUT\n</code></pre> \n<p>This uses <a href=\"https://github.com/jbmorley/changes\"><code>changes</code></a> (my semantic versioning script) to synthesize a version number from the git commit log, along with <a href=\"https://github.com/jbmorley/build-tools\"><code>build-tools</code></a> (a collection of build utilities) to generate an 18-digit build number that encodes the build timestamp and git sha. It then makes these available to subsequent build jobs using outputs.</p> \n<p>Once the different platform build jobs have completed, <code>publish-release</code> aggregates their artifacts and, on <code>main</code> builds, uploads the iOS app to TestFlight and creates a new GitHub release if necessary, attaching the build output. 😮‍💨</p> \n<hr /> \n<p>While it took much of the day to slowly iterate on the build pipeline, I finished with a new (automatically created) release on GitHub&mdash;<a href=\"https://github.com/inseven/opolua/releases/tag/2.1.0\">version 2.1.0</a>&mdash;comprising iOS, macOS and Windows apps. It&rsquo;s early days yet for OpoLua Qt, but we&rsquo;d love your feedback.</p> \n<p>Next up, Linux!</p>",
      "date_published": "2026-01-08T00:20:58-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-01-07-week-2-tuesday/",
      "title": "Week 2&mdash;Tuesday",
      "url": "https://jbmorley.co.uk/posts/2026-01-07-week-2-tuesday/"
    },
    {
      "content_html": "<p><em>This post is a spiritual continuation of my <a href=\"/december-adventure\">December Adventure</a>&mdash;I enjoyed the experience of keeping a daily devlog, so I&rsquo;m going to see how long I can keep it going.</em></p> \n<p>Having spent much of the first week of the year on festivities and wrapping up the last few bits of my 2025 <a href=\"/december-adventure\">December Adventure</a>, I decided to ease back into things slowly. Armed with my long list of issues from last year, I set about knocking out some bug fixes and&mdash;hopefully&mdash;clearing the way for future feature work.</p> \n<h2><a id=\"opolua-(qt)\"></a>OpoLua (Qt)</h2> \n<p><a href=\"https://opolua.org\">OpoLua</a> continues to loom over me. Tom is doing stellar work fleshing out our new Qt desktop-focused app, and rounding out our OPL implementation, but we&rsquo;ve yet to actually ship anything. As our resident GitHub Actions whisperer, that task tends to fall to me.</p> \n<p>The Qt app has been building successfully for macOS in CI for the past couple of weeks (since I first took a crack at it on <a href=\"/posts/2025-12-13-december-adventure-day-12/\">day 12</a> of my December Adventure), but an issue with the way I was linking the Qt frameworks meant that it would only run if Qt was installed. Thankfully, after a little investigation, the fix proved simple&mdash;Qt provides <code>macdeployqt</code> which can embed the Qt frameworks and correctly sign an app for <a href=\"https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution\">notarization</a> (an entirely unexpected convenience if you&rsquo;re used to developing for Apple platforms):</p> \n<pre><code class=\"language-sh\">macdeployqt  \"OpoLua Qt.app\" \\\n    -verbose=3 \\\n    -sign-for-notarization=\"Developer ID Application: Jason Morley (QS82QFHKWB)\"\n</code></pre> \n<p>With the addition of just this simple line, macOS Qt builds are now working!</p> \n<p>Buoyed by my success, I decided to take a crack at Windows builds&mdash;what could possibly go wrong? Much to my surprise, very little! GitHub Action&rsquo;s Windows runners are, as you might expect, pre-configured for development, and you can opt to use bash for your scripting, allowing me to lean on my macOS and Linux scripting experience rather than trying to wrap my head around PowerShell. Since we want to statically link Qt for the Windows app, this necessitates building Qt itself from source (see <a href=\"https://github.com/inseven/opolua/blob/fa41dd688c15207ba91cd2c619d137a4ea35a2d3/scripts/install-qt.sh\"><code>install-qt.sh</code></a>) which takes about 1 &frac12; hours on GitHub Actions, but this shouldn&rsquo;t change often, so we can cache the builds, leaving us with typical build times of about 2 minutes.</p> \n<p>\n <body>   \n  <img src=\"/posts/2026-01-06-week-2-monday/opolua-qt-windows/400.png\" width=\"400\" height=\"266\" x-srcset=\"/posts/2026-01-06-week-2-monday/opolua-qt-windows/400.png 400 266,/posts/2026-01-06-week-2-monday/opolua-qt-windows/800.png 800 533,/posts/2026-01-06-week-2-monday/opolua-qt-windows/1200.png 1200 800,/posts/2026-01-06-week-2-monday/opolua-qt-windows/1600.png 1600 1066,\" />  \n </body></p> \n<p><em>Since I don&rsquo;t use Windows for development, this is one of the first times I&rsquo;ve used OpoLua on Windows 11, and it feels like quite an achievement. 🥳</em></p> \n<p>I&rsquo;ve still a few tweaks to make to the Windows builds before merging my changes into <code>main</code>. After that, I&rsquo;ll start to take a look at tidying up the OpoLua licensing story so we can actually ship these binaries (Qt has some pretty strict requirements if you don&rsquo;t buy a commercial license).</p>",
      "date_published": "2026-01-06T12:49:22-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-01-06-week-2-monday/",
      "title": "Week 2&mdash;Monday",
      "url": "https://jbmorley.co.uk/posts/2026-01-06-week-2-monday/"
    },
    {
      "content_html": "<p>Having set out to work on a range of mostly Psion-related ideas and tasks during my 2025 <a href=\"/december-adventure\">December Adventure</a>, I&rsquo;m incredibly pleased with what I accomplished: I moved a collection of things forwards, started some new projects, and had quite a bit of fun doing it.</p> \n<p>Some of my highlights:</p> \n<ul> \n <li>began my Organiser journey (<a href=\"/posts/2025-12-03-december-adventure-day-02/#comms-link\">2</a>, <a href=\"/posts/2025-12-06-december-adventure-day-04/\">4</a>, <a href=\"/posts/2025-12-20-december-adventure-day-19/\">19</a>)</li> \n <li>nudged Psion emulation forwards (<a href=\"/posts/2025-12-16-december-adventure-day-15/\">15</a>, <a href=\"/posts/2025-12-17-december-adventure-day-16/\">16</a>, <a href=\"/posts/2026-01-02-december-adventure-day-30\">30</a>)</li> \n <li>maintained various retro devices (<a href=\"/posts/2025-12-27-december-adventure-day-26/\">26</a>, <a href=\"/posts/2025-12-29-december-adventure-day-27/\">27</a>)</li> \n <li>continued to slowly back away from Big Tech (<a href=\"/posts/2025-12-19-december-adventure-day-17/#home-infrastructure\">17</a>, <a href=\"/posts/2025-12-19-december-adventure-day-18/\">18</a>, <a href=\"/posts/2025-12-23-december-adventure-day-22/\">22</a>, <a href=\"/posts/2025-12-25-december-adventure-day-23/\">23</a>)</li> \n <li>designed and printed things for our home (<a href=\"/posts/2025-12-10-december-adventure-day-09/#brackets\">9</a>, <a href=\"/posts/2025-12-14-december-adventure-day-13/\">13</a>, <a href=\"/posts/2025-12-25-december-adventure-day-24/#cable-labels\">24</a>)</li> \n <li>helped keep OPL alive (<a href=\"/posts/2025-12-06-december-adventure-day-05/#opl-syntax-highlighting\">5</a>, <a href=\"/posts/2025-12-07-december-adventure-day-06/\">6</a>, <a href=\"/posts/2025-12-07-december-adventure-day-07/\">7</a>, <a href=\"/posts/2025-12-08-december-adventure-day-08/\">8</a>, <a href=\"/posts/2025-12-10-december-adventure-day-09/#opolua\">9</a>, <a href=\"/posts/2025-12-12-december-adventure-day-11/\">11</a>, <a href=\"/posts/2025-12-13-december-adventure-day-12/\">12</a>)</li> \n <li>made it just that little bit little easier to daily-drive Psions in the 2026 (<a href=\"/posts/2025-12-03-december-adventure-day-02/#manuals\">2</a>, <a href=\"/posts/2025-12-30-december-adventure-day-28/\">28</a>, <a href=\"/posts/2025-12-30-december-adventure-day-29/#reconnect\">29</a>, <a href=\"/posts/2026-01-04-december-adventure-day-31/#battery-holder\">31</a>)</li> \n</ul> \n<p>I&rsquo;ve really enjoyed this December Adventure. It&rsquo;s encouraged me to be more deliberate about the ideas I explore each day, and I&rsquo;ve found the practice of daily write-ups to be incredibly positive.</p> \n<p>The regular cadence has forced me to metaphorically put down what I&rsquo;m working on, pause, and reflect on what I&rsquo;ve done and where it&rsquo;s going. This has helped me significantly in planning and prioritizing when working on increasingly interdependent projects. It&rsquo;s also been incredibly rewarding to share more frequently what I create and receive realtime feedback, while also gaining the sense of external accountability (real or imagined) that comes from setting my approach down in a public forum.</p> \n<p>With that in mind, I&rsquo;m keen to continue a more regular practice of writing going into the new year. I&rsquo;m not sure quite what that needs to look like yet, so I imagine you&rsquo;ll see a few experiments over the coming weeks and months&mdash;I don&rsquo;t want to overwhelm folks (or myself) with daily updates, but I think whatever I do needs to be regular to ensure that I don&rsquo;t end up with a backlog.</p> \n<p>To start with, I&rsquo;d like to focus on specific projects to help move them forwards a little more intentionally, so I&rsquo;m planning to have a theme for each week. I&rsquo;ll write brief daily journal or devlog (that I may or may not publish), and edit it into weeknotes. </p> \n<h1><a id=\"next-steps\"></a>Next Steps</h1> \n<p>I&rsquo;d love to keep working on many of my December Adventure themes in the coming year. For the established projects, I plan to track the new tasks that have emerged during the month as issues in their respective repositories. This will allow others to engage with them in a public forum, and ensure I don&rsquo;t feel so overwhelmed (or forget anything):</p> \n<ul> \n <li><a href=\"https://opolua.org\">OpoLua</a> \n  <ul> \n   <li>Automated OpoLua Qt builds (<a href=\"https://github.com/inseven/opolua/issues/614\">#614</a>)</li> \n   <li>OpoLua Qt macOS builds don&rsquo;t correctly embed Qt (<a href=\"https://github.com/inseven/opolua/issues/615\">#615</a>)</li> \n   <li>Move common resources into core directory (<a href=\"https://github.com/inseven/opolua/issues/617\">#617</a>)</li> \n   <li>Add documentation to the website (<a href=\"https://github.com/inseven/opolua/issues/618\">#618</a>)</li> \n   <li>Define and support EPOC16 SIS files (<a href=\"https://github.com/inseven/opolua/issues/619\">#619</a>)</li> \n  </ul></li> \n <li><a href=\"https://codeberg.org/psion/psiemu\">PsiEmu</a> \n  <ul> \n   <li>Automatically download ROMs (<a href=\"https://codeberg.org/psion/psiemu/issues/1\">#1</a>)</li> \n   <li>MAME creates <code>cfg</code> and <code>nvram</code> directories in the current working directory (<a href=\"https://codeberg.org/psion/psiemu/issues/2\">#2</a>)</li> \n   <li>Support serial port configuration (<a href=\"https://codeberg.org/psion/psiemu/issues/3\">#3</a>)</li> \n   <li>Package for various platforms (<a href=\"https://codeberg.org/psion/psiemu/issues/4\">#4</a>)</li> \n   <li>Support browsing and mounting SSDs (<a href=\"https://codeberg.org/psion/psiemu/issues/5\">#5</a>)</li> \n  </ul></li> \n <li><a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a> \n  <ul> \n   <li>Support incremental backups for EPOC16 and EPOC32 devices (<a href=\"https://github.com/inseven/reconnect/issues/126\">#126</a>)</li> \n   <li>Support converting EPOC16 PIC files (<a href=\"https://github.com/inseven/reconnect/issues/350\">#350</a>)</li> \n   <li>Per file-type conversion options (<a href=\"https://github.com/inseven/reconnect/issues/352\">#352</a>)</li> \n   <li>Support converting EPOC16 Word files to Markdown (<a href=\"https://github.com/inseven/reconnect/issues/354\">#354</a>)</li> \n   <li>Creating folders fails on EPOC16 devices (<a href=\"https://github.com/inseven/reconnect/issues/351\">#351</a>)</li> \n   <li>Adopt Glitter update library (<a href=\"https://github.com/inseven/reconnect/issues/353\">#353</a>)</li> \n   <li>Add Word2Text license to the website (<a href=\"https://github.com/inseven/reconnect/issues/355\">#355</a>)</li> \n   <li>Don&rsquo;t install stub SIS files on EPOC16 devices (<a href=\"https://github.com/inseven/reconnect/issues/356\">#356</a>)</li> \n   <li>Use the Word2Text package (<a href=\"https://github.com/inseven/reconnect/issues/357\">#357</a>)</li> \n   <li>Automatically expand the &lsquo;My Psion&rsquo; sidebar item when a device connects (<a href=\"https://github.com/inseven/reconnect/issues/358\">#358</a>)</li> \n   <li>Reconnect sometimes hangs when quitting the main browser app (<a href=\"https://github.com/inseven/reconnect/issues/359\">#359</a>)</li> \n   <li>Add a wallpaper manager (<a href=\"https://github.com/inseven/reconnect/issues/360\">#360</a>)</li> \n  </ul></li> \n <li><a href=\"https://github.com/plptools/plptools\">plptools</a> \n  <ul> \n   <li>Support server mode (<a href=\"https://github.com/plptools/plptools/issues/63\">#63</a>)</li> \n  </ul></li> \n <li><a href=\"https://folders.jbmorley.co.uk\">Folders</a> \n  <ul> \n   <li>Show filenames (<a href=\"https://github.com/inseven/folders/issues/231\">#231</a>)</li> \n   <li>Crash when changing sidebar selection or sort order during initial load (<a href=\"https://github.com/inseven/folders/issues/232\">#232</a>)</li> \n   <li>Improve the sort drop-down menu (<a href=\"https://github.com/inseven/folders/issues/233\">#233</a>)</li> \n  </ul></li> \n <li><a href=\"https://thoughts.jbmorley.co.uk\">Thoughts</a> \n  <ul> \n   <li>Support image attachments (<a href=\"https://github.com/inseven/thoughts/issues/185\">#185</a>)</li> \n  </ul></li> \n <li><a href=\"https://psion.community\">Psion Community Website</a> \n  <ul> \n   <li>Show a sidebar with the site structure (<a href=\"https://codeberg.org/psion/website/issues/1\">#1</a>)</li> \n  </ul></li> \n</ul> \n<p><em>If you&rsquo;re excited to help work on some of these&mdash;or any other&mdash;projects, please don&rsquo;t hesitate to <a href=\"mailto:hello@jbmorley.co.uk\">get in touch</a>; I&rsquo;m always looking for collaborators.</em></p> \n<p>There are also a few smaller projects I&rsquo;d still love to write up, or otherwise publish:</p> \n<ul> \n <li><a href=\"https://rmrsoft.com\">RMRSoft</a> preservation</li> \n <li>MiSTer x PVM</li> \n <li>My minimalist read later strategy</li> \n <li>Nezumi</li> \n <li><a href=\"/projects/psiboard\">PsiBoard</a></li> \n <li>OPL support for Highlight.js</li> \n <li>Little Luggable assembly</li> \n</ul> \n<p>This just leaves the ideas and explorations that will have to wait for next year&rsquo;s adventure (or a lazy Sunday):</p> \n<ul> \n <li>Keyboard support for the LZ64</li> \n <li>Psion webring</li> \n <li>Software Index additions (UID listing, CLI for plptools/Linux)</li> \n <li>Revisit and ship Thoughts for EPOC32</li> \n <li>Design a minimal Psion USB-C cable</li> \n <li>Archive Palmtop magazine scans</li> \n <li>Web-based Psion emulation</li> \n <li>Thoughts for iOS</li> \n <li>Print feet for Anytime x Nixie</li> \n <li>Try out GlobalTalk</li> \n <li>Time zone logger</li> \n <li>Write about model-viewer</li> \n <li>Write about my 3D printed brackets</li> \n <li>Series 7 emulation</li> \n <li>Try out Plan9</li> \n</ul> \n<h1><a id=\"finally,-thank-you\"></a>Finally, Thank You</h1> \n<p>I&rsquo;d like to take the time to thank everyone who followed along this month, and to thank the couple of folks who <a href=\"/support\">donated</a> this month (you know who you are) 🙇‍♂️. Waking up to your emails and messages really gave me a huge boost. If you have the time I strongly encourage you to give a little love to the creators and open source developers who make a difference to your life&mdash;the emotional support and encouragement goes immeasurably far.</p>",
      "date_published": "2026-01-04T23:37:55-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-01-04-beyond-december/",
      "title": "Beyond December",
      "url": "https://jbmorley.co.uk/posts/2026-01-04-beyond-december/"
    },
    {
      "content_html": "<p>Day 31 brought my <a href=\"/december-adventure\">December Adventure</a> to a close for another year. I spent much of the day trying to work out how to write up day 30&rsquo;s <a href=\"https://github.com/plptools/plptools\">plptools</a>-focused AI experiment (about which I remain deeply conflicted), and reflecting on the past month of adventuring. I also managed to squeeze in a last couple of tasks: enabling <a href=\"https://doxygen.nl/\">Doxygen</a> builds for plptools, and designing and printing a holder for 2 AA batteries (a must when daily-driving my two favorite writing devices&mdash;a Psion, or Pomera DM30).</p> \n<p>Since I&rsquo;ve a fair few thoughts about where to take everything I&rsquo;ve worked on in my adventures, I&rsquo;ll cover these in a follow-up post.</p> \n<h1><a id=\"doxygen\"></a>Doxygen</h1> \n<p>In plptools, the current group of maintainers have inherited a C++ codebase that&rsquo;s very much of its era, making it hard to follow at times. With a view to helping us (and newcomers) understand what&rsquo;s going on, I&rsquo;d like to encourage a gradual process of adding documentation (especially class-level documentation) to the project. To help with this, I added a simple Doxygen configuration to the project and set up documentation builds in our CI.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2026-01-04-december-adventure-day-31/doxygen-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2026-01-04-december-adventure-day-31/doxygen@2x/1600.png\"\n             width=\"800.0\"\n             height=\"740.0\"\n             />\n    </picture>\n</div>\n</p> \n<p>You can view the (sparse) documentation <a href=\"https://plptools.github.io/plptools/\">here</a>.</p> \n<h1><a id=\"battery-holder\"></a>Battery Holder</h1> \n<p>There&rsquo;s a myriad designs for AA battery holders out there, but I couldn&rsquo;t find any simple screw-cap tubes&mdash;something that would fit in a pencil case&mdash;so I designed my own:</p> \n<p>\n <body>   \n  <img src=\"/posts/2026-01-04-december-adventure-day-31/battery-holder/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2026-01-04-december-adventure-day-31/battery-holder/400.jpeg 400 300,/posts/2026-01-04-december-adventure-day-31/battery-holder/800.jpeg 800 600,/posts/2026-01-04-december-adventure-day-31/battery-holder/1200.jpeg 1200 900,/posts/2026-01-04-december-adventure-day-31/battery-holder/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>I&rsquo;d like to update the design to add a rubber o-ring to help the screw lid to stay closed and give it a water-tight seal but, so far, I&rsquo;m pretty pleased with the design. I plan to publish the STL files in the coming days.</p>",
      "date_published": "2026-01-04T15:39:02-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-01-04-december-adventure-day-31/",
      "title": "December Adventure Day 31",
      "url": "https://jbmorley.co.uk/posts/2026-01-04-december-adventure-day-31/"
    },
    {
      "content_html": "<p><a href=\"https://reconnect.jbomrley.co.uk\">Reconnect</a> was on my mind as I started day 30 of my <a href=\"/december-adventure\">December Adventure</a>, and I found my thoughts drifting to its relationship with <a href=\"https://github.com/plptools/plptool\">plptools</a>, which provides the <a href=\"https://thoukydides.github.io/riscos-psifs/plp.html\">Psion Link Protocol</a> implementation and many other conveniences. Allowing myself the distraction&mdash;longer-term architectural noodlings are always useful&mdash;I ended up spending much of the day focused on plptools, exploring new features and considering its future direction. I also tool a little time to fix a longstanding bug in my website that I spotted while writing-up this adventure.</p> \n<h1><a id=\"plptools\"></a>plptools</h1> \n<p>Speaking with <a href=\"https://psion.community/\">Alex</a> and Fabrice&mdash;the other maintainers of plptools&mdash;we settled on a couple of small improvements that would significantly improve the usability and testability of plptools. Both are aimed at improving connectivity and compatibility:</p> \n<ul> \n <li><p><strong>Support serial devices without DTR/DSR signalling</strong></p> <p>Some cheaper RS232 adapters and operating systems (looking at you, <a href=\"\">Haiku</a>) don&rsquo;t support these hardware signals meaning they don&rsquo;t work with plptools. By offering a compatibility mode that ignores these signals, we should be able to significantly increase support at the cost of some connection robustness. A worthwhile trade-off.</p></li> \n <li><p><strong>TCP Serial Port Emulation</strong></p> <p>We&rsquo;d love to be able to connect plptools to Psion emulators. This would serve, not only as a convenience for transferring files to and from the emulators themselves, but as a way to quickly test plptools against a broad range of deices. (Shocking as it may sound, we don&rsquo;t always have every Psion device ever made to hand.) MAME provides an emulated serial port that streams unframed serial data over a TCP connection, and we would like to explicitly support this in plptools.</p></li> \n</ul> \n<p>Keen to dig into something with some near-term benefits, I decided to take a shot at getting plptools to work with MAME.</p> \n<h2><a id=\"configuring-mame\"></a>Configuring MAME</h2> \n<p>First-up, I spent a little time figuring out how to enable the serial port in MAME:</p> \n<pre><code class=\"language-sh\">mame \\\n    psion3a2 \\\n    -sibo serial \\\n    -sibo:serial:rs232 null_modem \\\n    -bitbanger socket.127.0.0.1:1234 \\\n</code></pre> \n<p>Constructing this command took quite a lot longer than I&rsquo;d have liked so, for future travellers, here&rsquo;s my understanding of how it breaks down:</p> \n<ul> \n <li><code>-sibo serial</code>&mdash;enable the serial feature</li> \n <li><code>-sibo:serial:rs232 null_modem</code>&mdash;configure the serial feature to use a null modem</li> \n <li><code>-bitbanger socket.127.0.0.1:1234</code>&mdash;use the bitbanger interface connecting to <code>127.0.0.1</code>, port <code>1234</code></li> \n</ul> \n<p></p>\n<aside>\n  Note that the ordering of the image (\n <code>psion3a2</code>) and flags is very important&mdash;each gives MAME context for the next. \n</aside>\n<p></p> \n<p>In the process of working all this out, I discovered a couple of commands which might be helpful to others:</p> \n<ul> \n <li><p>Use <code>-listslots</code> to show image-specific options:</p> <pre><code class=\"language-sh\">mame psion3a2 -listslots\n</code></pre> <p>This will output something like:</p> <pre><code class=\"language-plaintext\">SYSTEM           SLOT NAME        SLOT OPTIONS     SLOT DEVICE NAME\n---------------- ---------------- ---------------- ----------------------------\npsion3a2         sibo             fax              Psion 3-Fax Modem\n                                parallel         Psion 3-Link Parallel Printer Interface\n                                serial           Psion 3-Link RS232 Serial Interface\n</code></pre></li> \n <li><p>Used in combination with a specific slot option, <code>'-listslots'</code> will show you the additional slots and options for that slot<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>:</p> <pre><code class=\"language-sh\">mame psion3a2 -sibo serial -listslots\n</code></pre> <p>This shows the serial port specific configuration options:</p> <pre><code class=\"language-plaintext\">SYSTEM           SLOT NAME        SLOT OPTIONS     SLOT DEVICE NAME\n---------------- ---------------- ---------------- ----------------------------\npsion3a2         sibo             fax              Psion 3-Fax Modem\n                                parallel         Psion 3-Link Parallel Printer Interface\n                                serial           Psion 3-Link RS232 Serial Interface\n\n               sibo:serial:rs232 dec_loopback     RS-232 Loopback (DEC 12-15336-00)\n                                h19              Heath H19 Terminal (Serial Port)\n                                ie15             IE15 Terminal\n                                keyboard         Serial Keyboard\n                                loopback         RS-232 Loopback\n                                mockingboard     Sweet Micro Systems Mockingboard D\n                                msystems_mouse   Mouse Systems Non-rotatable Mouse (HLE)\n                                nss_tvi          Novag Super System TV Interface\n                                null_modem       RS-232 Null Modem\n                                patch            RS-232 Patch Box\n                                printer          Serial Printer\n                                pty              Pseudo Terminal\n                                rs232_sync_io    RS-232 Synchronous I/O\n                                rs_printer       Radio Shack Serial Printer\n                                scorpion         Micro-Robotics Scorpion Intelligent Controller\n                                sunkbd           Sun Keyboard Adaptor\n                                swtpc8212        SWTPC8212 Terminal\n                                terminal         Serial Terminal\n                                votraxtnt        Votrax Type 'N Talk (Serial Port)\n</code></pre></li> \n</ul> \n<p>Armed with an appropriate command, I was quickly able to use netcat (<code>nc -l -p 1234</code>) to echo text sent from the Psion&rsquo;s Comms program:</p> \n<p>\n <body>   \n  <img src=\"/posts/2026-01-02-december-adventure-day-30/hello-world@2x/400.png\" width=\"400\" height=\"259\" x-srcset=\"/posts/2026-01-02-december-adventure-day-30/hello-world@2x/400.png 400 259,/posts/2026-01-02-december-adventure-day-30/hello-world@2x/800.png 800 519,/posts/2026-01-02-december-adventure-day-30/hello-world@2x/1200.png 1200 779,/posts/2026-01-02-december-adventure-day-30/hello-world@2x/1600.png 1600 1039,\" />  \n </body></p> \n<h2><a id=\"supporting-tcp-(or-now-i-have-an-ai-problem)\"></a>Supporting TCP (or Now I Have an AI Problem)</h2> \n<p>The serial port implementation in plptools is isolated to <a href=\"https://github.com/plptools/plptools/blob/9d54a87637f2f852def56ad057fcbfc854f2b07e/ncpd/packet.cc#L188\"><code>packet.cc</code></a> where it&rsquo;s treated as a classic POSIX file descriptor. This makes it easy to drop in a replacement TCP socket file descriptor (at least as a proof-of-concept). Since most C socket handling is boilerplate, and I knew exactly what I wanted, I thought it might prove a good test for AI&mdash;I find almost every aspect of generative AI deeply concerning, but I do my best to try it out every now and again so I can speak from at least some level of experience and knowledge. With very little prompting, <a href=\"https://claude.ai\">Claude</a> (<a href=\"https://www.anthropic.com/news/claude-sonnet-4-5\">Sonnet 4.5</a>) was able to produce the following:</p> \n<pre><code class=\"language-c\">// This blocks rather than going back into a listening state.\nint\ninit_tcp(const char *port_str, int debug)\n{\n    int listen_fd, conn_fd;\n    struct sockaddr_in addr;\n    int port = atoi(port_str);\n    int optval = 1;\n\n    if (debug)\n        printf(\"creating TCP listener on port %d...\\n\", port);\n\n    listen_fd = socket(AF_INET, SOCK_STREAM, 0);\n    if (listen_fd &lt; 0) {\n        perror(\"socket\");\n        exit(1);\n    }\n\n    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &amp;optval, sizeof(optval));\n\n    memset(&amp;addr, 0, sizeof(addr));\n    addr.sin_family = AF_INET;\n    addr.sin_addr.s_addr = INADDR_ANY;\n    addr.sin_port = htons(port);\n\n    if (bind(listen_fd, (struct sockaddr *)&amp;addr, sizeof(addr)) &lt; 0) {\n        perror(\"bind\");\n        exit(1);\n    }\n\n    if (listen(listen_fd, 1) &lt; 0) {\n        perror(\"listen\");\n        exit(1);\n    }\n\n    if (debug)\n        printf(\"waiting for connection...\\n\");\n\n    conn_fd = accept(listen_fd, NULL, NULL);\n    if (conn_fd &lt; 0) {\n        perror(\"accept\");\n        exit(1);\n    }\n\n    close(listen_fd);\n\n    if (debug)\n        printf(\"connection accepted, fd=%d\\n\", conn_fd);\n\n    return conn_fd;\n}\n</code></pre> \n<p>This function accepts an incoming TCP connection and returns a file descriptor, serving as a drop-in replacement for <code>init_serial</code> as implemented by <a href=\"https://github.com/plptools/plptools/blob/9d54a87637f2f852def56ad057fcbfc854f2b07e/ncpd/mp_serial.c#L58\"><code>mp_serial.c</code></a>. I updated the code to call <code>init_tcp</code> with the appropriate arguments and, without any further modification, I was able to start up the plptools daemon (<code>ncpd</code>) and connect to a MAME Psion emulator. Success! 🥳</p> \n<p>(I&rsquo;d include a screenshot here, but my attempts to reproduce the experiment have been unsuccessful.)</p> \n<p>In spite of this early success, I now feel like I have an AI problem: this code is clearly not production quality (it&rsquo;s blocking; errors aren&rsquo;t propagated; it makes no attempt to support multiple sequential or concurrent connections) and, while some might be willing to commit it to a project, the legality and copyright is deeply ambiguous. Although I&rsquo;m not too worried that what I ultimately produce will be derivative (almost all generated code is boilerplate), I regret using AI here: there&rsquo;s a lot of work required to turn it into production code and I doubt it saved me more than half an hour of development time, while incurring significant environmental, ethical, and legal cost.</p> \n<p>I plan to revisit this over the coming weeks and (manually) design and implement an architecture which better fits plptools.</p> \n<h1><a id=\"date-formatting\"></a>Date Formatting</h1> \n<p>If you view the <a href=\"/december-adventure\">December Adventure</a> or <a href=\"/archive\">Archive</a> pages on this website, you&rsquo;ll see that the list of posts is broken into sections, one for each year. This website is built using <a href=\"https://incontext.jbmorley.co.uk\">InContext</a> (my own take on a static site generator) and uses <a href=\"https://github.com/tomsci/tomscis-lua-templater\">Tilt</a>, a Lua-based templating language designed by <a href=\"https://github.com/tomsci\">Tom</a>, my partner in crime on many a project.</p> \n<p>The template for generating these archive pages looks like this:</p> \n<pre><code class=\"language-html\">{% include \"common.lua\" %}\n{% function content() %}\n    &lt;div class=\"post\"&gt;\n        {% include \"post_header.html\" %}\n        &lt;article class=\"post-content\"&gt;\n\n            {{ document.render() }}\n\n            {%\n                -- Get the posts.\n                posts = document.query(\"posts\")\n                this_year = \"\"\n                previous_year = \"\"\n            %}\n\n            {% for index, post in ipairs(posts) do %}\n\n                {%\n                    -- Get the year of the current post.\n                    if post.date then\n                        this_year = post.date.format(\"yyyy\")\n                    else\n                        this_year = \"Undated\"\n                    end\n                %}\n\n                {%\n                    -- Start a new section if the year is different from the previous one.\n                %}\n                {% if index == 1 then %}\n                    {% if this_year then %}\n                        {% if not query then %}&lt;h1&gt;{{ this_year }}&lt;/h1&gt;{% end %}\n                    {% end %}\n                    &lt;ul class=\"pagelist short\"&gt;\n                {% else %}\n                    {% if this_year ~= previous_year then %}\n                        &lt;/ul&gt;\n                        {% if this_year then %}\n                            &lt;h1&gt;{{ this_year }}&lt;/h1&gt;\n                        {% end %}\n                        &lt;ul class=\"pagelist short\"&gt;\n                    {% end %}\n                {% end %}\n\n                &lt;li&gt;&lt;span class=\"date\"&gt;{% if post.date then %}{{ post.date.format(site.metadata.day_month_format_short) }}{% end %}&lt;/span&gt; &lt;a href=\"{{ post.url }}\"&gt;{{ post.title }}{% if post.subtitle then %}: {{ post.subtitle }}{% end %}&lt;/a&gt;&lt;/li&gt;\n\n                {% if last(posts, index) then %}\n                    &lt;/ul&gt;\n                {% end %}\n\n                {% previous_year = this_year %}\n            {% end %}\n\n        &lt;/article&gt;\n\n    &lt;/div&gt;\n{% end %}\n{% include \"default.html\" %}\n</code></pre> \n<p>The approach to determining the section fairly simple: the template iterates over the ordered posts (<code>for index, post in ipairs(posts) do</code>), generates the text representation of the year component of each post&rsquo;s publish date (<code>this_year = post.date.format(\"yyyy\")</code>) and, if it differs from that of the previous post (<code>if this_year ~= previous_year then</code>), prints a new header, and starts a new <code>ul</code>. While it feels a little inelegant (functional, it is not), this has always proven reliable. Until yesterday:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2026-01-02-december-adventure-day-30/archive-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2026-01-02-december-adventure-day-30/archive@2x/1600.png\"\n             width=\"800.0\"\n             height=\"690.0\"\n             />\n    </picture>\n</div>\n</p> \n<p>The astute reader might have already noticed the problem in the template, but it took me a little while to spot: it turns out there&rsquo;s quite a difference between <code>Y</code> and <code>y</code> <a href=\"https://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_Patterns\">date format specifiers</a> (used when getting the years from posts). They are defined as follows:</p> \n<ul> \n <li><code>y</code> &mdash;Year. Normally the length specifies the padding, but for two letters it also specifies the maximum length.</li> \n <li><code>Y</code>&mdash;Year (in &ldquo;Week of Year&rdquo; based calendars). Normally the length specifies the padding, but for two letters it also specifies the maximum length. This year designation is used in ISO year-week calendar as defined by ISO 8601, but can be used in non-Gregorian based calendar systems where week date processing is desired. <strong>May not always be the same value as calendar year.</strong></li> \n</ul> \n<p>I was incorrectly using the &lsquo;week of year&rsquo; year. 🤦</p> \n<p>Week numbers are a fascinating unit of time as, depending on how the weeks align with a given year, the last days of a year might appear in week 1 of the following year, or the first days of the new year, in week 52 of the previous one. Thankfully, the fix was simple: change <code>YYYY</code> to <code>yyyy</code>.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>Turtles all the way down.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2026-01-02T14:09:59-08:00",
      "id": "https://jbmorley.co.uk/posts/2026-01-02-december-adventure-day-30/",
      "title": "December Adventure Day 30",
      "url": "https://jbmorley.co.uk/posts/2026-01-02-december-adventure-day-30/"
    },
    {
      "content_html": "<p>With the month coming to a close, I decided to spend the last few days of my <a href=\"/december-adventure\">December Adventure</a> to wrap-up some of the loose ends of the projects I&rsquo;ve worked on this month: I found a few outstanding changes in <a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a> when I <a href=\"/posts/2025-12-30-december-adventure-day-28\">added Psion Word conversion support</a>, and there was some further cleanup to do to the <a href=\"https://opolua.org\">OpoLua</a> source tree to set us up for new features in 2026.</p> \n<h1><a id=\"opolua\"></a>OpoLua</h1> \n<p>As OpoLua has grown, we&rsquo;re using it in more-and-more projects: it now serves as a cross-platform library for working with EPOC16 and EPOC32 files, and we&rsquo;re using it in both Reconnect and the <a href=\"https://software.psion.community\">Psion Software Index</a>. As is always the case with software projects, the source structure hasn&rsquo;t evolved at the same pace, so I took some time to tidy it up: I cleaned up the top-level Swift package, added smoke-test builds for this, and updated the OpoLua app to use the package, rather than including the source files directly. Since Reconnect and OpoLua now use the same &lsquo;OpoLuaCore&rsquo; library, it will make keeping Reconnect up-to-date much easier. Over time, I hope to move more functionality from the OpoLua app into OpoLuaCore to allow us to use it in other apps.</p> \n<h1><a id=\"reconnect\"></a>Reconnect</h1> \n<p>I caught Reconnect up to the new version of OpoLua and finally got around to adopting the support for customizing SIS file install locations that <a href=\"https://github.com/tomsci\">Tom</a> added sometime ago, unlocking using SIS files with EPOC16&mdash;while this was never supported for EPOC16, I hope it will allow us to start packaging EPOC16 programs on the Psion Software Index to make that easier for people to jump into.</p> \n<p>I also spent a little time triaging the <a href=\"https://github.com/inseven/reconnect/issues\">open issues</a>, and had a quick look at what it would take to convert EPOC16 PIC images&mdash;it turns out these are just multi-bitmap MBM files with black and grey planes, which I didn&rsquo;t quite combine correctly on my first attempt:</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-30-december-adventure-day-29/screenshot-large/400.png\" width=\"400\" height=\"133\" x-srcset=\"/posts/2025-12-30-december-adventure-day-29/screenshot-large/400.png 400 133,/posts/2025-12-30-december-adventure-day-29/screenshot-large/800.png 800 266,/posts/2025-12-30-december-adventure-day-29/screenshot-large/1200.png 960 320,/posts/2025-12-30-december-adventure-day-29/screenshot-large/1600.png 960 320,\" />  \n </body></p> \n<hr /> \n<p>A solid day of laying the groundwork for the future. 🧱</p>",
      "date_published": "2025-12-30T13:59:59-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-30-december-adventure-day-29/",
      "title": "December Adventure Day 29",
      "url": "https://jbmorley.co.uk/posts/2025-12-30-december-adventure-day-29/"
    },
    {
      "content_html": "<p>Inspired by <a href=\"https://oldbytes.space/@thelastpsion\">Alex</a>&rsquo;s evergreen enthusiasm for the Psion Series 3 devices, and my recent <a href=\"/posts/2025-12-27-december-adventure-day-26\">Series 3a maintenance efforts</a>, I&rsquo;d love to find a way to incorporate these into my daily routines. Boasting a full keyboard, they are natural writing devices so, for day 28 of my <a href=\"/december-adventure\">December Adventure</a>, I decided to focus on improving my workflows for journaling and writing-up projects on my Series 3mx.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-30-december-adventure-day-28/writing/400.jpeg\" width=\"400\" height=\"299\" x-srcset=\"/posts/2025-12-30-december-adventure-day-28/writing/400.jpeg 400 299,/posts/2025-12-30-december-adventure-day-28/writing/800.jpeg 800 599,/posts/2025-12-30-december-adventure-day-28/writing/1200.jpeg 1200 899,/posts/2025-12-30-december-adventure-day-28/writing/1600.jpeg 1600 1199,\" />  \n </body></p> \n<p>Out of the box, EPOC16 devices are well set up for writing&mdash;Psion Word is an entirely serviceable word processor. This leaves just the process of getting the files into modern formats, and into my other workflows. <a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a>, my modern Psion connectivity suite for macOS, supports file transfer for EPOC16 devices, but doesn&rsquo;t support conversion for these older file formats, so I set about fixing this sort-fall, starting with Word file conversion.</p> \n<p><a href=\"https://smittytone.net\">Tony Smith</a> has already produced an MIT licensed Word parser and converter written in Swift, <a href=\"https://smittytone.net/word2text/\">Word2Text</a>, (thanks Tony!), so this seemed like the natural starting point. Armed with a fully-functional implementation, there wasn&rsquo;t too much to do but plumb it into Reconnect&rsquo;s existing file conversion architecture:</p> \n<pre><code class=\"language-swift\">private static let converters: [Conversion] = [\n\n    // MBM\n    Conversion { entry in\n        return entry.fileType == .mbm || entry.pathExtension.lowercased() == \"mbm\"\n    } filename: { entry in\n        return entry.name\n            .deletingPathExtension\n            .appendingPathExtension(\"tiff\")\n    } perform: { sourceURL, destinationURL in\n        let outputURL = destinationURL.appendingPathComponent(sourceURL.lastPathComponent.deletingPathExtension,\n                                                              conformingTo: .tiff)\n        try PsiLuaEnv().convertMultiBitmap(at: sourceURL, to: outputURL)\n        try FileManager.default.removeItem(at: sourceURL)\n        return outputURL\n    },\n\n    // WRD\n    Conversion { entry in\n        return entry.pathExtension.lowercased() == \"wrd\"\n    } filename: { entry in\n        return entry\n            .name\n            .deletingPathExtension\n            .appendingPathExtension(\"txt\")\n    } perform: { sourceURL, destinationURL in\n        let outputURL = destinationURL.appendingPathComponent(sourceURL.lastPathComponent.deletingPathExtension,\n                                                              conformingTo: .plainText)\n        let data = try Data(contentsOf: sourceURL)\n        let bytes = [UInt8](data)[...]\n        let result: ProcessResult = PsionWord.processFile(bytes)\n        guard result.errorCode == .noError else {\n            throw ReconnectError.unknown\n        }\n        try result.text.write(to: outputURL, atomically: true, encoding: .utf8)\n        return outputURL\n    }\n\n]\n</code></pre> \n<p>Looking again at this code, there are some elements of the architecture that I&rsquo;d love to revisit: I especially dislike the duplicated filename generation code. This exists because macOS needs to know the target filename upfront for drag-and-drop operations, but I could at least change my converter API to inject it back into the <code>perform:</code> block.</p> \n<p>Architectural aspirations aside, everything just worked, and it&rsquo;s now possible to effortlessly convert Psion Word files to text by drag-and-dropping them from Reconnect! 🥳</p> \n<p>While I was at it, I also added a new &lsquo;Conversions&rsquo; tab to the Reconnect settings:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2025-12-30-december-adventure-day-28/conversions-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-30-december-adventure-day-28/conversions@2x/1600.png\"\n             width=\"712.0\"\n             height=\"279.0\"\n             />\n    </picture>\n</div>\n</p> \n<p>I plan to expand on this in the future to allow per-file type options: Word2Text supports conversion to text or Markdown and I&rsquo;d love to expose that functionality in Reconnect.</p>",
      "date_published": "2025-12-30T13:11:21-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-30-december-adventure-day-28/",
      "title": "December Adventure Day 28",
      "url": "https://jbmorley.co.uk/posts/2025-12-30-december-adventure-day-28/"
    },
    {
      "content_html": "<p>After altogether too long, day 27 of my <a href=\"/december-adventure\">December Adventure</a> saw me finally tackling, &lsquo;remove Libretto 50CT batteries&rsquo;, which my <a href=\"/posts/2025-12-01-december-adventure-2025-day-01/\">Organiser Il lucky dip</a> has reminded me of countless times this month. I filmed the process, but as there are already good <a href=\"https://www.ifixit.com/Teardown/Toshiba+Libretto+50CT+Teardown/2090\">teardown guides</a>, I&rsquo;ve focused on illustrative stills instead of publishing the whole video.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-29-december-adventure-day-27/overview/400.jpeg\" width=\"400\" height=\"225\" x-srcset=\"/posts/2025-12-29-december-adventure-day-27/overview/400.jpeg 400 225,/posts/2025-12-29-december-adventure-day-27/overview/800.jpeg 800 450,/posts/2025-12-29-december-adventure-day-27/overview/1200.jpeg 1200 675,/posts/2025-12-29-december-adventure-day-27/overview/1600.jpeg 1600 900,\" />  \n </body></p> \n<p class=\"caption\">My daily driver Libretto 50CT awaiting surgery on the operating table</p> \n<p>As I had feared, the BIOS batteries in both machines had started to leak ever so slightly. The leak wasn&rsquo;t apparent at first, but the battery contacts were corroded and it was clear it had travelled along the battery wires and into the JST connector on the mainboard.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-29-december-adventure-day-27/battery/400.jpeg\" width=\"400\" height=\"225\" x-srcset=\"/posts/2025-12-29-december-adventure-day-27/battery/400.jpeg 400 225,/posts/2025-12-29-december-adventure-day-27/battery/800.jpeg 800 450,/posts/2025-12-29-december-adventure-day-27/battery/1200.jpeg 1200 675,/posts/2025-12-29-december-adventure-day-27/battery/1600.jpeg 1600 900,\" />  \n </body></p> \n<p class=\"caption\">You can just make out the green corrosion on the connector</p> \n<p>Having found the corrosion, I decided to fully tear down both machines and clean the affected area of the mainboard with white vinegar, followed by a healthy rinse in IPA. While I could have done this with the mainboard in the case, I didn&rsquo;t want to risk further damaging the already-brittle plastics with the vinegar.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-29-december-adventure-day-27/cleaning/400.jpeg\" width=\"400\" height=\"225\" x-srcset=\"/posts/2025-12-29-december-adventure-day-27/cleaning/400.jpeg 400 225,/posts/2025-12-29-december-adventure-day-27/cleaning/800.jpeg 800 450,/posts/2025-12-29-december-adventure-day-27/cleaning/1200.jpeg 1200 675,/posts/2025-12-29-december-adventure-day-27/cleaning/1600.jpeg 1600 900,\" />  \n </body></p> \n<p>I used an old stiff-bristle paintbrush<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup> for the process and focused on the area around the JST connector. After giving the mainboard time to dry, I reassembled everything taking care not to crack the case and verified both machines still worked.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-29-december-adventure-day-27/working/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-29-december-adventure-day-27/working/400.jpeg 400 300,/posts/2025-12-29-december-adventure-day-27/working/800.jpeg 800 600,/posts/2025-12-29-december-adventure-day-27/working/1200.jpeg 1200 900,/posts/2025-12-29-december-adventure-day-27/working/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>Thankfully, it looks like I caught the leaks in time, and both machines booted up. I&rsquo;d still like to switch out the spinning hard disk in my secondary device, but that&rsquo;s one for another day. 😮‍💨</p> \n<hr /> \n<p>Perhaps even more concerning than the leaky batteries was discovering the damage Hawaii is doing to the Libretto I have in daily rotation&mdash;the climate here is brutal and sadly shortens the life of devices, old and new.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-29-december-adventure-day-27/corrosion/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-29-december-adventure-day-27/corrosion/400.jpeg 400 300,/posts/2025-12-29-december-adventure-day-27/corrosion/800.jpeg 800 600,/posts/2025-12-29-december-adventure-day-27/corrosion/1200.jpeg 1200 900,/posts/2025-12-29-december-adventure-day-27/corrosion/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p class=\"caption\">The humid salt air is rapidly corroding the metallic paint on the Libretto&rsquo;s case</p> \n<p>There&rsquo;s not much I can do about the climate&mdash;I keep what I can in weatherproof totes with desiccants&mdash;but I can be more selective about the devices I keep out. Seeing this damage is a good reminder to be more deliberate about the devices I use, an encouragement to let a few go to good homes, and practice a little more minimalism.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>I&rsquo;ve ordered some antistatic brushes for future work.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-12-29T12:33:31-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-29-december-adventure-day-27/",
      "title": "December Adventure Day 27",
      "url": "https://jbmorley.co.uk/posts/2025-12-29-december-adventure-day-27/"
    },
    {
      "content_html": "<p>Having taken what amounted to a day off from my <a href=\"/december-adventure\">December Adventure</a> for Christmas, day 26 saw me return to my <a href=\"/posts/2025-12-21-december-adventure-day-20/#ideas\">original list of ideas</a> as I took the time to reinforce the hinges on my <a href=\"/computers/psion/series-3a/\">Psion Series 3a</a>. As I <a href=\"/posts/2025-12-15-december-adventure-day-14\">noted previously</a>, this was on hold as I worked out how to record and document the process to help others looking to do it themselves. Armed with my <a href=\"/posts/2025-12-25-december-adventure-day-24/#adventure-preparation\">new desk tripod</a>, I gave it a shot.</p> \n<p>I also took some time to finish moving both <a href=\"https://statuspanel.io\">StatusPanel</a> and <a href=\"https://anytime.world\">Anytime</a> from Caddy to a combination of nginx and certbot. I&rsquo;ll not go into more detail here, but I&rsquo;m happy to be back on more mainstream infrastructure, and hopeful it will reduce maintenance costs in the future.</p> \n<h1><a id=\"hinge-reinforcement\"></a>Hinge Reinforcement</h1> \n<p>Anyone who&rsquo;s searched for a Psion on eBay will know that the hinges are their Achilles' heel&mdash;with the exception of the (hingeless) <a href=\"https://en.wikipedia.org/wiki/Psion_Organiser\">Organisers</a>, if the hinges haven&rsquo;t cracked yet, it&rsquo;s only a matter of time. To address this, members of the Psion community have taken to using <a href=\"https://www.jbweld.com/\">JB Weld</a> to reinforce hinges, and even restore broken ones. This works incredibly well and, since my wood-grain Series 3a was in need of this treatment, I thought it would be a good idea to document it.</p> \n<h2><a id=\"disassembly\"></a>Disassembly</h2> \n<p>Thankfully, with the Series 3 range of devices, you can get access the hinges without opening the device&mdash;this means you can avoid subjecting them to one last open that might crack the hinges:</p> \n<ol> \n <li>Remove the battery cover.</li> \n <li>Unscrew the four screws holding the spine in place.</li> \n <li>Gently ease the spine off the device, taking care not to damage the battery cable on the right-hand side.</li> \n <li>Using some tweezers, side the cable out of the cable guide, and unplug the JST connector, and fully remove the spine.</li> \n</ol> \n<p>\n <body>   \n  <div class=\"photo-container\"> \n   <video class=\"photo-video\" poster=\"/posts/2025-12-27-december-adventure-day-26/disassembly/thumbnail.jpeg\" controls> \n    <source src=\"/posts/2025-12-27-december-adventure-day-26/disassembly/video.mov\" type=\"video/mp4\" /> Your browser does not support the video tag. \n   </video> \n  </div>  \n </body></p> \n<h2><a id=\"application\"></a>Application</h2> \n<p>The process I&rsquo;ve settled on for my Psions is to just reinforce the screen-side of the hinges&mdash;these are easy to access and most prone to cracking. The goal is to fill the recess on the back of each hinge and fillet or taper the JB Weld down to the curve of the hinges themselves, taking care not to get any epoxy onto the other half of the hinges. This is a delicate operation, so go slow, and use a small palette knife like implement: I started with a cut-off Q-tip which proved too large, and ultimately settled on some rounded tweezers.</p> \n<p>\n <body>   \n  <div class=\"photo-container\"> \n   <video class=\"photo-video\" poster=\"/posts/2025-12-27-december-adventure-day-26/application/thumbnail.jpeg\" controls> \n    <source src=\"/posts/2025-12-27-december-adventure-day-26/application/video.mov\" type=\"video/mp4\" /> Your browser does not support the video tag. \n   </video> \n  </div>  \n </body></p> \n<h2><a id=\"complete\"></a>Complete</h2> \n<p>If everything&rsquo;s gone well you&rsquo;ll have a small fillet of JB Weld on the back of each hinge. After 24 hours, you can replace the spine, batteries, and battery cover and your Psion should be ready for another 20 years of daily use. 🤞</p> \n<p>\n <body>   \n  <div class=\"photo-container\"> \n   <video class=\"photo-video\" poster=\"/posts/2025-12-27-december-adventure-day-26/showcase/thumbnail.jpeg\" controls> \n    <source src=\"/posts/2025-12-27-december-adventure-day-26/showcase/video.mov\" type=\"video/mp4\" /> Your browser does not support the video tag. \n   </video> \n  </div>  \n </body></p> \n<hr /> \n<p>I&rsquo;m looking forward to getting back to using my Series 3a once the JB Weld has cured&mdash;I&rsquo;ve still never tried a Series 3 or 3a with <a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a>, and I&rsquo;m looking forward to breaking out the 3Link and doing so.</p>",
      "date_published": "2025-12-27T17:59:47-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-27-december-adventure-day-26/",
      "title": "December Adventure Day 26",
      "url": "https://jbmorley.co.uk/posts/2025-12-27-december-adventure-day-26/"
    },
    {
      "content_html": "<p>Day 25 (Merry Christmas to those of you who celebrate) of my <a href=\"/december-adventure\">December Adventure</a> brought somewhat of a departure from the regular adventuring: I wrote up my adventures from the past few days, tried to slow a little, cooked, and primed a sign Sarah&rsquo;s painting for a friend&rsquo;s business.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-27-december-adventure-day-25/sign/400.jpeg\" width=\"400\" height=\"533\" x-srcset=\"/posts/2025-12-27-december-adventure-day-25/sign/400.jpeg 400 533,/posts/2025-12-27-december-adventure-day-25/sign/800.jpeg 800 1066,/posts/2025-12-27-december-adventure-day-25/sign/1200.jpeg 1200 1600,/posts/2025-12-27-december-adventure-day-25/sign/1600.jpeg 1600 2133,\" />  \n </body></p> \n<p>Normal service will (hopefully) return with day 26.</p>",
      "date_published": "2025-12-27T13:03:58-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-27-december-adventure-day-25/",
      "title": "December Adventure Day 25",
      "url": "https://jbmorley.co.uk/posts/2025-12-27-december-adventure-day-25/"
    },
    {
      "content_html": "<p>Having spent <a href=\"/posts/2025-12-25-december-adventure-day-23\">day 23</a> of my <a href=\"/december-adventure\">December Adventure</a> on some fairly process-heavy sysadmin, much of day 24&rsquo;s adventure time was consumed writing it up&mdash;I always struggle to work out how much detail to include, and end up constantly reworking such posts. In spite of that, I managed to roll nginx out to some of the services I run (including this website, the <a href=\"https://software.psion.community\">Psion Software Index</a>, and <a href=\"https://writeme.jbmorley.co.uk\">WriteMe</a>), and spent a little time organizing my retro computing and preparing for future adventures. I&rsquo;ll not write about the switch from Caddy to nginx more here; if you&rsquo;re curious, check out the various projects on GitHub.</p> \n<h1><a id=\"cable-labels\"></a>Cable Labels</h1> \n<p>Whenever I pick up a retro computer, I try to sort out a convenient way to power it using USB-C: I&rsquo;m keen to actively use the devices I have and not having to carry an extra power brick significantly reduces the friction of doing so. Often they&rsquo;ll use some kind of <a href=\"https://en.wikipedia.org/wiki/Coaxial_power_connector\">barrel connector</a> at one of the standard voltages (5V, 9V, 12V, 15V, etc). This makes buying cables easy (there&rsquo;s a plethora of USB PD cables out there), but means I have an increasing array of cables that <em>really</em> shouldn&rsquo;t be plugged into the wrong device, lest I blow something up.</p> \n<p>Some time ago, I came across a model on <a href=\"https://makerworld.com/en/models/475787-cable-clip-with-label-for-network-ethernet-cable?from=search#profileId-386620\">MakerWorld</a> for labelling network cables, and I thought I&rsquo;d give it a go.</p> \n<p>\n <body>   \n  <div class=\"photo-container\"> \n   <video class=\"photo-video\" poster=\"/posts/2025-12-25-december-adventure-day-24/timelapse/thumbnail.jpeg\" controls> \n    <source src=\"/posts/2025-12-25-december-adventure-day-24/timelapse/video.mov\" type=\"video/mp4\" /> Your browser does not support the video tag. \n   </video> \n  </div>  \n </body></p> \n<p>Since the model was designed for network cables (which have a fairly large diameter), it was a little large for my power cables. After a couple of test prints, I found that scaling the model to 70% worked perfectly:</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-25-december-adventure-day-24/cable/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-25-december-adventure-day-24/cable/400.jpeg 400 300,/posts/2025-12-25-december-adventure-day-24/cable/800.jpeg 800 600,/posts/2025-12-25-december-adventure-day-24/cable/1200.jpeg 1200 900,/posts/2025-12-25-december-adventure-day-24/cable/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>I plan to print a few of these over the coming days for each of the <a href=\"/computers\">devices</a> I try to keep in daily rotation.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-25-december-adventure-day-24/sizes/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-25-december-adventure-day-24/sizes/400.jpeg 400 300,/posts/2025-12-25-december-adventure-day-24/sizes/800.jpeg 800 600,/posts/2025-12-25-december-adventure-day-24/sizes/1200.jpeg 1200 900,/posts/2025-12-25-december-adventure-day-24/sizes/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p class=\"caption\">Trying out different sizes and color combinations</p> \n<h1><a id=\"adventure-preparation\"></a>Adventure Preparation</h1> \n<p>Following my struggles to photograph my various projects, Sarah treated me to an early Christmas present in the form of a desk tripod from Facebook Marketplace (presumably owned by a lapsed TikToker). The anglepoise mechanism was missing a bolt which was easy enough to fix, and I now have an overhead camera setup:</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-25-december-adventure-day-24/tripod/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-25-december-adventure-day-24/tripod/400.jpeg 400 300,/posts/2025-12-25-december-adventure-day-24/tripod/800.jpeg 800 600,/posts/2025-12-25-december-adventure-day-24/tripod/1200.jpeg 1200 900,/posts/2025-12-25-december-adventure-day-24/tripod/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>This should unlock both my Libretto 50CT and Series 3a maintenance tasks, and I&rsquo;m unduly excited about it. I couldn&rsquo;t resist a quick test to see how well it works:</p> \n<p>\n <body>   \n  <div class=\"photo-container\"> \n   <video class=\"photo-video\" poster=\"/posts/2025-12-25-december-adventure-day-24/psion/thumbnail.jpeg\" controls> \n    <source src=\"/posts/2025-12-25-december-adventure-day-24/psion/video.mov\" type=\"video/mp4\" /> Your browser does not support the video tag. \n   </video> \n  </div>  \n </body></p>",
      "date_published": "2025-12-25T18:39:44-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-25-december-adventure-day-24/",
      "title": "December Adventure Day 24",
      "url": "https://jbmorley.co.uk/posts/2025-12-25-december-adventure-day-24/"
    },
    {
      "content_html": "<p>Continuing with <a href=\"/posts/2025-12-23-december-adventure-day-22\">day 22&rsquo;s infrastructural focus</a>, day 23 of my (increasingly random) <a href=\"/december-adventure\">December Adventure</a> brought further thoughts on using <a href=\"https://letsencrypt.org/\">Let&rsquo;s Encrypt</a> SSL certificates, webserver changes, and a side-quest into <a href=\"https://en.wikipedia.org/wiki/Dnsmasq\">dnsmasq</a>.</p> \n<h1><a id=\"vanity-urls\"></a>Vanity URLs</h1> \n<p>One of the larger inconveniences of running self-hosted infrastructure (beyond the obvious administrative cost and decision fatigue that comes from needing to understand many disparate projects) is the myriad different URLs, ports, and self-signed certificates that you need to interact with: default setups will have you needing to remember IP addresses, along with the various ports numbers different services run on. Coupled with the increasing challenge of coaxing browsers to trust self-signed certificates (especially on mobile), this can make for a deeply frustrating experience. The key to tidying up this mess is to run your own DNS server, specify unique domain names for each service, and then use a <a href=\"https://en.wikipedia.org/wiki/Reverse_proxy\">reverse proxy</a> to forward on the relevant ports.</p> \n<p>Having never set up my own DNS server before, I went about configuring dnsmasq on one of my Raspberry Pis. I had expected the process to be incredibly complex, but, as I don&rsquo;t need it to do much, everything proved incredibly simple. The config file (<code>/etc/dnsmasq.d/local.conf</code>) ended up looking like this:</p> \n<pre><code class=\"language-ini\"># Listen on ethernet and Tailscale.\ninterface=eth0\ninterface=tailscale0\nbind-interfaces\n\n# Ignore local /etc/hosts and /etc/resolv.conf.\nno-hosts\nno-resolv\n\n# Forward unknown requests to Cloudflare.\nserver=1.1.1.1\n\n# Provide responses for home.jbmorley.co.uk and sub-domains.\naddress=/home/jbmorley.co.uk/100.119.64.74\naddress=/immich.home.jbmorley.co.uk/100.119.64.74\naddress=/jellyfin.home.jbmorley.co.uk/100.119.64.74\naddress=/syncthing.home.jbmorley.co.uk/100.119.64.74\n</code></pre> \n<p>I was able to check this was working locally on my Raspberry Pi by explicitly querying localhost using <code>nslookup</code>:</p> \n<pre><code class=\"language-plaintext\"># nslookup immich.home.jbmorley.co.uk localhost\nServer:         localhost\nAddress:        ::1#53\n\nName:   immich.home.jbmorley.co.uk\nAddress: 100.119.64.74\n</code></pre> \n<p>The custom DNS entries all resolve to an IP address on my <a href=\"https://tailscale.com/\">Tailscale</a> VPN, meaning they should work whenever I&rsquo;m connected to Tailscale, irrespective of where I am.</p> \n<p>I was also able to take advantage of Tailscale&rsquo;s built-in support to automatically route only the DNS queries for the <code>home.jbmorley.co.uk</code> suffix to my DNS server using <a href=\"https://tailscale.com/learn/why-split-dns\">Split DNS</a>.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2025-12-25-december-adventure-day-23/nameservers-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-25-december-adventure-day-23/nameservers@2x/1600.png\"\n             width=\"716.0\"\n             height=\"526.5\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Tailscale&rsquo;s <a href=\"https://tailscale.com/kb/1081/magicdns\">MagicDNS</a> will defer all DNS queries for <code>home.jbmorley.co.uk</code> to <code>100.126.47.22</code></p> \n<h1><a id=\"ssl\"></a>SSL</h1> \n<p>The goal of using fully qualified sub-domains for a domain I own (see above) is to allow me to generate SSL certificates that will be accepted by all my devices. For this, I can use Let&rsquo;s Encrypt with the DNS-01 challenge with Cloudflare that I dug into on <a href=\"/posts/2025-12-23-december-adventure-day-22/\">day 22</a>.</p> \n<p>While I originally planned to use Caddy to generate certificates, I eventually accepted that I don&rsquo;t want to have to maintain a custom build outside of the package manager to be able to use the DNS challenge plugins. Instead, I opted to use <a href=\"https://certbot.eff.org/\"><code>certbot</code></a> for this. I generated a certificate for <code>home.jbmorley.co.uk</code> and a wildcard certificate for <code>*.home.jbmorley.co.uk</code>. This ensures I don&rsquo;t have to generate a new certificate for each and every service I run:</p> \n<pre><code class=\"language-sh\">sudo certbot certonly \\\n    --dns-cloudflare \\\n    --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \\\n    -d \"*.home.jbmorley.co.uk\" \\\n    -d \"home.jbmorley.co.uk\"\n</code></pre> \n<p></p>\n<aside>\n  As part of this process, \n <code>certbot</code> schedules a job to automatically refresh the certificate so, in theory, I don&rsquo;t need to think about this again. \n</aside>\n<p></p> \n<h1><a id=\"web-servers\"></a>Web Servers</h1> \n<p>Having decided not to use Caddy for SSL, I returned to <a href=\"https://nginx.org/en/\">nginx</a> and set up a handful of sites: a simple static site at <code>home.jbmorley.co.uk</code> which offers links to all my other services, and a collection of reverse proxies.</p> \n<h2><a id=\"links\"></a>Links</h2> \n<p>Some time ago, created a lightweight home page with links to the various services I run. This proved incredibly useful, and has become a crucial part of my home infrastructure:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2025-12-25-december-adventure-day-23/links-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-25-december-adventure-day-23/links@2x/1600.png\"\n             width=\"800.0\"\n             height=\"774.5\"\n             />\n    </picture>\n</div>\n</p> \n<p>Needless to say, the nginx configuration for this proved very simple:</p> \n<pre><code class=\"language-plaintext\">server {\n    listen 80;\n    server_name home.jbmorley.co.uk links.home.jbmorley.co.uk;\n\n    return 301 https://$host$request_uri;\n}\n\nserver {\n    listen 443 ssl http2;\n    server_name home.jbmorley.co.uk links.home.jbmorley.co.uk;\n\n    ssl_certificate     /etc/letsencrypt/live/home.jbmorley.co.uk/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/home.jbmorley.co.uk/privkey.pem;\n\n    root /var/www/links;\n    index index.html;\n\n    location / {\n        try_files $uri $uri/ =404;\n    }\n}\n</code></pre> \n<h2><a id=\"reverse-proxies\"></a>Reverse Proxies</h2> \n<p>Configuring the reverse proxies proved equally simple. For example, my Syncthing proxy looks like this:</p> \n<pre><code>server {\n    listen 80;\n    server_name syncthing.home.jbmorley.co.uk;\n\n    return 301 https://$host$request_uri;\n}\n\nserver {\n    listen 443 ssl;\n    http2 on;\n    server_name syncthing.home.jbmorley.co.uk;\n\n    ssl_certificate     /etc/letsencrypt/live/home.jbmorley.co.uk/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/home.jbmorley.co.uk/privkey.pem;\n\n    location / {\n        proxy_pass https://127.0.0.1:8384;\n\n        proxy_http_version 1.1;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto https;\n\n        # Syncthing uses a self-signed cert by default\n        proxy_ssl_verify off;\n    }\n}\n</code></pre> \n<p>The only notable element here is the <code>proxy_ssl_verify off</code> which ensures nginx will accept the self-signed certificate that Syncthing generates by default.</p> \n<hr /> \n<p>The day proved significantly more work-like than I&rsquo;d been hoping for. Still, it&rsquo;s great to feel like I&rsquo;m laying the groundwork for future self-hosted infrastructure and tooling.</p>",
      "date_published": "2025-12-25T15:25:47-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-25-december-adventure-day-23/",
      "title": "December Adventure Day 23",
      "url": "https://jbmorley.co.uk/posts/2025-12-25-december-adventure-day-23/"
    },
    {
      "content_html": "<p>Having <a href=\"/posts/2025-12-22-december-adventure-day-21\">set an intention</a> to wrap up some projects and focus on more retro-related explorations, I immediately found myself distracted by other things on day 22 of my <a href=\"/december-adventure\">December Adventure</a>: I&rsquo;ve been thinking a lot about the seemingly bleak future of GitHub and am keen to start experimenting with <a href=\"https://forgejo.org/\">Forgejo</a> locally with a view to potentially maintaining a self-hosted instance. In preparation for this, I decided to finally try to wrap my head around using <a href=\"https://letsencrypt.org/\">Let&rsquo;s Encrypt</a> to generate SSL certificates on machines that aren&rsquo;t Internet-facing&mdash;something I&rsquo;d like for self-hosted infrastructure as well as using Cloudflare to proxy my this website. I also started putting together a website for my partner to showcase her art.</p> \n<h1><a id=\"caddy,-let's-encrypt,-and-cloudflare\"></a>Caddy, Let&rsquo;s Encrypt, and Cloudflare</h1> \n<p>Some time ago, I switched to using <a href=\"https://caddyserver.com/\">Caddy</a> for hosting this website, projects like <a href=\"https://anytime.world/\">Anytime</a>, <a href=\"https://statuspanel.io/\">StatusPanel</a>, and <a href=\"https://writeme.jbmorley.co.uk/\">WriteMe</a>, and for local bits of infrastructure. I was sold by the dream of built-in, auto-provisioning SSL using Let&rsquo;s Encrypt, and it worked well. However, one thing I&rsquo;ve never taken the time to set up is out-of-band challenges like <a href=\"https://letsencrypt.org/docs/challenge-types/#dns-01-challenge\">DNS-01</a> that allow me to take advantage of things like Cloudflare&rsquo;s proxy, or generate SSL certificates for private local infrastructure.</p> \n<p>The DNS-01 challenge works by writing a DNS TXT record as a proof of ownership. Enabling this in Caddy is a simple matter of specifying the <code>dns cloudflare</code> challenge. For this website, the whole configuration looks like this:</p> \n<pre><code class=\"language-plaintext\">jbmorley.co.uk {\n        root * /var/www/jbmorley.co.uk\n        encode zstd gzip\n        file_server {\n                    index index.html index.json\n        }\n        tls {\n                dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n        }\n}\n</code></pre> \n<p>As you would expect from Caddy, this is wonderfully simple, and my hope was that I could use it for both my public services as well as my personal infrastructure, which I would achieve by adding local DNS entries for valid <code>jbmorley.co.uk</code> subdomains. Unfortunately, it turns out the whole thing is significantly more complex: the version of Caddy packaged with Ubuntu doesn&rsquo;t include any of the DNS challenge plugins and, since Caddy&rsquo;s plugins are compiled-in, you can&rsquo;t just install additional package.</p> \n<p>I ended up spending much of the day trying to come up with an approach to this, ultimately settling on a <a href=\"https://github.com/CaddyBuilds/caddy-cloudflare\">community maintained</a> Docker image which builds-in the Cloudflare DNS-01 plugin. My <code>docker-compose.yml</code> for this is as follows:</p> \n<pre><code class=\"language-yaml\">services:\n  caddy:\n    image: ghcr.io/caddybuilds/caddy-cloudflare:latest\n    restart: unless-stopped\n    network_mode: host\n    cap_add:\n      - NET_ADMIN\n    volumes:\n      - /etc/caddy:/etc/caddy\n      - /var/www:/var/www\n      - caddy_data:/data\n      - caddy_config:/config\n    environment:\n      - CLOUDFLARE_API_TOKEN=...\n\nvolumes:\n  caddy_data:\n    external: true\n  caddy_config:\n</code></pre> \n<p>This configuration is slightly tweaked from the one in the project README: it specifies <code>network_mode: host</code> to allow Caddy to see host ports provided by other apps and Docker containers, rather than requiring me to manage network interfaces between Docker containers.</p> \n<p>While it &lsquo;works&rsquo;, deploying Caddy like this concerns me as it introduces another component with a security update life cycle independent of the host OS, and exposes a critical part of my infrastructure to third-party management and build infrastructure. With that in mind, although I switched my staging server (which hosts this website) over to this new approach, I decided to sleep on it and take stock in the morning.</p> \n<h1><a id=\"website-design\"></a>Website Design</h1> \n<p>In the afternoon, I took some time to work on a lightweight website to allow my partner Sarah to showcase her paintings. This gave me a nice excuse to spend a little more time with <a href=\"https://gohugo.io/\">Hugo</a>, which I last used to set up the <a href=\"https://psion.community\">Psion community website</a>.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2025-12-23-december-adventure-day-22/website-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-23-december-adventure-day-22/website@2x/1600.png\"\n             width=\"800.0\"\n             height=\"772.0\"\n             />\n    </picture>\n</div>\n</p> \n<p>Keeping things simple, we decided to use a grid for the home page. Iterating over all the pages in a section is easy to do in Hugo templates and, while I still prefer the <a href=\"https://incontext.jbmorley.co.uk\">InContext</a><sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup> approach to multimedia for more involved sites, it&rsquo;s great to be able to specify image dimensions directly in templates:</p> \n<pre><code class=\"language-plaintext\">{{ define \"main\" }}\n\n    {{ .Content }}\n\n    &lt;div class=\"gallery-grid\"&gt;\n        {{ range (where .Site.Pages \"Section\" \"art\") }}\n            {{ range .Pages }}\n                &lt;div class=\"gallery-item\"&gt;\n                    {{ if .Params.thumbnail }}\n                        {{ $image := .Resources.Get .Params.thumbnail }}\n                        &lt;a href=\"{{ .RelPermalink }}\"&gt;\n                            &lt;img src=\"{{ ($image.Fit \"600x600\").RelPermalink }}\" alt=\"{{ .Title }}\" title=\"{{ .Title }}\"&gt;\n                        &lt;/a&gt;\n                    {{ else }}\n                        Missing Thumbnail\n                    {{ end }}\n                &lt;/div&gt;\n            {{ end }}\n        {{ end }}\n    &lt;/div&gt;\n\n{{ end }}\n</code></pre> \n<p>This template iterates over all the top-level pages in the &lsquo;art&rsquo; section and, for each page, generates a thumbnail matching the one specified in the that page&rsquo;s Frontmatter. I was briefly caught out by Hugo&rsquo;s need to differentiate between <a href=\"https://gohugo.io/content-management/page-bundles/\">branch and leaf pages / bundles</a> (branches should be called <code>_index.md</code>, and leaves <code>index.md</code>), but I got there in the end. The corresponding directory structure looks like:</p> \n<pre><code class=\"language-plaintext\">content\n├── _index.md\n├── about\n│&nbsp;&nbsp; └── index.md\n├── art\n│&nbsp;&nbsp; ├── _index.md\n│&nbsp;&nbsp; ├── basil-and-maverick\n│&nbsp;&nbsp; │&nbsp;&nbsp; ├── basmav.png\n│&nbsp;&nbsp; │&nbsp;&nbsp; └── index.md\n│&nbsp;&nbsp; ├── kale-flower\n│&nbsp;&nbsp; │&nbsp;&nbsp; ├── index.md\n│&nbsp;&nbsp; │&nbsp;&nbsp; └── preview.jpg\n│&nbsp;&nbsp; ├── manu-o-ku\n│&nbsp;&nbsp; │&nbsp;&nbsp; ├── index.md\n│&nbsp;&nbsp; │&nbsp;&nbsp; └── manuoku.png\n│&nbsp;&nbsp; ├── na-pali\n│&nbsp;&nbsp; │&nbsp;&nbsp; ├── index.md\n│&nbsp;&nbsp; │&nbsp;&nbsp; └── na-pali.png\n│&nbsp;&nbsp; ├── passion-orange-guava\n│&nbsp;&nbsp; │&nbsp;&nbsp; ├── index.md\n│&nbsp;&nbsp; │&nbsp;&nbsp; └── pog.png\n│&nbsp;&nbsp; └── red-flower\n│&nbsp;&nbsp;     ├── index.md\n│&nbsp;&nbsp;     └── preview.jpg\n└── contact\n    └── index.md\n</code></pre> \n<p>With this this template in place, it&rsquo;s now a matter of slowly fleshing the content out and cleaning up the layout. We&rsquo;re going to have to be especially careful when generating preview images as paintings can look incredibly flat with the wrong color profiles. I&rsquo;ve also noticed that some of the built-in macOS apps aren&rsquo;t able to correctly render the high-resolution TIFF scans we have.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>My personal static site builder.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-12-23T17:44:26-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-23-december-adventure-day-22/",
      "title": "December Adventure Day 22",
      "url": "https://jbmorley.co.uk/posts/2025-12-23-december-adventure-day-22/"
    },
    {
      "content_html": "<p>Day 21 brought very little progress with my <a href=\"/december-adventure\">December Adventure</a>. Instead, I took the time to finish playing &lsquo;<a href=\"https://thedriftergame.com/\">The Drifter</a>&rsquo;, an incredibly well executed modern point and click adventure. I strongly recommend it if you enjoy such things.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-22-december-adventure-day-21/the-drifter/400.png\" width=\"400\" height=\"251\" x-srcset=\"/posts/2025-12-22-december-adventure-day-21/the-drifter/400.png 400 251,/posts/2025-12-22-december-adventure-day-21/the-drifter/800.png 800 502,/posts/2025-12-22-december-adventure-day-21/the-drifter/1200.png 1200 753,/posts/2025-12-22-december-adventure-day-21/the-drifter/1600.png 1600 1004,\" />  \n </body></p> \n<p>I reflected more on my December Adventure &lsquo;<a href=\"/posts/2025-12-21-december-adventure-day-20\">progress</a>&rsquo; and the slightly daunting list of ideas I have remaining. I think that, rather than starting new things, I&rsquo;d like to wrap up some of the ideas I&rsquo;ve worked on this month, and take a few days for more open-ended retro-related play and exploration. My partner would also like some help making a website for selling her art, so expect a few side-quests in that direction as well.</p>",
      "date_published": "2025-12-22T11:16:17-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-22-december-adventure-day-21/",
      "title": "December Adventure Day 21",
      "url": "https://jbmorley.co.uk/posts/2025-12-22-december-adventure-day-21/"
    },
    {
      "content_html": "<p>Day 20 didn&rsquo;t afford me much time to work on my <a href=\"/december-adventure\">December Adventure</a>, so I thought it might be a good opportunity to take stock, and do a little housekeeping.</p> \n<h1><a id=\"ideas\"></a>Ideas</h1> \n<p>Since I&rsquo;m two-thirds of the way through the month, I wanted to see which of my <a href=\"/posts/2025-12-01-december-adventure-2025-day-01\">initial ideas</a> remain. In the last 20 days, I&rsquo;ve taken a crack at:</p> \n<ul> \n <li><a href=\"/posts/2025-12-20-december-adventure-day-19\">Improve the Ideas UX</a></li> \n <li><a href=\"/posts/2025-12-06-december-adventure-day-04\">Learn about the Organiser Comms Link</a></li> \n <li><a href=\"/posts/2025-12-03-december-adventure-day-03/#i'm-feeling-lucky\">Software Index ‘I’m Feeling Lucky’ button</a></li> \n <li><a href=\"/posts/2025-12-07-december-adventure-day-06\">Ship OpoLua 2.0 for iOS</a></li> \n <li><a href=\"/posts/2025-12-16-december-adventure-day-15\">Write about MAME emulation</a></li> \n <li><a href=\"/posts/2025-12-06-december-adventure-day-05\">OPL support for highlight.js</a></li> \n <li><a href=\"/posts/2025-12-03-december-adventure-day-02/#manuals\">Add manuals to psion.community</a></li> \n</ul> \n<p>I also distracted myself into a few bits of 3D design and printing that weren&rsquo;t on the list:</p> \n<ul> \n <li><a href=\"/posts/2025-12-10-december-adventure-day-09/#brackets\">MagSafe bracket</a></li> \n <li><a href=\"/posts/2025-12-14-december-adventure-day-13/#greaseweazle\">Greaseweazle case</a></li> \n <li><a href=\"/posts/2025-12-14-december-adventure-day-13/#mini-patch-panels\">Patch panels for my UCTRONICS Pi Rack SSD</a></li> \n</ul> \n<p>I even started to create a <a href=\"/posts/2025-12-17-december-adventure-day-16/\">Psion-focused launcher for MAME</a>.</p> \n<p>This leaves me with:</p> \n<ul> \n <li>Keyboard support for the LZ64</li> \n <li>Psion webring</li> \n <li>Software Index UID listing</li> \n <li>Software Index CLI for Linux</li> \n <li>Software Index spelunking</li> \n <li>PsiBoard documentation and write-up</li> \n <li>PsiBoard finishing touches</li> \n <li>PsiBoard charging status LED</li> \n <li>Ship Thoughts for EPOC32</li> \n <li>Minimal Psion USB-C cable</li> \n <li>Backups in Reconnect</li> \n <li>Organiser connectivity in Reconnect</li> \n <li>Write about RMRSoft preservation</li> \n <li>Ship OpoLua Qt</li> \n <li>Series 3a hinge reinforcement</li> \n <li>Archive Palmtop magazine scans</li> \n <li>Web-based Psion emulation</li> \n <li>Archive more Psion ROMs</li> \n <li>Add guides to psion.community</li> \n <li>Support attachments in Thoughts</li> \n <li>Thoughts for iOS</li> \n <li>Print feet for Anytime x Nixie</li> \n <li>Try out GlobalTalk</li> \n <li>Write up my read later strategy</li> \n <li>Time zone logger</li> \n <li>Little Luggable keyboard improvements</li> \n <li>Write up MiSTer x PVM</li> \n <li>Show filenames in Folders</li> \n <li>Support filtering in Folders</li> \n <li>Remove Libretto 50CT batteries</li> \n <li>Write about model-viewer</li> \n <li>Write about my 3D printed brackets</li> \n <li>Series 7 emulation</li> \n <li>Write up Nezumi</li> \n <li>Try out Plan9</li> \n</ul> \n<p>(It&rsquo;s good know that I&rsquo;m not going to run out any time soon.)</p> \n<p>While the fairly random nature of the <a href=\"/posts/2025-12-01-december-adventure-2025-day-01\">Organiser II lucky dip</a> I&rsquo;ve relied on for much of my adventuring has been enjoyable, that&rsquo;s a fairly long list and I&rsquo;d like to be a little more deliberate in the coming days&mdash;I don&rsquo;t anticipate quite so much spare time as we move into the holidays themselves and I&rsquo;d like to make it count. This list will serve as inspiration, but I&rsquo;ll be more directed in what I choose.</p> \n<p>That said, I&rsquo;m also going to add one more thing to the list: I had a really fun conversation with <a href=\"https://github.com/tomsci\">Tomsci</a> about how we approach EPOC16 support in <a href=\"https://opolua.org\">OpoLua</a>, during which we decided to look into auto-generating EPOC32-era Psion install (SIS) files for use on EPOC16 devices. We aim to do this as part of the <a href=\"https://software.psion.community\">Psion Software Index</a> indexing process, meaning that these new installers will be available for anyone to use, in OpoLua, <a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a>, or other tooling. I&rsquo;d love to start planning this out.</p> \n<h1><a id=\"gridify\"></a>Gridify</h1> \n<p>During the process of documenting my <a href=\"/posts/2025-12-20-december-adventure-day-19\">Organiser II icon designs</a>, I found myself writing a little Python script (which I&rsquo;ve dubbed <code>gridify</code>) to enlarge pixel art images and add grid lines. The script is tiny but, in the interests of sharing, I&rsquo;ve uploaded it to <a href=\"https://codeberg.org/jbmorley/gridify\">Codeberg</a> and assigned an MIT license.</p> \n<p>Gridify lets you generate previews like this:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2025-12-21-december-adventure-day-20/icons-large-dark/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-21-december-adventure-day-20/icons-large/1600.png\"\n             width=\"1600\"\n             height=\"380\"\n             />\n    </picture>\n</div>\n</p> \n<p>Thinking more about the announced GitHub pricing changes for self-hosted Actions runners, I wonder if a tiny project like this might be a perfect excuse to try out CI alternatives. While GitHub have flip-flopped on this, <a href=\"https://resources.github.com/actions/2026-pricing-changes-for-github-actions/\">announcing that they&rsquo;re delaying the planned per-minute metered pricing for using self-hosted runners</a>, the writing&rsquo;s on the wall, and I need to find an approach that&rsquo;s more sustainable.</p>",
      "date_published": "2025-12-21T19:58:59-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-21-december-adventure-day-20/",
      "title": "December Adventure Day 20",
      "url": "https://jbmorley.co.uk/posts/2025-12-21-december-adventure-day-20/"
    },
    {
      "content_html": "<p>I woke up on day 19 of my <a href=\"/december-adventure\">December Adventure</a> feeling that I&rsquo;ve not used my Psions nearly enough in the past week: while I&rsquo;ve been doing many Psion adjacent things with <a href=\"https://opolua.org\">OpoLua</a> and <a href=\"https://codeberg.org/psion/psiemu\">PsiEmu</a>, I&rsquo;ve not taken the time to sit down and tinker with the devices that inspire my adventuring and serve as enjoyably disconnected tools. With that in mind, I decided to tackle the first item in the <a href=\"/posts/2025-12-01-december-adventure-2025-day-01\">Organiser II lucky dip</a> and, &lsquo;improve the Ideas UX&rsquo;.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-20-december-adventure-day-19/prompt/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-20-december-adventure-day-19/prompt/400.jpeg 400 300,/posts/2025-12-20-december-adventure-day-19/prompt/800.jpeg 800 600,/posts/2025-12-20-december-adventure-day-19/prompt/1200.jpeg 1200 900,/posts/2025-12-20-december-adventure-day-19/prompt/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>Ever since reading about the Organiser II&rsquo;s OPL support for <a href=\"https://www.jaapsch.net/psion/manlzpg.htm#A-6\">user-defined characters</a>, I&rsquo;ve been keen to try it out and replicate the classic menu screen that the built-in programs provide.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-20-december-adventure-day-19/home/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-20-december-adventure-day-19/home/400.jpeg 400 300,/posts/2025-12-20-december-adventure-day-19/home/800.jpeg 800 600,/posts/2025-12-20-december-adventure-day-19/home/1200.jpeg 1200 900,/posts/2025-12-20-december-adventure-day-19/home/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p class=\"caption\">Organiser II LZ home and app menus are a scrollable grid of options with a title bar containing an icon and clock</p> \n<p>Organiser II icons are 5px by 8px, and loaded as a sequence of 8 bytes using the <code>UDG</code> command. For example, the following character is written to user-defined character 1 with <code>UDG 1,30,14,4,14,30,14,11,25</code>.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2025-12-20-december-adventure-day-19/udg-dark/1600.gif\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-20-december-adventure-day-19/udg/1600.gif\"\n             width=\"477\"\n             height=\"424\"\n             />\n    </picture>\n</div>\n</p> \n<p>There&rsquo;s one additional constraint when creating icons for the title bar: the bottom two-rows of pixels are taken up with the underline, meaning that the icons themselves can only be 5px by 6px.</p> \n<p>Armed with this information, I spent an enjoyable analog hour-or-so experimenting with icons:</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-20-december-adventure-day-19/sketch/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-20-december-adventure-day-19/sketch/400.jpeg 400 300,/posts/2025-12-20-december-adventure-day-19/sketch/800.jpeg 800 600,/posts/2025-12-20-december-adventure-day-19/sketch/1200.jpeg 1200 900,/posts/2025-12-20-december-adventure-day-19/sketch/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>Given the restrictions, I&rsquo;m pretty pleased with what I came up with. I&rsquo;m particularly fond of the following ones:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2025-12-20-december-adventure-day-19/icons-large-dark/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-20-december-adventure-day-19/icons-large/1600.png\"\n             width=\"1600\"\n             height=\"380\"\n             />\n    </picture>\n</div>\n</p> \n<p>The lightbulb&mdash;an early design&mdash;remains my favorite for the ideas app, so I set about writing the top-level menu picker. Thankfully, as with OPL on later Psions, there&rsquo;s a lot of conveniences for producing standard UI and it&rsquo;s easy to replicate the menu screen with a combination of <code>UDG</code>, <code>CLOCK</code>, and <code>MENUN</code>:</p> \n<pre><code class=\"language-opl\">menu::\nCLS\nUDG 0,14,17,21,14,10,4,0,31\nUDG 2,0,0,0,0,0,0,0,31\nPRINT CHR$(0);REPT$(CHR$(2),14)\nCLOCK(1)\nm%=MENUN(2,\"New,View,Random\")\nIF m%=0\n  GOTO quit::\nELSEIF m%=1\n  GOTO new::\nELSEIF m%=2\n  GOTO show::\nELSE m%=3\n  GOTO rand::\nENDIF\n</code></pre> \n<p>In the process of updating the program, I kept hitting up against an out of memory error on my Organiser II. It turns out the internal drive is drive &lsquo;A&rsquo; and not drive &lsquo;C&rsquo; as one might expect of DOS and Windows era computers&mdash;I&rsquo;ve been happily writing to (and filling up) my write-once 64k datapak for the past couple of weeks. Time to get a <a href=\"https://www.jaapsch.net/psion/galdev1.htm#formatter\">datapak formatter</a>. 🤦</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-20-december-adventure-day-19/complete/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-20-december-adventure-day-19/complete/400.jpeg 400 300,/posts/2025-12-20-december-adventure-day-19/complete/800.jpeg 800 600,/posts/2025-12-20-december-adventure-day-19/complete/1200.jpeg 1200 900,/posts/2025-12-20-december-adventure-day-19/complete/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>While the ordering of menu items could be better, I&rsquo;m pretty pleased with the outcome. 💡</p> \n<hr /> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-20-december-adventure-day-19/advent-of-beeb/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-20-december-adventure-day-19/advent-of-beeb/400.jpeg 400 300,/posts/2025-12-20-december-adventure-day-19/advent-of-beeb/800.jpeg 800 600,/posts/2025-12-20-december-adventure-day-19/advent-of-beeb/1200.jpeg 1200 900,/posts/2025-12-20-december-adventure-day-19/advent-of-beeb/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p class=\"caption\">Enjoying Colin&rsquo;s <a href=\"https://www.youtube.com/watch?v=RHdgIHr78RM\">Advent of Beeb</a> while designing Organiser II icons—heaven!</p>",
      "date_published": "2025-12-20T13:52:25-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-20-december-adventure-day-19/",
      "title": "December Adventure Day 19",
      "url": "https://jbmorley.co.uk/posts/2025-12-20-december-adventure-day-19/"
    },
    {
      "content_html": "<p>My <a href=\"/december-adventure\">December Adventure</a> took a complete backseat on day 18 as I continued setting up my <a href=\"/posts/2025-12-19-december-adventure-day-17/#home-infrastructure\">new home infrastructure</a>&mdash;the continued decay of <a href=\"https://github.com\">GitHub</a> and other cloud-based services makes this a priority.</p> \n<p>While it took much of the day, I managed to install <a href=\"https://ubuntu.com/server\">Ubuntu server</a>, set up 12TB of ZFS RAIDz1 storage, install <a href=\"https://syncthing.net\">Syncthing</a>, and kick-off an initial sync. With that done, I&rsquo;m in a good position to slowly tick away at the remaining tasks, and explore the additional services I can run. (I&rsquo;m keen to look at self-hosted SCM and CI in the future, but that&rsquo;s definitely one for another day.)</p> \n<p>I hope to return to adventuring on day 19, but I&rsquo;d certainly like to find some time to write more about my various efforts to move away from Big Tech&mdash;I&rsquo;m pretty pleased with the workflows I&rsquo;ve established over the last few years and I&rsquo;d love to share some of that<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>The list of December Adventure ideas includes, &lsquo;write up my read later strategy&rsquo;, which is peripherally related, so there&rsquo;s still a chance I&rsquo;ll do a little of that this month.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-12-19T14:31:44-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-19-december-adventure-day-18/",
      "title": "December Adventure Day 18",
      "url": "https://jbmorley.co.uk/posts/2025-12-19-december-adventure-day-18/"
    },
    {
      "content_html": "<p>Keen to get <a href=\"https://codeberg.org/psion/psiemu\">psiemu</a><sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>, my lightweight launcher for Psion MAME emulators into a usable state, I decided to push on with it on day 17 of my (somewhat Psion themed) <a href=\"/december-adventure\">December Adventure</a>.</p> \n<h1><a id=\"psiemu\"></a>PsiEmu</h1> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-19-december-adventure-day-17/overview@2x/400.png\" width=\"400\" height=\"260\" x-srcset=\"/posts/2025-12-19-december-adventure-day-17/overview@2x/400.png 400 260,/posts/2025-12-19-december-adventure-day-17/overview@2x/800.png 800 521,/posts/2025-12-19-december-adventure-day-17/overview@2x/1200.png 1200 782,/posts/2025-12-19-december-adventure-day-17/overview@2x/1600.png 1600 1043,\" />  \n </body></p> \n<p>My goal with PsiEmu is to create a relatively self-contained solution for getting started with the Psion emulators built into MAME (and maybe even some other emulators in the future). To do this, it should be able to fetch ROMs on your behalf, offer conveniences for managing settings specific to Psion emulation (mounting SSDs, configuring serial ports, etc), and have &lsquo;sandboxed&rsquo; settings and data directories&mdash;it shouldn&rsquo;t break existing MAME configurations. I really want PsiEmu to be a one-stop shop that makes Psion emulation significantly more accessible.</p> \n<p>I ended up spending much of the morning talking through this approach with <a href=\"https://oldbytes.space/@thelastpsion\">Alex</a> who was initially expecting to use PsiEmu as a MAME command-line generator, manually adding options for things like serial port configuration. This user experience would be far more complex than I&rsquo;d like, and the discussion proved incredibly helpful, allowing me to better understand just how important those extra options are, while also giving me a chance to share my goals and have Alex help guide the design. For example, it&rsquo;s clear that having <code>psiemu</code> remain resident to allow emulators to be quickly relaunched is key to software development workflows; as is being able to quickly tweak settings like serial port configurations.</p> \n<p>In addition to the various configuration options, I&rsquo;d also like to add some built-in guides: MAME can be pretty opaque at times, and things like keyboard shortcuts for Psion-specific hardware keys vary subtly from device-to-device. I imagine a per-device &lsquo;info card&rsquo;, as well as an app-wide help (akin to the built-in help on Psions themselves). This seemed like a perfect minimal case study to try out &lsquo;windowing&rsquo; in Curses, so I knocked something up:</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-19-december-adventure-day-17/scrolling@2x/400.gif\" width=\"400\" height=\"266\" x-srcset=\"/posts/2025-12-19-december-adventure-day-17/scrolling@2x/400.gif 400 266,/posts/2025-12-19-december-adventure-day-17/scrolling@2x/800.gif 800 532,/posts/2025-12-19-december-adventure-day-17/scrolling@2x/1200.gif 1200 798,/posts/2025-12-19-december-adventure-day-17/scrolling@2x/1600.gif 1600 1064,\" />  \n </body></p> \n<p>This makes use of &lsquo;pads&rsquo; which are Curses' primitive for scrollviews. They&rsquo;re not the easiest thing to work with, but they allow for an optimized or naive approach to scrolling and will be incredibly useful as the list of emulators grows and I start adding things like lists of settings.</p> \n<p>I also added a handful of additional emulator configurations: Dutch and French Series 3mx variants, and a French Siena. I also experimented with adding an Organiser II which I&rsquo;m not ready to commit yet.</p> \n<h1><a id=\"home-infrastructure\"></a>Home Infrastructure</h1> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-19-december-adventure-day-17/lincstation/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-19-december-adventure-day-17/lincstation/400.jpeg 400 300,/posts/2025-12-19-december-adventure-day-17/lincstation/800.jpeg 800 600,/posts/2025-12-19-december-adventure-day-17/lincstation/1200.jpeg 1200 900,/posts/2025-12-19-december-adventure-day-17/lincstation/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>My afternoon proved somewhat distracted as I received delivery of a <a href=\"https://www.lincplustech.com/products/lincstation-n2-network-attached-storage\">LincStation N2</a> and took some time to try it out. This will be serve as a (hopefully more robust) replacement for a Raspberry Pi 5 that has been acting as my local <a href=\"https://syncthing.net/\">Syncthing</a> node and home-infrastructure for the past year-or-so.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>Or should that be &lsquo;PsiEmu&rsquo;?&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-12-19T13:33:16-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-19-december-adventure-day-17/",
      "title": "December Adventure Day 17",
      "url": "https://jbmorley.co.uk/posts/2025-12-19-december-adventure-day-17/"
    },
    {
      "content_html": "<p>Having made good progress writing a <a href=\"https://codeberg.org/psion/psiemu\">lightweight Psion emulator launcher</a> on <a href=\"/posts/2025-12-16-december-adventure-day-15\">day 15</a> of my <a href=\"/december-adventure\">December Adventure</a>, I decided to continue with it, flesh out the device support, and add a few quality-of-life improvements.</p> \n<h1><a id=\"psiemu\"></a>psiemu</h1> \n<p>As an active contributor to the <a href=\"https://github.com/explit28/Psion-ROM\">explit28/Psion-ROM</a> repository on GitHub, I&rsquo;m aware of the large number of Psion variants out there, but I&rsquo;d not fully internalized it until I tried to produce a UI to make it quick and easy to see them all, and select one.</p> \n<p>After some consideration, I decided to separate the different vendors into their own sections, with Psion and Acorn being the most obvious ones. Ultimately, I&rsquo;d like to add Geofox, Diamond, Oregon Scientific and Ericsson in there too, but I don&rsquo;t think we&rsquo;ve complete emulation for any of their devices yet.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-17-december-adventure-day-16/vendors@2x/400.gif\" width=\"400\" height=\"266\" x-srcset=\"/posts/2025-12-17-december-adventure-day-16/vendors@2x/400.gif 400 266,/posts/2025-12-17-december-adventure-day-16/vendors@2x/800.gif 800 532,/posts/2025-12-17-december-adventure-day-16/vendors@2x/1200.gif 1200 798,/posts/2025-12-17-december-adventure-day-16/vendors@2x/1600.gif 1600 1064,\" />  \n </body></p> \n<p class=\"caption\">Modeling the &lsquo;Vendor → Device → Variant&rsquo; hierarchy</p> \n<p>Introducing new sections forced me to rework the layout and selection handling. To keep things simple, I broke my rendering code into helpers responsible for the different sections. For example,</p> \n<pre><code class=\"language-python\">def render_device_section(devices, is_section_active, y_pos, selection):\n\n    for device_index, profile in enumerate(devices):\n        title = profile[\"title\"].ljust(22)\n        variants = profile[\"variants\"]\n\n        for variant_index, variant in enumerate(variants):\n            name = variant[\"name\"]\n            languages = language_symbol(variant)\n            if is_section_active and device_index == selection.device and variant_index == selection.variant:\n                name = \"-&gt; \" + name\n            else:\n                name = \"   \" + name\n            title += f\"{name} {languages}  \"\n\n        stdscr.addstr(y_pos + device_index, 0, \"  \" + title)\n\n    return y_pos + len(devices)\n</code></pre> \n<p>This renders vendor data in the following structure:</p> \n<pre><code class=\"language-python\">    {\n        \"name\": \"Acorn\",\n        \"devices\": [\n\n            {\n                \"id\": \"pocketbk\",\n                \"title\": \"Acorn Pocket Book\",\n                \"resolution\": (240, 80),\n                \"scale\": 2,\n                \"variants\": [\n                    {\n                        \"name\": \"V1.91F\",\n                        \"bios\": \"191f\",\n                        \"languages\": [\n                            \"en-GB\",\n                        ],\n                    }\n                ]\n            },\n\n            # ...\n            \n        ],\n    },\n</code></pre> \n<p>Right now the layout code is all hand-crafted as I&rsquo;m fairly new to <a href=\"https://docs.python.org/3/howto/curses.html\">Curses</a>, but I have a sense there are many conveniences I can take advantage of in the future. I&rsquo;m looking forward to learning what&rsquo;s possible.</p> \n<p>In addition to adding support for different vendors, I decided to lean more on structured data, starting with supported languages (as shown in the example above). Using metadata like this should allow for richer UI in the future, and I experimented with using emoji as a minimal way to display the languages:</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-17-december-adventure-day-16/languages@2x/400.gif\" width=\"400\" height=\"266\" x-srcset=\"/posts/2025-12-17-december-adventure-day-16/languages@2x/400.gif 400 266,/posts/2025-12-17-december-adventure-day-16/languages@2x/800.gif 800 532,/posts/2025-12-17-december-adventure-day-16/languages@2x/1200.gif 1200 798,/posts/2025-12-17-december-adventure-day-16/languages@2x/1600.gif 1600 1064,\" />  \n </body></p> \n<p>Ultimately, I find the additional color distracts me from the other details in the grid, so I&rsquo;ve decided not to keep them, but I enjoyed seeing what&rsquo;s possible.</p> \n<p>As I progressed through the day, I added more devices and variants to the launcher, making sure the relevant ROMs were present in the <a href=\"https://github.com/explit28/Psion-ROM\">explit28/Psion-ROM</a> repository. My hope is that this can serve as a single source for all preserved Psion and Psion-related firmware. The MAME emulators are a good test for that.</p> \n<p>Wrapping up, I created a <a href=\"https://codeberg.org/psion/psiemu\">new repository</a> on Codeberg and pushed what I have. Nascent as it is, it&rsquo;s nice to get the launcher to a point where I&rsquo;m happy to share it and invite collaboration.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-17-december-adventure-day-16/screenshot-alex/400.png\" width=\"400\" height=\"250\" x-srcset=\"/posts/2025-12-17-december-adventure-day-16/screenshot-alex/400.png 400 250,/posts/2025-12-17-december-adventure-day-16/screenshot-alex/800.png 800 500,/posts/2025-12-17-december-adventure-day-16/screenshot-alex/1200.png 1200 750,/posts/2025-12-17-december-adventure-day-16/screenshot-alex/1600.png 1600 1000,\" />  \n </body></p> \n<p>The day ended on a high when <a href=\"https://oldbytes.space/@thelastpsion\">Alex</a> shared a screenshot of it running. 🥳</p>",
      "date_published": "2025-12-17T11:19:13-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-17-december-adventure-day-16/",
      "title": "December Adventure Day 16",
      "url": "https://jbmorley.co.uk/posts/2025-12-17-december-adventure-day-16/"
    },
    {
      "content_html": "<p>Day 15 of my <a href=\"/december-adventure\">December Adventure</a> started with a request from <a href=\"https://github.com/tomsci\">Tom</a> for some <a href=\"https://en.wikipedia.org/wiki/Psion_Series_3#Psion_Series_3\">Psion Series 3</a> assets for <a href=\"https://opolua.org\">OpoLua</a>. Since I don&rsquo;t have a Series 3 to hand here in Hawai'i, this seemed like the perfect excuse to remind myself how to fire-up an emulator using MAME and focus on, &lsquo;write about <a href=\"https://www.mamedev.org/\">MAME</a> emulation&rsquo;, from my <a href=\"/posts/2025-12-01-december-adventure-2025-day-01\">original list</a> of adventuring possibilities.</p> \n<h1><a id=\"psion-emulation\"></a>Psion Emulation</h1> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2025-12-16-december-adventure-day-15/series3a-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-16-december-adventure-day-15/series3a@2x/1600.png\"\n             width=\"800.0\"\n             height=\"346.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">MAME includes many Psion hardware and language variants</p> \n<p>The Psion emulation story in MAME is really impressive, but I don&rsquo;t find the MAME launcher works well for standalone computers&mdash;it&rsquo;s really designed for launching individual games. Instead, I run specific emulators from the command line. For example, I launch the Series 3a emulator as follows:</p> \n<pre><code class=\"language-sh\">mame \\\n    -window \\\n    -nomaximize \\\n    -skip_gameinfo \\\n    -rompath ~/Software/Psion/ROMs \\\n    -prescale 2 \\\n    -resolution 960x320 \\\n    psion3a\n</code></pre> \n<p>There&rsquo;s a few notable options in there which&mdash;in my opinion&mdash;really help the Psion emulation shine:</p> \n<ul> \n <li><code>-skip_ganeminfo</code>&mdash;don&rsquo;t show unnecessary dialogs</li> \n <li><code>-nomaximize</code>&mdash;run in windowed mode&mdash;I prefer this as the Psions have fairly unusual aspect resolutions</li> \n <li><code>-prescale 2</code>&mdash;enable pixel-doubling for high-resolution displays</li> \n <li><code>-resolution 960x320</code>&mdash;coupled with pixel-doubling, this ensures the emulation displays at exactly 2x for crisp pixels</li> \n</ul> \n<p>There&rsquo;s also some useful keyboard shortcuts to be aware of. These vary from device to device. For the Series 3a, they look like this:</p> \n<table> \n <thead> \n  <tr> \n   <th>Device Key</th> \n   <th>Shortcut</th> \n  </tr> \n </thead> \n <tbody> \n  <tr> \n   <td>On (Esc)</td> \n   <td>Esc</td> \n  </tr> \n  <tr> \n   <td>Off (Psion + Esc)</td> \n   <td>Alt (Opt on macOS) + 1</td> \n  </tr> \n  <tr> \n   <td>Psion</td> \n   <td>Alt (Opt on macOS)</td> \n  </tr> \n  <tr> \n   <td>Help</td> \n   <td>F10</td> \n  </tr> \n  <tr> \n   <td>Menu</td> \n   <td>F11 (Shift + F11 on macOS)</td> \n  </tr> \n  <tr> \n   <td>System</td> \n   <td>F1</td> \n  </tr> \n  <tr> \n   <td>Data</td> \n   <td>F2</td> \n  </tr> \n  <tr> \n   <td>Word</td> \n   <td>F3</td> \n  </tr> \n  <tr> \n   <td>Agenda</td> \n   <td>F4</td> \n  </tr> \n  <tr> \n   <td>Time</td> \n   <td>F5</td> \n  </tr> \n  <tr> \n   <td>World</td> \n   <td>F6</td> \n  </tr> \n  <tr> \n   <td>Calc</td> \n   <td>F7</td> \n  </tr> \n  <tr> \n   <td>Sheet</td> \n   <td>F8</td> \n  </tr> \n </tbody> \n</table> \n<h1><a id=\"a-custom-launcher\"></a>A Custom Launcher</h1> \n<p>Needless to say, I find myself looking up the MAME command line and keyboard shortcuts every time I need to use an emulator. I&rsquo;ve written a number of shell scripts and aliases to help<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>, but there are so many device variants that this approach gets unwieldy pretty. Finding ROMs can also be challenging&mdash;most are available in <a href=\"https://github.com/explit28/Psion-ROM\">explit28/Psion-ROM</a> where we&rsquo;re trying to dump, document, and preserve everything we can find, but it&rsquo;s currently incomplete.</p> \n<p>With that in mind, I decided to finally bite the bullet and start writing my own launcher. The goal here is to clearly present all the different device types and variants to help folks see what&rsquo;s available. I&rsquo;d also like to add support for automatically downloading ROMs in the future.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-16-december-adventure-day-15/preview/400.gif\" width=\"400\" height=\"266\" x-srcset=\"/posts/2025-12-16-december-adventure-day-15/preview/400.gif 400 266,/posts/2025-12-16-december-adventure-day-15/preview/800.gif 800 532,/posts/2025-12-16-december-adventure-day-15/preview/1200.gif 1200 798,/posts/2025-12-16-december-adventure-day-15/preview/1600.gif 1600 1064,\" />  \n </body></p> \n<p>I&rsquo;m using Python and <a href=\"https://docs.python.org/3/howto/curses.html\">Curses</a> to create this to ensure I&rsquo;ve something instantly cross-platform, as I know most of the Psion community choose to use Linux.</p> \n<p>It&rsquo;s been a really fun experience so far, playing around with writing an interactive terminal UI and seeing future possibilities. I&rsquo;ve opted for a grid-layout to show devices and variants<sup id=\"fnref2\"><a href=\"#fn2\" rel=\"footnote\">2</a></sup>, and I&rsquo;ve realized I can add contextual footers showing additional device details and, ultimately, interactive pickers for configuring attached SSDs, serial ports, etc.</p> \n<p>I hope to publish this nascent launcher over the coming days. 🚀</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>If you&rsquo;re using Linux, you can also create <a href=\"https://wiki.archlinux.org/title/Desktop_entries\">desktop entries</a> for quickly launching each Psion emulators.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> <p>For example, to create a local Series 3a desktop entry, create <code>.local/share/applications/psion-series-3a.desktop</code> containing something like:</p> <pre><code class=\"language-ini\">  [Desktop Entry]\n\n  Type=Application\n  Version=1.0\n  Name=Psion Series 3a\n  Path=/home/parallels\n  Exec=/usr/bin/mame -window -nomaximize -skip_gameinfo -rompath /home/parallels/Software/Psion/ROMs -prescale 2 -resolution 960x320 psion3a\n  Terminal=false\n</code></pre> </li> \n  <li id=\"fn2\"> <p>Perhaps I was influenced by <a href=\"https://www.gingerbeardman.com/\">Matt Sephton</a>&rsquo;s notes on <a href=\"https://blog.gingerbeardman.com/2025/10/11/how-to-tame-a-user-interface-using-a-spreadsheet/\">using spreadsheets for UI design</a>.&nbsp;<a href=\"#fnref2\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-12-16T13:06:29-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-16-december-adventure-day-15/",
      "title": "December Adventure Day 15",
      "url": "https://jbmorley.co.uk/posts/2025-12-16-december-adventure-day-15/"
    },
    {
      "content_html": "<p>I&rsquo;m trying to stick to daily write-ups of my <a href=\"/december-adventure\">December Adventure</a>, meaning that I write about the less exciting days, as well as the more exciting ones. Day 14 falls firmly in latter camp.</p> \n<p>I had hoped to hit the ground running by reinforcing the hinges of my Psion Series 3a with JB Weld, and later carefully opening my Toshiba Libretto 50CTs to remove their (hopefully non-leaky) backup batteries. Instead <a href=\"/posts/2025-12-14-december-adventure-day-13/\">day 13&rsquo;s write-up</a> proved a slow one, and then I found myself trapped, deliberating how to photograph and film these tasks so I can document them.</p> \n<p>I have been experimenting with a (very flimsy) lightweight tripod and my iPhone to capture top-down photos with very little success: the camera slowly sinks as the tripod really isn&rsquo;t designed for this orientation; it&rsquo;s hard to get good lighting; and the iPhone&rsquo;s automatic exposure and magic colour-correction means images vary wildly. It&rsquo;s also hard to keep the whole plane in focus when doing closeup work. (This really isn&rsquo;t my area of expertise.)</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-15-december-adventure-day-14/psiboard-black/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-15-december-adventure-day-14/psiboard-black/400.jpeg 400 300,/posts/2025-12-15-december-adventure-day-14/psiboard-black/800.jpeg 800 600,/posts/2025-12-15-december-adventure-day-14/psiboard-black/1200.jpeg 1200 900,/posts/2025-12-15-december-adventure-day-14/psiboard-black/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-15-december-adventure-day-14/psiboard-yellow/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-15-december-adventure-day-14/psiboard-yellow/400.jpeg 400 300,/posts/2025-12-15-december-adventure-day-14/psiboard-yellow/800.jpeg 800 600,/posts/2025-12-15-december-adventure-day-14/psiboard-yellow/1200.jpeg 1200 900,/posts/2025-12-15-december-adventure-day-14/psiboard-yellow/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p class=\"caption\">The iPhone makes it incredibly challenging to maintain consistent exposure and color temperature</p> \n<p>As I find myself working more and more on physical projects, I&rsquo;d love to come up with a setup that I don&rsquo;t need to think about. I plan to try <a href=\"https://halide.cam/\">Halide</a> over the next few days in the hope that this will give me more control, and I&rsquo;ve been noodling on a seemingly TikTok-focused tripod that&rsquo;s essentially an anglepoise arm with a phone mount and ring light. Perhaps I can get into retro unboxings. 📦🙃</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-15-december-adventure-day-14/desk-tripod/400.jpeg\" width=\"400\" height=\"348\" x-srcset=\"/posts/2025-12-15-december-adventure-day-14/desk-tripod/400.jpeg 400 348,/posts/2025-12-15-december-adventure-day-14/desk-tripod/800.jpeg 800 696,/posts/2025-12-15-december-adventure-day-14/desk-tripod/1200.jpeg 1200 1044,/posts/2025-12-15-december-adventure-day-14/desk-tripod/1600.jpeg 1500 1306,\" />  \n </body></p> \n<p class=\"caption\">Amazon—🤢—sells near-identical social media &lsquo;desk tripods&rsquo; by a seemingly infinite number of brands</p>",
      "date_published": "2025-12-15T12:18:23-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-15-december-adventure-day-14/",
      "title": "December Adventure Day 14",
      "url": "https://jbmorley.co.uk/posts/2025-12-15-december-adventure-day-14/"
    },
    {
      "content_html": "<p>Having spent much of the past week of my <a href=\"/december-adventure\">December Adventure</a> working on <a href=\"https://opolua.org\">OpoLua</a>, I decided it was time for a change. I again consulted the <a href=\"/posts/2025-12-01-december-adventure-2025-day-01/\">Organiser II lucky dip</a>, which called for, &lsquo;Series 3a hinge replacement&rsquo;.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-14-december-adventure-day-13/prompt/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-14-december-adventure-day-13/prompt/400.jpeg 400 300,/posts/2025-12-14-december-adventure-day-13/prompt/800.jpeg 800 600,/posts/2025-12-14-december-adventure-day-13/prompt/1200.jpeg 1200 900,/posts/2025-12-14-december-adventure-day-13/prompt/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>While this prompt is slightly incorrect (it should read &lsquo;reinforcement&rsquo;), it was nice to have something come up that isn&rsquo;t purely software; a perfect change of pace for the weekend. There&rsquo;s something incredibly cathartic and soothing about the process of maintenance, and I immediately found myself thinking of other tasks I&rsquo;d also like to get to: I need to check the batteries in my Libretto 50CTs, finally get my <a href=\"https://github.com/keirf/Greaseweazle/wiki\">Greaseweazle</a> working, and tidy up my home rack a little.</p> \n<p>While some readers will be disappointed that I didn&rsquo;t immediately break out the JB Weld and launch into reinforcing my Series 3a&rsquo;s hinges, I decided to pace myself and approach a few of these tasks over the course of the weekend, picking whichever spoke to me in the moment.</p> \n<h1><a id=\"greaseweazle\"></a>Greaseweazle</h1> \n<p>Like many folks who play around with retro hardware, I picked up a Greaseweazle some time ago to allow me to read whatever floppy disks I encounter in my travels. And (I suspect), like many, my Greaseweazle has been sitting in a box since I got it: I managed to buy a 3 &frac12;&quot; floppy drive, but failed to pick up any cables, and certainly didn&rsquo;t start looking for a case.</p> \n<p>Since I can&rsquo;t do much with it until I have the appropriate cables, I ordered a <a href=\"https://www.ebay.com/itm/256922871190\">power cable</a> and <a href=\"https://www.cablesonline.com/36unflopdriv.html\">floppy drive ribbon cable</a>, and set about looking for a case to print. While I usually design parts for myself, I wanted to try a community design this time and, after some browsing, I selected <a href=\"https://www.printables.com/model/1114270-case-for-greaseweazle-v41-and-35-floppy-drive\">this design</a> by <a href=\"https://www.printables.com/@Dekkia\">Dekkia</a>, remixed from <a href=\"https://www.printables.com/model/918695-case-for-greaseweazle-f7-and-35-floppy-drive\">this one</a> for earlier Greazeweazle models.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-14-december-adventure-day-13/bottom/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-14-december-adventure-day-13/bottom/400.jpeg 400 300,/posts/2025-12-14-december-adventure-day-13/bottom/800.jpeg 800 600,/posts/2025-12-14-december-adventure-day-13/bottom/1200.jpeg 1200 900,/posts/2025-12-14-december-adventure-day-13/bottom/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>The case, designed in <a href=\"https://openscad.org/\">OpenSCAD</a>, prints in two halves and ended up being a pretty good color match for my floppy drive, when printed using Bambu Labs&rsquo;s <a href=\"https://us.store.bambulab.com/products/pla-matte?id=43292383936648\">Matte Bone White filament</a>.</p> \n<p>I regret not noticing that the design had been modified to remove the threaded inserts from the original, but thankfully I was able to drill out the holes and fit some I had lying around&mdash;the goal was not to produce something perfect, but something that helps me get the Greaseweazle up and running.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-14-december-adventure-day-13/inserts/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-14-december-adventure-day-13/inserts/400.jpeg 400 300,/posts/2025-12-14-december-adventure-day-13/inserts/800.jpeg 800 600,/posts/2025-12-14-december-adventure-day-13/inserts/1200.jpeg 1200 900,/posts/2025-12-14-december-adventure-day-13/inserts/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p class=\"caption\">The top case with threaded inserts installed</p> \n<p>With the threaded inserts installed, it was just a matter of screwing everything together and waiting for the cables to turn up.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-14-december-adventure-day-13/complete/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-14-december-adventure-day-13/complete/400.jpeg 400 300,/posts/2025-12-14-december-adventure-day-13/complete/800.jpeg 800 600,/posts/2025-12-14-december-adventure-day-13/complete/1200.jpeg 1200 900,/posts/2025-12-14-december-adventure-day-13/complete/1600.jpeg 1600 1200,\" />  \n </body></p> \n<h1><a id=\"mini-patch-panels\"></a>Mini Patch Panels</h1> \n<p>I have a small home 4U rack that hosts&mdash;amongst other things&mdash;my Raspberry Pis in a <a href=\"https://thepihut.com/products/uctronics-19-1u-raspberry-pi-rack-mount-with-ssd-mounting-brackets\">UCTRONICS 1U rack bracket</a>. This holds up to 4 Raspberry Pis on individual sleds:</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-14-december-adventure-day-13/sled/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-14-december-adventure-day-13/sled/400.jpeg 400 300,/posts/2025-12-14-december-adventure-day-13/sled/800.jpeg 800 600,/posts/2025-12-14-december-adventure-day-13/sled/1200.jpeg 1200 900,/posts/2025-12-14-december-adventure-day-13/sled/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>Since I only need to use two of the sleds, I decided to replace the other ones with custom keystone-compatible blanking plates to allow me to manage the ethernet and USB cabling within this single 1U of space, obviating the need for a separate patch panel. I designed the prerequisite part, printed, and fitted it.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-14-december-adventure-day-13/panel/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-14-december-adventure-day-13/panel/400.jpeg 400 300,/posts/2025-12-14-december-adventure-day-13/panel/800.jpeg 800 600,/posts/2025-12-14-december-adventure-day-13/panel/1200.jpeg 1200 900,/posts/2025-12-14-december-adventure-day-13/panel/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>With these new mini patch panels in place (one serving each Pi), I now have a whole 1U of space to play around with for more adventures in the future.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-14-december-adventure-day-13/rack/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-14-december-adventure-day-13/rack/400.jpeg 400 300,/posts/2025-12-14-december-adventure-day-13/rack/800.jpeg 800 600,/posts/2025-12-14-december-adventure-day-13/rack/1200.jpeg 1200 900,/posts/2025-12-14-december-adventure-day-13/rack/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>I hope to publish these designs over the next few weeks. If you&rsquo;re excited to get hold of them before then, please feel free to <a href=\"/about\">get in touch</a>.</p> \n<h1><a id=\"opolua\"></a>OpoLua</h1> \n<p>I couldn&rsquo;t get through the day without making a couple of tweaks to OpoLua: I <a href=\"https://github.com/inseven/opolua/commit/fef032574d566d25f7a03b3659761692ae1e853c\">renamed the macOS Qt builds</a> to &lsquo;OpoLua Qt.app&rsquo; to allow them to live side-by-side with the Mac Catalyst versions, and I <a href=\"https://github.com/inseven/opolua/commit/cc65ac1cc30fedf1936cd9d74153bcba4d8e9d01\">switched to using a pre-built version of Qt</a> which cuts build times by about 55 minutes. 🏎️</p>",
      "date_published": "2025-12-14T16:42:16-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-14-december-adventure-day-13/",
      "title": "December Adventure Day 13",
      "url": "https://jbmorley.co.uk/posts/2025-12-14-december-adventure-day-13/"
    },
    {
      "content_html": "<p>Continuing with Day 12 of my <a href=\"/december-adventure\">December Adventure</a>, I decided to make the most of the little momentum I&rsquo;ve built up around <a href=\"https://opolua.org\">OpoLua</a> over the past few days and continue working on Qt builds.</p> \n<p>I had an outstanding branch with macOS Qt builds from the early days of development, so I cautiously cherry-picked the relevant changes to see if it still built. Much to my surprise, it did! I followed my usual pragmatic approach of hand-crafting a <a href=\"https://github.com/inseven/opolua/blob/main/scripts/build-qt.sh\">simple build script</a> rather than trying to use any magic tooling; something I have found to be significantly more maintainable over the lifetime of a project. You can check out the full change <a href=\"https://github.com/inseven/opolua/commit/f90c0e8029cac8a7a2029d8ae8b82bce6833f795\">here</a> if you&rsquo;re curious.</p> \n<p>At this early stage, I&rsquo;m making no attempt to publish the binaries&mdash;we need to update the OpoLua license to conform to Qt&rsquo;s requirements around statically linking before we can do that. For the time being this will help give us greater confidence that we&rsquo;re not breaking things, and allow us to test our work.</p> \n<hr /> \n<p>Having broken the back of&mdash;at least&mdash;the macOS Qt builds, I found my thoughts returning to the incredibly absurd and unsustainable situation we&rsquo;ve found ourselves in: centralized web-based infrastructure, lock-in, spiralling service prices, aggressive deployment of unproven AI, inflation, and skyrocketing consumer hardware prices. These trends concern me, and I&rsquo;m worried that, even though I try to build open tooling, by targeting closed platforms like macOS and iOS, I play a part in perpetuating this imbalance.</p>",
      "date_published": "2025-12-13T13:15:41-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-13-december-adventure-day-12/",
      "title": "December Adventure Day 12",
      "url": "https://jbmorley.co.uk/posts/2025-12-13-december-adventure-day-12/"
    },
    {
      "content_html": "<img src='/photos/snapshots/2025-12-12-18-02-54-sunset/1600.jpeg' width='1600' height='1200'></img><p>It's altogether too easy to forget the beautiful world we have on our doorstep here in Hawai'i.</p>",
      "date_published": "2025-12-12T18:02:54-08:00",
      "id": "https://jbmorley.co.uk/photos/snapshots/2025-12-12-18-02-54-sunset/",
      "title": "Sunset",
      "url": "https://jbmorley.co.uk/photos/snapshots/2025-12-12-18-02-54-sunset/"
    },
    {
      "content_html": "<p>Day 11 of my <a href=\"/december-adventure\">December Adventure</a> proved another slow one, in spite of the <a href=\"/posts/2025-12-01-december-adventure-2025-day-01\">Organiser lucky dip</a> demanding that I yet again return to <a href=\"https://opolua.org\">OpoLua</a> to, &lsquo;ship OpoLua Qt&rsquo;.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-12-december-adventure-day-11/prompt/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-12-december-adventure-day-11/prompt/400.jpeg 400 300,/posts/2025-12-12-december-adventure-day-11/prompt/800.jpeg 800 600,/posts/2025-12-12-december-adventure-day-11/prompt/1200.jpeg 1200 900,/posts/2025-12-12-december-adventure-day-11/prompt/1600.jpeg 1600 1200,\" />  \n </body></p> \n<h1><a id=\"mac-app-store\"></a>Mac App Store</h1> \n<p>The day got off to a good start with Apple approving the macOS Catalyst build. This is now available to download from the <a href=\"https://apps.apple.com/app/opolua/id1604029880\">Mac App Store</a>. 🥳</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n                        >\n        <source srcset=\"/posts/2025-12-12-december-adventure-day-11/software-index-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-12-december-adventure-day-11/software-index@2x/1600.png\"\n             width=\"800.0\"\n             height=\"579.5\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Discovering OPL programs is easy using the built-in Software Index</p> \n<h1><a id=\"opolua-qt\"></a>OpoLua Qt</h1> \n<p>Shipping the new Qt version of OpoLua is something I&rsquo;ve been wanting to do for some months now&mdash;it brings a comprehensive suite of command line tools and the OPL runtime to Linux, Windows, and macOS, and it&rsquo;s about time we get it out there.</p> \n<p>I ended up spending the day doing the kind of boring project administration that is pretty thankless in the moment, but makes many things easier going forwards. The primary change was to move the iOS app into its own subdirectory, making the Qt and iOS targets peers in the <a href=\"https://github.com/inseven/opolua\">source code</a>. I also took a moment to update the app icon on the website to use the original EPOC32 OPL icon&mdash;this makes things a little less Apple-centric and, over time, I&rsquo;ll make more changes to make room for the Qt app<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-12-december-adventure-day-11/icon/1600.png\" width=\"192\" height=\"192\" x-srcset=\"/posts/2025-12-12-december-adventure-day-11/icon/1600.png 192 192,/posts/2025-12-12-december-adventure-day-11/icon/800.png 192 192,/posts/2025-12-12-december-adventure-day-11/icon/1200.png 192 192,/posts/2025-12-12-december-adventure-day-11/icon/400.png 192 192,\" />  \n </body></p> \n<p class=\"caption\">This early OPL icon now feels far more suitable</p> \n<hr /> \n<p>With some of the preparatory work for OpoLua Qt in place, I hope day 12 will bring more visible progress: I&rsquo;d love to have automated Mac, Windows, and Linux builds in the next few days even if, in the first instance, they&rsquo;re just smoke test builds to ensure we can&rsquo;t break things.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>Freed from the restrictions of iOS and mobile application UX, the Qt version of OpoLua works quite differently to the iOS app, eschewing the centralised program library. This means we&rsquo;ll have to provide distinct documentation, screenshots, and support sections on the website.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-12-12T17:20:41-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-12-december-adventure-day-11/",
      "title": "December Adventure Day 11",
      "url": "https://jbmorley.co.uk/posts/2025-12-12-december-adventure-day-11/"
    },
    {
      "content_html": "<p>Day 10 brought a much needed pause in my <a href=\"/december-adventure\">adventuring</a> to catch up on other aspects of life, with a couple of exceptions:</p> \n<ul> \n <li>I added &lsquo;try out Plan 9&rsquo; to the <a href=\"/posts/2025-12-01-december-adventure-2025-day-01\">Organser lucky dip</a></li> \n <li>The macOS build of <a href=\"https://opolua.org\">OpoLua</a> was rejected from the Mac App Store as &lsquo;incomplete&rsquo;&mdash;it seems like they couldn&rsquo;t figure out how to use the app, which makes some sense as it&rsquo;s fairly niche. I&rsquo;ve updated the instructions and provided some additional example files. 🤞</li> \n</ul>",
      "date_published": "2025-12-11T12:16:44-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-11-december-adventure-day-10/",
      "title": "December Adventure Day 10",
      "url": "https://jbmorley.co.uk/posts/2025-12-11-december-adventure-day-10/"
    },
    {
      "content_html": "<p>Having submitted version 2.0.0 of <a href=\"https://opolua.org\">OpoLua</a> to the App Store during <a href=\"/posts/2025-12-08-december-adventure-day-08\">day 8</a> of my <a href=\"/december-adventure\">December Adventure</a>, I braced myself for the inevitable rejection, and set about having a somewhat slower day. Much to my surprise, it was approved! Find it <a href=\"https://apps.apple.com/app/opolua/id1604029880\">here</a>. 🎉</p> \n<h1><a id=\"opolua\"></a>OpoLua</h1> \n<p>Spurred on by this unexpected win, I generated screenshots for the macOS build, submitted it to the Mac App Store, and did a little project housekeeping.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-10-december-adventure-day-09/mac@2x/400.png\" width=\"400\" height=\"250\" x-srcset=\"/posts/2025-12-10-december-adventure-day-09/mac@2x/400.png 400 250,/posts/2025-12-10-december-adventure-day-09/mac@2x/800.png 800 500,/posts/2025-12-10-december-adventure-day-09/mac@2x/1200.png 1200 750,/posts/2025-12-10-december-adventure-day-09/mac@2x/1600.png 1600 1000,\" />  \n </body></p> \n<p class=\"caption\">The Welcome example program running on macOS</p> \n<p>Version 2 brings a dedicated macOS Catalyst build, allowing us to better target Mac-specific behaviour. Longer term, I anticipate we&rsquo;ll replace this with the Qt build, but there&rsquo;s a lot of work to do before that happens.</p> \n<p>With a little more confidence that OpoLua still passes App Review guidelines, I took some time to try out <a href=\"https://developer.apple.com/icon-composer/\">Icon Composer</a> and create icons that will hopefully play better with Liquid Glass&mdash;Apple apply a bunch of heuristics to generate icons if you don&rsquo;t provide your own, so doing this me much greater control over how things appear. This updated icon isn&rsquo;t shipping in 2.0.0, but it&rsquo;s ready and waiting for the next point release.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n                        >\n        <source srcset=\"/posts/2025-12-10-december-adventure-day-09/icon-composer-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-10-december-adventure-day-09/icon-composer@2x/1600.png\"\n             width=\"800.0\"\n             height=\"512.5\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Icon Composer lets you specify how icons appear in all display styles</p> \n<p>I also took a little time to give the Revo some love: <a href=\"https://github.com/kapfab\">Fabrice</a> (who&rsquo;s been rigorously testing OpoLua throughout our development process) noticed that the toolbar <a href=\"https://github.com/kapfab\">doesn&rsquo;t display correctly</a>, so I grabbed a few example screenshots and Revo-specific resource files to help us improve our support.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-10-december-adventure-day-09/mbornes-revo/400.png\" width=\"400\" height=\"133\" x-srcset=\"/posts/2025-12-10-december-adventure-day-09/mbornes-revo/400.png 400 133,/posts/2025-12-10-december-adventure-day-09/mbornes-revo/800.png 480 160,/posts/2025-12-10-december-adventure-day-09/mbornes-revo/1200.png 480 160,/posts/2025-12-10-december-adventure-day-09/mbornes-revo/1600.png 480 160,\" />  \n </body></p> \n<h1><a id=\"brackets\"></a>Brackets</h1> \n<p>One of the possible prompts I set myself myself for my December Adventure was to, &lsquo;write about my 3D printed brackets&rsquo;. While that day&rsquo;s yet to come, I did take a little time to revisit my MagSafe bracket to obviate the need for screws and thermal inserts, and make it printable without supports.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n                        >\n        <source srcset=\"/posts/2025-12-10-december-adventure-day-09/print-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-10-december-adventure-day-09/print@2x/1600.png\"\n             width=\"800.0\"\n             height=\"633.0\"\n             />\n    </picture>\n</div>\n</p> \n<p>I&rsquo;m pretty pleased with the outcome:</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-10-december-adventure-day-09/magsafe/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-10-december-adventure-day-09/magsafe/400.jpeg 400 300,/posts/2025-12-10-december-adventure-day-09/magsafe/800.jpeg 800 600,/posts/2025-12-10-december-adventure-day-09/magsafe/1200.jpeg 1200 900,/posts/2025-12-10-december-adventure-day-09/magsafe/1600.jpeg 1600 1200,\" />  \n </body></p> \n<hr /> \n<p>For day 10, I plan to return to the <a href=\"/posts/2025-12-01-december-adventure-2025-day-01\">Organiser II lucky dip</a> and am hopeful of a little more retro spelunking&mdash;I&rsquo;ve got my fingers crossed for <a href=\"https://marchintosh.com/globaltalk.html\">GlobalTalk</a>.</p>",
      "date_published": "2025-12-10T10:28:56-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-10-december-adventure-day-09/",
      "title": "December Adventure Day 09",
      "url": "https://jbmorley.co.uk/posts/2025-12-10-december-adventure-day-09/"
    },
    {
      "content_html": "<p>Day 8 continues the &lsquo;ship <a href=\"https://opolua.org\">OpoLua</a>&rsquo; portion of my <a href=\"/december-adventure\">December Adventure</a>. Since I&rsquo;m already three days into this (and since I detailed absolutely every one of day 7&rsquo;s changes), I&rsquo;m going to keep this write-up brief, with links to the relevant PRs on GitHub for the curious.</p> \n<p>I put the last few changes in place in preparation for submitting to App Store Review:</p> \n<ul> \n <li>fix: 😶‍🌫️ Ensure content isn’t occluded by the sidebar (<a href=\"https://github.com/inseven/opolua/pull/568\">#568</a>)</li> \n <li>fix: 👆 Ensure drag gestures are passed to OPL on iOS 26 (<a href=\"https://github.com/inseven/opolua/pull/567\">#567</a>)</li> \n <li>fix: 🛑 Show errors when the Software Index fails to load (<a href=\"https://github.com/inseven/opolua/pull/566\">#566</a>)</li> \n <li>fix: ⏳ Show a progress spinner when loading the Software Index (<a href=\"https://github.com/inseven/opolua/pull/565\">#565</a>)</li> \n <li>fix: 🤐 Don&rsquo;t show Software Index entries without names (<a href=\"https://github.com/inseven/opolua/pull/564\">#564</a>)</li> \n <li>fix: 💎 Use the iOS 26 style buttons for the installer (<a href=\"https://github.com/inseven/opolua/pull/563\">#563</a>)</li> \n</ul> \n<p>Most of these were small legacy bug fixes, or final bits of polish to the new Software Index views. Perhaps the only one that really warrants comment is the change relating to drag gestures: iOS 26 has introduced a new navigation stack pop gesture recognizer (<code>.interactiveContentPopGestureRecognizer</code>) which now helpfully steals left-to-right drag gestures from anywhere in your views, and needed to be disabled.</p> \n<p>With these changes in-place, I updated the iPhone and iPad screenshots and submitted the iOS build to Apple for review. I&rsquo;ll follow up with the macOS build if everything goes smoothly. 🤞</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n                        >\n        <source srcset=\"/posts/2025-12-08-december-adventure-day-08/tile-fall-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-08-december-adventure-day-08/tile-fall@2x/1600.png\"\n             width=\"800.0\"\n             height=\"551.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Tile Fall running in OpoLua on the iPad Pro 11&quot;</p> \n<hr /> \n<p>Having had a fairly intense few days getting OpoLua ready for submission, I took advantage of the wait to catch up on my RSS feeds, and other aspects of life. A friend also reminded me that, for my own well-being, it&rsquo;s important to focus on things that help me feel like I&rsquo;m really getting to exercise my various engineering skills&mdash;something which catching up to new Apple SDKs does not&mdash;so I&rsquo;m going to reflect on that over the coming days.</p>",
      "date_published": "2025-12-09T00:49:53-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-08-december-adventure-day-08/",
      "title": "December Adventure Day 08",
      "url": "https://jbmorley.co.uk/posts/2025-12-08-december-adventure-day-08/"
    },
    {
      "content_html": "<p>Keen to preserve the momentum from <a href=\"/posts/2025-12-07-december-adventure-day-06\">day 6</a> of my <a href=\"/december-adventure\">December Adventure</a>, I decided to continue working on <a href=\"https://opolua.org\">OpoLua</a>&mdash;I&rsquo;d really like to ship version 2 before the month is over.</p> \n<h1><a id=\"opolua-2.0\"></a>OpoLua 2.0</h1> \n<p>While it doesn&rsquo;t make for the most exciting write-up, I pressed on, polishing the <a href=\"https://software.psion.info\">Psion Software Index</a> browser, and ironing out all the little wrinkles introduced by iOS 26 and the new SDK. For the curious, I&rsquo;ve detailed some of the changes in the following sections.</p> \n<h2><a id=\"improved-library-management\"></a>Improved Library Management</h2> \n<p>With the introduction of the Software Index browser, there are now three different ways to get software into OpoLua: adding a folder to the sidebar, opening a standalone SIS (installer) file, and downloading a program from the Software Index. To try to better differentiate these flows, I switched to a plus-badged folder symbol for adding folders to the library, and moved the installer and Software Index flows to a menu in the main programs view. My hope is that by associating the actions with particular views, users will have an improved context for understanding the operations.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n                        >\n        <source srcset=\"/posts/2025-12-07-december-adventure-day-07/library-dark@3x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-07-december-adventure-day-07/library@3x/1600.png\"\n             width=\"393.0\"\n             height=\"852.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">The Library view now boasts an add folder button</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n                        >\n        <source srcset=\"/posts/2025-12-07-december-adventure-day-07/install-menu-dark@3x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-07-december-adventure-day-07/install-menu@3x/1600.png\"\n             width=\"393.0\"\n             height=\"852.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Tapping the plus button in programs views shows an install menu</p> \n<h2><a id=\"software-index-browser\"></a>Software Index Browser</h2> \n<p>Thankfully I&rsquo;d already implemented most of the Software Index browser as part of the work to add it to <a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a>, my modern Psion <a href=\"https://github.com/plptools/plptools\">plptools</a>-based connectivity suite for macOS. This left a few remaining tasks:</p> \n<ul> \n <li>showing a spinner during initial load</li> \n <li>showing a progress indicator (spinner) when downloading programs</li> \n <li>displaying program hashes (used to differentiate between multiple builds with the same version) using a monospaced font</li> \n</ul> \n<p>These proved relatively simple, with the exception of using a monospaced font, which necessitated a custom <code>View</code> extension as the <code>.monospaced</code> modifier isn&rsquo;t available prior to iOS 16 (OpoLua targets iOS 15):</p> \n<pre><code class=\"language-swift\">struct Monospaced: ViewModifier {\n\n    @Environment(\\.font) var font\n\n    func body(content: Content) -&gt; some View {\n        if #available(macOS 13.0, iOS 16.0, *) {\n            return content\n                .monospaced()\n        } else {\n            return content\n                .font((font ?? .body).monospaced())\n        }\n    }\n}\n\nextension View {\n\n    func monospacedCompat() -&gt; some View {\n        return modifier(Monospaced())\n   }\n\n}\n</code></pre> \n<h2><a id=\"silkscreen-buttons\"></a>Silkscreen Buttons</h2> \n<p>I updated the corner radii of the in-program soft silkscreen buttons&mdash;it turns out these were picking up the new metrics on iOS 26 and clipping the symbols.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n                        >\n        <source srcset=\"/posts/2025-12-07-december-adventure-day-07/silkscreen-dark@3x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-07-december-adventure-day-07/silkscreen@3x/1600.png\"\n             width=\"533.33333333333\"\n             height=\"246.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">iOS 26 wants to round absolutely everything</p> \n<h2><a id=\"dismiss-buttons\"></a>Dismiss Buttons</h2> \n<p>iOS 26 has all-but entirely moved away from buttons with text: &lsquo;Done&rsquo;, &lsquo;Save&rsquo;, and &lsquo;Cancel&rsquo; buttons have all been replaced by xmarks and checkmarks. Unfortunately folks using older versions of iOS still (rightly) expect the older behaviour, necessitating conditional code like this:</p> \n<pre><code class=\"language-swift\">ToolbarItem(placement: .confirmationAction) {\n    Button {\n        dismiss()\n    } label: {\n        if #available(iOS 26, *) {\n            Image(systemName: \"xmark\")\n        } else {\n            Text(\"Done\")\n        }\n    }\n}\n</code></pre> \n<p>The Settings, Installer, and Software Index views all needed updating in subtly different ways as we use a combination of UIKit and SwiftUI depending on the specific needs of each view and view controller.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n                        >\n        <source srcset=\"/posts/2025-12-07-december-adventure-day-07/settings-dark@3x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-07-december-adventure-day-07/settings@3x/1600.png\"\n             width=\"393.0\"\n             height=\"852.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">The Settings view boasting a new symbolic dismiss button</p> \n<p>While this kind of seemingly simple design language should make things easier, selecting the correct symbol and button type continues to require altogether too much deliberation: platform-provided semantic confirmation buttons on iOS 26 use a checkmark and are tinted making them far too prominent for most scenarios, often forcing me to override the platform defaults.</p> \n<h2><a id=\"swipe-actions\"></a>Swipe Actions</h2> \n<p>Continuing with the trend of replacing text with icons, swipe actions now use symbols, and the swipe action for removing library folders needed updating accordingly.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n                        >\n        <source srcset=\"/posts/2025-12-07-december-adventure-day-07/remove-folder-dark@3x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-07-december-adventure-day-07/remove-folder@3x/1600.png\"\n             width=\"393.0\"\n             height=\"852.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Nothing is safe from the iOS 26 redesign</p> \n<hr /> \n<p>With all these changes in place, I&rsquo;m left with perhaps another day of things to do before I can submit to the App Store:</p> \n<ul> \n <li>the Installer &lsquo;action&rsquo; buttons have the wrong radii on iOS 26</li> \n <li>the Software Index should only show OPL programs</li> \n <li>the new iOS 26 dismiss gesture steals drags from the running OPL program</li> \n <li>macOS and iPadOS need testing</li> \n</ul> \n<p>Days like this are hard: I always find this process of adopting new Apple SDKs and UIs incredibly gruelling&mdash;a constant process of firefighting and decision fatigue that leaves me with very little room to apply the kind of slow, considered design I&rsquo;d like to bring to my apps. I find myself uncertain of my own work, wondering if I need to look to other platforms in the future.</p> \n<p>That said, I&rsquo;m excited to be moving towards shipping OpoLua. If you&rsquo;d like to try out the latest TestFlight build, please drop me an email at <a href=\"mailto:support@opolua.org\">support@opolua.org</a>.</p> \n<h1><a id=\"organiser-ii-software\"></a>Organiser II Software</h1> \n<p>Seeking a moment of respite, I looked into trying out a few more games on my Organsier II, only to find that many need to be written to ROMs to work. As I&rsquo;ve only a single 64k datapak, I immediately found myself on eBay looking for some, and possibly nerd-sniped myself into building modern storage solutions. Pursuits for another day&hellip;</p>",
      "date_published": "2025-12-07T22:54:29-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-07-december-adventure-day-07/",
      "title": "December Adventure Day 07",
      "url": "https://jbmorley.co.uk/posts/2025-12-07-december-adventure-day-07/"
    },
    {
      "content_html": "<p>Day 6 of my <a href=\"/december-adventure\">December Adventure</a> started with a slew of GitHub notifications from <a href=\"jttps://github.com/tomsci\">Tom</a>, who&rsquo;s been working on yet more fixes for <a href=\"https://opolua.org\">OpoLua</a> and, as if by fate, my Organiser II <a href=\"/posts/2025-12-01-december-adventure-2025-day-01\">lucky dip</a> instructed me to, &lsquo;Ship OpoLua 2.0 for iOS&rsquo;. Who am I to argue? 🤷</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-07-december-adventure-day-06/prompt/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-07-december-adventure-day-06/prompt/400.jpeg 400 300,/posts/2025-12-07-december-adventure-day-06/prompt/800.jpeg 800 600,/posts/2025-12-07-december-adventure-day-06/prompt/1200.jpeg 1200 900,/posts/2025-12-07-december-adventure-day-06/prompt/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>It&rsquo;s been over a year since we last shipped version 1.1.2 of OpoLua and, in that time, Tom&rsquo;s added database support and a myriad fixes, allowing us to run many new programs. We&rsquo;ve also added support for browsing and installing from the <a href=\"https://software.psion.community\">Psion Software Index</a> directly in-app. Enough to warrant a major version bump.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n                        >\n        <source srcset=\"/posts/2025-12-07-december-adventure-day-06/software-index-dark@3x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-07-december-adventure-day-06/software-index@3x/1600.png\"\n             width=\"393.0\"\n             height=\"852.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Browsing the Software Index in OpoLua</p> \n<p>There remain a few things to be done before we can submit to the App Store:</p> \n<ul> \n <li>build and test against the new iOS 26 SDK</li> \n <li>conditionally change the UI to match iOS 26 (thanks Apple 🤦)</li> \n <li>support iOS 26 translucent icons</li> \n <li>adopt the new <a href=\"https://software.psion.community/api/docs/\">Software Index API</a></li> \n <li>ensure only safe for work programs are available in the Software Index browser<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup></li> \n <li>increment the version number</li> \n</ul> \n<p>Since there&rsquo;s really no way to shortcut this kind of work, I simply set about working my way through it: I managed to catch up to the new Software Index API, ensuring this build should work well into the future, and make a few of the iOS 26 UX tweaks. More to come on day 7.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>I&rsquo;m looking at you, <a href=\"https://software.psion.community/programs/uid/0x1000af86/\">Strip Poker</a>.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-12-07T11:26:39-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-07-december-adventure-day-06/",
      "title": "December Adventure Day 06",
      "url": "https://jbmorley.co.uk/posts/2025-12-07-december-adventure-day-06/"
    },
    {
      "content_html": "<p>Having finished day 4 of my <a href=\"/december-adventure\">December Adventure</a> by getting distracted trying to improve the appearance of code on my website just before going to bed, I decided to skip the <a href=\"/posts/2025-12-01-december-adventure-2025-day-01\">random selection</a> and continue with this for day 5&mdash;I regularly write about code and I&rsquo;d like to improve the reading experience a little.</p> \n<h1><a id=\"jetbrains-mono\"></a>JetBrains Mono</h1> \n<p>I&rsquo;ve used <a href=\"https://fonts.google.com/specimen/JetBrains+Mono\">JetBrains Mono</a> as my terminal font ever since reading <a href=\"https://dx13.co.uk/articles/2023/02/17/monospaced/\">Mike&rsquo;s post exploring different monospaced fonts</a>&mdash;it took a little getting used to, but it&rsquo;s got quite a bit of personality, which I appreciate.</p> \n<p>Now JetBrains Mono is available in Google Fonts, I updated my <code>code</code> blocks to use this instead of the default monospaced browser font.</p> \n<h1><a id=\"opl-syntax-highlighting\"></a>OPL Syntax Highlighting</h1> \n<p>Having nerd-sniped myself a few days ago, I decided it was time to look into adding OPL support to <a href=\"https://highlightjs.org\">highlight.js</a>. It&rsquo;s a language I seem to write about a lot, and it would be great to have syntax highlighting when I do.</p> \n<p>Thankfully, highlight.js has pretty good <a href=\"https://highlightjs.readthedocs.io/en/latest/language-guide.html\">documentation</a> explaining how to define and register additional languages, making the process pretty simple. The current definition looks like this:</p> \n<pre><code class=\"language-javascript\">hljs.registerLanguage('opl', function() {\n  return {\n    case_insensitive: true,\n    keywords: {\n      $pattern: /[a-z$&amp;%:]+/,\n      keyword: 'abs acos acs addr adjustalloc alert alias alloc alog and app append appendsprite asc asin asn at atan atn back beep begintrans bookmark break busy byref cache cachehdr cacherec cachetidy call cancel changesprite chr$ clearflags clock close closesprite cls cmd$ committrans compact compress const continue copy copyw cos cosh count create createsprite cursor datetosecs datim$ day dayname$ days daystodate dbuttons dcheckbox dchoice ddate declare declare external dedit deditmulti defaultwin deg delete deletew dfile dfloat dialog diaminit diampos dinit dinits dir$ dirw$ disp dlong do dow dposition drawsprite dtext dtime dxinput edit else elseif end enda endif endp endv endwh eng entersend entersend0 eof erase err err$ errx$ escape eval exist exp external fac find findfield findlib findw first fix fix$ flt font free freealloc gat gborder gbox gbutton gcircle gclock gclose gcls gcolor gcolorbackground gcolorinfo gcopy gcreate gcreatebit gdrawobject gellipse gen$ get get$ getcmd$ getdoc$ getevent getevent32 geteventa32 geteventc getkey getlibh gfill gfont ggmode ggrey gheight gidentity ginfo ginfo32 ginvert giprint glineby glineto gloadbit gloadfont global gmove gorder goriginx goriginy goto gotomark gpatt gpeekline gpixel gpoly gprint gprintb gprintclip grank gsavebit gscroll gsetpenwidth gsetwin gstyle gtmode gtwidth gunloadfont gupdate guse gvisible gwidth gx gxborder gxborder32 gxprint gy hex$ hour iabs if in include input insert int intf intrans ioa ioc iocancel ioclose ioopen ioread ioseek iosignal iow iowait iowaitstat iowaitstat32 iowrite ioyield key key$ keya keyc killmark kmod kstat last lclose left$ len lenalloc linklib ln loadlib loadm loc local lock log lopen lower$ lprint m0 m1 m2 m3 m4 m5 m6 m7 m8 m9 max mcard mcardx mcasc mean menu menun mid$ min minit minute mkdir mod modify month month$ mpopup newobj newobjh next not num$ odbinfo off onerr open openr opx or os out p1 p2 p3 p4 p5 parse$ pause peek$ peekb peekf peekl peekw pi pointerfilter poke$ pokeb pokef pokel pokew pos position possprite print proc put rad raise rand randomize realloc recall recsize rename rept$ return right$ rmdir rnd rollback round sci$ screen screeninfo second secstodate send setdoc setflags sethelp sethelpuid setname setpath sgn showhelp sin sinh size space sqr sqrt statuswin statwininfo std stdev stop store style sum tan tanh testevent trap uadd udg unloadlib unloadm until update upper$ use usesprite usr usr$ usub val var vector view week while year'\n    },\n    contains: [\n      {\n        className: 'string',\n        begin: '\"',\n        end: '\"'\n      },\n      hljs.COMMENT(\n        'REM',\n        '\\n',\n        {\n          contains: [{\n            className: 'doc',\n            begin: '@\\\\w+'\n          }]\n        }\n      )\n    ]\n  }\n});\n</code></pre> \n<p>There&rsquo;s a couple of things to note with this implementation:</p> \n<ul> \n <li><p><code>$pattern</code></p> <p>By default highlight.js uses <code>\\w+</code> to detect and split keyword tokens. This doesn&rsquo;t work for OPL as operations and functions can be optionally post-fixed with <code>$</code>, <code>&amp;</code>, or <code>$</code>, to indicate the return type, necessitating a different regex pattern. To make things a little more complex, OPL also permits user-defined procedures with names that collide with the built-in operations and functions, relying on a trailing colon to differentiate them&mdash;the pattern therefore also includes <code>:</code> to ensure keywords don&rsquo;t over-match on user procedures.</p></li> \n <li><p><code>keyword</code></p> <p>Huge thanks to <a href=\"https://github.com/kapfab\">Fabrice</a> for collating all the OPL operations and functions which made it incredibly painless to put together the list of keywords, saving me quite a bit of time.</p></li> \n</ul> \n<p>With this new language definition in place, OPL looks just that a little bit prettier:</p> \n<pre><code class=\"language-opl\">PROC hello:\n  PRINT \"Hello, World!\"\n  GET\nENDP\n</code></pre>",
      "date_published": "2025-12-06T12:46:52-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-06-december-adventure-day-05/",
      "title": "December Adventure Day 05",
      "url": "https://jbmorley.co.uk/posts/2025-12-06-december-adventure-day-05/"
    },
    {
      "content_html": "<p>For day 4 of my <a href=\"/december-adventure\">December Adventure</a>, I again consulted the Organiser II which, in a clear cry for help, demanded that I revisit <a href=\"/posts/2025-12-03-december-adventure-day-02\">day 2</a> and, &lsquo;learn about the Organiser Comm Link&rsquo;.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-06-december-adventure-day-04/prompt/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-06-december-adventure-day-04/prompt/400.jpeg 400 300,/posts/2025-12-06-december-adventure-day-04/prompt/800.jpeg 800 600,/posts/2025-12-06-december-adventure-day-04/prompt/1200.jpeg 1200 900,/posts/2025-12-06-december-adventure-day-04/prompt/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>Tasked with this for a second day, I set myself a goal of transferring files between my LZ64 and my Mac&mdash;there&rsquo;s a nice selection of software out there that I&rsquo;d love to try out, and I&rsquo;d like to be able to develop programs on my Mac instead of fighting the A-Z keyboard all the time.</p> \n<h1><a id=\"comms-link\"></a>Comms Link</h1> \n<p>Jaap Scherphuis' fantastic <a href=\"https://www.jaapsch.net/psion/index.htm\">Psion II Page</a> hosts a copy of the <a href=\"https://www.jaapsch.net/psion/mancomms2.htm\">Comms Link manual</a> which details how to configure later models of the Organiser II Comms Link to use the new &lsquo;PSION&rsquo; protocol<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup> to talk to a DOS server to send and receive files. While I&rsquo;ve exclusively used modern software for talking to my Psions, many members of the community use one variant of <a href=\"https://en.wikipedia.org/wiki/DOSBox\">DOSBox</a> or another to run the original PC software, and it seemed like a good time to try it out.</p> \n<p>It turns out that setup is incredibly easy&mdash;it&rsquo;s a matter of a single configuration option to forward a host serial port to DOSBox, after which your DOS software should just see the comm port. In <a href=\"https://dosbox-x.com/\">DOSBox-X</a><sup id=\"fnref2\"><a href=\"#fn2\" rel=\"footnote\">2</a></sup> on my Mac, I needed to edit <code>~/Library/Preferences/DOSBox-X 0.83.15 Preferences</code> to ensure the <code>serial</code> section contained the following:</p> \n<pre><code class=\"language-ini\">[serial]\n\nserial1 = directserial realport:cu.usbserial-A91MGK6M\n</code></pre> \n<p></p>\n<aside>\n  The identifier following \n <code>realport:</code> is the basename of your serial port device. My FTDI adapter appears as \n <code>/dev/cu.usbserial-A91MGK6M</code>, so I use \n <code>cu.usbserial-A91MGK6M</code>. \n</aside>\n<p></p> \n<p>With my USB-C RS232 adapter connected and the configuration in place, running the PC server was just a matter of launching <code>CL.EXE</code> from Comms Link 2.11 (see <a href=\"https://www.jaapsch.net/psion/progs2.htm#pc\">Jaap&rsquo;s Software for PC listings</a> for the download).</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n                        >\n        <source srcset=\"/posts/2025-12-06-december-adventure-day-04/cl-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-06-december-adventure-day-04/cl@2x/1600.png\"\n             width=\"800.0\"\n             height=\"519.0\"\n             />\n    </picture>\n</div>\n</p> \n<p>This early implementation of the Psion Link Protocol exposes the PCs file as a share to the Psion, meaning everything else takes place on device using the the <code>Transmit</code> and <code>Receive</code> options in the <code>Comms</code> software; just follow the instructions to specify the local and remote filenames.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-06-december-adventure-day-04/transmit/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-06-december-adventure-day-04/transmit/400.jpeg 400 300,/posts/2025-12-06-december-adventure-day-04/transmit/800.jpeg 800 600,/posts/2025-12-06-december-adventure-day-04/transmit/1200.jpeg 1200 900,/posts/2025-12-06-december-adventure-day-04/transmit/1600.jpeg 1600 1200,\" />  \n </body></p> \n<h1><a id=\"worm2\"></a>Worm2</h1> \n<p>While transferring files is slow with a maximum baud of 9600 (and you can only send one file at once), I was able to try out Jaap&rsquo;s <a href=\"https://www.jaapsch.net/psion/progs.htm#games\">Worm2</a>, an incredibly comprehensive <a href=\"https://en.wikipedia.org/wiki/Snake_(video_game_genre)\">snake</a> clone for a 4-line text-only device. This involves copying the 4 OPL procedures to the Organiser II, updating the grid dimensions to match the LZ64&rsquo;s larger screen, and compiling each, before finally running <code>Worm2</code>&mdash;slightly more involved than a modern app store.</p> \n<p>\n <body>   \n  <div class=\"photo-container\"> \n   <video class=\"photo-video\" poster=\"/posts/2025-12-06-december-adventure-day-04/worm2/thumbnail.jpeg\" controls> \n    <source src=\"/posts/2025-12-06-december-adventure-day-04/worm2/video.mov\" type=\"video/mp4\" /> Your browser does not support the video tag. \n   </video> \n  </div>  \n </body></p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>An early version of the Psion Link Protocol used by EPOC16 and EPOC32 devices.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n  <li id=\"fn2\"> <p>On Alex&rsquo;s advice, I tried with <a href=\"https://www.dosbox-staging.org/\">DOSBox Staging</a> which includes support for changing the configuration from within DOSBox itself. Unfortunately, I hit a segfault early on so, while this looks idea, I had to retreat to <a href=\"https://dosbox-x.com/\">DOSBox-X</a>.&nbsp;<a href=\"#fnref2\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-12-06T12:13:11-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-06-december-adventure-day-04/",
      "title": "December Adventure Day 04",
      "url": "https://jbmorley.co.uk/posts/2025-12-06-december-adventure-day-04/"
    },
    {
      "content_html": "<p>Continuing my <a href=\"/december-adventure\">December Adventure</a>, I started the day by writing up <a href=\"\">yesterday&rsquo;s dive into Psion manuals and the Organiser II Comms Link</a>. I also added a couple of new items to <a href=\"http://localhost:8000/posts/2025-12-01-december-adventure-2025-day-01/\">my LZ6 Ideas program</a> (aka &lsquo;lucky dip&rsquo;) to ensure they&rsquo;re in the running for future adventuring:</p> \n<ul> \n <li>Series 7 emulation</li> \n <li>Write up Nezumi</li> \n</ul> \n<p>With these additional entries in place, I rolled the metaphorical dice for the day&rsquo;s task. This turned out to be &lsquo;Software Index spelunking&rsquo;&mdash;yet another wonderfully chill excursion, and something I&rsquo;d been hoping would come up early in the month. My idea behind this entry was to allow myself an opportunity to simply enjoy exploring some of the software the <a href=\"https://software.psion.community\">Psion Software Index</a>, perhaps even making the exercise a daily feature.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-03-december-adventure-day-03/prompt/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-03-december-adventure-day-03/prompt/400.jpeg 400 300,/posts/2025-12-03-december-adventure-day-03/prompt/800.jpeg 800 600,/posts/2025-12-03-december-adventure-day-03/prompt/1200.jpeg 1200 900,/posts/2025-12-03-december-adventure-day-03/prompt/1600.jpeg 1600 1200,\" />  \n </body></p> \n<h1><a id=\"software-index-spelunking\"></a>Software Index Spelunking</h1> \n<p>Since I had my Series 7 with me for yesterday&rsquo;s write-up, I decided to try an EPOC32 program or two<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup> and, with the Software Index built-into <a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a>, doing so was simply a matter of scrolling until something took my interest and clicking the &lsquo;Install&rsquo; button.</p> \n<p>Focusing on programs with color icons (as an indicator that they might have dedicated Series 7 support), I selected &lsquo;Colors&rsquo; by vorbauer.com, which turned out to be a disappointingly unplayable game of <a href=\"https://en.wikipedia.org/wiki/Simon_(game)\">Simon</a>. While I can&rsquo;t tell if it&rsquo;s a function of my aging Series 7&rsquo;s screen or not, tile flashes were barely visible and far too fast to discern, meaning I was unable to make it past the first sequence.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-03-december-adventure-day-03/colors/400.png\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-03-december-adventure-day-03/colors/400.png 400 300,/posts/2025-12-03-december-adventure-day-03/colors/800.png 640 480,/posts/2025-12-03-december-adventure-day-03/colors/1200.png 640 480,/posts/2025-12-03-december-adventure-day-03/colors/1600.png 640 480,\" />  \n </body></p> \n<p>Despite feeling slightly robbed of an engaging game of Simon, I took some screenshots, wrote a brief description, and added it to the Software Index. Find it <a href=\"https://software.psion.community/programs/id/colors/\">here</a>.</p> \n<h1><a id=\"i'm-feeling-lucky\"></a>I&rsquo;m Feeling Lucky</h1> \n<p>With one program down, I decided to bring forward another task from the list to work on: the Software Index ‘I’m Feeling Lucky’ button. Inspired by Colin Hoad&rsquo;s 2024 &lsquo;<a href=\"https://www.youtube.com/playlist?list=PLDgmuFhqDXWouhWZmVieoQ_ytA183Cb9-\">Advent of Beeb</a>&rsquo;, I&rsquo;ve been meaning to add this early-Google style button<sup id=\"fnref2\"><a href=\"#fn2\" rel=\"footnote\">2</a></sup> for a little while. The idea is that it will encourage me (and others) to dive randomly into the library and, hopefully, discover new and exciting programs.</p> \n<p>Since the Software Index home page already preloads the top-level program listing using JavaScirpt (for the lazy loading and filtering features), adding the button was simply a matter of selecting a random in-memory item from the list and navigating to it:</p> \n<pre><code class=\"language-javascript\">function selectRandomGroup() {\n    const index = Math.floor(Math.random() * filteredGroups.length);\n    const group = filteredGroups[index];\n    window.location.href = \"/programs/\" + group.id;\n}\n\n/* ... */\n\nluckyButton.addEventListener('click', function(event) {\n    selectRandomGroup();\n});\n</code></pre> \n<p>Happily, this also respects the current filter, which means you can limit results to EPOC16 or EPOC32 programs, and hopefully, to specific program categories in the future. You can see the <a href=\"https://github.com/inseven/psion-software-index/commit/af88655c464db56ded68d8989055b80d48ce2d4f\">full change</a> on GitHub if you&rsquo;re curious, and I encourage you to try it out (and maybe even <a href=\"https://software.psion.community/contributing/\">contribute some screenshots and metadata</a>).</p> \n<h1><a id=\"quality-of-life-improvements\"></a>Quality of Life Improvements</h1> \n<p>Energized by the surprising ease of adding the &lsquo;I&rsquo;m Feeling Lucky&rsquo; button (and forgetting my plans to stick to just one thing a day), I decided to add one small quality of life improvement to the index: program versions are now sorted in reverse order, meaning the most recent version will always appear at the top of the page (see <a href=\"https://software.psion.community/programs/uid/0x101f438f/\">Zher0es</a> for an example).</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n                        >\n        <source srcset=\"/posts/2025-12-03-december-adventure-day-03/zher0es-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-03-december-adventure-day-03/zher0es@2x/1600.png\"\n             width=\"800.0\"\n             height=\"769.0\"\n             />\n    </picture>\n</div>\n</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>Alas I only managed one.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n  <li id=\"fn2\"> <p>I have come to think of it as the &lsquo;Colin&rsquo; button.&nbsp;<a href=\"#fnref2\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-12-03T20:08:39-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-03-december-adventure-day-03/",
      "title": "December Adventure Day 03",
      "url": "https://jbmorley.co.uk/posts/2025-12-03-december-adventure-day-03/"
    },
    {
      "content_html": "<p>After the <a href=\"/posts/2025-12-01-december-adventure-2025-day-01/\">Organiser II escapades of day 1</a>, I&rsquo;d been hoping for a slower day, and the LZ64 lucky dip delivered, with the first two entries being, &lsquo;Learn about the Organiser Comm Link&rsquo;, and &lsquo;Add manuals to psion.community&rsquo;. Exactly the kind of chill tasks I was looking for.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-03-december-adventure-day-02/prompt/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-03-december-adventure-day-02/prompt/400.jpeg 400 300,/posts/2025-12-03-december-adventure-day-02/prompt/800.jpeg 800 600,/posts/2025-12-03-december-adventure-day-02/prompt/1200.jpeg 1200 900,/posts/2025-12-03-december-adventure-day-02/prompt/1600.jpeg 1600 1200,\" />  \n </body></p> \n<h1><a id=\"manuals\"></a>Manuals</h1> \n<p>Since the Comms Link is a chunky device (especially when paired with the necessary cables and adapters to plug it into a modern computer), I didn&rsquo;t have it with me over my morning coffee. I therefore opted to start the day by focusing on adding manuals to the <a href=\"https://psion.community\">Psion Community website</a>.</p> \n<p>The goal is to make it easier for folks to find documentation on all eras of Psion computers, akin to <a href=\"https://www.jaapsch.net/psion/index.htm\">Jaap&rsquo;s Organiser II pages</a>. It&rsquo;s mostly an exercise in cataloguing what we already have, uploading things to <a href=\"https://archive.org\">archive.org</a> if they don&rsquo;t exist, and adding links to the website<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>. <a href=\"https://oldbytes.space/@thelastpsion\">Alex</a> very kindly shared his collection with me and, combining it with mine, I started working my way through.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n                        >\n        <source srcset=\"/posts/2025-12-03-december-adventure-day-02/website-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-12-03-december-adventure-day-02/website@2x/1600.png\"\n             width=\"800.0\"\n             height=\"685.0\"\n             />\n    </picture>\n</div>\n</p> \n<p>Progress was a little slow as I had to make a couple of tweaks to the website itself, but I was able to add links to about 11 manuals across 7 Psion models, uploading 3 that didn&rsquo;t appear to exist on archive.org. Now that I&rsquo;ve established a pattern, it should be much easier to add more entries over time&mdash;I&rsquo;ve very intentionally opted for a simple list layout in the hope that this will make it more welcoming and encourage others to propose updates to the page.</p> \n<h1><a id=\"comms-link\"></a>Comms Link</h1> \n<p>While the day proved unexpectedly busy (I found myself judging a local latte art competition), I still managed to find a few moments to poke at the Comms Link.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-03-december-adventure-day-02/comms/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-03-december-adventure-day-02/comms/400.jpeg 400 300,/posts/2025-12-03-december-adventure-day-02/comms/800.jpeg 800 600,/posts/2025-12-03-december-adventure-day-02/comms/1200.jpeg 1200 900,/posts/2025-12-03-december-adventure-day-02/comms/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>Just like the 3 Link for the Series 3 and Series 3a, the Comms Link has a built-in ROM which includes all the necessary software for the Organiser. The software uses all 4 lines of the display, telling me that I was lucky enough to pick up a later model with explicit support for the LZ64. While I didn&rsquo;t get a chance to go further, those menu entries suggest there&rsquo;s support for manually sending and receiving individual files, and I understand these later versions also have nascent support for the <a href=\"https://www.jaapsch.net/psion/protocol.htm\">Psion Link Protocol</a>. Something for day 3.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>We may choose to re-host these in the future, but that&rsquo;s a job for another day.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-12-03T11:20:59-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-03-december-adventure-day-02/",
      "title": "December Adventure Day 02",
      "url": "https://jbmorley.co.uk/posts/2025-12-03-december-adventure-day-02/"
    },
    {
      "content_html": "<p>Compiling the ideas for this year&rsquo;s <a href=\"/december-adventure\">December Adventure</a>, I found myself coming up with a seemingly infinite list of possibilities<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>. I also noticed that many of them were relatively small and self-contained&mdash;tasks that push forward my projects in meaningful ways, but not ones that will take the full month. With that in mind, I&rsquo;ve decided to do something a little more like an advent calendar this year, and select a random idea from a list each day.</p> \n<p>Of course, it wouldn&rsquo;t be a Psion-themed December adventure without involving at least one of these wonderful portable computers. With that in mind, I&rsquo;ve decided to start getting to grips with my new <a href=\"https://en.wikipedia.org/wiki/Psion_Organiser#Organiser_II\">Organiser II LZ64</a> by writing a small <a href=\"https://en.wikipedia.org/wiki/Open_Programming_Language\">OPL</a> program that will generate random selections for me.</p> \n<p>The LZ64 came with a small but incredibly comprehensive manual<sup id=\"fnref2\"><a href=\"#fn2\" rel=\"footnote\">2</a></sup> which, combined with my past experience of OPL, was enough to get me started. Given there&rsquo;s language-level support for &lsquo;databases&rsquo; in OPL, it makes sense to store my list of ideas in a data file and then randomly select and display a row. It&rsquo;s a very long time since I&rsquo;ve worked with data files, so I enjoyed diving into the programming manual over a cup of coffee<sup id=\"fnref3\"><a href=\"#fn3\" rel=\"footnote\">3</a></sup>.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-12-01-december-adventure-2025-day-01/creating-a-data-file/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-12-01-december-adventure-2025-day-01/creating-a-data-file/400.jpeg 400 300,/posts/2025-12-01-december-adventure-2025-day-01/creating-a-data-file/800.jpeg 800 600,/posts/2025-12-01-december-adventure-2025-day-01/creating-a-data-file/1200.jpeg 1200 900,/posts/2025-12-01-december-adventure-2025-day-01/creating-a-data-file/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>To avoid needing to first write a top-level menu and editor, I started by creating a new data file each time, programmatically populating it with some sample data, and then exercising the code for random selection. Typing things on the LZ64&rsquo;s quirky A-Z keyboard (which also lacks a traditional shift key, forcing you to always use caps lock) was slow going, but I was able to get something going.</p> \n<p>\n <body>   \n  <div class=\"photo-container\"> \n   <video class=\"photo-video\" poster=\"/posts/2025-12-01-december-adventure-2025-day-01/proof-of-concept/thumbnail.jpeg\" controls> \n    <source src=\"/posts/2025-12-01-december-adventure-2025-day-01/proof-of-concept/video.mov\" type=\"video/mp4\" /> Your browser does not support the video tag. \n   </video> \n  </div>  \n </body></p> \n<p>For the curious, the code (transcribed) looks something like this<sup id=\"fnref4\"><a href=\"#fn4\" rel=\"footnote\">4</a></sup>:</p> \n<pre><code class=\"language-opl\">RANDOM:\n\nLOCAL c,p%,k%\n\nCREATE \"C:IDEAS\",A,t$\n\nA.t$=\"Add Im feeling lucky to the Software Index\"\nAPPEND\n\nA.t$=\"Software Index UID table\"\nAPPEND\n\nA.t$=\"Psion webring\"\nAPPEND\n\nc=COUNT\n\nDO\n  p%=INT(RND*c)\n  POSITION p%+1\n\n  PRINT A.t$  \n  PRINT \"TRY AGAIN (Y/n)\"\n  k%=GET\n\n  CLS\n\nUNTIL k%=%N OR k%=%n OR k%=1\n\nPRINT \"Quitting...\"\n\nCLOSE\nDELETE \"C:IDEAS\"\n</code></pre> \n<p class=\"caption\">The LZ64 helpfully tells me this takes a whopping 604 bytes. Everything counts when all you&rsquo;ve got is 64k total memory.</p> \n<p>With a proof-of-concept in place, I set about adding support for paging through, and managing entries. Refactoring the code proved significantly more tiresome than I&rsquo;d expected as the LZ64 doesn&rsquo;t appear to have any kind of copy-and-paste support, and this generation of OPL requires each procedure to be in a separate file. Not having any desire to manage files using the Organiser&rsquo;s somewhat cumbersome menus, I opted for the lesser of two evils: a combination of labels and <code>GOTO</code> statements 🤷. Happily, it <em>does</em> provide conveniences like the <code>MENU</code> function, so I was able to put something together relatively easily:</p> \n<pre><code class=\"language-opl\">RANDOM:\n\nLOCAL c,p%,k%,m%\n\nGOTO init::\n\nmenu::\nWHILE 1\n  k%=GET\n  IF k%=1\n    GOTO quit::\n  ELSEIF k%=2\n    m%=MENU(\"Random,New,Delete,Quit\")\n    IF m%=1\n      GOTO rand::\n    ELSEIF m%=2\n      GOTO add::\n    ELSEIF m%=3\n      GOTO delete::\n    ELSEIF m%=4\n      GOTO quit::\n    ENDIF\n  ELSEIF k%=5\n    GOTO prev::\n  ELSEIF k%=6\n    GOTO next::\n  ELSEIF k%=13\n    GOTO rand::\n  ELSEIF k%=32\n    GOTO add::\n  ENDIF\nENDWH\n\ninit::\nIF NOT EXIST(\"C:IDEAS\")\n  CREATE \"C:IDEAS\",A,t$\nELSE\n  OPEN \"C:IDEAS\",A,t$\nENDIF\nGOTO show::\n\nshow::\nCLS\nPRINT POS,\"/\",COUNT\nIF COUNT&lt;1\n  PRINT \"No items\"\nELSE\n  PRINT A.t$\nENDIF\nGOTO menu::\n\nprev::\np%=POS-1\nIF p%&lt;1\n  p%=COUNT\nENDIF\nPOSITION p%\nGOTO show::\n\nnext::\np%=POS+1\nIF p%&gt;COUNT\n  p%=0\nENDIF\nPOSITION p%\nGOTO show::\n\nrand::\nc=COUNT\np%=INT(RND*c)\nPOSITION p%+1\nGOTO show::\n\ndelete::\nERASE\nIF POS&gt;COUNT\n  POSITION COUNT\nENDIF\nGOTO show::\n\nquit::\nPRINT \"Quit...\"\nCLOSE\nSTOP\n\nadd::\nCLS\nPRINT \"New\"\nINPUT A.t$\nAPPEND\nGOTO show::\n</code></pre> \n<p>Clocking in at 1310 bytes, this now gives me the tools I need to (very slowly) enter my list of ideas for the coming days, select a random one each day, and append new ideas as the month progresses. Perhaps unsurprisingly, I&rsquo;m going to call it &lsquo;Ideas&rsquo;.</p> \n<p>That list of ideas currently looks something like this (see <a href=\"/posts/2025-11-30-december-adventure-2025/\">this year&rsquo;s introduction</a> for background to some of these).</p> \n<p>Psion ideas:</p> \n<ul> \n <li>Improve the Ideas UX</li> \n <li>Learn about the Organiser Comms Link</li> \n <li>Keyboard support for the LZ64</li> \n <li>Psion webring</li> \n <li><a href=\"https://software.psion.community\">Software Index</a> UID listing</li> \n <li>Software Index &lsquo;I&rsquo;m Feeling Lucky&rsquo; button</li> \n <li>Software Index CLI for Linux</li> \n <li>Software Index spelunking</li> \n <li><a href=\"/projects/psiboard\">PsiBoard</a> documentation and write-up</li> \n <li>PsiBoard finishing touches</li> \n <li>PsiBoard charging status LED</li> \n <li>Ship <a href=\"https://github.com/jbmorley/thoughts-lite/\">Thoughts for EPOC32</a></li> \n <li>Minimal Psion USB-C cable</li> \n <li>Backups in <a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a></li> \n <li>Organiser connectivity in Reconnect</li> \n <li>Write about <a href=\"https://rmrsoft.com\">RMRSoft</a> preservation</li> \n <li>Ship <a href=\"https://opolua.org\">OpoLua</a> 2.0 for iOS</li> \n <li>Ship OpoLua Qt</li> \n <li>Series 3a hinge reinforcement</li> \n <li>Archive Palmtop magazine scans</li> \n <li>Write about MAME emulation</li> \n <li>Web-based Psion emulation</li> \n <li>OPL support for highlight.js</li> \n <li>Archive more Psion ROMs</li> \n <li>Add guides to <a href=\"https://psion.community\">psion.community</a></li> \n <li>Add manuals to psion.community</li> \n</ul> \n<p>Non-Psion ideas:</p> \n<ul> \n <li>Support attachments in <a href=\"https://thoughts.jbmorley.co.uk\">Thoughts</a></li> \n <li>Thoughts for iOS</li> \n <li>Print feet for <a href=\"/projects/anytime-nixie\">Anytime x Nixie</a></li> \n <li>Try out <a href=\"https://marchintosh.com/globaltalk.html\">GlobalTalk</a></li> \n <li>Write up my read later strategy</li> \n <li>Time zone logger</li> \n <li><a href=\"/projects/little-luggable\">Little Luggable</a> keyboard improvements</li> \n <li>Write up MiSTer x PVM</li> \n <li>Show filenames in <a href=\"https://folders.jbmorley.co.uk\">Folders</a></li> \n <li>Support filtering in Folders</li> \n <li>Remove Libretto 50CT batteries</li> \n <li>Write about <a href=\"https://modelviewer.dev/\"><code>model-viewer</code></a></li> \n <li>Write about my 3D printed brackets</li> \n</ul> \n<p>Since I&rsquo;m selecting things at random, I&rsquo;ve intentionally let this list grow to include many wishlist and quality-of-life improvement items. I plan to let myself re-roll if ideas don&rsquo;t speak to me on a given day, but I&rsquo;ll note down the ones I skipped for folks following along.</p> \n<hr /> \n<p>In spite of it&rsquo;s quirks, the Organiser is already proving a very enjoyable device to use&mdash;it&rsquo;s fascinating to see how many of the UX patterns in SIBO and beyond have their roots in these text-only devices.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>This might very well be a side effect of having spent most of November traveling, instead of working on projects.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n  <li id=\"fn2\"> <p>There&rsquo;s a <a href=\"https://www.jaapsch.net/psion/manlzpg.htm\">digial version</a> on <a href=\"https://www.jaapsch.net/psion/index.htm\">Jaap&rsquo;s Psion II Page</a> but I can&rsquo;t find a scan of the paper version on the Internet Archive. Another project for when I finally acquire a book scanner.&nbsp;<a href=\"#fnref2\" rev=\"footnote\">↩</a></p> </li> \n  <li id=\"fn3\"> <p>It was actually matcha, but &lsquo;coffee&rsquo; seems the de rigueur hot drink for such narratives. 🍵&nbsp;<a href=\"#fnref3\" rev=\"footnote\">↩</a></p> </li> \n  <li id=\"fn4\"> <p>And there&rsquo;s yet another idea to add to the list: OPL syntax highlighting for <a href=\"https://highlightjs.org/\">highlight.js</a> 🙃.&nbsp;<a href=\"#fnref4\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-12-01T23:54:58-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-12-01-december-adventure-2025-day-01/",
      "title": "December Adventure Day 01",
      "url": "https://jbmorley.co.uk/posts/2025-12-01-december-adventure-2025-day-01/"
    },
    {
      "content_html": "<p>While I didn&rsquo;t manage to continue for the full month, I really enjoyed last year&rsquo;s <a href=\"/december-adventure\">December Adventure</a><sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>. It provided me with a clear focus and, far more importantly, gave me permission to spend my time creating, and writing about those creations. With December 2025 fast approaching, I&rsquo;ve decided to indulge myself again.</p> \n<p>Just like last year, I&rsquo;m planning a Psion-themed adventure, with room for a few (probably retro) side-quests as-and-when the mood (or need) takes me. I anticipate things might be a little more piecemeal this year as I&rsquo;ve a number of smaller ideas to try out. I also intend to shift pace a little on the weekends to ensure I don&rsquo;t burn myself out on what&rsquo;s meant to be a fun little excursion.</p> \n<p>Here&rsquo;s a few of my initial ideas of things to explore:</p> \n<ul> \n <li><p><strong>Psion webring</strong></p> <p>I&rsquo;ve really enjoyed seeing webrings reappear in some online communities (e.g., <a href=\"https://webring.xxiivv.com/\">https://webring.xxiivv.com/</a>) and I&rsquo;d love to have something like that in the Psion space.</p></li> \n <li><p><strong>Psion community website improvements</strong></p> <p>There&rsquo;ll always be an endless list of things we could do with the new (and currently fairly minimal) <a href=\"https://psion.community\">Psion Community website</a>, and I&rsquo;d like to take a little time to focus on making it a richer resource for folks discovering the world of Psion for the first time. While the <a href=\"https://software.psion.info\">Software Index</a> attempts to catalogue EPOC16 and EPOC32 programs, there are many secondary resources (desktop software, manuals, etc) that exist on the Internet Archive, but aren&rsquo;t centrally indexed. It&rsquo;d be great to follow the example of Jaap Scherphuis' wonderful <a href=\"https://www.jaapsch.net/psion/index.htm\">Organiser II pages</a> and collect together useful links for <a href=\"https://en.wikipedia.org/wiki/Psion_MC\">MC devices</a> and beyond.</p></li> \n <li><p><strong>Organiser explorations</strong></p> <p>I&rsquo;ve just acquired a <a href=\"https://en.wikipedia.org/wiki/Psion_Organiser#Organiser_II\">Psion Organiser II</a> LZ64, and I&rsquo;m excited to take some time to discover this &lsquo;new&rsquo; machine.</p> \n  <ul> \n   <li><p>I anticipate very quickly wanting to connect it to my Mac so I&rsquo;ll need to see what options are available and, potentially, add support to <a href=\"https://reconnect.jbmorley.co.uk\">Reconnect</a>. Unlike the later Series 3 and Series 5 devices, Organisers aren&rsquo;t supported by <a href=\"https://github.com/plptools/plptools\">plptools</a> which provides the connectivity in Reconnect, so I&rsquo;d need to get my hands dirty with the Psion Link Protocol.</p></li> \n   <li><p>Using <a href=\"https://oldbytes.space/@thelastpsion\">Alex</a>&rsquo;s LZ64 during the recent <a href=\"https://www.computinghistory.org.uk/det/74558/Retro-Computer-Festival-2025-(Gaming-Edition)-Saturday-15th-November/\">Retro Computer Festival</a> at the <a href=\"https://www.computinghistory.org.uk/\">Center for Computing History</a>, I was struck by both how good the screen is, and how impractical the A-Z keyboard is if you&rsquo;re used to QWERTY layouts. With that in mind, I wondered about seeing if I could create a little hardware dongle to let me use a regular USB keyboard (assuming one doesn&rsquo;t already exist<sup id=\"fnref2\"><a href=\"#fn2\" rel=\"footnote\">2</a></sup>) and turn it into some form of minimal writer deck.</p></li> \n  </ul></li> \n <li><p><strong>Psion emulation</strong></p> <p>There&rsquo;s been some amazing progress in the field of Psion emulation in the past few years, with MAME becoming the go-to solution for all things Psion. I&rsquo;d love to figure out how to showcase all the options, either with a simple blog post, or perhaps something a little more invovled&mdash;I dream of using Emscripten to embed the emulators in a dedicated website akin to absolutely wonderful <a href=\"https://aresluna.org/frame-of-preference/\">Frame of preference</a> interctive showcase of Mac settings dialogs.</p></li> \n <li><p><strong>Wrapping up the PsiBoard</strong></p> <p>I spent much of October working on the <a href=\"/projects/psiboard\">PsiBoard</a>, my Psion Bluetooth keyboard, to ensure it was complete for my father&rsquo;s 80th birthday in November. Now that deadline has passed, I&rsquo;d like to take the time to finish writing up the project, address a few firmware and design issues, and make it available so others can make their own. <a href=\"https://pcbway.com\">PCBWay</a> kindly offered to pay for prototype parts going forwards (I guess this makes me an influencer 🤷) which will take the edge off the last few hardware bug fixes and iterations.</p></li> \n <li><p><strong>Shipping OpoLua 2.0</strong></p> <p>Over the past few months, <a href=\"https://github.com/tomsci\">Tomsci</a> has been working hard to create a Qt version of <a href=\"https://opolua.org\">OpoLua</a>, our modern <a href=\"https://en.wikipedia.org/wiki/Open_Programming_Language\">OPL</a> runtime, for Windows, Linux and macOS. This is now all-but ready to go, but we need to revisit the licensing and set up CI for the Qt builds before we&rsquo;re ready to ship it. There&rsquo;s also a whole host of improvements and bug fixes to ship in the iOS app (thanks to Fabrice for keeping us on our toes by trying out seemingly every OPL program ever published), along with an integrated <a href=\"https://software.psion.community\">Software Index</a> which should make it significantly easier for folks to discover the wealth of OPL programs out there.</p></li> \n</ul> \n<p>And a few weekend side-quest ideas:</p> \n<ul> \n <li><p><strong>Psion Series 3a hinge reinforcement</strong></p> <p>I&rsquo;ve a Psion Series 3a in need of JB Weld hinge reinforcement that I&rsquo;d love to document to help other who need to do the same.</p></li> \n <li><p><strong>Toshiba Libretto 50CT battery removal</strong></p> <p>The BIOS backup batteries on Toshiba Librettos are, like many batteries from that era, well known for leaking. I&rsquo;ve a couple of Libretto 50CTs which desperately need their batteries removing. I&rsquo;d love to take the time to do it, and perhaps print some of <a href=\"https://www.youtube.com/watch?v=AdeswJreJ98\">polymatt</a>&rsquo;s replacement parts in the process to stop their cases degrading further. Maybe I could even try a Psion-style JB Weld preventative treatment.</p></li> \n <li><p><strong>Something ZX Spectrum related</strong></p> <p>I picked up my ZX Spectrum<sup id=\"fnref3\"><a href=\"#fn3\" rel=\"footnote\">3</a></sup> when visiting my folks in the UK and I&rsquo;m excited to tinker around with it. While I&rsquo;ve enjoyed playing with the Spectrum core on the MiSTer, it&rsquo;s not a machine I know at all well, and it&rsquo;d be great to find a little project to help me really get to know the hardware and software.</p></li> \n <li><p><strong>Software Index spelunking</strong></p> <p>Thanks to the great archival work of community members, the <a href=\"https://software.psion.community\">Psion Software Index</a> contains a wealth of programs for EPOC16 and EPOC32. I&rsquo;m keen to dig into the library and start documenting and screenshotting the various programs; perhaps something to do over a morning coffee.</p></li> \n</ul> \n<p>Writing this list, I realize there&rsquo;s a huge wealth of possibilities! Let&rsquo;s see what December brings&hellip;</p> \n<hr /> \n<p>If you&rsquo;re looking for other folks to follow in the December, I highly recommend Colin Hoad, who&rsquo;s reprising his <a href=\"https://www.youtube.com/watch?v=RHdgIHr78RM\">Advent of Beeb</a>.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>You can find out a little more about the idea of a &lsquo;December Adventure&rsquo; over at <a href=\"https://eli.li/december-adventure\">Oatmeal</a>. (I&rsquo;m glad I reread this when looking out the link as it reminded me that it&rsquo;s meant to be &lsquo;low key&rsquo;.)&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n  <li id=\"fn2\"> <p>The Organiser devices are entirely new to me and I&rsquo;ve a lot of research to do.&nbsp;<a href=\"#fnref2\" rev=\"footnote\">↩</a></p> </li> \n  <li id=\"fn3\"> <p>Acquired for the princely sum of £5 at a car boot sale in the 90s.&nbsp;<a href=\"#fnref3\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-11-30T12:23:42-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-11-30-december-adventure-2025/",
      "title": "December Adventure 2025",
      "url": "https://jbmorley.co.uk/posts/2025-11-30-december-adventure-2025/"
    },
    {
      "content_html": "<p>While hunting down sources for the <a href=\"https://software.psion.community\">Psion Software Index</a>, I came across a Windows 95 Psion screen saver. Needless to say, I couldn&rsquo;t resist recreating it for modern macOS.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-10-11-psion-screen-saver/preview/400.gif\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-10-11-psion-screen-saver/preview/400.gif 400 300,/posts/2025-10-11-psion-screen-saver/preview/800.gif 800 600,/posts/2025-10-11-psion-screen-saver/preview/1200.gif 1024 768,/posts/2025-10-11-psion-screen-saver/preview/1600.gif 1024 768,\" />  \n </body></p> \n<ul class=\"actions\"> \n <li><a class=\"symbol github\" href=\"https://github.com/inseven/psion-screen-saver\">GitHub</a></li> \n <li><a class=\"symbol zip\" href=\"https://github.com/inseven/psion-screen-saver/releases/latest/\">Latest Release</a></li> \n</ul>",
      "date_published": "2025-10-11T22:16:15-07:00",
      "id": "https://jbmorley.co.uk/posts/2025-10-11-psion-screen-saver/",
      "title": "Psion Screen Saver",
      "url": "https://jbmorley.co.uk/posts/2025-10-11-psion-screen-saver/"
    },
    {
      "content_html": "<p>My take on a <a href=\"/projects/psiboard\">Psion bluetooth keyboard</a> has been in the works since 2018. It&rsquo;s one of my first hardware projects and, if I&rsquo;m honest with myself, was a litte beyond my abilities at the time. There were just too many new things all at once: microcontrollers, 3D printing, and PCB design.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/psiboard-so-far/400.jpeg\" width=\"400\" height=\"265\" x-srcset=\"/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/psiboard-so-far/400.jpeg 400 265,/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/psiboard-so-far/800.jpeg 800 531,/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/psiboard-so-far/1200.jpeg 1200 797,/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/psiboard-so-far/1600.jpeg 1600 1062,\" />  \n </body></p> \n<p class=\"caption\">The PsiBoard, while working, has never felt complete</p> \n<p>Now that I&rsquo;ve a little more experience behind me, I&rsquo;m going to have a go at finishing it&mdash;I receive a constant trickle of emails from folks intereted in building their own, and I&rsquo;d love to be able to offer a complete set of parts and instructions.</p> \n<h1><a id=\"shifting-sands-and-a-new-direction\"></a>Shifting Sands and a New Direction</h1> \n<p>The world of custom keyboards has moved on significantly since 2018, with devices like the <a href=\"https://www.sparkfun.com/pro-micro-5v-16mhz.html\">Pro Micro</a> and it&rsquo;s cousin the <a href=\"https://nicekeyboards.com/nice-nano/\">nice!nano</a> becoming the go-to microcontrolers, supported by a wide range of off-the-shelf customizable firmware.</p> \n<p>Moving forwards, I&rsquo;m going to use <a href=\"https://zmk.dev/\">ZMK</a> and a nice!nano. This is a combination I&rsquo;ve experience with from my work on the <a href=\"/projects/little-luggable\">Little Luggable</a>, and it should allow me to foucs on the electronics and the industrial design, rather than spending my days working around already-solved firmware issues.</p> \n<h1><a id=\"electronics\"></a>Electronics</h1> \n<p>\n <body>   \n  <img src=\"/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/ribbon-cable/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/ribbon-cable/400.jpeg 400 300,/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/ribbon-cable/800.jpeg 800 600,/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/ribbon-cable/1200.jpeg 1200 900,/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/ribbon-cable/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p class=\"caption\">The Psion Series 5/5mx keyboard connects using a 20-pin ribbon cable</p> \n<p>When I last worked on the PsiBoard, my understanding of the keyboard matrix was as follows (ribbon cable pin numbering given in parentheses):</p> \n<table> \n <thead> \n  <tr> \n   <th></th> \n   <th><strong>Col 01 (15)</strong></th> \n   <th><strong>Col 02 (11)</strong></th> \n   <th><strong>Col 03 (10)</strong></th> \n   <th><strong>Col 04 (9)</strong></th> \n   <th><strong>Col 05 (8)</strong></th> \n   <th><strong>Col 06 (7)</strong></th> \n   <th><strong>Col 07 (6)</strong></th> \n   <th><strong>Col 08 (5)</strong></th> \n   <th><strong>Col 09 (4)</strong></th> \n   <th><strong>Col 10 (3)</strong></th> \n   <th><strong>Col 11 (2)</strong></th> \n   <th><strong>Col 12 (1)</strong></th> \n  </tr> \n </thead> \n <tbody> \n  <tr> \n   <td><strong>Row 01 (20)</strong></td> \n   <td></td> \n   <td>Space</td> \n   <td>Up</td> \n   <td>. /</td> \n   <td>Left</td> \n   <td>Right</td> \n   <td>Left Shift</td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td></td> \n  </tr> \n  <tr> \n   <td><strong>Row 02 (19)</strong></td> \n   <td>Z</td> \n   <td>X</td> \n   <td>C</td> \n   <td>V</td> \n   <td>B</td> \n   <td>N</td> \n   <td></td> \n   <td>Right Shift</td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td></td> \n  </tr> \n  <tr> \n   <td><strong>Row 03 (18)</strong></td> \n   <td>H</td> \n   <td>J</td> \n   <td>K</td> \n   <td>M</td> \n   <td>. ?</td> \n   <td>Down</td> \n   <td></td> \n   <td></td> \n   <td>Fn</td> \n   <td></td> \n   <td></td> \n   <td></td> \n  </tr> \n  <tr> \n   <td><strong>Row 04 (17)</strong></td> \n   <td>Tab</td> \n   <td>A</td> \n   <td>S</td> \n   <td>D</td> \n   <td>F</td> \n   <td>G</td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td>Left Control</td> \n  </tr> \n  <tr> \n   <td><strong>Row 05 (16)</strong></td> \n   <td>1</td> \n   <td>2</td> \n   <td>3</td> \n   <td>4</td> \n   <td>5</td> \n   <td>6</td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td></td> \n  </tr> \n  <tr> \n   <td><strong>Row 06 (14)</strong></td> \n   <td>U</td> \n   <td>I</td> \n   <td>O</td> \n   <td>P</td> \n   <td>L</td> \n   <td>Enter</td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td>Menu</td> \n   <td></td> \n   <td></td> \n  </tr> \n  <tr> \n   <td><strong>Row 07 (13)</strong></td> \n   <td>Q</td> \n   <td>W</td> \n   <td>E</td> \n   <td>R</td> \n   <td>T</td> \n   <td>Y</td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td>Esc</td> \n   <td></td> \n  </tr> \n  <tr> \n   <td><strong>Row 08 (12)</strong></td> \n   <td>7</td> \n   <td>8</td> \n   <td>9</td> \n   <td>0</td> \n   <td>Del</td> \n   <td>&lsquo; -</td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td></td> \n   <td></td> \n  </tr> \n </tbody> \n</table> \n<p>This has 12 columns, and 8 rows, requiring 20 I/O pins&mdash;too many for most microcontrollers (the nice!nano has only 18). Previously, to work around this, I tied the modifier keys to a single column, leading to issues identifying multiple modifier keys<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>.</p> \n<p>With the benefit of experience, I&rsquo;ve realized that these modifier keys are intentionally broken-out to allow diodes to be introduced on each of the modifer lines to avoid exactly this problem&mdash;Psion skipped putting diodes on the flexible PCB itself, but made it possible to add them where it really matters.</p> \n<p>Taking this into account, here&rsquo;s my updated understanding of what the Series 5 keyboard matrix should look really look like:</p> \n<table> \n <thead> \n  <tr> \n   <th></th> \n   <th>Col 0 (20)</th> \n   <th>Col 1 (19)</th> \n   <th>Col 2 (18)</th> \n   <th>Col 3 (17)</th> \n   <th>Col 4 (16)</th> \n   <th>Col 5 (14)</th> \n   <th>Col 6 (13)</th> \n   <th>Col 7 (12)</th> \n  </tr> \n </thead> \n <tbody> \n  <tr> \n   <td><strong>Row 0 (15)</strong></td> \n   <td></td> \n   <td>Z</td> \n   <td>H</td> \n   <td>Tab</td> \n   <td>1</td> \n   <td>U</td> \n   <td>Q</td> \n   <td>7</td> \n  </tr> \n  <tr> \n   <td><strong>Row 1 (11)</strong></td> \n   <td>Space</td> \n   <td>X</td> \n   <td>J</td> \n   <td>A</td> \n   <td>2</td> \n   <td>I</td> \n   <td>W</td> \n   <td>8</td> \n  </tr> \n  <tr> \n   <td><strong>Row 2 (10)</strong></td> \n   <td>Up</td> \n   <td>C</td> \n   <td>K</td> \n   <td>S</td> \n   <td>3</td> \n   <td>O</td> \n   <td>E</td> \n   <td>9</td> \n  </tr> \n  <tr> \n   <td><strong>Row 3 (9)</strong></td> \n   <td>, /</td> \n   <td>V</td> \n   <td>M</td> \n   <td>D</td> \n   <td>4</td> \n   <td>P</td> \n   <td>R</td> \n   <td>0</td> \n  </tr> \n  <tr> \n   <td><strong>Row 4 (8)</strong></td> \n   <td>Left</td> \n   <td>B</td> \n   <td>. ?</td> \n   <td>F</td> \n   <td>5</td> \n   <td>L</td> \n   <td>T</td> \n   <td>Del</td> \n  </tr> \n  <tr> \n   <td><strong>Row 5 (7)</strong></td> \n   <td>Right</td> \n   <td>N</td> \n   <td>Down</td> \n   <td>G</td> \n   <td>6</td> \n   <td>Enter</td> \n   <td>Y</td> \n   <td>‘ ~</td> \n  </tr> \n  <tr> \n   <td><strong>Row 6 (6,5,4,3,2,1)</strong></td> \n   <td>Left Shift</td> \n   <td>Right Shift</td> \n   <td>Fn</td> \n   <td>Left Control</td> \n   <td></td> \n   <td>Menu</td> \n   <td>Esc</td> \n   <td></td> \n  </tr> \n </tbody> \n</table> \n<p></p>\n<aside>\n  This table is transposed from my first table to make it a little easier to read. \n</aside>\n<p></p> \n<p>Our new row 6 is created by tying pins 1-6 from the ribbon cable to a single pin, by means of a diode on each wire. This prevents loops in the keyboard matrix by ensuring current can only flow in a single direction through the circuit. Happily, this also only requires 15 I/O pins, leaving 3 spare pins on the nice!nano.</p> \n<p>\n <body>   \n  <img src=\"/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/breadboard/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/breadboard/400.jpeg 400 300,/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/breadboard/800.jpeg 800 600,/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/breadboard/1200.jpeg 1200 900,/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/breadboard/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p class=\"caption\">Using a breadboard to test the new modifier row and diodes</p> \n<p>Returning to a breadboard for prototyping, I was able to confirm this all works correctly and give myself a testbed for my customized ZMK firmware.</p> \n<h1><a id=\"next-steps\"></a>Next Steps</h1> \n<p>With the wiring in place, the next step is to convince myself I can get all the functionality I want with the ZMK firmware, especially around power management.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>Pressing more than one modifier key at once would lead to loops in the circuit; an issue known as <a href=\"https://en.wikipedia.org/wiki/Key_rollover\">key rollover</a>.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-10-03T13:07:04-07:00",
      "id": "https://jbmorley.co.uk/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/",
      "title": "Revisiting the Psion Bluetooth Keyboard",
      "url": "https://jbmorley.co.uk/posts/2025-10-03-revisiting-the-psion-bluetooth-keyboard/"
    },
    {
      "content_html": "<img src='/photos/patterns/IMG_8263/1600.jpeg' width='1600' height='1200'></img>",
      "date_published": "2025-09-13T11:44:48-07:00",
      "id": "https://jbmorley.co.uk/photos/patterns/IMG_8263/",
      "url": "https://jbmorley.co.uk/photos/patterns/IMG_8263/"
    },
    {
      "content_html": "<img src='/photos/2025/08/tokyo-and-hakodate/IMG_8214/1600.jpeg' width='1600' height='2133'></img><p>I'd like to think this was intentional.</p>",
      "date_published": "2025-09-09T21:55:58-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_8214/",
      "title": "Shadows",
      "url": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_8214/"
    },
    {
      "content_html": "<video style='width: 100%;' poster='/photos/2025/08/tokyo-and-hakodate/IMG_8219/thumbnail.jpeg' controls><source src='/photos/2025/08/tokyo-and-hakodate/IMG_8219/video.mov' type='video/mp4' />Your browser does not support the video tag.</video>",
      "date_published": "2025-09-09T19:10:15-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_8219/",
      "title": "Rain",
      "url": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_8219/"
    },
    {
      "content_html": "<img src='/photos/2025/08/tokyo-and-hakodate/IMG_8088/1600.jpeg' width='1600' height='1200'></img><p>After spending a large portion of the last decade in the US, it's incredibly refreshing to see so many bicycles in Tokyo.</p>",
      "date_published": "2025-09-08T13:29:05-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_8088/",
      "title": "Wheels",
      "url": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_8088/"
    },
    {
      "content_html": "<img src='/photos/2025/08/tokyo-and-hakodate/IMG_8039/1600.jpeg' width='1600' height='1200'></img>",
      "date_published": "2025-09-07T10:15:03-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_8039/",
      "title": "POST",
      "url": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_8039/"
    },
    {
      "content_html": "<img src='/photos/2025/08/tokyo-and-hakodate/IMG_8031/1600.jpeg' width='1600' height='1200'></img><p>Who can resist a good manhole cover?!</p>",
      "date_published": "2025-09-06T13:57:19-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_8031/",
      "title": "Squid",
      "url": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_8031/"
    },
    {
      "content_html": "<img src='/photos/2025/08/tokyo-and-hakodate/IMG_7857/1600.jpeg' width='1600' height='1200'></img>",
      "date_published": "2025-09-03T14:06:32-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_7857/",
      "title": "Denny's",
      "url": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_7857/"
    },
    {
      "content_html": "<img src='/photos/2025/08/tokyo-and-hakodate/IMG_7753/1600.jpeg' width='1600' height='2133'></img><p>The e-boxes in Akihabara are a veritable museum of consumer electronics, making it hard to resist taking photos of most everything there. This [fairly expensive] Sony Walkman was, with its glorious button design and layout, completely irresitable.</p>",
      "date_published": "2025-08-31T14:04:38-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_7753/",
      "title": "Walkman",
      "url": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_7753/"
    },
    {
      "content_html": "<img src='/photos/2025/08/tokyo-and-hakodate/IMG_7667/1600.jpeg' width='1600' height='1200'></img><p>The Yamanashi Press and Broadcast Center remains one of my favourite buildings in Tokyo, and one of very few Metabolist buildings still standing. I felt incredibly lucky to discover a new vantage point to sit and enjoy it.</p>",
      "date_published": "2025-08-28T14:32:19-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_7667/",
      "title": "Metabolism",
      "url": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_7667/"
    },
    {
      "content_html": "<img src='/photos/2025/08/tokyo-and-hakodate/IMG_7646/1600.jpeg' width='1600' height='1200'></img>",
      "date_published": "2025-08-28T12:01:46-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_7646/",
      "title": "Tamagotchi",
      "url": "https://jbmorley.co.uk/photos/2025/08/tokyo-and-hakodate/IMG_7646/"
    },
    {
      "content_html": "<img src='/photos/patterns/IMG_7628/1600.jpeg' width='1600' height='1200'></img>",
      "date_published": "2025-08-25T15:09:19-07:00",
      "id": "https://jbmorley.co.uk/photos/patterns/IMG_7628/",
      "url": "https://jbmorley.co.uk/photos/patterns/IMG_7628/"
    },
    {
      "content_html": "<img src='/photos/patterns/IMG_7627/1600.jpeg' width='1600' height='2133'></img>",
      "date_published": "2025-08-25T15:09:11-07:00",
      "id": "https://jbmorley.co.uk/photos/patterns/IMG_7627/",
      "url": "https://jbmorley.co.uk/photos/patterns/IMG_7627/"
    },
    {
      "content_html": "<img src='/photos/patterns/IMG_7626/1600.jpeg' width='1600' height='2133'></img>",
      "date_published": "2025-08-25T15:08:55-07:00",
      "id": "https://jbmorley.co.uk/photos/patterns/IMG_7626/",
      "url": "https://jbmorley.co.uk/photos/patterns/IMG_7626/"
    },
    {
      "content_html": "<img src='/photos/patterns/IMG_7625/1600.jpeg' width='1600' height='2133'></img>",
      "date_published": "2025-08-25T15:08:44-07:00",
      "id": "https://jbmorley.co.uk/photos/patterns/IMG_7625/",
      "url": "https://jbmorley.co.uk/photos/patterns/IMG_7625/"
    },
    {
      "content_html": "<img src='/photos/2025/04/japan/IMG_6562/1600.jpeg' width='1600' height='1200'></img><p>Patterns are everywhere in Japan, including this beautiful etching hidden on the underside of a luggage shelf on the Hibya line trains.</p>",
      "date_published": "2025-05-04T15:19:41-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/04/japan/IMG_6562/",
      "title": "Pervasive Patterns",
      "url": "https://jbmorley.co.uk/photos/2025/04/japan/IMG_6562/"
    },
    {
      "content_html": "<img src='/photos/2025/04/japan/R0005352/1600.jpeg' width='1600' height='1066'></img><p>Tako-yaki is apparently a big deal in Osaka.</p>",
      "date_published": "2025-04-30T12:27:20-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/04/japan/R0005352/",
      "title": "🐙",
      "url": "https://jbmorley.co.uk/photos/2025/04/japan/R0005352/"
    },
    {
      "content_html": "<img src='/photos/2025/04/japan/R0005349/1600.jpeg' width='1600' height='1066'></img>",
      "date_published": "2025-04-30T12:24:38-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/04/japan/R0005349/",
      "title": "Waterfront",
      "url": "https://jbmorley.co.uk/photos/2025/04/japan/R0005349/"
    },
    {
      "content_html": "<img src='/photos/2025/04/japan/R0005347/1600.jpeg' width='1600' height='2400'></img><p>You can see the Don Quijote in Shinsaibashi for miles around thanks to the wonderfully 80s-feeling ride integrated into the building. Sadly it wasn't running when I was there, but it's still a striking addition to the waterfront.</p>",
      "date_published": "2025-04-30T12:24:18-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/04/japan/R0005347/",
      "title": "ドンキ",
      "url": "https://jbmorley.co.uk/photos/2025/04/japan/R0005347/"
    },
    {
      "content_html": "<img src='/photos/2025/04/japan/R0005327/1600.jpeg' width='1600' height='1066'></img><p>I couldn't find much of an explanation, but this installation—taking up the whole front-half of Brazil's pavilion—seemed to be about the cycle of rebith and destruction, with these inflatable sculptures shrinking and growing over the course of the piece.</p>",
      "date_published": "2025-04-29T19:09:44-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/04/japan/R0005327/",
      "title": "Brazil",
      "url": "https://jbmorley.co.uk/photos/2025/04/japan/R0005327/"
    },
    {
      "content_html": "<img src='/photos/2025/04/japan/R0005256/1600.jpeg' width='1600' height='1066'></img><p>Needless to say, Expo 2025 has its own manhole covers.</p>",
      "date_published": "2025-04-29T14:17:59-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/04/japan/R0005256/",
      "title": "Manhole Cover",
      "url": "https://jbmorley.co.uk/photos/2025/04/japan/R0005256/"
    },
    {
      "content_html": "<img src='/photos/2025/04/japan/R0005249/1600.jpeg' width='1600' height='1066'></img><p>It was great to see Tezuka's Astro Boy getting some love at the expo.</p>",
      "date_published": "2025-04-29T14:14:41-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/04/japan/R0005249/",
      "title": "Pasona Natureverse",
      "url": "https://jbmorley.co.uk/photos/2025/04/japan/R0005249/"
    },
    {
      "content_html": "<img src='/photos/2025/04/japan/R0005232/1600.jpeg' width='1600' height='1066'></img><p>Weird as they were, the different artistic interpretations of Myaku-Myaku around the expo were so much more engaging than the branded items on sale.</p>",
      "date_published": "2025-04-29T13:02:51-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/04/japan/R0005232/",
      "title": "More Myaku",
      "url": "https://jbmorley.co.uk/photos/2025/04/japan/R0005232/"
    },
    {
      "content_html": "<img src='/photos/2025/04/japan/R0005213/1600.jpeg' width='1600' height='1066'></img><p>Ishiguro Hiroshi's robitic visions of the future were somewhat unnerving—these robots were just lifelike enough to reach the uncanny valley and, coupled with the sound design of the exhibit proved disconcerting.</p>",
      "date_published": "2025-04-29T12:24:52-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/04/japan/R0005213/",
      "title": "Future of Life?",
      "url": "https://jbmorley.co.uk/photos/2025/04/japan/R0005213/"
    },
    {
      "content_html": "<img src='/photos/2025/04/japan/R0005162/1600.jpeg' width='1600' height='1066'></img><p>Myaku Myaku—Expo 2025's bizarre mascot—could be found everywhere and in every creepy form you could imagine.</p>",
      "date_published": "2025-04-29T10:21:42-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/04/japan/R0005162/",
      "title": "Myaku-Neko",
      "url": "https://jbmorley.co.uk/photos/2025/04/japan/R0005162/"
    },
    {
      "content_html": "<img src='/photos/2025/04/japan/R0005134/1600.jpeg' width='1600' height='1066'></img>",
      "date_published": "2025-04-25T09:49:31-07:00",
      "id": "https://jbmorley.co.uk/photos/2025/04/japan/R0005134/",
      "url": "https://jbmorley.co.uk/photos/2025/04/japan/R0005134/"
    },
    {
      "content_html": "<p>For the last 4 years, I&rsquo;ve been working on Bookmarks, a Pinboard client for iOS, macOS, and iPadOS. Bookmarks was my attempt at bringing a Pinboard-like visual experience to private bookmarking. It&rsquo;s been in public TestFlight beta for the last couple of years and has picked up a fair number of users. After much consideration, I&rsquo;ve decided to stop working on it.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2025-04-02-retiring-bookmarks/screenshot-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-04-02-retiring-bookmarks/screenshot@2x/1600.png\"\n             width=\"800.0\"\n             height=\"528.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Bookmarks focuses on offering a more visual experience</p> \n<p>Before getting into the details, I&rsquo;d like to say thank you to everyone who&rsquo;s been helping me test Bookmarks, and has provided feedback and suggestions during development. It makes a huge difference to know that what I&rsquo;m building helps others and is very much why I started writing software in the first place. Thank you!</p> \n<p>While I&rsquo;m really proud of what I&rsquo;ve created with Bookmarks (it&rsquo;s an app I&rsquo;ve used almost daily for the last few years), I want to invest my time elsewhere. Maintaining indie software represents a huge amount of work and, of all the <a href=\"/software/\">software</a> I write and maintain, Bookmarks deviates most from the product I&rsquo;d design if I started again today<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>, and is the one with the most alternatives in the market<sup id=\"fnref2\"><a href=\"#fn2\" rel=\"footnote\">2</a></sup>.</p> \n<p>Bookmarks is open source (under the <a href=\"https://github.com/inseven/bookmarks/blob/main/LICENSE\">MIT license</a>), so if you&rsquo;re interested in taking over the project, I encourage you to do so and am happy to help you get up to speed.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>I dream of a local-first peer-to-peer solution with a focus on archiving and full-text search.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n  <li id=\"fn2\"> <p>I&rsquo;ve been enjoying using <a href=\"https://raindrop.io\">Raindrop</a> for the last couple of months. It matches almost exactly what I was trying to build with Bookmarks and has clients for most every platform out there.&nbsp;<a href=\"#fnref2\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-04-02T13:38:26-07:00",
      "id": "https://jbmorley.co.uk/posts/2025-04-02-retiring-bookmarks/",
      "title": "Retiring Bookmarks",
      "url": "https://jbmorley.co.uk/posts/2025-04-02-retiring-bookmarks/"
    },
    {
      "content_html": "<img src='/photos/patterns/IMG_6012/1600.jpeg' width='1600' height='1200'></img>",
      "date_published": "2025-03-29T12:30:19-07:00",
      "id": "https://jbmorley.co.uk/photos/patterns/IMG_6012/",
      "url": "https://jbmorley.co.uk/photos/patterns/IMG_6012/"
    },
    {
      "content_html": "<p>Over the past couple of months, I&rsquo;ve been working on-and-off on <a href=\"https://github.com/inseven/reporter\">Reporter</a>, a new command line utility to make it easier to keep track of changes to your files.</p> \n<p>Reporter generates email reports letting you know about changes over time. It&rsquo;s intended to fill in one of the missing pieces from services like Dropbox for folks who choose to self-host their files using network shares, or tools like <a href=\"https://syncthing.net/\">Syncthing</a><sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>, providing both a quick sanity check that file changes match expectations and an easy way to discover edits in files shared with others.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2025-03-03-tracking-file-changes/report-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2025-03-03-tracking-file-changes/report@2x/1600.png\"\n             width=\"800.0\"\n             height=\"770.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Reporter generates clean email reports showing file changes</p> \n<p>Reporter works by scanning your files and generating a snapshot containing paths, modification times, sizes, and shasums. It then compares this snapshot to the previous one and sends an email summary if there have been any changes. It includes a built in SMTP client (using <a href=\"https://github.com/Kitura/Swift-SMTP\">Swift-SMTP</a> from the Kitura project) to avoid the need to rely on platform-provided mailers and, while it&rsquo;s written in <a href=\"https://www.swift.org/\">Swift</a>, compiles and runs happily on Linux (I run it primarily on my Raspberry Pi based home Syncthing mirror).</p> \n<p>There&rsquo;s not much more to say. Reporter is a simple tool with a single purpose. I find it incredibly useful and maybe you will too. Right now, you&rsquo;ll need to build it from source (and there are some <a href=\"https://github.com/inseven/reporter/issues\">known issues</a>), but if there&rsquo;s interest, I&rsquo;ll look into packaging for various platforms. You can find it on <a href=\"https://github.com/inseven/reporter\">GitHub</a>. I&rsquo;d love to hear how you get on.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>Which, although a little nuanced to set up, is an absolutely wonderful tool I can&rsquo;t recommend enough.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2025-03-03T10:59:46-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-03-03-tracking-file-changes/",
      "title": "Tracking File Changes",
      "url": "https://jbmorley.co.uk/posts/2025-03-03-tracking-file-changes/"
    },
    {
      "content_html": "<p>Splitting my time between Linux and macOS, and syncing files between the two systems, I often find myself trying to view and edit macOS-specific files on Linux. One particular frustration comes in the form of <code>.webloc</code> files&mdash;Apple&rsquo;s custom file format for URLs. If you&rsquo;ve ever dragged-and-dropped a URL on macOS, you&rsquo;ve probably encountered one of these; they&rsquo;re a pretty simple (if Apple-specific) format, but I&rsquo;ve found them to be almost entirely unsupported on Linux.</p> \n<p>The few options out there seem either incomplete (<a href=\"https://blog.scottlowe.org/2016/12/21/opening-webloc-files-ubuntu/\">Opening Web Internet Location Files on Ubuntu</a>) or overly heavyweight (<a href=\"https://benchdoos.github.io/\">WeblocOpener</a>). With that in mind, here&rsquo;s my slightly more complete set of instructions for adding support for opening <code>.webloc</code> files to Linux.</p> \n<p>The approach involves creating two new files: a <a href=\"https://specifications.freedesktop.org/mime-apps-spec/latest/\">MIME database entry</a> telling Linux about the existence of the <code>application/x-webloc</code> MIME type; and a <a href=\"https://wiki.archlinux.org/title/Desktop_entries\">desktop entry</a> which provides a simple handler for the new MIME type, using <code>xdg-open</code> to open the URL with the default browser.</p> \n<ol> \n <li><p>Create the MIME entry in <code>~/.local/share/mime/packages/application-x-webloc.xml</code>:</p> <pre><code class=\"language-xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\n&lt;mime-info xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\"&gt;\n   &lt;mime-type type=\"application/x-webloc\"&gt;\n     &lt;comment&gt;Webloc&lt;/comment&gt;\n     &lt;glob pattern=\"*.webloc\"/&gt;\n   &lt;/mime-type&gt;\n&lt;/mime-info&gt;\n</code></pre></li> \n <li><p>Create the desktop entry in <code>~/.local/share/applications/webloc.desktop</code>:</p> <pre><code class=\"language-ini\">[Desktop Entry]\nName=Open Webloc Files\nComment=Open webloc files in the default browser\nExec=sh -c \"xdg-open `plistutil -f xml -i \\\\\"$1\\\\\" | xmllint --xpath \\\\\"//dict/string/text()\\\\\" -`\" -- %f\nIcon=text-html\nTerminal=false\nType=Application\nMimeType=application/x-webloc;\nNoDisplay=true\n</code></pre> <p>This uses a combination of <code>plistutil</code> and <code>xmllint</code> to extract the URL from the webloc property list file so you&rsquo;ll also need to install these.</p> <p>Fedora:</p> <pre><code class=\"language-shell\">sudo dnf -y install libplist libxml2\n</code></pre> <p>Ubuntu:</p> <pre><code class=\"language-shell\">sudo apt-get install -y libplist-utils libxml2-utils\n</code></pre></li> \n <li><p>Finally, update the MIME and desktop databases:</p> <pre><code class=\"language-shell\">update-mime-database ~/.local/share/mime\nupdate-desktop-database ~/.local/share/applications\n</code></pre></li> \n</ol> \n<p>After completing these steps, double clicking a <code>.webloc</code> file in your file manager of choice should open the link in your default browser.</p> \n<hr /> \n<p><strong>20 March 2025</strong>: Updated to add <code>--f xml</code> to force an XML output format irrespective of the webloc file input format; thanks for the feedback Nathan!</p> \n<p><strong>6 December 2025</strong>: Updated to add instructions for installing <code>libplist</code> and <code>libxml2</code> on Ubuntu. Thanks Jon!.</p>",
      "date_published": "2025-02-12T15:00:40-08:00",
      "id": "https://jbmorley.co.uk/posts/2025-02-12-webloc-files-and-linux/",
      "title": "Webloc Files and Linux",
      "url": "https://jbmorley.co.uk/posts/2025-02-12-webloc-files-and-linux/"
    },
    {
      "content_html": "<p>After taking up altogether too much of my <a href=\"/tags/#december-adventure\">December Adventure</a>, day 19 finally brought working drag-and-drop file transfer to <a href=\"https://github.com/inseven/reconnect\">Reconnect</a>, my Psion connectivity software for macOS. 🎉</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-20-december-adventure-day-19/file-transfer/400.gif\" width=\"400\" height=\"273\" x-srcset=\"/posts/2024-12-20-december-adventure-day-19/file-transfer/400.gif 400 273,/posts/2024-12-20-december-adventure-day-19/file-transfer/800.gif 800 546,/posts/2024-12-20-december-adventure-day-19/file-transfer/1200.gif 1200 820,/posts/2024-12-20-december-adventure-day-19/file-transfer/1600.gif 1600 1093,\" />  \n </body></p> \n<p>There&rsquo;s not much to add technically beyond what I&rsquo;ve covered in previous posts, so feel free to check out my write-ups from <a href=\"/posts/2024-12-16-december-adventure-day-15/\">day 15</a> and <a href=\"/posts/2024-12-19-december-adventure-day-18/\">day 18</a> if you&rsquo;re curious about the details.</p> \n<p>I&rsquo;ve yet to add support for dragging folders which will require some refactoring: folders are currently handled as a collection of individual file transfers and that will need to change to allow me to track the overall progress and provide a single completion for the <code>NSItemProvider</code>-based drag operation. With that in place, I&rsquo;ll be ready to push a release. Lots to keep me busy on day 20!</p>",
      "date_published": "2024-12-20T11:01:14-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-20-december-adventure-day-19/",
      "title": "December Adventure Day 19",
      "url": "https://jbmorley.co.uk/posts/2024-12-20-december-adventure-day-19/"
    },
    {
      "content_html": "<p>Day 18 of my <a href=\"/tags/#december-adventure\">December Adventure</a> brought a new wave of pain (I strongly advise against fracturing your foot, however exciting it might seem), so everything went slowly and there&rsquo;s not too much to report&mdash;I mostly spent my time thinking about how things needed to work instead of doing. Still, in the interests of journaling the progress I do make, here&rsquo;s a little update.</p> \n<h1><a id=\"reconnect\"></a>Reconnect</h1> \n<p>I&rsquo;m still committed to adding full support for drag and drop to <a href=\"https://github.com/inseven/reconnect\">Reconnect</a>, my Psion connectivity sofware for macOS. The remaining work entails figuring out how drag-and-drop operations should interact with the Transfers window&mdash;it feels like the correct place to show progress for drag-initiated transfers, but it&rsquo;s not currently set up for that.</p> \n<p>With drag operations, apps don&rsquo;t know the file&rsquo;s destination&mdash;they pass the data to the OS and it does what&rsquo;s necessary to create/copy/move the file. That means the model object that backs the Transfers window and manages on-going transfer operations needs to expose a slightly lower level API that can be used for both the regular transfers from the Psion to the user&rsquo;s &lsquo;Downloads&rsquo; directory, and the <code>NSItemProvider</code> implementation used for drag-and-drop.</p> \n<p>Right now, the API to download a file from the Psion looks like this:</p> \n<pre><code class=\"language-swift\">@MainActor @Observable\nclass TransfersModel {\n\n    func download(from source: FileServer.DirectoryEntry,\n                  to destinationURL: URL? = nil,\n                  convertFiles: Bool) async throws\n\n}\n</code></pre> \n<p>The implementation always defaults to using the users &lsquo;Downloads&rsquo; directory if <code>destinationURL</code> is <code>nil</code>:</p> \n<pre><code class=\"language-swift\">let fileManager = FileManager.default\nlet downloadsURL = fileManager.urls(for: .downloadsDirectory,\n                                    in: .userDomainMask)[0]\nlet destinationURL = destinationURL ?? downloadsURL.appendingPathComponent(source.name)\nlet temporaryURL = fileManager.temporaryDirectory.appendingPathComponent((UUID().uuidString))\n...\ntry fileManager.moveItem(temporaryURL, to: destinationURL)\n...\nreturn destinationURL\n</code></pre> \n<p>Instead, of this, it needs to return the resulting URL to the caller and only move the temporary file used for transfers if an explicit destination is specified. The prototype needs to look something like this:</p> \n<pre><code class=\"language-swift\">func download(from source: FileServer.DirectoryEntry,\n              to destinationURL: URL? = nil,\n              convertFiles: Bool) async throws -&gt; URL\n\n</code></pre> \n<p>And internally, the behavior needs to be a little more like this, moving the temporary file only if <code>destinationURL</code> is non-nil:</p> \n<pre><code class=\"language-swift\">...\nlet destinationURL = if let destinationURL {\n    try fileManager.moveItem(temporaryURL, to: destinationURL)\n    return destinationURL\n} else {\n    return temporaryURL\n}\n...\nreturn destinationURL\n</code></pre> \n<p><em>I&rsquo;m sure there&rsquo;s a cleaner way to write this&mdash;I&rsquo;m still getting used to Swift&rsquo;s new conditional assignments.</em></p> \n<p>This small change of should allow me to build on the example code from <a href=\"/posts/2024-12-16-december-adventure-day-15/\">day 15</a> as follows:</p> \n<pre><code class=\"language-swift\">TableRow(file)\n    .itemProvider {\n        let provider = NSItemProvider()\n        provider.suggestedName = file.name\n        provider.registerFileRepresentation(for: .data) { completion in\n            Task {\n                let url = await self.browserModel.download([file.id], convertFiles: false)\n                let data = try Data(contentsOf: url)\n                completion(data, false, nil)\n            }\n            return nil\n        }\n        return provider\n    }\n\n</code></pre> \n<p>I still need to work out how to update the suggested filename if it changes during file conversion (e.g., &lsquo;image.mbm&rsquo; to &lsquo;image.tiff&rsquo;) and if possible I&rsquo;d like to make that conversion decision a user-interactive part of the file transfer.</p> \n<p>The legacy download behavior (when the user clicks the download toolbar button) can now be achieved by explicitly passing the downloads directory:</p> \n<pre><code class=\"language-swift\">Task {\n    let downloadsURL = FileManager.default.urls(for: .downloadsDirectory,\n                                                in: .userDomainMask)[0]\n    try? await transfersModel.download(from: file,\n                                       to: downloadsURL,\n                                       convertFiles: convertFiles)\n}\n</code></pre> \n<h1><a id=\"psion-emulation\"></a>Psion Emulation</h1> \n<p>While it feels like I&rsquo;m rapidly running out of days in December, I&rsquo;m still keen to contribute a little to the Psion emulation story: I&rsquo;d like to finish my brief write-up explaining some of the emulation options available in <a href=\"https://www.mamedev.org\">MAME</a>, and I&rsquo;d really love to get a handle on what would be involved in producing something like <a href=\"https://infinitemac.org\">Infinite Mac</a>&mdash;a website showcasing emulators for every Mac&mdash;for Psions.</p> \n<p>With my sights set on&mdash;shall we call it &lsquo;Infinite Psion&rsquo;?&mdash;I had a go at compiling MAME for the web using <a href=\"https://emscripten.org\">Emscripten</a>. MAME has some <a href=\"https://docs.mamedev.org/initialsetup/compilingmame.html#emscripten-javascript-and-html\">instructions for doing just this</a>, and the Emscripten <a href=\"https://emscripten.org/docs/getting_started/index.html\">Getting Started</a> seems pretty accessible. It should be as simple as:</p> \n<pre><code class=\"language-bash\">cd ~/Projects\ngit clone https://github.com/emscripten-core/emsdk.git\ncd emsdk\n./emsdk install latest\n./emsdk activate latest\nsource ./emsdk_env.sh\n\ncd ~/Projects\ngit clone git@github.com:mamedev/mame.git\ncd mame\nemmake make SUBTARGET=psion3a SOURCES=psion/psion3a.cpp\n</code></pre> \n<p>Needless to say, it didn&rsquo;t just work out of the box, and I now have a number of errors to contend with which, I suspect, require me to think all-too hard about what&rsquo;s going on under the hood in JavaScript and WASM-land. 🫠</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-19-december-adventure-day-18/mame-emscripten-fail@2x/400.png\" width=\"400\" height=\"289\" x-srcset=\"/posts/2024-12-19-december-adventure-day-18/mame-emscripten-fail@2x/400.png 400 289,/posts/2024-12-19-december-adventure-day-18/mame-emscripten-fail@2x/800.png 800 579,/posts/2024-12-19-december-adventure-day-18/mame-emscripten-fail@2x/1200.png 1200 869,/posts/2024-12-19-december-adventure-day-18/mame-emscripten-fail@2x/1600.png 1600 1159,\" />  \n </body></p>",
      "date_published": "2024-12-19T19:01:28-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-19-december-adventure-day-18/",
      "title": "December Adventure Day 18",
      "url": "https://jbmorley.co.uk/posts/2024-12-19-december-adventure-day-18/"
    },
    {
      "content_html": "<p>Life got in the way of day 17 of my <a href=\"/tags/#december-adventure\">December Adventure</a> making for a slow day with little progress. Still, I managed to find the time to correct my design for a <a href=\"/posts/2024-12-17-december-adventure-day-16/\">replacement MNT Pocket Reform backplate</a> and post it on <a href=\"https://github.com/jbmorley/pocket-reform-backplate\">GitHub</a>.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-18-december-adventure-day-17/backplate-technical-drawing/400.png\" width=\"400\" height=\"282\" x-srcset=\"/posts/2024-12-18-december-adventure-day-17/backplate-technical-drawing/400.png 400 282,/posts/2024-12-18-december-adventure-day-17/backplate-technical-drawing/800.png 800 565,/posts/2024-12-18-december-adventure-day-17/backplate-technical-drawing/1200.png 1191 842,/posts/2024-12-18-december-adventure-day-17/backplate-technical-drawing/1600.png 1191 842,\" />  \n </body></p> \n<p>Thankfully I spotted yesterday&rsquo;s mistake fast enough that PCBWay hadn&rsquo;t started manufacture and I was able to simply provide them with the updated designs. Hopefully I&rsquo;ve not missed anything this time around. 🤞🏻</p> \n<p>If you&rsquo;re thinking about trying these designs out for yourself, I encourage you to wait until I&rsquo;ve received the parts from PCBWay and can confirm they fit correctly.</p>",
      "date_published": "2024-12-18T09:58:16-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-18-december-adventure-day-17/",
      "title": "December Adventure Day 17",
      "url": "https://jbmorley.co.uk/posts/2024-12-18-december-adventure-day-17/"
    },
    {
      "content_html": "<p>After an increasingly frustrating few days bashing my head against Apple&rsquo;s development environment, I decided it was time for a change of pace for day 16 of my Psion-themed <a href=\"/tags/#december-adventure\">December Adventure</a>&mdash;it <em>is</em> meant to be a bit of low-stakes fun afer all.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-17-december-adventure-day-16/pocket-reform/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-17-december-adventure-day-16/pocket-reform/400.jpeg 400 300,/posts/2024-12-17-december-adventure-day-16/pocket-reform/800.jpeg 800 600,/posts/2024-12-17-december-adventure-day-16/pocket-reform/1200.jpeg 1200 900,/posts/2024-12-17-december-adventure-day-16/pocket-reform/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p class=\"caption\">The incredibly extra MNT Pocket Reform</p> \n<p>I&rsquo;ve enjoyed owning an <a href=\"https://shop.mntre.com/products/mnt-pocket-reform\">MNT Pocket Reform</a> ever since their Crowd Supply campaign. It&rsquo;s no Psion (I have to stay on theme somehow), but I&rsquo;ve been incredibly impressed with how GNOME scales down to smaller screens and I love having a diminutive device on-hand that&rsquo;s powerful enough to serve as a modern development environment with sufficient storage to have a copy of all my data (thanks <a href=\"https://syncthing.net/\">Syncthing</a>). Still, it&rsquo;s far from perfect and feels a lot like a fussy sports car that demands frequent fettling&mdash;the keyboard controller is unpredictable, the WiFi cuts out, and it power cycles without notice.</p> \n<p>I&rsquo;ve long suspected many of these issues are related to poor thermals so, when I saw <a href=\"https://community.mnt.re/t/solving-heat-related-issues-with-custom-lid/2809\">pandora&rsquo;s post</a> on the MNT forums describing the amazing improvements they&rsquo;ve seen by replacing the <a href=\"https://en.wikipedia.org/wiki/FR-4\">FR-4</a>-based PCB backplate with an aluminum panel, I knew I had to try it out for myself. Although they&rsquo;ve published design files on their <a href=\"https://github.com/FesixGermany/mnt_pocket_reform_backplate\">GitHub</a>, it seemed like a good opportunity to try my hand at designing and ordering a custom CNC part.</p> \n<p>Earlier this year, I ordered a prototype stand for <a href=\"https://statuspanel.io\">StatusPanel</a> from <a href=\"https://www.pcbway.com\">PCBWay</a> (no, this post isn&rsquo;t sponsored) that just happened to be purple anodized aluminum so I offered it up in place of the existing backplate to see how it felt and how well the color matched.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-17-december-adventure-day-16/status-panel-experiment/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-17-december-adventure-day-16/status-panel-experiment/400.jpeg 400 300,/posts/2024-12-17-december-adventure-day-16/status-panel-experiment/800.jpeg 800 600,/posts/2024-12-17-december-adventure-day-16/status-panel-experiment/1200.jpeg 1200 900,/posts/2024-12-17-december-adventure-day-16/status-panel-experiment/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p>Much to my surprise, the color matched pretty closely&mdash;well enough to order a prototype. The contrasting darker purple might even prove a design feature.</p> \n<p>Encouraged by this, I set about figuring out the exact dimensions and tolerances of the backplate. Fortunately, MNT Research publishes <a href=\"https://source.mnt.re/reform/pocket-reform\">all the schematics and designs</a> for the Pocket Reform on GitHub, so I was able to use the backplate PCB designs for dimensions and the keyboard bezel technical drawings to get the CNC tolerances and details of the countersinks.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-17-december-adventure-day-16/schematics/400.jpeg\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-17-december-adventure-day-16/schematics/400.jpeg 400 300,/posts/2024-12-17-december-adventure-day-16/schematics/800.jpeg 800 600,/posts/2024-12-17-december-adventure-day-16/schematics/1200.jpeg 1200 900,/posts/2024-12-17-december-adventure-day-16/schematics/1600.jpeg 1600 1200,\" />  \n </body></p> \n<p class=\"caption\">Sometimes it&rsquo;s easier to work on paper</p> \n<p>I had originally thought I would be able simply amend the keyboard bezel designs and use this for the backplate but when I sized it up (boy am I glad I did this), the screw holes didn&rsquo;t line up:</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-17-december-adventure-day-16/keyboard-bezel/400.jpeg\" width=\"400\" height=\"533\" x-srcset=\"/posts/2024-12-17-december-adventure-day-16/keyboard-bezel/400.jpeg 400 533,/posts/2024-12-17-december-adventure-day-16/keyboard-bezel/800.jpeg 800 1066,/posts/2024-12-17-december-adventure-day-16/keyboard-bezel/1200.jpeg 1200 1600,/posts/2024-12-17-december-adventure-day-16/keyboard-bezel/1600.jpeg 1600 2133,\" />  \n </body></p> \n<p class=\"caption\">Testing the keyboard bezel in place of the backplate</p> \n<p>Much to my surprise, the vertical spacing of the screw holes on the top half of the case are different to the keyboard&mdash;the horizontal and vertical insets are very slightly different. I was able to confirm this by comparing the top PCB and keyboard bezel designs. Instead, I created my own designs from scratch, using a combination of dimensions from both:</p> \n<p><a href=\"technical-drawing.pdf\">\n  <body>   \n   <img src=\"/posts/2024-12-17-december-adventure-day-16/technical-drawing-preview/400.png\" width=\"400\" height=\"282\" x-srcset=\"/posts/2024-12-17-december-adventure-day-16/technical-drawing-preview/400.png 400 282,/posts/2024-12-17-december-adventure-day-16/technical-drawing-preview/800.png 800 565,/posts/2024-12-17-december-adventure-day-16/technical-drawing-preview/1200.png 1191 842,/posts/2024-12-17-december-adventure-day-16/technical-drawing-preview/1600.png 1191 842,\" />  \n  </body></a></p> \n<p>With the design in place, I kicked off an order on PCBWay and now it&rsquo;s just a matter of waiting; $70 shipped to Hawaii doesn&rsquo;t seem at all bad for a custom milled and anodized part.</p> \n<p>\n <x-model src=\"case.stl\" /></p> \n<p></p>\n<aside>\n Writing this up, I&rsquo;ve just noticed that I completely forgot the two screw holes along the bottom edge and the one on the top edge. Don&rsquo;t use order your own version using these designs! 🫠\n</aside>\n<p></p> \n<hr /> \n<p>Having discovered such a significant mistake, I guess day 17 will involve a little more design work. That will also give me a chance to publish the design files so that others can use them (so long as everything works out in the end). I&rsquo;ve reached out to PCBWay and let them know there&rsquo;s an error in my design and hopefully they&rsquo;ve not started production yet. 🤞🏻</p>",
      "date_published": "2024-12-17T14:47:47-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-17-december-adventure-day-16/",
      "title": "December Adventure Day 16",
      "url": "https://jbmorley.co.uk/posts/2024-12-17-december-adventure-day-16/"
    },
    {
      "content_html": "<p>Day 15 of my Psion <a href=\"/tags/#december-adventure\">December Adventure</a> brought a continued focus on <a href=\"#reconnect\">Reconnect</a>, my Psion connectivity software for macOS. Unfortunately I spent much of the day wrestling with the platform, so these notes focus more on figuring out how the APIs work than they do on presenting a complete feature. That&rsquo;s just how it goes sometimes.</p> \n<h1><a id=\"reconnect\"></a>Reconnect</h1> \n<p>Eager to continue to improve the file transfer experience, I&rsquo;ve been looking at adding drag-and-drop support to the file browser. This should have been easy, but a bunch of rough edges sent me down many rabbit holes&mdash;Reconnect is implemented in SwiftUI which continues to fall short on macOS, either failing to provide support for native macOS behavior, deviating significantly from the documentation, or being so buggy that it&rsquo;s simply unusable. I hit up against many of these issues but I think I figured it out by the end of the day, so I&rsquo;ll try to walk through the solution in detail here in the hope that it helps others.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2024-12-16-december-adventure-day-15/reconnect-browser-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2024-12-16-december-adventure-day-15/reconnect-browser@2x/1600.png\"\n             width=\"800.0\"\n             height=\"504.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">The main Reconnect file browser window</p> \n<p>The main browser window of Reconnect uses a SwiftUI <a href=\"https://developer.apple.com/documentation/SwiftUI/Table\"><code>Table</code></a>. The code looks something like this:</p> \n<pre><code class=\"language-swift\">Table(browserModel.files, selection: $browserModel.fileSelection) {\n    TableColumn(\"\") { file in\n        Image(file.fileType.image)\n    }\n    .width(16.0)\n    TableColumn(\"Name\", value: \\.name)\n    TableColumn(\"Date Modified\") { file in\n        Text(file.modificationDate.formatted(date: .long, time: .shortened))\n            .foregroundStyle(.secondary)\n    }\n    TableColumn(\"Size\") { file in\n        if file.isDirectory {\n            Text(\"--\")\n                .foregroundStyle(.secondary)\n        } else {\n            Text(file.size.formatted(.byteCount(style: .file)))\n                 .foregroundStyle(.secondary)\n        }\n    }\n    TableColumn(\"Type\") { file in\n        FileTypePopover(file: file)\n            .foregroundStyle(.secondary)\n    }\n}\n</code></pre> \n<p>In order to support drag-and-drop of these rows, SwiftUI requires us to use a slightly different <code>Table</code> constructor which allows each row to be customized:</p> \n<pre><code class=\"language-swift\">Table(of: FileServer.DirectoryEntry.self, selection: $browserModel.fileSelection) {\n    ...\n} rows: {\n    ForEach(browserModel.files) { file in\n        TableRow(file)\n    }\n}\n</code></pre> \n<p>This alternative constructor pushes the file/row iteration down into a <code>ForEach</code> (in the <code>rows</code> view builder) which lets us apply view modifiers to each <code>TableRow</code>. For example, if <code>file</code> conformed to the <a href=\"https://developer.apple.com/documentation/CoreTransferable/Transferable\"><code>Transferable</code></a> protocol, making these rows draggable <em>should</em> be as simple as:</p> \n<pre><code class=\"language-swift\">Table(of: FileServer.DirectoryEntry.self, selection: $browserModel.fileSelection) {\n    ...\n} rows: {\n    ForEach(browserModel.files) { file in\n        TableRow(file)\n           .draggable(file)\n    }\n}\n</code></pre> \n<p>But this is where things start to go awry. SwiftUI actually provides two different mechanisms for making things draggable: <a href=\"https://developer.apple.com/documentation/swiftui/view/draggable(_:)\"><code>.draggable</code></a>, and <a href=\"https://developer.apple.com/documentation/swiftui/tablerowcontent/itemprovider(_:)\"><code>.itemProvider.</code></a> <code>.draggable</code> is the new hotness which, of course, means it&rsquo;s not actually flexible enough to do what we need in Reconnect.</p> \n<p>In order to see why <code>.draggable</code> doesn&rsquo;t work, I&rsquo;ll first show some pseudo-code for a complete solution using <code>.itemProvider</code> (where I got to by the end of the day) and then break down how it works:</p> \n<pre><code class=\"language-swift\">Table(of: FileServer.DirectoryEntry.self, selection: $browserModel.fileSelection) {\n    ...\n} rows: {\n    ForEach(browserModel.files) { file in\n        TableRow(file)\n            .itemProvider {\n                let provider = NSItemProvider()\n                provider.suggestedName = file.name\n                provider.registerFileRepresentation(for: .data) { completion in\n                    Task {\n                        let data = await self.browserModel.download([file.id])\n                        completion(file, false, nil)\n                    }\n                    return nil\n                }\n                return provider\n            }\n    }\n}\n</code></pre> \n<p>There&rsquo;s a few things going on in this code:</p> \n<ul> \n <li><a href=\"https://developer.apple.com/documentation/foundation/nsitemprovider/2890244-suggestedname\"><code>.suggestedName</code></a> lets drag destinations like Finder know what the dragged file should be called</li> \n <li><a href=\"https://developer.apple.com/documentation/foundation/nsitemprovider/4011315-registerfilerepresentation\"><code>.registerFileRepresentation</code></a> provides a completion block (load handler) that can initiate a file transfer from the Psion to fetch the file data and, crucially, specifies a content type of <code>.data</code> which tells macOS tells macOS to expect <code>completion</code> to be called asynchronously (I wasted <em>a lot</em> of time figuring that out)<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup></li> \n <li>the load handler can optionally return a <a href=\"https://developer.apple.com/documentation/foundation/progress\"><code>Progress</code></a> instance as a way to communicate load progress, but I&rsquo;ve not found it makes any difference to the user experience, so <code>nil</code> seems to be sufficient</li> \n</ul> \n<p>So why isn&rsquo;t <code>.draggable</code> good enough? The equivalent implementation certainly seems cleaner:</p> \n<pre><code class=\"language-swift\">Table(of: FileServer.DirectoryEntry.self, selection: $browserModel.fileSelection) {\n    ...\n} rows: {\n    ForEach(browserModel.files) { file in\n        TableRow(file)\n            .draggable(file)\n    }\n}\n</code></pre> \n<p>And, the corresponding <code>Transferable</code> conformance:</p> \n<pre><code class=\"language-swift\">extension FileServer.DirectoryEntry: Transferable {\n\n    static var transferRepresentation: some TransferRepresentation {\n        FileRepresentation(exportedContentType: .data) { item in\n            return SendTransferredFile(await self.browserModel.download([item.id]))\n        }\n    }\n\n}\n</code></pre> \n<p>This is much more modern and supports an async block by default, avoiding the need to wrap code in a <code>Task</code><sup id=\"fnref2\"><a href=\"#fn2\" rel=\"footnote\">2</a></sup>. Unfortunately, there&rsquo;s one key aspect missing&mdash;there&rsquo;s no way to specify the desired filename, without which, Finder will always create a file named &lsquo;data&rsquo; for every drop operation. Assuming that <code>.draggable</code> and <code>.itemProvider</code> offered identical functionality, I spent a lot of time going down dead-ends trying to work around this until I finally chanced upon the <code>.itemProvider</code>-based solution<sup id=\"fnref3\"><a href=\"#fn3\" rel=\"footnote\">3</a></sup>. 🤦🏻‍♂️</p> \n<hr /> \n<p>Having spent altogether too long poking at <code>.draggable</code> and <code>Transferable</code> before settling on <code>.itemProvider</code> and <code>NSItemProvider</code>, I wasn&rsquo;t able to fully implement drag-and-drop support, but hopefully I&rsquo;m well set up to get something working on day 16.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>Without this, Finder will beachball until <code>completion</code> is called. Unfortunately, I&rsquo;ve not been able to find any official documentation stating this, only <a href=\"https://stackoverflow.com/questions/74449458/how-to-use-transferable-in-an-asynchronous-way-in-swiftui\">a discussion on StackOverflow</a> that put me on the right path.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n  <li id=\"fn2\"> <p>Though ironically, even with explicit async support, this method still has the same absurd synchronous behavior of <code>NSItemProvider.registerFileRepresentation</code> for any data type excepting <code>.data</code> and <code>.folder</code>.&nbsp;<a href=\"#fnref2\" rev=\"footnote\">↩</a></p> </li> \n  <li id=\"fn3\"> <p>Do let me know if I&rsquo;ve missed something. I&rsquo;d love to be able to use <code>Transferable</code> instead.&nbsp;<a href=\"#fnref3\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2024-12-16T16:47:30-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-16-december-adventure-day-15/",
      "title": "December Adventure Day 15",
      "url": "https://jbmorley.co.uk/posts/2024-12-16-december-adventure-day-15/"
    },
    {
      "content_html": "<p>Day 14 of my Psion-related <a href=\"/tags/#december-adventure\">December Adventure</a> was a somewhat frustrating one&mdash;I broke my foot a few months ago and, even now, the pain will turn my brain to mush from time to time. Still, I pressed on with <a href=\"#reconnect\">Reconnect</a>, my Psion connectivity software for macOS.</p> \n<h1><a id=\"reconnect\"></a>Reconnect</h1> \n<p>One of the things that&rsquo;s easy to forget in the rose-tinted haze of nostalgia is that most 90s-era devices didn&rsquo;t use standard file formats, necessitating translation when moving files back and forth. Psions are no exception. Thanks to Tom&rsquo;s work on <a href=\"https://opolua.org\">OpoLua</a>, Reconnect has built-in support for converting Psion&rsquo;s MBM image format, but my implementation has never felt quite right.</p> \n<p>MBM files can store more than one image (literally <strong>M</strong>ultiple <strong>B</strong>it<strong>M</strong>ap). Most you&rsquo;ll encounter as a user contain a single image (screenshots, etc), but as a developer, you&rsquo;ll often use asset files with multiple images&mdash;EPOC32&rsquo;s Eikon.mbm, for example, contains all the images used in the main user interface. While it&rsquo;s pretty obvious that single-image MBMs should be converted to a single file (PNG, GIF, etc), it&rsquo;s less clear what should happen to a multi-image MBM as there are few user-friendly modern file formats that support multiple images.</p> \n<p>My first implementation simply unpacked images into the Downloads directory, but for large image files (Eikon.mbm contains 87 images) it can get messy quickly. After much consideration, I&rsquo;ve settled&mdash;for the time being at least&mdash;on converting all images to TIFFs which natively support multiple images. These can be easily viewed using Preview on macOS.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2024-12-15-december-adventure-day-14/preview-eikon-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2024-12-15-december-adventure-day-14/preview-eikon@2x/1600.png\"\n             width=\"800.0\"\n             height=\"615.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Viewing the converted Eikon.mbm TIFF file in Preview</p> \n<p>This change to using TIFF files for MBMs is available in <a href=\"https://github.com/inseven/reconnect/releases/tag/0.14.9\">Reconnect 0.14.9</a>. In the long term, I plan to offer different options for converting multi-image MBMs&mdash;a folder of images, multi-frame GIF, zip file, etc.</p> \n<hr /> \n<p>Hopefully day 15 will bring a little more capacity and I can continue to push on with improving the file transfer experience in Reconnect; I&rsquo;d still like to add drag-and-drop support.</p>",
      "date_published": "2024-12-15T11:50:24-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-15-december-adventure-day-14/",
      "title": "December Adventure Day 14",
      "url": "https://jbmorley.co.uk/posts/2024-12-15-december-adventure-day-14/"
    },
    {
      "content_html": "<p>After taking day 12 off from my Psion <a href=\"/tags/#december-adventure\">December Adventure</a>, I returned on day 13, adding a few quality of life improvements to <a href=\"#reconnect\">Reconnect</a>, my modern Psion connectivity software for macOS&mdash;I&rsquo;m planning to spend the rest of the month focused on this and the other tooling that keeps these 30 year old computers going in 2024.</p> \n<h1><a id=\"reconnect\"></a>Reconnect</h1> \n<p>Working on <a href=\"https://github.com/inseven/reconnect\">Reconnect</a> feels slower than cranking out OPL but there remain a collection of little fixes that will improve my daily experience and, hopefully, make it a little more enjoyable for others to use.</p> \n<p>Continuing my focus on the Transfers window, I plumbed through file sizes so I can show the number of bytes remaining in a transfer. This is particularly useful on low-memory devices like the Psions where it&rsquo;s easy to inadvertently copy a file larger than the whole device&rsquo;s storage&mdash;the extra context can help spot such a mistake early. I also added support for showing previews of completed downloads which I find helps in quickly identifying the correct screenshots for these write-ups.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2024-12-14-december-adventure-day-13/transfers-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2024-12-14-december-adventure-day-13/transfers@2x/1600.png\"\n             width=\"472.0\"\n             height=\"720.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Transfers, now with file sizes and previews!</p> \n<p>It doesn&rsquo;t feel like there&rsquo;s much novelty in the underlying code itself, with the minor exception of the approach taken to generating the file previews. This necessitated wrapping QuickLook&rsquo;s classic completion-block API as a new cancellable <code>async</code> function to make it easier to call from SwiftUI:</p> \n<pre><code class=\"language-swift\">extension QLThumbnailGenerator {\n\n    public func thumbnailRepresentation(fileAt url: URL,\n                                        size: CGSize,\n                                        scale: CGFloat,\n                                        iconMode: Bool) async throws -&gt; QLThumbnailRepresentation {\n        let request = Request(fileAt: url, size: size, scale: scale, representationTypes: .thumbnail)\n        request.iconMode = iconMode\n        return try await withTaskCancellationHandler {\n            try await withCheckedThrowingContinuation { continuation in\n                generateRepresentations(for: request) { (thumbnail, type, error) in\n                    if let error {\n                        continuation.resume(throwing: error)\n                        return\n                    }\n                    continuation.resume(returning: thumbnail!)\n                }\n            }\n        } onCancel: {\n            cancel(request)\n        }\n    }\n\n}\n</code></pre> \n<p>While it looks convoluted, this code is actually relatively simple: it uses the platform-provided <code>withCheckedThrowingContinuation</code> function to convert the classic <code>QLThumbnailGenerator.generateRepresentations</code> completion-block API into an <code>async</code> call, and then wraps that with <code>withTaskCancellationHandler</code> which returns a cancellable <code>Task</code> that will call <code>QLThumbnailGenerator.cancel</code>.</p> \n<p>It&rsquo;s frustrating to have to massage platform-provided APIs in this way, but once it&rsquo;s done, it&rsquo;s easy to use in SwiftUI:</p> \n<pre><code>@MainActor\nstruct ThumbnailView: View {\n\n    let url: URL\n    let size: CGSize\n\n    @State var image: NSImage? = nil\n\n    var body: some View {\n        Image(nsImage: image ?? NSWorkspace.shared.icon(forFile: url.path))\n            .task {\n                let thumbnail = try? await QLThumbnailGenerator.shared.thumbnailRepresentation(fileAt: url, size: size, scale: 2.0, iconMode: true)\n                image = thumbnail?.nsImage\n            }\n    }\n\n}\n\n</code></pre> \n<p>The <code>.task</code> view modifier ensures the new async API is run once when the view is first attached and cancelled when it&rsquo;s dismissed (if necessary).</p> \n<p>With these usability improvements in-place, I took some time to experiment with adding drag-and-drop support and improving multi-image file conversions&mdash;is it better to convert multi-image MBMs to gifs, tiffs, or a zipped directory of image files? Questions for day 14 and beyond.</p>",
      "date_published": "2024-12-14T17:04:05-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-14-december-adventure-day-13/",
      "title": "December Adventure Day 13",
      "url": "https://jbmorley.co.uk/posts/2024-12-14-december-adventure-day-13/"
    },
    {
      "content_html": "<p>After nearly two weeks working on my Psion-related <a href=\"/tags/#december-adventure\">December Adventure</a>, I find I&rsquo;m slowing down. I&rsquo;m at the stage where the bones of the projects are in place and now it&rsquo;s a matter of slowly chipping away at a long list of issues. <a href=\"#reconnect\">Reconnect</a>, my focus for the day, feels particularly like this, so I allowed myself a little time drafting my <code>x-image</code> write-up, and playing around with Blender to create a splash screen for <a href=\"#thoughts-for-epoc\">Thoughts for EPOC</a>.</p> \n<h1><a id=\"reconnect\"></a>Reconnect</h1> \n<p>I set out with the continued goal of improving the experience around file transfers, starting with adding icons to the transfers window. This entailed much remembering how Reconnect works under the hood, and setting the ground work for treating local and remote file references interchangably&mdash;in the first instance, this allows me to reference files irrespective of transfer direction (download from Psion, or upload to Psion), but hopefully it will unlock adding support for browsing local file systems as well as remote ones in the future. My ultimate goal here is to show full (locally stored) device backups in the sidebar and allow users to inspect them to access and restore files.</p> \n<pre><code>enum FileReference {\n    case local(URL)\n    case remote(FileServer.DirectoryEntry)\n}\n</code></pre> \n<p>With this simple <code>FileReference</code> wrapper, I can expose a source-agnostic file item to the transfer operation which I can switch on to select an appropriate icon in SwiftUI:</p> \n<pre><code>switch transfer.item {\ncase .local(let url):\n    Image(.fileUnknown16)\ncase .remote(let file):\n    Image(file.fileType.image)\n}\n</code></pre> \n<p>This first-cut just shows an unknown icon for macOS-local files, but it gives me a clear point of customization where I can fetch QuickLook previews and icons.</p> \n<p>It works great for downloads:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2024-12-12-december-adventure-day-11/transfers-with-icons-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2024-12-12-december-adventure-day-11/transfers-with-icons@2x/1600.png\"\n             width=\"472.0\"\n             height=\"720.0\"\n             />\n    </picture>\n</div>\n</p> \n<p>There&rsquo;s a long list of fairly dull little improvements like this that I&rsquo;d like to chip away at so I&rsquo;ll try to do one a day for the next few days.</p> \n<h1><a id=\"thoughts-for-epoc\"></a>Thoughts for EPOC</h1> \n<p>Trying to remind myself this is meant to be fun, I fired up Blender (which I have no idea how to use), and set about rendering out some 90s-era 3D text for the Thoughts for EPOC splash screen.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-12-december-adventure-day-11/thoughts-splash-blender@2x/400.png\" width=\"400\" height=\"291\" x-srcset=\"/posts/2024-12-12-december-adventure-day-11/thoughts-splash-blender@2x/400.png 400 291,/posts/2024-12-12-december-adventure-day-11/thoughts-splash-blender@2x/800.png 800 582,/posts/2024-12-12-december-adventure-day-11/thoughts-splash-blender@2x/1200.png 1200 874,/posts/2024-12-12-december-adventure-day-11/thoughts-splash-blender@2x/1600.png 1600 1165,\" />  \n </body></p> \n<p>So far, I&rsquo;ve added the text, a camera through which to render the scene, and a light source. Nothing I do with the light source seems to work, but at least I was able to render something:</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-12-december-adventure-day-11/thoughts-splash-render/400.png\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-12-december-adventure-day-11/thoughts-splash-render/400.png 400 300,/posts/2024-12-12-december-adventure-day-11/thoughts-splash-render/800.png 640 480,/posts/2024-12-12-december-adventure-day-11/thoughts-splash-render/1200.png 640 480,/posts/2024-12-12-december-adventure-day-11/thoughts-splash-render/1600.png 640 480,\" />  \n </body></p> \n<p>This is going to take a while.</p>",
      "date_published": "2024-12-12T14:36:22-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-12-december-adventure-day-11/",
      "title": "December Adventure Day 11",
      "url": "https://jbmorley.co.uk/posts/2024-12-12-december-adventure-day-11/"
    },
    {
      "content_html": "<p>After the milestone of &lsquo;shipping&rsquo; <a href=\"https://github.com/jbmorley/thoughts-lite/releases/tag/0.01\">Thoughts for EPOC 0.01</a>, I decided to take something of a day off for day 10 of my <a href=\"/tags/#december-adventure\">December Adventure</a>, tidy up a few things, and address one or two of the frustrations I&rsquo;ve encountered on the way. I worked on my <a href=\"#website-tweaks\">website</a>, <a href=\"#thoughts-for-epoc\">Thoughts for EPOC</a>, and <a href=\"#reconnect\">Reconnect</a>&mdash;as one of my readers pointed out, this is starting to look more like a developer diary than a December Adventure, offering a bit of a peek into how I develop and maintain my own little ecosystem of software.</p> \n<h1><a id=\"website-tweaks\"></a>Website Tweaks</h1> \n<p>Updating my website every day has given me ample opportunity to notice a few rough edges that I wanted to tidy up, focused almost entirely around the symbols and icons I use to identify the different sections of the site.</p> \n<h2><a id=\"support\"></a>Support</h2> \n<p>A couple of months ago, I set up a <a href=\"https://github.com/sponsors/jbmorley/\">GitHub Sponsors</a> page and, ever since, I&rsquo;ve been slowly working out how to talk about it, how to integrate it into my website, and how to make room for one-time services like Buy Me a Coffee. I&rsquo;m not good at asking for money even though it&rsquo;s necessary&mdash;doing so has always felt somewhat gauche. With that in mind, I&rsquo;ve tried to surface it in subtle ways. For the time being, I&rsquo;ve settled on a <a href=\"/support/\">Support</a> page where I can give a quick overview of what I do and link to places to send me money. To try to gently nudge people towards the page, I&rsquo;ve added the now-fairly-common heart symbol at the end of the site&rsquo;s navigation.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2024-12-11-december-adventure-day-10/site-header-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2024-12-11-december-adventure-day-10/site-header@2x/1600.png\"\n             width=\"800.0\"\n             height=\"183.5\"\n             />\n    </picture>\n</div>\n</p> \n<p>The hope is that using a colored symbol instead of text helps it stand out, but also hopefully makes it clear it&rsquo;s not part of the main content of the site.</p> \n<p>I&rsquo;ve also selected a suitable heart for the 2004 theme from the GNOME 2.8.0 icons I used back in the day. (You can try the theme out yourself by selecting the little paintbrush in the bottom-right of this site.)</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2024-12-11-december-adventure-day-10/site-header-2004-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2024-12-11-december-adventure-day-10/site-header-2004@2x/1600.png\"\n             width=\"800.0\"\n             height=\"257.0\"\n             />\n    </picture>\n</div>\n</p> \n<p>I&rsquo;m really quite pleased how this one turned out.</p> \n<h2><a id=\"more-symbols\"></a>More Symbols</h2> \n<p>In the process of adding the heart, and testing it out on my 2004 theme, I noticed I was missing icons for the style guide and drafts sections and, perhaps more importantly to this December Adventure, missing symbols for the <a href=\"https://pdavision.co.uk\">pdavision</a> and <a href=\"https://psionstyle.co.uk\">PsionStyle</a> links on the main theme. So, I ferreted around the GNOME icons for the 2004 theme, and then broke out Inkscape to create a couple of symbols:</p> \n<div class=\"center\"> \n <a href=\"https://psionstyle.co.uk\"><img width=\"128\" height=\"128\" src=\"/images/custom/psionstyle.svg\" /></a> \n <a href=\"https://pdavision.co.uk\"><img width=\"128\" height=\"128\" src=\"/images/custom/pdavision.svg\" /></a> \n</div> \n<h1><a id=\"thoughts-for-epoc\"></a>Thoughts for EPOC</h1> \n<h2><a id=\"tracking-issues\"></a>Tracking Issues</h2> \n<p>With <a href=\"https://github.com/jbmorley/thoughts-lite\">Thoughts for EPOC</a> growing, I decided it was time to move my task list out of Obsidian and into <a href=\"https://github.com/jbmorley/thoughts-lite\">GitHub Issues</a> (which I much prefer for larger projects). When working with GitHub Issues, I use <a href=\"https://madebywindmill.com/taska\">Taska</a> which offers a great workflow for raising issues and I find makes everything significantly more manageable.</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2024-12-11-december-adventure-day-10/thoughts-issues-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2024-12-11-december-adventure-day-10/thoughts-issues@2x/1600.png\"\n             width=\"800.0\"\n             height=\"545.0\"\n             />\n    </picture>\n</div>\n</p> \n<p class=\"caption\">Taska turns GitHub Issues back into a lightweight todo list</p> \n<h2><a id=\"continuous-integration\"></a>Continuous Integration</h2> \n<p>It turns out I successfully <a href=\"https://xkcd.com/356/\">nerd sniped</a> <a href=\"https://github.com/tomsci\">Tom</a> and he updated <a href=\"https://opolua.org\">OpoLua</a>&rsquo;s OPL compiler to add support for generating AIF files. Needless too say, I immediately had to adopt this and <a href=\"https://github.com/jbmorley/thoughts-lite/pull/16\">update Toughts</a> to automatically create <code>.app</code> and <code>.aif</code> files using GitHub Actions. 🙏🏻🎉</p> \n<h1><a id=\"reconnect\"></a>Reconnect</h1> \n<p>Writing OPL every day and documenting the process means I&rsquo;m relying heavily on <a href=\"https://github.com/incontext/reconnect\">Reconnect</a>, my <a href=\"https://github.com/rrthomas/plptools\">plptools</a>-based Psion connectivity suite for macOS. Needless to say, I&rsquo;ve encountered a few frustrations with it over the past few days, so I took some time to focus on a few quality of life improvements.</p> \n<p>Working with SwiftUI always goes slower than I think it should for a declarative UI framework, but I managed to make some progress on improving the file transfer workflow. Specifically, the Transfers window now shows a small magnifying glass alongside downloads allowing you to reveal completed transfers in Finder&mdash;a small but significant UX optimization:</p> \n<p>\n\n<div class=\"photo-container\">\n    <picture\n        alt=\"\"        title=\"\"        >\n        <source srcset=\"/posts/2024-12-11-december-adventure-day-10/reconnect-transfers-dark@2x/1600.png\" media=\"(prefers-color-scheme: dark)\" />\n        <img class=\"photo-img\"\n             src=\"/posts/2024-12-11-december-adventure-day-10/reconnect-transfers@2x/1600.png\"\n             width=\"772.0\"\n             height=\"671.0\"\n             />\n    </picture>\n</div>\n</p> \n<p>The &lsquo;Clear&rsquo; button is also new and, just like the Safari downloads popover, clears any completed and cancelled transfers. I&rsquo;ve also added support for automatically converting files with the <code>.mbm</code> extension, and not just image files with the relevant UID headers.</p> \n<hr /> \n<p>It&rsquo;s been surprisingly fun to return to Reconnect and tidy things a little. Given that, I have a few more visual and functional improvements in mind for day 11.</p>",
      "date_published": "2024-12-11T12:31:09-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-11-december-adventure-day-10/",
      "title": "December Adventure Day 10",
      "url": "https://jbmorley.co.uk/posts/2024-12-11-december-adventure-day-10/"
    },
    {
      "content_html": "<p>Continuing with day 09 of my <a href=\"/tags/#december-adventure\">December Adventure</a>, I decided to focus on creating an installer for my OPL version of <a href=\"https://github.com/jbmorley/thoughts-lite\">Thoughts</a>&mdash;there&rsquo;s enough basic functionality in place for it to be useful (to me at least) and I&rsquo;d love others to be able to try it out.</p> \n<p>Creating a Psion installer (SIS file) is pretty simple, especially if you&rsquo;re not worried about localization. It&rsquo;s just a matter of specifying the supported languages, program metadata, then listing the files (and their destinations) and any secondary installers. The package file for Thoughts looks like this:</p> \n<pre><code class=\"language-plaintext\">&amp;EN\n #{\"Thoughts\"},(0x100092ca),0,1,0\n \"C:\\System\\Apps\\Thoughts\\Thoughts.app\"-\"!:\\System\\Apps\\Thoughts\\Thoughts.app\"\n \"C:\\System\\Apps\\Thoughts\\Thoughts.aif\"-\"!:\\System\\Apps\\Thoughts\\Thoughts.aif\"\n\n @\"SystInfo.sis\",(0x10000b90)\n</code></pre> \n<p><em>The <code>!</code> in the destinations indicates that the files can be installed to any drive the user selects, allowing them to install programs to a Compact Flash card.</em></p> \n<p>This package file will create an installer that bundles the all-important SystInfo.opx library that Thoughts relies on to generate timestamps.</p> \n<p>Although Psion never provided any first-party EPOC32-based tools for working with SIS files, <a href=\"http://neuon.com\">Neuon</a>, a fairly prolific software development group, published <a href=\"https://web.archive.org/web/20141012083317/http://neuon.com/downloads/nsisutil/\">nSISUtil</a> which can compile package files on-device. This makes it possible to develop and package an app directly on a Psion which, of course, is exactly what I did.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-09-december-adventure-day-09/nsisutil-creating-sis/400.png\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-09-december-adventure-day-09/nsisutil-creating-sis/400.png 400 300,/posts/2024-12-09-december-adventure-day-09/nsisutil-creating-sis/800.png 640 480,/posts/2024-12-09-december-adventure-day-09/nsisutil-creating-sis/1200.png 640 480,/posts/2024-12-09-december-adventure-day-09/nsisutil-creating-sis/1600.png 640 480,\" />  \n </body></p> \n<p class=\"caption\">Creating the installer on-device using nSISUtil</p> \n<p>Just like resource files, we don&rsquo;t yet have support for generating installers using the <a href=\"https://opolua.org\">OpoLua</a> tooling, but perhaps at some point I&rsquo;ll be able to automate the whole process on GitHub Actions. 🙃</p> \n<p>With the installer in-hand, I again turned to the Revo as my test device, and was able to confirm it worked, and verify <a href=\"/posts/2024-12-09-december-adventure-day-08/\">yesterday</a>&rsquo;s fix for note creation.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-09-december-adventure-day-09/thoughts-about-to-install/400.png\" width=\"400\" height=\"133\" x-srcset=\"/posts/2024-12-09-december-adventure-day-09/thoughts-about-to-install/400.png 400 133,/posts/2024-12-09-december-adventure-day-09/thoughts-about-to-install/800.png 480 160,/posts/2024-12-09-december-adventure-day-09/thoughts-about-to-install/1200.png 480 160,/posts/2024-12-09-december-adventure-day-09/thoughts-about-to-install/1600.png 480 160,\" />  \n </body></p> \n<p class=\"caption\">Installing Thoughts on my Revo</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-09-december-adventure-day-09/editor-hello-world/400.png\" width=\"400\" height=\"133\" x-srcset=\"/posts/2024-12-09-december-adventure-day-09/editor-hello-world/400.png 400 133,/posts/2024-12-09-december-adventure-day-09/editor-hello-world/800.png 480 160,/posts/2024-12-09-december-adventure-day-09/editor-hello-world/1200.png 480 160,/posts/2024-12-09-december-adventure-day-09/editor-hello-world/1600.png 480 160,\" />  \n </body></p> \n<p class=\"caption\">Capturing mission critical information</p> \n<p>And with that, Thoughts for EPOC has reached an important milestone&mdash;version 0.01! You can view the release on <a href=\"https://github.com/jbmorley/thoughts-lite/releases/tag/0.01\">GitHub</a>, or download it directly here. I&rsquo;d love your feedback!</p> \n<ul class=\"actions\"> \n <li><a class=\"symbol zip\" href=\"https://github.com/jbmorley/thoughts-lite/releases/download/0.01/Thoughts.0.01.sis\">Download</a></li> \n</ul> \n<p></p>\n<aside>\n Remember, you&rsquo;ll also need to install \n <a href=\"https://software.psion.info/programs/0x1000412b/\">Editor</a> to be able to edit newly created notes.\n</aside>\n<p></p>",
      "date_published": "2024-12-09T23:40:02-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-09-december-adventure-day-09/",
      "title": "December Adventure Day 09",
      "url": "https://jbmorley.co.uk/posts/2024-12-09-december-adventure-day-09/"
    },
    {
      "content_html": "<p>I started day 08 a little behind typing up Saturday&rsquo;s <a href=\"/tags/#december-adventure\">December Adventure</a> unexpected side-quest. And then bumped into a frustrating crash in <a href=\"https://incontext.jbmorley.co.uk\">InContext</a>, my site builder, that slowed everything up. With that in mind, I decided to return to my more mellow OPL explorations for the rest of the day. (But not before I shipped a proof-of-concept update to my website&rsquo;s image handling which will hopefully mean images work in the feed.)</p> \n<h1><a id=\"thoughts\"></a>Thoughts</h1> \n<p>Returning to Thoughts for EPOC<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>&mdash;there are a lot of little bugs to fix and tiny improvements I&rsquo;d like to make:</p> \n<ul> \n <li>Editor doesn’t correctly open new notes on the Revo</li> \n <li>Create the destination directory if it doesn’t exist</li> \n <li>Build a <code>.app</code> instead of a <code>.opo</code></li> \n <li>Adopt the constants in Const.oph for key event handling</li> \n <li>Render a 90s style splash screen</li> \n <li>Create an installer</li> \n <li>Design an app icon</li> \n <li>Support destination directory customization</li> \n <li>Add a toolbar for quick actions</li> \n <li>Add a toolbar for quick actionsDesign toolbar icons</li> \n <li>Document the dependencies and build process in the project readme</li> \n</ul> \n<p>Thankfully, it&rsquo;s a heck of a lot more relaxing than working on a large Swift codebase like InContext, so I just started working my way through them.</p> \n<h2><a id=\"launching-editor\"></a>Launching Editor</h2> \n<p>Remembering back to <a href=\"/posts/2024-12-07-december-adventure-day-06/\">day 06</a>, I was seeing issues opening the newly created notes in Editor on the Revo—it was just opening the last file I&rsquo;d been editing. It turns out this was a side effect of using a more recent version of Editor (1.41) that revealed a bug in the way I was calling <code>RUNAPP&amp;:</code>. I was passing the command <code>2</code> (run), instead of <code>0</code> (open), which was causing Editor to ignore the document parameter and open the most recent document.</p> \n<p>I wish there were constants for this (Const.oph contains a constant for <code>KCmdLetterCreate$</code> which is used by <code>CMD$:</code>, but no equivalent for <code>RUNAPP&amp;:</code>), but at least I have a fix (<a href=\"https://github.com/jbmorley/thoughts-lite/pull/3/files\">#3</a>):</p> \n<pre><code class=\"language-plaintext\">k&amp; = RUNAPP&amp;:(\"Editor\", path$, \"\", 0)\n</code></pre> \n<p>A happy side-effect of using this later version of Editor is that it also comes in color for the Series 7:</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-09-december-adventure-day-08/editor-color/400.png\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-09-december-adventure-day-08/editor-color/400.png 400 300,/posts/2024-12-09-december-adventure-day-08/editor-color/800.png 640 480,/posts/2024-12-09-december-adventure-day-08/editor-color/1200.png 640 480,/posts/2024-12-09-december-adventure-day-08/editor-color/1600.png 640 480,\" />  \n </body></p> \n<p class=\"caption\">Look at those rich 256-color icons!</p> \n<h2><a id=\"create-the-destination-directory\"></a>Create the Destination Directory</h2> \n<p>Nice and simple. Check if the directory exists and, if it doesn&rsquo;t, create it (<a href=\"https://github.com/jbmorley/thoughts-lite/pull/4\">#4</a>):</p> \n<pre><code class=\"language-plaintext\">REM Ensure the destination directory exists.\nIF NOT EXIST(directory$)\n    MKDIR directory$\nENDIF\n</code></pre> \n<p>I&rsquo;ll make this configurable later on.</p> \n<h2><a id=\"build-an-app\"></a>Build an App</h2> \n<p>OPL programs are compiled to &lsquo;executable&rsquo; <code>.opo</code> files by default. These don&rsquo;t have an icon, and don&rsquo;t appear in the Extras bar&mdash;for this to happen, they need to be built as a <code>.app</code> with a corresponding <code>.aif</code> resource file which contains the app caption, unique identifier, and icon. Fortunately, and much to my surprise as I&rsquo;ve forgotten most of my 90s-era OPL, achieving this simply a matter of adding a declaration to the top of your OPL file (<a href=\"https://github.com/jbmorley/thoughts-lite/pull/7\">#7</a>):</p> \n<pre><code class=\"language-plaintext\">APP Thoughts, &amp;100092ca\n    CAPTION \"Thoughts\", 1\n ENDA\n</code></pre> \n<p>There&rsquo;s a tiny bit of nuance here: OPL apps are identified by globally unique 32-bit integers which were managed centrally by Psion who aren&rsquo;t about to be issuing new ones anytime soon. Thankfully, they allocated them in blocks of 10 and I was issued a block starting at &amp;100092c8 back in the 90s. I&rsquo;ve only used two so far, so &amp;100092ca it is!</p> \n<p>In case you&rsquo;re curious (and for my own reference), my current programs are as follows:</p> \n<table> \n <thead> \n  <tr> \n   <th>UID</th> \n   <th>Program</th> \n   <th>Description</th> \n  </tr> \n </thead> \n <tbody> \n  <tr> \n   <td>0x100092c8</td> \n   <td>nEzumi</td> \n   <td>Virtual pet mouse for your Psion.</td> \n  </tr> \n  <tr> \n   <td>0x100092c9</td> \n   <td>Greedy</td> \n   <td>Rebuild of the EPOC16 game Greedy by Frédéric Botton for EPOC32.</td> \n  </tr> \n  <tr> \n   <td>0x100092ca</td> \n   <td>Thoughts</td> \n   <td>Thoughts!</td> \n  </tr> \n </tbody> \n</table> \n<p>The only small wrinkle is that <a href=\"https://opolua.org\">OpoLua</a>&rsquo;s compile.lua (which I&rsquo;ve been using for my <a href=\"https://github.com/jbmorley/thoughts-lite/actions\">GitHub Actions CI builds</a>) doesn&rsquo;t support generating <code>.app</code> and <code>.aif</code> files so, while I can still benefit from the &lsquo;compiler&rsquo; checks, I can&rsquo;t automate the full build process (yet).</p> \n<h2><a id=\"constants-for-key-event-handling\"></a>Constants for Key Event Handling</h2> \n<p>As I&rsquo;m slowly getting back into OPL programming and remembering the various conventions, there are occasional refactorings that need to happen to keep everything clean. One of those is to adopt the constants in Const.oph wherever possible. Starting using <code>KEvAType%</code> makes indexing into the <code>ev&amp;</code> array a little clearer and prepares the event processing for more complex behaviors (<a href=\"https://github.com/jbmorley/thoughts-lite/pull/5\">#5</a>):</p> \n<pre><code class=\"language-plaintext\">WHILE 1\n    GETEVENT32 ev&amp;()\n    IF ev&amp;(KEvAType%) = KKeyMenu% OR ev&amp;(KEvAType%) = KKeySidebarMenu%\n        REM Menu button.\n        menu:\n    ELSEIF ev&amp;(KEvAType%) = %t AND ev&amp;(KEvAKMod%) = KKModFn%\n        REM Global hotkey (Fn + T)\n        new:\n    ELSEIF ev&amp;(KEvAType%) &lt; 32\n        REM Key codes are modified if control is pressed.\n        k&amp; = ev&amp;(KEvAType%) + %a - 1\n        IF k&amp; = %n\n            new:\n        ELSEIF k&amp; = %e AND ev&amp;(KEvAKMod%) = KKModControl%\n            close:\n        ELSEIF k&amp; = %k AND ev&amp;(KEvAKMod%) = KKModControl%\n            clear:\n        ELSEIF k&amp; = %a AND ev&amp;(KEvAKMod%) = (KKModControl% OR KKModShift%)\n            about:\n        ELSE\n            PRINT k&amp;\n        ENDIF\n    ELSEIF (ev&amp;(KEvAType%) AND &amp;400) = 0\n        PRINT ev&amp;(1), ev&amp;(2), ev&amp;(3), ev&amp;(4), ev&amp;(5)\n    ENDIF\nENDWH\n</code></pre> \n<hr /> \n<p>Well, that&rsquo;s me done for the day and I can update my on-going Thoughts todo list:</p> \n<ul> \n <li><del>Editor doesn’t correctly open new notes on the Revo</del></li> \n <li><del>Create the destination directory if it doesn’t exist</del></li> \n <li><del>Build a <code>.app</code> instead of a <code>.opo</code></del></li> \n <li><del>Adopt the constants in Const.oph for key event handling</del></li> \n <li>Render a 90s style splash screen</li> \n <li>Create an installer</li> \n <li>Design an app icon</li> \n <li>Support destination directory customization</li> \n <li>Add a toolbar for quick actions</li> \n <li>Add a toolbar for quick actionsDesign toolbar icons</li> \n <li>Document the dependencies and build process in the project readme</li> \n</ul> \n<p>I also added one new task:</p> \n<ul> \n <li>Automate <code>.app</code> and <code>.aif</code> builds</li> \n</ul> \n<p>Still a bunch of things to keep me occupied this month<sup id=\"fnref2\"><a href=\"#fn2\" rel=\"footnote\">2</a></sup> 🥲</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>Thoughts Lite? Thoughts, EPOC Edition? I really don&rsquo;t know what I&rsquo;m calling this.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n  <li id=\"fn2\"> <p>If you enjoy creating retro graphics and would like to join me on this little adventure, I&rsquo;d <a href=\"mailto:hello@jbmorley.co.uk\">love your help</a>!&nbsp;<a href=\"#fnref2\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2024-12-09T13:57:21-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-09-december-adventure-day-08/",
      "title": "December Adventure Day 08",
      "url": "https://jbmorley.co.uk/posts/2024-12-09-december-adventure-day-08/"
    },
    {
      "content_html": "<p>When I started the day&mdash;day 07 of my <a href=\"/tags/#december-adventure\">December Adventure</a>&mdash;I anticipated spending it working on some period-correct graphics for my OPL-based version of <a href=\"https://github.com/jbmorley/thoughts-lite\">Thoughts for EPOC</a>. Little did I know that, waiting in my inbox, would be an email from Jon bestowing upon me a side-quest: the mystery of the missing images! It&rsquo;s proving pretty involved, so I expect it to span multiple days.</p> \n<p>Jon begins:</p> \n<blockquote> \n <p>I just wanted to ping to say how much I&rsquo;m enjoying your adventure days, and to beg you for images of the things you&rsquo;re building.</p> \n</blockquote> \n<p>Thank you.</p> \n<blockquote> \n <p>Then I checked your website and lo and behold, there are images. I wonder if you know that they are absent on the feed?</p> \n</blockquote> \n<p>Well, balls. I&rsquo;m ashamed to say to say that, yes, I do know they&rsquo;re absent from the feed. In fact, they&rsquo;ve been missing ever since I implemented a <em>clever</em> approach to trying to make my website more friendly to low bandwidth connections and browsers without JavaScript. 🤦🏻‍♂️</p> \n<p>He continues:</p> \n<blockquote> \n <p>I looked at the feed and it does have the image reference but it&rsquo;s enshrouded in mystery:</p> \n <pre><code class=\"language-html\">&lt;x-image width=\"480.0\" height=\"160.0\"&gt;\n &lt;x-source width=\"480\" height=\"160\" src=\"/posts/2024-12-07-december-adventure-day-06/revo/1600.png\"&gt;&lt;/x-source&gt;\n &lt;x-source width=\"480\" height=\"160\" src=\"/posts/2024-12-07-december-adventure-day-06/revo/1200.png\"&gt;&lt;/x-source&gt;\n &lt;x-source width=\"480\" height=\"160\" src=\"/posts/2024-12-07-december-adventure-day-06/revo/800.png\"&gt;&lt;/x-source&gt;\n &lt;x-source width=\"400\" height=\"133\" src=\"/posts/2024-12-07-december-adventure-day-06/revo/400.png\"&gt;&lt;/x-source&gt;\n &lt;noscript&gt;\n     &lt;img width=\"400\" height=\"133\" src=\"/posts/2024-12-07-december-adventure-day-06/revo/400.png\" /&gt;\n &lt;/noscript&gt;\n&lt;/x-image&gt;\n</code></pre> \n <p>The mystery being the <code>&lt;x-source&gt;</code> stuff which didn&rsquo;t exist when I was growing up :-), and the weird <code>&lt;body&gt;</code> tag (should that be there) and the <code>&lt;img&gt;</code> tag, the one that contains the gem, being in a <code>&lt;noscript&gt;</code> thingie.</p> \n</blockquote> \n<p>It makes complete sense that&rsquo;s a mystery: <code>x-image</code> is a custom HTML tag that wrote for my website and, as far as I can see, something I never wrote about or documented. Perhaps it&rsquo;s time for that to change&mdash;<code>x-image</code> represents the end (or so I thought) of a pretty deep dive into the behavior of the different HTML image tags and something I&rsquo;d appreciate feedback on.</p> \n<p>The real issue here appears to be that the <code>noscript</code> that&rsquo;s meant to ensure feed readers (and browsers without JavaScript) gracefully fall back to displaying a small image isn&rsquo;t working.</p> \n<p>Oh, and yes, there shouldn&rsquo;t be any <code>&lt;body&gt;</code> tags int the <code>content_html</code> of my JSON Feed.</p> \n<blockquote> \n <p>Anyway, I hope that helps, and please carry on!</p> \n</blockquote> \n<p>It does indeed. Thank you. I shall.</p> \n<h1><a id=\"some-background\"></a>Some Background</h1> \n<p>This website is built using <a href=\"https://incontext.jbmorley.co.uk\">InContext</a>&mdash;my own static website builder. I started working on InContext in 2015 when the myriad static site builders we have these days were significantly less mature. The primary goal was to create a builder that would treat media files (photos, videos, audio, STL models, etc) as first class citizens rather than as an after thought to be thrown in an asset directory<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>; I wanted to break my dependence on mercurial companies like Google, Apple, and Flickr for hosting my photo library.</p> \n<p>Having built pretty much every part of the pipeline that takes my writings and puts them in a web browser (or in this case a feed reader) gives me a lot of control, but also means there&rsquo;s a lot of places things can go wrong.</p> \n<h1><a id=\"starting-small\"></a>Starting Small</h1> \n<p>I decided to start with the errant <code>body</code> tag. This seemed like a red herring so I wanted to eliminate it as a possibility. On closer inspection, it turns out that <em>all</em> inline HTML content was being wrapped with unnecessary <code>html</code> and <code>body</code> tags.</p> \n<p>InContext uses a Lua-based templating language called <a href=\"https://github.com/tomsci/tomscis-lua-templater\">Tilt</a><sup id=\"fnref2\"><a href=\"#fn2\" rel=\"footnote\">2</a></sup> which calls back into Swift to pull content and query documents. A simple page template looks like this:</p> \n<pre><code class=\"language-html\">&lt;html&gt;\n    &lt;head&gt;\n        &lt;title&gt;{{ incontext.titlecase(document.title) }}&lt;/title&gt;\n    &lt;/head&gt;\n    &lt;body&gt;\n          &lt;h1&gt;{{ incontext.titlecase(document.title) }}&lt;/h1&gt;\n          {{ document.render() }}\n    &lt;/body&gt;\n&lt;/html&gt;\n</code></pre> \n<p>And the JSON Feed is created similarly, with each entry&rsquo;s <code>content_html</code> being generated using the <code>render</code> method:</p> \n<pre><code class=\"language-lua\">for _, post in ipairs(posts) do\n  local item = {\n      id = site.url .. post.url,\n      url = site.url .. post.url\n  }\n  if post.content then\n      do\n          item.content_html = post.render()\n      end\n  end\n  ...\n  table.insert(feed.items, item)\nend\n</code></pre> \n<p>This calls through to the following code in Swift:</p> \n<pre><code class=\"language-swift\">struct DocumentContext: EvaluationContext {\n\n    func html() throws -&gt; String {\n        let content = try SwiftSoup.parse(document.contents)\n        for transform in transforms {\n            try transform.transform(renderTracker: renderTracker, document: self, content: content)\n        }\n        return try content.html()\n    }\n\n}\n</code></pre> \n<p>With a few breakpoints, I was able to determine that somewhere between <code>document.contents</code> and <code>content.html()</code>, outer <code>html</code> and <code>body</code> tags are introduced.</p> \n<p><code>document.contents</code> represents the raw HTML content of the page (converted from Markdown) and this part of the pipeline converts the HTML to a DOM to allow for a collection of lightweight transforms to run to fix-up things like relative paths before returning it to HTML to be included by the templates. It turns out that SwiftSoup <em>always</em> constructs a <code>body</code> irrespective of the input HTML that I need to strip:</p> \n<pre><code class=\"language-swift\">struct DocumentContext: EvaluationContext {\n\n    func html() throws -&gt; String {\n        let content = try SwiftSoup.parseBodyFragment(document.contents)\n        for transform in transforms {\n            try transform.transform(renderTracker: renderTracker, document: self, content: content)\n        }\n        return try content.body()?.html() ?? \"\"\n    }\n\n}\n</code></pre> \n<p>You&rsquo;ll notice I&rsquo;ve also changed the <code>SwiftSoup.parse</code> call to <code>SwiftSoup.parseBodyFragment</code>&mdash;some digging into the documentation revealed that <code>SwiftSoup.parse</code> isn&rsquo;t guaranteed to correctly preserve some tags if the input isn&rsquo;t a full HTML document.</p> \n<p>One mystery solved. 🔎</p> \n<hr /> \n<p>All the moving parts of InContext made progress slow&mdash;it&rsquo;s all a little more nuanced than a starter-OPL program&mdash;so that&rsquo;s all for day 07. I anticipate day 08 will bring more investigations into <code>x-image</code> in preparation for the 90s-era graphics I want to create for Thoughts.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>Things like Hugo&rsquo;s <a href=\"https://gohugo.io/content-management/page-resources/\">Page Resources</a> come pretty close these days but, back in 2015, it just wasn&rsquo;t happening.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n  <li id=\"fn2\"> <p>Thanks <a href=\"https://github.com/tomsci\">Tom</a>!&nbsp;<a href=\"#fnref2\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2024-12-08T11:27:04-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-08-december-adventure-day-07/",
      "title": "December Adventure Day 07",
      "url": "https://jbmorley.co.uk/posts/2024-12-08-december-adventure-day-07/"
    },
    {
      "content_html": "<p>Nearly a week into my <a href=\"/tags/#december-adventure\">December Adventure</a> (happy Friday!) and it feels like I&rsquo;ve a text file full of notes and ideas, and a downloads directory full of reference files and links. To ensure I can stay on top of things as the month progresses, I spent a little time on <a href=\"#thoughts-for-epoc\">Thoughts</a>, but also allowed myself to tie up some loose ends and work on some related side-quests (<a href=\"#roms\">ROMs</a>, <a href=\"#psion.info\">psion.info</a>, and <a href=\"#opolua\">OpoLua</a>) lest they continue to bounce around my head.</p> \n<h1><a id=\"thoughts-for-epoc\"></a>Thoughts for EPOC</h1> \n<p>I took some time out to set up CI builds using GitHub Acitons and <a href=\"https://github.com/inseven/opolua/blob/main/src/compile.lua\">compile.lua</a> from <a href=\"https://opolua.org\">OpoLua</a>. I hit up against a few teething troubles&mdash;compile.lua didn&rsquo;t parse the <code>MENU</code> function correctly—but <a href=\"https://github.com/tomsci\">Tom</a> kindly poked around and fixed a few things. With the fix in place, compiling Thoughts was a simple matter of ensuring my GitHub Actions runner had Lua installed and doing something like:</p> \n<pre><code class=\"language-bash\">COMPILE_PATH=\"$SCRIPTS_DIRECTORY/opolua/src/compile.lua\"\n\nlua \"$COMPILE_PATH\" \"$ROOT_DIRECTORY/Thoughts/Thoughts.opp\" \"$ROOT_DIRECTORY/Thoughts/Thoughts.opo\"\n</code></pre> \n<p>I did encounter a small issue relating to my dependency on SystInfo.opx which seems worth noting here to aid others and for my own future reference. Specifically, the only downloadable version of SystInfo I&rsquo;ve been able to find<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup> includes a <code>.txh</code> header file, not an <code>.oxh</code> and, while nOPL+ is more than happy for me to <code>IMPORT \"SystInfo.txh\"</code>, compile.lua only has hard-coded support for <code>SystInfo.oxh</code>. I worked around this by creating my own <code>SystInfo.oxh</code> by importing the <code>.txh</code> into OPL on the Psion and saving the resulting file (<code>.oxh</code> files are in the EPOC32-native OPL format and <code>.txh</code> files are plaintext). This allows me to <code>IMPORT \"SysInfo.oxh\"</code> which works in both nOPL+ and GitHub Actions. Feels like something we need to improve on in the future.</p> \n<p>I&rsquo;m unsure how the CI will grow as I start to add more source files, resources, and ultimately package everything up as an installer, but it feels like a great start&mdash;perhaps one of the first times anyone&rsquo;s had continuous integration for an OPL program?</p> \n<p>With the administrative bits of build and continuous integration out of the way, I took a little time to add <a href=\"https://github.com/jbmorley/thoughts-lite/pull/2\">global hotkey support</a> so I can quickly compose a new note whatever I&rsquo;m doing. Fortunately, unlike modern pesky security conscious operating systems, System.opx provides an off-the-shelf way of doing this, in the form of <code>CAPTUREKEY&amp;:</code>&mdash;calling this this ensures matching key events are always dispatched to our program&rsquo;s waiting <code>GETEVENT32</code>:</p> \n<pre><code class=\"language-plaintext\">REM Capture the global hotkey.\ncapture&amp; = CAPTUREKEY&amp;:(%t, KModifierFunc&amp;, KModifierFunc&amp;)\n\nWHILE 1\n    GETEVENT32 ev&amp;()\n    k&amp; = ev&amp;(1)\n    IF k&amp; = KKeyMenu% OR k&amp; = KKeySidebarMenu%\n        REM Menu button.\n        menu:\n    ELSEIF k&amp; = %t AND ev&amp;(4) = KKModFn%\n        REM Global hotkey (Fn + T)\n        new:\n    ELSEIF k&amp; &lt; 32\n\n      ...\n\n    ENDIF\nENDWH\n</code></pre> \n<p>Hard coding the global hotkey to Fn + T feels a little ugly so I plan to allow users to customize this in the future but, for the moment, it works! 🎉</p> \n<p>After the last few days, my energy for working on Thoughts was running low, but I did take a few moments to try it out on my Revo. That immediately reminded me that I need an installer as I didn&rsquo;t have SystInfo.opx installed, and that I need to automatically create the directory notes are stored in if it doesn&rsquo;t exist. Still, I was able to get it running after a little manual futzing.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-07-december-adventure-day-06/revo/400.png\" width=\"400\" height=\"133\" x-srcset=\"/posts/2024-12-07-december-adventure-day-06/revo/400.png 400 133,/posts/2024-12-07-december-adventure-day-06/revo/800.png 480 160,/posts/2024-12-07-december-adventure-day-06/revo/1200.png 480 160,/posts/2024-12-07-december-adventure-day-06/revo/1600.png 480 160,\" />  \n </body></p> \n<p class=\"caption\">Everything feels tiny on the Revo after the Series 7</p> \n<p>Unfortunately, while Thoughts happily creates the new note on the file system and correctly writes the metadata, the call to <code>RUNAPP&amp;:</code> seems to loose the path and Editor opens an empty file every time. A problem for another day.</p> \n<h1><a id=\"roms\"></a>ROMs</h1> \n<p>I finally got around to adding George&rsquo;s recent Series 7 ROM dump to <a href=\"https://github.com/explit7/Psion-ROM\">explit7/Psion-ROM</a>.</p> \n<table> \n <thead> \n  <tr> \n   <th>Device</th> \n   <th>EPOC32 Version</th> \n   <th>ROM Version</th> \n   <th>Language</th> \n   <th>Filename</th> \n   <th>MD5 Checksum</th> \n  </tr> \n </thead> \n <tbody> \n  <tr> \n   <td>Psion Series 7</td> \n   <td></td> \n   <td>1.05 (254) (Build 754)</td> \n   <td>English</td> \n   <td>series7<em>1.05</em>254<em>build</em>756.bin</td> \n   <td><code>c78c3cf48d2fd7b8f0d5bc9cadd79159</code></td> \n  </tr> \n </tbody> \n</table> \n<p>It&rsquo;s been really fun to watch efforts to get this ROM booting in emulation over the last week&mdash;clearly early days but exciting stuff!</p> \n<h1><a id=\"psion.info\"></a>psion.info</h1> \n<p>For the past couple of months, I&rsquo;ve been slowly working on a <a href=\"https://gohugo.io\">Hugo</a>-based version of the <a href=\"https://psion.info\">psion.info</a> landing site that <a href=\"https://oldbytes.space/@thelastpsion\">Alex</a> put together. My goal is to be able to offer something up which is a little easier to maintain, that scales well to hosting the wealth of information the <a href=\"https://discord.gg/8ZkKKkA\">Discord</a> community shares, and is easy for others to contribute to. Since I&rsquo;ve been collecting quite a bit of Psion-related documentation on the course of this December Adventure, I thought it might be nice to pause and see how that might fit into this prototype site.</p> \n<p>First up, the Hugo documentation provides a great guide on <a href=\"https://gohugo.io/content-management/data-sources/\">generating tables from CSV files</a> which I was able to pair with <a href=\"https://datatables.net\">DataTables</a> to render out a sortable and searchable table of the ROMs I&rsquo;ve been adding to GitHub this week:</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-07-december-adventure-day-06/psion-info-roms@2x/400.png\" width=\"400\" height=\"350\" x-srcset=\"/posts/2024-12-07-december-adventure-day-06/psion-info-roms@2x/400.png 400 350,/posts/2024-12-07-december-adventure-day-06/psion-info-roms@2x/800.png 800 700,/posts/2024-12-07-december-adventure-day-06/psion-info-roms@2x/1200.png 1200 1050,/posts/2024-12-07-december-adventure-day-06/psion-info-roms@2x/1600.png 1600 1400,\" />  \n </body></p> \n<p>With the appropriate templates and shortcodes added to the site theme, this ends up being a really simple page to create and edit that can hopefully be a helpful resource for people looking to dig into the details of the Psion handhelds and get started with emulation.</p> \n<p>I also knocked up a proof-of-concept for publishing a folder of images, like this collection of Psion marketing images:</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-07-december-adventure-day-06/psion-info-marketing@2x/400.png\" width=\"400\" height=\"350\" x-srcset=\"/posts/2024-12-07-december-adventure-day-06/psion-info-marketing@2x/400.png 400 350,/posts/2024-12-07-december-adventure-day-06/psion-info-marketing@2x/800.png 800 700,/posts/2024-12-07-december-adventure-day-06/psion-info-marketing@2x/1200.png 1200 1050,/posts/2024-12-07-december-adventure-day-06/psion-info-marketing@2x/1600.png 1600 1400,\" />  \n </body></p> \n<p>Even if we don&rsquo;t end up using this new variant of the site, it&rsquo;s proving a really interesting exercise to figuring out how to store and present many years of fairly involved information. I see why Alex is so drawn into the <a href=\"https://doc.psion.info\">Psion Documentation Project</a>.</p> \n<h1><a id=\"opolua\"></a>OpoLua</h1> \n<p>On a roll, and with fresh and newly acquired knowledge of <a href=\"https://datatables.net\">DataTables</a>, I stepped over to <a href=\"https://opolua.org\">OpoLua</a>, our OPL runtime for iOS and macOS. I&rsquo;ve been meaning to simplify how we track <a href=\"https://opolua.org/status/\">what programs do and don&rsquo;t work</a> for some time now as updating an HTML table for every new program is painfully slow. I&rsquo;ve shifted this over to a <a href=\"https://github.com/inseven/opolua/blob/f6e5cb622eb7610fbfbfb35d972c62ae4903389a/docs/_data/status.csv\">CSV file</a> stored in the source tree which we can hopefully add to more easily over time meaning the list will remain current (did I mention we accept <a href=\"https://github.com/inseven/opolua/pulls\">PRs</a>?). This should also make it much easier to use this as a source-of-truth when recommending programs to try out in OpoLua itself.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-07-december-adventure-day-06/opolua-status@2x/400.png\" width=\"400\" height=\"357\" x-srcset=\"/posts/2024-12-07-december-adventure-day-06/opolua-status@2x/400.png 400 357,/posts/2024-12-07-december-adventure-day-06/opolua-status@2x/800.png 800 714,/posts/2024-12-07-december-adventure-day-06/opolua-status@2x/1200.png 1200 1072,/posts/2024-12-07-december-adventure-day-06/opolua-status@2x/1600.png 1600 1429,\" />  \n </body></p> \n<hr /> \n<p>The list of possible directions for this December <del>Adventure</del> Random Walk seems to get longer-and-longer but hopefully I&rsquo;ve managed to tidy things up a bit and I can focus a little more—there&rsquo;s still lots to do to tidy up Thoughts and I&rsquo;ve barely touched on my Psion emulation related ideas. 😮‍💨</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p><a href=\"https://opl-dev.sourceforge.net/OPL-Symbian_OS_v5/downloads/systinfo.zip\">https://opl-dev.sourceforge.net/OPL-Symbian_OS_v5/downloads/systinfo.zip</a>&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2024-12-07T10:57:30-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-07-december-adventure-day-06/",
      "title": "December Adventure Day 06",
      "url": "https://jbmorley.co.uk/posts/2024-12-07-december-adventure-day-06/"
    },
    {
      "content_html": "<p>After the relative successes of the last few days, it&rsquo;s been a day of taking stock in my <a href=\"/tags/#december-adventure\">December Adventure</a> and focusing exclusively on Thoughts&mdash;no ROM uploads today.</p> \n<p>I started the day by creating a GitHub repository for <a href=\"https://github.com/jbmorley/thoughts-lite\">Thoughts</a> and committed the current version. Without native support for git on my Psion<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>, I suspect updates will be a little more sporadic than usual, but I&rsquo;ll try to push each day&rsquo;s additions.</p> \n<p>Having the Thoughts source on my Mac for the first time since starting, I tried running it through Tom&rsquo;s new <a href=\"https://github.com/inseven/opolua/blob/main/src/compiler.lua\">Lua-based OPL compiler</a> and was pleasantly surprised to see it worked perfectly. Perhaps tomorrow I&rsquo;ll set up automated builds using GitHub Actions.</p> \n<p>I also took a little time out over my morning coffee to improve my workflow—switching between running programs is a multi-step process in EPOC32 and I&rsquo;m constantly bouncing back and forth between my source, OPL reference, and Thoughts itself. Thanks to a suggestion by Fabrice, I settled on <a href=\"https://software.psion.info/programs/0x100016e9/\">nSwitcher</a> which offers a <a href=\"https://en.wikipedia.org/wiki/NeXTSTEP\">NeXTSTEP</a>-like dock and the Ctrl + Space global shortcut for cycling between programs.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-06-december-adventure-day-05/nswitcher/400.png\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-06-december-adventure-day-05/nswitcher/400.png 400 300,/posts/2024-12-06-december-adventure-day-05/nswitcher/800.png 640 480,/posts/2024-12-06-december-adventure-day-05/nswitcher/1200.png 640 480,/posts/2024-12-06-december-adventure-day-05/nswitcher/1600.png 640 480,\" />  \n </body></p> \n<p class=\"caption\">nSwitcher makes me feel elite</p> \n<p>With everything a little tidier, and the basic functionality in place, I went about setting up an application framework on which I can build other features&mdash;OPL apps commonly centered around a polling loop, and Thoughts will follow this pattern.</p> \n<p>First up, the <code>main:</code> needs to watch for input events:</p> \n<pre><code class=\"language-opl\">PROC main:\n    GLOBAL CRLF$(2)\n    LOCAL ev&amp;(16), k&amp;\n\n    initglobals:\n    clear:\n\n    WHILE 1\n        GETEVENT32 ev&amp;()\n        k&amp; = ev&amp;(1)\n        IF k&amp; = KKeyMenu% OR k&amp; = KKeySidebarMenu%\n            menu:\n        ELSEIF k&amp; &lt; 32\n            REM Key codes are modified if control is pressed. I don't know why.\n            k&amp; = k&amp; + %a - 1\n            IF k&amp; = %n\n                new:\n            ELSEIF k&amp; = %e AND ev&amp;(4) = KKModControl%\n                close:\n            ELSEIF k&amp; = %k AND ev&amp;(4) = KKModControl%\n                clear:\n            ELSEIF k&amp; = %a AND ev&amp;(4) = (KKModControl% OR KKModShift%)\n                about:\n            ELSE\n                PRINT k&amp;\n            ENDIF\n        ELSEIF (ev&amp;(1) AND &amp;400) = 0\n            PRINT ev&amp;(1), ev&amp;(2), ev&amp;(3), ev&amp;(4), ev&amp;(5)\n        ENDIF\n    ENDWH\n\nENDP\n</code></pre> \n<p>Here, I&rsquo;m using the synchronous, blocking <code>GETEVENT32</code> call to poll for events. If you look at the event processing though, you&rsquo;ll notice some pretty grim manipulation of the key code to handle keyboard shortcuts (things like Ctrl + N, etc). I&rsquo;ve not found a good discussion of why yet, but hopefully I can figure it out over the next few days and update the code with an explanation at the very least; magic numbers make me incredibly nervous.</p> \n<p>The original code is now pushed down into the <code>new:</code> procedure, and I&rsquo;m able to add the other functionality.</p> \n<p>A main menu:</p> \n<pre><code class=\"language-opl\">PROC menu:\n    LOCAL m%\n    \n    mINIT\n    mCARD \"File\", \"New\", -%n, \"Close\", %e\n    mCARD \"View\", \"Clear\", %k\n    mCARD \"Tools\", \"About Thoughts...\", %A\n    m% = MENU\n    \n    IF m% = %n\n        new:\n    ELSEIF m% = %e\n        close:\n    ELSEIF m% = %k\n        clear:\n    ELSEIF m% = %A\n        about:\n    ENDIF\nENDP\n</code></pre> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-06-december-adventure-day-05/menu/400.png\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-06-december-adventure-day-05/menu/400.png 400 300,/posts/2024-12-06-december-adventure-day-05/menu/800.png 640 480,/posts/2024-12-06-december-adventure-day-05/menu/1200.png 640 480,/posts/2024-12-06-december-adventure-day-05/menu/1600.png 640 480,\" />  \n </body></p> \n<p>And an about screen:</p> \n<pre><code class=\"language-opl\">PROC about:\n    dINIT \"About Thoughts\"\n    dTEXT \"\", \"Copyright \" + CHR$(169) + \" Jason Morley 2024\"\n    dBUTTONS \"OK\", 13 + $100\n    DIALOG\nENDP\n</code></pre> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-06-december-adventure-day-05/about/400.png\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-06-december-adventure-day-05/about/400.png 400 300,/posts/2024-12-06-december-adventure-day-05/about/800.png 640 480,/posts/2024-12-06-december-adventure-day-05/about/1200.png 640 480,/posts/2024-12-06-december-adventure-day-05/about/1600.png 640 480,\" />  \n </body></p> \n<p>Thankfully OPL provides dedicated structures and calls for presenting UI like menus and dialogs, making it incredibly easy to create programs with a native look and feel.</p> \n<hr /> \n<p>As Thoughts is starting to take shape, I find myself wanting to add some 90s-era graphics to give it character. I also need to add global hot key support, and build an installable app. Lots to keep me entertained.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>Yes, that would be an interesting side-quest. No, I&rsquo;m not going to try it.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2024-12-06T09:18:48-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-06-december-adventure-day-05/",
      "title": "December Adventure Day 05",
      "url": "https://jbmorley.co.uk/posts/2024-12-06-december-adventure-day-05/"
    },
    {
      "content_html": "<p>It&rsquo;s been a successful fourth day of my <a href=\"/tags/#december-adventure\">December Adventure</a>—there are a few too many threads but I feel like I&rsquo;m making progress: I went on a tiny side-quest to fix the way tables are displayed on my website, <a href=\"#roms\">uploaded</a> a few more ROMs, and continued to <a href=\"#thoughts-lite\">bash my head</a> at OPL.</p> \n<p>Just like yesterday, I find myself incredibly grateful for the on-going interest and support others have given me&mdash;both Fabrice and <a href=\"https://github.com/tomsci\">Tom</a> kept me on-track with my OPL pursuits and <a href=\"https://oldbytes.space/@thelastpsion\">Alex</a> gave me a couple of pointers about the Psion Workabout.</p> \n<h1><a id=\"roms\"></a>ROMs</h1> \n<p>You know the drill at this point: I added some new ROMs to the <a href=\"https://github.com/explit7/Psion-ROM\">Psion-ROM</a> repository—Workabout and Workabout MX this time around.</p> \n<p>The Workabout devices are rugged, industrial handhelds, intended for use in warehouse environments. They run seemingly fully-featured EPOC16 and have absolutely tiny screens making for perhaps the cutest rendition of the operating system I&rsquo;ve seen.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-04-december-adventure-day-04/workabout-screenshot@2x/400.png\" width=\"400\" height=\"249\" x-srcset=\"/posts/2024-12-04-december-adventure-day-04/workabout-screenshot@2x/400.png 400 249,/posts/2024-12-04-december-adventure-day-04/workabout-screenshot@2x/800.png 800 498,/posts/2024-12-04-december-adventure-day-04/workabout-screenshot@2x/1200.png 1200 747,/posts/2024-12-04-december-adventure-day-04/workabout-screenshot@2x/1600.png 1558 970,\" />  \n </body></p> \n<p class=\"caption\">EPOC in miniature</p> \n<h2><a id=\"psion-workabout\"></a>Psion Workabout</h2> \n<table> \n <thead> \n  <tr> \n   <th>Device</th> \n   <th>EPOC16 Version</th> \n   <th>ROM Version</th> \n   <th>Language</th> \n   <th>Filename</th> \n   <th>MD5 Checksum</th> \n  </tr> \n </thead> \n <tbody> \n  <tr> \n   <td>Psion Workabout</td> \n   <td>3.56f</td> \n   <td>0.24b</td> \n   <td>English</td> \n   <td>w1_v0.24b.bin</td> \n   <td><code>1afac14fe87e19e7d29d494177dc58d9</code></td> \n  </tr> \n  <tr> \n   <td>Psion Workabout</td> \n   <td>3.56f</td> \n   <td>1.00f</td> \n   <td>English</td> \n   <td>w1_v1.00f.bin</td> \n   <td><code>87c84a27bc71df5e19ac1208735a7a1e</code></td> \n  </tr> \n  <tr> \n   <td>Psion Workabout</td> \n   <td>3.96f</td> \n   <td>2.40f</td> \n   <td>English</td> \n   <td>w1_v2.40f.bin</td> \n   <td><code>10b9a0c9174aec0316571827dce42013</code></td> \n  </tr> \n </tbody> \n</table> \n<h2><a id=\"psion-workabout-mx\"></a>Psion Workabout MX</h2> \n<table> \n <thead> \n  <tr> \n   <th>Device</th> \n   <th>EPOC16 Version</th> \n   <th>ROM Version</th> \n   <th>Language</th> \n   <th>Filename</th> \n   <th>MD5 Checksum</th> \n  </tr> \n </thead> \n <tbody> \n  <tr> \n   <td>Psion Workabout MX</td> \n   <td>4.31f</td> \n   <td>7.20f</td> \n   <td>English</td> \n   <td>w2mx_v7.20f.bin</td> \n   <td><code>d5e5c2aa32f9888e7fec8d2214f1547e</code></td> \n  </tr> \n </tbody> \n</table> \n<p>Unfortunately I didn&rsquo;t get around to adding the new Series 7 ROM dump to the repository. Tomorrow, I hope. 🤞🏻</p> \n<h1><a id=\"thoughts-lite\"></a>Thoughts-Lite</h1> \n<p>Thanks to input from both Tom and Fabrice, the EPOC version of Thoughts feels like it made massive progress today as I was finally able to put everything I&rsquo;ve worked on over the last few days into an end-to-end demo.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-04-december-adventure-day-04/thoughts-files/400.png\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-04-december-adventure-day-04/thoughts-files/400.png 400 300,/posts/2024-12-04-december-adventure-day-04/thoughts-files/800.png 640 480,/posts/2024-12-04-december-adventure-day-04/thoughts-files/1200.png 640 480,/posts/2024-12-04-december-adventure-day-04/thoughts-files/1600.png 640 480,\" />  \n </body></p> \n<p class=\"caption\">Timestamped files generated using Thoughts</p> \n<p>I ended yesterday stuck trying to apply the long representation of the UTC offset returned by <code>SIUTCOffset&amp;:</code> to the date/time returned by <code>DTNOW&amp;:</code>. I had incorrectly assumed that the date/time would be conveniently represented as seconds since epoch (1970-01-01) allowing me to simply subtract the UTC offset. Instead, it turns out all Date.opx operations (functions prefixed by <code>DT</code>) return an opaque pointer to an underlying date/time object which you can only interact with through dedicated functions.</p> \n<p>Consider the following code:</p> \n<pre><code class=\"language-opl\">timestamp&amp;=DTNOW&amp;:\noffset&amp;=SIUTCOffset&amp;:\nyear&amp;=DTYEAR&amp;:(timestamp&amp;-offset&amp;)\n</code></pre> \n<p>This fails because, although <code>timestamp&amp;</code> is a long, it is actually a pointer, so subtracting <code>offset&amp;</code> results in a pointer to goodness knows where and the call to <code>DTYEAR&amp;:</code> ends up operating on random memory.</p> \n<p>Finding a suitable API to work around this proved challenging. Out of the box, OPL provides the following functions:</p> \n<ul> \n <li><code>DATETOSECS</code>&mdash;converts a collection of date and time components to a timestamp represented as seconds since epoch</li> \n <li><code>SECSTODATE</code>&mdash;extracts the date and time components from a timestamp represented as seconds since epoch</li> \n</ul> \n<p>These both look incredibly helpful and would seem to imply there&rsquo;s an easy way to get the current time as seconds since epoch. Unfortunately that&rsquo;s not the case: the language provides <code>DATIM$</code> which, &lsquo;returns the current date and time from the system clock as a string&rsquo;, but seemingly nothing that returns seconds since epoch. In fact, it looks like the best you can do with the native OPL API is:</p> \n<pre><code class=\"language-opl\">LOCAL timestamp&amp;\ntimestamp&amp; = DATETOSECS(YEAR, MONTH, DAY, HOUR, MINUTE, SECOND)\n</code></pre> \n<p>While it might not be immediately obvious, <code>YEAR</code>, <code>MONTH</code>, <code>DAY</code>, <code>HOUR</code>, <code>MINUTE</code>, and <code>SECOND</code> are all function calls, meaning this code is incredibly susceptible to race conditions and is likely to lead to some wacky results—consider what happens, for example, if the minute rolls-over before the <code>SECONDS</code> call is performed.</p> \n<p>Instead of using this mildly terrifying approach, and thanks to a suggestion from Tom, I settled on using <code>DTSECSDIFF&amp;:</code> from Date.opx which allows me to calculate the number of seconds between two dates:</p> \n<pre><code class=\"language-opl\">PROC NOW&amp;:\n    REM Return the current time as seconds since epoch.\n    LOCAL epoch&amp;, now&amp;, result&amp;\n\n    epoch&amp;=DTNEWDATETIME&amp;:(1970,1,1,0,0,0,0)\n    now&amp;=DTNOW&amp;:\n    result&amp;=DTSECSDIFF&amp;:(epoch&amp;,now&amp;)\n    DTDELETEDATETIME:(epoch&amp;)\n    DTDELETEDATETIME:(now&amp;)\n\n    RETURN result&amp;\nENDP\n</code></pre> \n<p>I have to manually define <code>epoch&amp;</code>, but this provides a robust way to get the current date/time as seconds since epoch and allows me to avoid Date.opx&rsquo;s quirky opaque date/time objects wherever possible.</p> \n<p>This approach necessitates an update to <code>ISO8601$:</code> which is (fortunately) significantly simplified as I can now take advantage of <code>SECSTODATE</code>:</p> \n<pre><code class=\"language-opl\">PROC ISO8601$:(timestamp&amp;, utcOffset&amp;)\n    REM Return an ISO 8601 formatted date and time with UTC offset.\n    REM Result is guaranteed to be 25 characters long.\n\n    LOCAL year%, month%, day%, hour%, minute%, second%, yearday%\n    LOCAL offsetSign$(1), offsetHours&amp;, offsetMinutes&amp;\n\n    REM Extract the timestamp components.\n    SECSTODATE timestamp&amp;, year%, month%, day%, hour%, minute%, second%, yearday%\n\n    REM Extract the offset components.\n    offsetHours&amp; = IABS(utcOffset&amp; / 3600)\n    offsetMinutes&amp; = MOD&amp;:(utcOffset&amp; / 60, 60)\n    IF (utcOffset&amp; &lt; 0)\n        offsetSign$=\"-\"\n    ELSE\n        offsetSign$=\"+\"\n    ENDIF\n\n    RETURN NUM$(year%, 4) + \"-\" + ZPAD$:(month%, 2) + \"-\" + ZPAD$:(day%, 2) + \"T\" + ZPAD$:(hour%, 2) + \":\" + ZPAD$:(minute%, 2) + \":\" + ZPAD$:(second%, 2) + offsetSign$ + ZPAD$:(offsetHours&amp;, 2) + \":\" + ZPAD$:(offsetMinutes&amp;, 2)\nENDP\n</code></pre> \n<p>It also allows me to generate my UTC filenames in a very similar way and just apply the UTC offset by subtracting it:</p> \n<pre><code class=\"language-opl\">PROC BASENAME$:(timestamp&amp;, utcOffset&amp;)\n    REM Return a string representation of timestamp&amp; and utcOffset&amp; suitable for using in a filename.\n    REM Result is guaranteed to be 19 characters long.\n    \n    LOCAL utcTimestamp&amp;\n    LOCAL year%, month%, day%, hour%, minute%, second%, yearday%\n\n    utcTimestamp&amp;=timestamp&amp;-utcOffset&amp;\n    SECSTODATE utcTimestamp&amp;, year%, month%, day%, hour%, minute%, second%, yearday%\n\n    RETURN NUM$(year%, 4) + \"-\" + ZPAD$:(month%, 2) + \"-\" + ZPAD$:(day%, 2) + \"-\" + ZPAD$:(hour%, 2) + \"-\" + ZPAD$:(minute%, 2) + \"-\" + ZPAD$:(second%, 2)\nENDP\n</code></pre> \n<p>With all this nuanced code in place, actually assembling a Frontmatter header, writing it to a timestamped file, and launching Editor&mdash;the bulk of the functionality&mdash;seemed oddly simple (albeit a little bloated):</p> \n<pre><code class=\"language-opl\">PROC main:\n    LOCAL k&amp;\n    LOCAL timestamp&amp;, offset&amp;, date$(25)\n    LOCAL basename$(22), path$(255)\n    LOCAL content$(255)\n    LOCAL handle%\n    LOCAL r%\n\n    REM Get the current timestamp and offset.\n    timestamp&amp;=NOW&amp;:\n    offset&amp;=SIUTCOffset&amp;:\n\n    REM Format the date, path, and content.\n    date$ = ISO8601$:(timestamp&amp;, offset&amp;)\n    basename$ = BASENAME$:(timestamp&amp;, offset&amp;)\n    path$ = \"C:\\Thoughts\\\" + basename$ + \".md\"\n    content$ = \"---\" + CRLF$: + \"date: \" + date$ + CRLF$: + \"---\" + REPT$(CRLF$:, 2)\n\n    REM Write the metadata to the file.\n    r% = IOOPEN(handle%, path$, 1)\n    IF (r% &lt;&gt; 0)\n        REM TODO: Present the error.\n        PRINT \"Failed to open file with error \" + NUM$(r%, 2) + \".\"\n        GET\n        RETURN\n    ENDIF\n    REM TODO: Check the number of bytes written.\n    IOWRITE(handle%, ADDR(content$) + 1, LEN(content$))\n    r% = IOCLOSE(handle%)\n    IF (r% &lt;&gt; 0)\n        REM TODO: Present the error.\n        PRINT \"Failed to close file with error \" + NUM$(r%,2) + \".\"\n        GET\n        RETURN\n    ENDIF\n\n    REM Open the file.\n    k&amp; = RUNAPP&amp;:(\"Editor\", path$, \"\", 2)\n\n    REM GET\nENDP\n</code></pre> \n<p>Now, running my newly translated &lsquo;Thoughts.opo&rsquo; will open a new timestamped file in Editor, ready for me to type my notes:</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-04-december-adventure-day-04/thoughts-new-document/400.png\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-04-december-adventure-day-04/thoughts-new-document/400.png 400 300,/posts/2024-12-04-december-adventure-day-04/thoughts-new-document/800.png 640 480,/posts/2024-12-04-december-adventure-day-04/thoughts-new-document/1200.png 640 480,/posts/2024-12-04-december-adventure-day-04/thoughts-new-document/1600.png 640 480,\" />  \n </body></p> \n<p>Tomorrow, I plan to add support for launching Thoughts from one of the Series 7&rsquo;s silkscreen buttons and implementing a global hotkey for devices like the Series 5 and Revo. I&rsquo;d also like to check this initial version of the project into git so others can follow along more easily.</p>",
      "date_published": "2024-12-05T00:30:54-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-04-december-adventure-day-04/",
      "title": "December Adventure Day 04",
      "url": "https://jbmorley.co.uk/posts/2024-12-04-december-adventure-day-04/"
    },
    {
      "content_html": "<p>Continuing my Psion-themed <a href=\"/tags/#december-adventure\">December Adventure</a>, I returned to Sunday&rsquo;s sorting of <a href=\"#roms\">ROMs</a>, and chipped away at the OPL take on <a href=\"#thoughts-lite\">Thoughts</a> I started <a href=\"/posts/2024-12-03-december-adventure-day-02/\">yesterday</a>. I&rsquo;m also rapidly learning that one of the advantages of slowing down a little and writing up my daily adventures is getting to share them with others and, quite wonderfully, receiving realtime feedback, suggestions and contributions.</p> \n<h1><a id=\"roms\"></a>ROMs</h1> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-03-december-adventure-day-03/emulators@2x/400.png\" width=\"400\" height=\"250\" x-srcset=\"/posts/2024-12-03-december-adventure-day-03/emulators@2x/400.png 400 250,/posts/2024-12-03-december-adventure-day-03/emulators@2x/800.png 800 500,/posts/2024-12-03-december-adventure-day-03/emulators@2x/1200.png 1200 750,/posts/2024-12-03-december-adventure-day-03/emulators@2x/1600.png 1600 1000,\" />  \n </body></p> \n<p>I spent some time adding more Psion ROMs to <a href=\"https://github.com/explit7/Psion-ROM\">explit7/Psion-ROM</a>&mdash;Sienna, Series 3c, and Series 3mx this time around. Thanks to feedback from <a href=\"https://oldbytes.space/@thelastpsion\">Alex</a>, I&rsquo;ve also added MD5 checksums and EPOC16 versions (the latter really helps differentiate between device variants and language variants). There are still a few gaps in the table as I&rsquo;ve yet to find for myself where the EPOC16 version comes from but hopefully I can flesh that out over the next couple of days.</p> \n<p>The additions are as follows:</p> \n<h2><a id=\"psion-siena\"></a>Psion Siena</h2> \n<table> \n <thead> \n  <tr> \n   <th>Device</th> \n   <th>EPOC16 Version</th> \n   <th>ROM Version</th> \n   <th>Language</th> \n   <th>Filename</th> \n   <th>MD5 Checksum</th> \n  </tr> \n </thead> \n <tbody> \n  <tr> \n   <td>Psion Siena</td> \n   <td>3.70f</td> \n   <td>4.20f</td> \n   <td>English</td> \n   <td>vine_v4.20f.bin</td> \n   <td><code>242e80fdbf9b353a05f6ff4d1db1c769</code></td> \n  </tr> \n </tbody> \n</table> \n<h2><a id=\"psion-series-3c\"></a>Psion Series 3c</h2> \n<table> \n <thead> \n  <tr> \n   <th>Device</th> \n   <th>EPOC16 Version</th> \n   <th>ROM Version</th> \n   <th>Language</th> \n   <th>Filename</th> \n   <th>MD5 Checksum</th> \n  </tr> \n </thead> \n <tbody> \n  <tr> \n   <td>Psion Series 3c</td> \n   <td>3.91f</td> \n   <td>5.20f</td> \n   <td>English</td> \n   <td>oak_v5.20f_eng.bin</td> \n   <td><code>3c1a079f53c00916e8d0dc11b35a0390</code></td> \n  </tr> \n </tbody> \n</table> \n<h2><a id=\"psion-series-3mx\"></a>Psion Series 3mx</h2> \n<table> \n <thead> \n  <tr> \n   <th>Device</th> \n   <th>EPOC16 Version</th> \n   <th>ROM Version</th> \n   <th>Language</th> \n   <th>Filename</th> \n   <th>MD5 Checksum</th> \n  </tr> \n </thead> \n <tbody> \n  <tr> \n   <td>Psion Series 3mx</td> \n   <td>4.08f</td> \n   <td>6.16f</td> \n   <td>English</td> \n   <td>maple_v6.16f_uk.bin</td> \n   <td><code>64572cc3522447179d1e6f3b8fb45360</code></td> \n  </tr> \n  <tr> \n   <td>Psion Series 3mx</td> \n   <td></td> \n   <td>6.20f</td> \n   <td>French</td> \n   <td>maple_v6.20f_fre.bin</td> \n   <td><code>1b367e2fb862545cf420ff74a7f85ea7</code></td> \n  </tr> \n </tbody> \n</table> \n<p>Looking over the listings of ROMs have been dumped for these devices, it&rsquo;s abundantly clear that we&rsquo;re missing many localizations&mdash;I understand we know of at least English, French, German, Italian, Flemish, and Dutch variants of the 3c, so we&rsquo;ve a long way to go. If you have a non-English version of these devices, please get in touch by dropping me a <a href=\"mailto:hello@jbmorley.co.uk\">mail</a> or joining us in <a href=\"https://discord.gg/8ZkKKkA\">Discord</a>. We&rsquo;d absolutely love to talk you through the process and would be eternally grateful!</p> \n<p><em>As an aside, I love how the ROM files use the device codenames and am tempted to surface those as an explicit column. I&rsquo;ve also just learned that we have a newly minted Series 7 ROM dump, so that&rsquo;s on the list for tomorrow.</em></p> \n<h1><a id=\"thoughts-lite\"></a>Thoughts-Lite</h1> \n<p>Fabrice Cappaert spotted a bug in my <code>ZPAD</code> implementation (see <a href=\"/posts/2024-12-03-december-adventure-day-02/\">yesterday&rsquo;s notes</a>)&mdash;I was using an <code>IF</code> instead of a <code>WHILE</code>-loop when checking to see if my target string was long enough, meaning it would never have worked for pads longer than 1. (Just goes to show that even for small programs, it&rsquo;s worth unit testing things.) Thankfully, he also offered up a significant simplification which we both suspect will work more efficiently given the costs of certain operations in OPL:</p> \n<pre><code class=\"language-opl\">PROC ZPAD$:(value&amp;,length%)\n    REM Left-pad strings with '0' up to a length, length%.\n    RETURN RIGHT$(REPT$(\"0\",length%)+NUM$(value&amp;,length%),length%)\nENDP\n</code></pre> \n<p>This approach also allows me to avoid using a local fixed-size string which makes it far more flexible than the previous implementation which implicitly limited <code>length%</code> to 10.</p> \n<p>Perhaps most-crucially however, was Fabrice&rsquo;s help with OPL time zone wrangling: the macOS version of Thoughts stores the UTC offset in the ISO 8601 timestamp and this is functionality I really want to preserve in the OPL version<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>. Yesterday, I was starting to get a little nervous about my ability to implement this as I couldn&rsquo;t find built-in functions to get the user&rsquo;s UTC offset, even though it&rsquo;s clear from the World app that the Psion knows where I am and my time relative to UTC.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-03-december-adventure-day-03/world-honolulu/400.png\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-03-december-adventure-day-03/world-honolulu/400.png 400 300,/posts/2024-12-03-december-adventure-day-03/world-honolulu/800.png 640 480,/posts/2024-12-03-december-adventure-day-03/world-honolulu/1200.png 640 480,/posts/2024-12-03-december-adventure-day-03/world-honolulu/1600.png 640 480,\" />  \n </body></p> \n<p class=\"caption\">My Series 7 clearly knows I live in Hawaii</p> \n<p>Thankfully, Fabrice pointed me at one of the first-party after-market native libraries&mdash;SystInfo.opx&mdash;which provides <code>SIUTCOffset&amp;:</code>. This returns the UTC offset in seconds and, a few calls to <code>MOD</code> and <code>IABS</code> later, I was able add the UTC offset to my ISO 8601 formatted date:</p> \n<pre><code class=\"language-opl\">PROC ISO8601$:(datetime&amp;,utcOffset&amp;)\n    REM Return an ISO 8601 formatted date and time with UTC offset.\n    REM Result is guaranteed to be 25 characters long.\n\n    LOCAL year&amp;, month&amp;, day&amp;, hour&amp;, minute&amp;, second&amp;\n    LOCAL offsetSign$(1), offsetHours&amp;, offsetMinutes&amp;\n    LOCAL result$(25)\n\n    REM Extract the datetime components.\n    year&amp;=DTYEAR&amp;:(datetime&amp;)\n    month&amp;=DTMONTH&amp;:(datetime&amp;)\n    day&amp;=DTDAY&amp;:(datetime&amp;)\n    hour&amp;=DTHOUR&amp;:(datetime&amp;)\n    minute&amp;=DTMINUTE&amp;:(datetime&amp;)\n    second&amp;=DTSECOND&amp;:(datetime&amp;)\n\n    REM Extract the offset components.\n    offsetHours&amp;=IABS(utcOffset&amp;/3600)\n    offsetMinutes&amp;=MOD&amp;:(utcOffset&amp;/60,60)\n    IF (utcOffset&amp; &lt; 0)\n        offsetSign$=\"-\"\n    ELSE\n        offsetSign$=\"+\"\n    ENDIF\n\n    RETURN NUM$(year&amp;,4) + \"-\" + ZPAD$:(month&amp;,2) + \"-\" + ZPAD$:(day&amp;,2) + \"T\" + ZPAD$:(hour&amp;,2) + \":\" + ZPAD$:(minute&amp;,2) + \":\" + ZPAD$:(second&amp;,2) + offsetSign$ + ZPAD$:(offsetHours&amp;,2) + \":\" + ZPAD$:(offsetMinutes&amp;,2)\nENDP\n</code></pre> \n<p class=\"caption\">It&rsquo;s not pretty, but it works</p> \n<p>It&rsquo;s not all been plain sailing though. In addition to storing an ISO 8601 date and time <em>with</em> a UTC offset in the note metadata, I wish to name the file using a UTC date and time to ensure they sort correctly on the file system. For example, a note published at <code>2024-12-02T16:05:06-10:00</code> should be named <code>2024-12-03-02-05-06.md</code>. While I had expected this to be simple (subtract <code>utcOffset%</code> from <code>datetime&amp;</code> and generate a string similarly to the code above), it seems OPL date/times are a little bit magical.</p> \n<p>Consider the following code:</p> \n<pre><code class=\"language-opl\">LOCAL timestamp&amp;, offset&amp;, utcTimestamp&amp;\nLOCAL year&amp;\n\nREM Get the current timestamp and offset.\ntimestamp&amp;=DTNOW&amp;:\noffset&amp;=SIUTCOffset&amp;:\n\nREM Calculate the UTC timestamp.\nutcTimestamp&amp;=timestamp&amp;-offset&amp;\n\nREM Extract the datetime components.\nyear&amp;=DTYEAR&amp;:(utcDatetime&amp;)\n</code></pre> \n<p>While you might expect this to work, it fails with &lsquo;Invalid arguments&rsquo; as <code>utcTimestamp&amp;</code> is apparently no longer a valid date/time, even though both <code>timestamp&amp;</code> and <code>utcTimestamp&amp;</code> both behave like longs when passed to many functions. I can only assume they&rsquo;re backed by a magical object under the hood. Hopefully I can find a way to get the UTC timestamp directly, or initialize a new date/time with a long, but that&rsquo;s one for another day.</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>The UTC offset allows notes to be displayed in note-local time, UTC time, or the reader-local time.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2024-12-03T22:35:46-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-03-december-adventure-day-03/",
      "title": "December Adventure Day 03",
      "url": "https://jbmorley.co.uk/posts/2024-12-03-december-adventure-day-03/"
    },
    {
      "content_html": "<p>Despite writing <a href=\"https://opolua.org\">OpoLua</a>, a modern <a href=\"https://en.wikipedia.org/wiki/Open_Programming_Language\">OPL</a> runtime for iOS and macOS, it&rsquo;s a long time since I&rsquo;ve actually written anything in OPL. So what better for a Psion-themed <a href=\"/tags/#december-adventure\">December Adventure</a> than to return to one of the languages that started it all for me?</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-03-december-adventure-day-02/noplplus/400.png\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-03-december-adventure-day-02/noplplus/400.png 400 300,/posts/2024-12-03-december-adventure-day-02/noplplus/800.png 640 480,/posts/2024-12-03-december-adventure-day-02/noplplus/1200.png 640 480,/posts/2024-12-03-december-adventure-day-02/noplplus/1600.png 640 480,\" />  \n </body></p> \n<p class=\"caption\">Remembering how to write OPL using nOPL+</p> \n<p>Earlier this year, I published <a href=\"https://thoughts.jbmorley.co.uk\">Thoughts</a>, a lightweight Markdown-based note taking app for macOS which I use primarily for journaling and I thought it would be nice to bring a couple of the main features to my writing experience on EPOC32&mdash;a kind of &lsquo;Thoughts-lite&rsquo; if you will.</p> \n<p>Thoughts is a simple app offering the following functionality:</p> \n<ul> \n <li>global hotkey for starting new notes</li> \n <li>automatic date and location tagging (stored in Frontmatter)</li> \n <li>Markdown syntax highlighting</li> \n <li>tag editor</li> \n</ul> \n<p>It generates Markdown files that look something like this:</p> \n<pre><code class=\"language-markdown\">---\ndate: '2024-11-26T09:23:55-10:00'\ntags: []\nlocation:\n  latitude: 2.15791762399229e+1\n  longitude: -1.58105437871125e+2\n  name: 61-535 Kamehameha Hwy\n  locality: \"Hale\\u02BBiwa\"\n---\n\nSelecting multiple matching words in Helix can be done using:\n\nmiw*vn\n</code></pre> \n<p>While it might disappoint some members of the Psion community<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>, I&rsquo;ve no plans to tackle a dedicated Markdown editor&mdash;I find the Symbian-published <a href=\"https://software.psion.info/programs/0x1000412b/\">Editor</a> just great for writing&mdash;but I would like to automatically create and timestamp new notes, and pre-populate them with some Frontmatter for filling in tags.</p> \n<p>Starting simple, my initial focus is on generating an <a href=\"https://en.wikipedia.org/wiki/ISO_8601\">IS0 8601</a>-formatted timestamp as used by Thoughts. Unfortunately OPL doesn&rsquo;t make this easy as it&rsquo;s lacking some basics&mdash;I first had to write a way to zero-pad numbers:</p> \n<pre><code class=\"language-opl\">PROC ZPAD$:(value&amp;,length%)\n    REM Left-pad numbers with '0' up to a length, length%, maximum 10.\n    LOCAL result$(10)\n    result$=NUM$(value&amp;,length%)\n    IF LEN(result$) &lt; length%\n        result$ = \"0\" + result$\n    ENDIF\n    RETURN result$\nENDP\n</code></pre> \n<p>With that in place, I was able to approach a procedure to format a datetime as ISO 8601:</p> \n<pre><code class=\"language-opl\">PROC ISO8601$:(datetime&amp;)\n    LOCAL year&amp;, month&amp;, day&amp;, hour&amp;, minute&amp;, second&amp;, result$(25)\n\n    REM Extract the relevant components.\n    year&amp;=DTYEAR&amp;:(datetime&amp;)\n    month&amp;=DTMONTH&amp;:(datetime&amp;)\n    day&amp;=DTDAY&amp;:(datetime&amp;)\n    hour&amp;=DTHOUR&amp;:(datetime&amp;)\n    minute&amp;=DTMINUTE&amp;:(datetime&amp;)\n    second&amp;=DTSECOND&amp;:(datetime&amp;)\n\n    REM Construct the ISO8601 string.\n    result$=NUM$(year&amp;,4) + \"-\" + ZPAD$:(month&amp;,2) + \"-\" + ZPAD$:(day&amp;,2) + \"T\" + ZPAD$:(hour&amp;,2) + \":\" + ZPAD$:(minute&amp;,2) + \":\" + ZPAD$:(second&amp;,2)\n\n    RETURN result$\nENDP\n</code></pre> \n<p>During this process, I found myself relying heavily on <a href=\"https://software.psion.info/programs/0x10000544/\">nOPL+</a>, a minimal OPL IDE that, crucially, includes a built-in OPL reference:</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-03-december-adventure-day-02/noplplus-help/400.png\" width=\"400\" height=\"300\" x-srcset=\"/posts/2024-12-03-december-adventure-day-02/noplplus-help/400.png 400 300,/posts/2024-12-03-december-adventure-day-02/noplplus-help/800.png 640 480,/posts/2024-12-03-december-adventure-day-02/noplplus-help/1200.png 640 480,/posts/2024-12-03-december-adventure-day-02/noplplus-help/1600.png 640 480,\" />  \n </body></p> \n<p>With my newly-crafted procedure, testing it is simply a matter of getting the current date, calling it with OPL&rsquo;s weird syntax, and printing the output (not forgetting to use <code>GET</code> to wait for the user to press a key lest my program exits before I see what it&rsquo;s done):</p> \n<pre><code class=\"language-opl\">INCLUDE \"date.opx\"\n\nPROC main:\n    LOCAL now&amp;, date$(25)\n    now&amp;=DTNOW&amp;:\n    date$=ISO8601$:(now&amp;)\n    PRINT date$\n    GET\nENDP\n</code></pre> \n<p>Success:</p> \n<pre><code class=\"language-plaintext\">2024-12-03T03:23:25\n</code></pre> \n<p>You might notice I&rsquo;ve made no attempt to tackle time zones in this implementation, but that&rsquo;s enough for day 2&mdash;remembering OPL&rsquo;s many quirks took quite a bit longer than I&rsquo;d expected and the seasonal festivities are already underway. 🎉</p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>You know who you are.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2024-12-03T05:34:35-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-03-december-adventure-day-02/",
      "title": "December Adventure Day 02",
      "url": "https://jbmorley.co.uk/posts/2024-12-03-december-adventure-day-02/"
    },
    {
      "content_html": "<p>After altogether too much deliberation I settled on a Psion-themed <a href=\"/tags/#december-adventure\">December Adventure</a> (with room for an occasional peripherally related side-quest). Since I&rsquo;ve collected many different Psion-related projects over the last 25-or-so years, I anticpate this being a bit of a random-walk, but hopefully things will start pulling together as the month progresses.</p> \n<p>Starting off slowly today, I set about writing up the Psion emulation options in MAME and packaging things up to make them easier for others to use (building on top of my journey into <a href=\"/posts/2024-11-29-uxn-desktop-entries/\">Linux desktop entries</a> a few days ago). That write-up is still in progress, but I was able to start getting to grips with all the ROMs the community has collected over the years and made my first pull request to the <a href=\"https://github.com/explit7/Psion-ROM\">explit7/Psion-ROM</a> GitHub repository. This repo is rapidly becoming the authorative resource for Psion images and my change adds the Psion Series 3a variant ROMs from the MAME project in the hope that they&rsquo;ll be a little easier for others to find.</p> \n<p>My understanding is that the MAME ROM set represents everything that&rsquo;s been dumped to-date. It contains the following device variants:</p> \n<table> \n <thead> \n  <tr> \n   <th>Device</th> \n   <th>RAM</th> \n   <th>Version</th> \n   <th>Language</th> \n   <th>Filename</th> \n  </tr> \n </thead> \n <tbody> \n  <tr> \n   <td>Psion Series 3a</td> \n   <td>1MB</td> \n   <td>3.22f</td> \n   <td>English</td> \n   <td>s3a_v3.22f_eng.bin</td> \n  </tr> \n  <tr> \n   <td>Acorn Pocket Book II</td> \n   <td>1MB</td> \n   <td>1.30f</td> \n   <td>English (ACN)</td> \n   <td>pb2_v1.30f_acn.bin</td> \n  </tr> \n  <tr> \n   <td>Psion Series 3a</td> \n   <td>2MB</td> \n   <td>3.40f</td> \n   <td>English</td> \n   <td>s3a_v3.40f_eng.bin</td> \n  </tr> \n  <tr> \n   <td>Psion Series 3a</td> \n   <td>2MB</td> \n   <td>3.40f</td> \n   <td>Italian</td> \n   <td>s3a_v3.40f_ita.bin</td> \n  </tr> \n  <tr> \n   <td>Psion Series 3a</td> \n   <td>2MB</td> \n   <td>3.40f</td> \n   <td>USA</td> \n   <td>s3a_v3.40f_usa.bin</td> \n  </tr> \n  <tr> \n   <td>Psion Series 3a</td> \n   <td>2MB</td> \n   <td>3.41f</td> \n   <td>German</td> \n   <td>s3a_v3.41f_deu.bin</td> \n  </tr> \n  <tr> \n   <td>Psion Series 3a</td> \n   <td>2MB</td> \n   <td>3.43f</td> \n   <td>Russian</td> \n   <td>s3a_v3.43f_rus.bin</td> \n  </tr> \n </tbody> \n</table> \n<p>The significant bump in version number between the 1MB and 2MB is fascinating to me&mdash;it turns out the 2MB version came with two additional programs (Spell and Patience) which I had always assumed were limited to the Series 3c and 3mx models.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-12-01-december-advenuture-day-01/more-programs@2x/400.png\" width=\"400\" height=\"133\" x-srcset=\"/posts/2024-12-01-december-advenuture-day-01/more-programs@2x/400.png 400 133,/posts/2024-12-01-december-advenuture-day-01/more-programs@2x/800.png 800 266,/posts/2024-12-01-december-advenuture-day-01/more-programs@2x/1200.png 1200 400,/posts/2024-12-01-december-advenuture-day-01/more-programs@2x/1600.png 1600 533,\" />  \n </body></p> \n<p class=\"caption\">Spell and Patience were introduced in the 2MB Series 3a</p> \n<p>We&rsquo;re still looking for Belgian and Dutch ROMs so if anyone out there has one, please drop me an <a href=\"mailto:hello@jbmorley.co.uk\">email</a> or join us on the <a href=\"https://discord.gg/8ZkKKkA\">Psion Discord</a>.</p>",
      "date_published": "2024-12-01T22:20:34-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-12-01-december-advenuture-day-01/",
      "title": "December Adventure Day 01",
      "url": "https://jbmorley.co.uk/posts/2024-12-01-december-advenuture-day-01/"
    },
    {
      "content_html": "<p>This year I&rsquo;m planning a <a href=\"https://eli.li/december-adventure\">December Adventure</a>:</p> \n<blockquote> \n <p>The December Adventure is <strong>low key</strong>. The goal is to write a little bit of code every day in December.</p> \n <p>Pick a project, or projects and work on them a little bit every day in December.</p> \n</blockquote> \n<p>Possible ideas include:</p> \n<ul> \n <li>getting <a href=\"https://incontext.jbmorley.co.uk\">InContext</a> working on Linux</li> \n <li>prototyping an offline-first syncing notepad using <a href=\"https://automerge.org\">Automerge</a></li> \n <li>improving and exploring the <a href=\"https://software.psion.info\">Psion Software Index</a> and other Psion-related side-quests</li> \n <li>figuring out ways to visualize all the personal data I&rsquo;ve collected over the years</li> \n <li>project housekeeping&mdash;reviewing and writing-up my various projects</li> \n <li>writing a proof-of-concept version of <a href=\"https://github.com/inseven/folders\">Folders</a> for GNOME</li> \n <li>exploring and writing tooling to migrate my photos from iCloud Photo Libary to a cross-platform offline-first approach</li> \n <li>improving this website</li> \n</ul> \n<p>Right now, I&rsquo;m leaning towards the Psion side-quests. Let&rsquo;s see what tomorrow brings.</p>",
      "date_published": "2024-11-30T12:29:00-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-11-30-december-advenutre-2024/",
      "title": "December Adventure 2024",
      "url": "https://jbmorley.co.uk/posts/2024-11-30-december-advenutre-2024/"
    },
    {
      "content_html": "<p>I&rsquo;ve been following the fantastic <a href=\"https://100r.co/site/uxn.html\">Uxn project</a> for a few years and enjoy using many of the tools Devine has put together for this unique platform&mdash;programs like <a href=\"https://100r.co/site/left.html\">Left</a>, <a href=\"https://100r.co/site/noodle.html\">Noodle</a>, <a href=\"https://git.sr.ht/%7Erabbits/uxn-utils/tree/main/item/gui/notepad\">Notepad</a>, and <a href=\"https://git.sr.ht/%7Erabbits/uxn-utils/tree/main/item/gui/m291\">m291</a>.</p> \n<p>To make it easy to launch these in GNOME, I&rsquo;ve added custom <a href=\"https://wiki.archlinux.org/title/Desktop_entries\">desktop entries</a> which are picked up by most Linux GUI launchers. Creating these files can be curiously fiddly (especially if you need to reference files in your home directory) so I thought I&rsquo;d post an example here for reference.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-11-29-uxn-desktop-entries/launcher-left/400.png\" width=\"400\" height=\"250\" x-srcset=\"/posts/2024-11-29-uxn-desktop-entries/launcher-left/400.png 400 250,/posts/2024-11-29-uxn-desktop-entries/launcher-left/800.png 800 500,/posts/2024-11-29-uxn-desktop-entries/launcher-left/1200.png 1200 750,/posts/2024-11-29-uxn-desktop-entries/launcher-left/1600.png 1600 1000,\" />  \n </body></p> \n<p>User-specific desktop entries are registered by adding <code>.desktop</code> files in <code>~/.local/share/applications</code>&mdash;my entry for Left, the Uxn writing tool, is called <code>left.desktop</code> and contains the following:</p> \n<pre><code class=\"language-ini\">[Desktop Entry]\n\nType=Application\nVersion=1.0\nName=Left\nComment=A writing tool\nExec=sh -c \"$HOME/.local/bin/uxn11 $HOME/roms/left.rom\"\nIcon=left.turnip\nTerminal=false\nCategories=Development;\n</code></pre> \n<p>There&rsquo;s a couple of things in here which weren&rsquo;t obvious to me and might benefit from calling out. Both of these are related to the lack of support for path expansion in desktop entries:</p> \n<ul> \n <li><p>User-specific icons must be placed in <code>~/.local/share/icons</code> and referenced by their filename <em>excluding</em> the path extension<sup id=\"fnref1\"><a href=\"#fn1\" rel=\"footnote\">1</a></sup>. In the example above, I&rsquo;m using one of the PNG files from the <a href=\"https://100r.co/site/projects.html\">Hundredrabbits projects page</a>, saved as <code>~/.local/share/icons/left.turnip.png</code> and referenced as <code>left.turnip</code>.</p></li> \n <li><p>You need to jump through <code>sh</code> if the executable is located in your home directory (or you need to reference user-specific paths). Since all the Uxn projects are configured to install ROMs to the <code>~/roms</code> directory, using <code>sh -c</code> makes it possible to use the <code>$HOME</code> environment variable to specify both the path of <code>uxn11</code> (the X11 Uxn emulator) and my local copy of the Left ROM.</p></li> \n</ul> \n<p>With a collection of entries like this, I&rsquo;m able to launch Uxn programs using the GNOME launcher and tools like <a href=\"https://github.com/davatorium/rofi\">rofi</a>. Since they all live in my user directory and only use relative paths, I can easily keep them in sync across my various devices using <a href=\"https://www.chezmoi.io/\">chezmoi</a>.</p> \n<p>\n <body>   \n  <img src=\"/posts/2024-11-29-uxn-desktop-entries/window-left/400.png\" width=\"400\" height=\"250\" x-srcset=\"/posts/2024-11-29-uxn-desktop-entries/window-left/400.png 400 250,/posts/2024-11-29-uxn-desktop-entries/window-left/800.png 800 500,/posts/2024-11-29-uxn-desktop-entries/window-left/1200.png 1200 750,/posts/2024-11-29-uxn-desktop-entries/window-left/1600.png 1600 1000,\" />  \n </body></p> \n<div class=\"footnotes\"> \n <hr /> \n <ol> \n  <li id=\"fn1\"> <p>Thanks to <a href=\"https://oldbytes.space/@thelastpsion\">Alex</a> for pointing me at this solution.&nbsp;<a href=\"#fnref1\" rev=\"footnote\">↩</a></p> </li> \n </ol> \n</div>",
      "date_published": "2024-11-29T14:07:31-08:00",
      "id": "https://jbmorley.co.uk/posts/2024-11-29-uxn-desktop-entries/",
      "title": "Uxn Desktop Entries",
      "url": "https://jbmorley.co.uk/posts/2024-11-29-uxn-desktop-entries/"
    },
    {
      "content_html": "<p>Today I published <a href=\"https://pypi.org/project/fastcommand/\"><code>fastcommand</code></a> to <a href=\"https://pypi.org/\">PyPI</a>.</p> \n<p><code>fastcommand</code> is a tiny Python package that wraps <code>argparse</code>, making it a little easier to create Python command-line utilities with multiple sub-commands. It provides a simple <code>command</code> decorator that can be used to annotate top-level functions. For example,</p> \n<pre><code class=\"language-python\">import fastcommand\n\n@fastcommand.command(\"hello\", help=\"say hello\")\ndef command_hello(options):\n    print(\"Hello, World!\")\n\ndef main():\n    cli = fastcommand.CommandParser(description=\"Simple fastcommand example.\")\n    cli.run()\n</code></pre> \n<p>Per-command options and arguments can be specified as follows:</p> \n<pre><code class=\"language-python\">@fastcommand.command(\"goodbye\", help=\"say goodbye\", arguments=[\n    fastcommand.Argument(\"name\"),\n    fastcommand.Argument(\"--wave\", \"-w\",\n                         action=\"store_true\", default=False)\n])\ndef command_goodbye(options):\n    print(f\"Goodbye, {options.name}!\")\n    if options.wave:\n        print(\"👋\")\n</code></pre> \n<p>These sub-commands behave exactly as you&rsquo;d expect:</p> \n<pre><code class=\"language-bash\">$ examples/hello.py -h       \nusage: hello.py [-h] [--verbose] {hello,goodbye} ...\n\nSimple fastcommand example.\n\npositional arguments:\n  {hello,goodbye}  command\n    hello          say hello\n    goodbye        say goodbye\n\noptions:\n  -h, --help       show this help message and exit\n  --verbose, -v    show verbose output\n</code></pre> \n<pre><code class=\"language-bash\">$ examples/hello.py goodbye --help\nusage: hello.py goodbye [-h] [--wave] name\n\npositional arguments:\n  name\n\noptions:\n  -h, --help  show this help message and exit\n  --wave, -ww\n</code></pre> \n<pre><code class=\"language-bash\">$ examples/hello.py goodbye Jason --wave\nGoodbye, Jason!\n👋\n</code></pre> \n<p>There are undoubtedly a myriad similar packages out there and, first-and-foremost <code>fastcommand</code> is a utility that helps me build the tools I&rsquo;ve needed to develop over the years, but perhaps it can help you too. Please let me know if you use it and feel free to raise GitHub <a href=\"https://github.com/jbmorley/fastcommand/\">issues and pull-requests</a>.</p> \n<p>Publishing <code>fastcommand</code> is the first step in publishing some of the other Python utilities I&rsquo;ve created over the years to help me with my software development&mdash;tools like <a href=\"https://github.com/jbmorley/changes\"><code>changes</code></a> which represents my own take on <a href=\"https://semver.org/\">semantic versioning</a> and <a href=\"https://www.conventionalcommits.org/en/v1.0.0/\">conventional commits</a>.</p>",
      "date_published": "2024-10-29T17:00:00-07:00",
      "id": "https://jbmorley.co.uk/posts/2024-10-30-fastcommand/",
      "title": "fastcommand",
      "url": "https://jbmorley.co.uk/posts/2024-10-30-fastcommand/"
    }
  ],
  "title": "Jason Morley",
  "version": "https://jsonfeed.org/version/1"
}