<rss version="2.0">
  <channel>
    <title>Devlog on Leon Mika</title>
    <link>https://lmika.org/categories/devlog/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Sun, 26 Apr 2026 11:18:46 +1000</lastBuildDate>
    
    <item>
      <title>Devlog: Imagen</title>
      <link>https://lmika.org/2026/04/26/devlog-imagen.html</link>
      <pubDate>Sun, 26 Apr 2026 11:18:46 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/04/26/devlog-imagen.html</guid>
      <description>&lt;p&gt;I won&amp;rsquo;t bore you with any justification on why I actually built this thing. It&amp;rsquo;s really nothing more than wanting a harness to play with Google&amp;rsquo;s Nano Banana. Up until now, I&amp;rsquo;ve been using Blogging Tools for that, but it lacked any ability to request changes to images in a chat-like interface (so called &amp;ldquo;multi-step flows&amp;rdquo;), where the context is preserved. So I made Imagen as a replacement.&lt;/p&gt;
&lt;div class=&#34;img-gallery&#34;&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2026/cleanshot-2026-04-26-at-11.00.58.png&#34;
     
      /&gt;

  &lt;figcaption&gt;
  
  The landing page, showing current chat sessions and &amp;#39;assets&amp;#39; (uploaded and generated images).
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2026/cleanshot-2026-04-26-at-11.01.22.png&#34;
     
      /&gt;

  &lt;figcaption&gt;
  
  An example of a chat session, showing the meta-commands, plus messages from the user and model.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2026/cleanshot-2026-04-26-at-11.01.29.png&#34;
     
      /&gt;

  &lt;figcaption&gt;
  
  Clicking &amp;#39;New Chat&amp;#39; will show you this: a prompt window along with a way to upload images.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2026/cleanshot-2026-04-26-at-11.01.40.png&#34;
     
      /&gt;

  &lt;figcaption&gt;
  
  Chat management section, allowing you to archive or delete any chats.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2026/cleanshot-2026-04-26-at-11.01.49.png&#34;
     
      /&gt;

  &lt;figcaption&gt;
  
  Clicking the gear icon brings you to the admin section, allowing you to revoke passkeys and end your session.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;/div&gt;
&lt;p&gt;This is a pretty typical chat-based AI model harness. You start a chat requesting an image, maybe uploading one you want modified, click &amp;ldquo;Send&amp;rdquo;, and wait for the model to respond.  There also exist a bunch of meta commands, entered using the &lt;code&gt;/&lt;/code&gt; prefix, that allow you change the model, and retry or undo the last request.&lt;/p&gt;
&lt;p&gt;Chats and assets are stored as regular files (assets as regular image files, chats as a Gob-serialised history of the chat) and metadata is managed by a Sqlite3 database. In terms of the tech stack, I&amp;rsquo;ve settled on a mix that works well for me. A Go backend, using &lt;a href=&#34;https://gofiber.io&#34;&gt;Fiber&lt;/a&gt; as the web middleware, and Go templates for server-side rendering. I eschewed Stimulus.js for this one though, deciding to try out &lt;a href=&#34;https://htmx.org&#34;&gt;HTMX&lt;/a&gt;. And it&amp;rsquo;s a unique way of working. I learnt that to really get the most out of HTMX, you really need to find ways not to use JavaScript to do something.&lt;/p&gt;
&lt;p&gt;The chat interface is a good example. While a more traditional frontend would post a message as JSON and rerender the page with a JSON response, posting a chat message here would return a server-side HTML render of the chat interface, which HTMX will swap out. This also include things like the message form itself, which would be rendered disabled from the server template, rather than being controlled by JavaScript. The good thing about that is that if you were to hard-refresh the page, it will appear disabled. No need to run JavaScript on load to detect the state and disable the form: the HTML is the state. As for getting updates while waiting for a message, well that&amp;rsquo;s just a poll. Nothing too sophisicated here.&lt;/p&gt;
&lt;p&gt;As you can probably guess, much of this was hand-rolled. I did use coding agents to assist with making the passkey authentication flow, the meta commands, and the OpenAI API integration. While I probably could&amp;rsquo;ve done the last two myself, if there was any justification for using coding agents for anything it was probably adding passkeys. The complexity involved with the passkey integration probably justified the use . And this is probably one area of AI-generated code that I&amp;rsquo;m more likely to trust the model over my own shoddy code.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Other Project Updates</title>
      <link>https://lmika.org/2026/03/29/devlog-other-project-updates.html</link>
      <pubDate>Sun, 29 Mar 2026 10:17:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/03/29/devlog-other-project-updates.html</guid>
      <description>&lt;p&gt;A few updates of some other projects I worked on recently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Webtools&lt;/strong&gt; saw some love as I needed some tooling made to make the icon easy to include in the &lt;a href=&#34;https://devlog.lmika.org/categories/well-read/&#34;&gt;Well Read&lt;/a&gt; Flutter project itself. Android expects the logo of a specific size, so I &amp;ldquo;commissioned&amp;rdquo; an &lt;a href=&#34;https://tools.lmika.app/android-icons/&#34;&gt;Android Icon Resizer&lt;/a&gt;, which will take one or more PNG files, resize them to what Android expects, and prepare them in a ZIP that could be extracted at the route of the &lt;code&gt;res/mipmap&lt;/code&gt; directory. It will also produce a small preview of the icon, rendering it in a circle so you can see how it looks on the device. It&amp;rsquo;s background savvy, layering the icon over the PNG with &amp;ldquo;background&amp;rdquo; in the filename.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/s0jaggyk13u39x7e.png&#34; width=&#34;600&#34; height=&#34;501&#34; alt=&#34;Auto-generated description: An Android Icon Resizer tool interface is displayed, allowing users to choose PNG files and preview a circular crop of selected images.&#34;&gt;
&lt;figcaption&gt;The Icon Resizer tool.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The background for Well Read is a gradient, so I also added a simple &lt;a href=&#34;https://tools.lmika.app/gradient-image/&#34;&gt;gradient maker&lt;/a&gt; to Webtools too. Finally, there was a need to resize the logo without resizing the image canvas size itself so that it looked good in the cut-out circle. This required the commission of the &lt;a href=&#34;https://tools.lmika.app/image-inner-resize/&#34;&gt;image inner resize&lt;/a&gt; tool.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/kukrpoflwwgn4or-.png&#34; width=&#34;600&#34; height=&#34;473&#34; alt=&#34;Auto-generated description: A gradient generation web page features a gradient preview, color selectors, gradient type options, rotation settings, image size dropdown, and a blue download button.&#34;&gt;
&lt;figcaption&gt;The gradient tool.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;It&amp;rsquo;s true I could&amp;rsquo;ve used any image editor to do this, but none of them were running and Claude was, so I figured it was easier doing it this way at the time.&lt;/p&gt;
&lt;p&gt;I did get my hands a little more dirty with &lt;strong&gt;UCL&lt;/strong&gt;. I was making some changes to Dequoter to allowed users to perform UCL transformations, and I needed a way for the user to reference the current line. UCL has pseudo-variables, so &lt;code&gt;@line&lt;/code&gt; was possible here. But I was finishing of a Go template transformation, and much like those templates, I was wondering if a simple dot could work: &lt;code&gt;add . 2&lt;/code&gt;, for example. This was a little larger than expected, as it did require changes to the AST, but it was simple enough to knock out in an hour. The host sets the value of dot via the pseudo-variable &lt;code&gt;.&lt;/code&gt;, and it even supports direct sub-indexing at the syntax level: &lt;code&gt;add .item.(0) 2&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Oh, and &lt;strong&gt;Dequoter&lt;/strong&gt; now allows actions to specify input values. Here it is being used for a new UCL transform action, which will map each line of the input to the result of a UCL expression.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/a-axir-c6rw50apr.png&#34; width=&#34;600&#34; height=&#34;474&#34; alt=&#34;Auto-generated description: A software interface displays a command line entry with a suggestion tooltip that reads add .2.&#34;&gt;
&lt;figcaption&gt;The action input prompt.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So, quite a busy week few weeks. Not sure what I&amp;rsquo;ll work on next. I do have an inkling of going back to that Godot project that&amp;rsquo;s been on the back-burner for about a month.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: gRPC Client - A Vibe-coded Client for Testing REST/gRPC Endpoints</title>
      <link>https://lmika.org/2026/03/29/devlog-grpc-client-a-vibecoded.html</link>
      <pubDate>Sun, 29 Mar 2026 09:56:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/03/29/devlog-grpc-client-a-vibecoded.html</guid>
      <description>&lt;p&gt;My current craving of vibe-coding various tools I need to do my job continues, with an attempt to build a REST/gRPC test client.&lt;/p&gt;
&lt;p&gt;This is motivated by my distaste with all the other clients I&amp;rsquo;ve tried. There&amp;rsquo;ve been a few, and I&amp;rsquo;ve been unhappy with each one. For one thing, they seem more heavyweight than my needs. I don&amp;rsquo;t know if this is just how they&amp;rsquo;re implemented, or it&amp;rsquo;s because the realm of HTTP request testing is complicated (It&amp;rsquo;s probably a bit of both).&lt;/p&gt;
&lt;p&gt;Another thing is that they&amp;rsquo;re tend to add features targeting testing teams that manage a central repository of test cases. I get the feeling that&amp;rsquo;s how most of them try to make money: offer the tool for free to get the foot in the door, then upsell them with a hosted test repository. This leaves half the interface either useless, or just bait for throwing up requests to setup a service account and pay them money.&lt;/p&gt;
&lt;p&gt;Finally, some are just awkward to use. The UI is inconsistent and buggy, and just gets in the way. I did wonder if the typical UI for these REST clients is partly to blame for this, but as you will see, it&amp;rsquo;s not like this project deviates from the mainstream, so I think it&amp;rsquo;s just comes down to implementation.&lt;/p&gt;
&lt;p&gt;Anyway, these frustrations, along with wanting to see what these coding agents are capable of, motivated me to  requested Claude Code to make me one. And it did a pretty decent job.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/wcshwyvqxu1ilkhp.png&#34; width=&#34;600&#34; height=&#34;400&#34; alt=&#34;Auto-generated description: A screenshot of a gRPC client interface showing request and response details, including headers and response data.&#34;&gt;
&lt;figcaption&gt;Main client area of gRPC Client.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;It&amp;rsquo;s called gRPC Client — stellar naming, I know — and it&amp;rsquo;s pretty typical of most other REST/gRPC test clients out there. Request details on the left, the latest response on the right. A list of saved requests, along with a history, in the sidebar, that can be recalled and resent. There&amp;rsquo;s a reflection browser for gRPC endpoints, and a command for producing an example request for service methods, a feature of Insomnia that I really like.&lt;/p&gt;
&lt;p&gt;The right sidebar has three parts, of which the first is the ability to define environment values, which can be included in the request properties via Go templates (this is a Wails app with a Go backend and HTML+JS frontend).&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/6ae4qncxgixszmcz.png&#34; width=&#34;600&#34; height=&#34;400&#34; alt=&#34;Auto-generated description: A user interface for a gRPC client is displayed, showing API request and environment settings with options to edit user ID information.&#34;&gt;
&lt;figcaption&gt;Editing environment values. This modal is only really useful for large values, like JWT tokens, but values can also be modified from the sidebar itself.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;One feature of this client is the ability to override HTTP headers at the global level, the second thing in the right sidebar. When testing APIs, I usually have a collection of pre-made requests for endpoints I&amp;rsquo;m familiar with. Yet I&amp;rsquo;m always editing the headers when the user or auth token changes. The motivation with this feature was to make the change once, so that I don&amp;rsquo;t need to do so when I switch saved requests. I originally requested a way to do this for the hostname and port too, but I found this wasn&amp;rsquo;t as useful for me.&lt;/p&gt;
&lt;p&gt;In practice, I ended up simply using environment values and request scripts, so-called &amp;ldquo;Hooks&amp;rdquo;, which is the third thing in the right sidebar. This is some JavaScript that executes before each requests, allowing the user to modify the headers and body using the values from the environment. What I would do is set the username as an environment value, and a hook would set the HTTP header, and generate the payload for a JWT token, saving me from getting this from the browser (the endpoints I need to test just need the payload, not the signature, so this simply involvers marshalling a JSON object to Base64).&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/llsonbzzizbjy-tl.png&#34; width=&#34;600&#34; height=&#34;400&#34; alt=&#34;Auto-generated description: A user interface displays a gRPC client with a pop-up window for editing a hook script.&#34;&gt;
&lt;figcaption&gt;Editing a hook. The placeholder gives a good example of what is supported.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;These are all features of most REST clients out there, but they&amp;rsquo;re always hidden away and difficult to modify, which means I&amp;rsquo;m never using them. Placing them in the main client area makes them front and centre, so to speak, and so much easier to change.&lt;/p&gt;
&lt;p&gt;One other thing I requested was the use of &lt;a href=&#34;https://github.com/tailscale/hujson&#34;&gt;HuJSON&lt;/a&gt; for the request body. It&amp;rsquo;s a little crazy to think that all these REST/gRPC clients, designed for use by humans, require their users to write perfect JSON for the body, especially when I&amp;rsquo;m left wanting to comment out parts of the body as I test something. This gets translated to JSON when the request is being sent.&lt;/p&gt;
&lt;p&gt;So all in all, it&amp;rsquo;s a pretty decent client, and despite it being like many other clients out there, it&amp;rsquo;s pleasant to use. And I can&amp;rsquo;t really put my finger on why. Might be that it&amp;rsquo;s just simpler than all the other ones out there. It doesn&amp;rsquo;t have tabs, for example, which does making testing multiple endpoints a little annoying, and I&amp;rsquo;m wondering if it&amp;rsquo;s worth asking Claude to add them. Yet at the same time, the frustration I had with other clients came about with their implementation of tabs. Likewise for saved requests. This just has a single list of saved requests that are either REST or gRPC requests. No need to worry about folders, or request types I don&amp;rsquo;t use, like GraphQL.&lt;/p&gt;
&lt;p&gt;I guess if you&amp;rsquo;re vibe coding something for your own use, less is indeed more.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Weiro - Categories, Pages, and Upload Editing</title>
      <link>https://lmika.org/2026/03/29/devlog-weiro-categories-pages-and.html</link>
      <pubDate>Sun, 29 Mar 2026 08:53:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/03/29/devlog-weiro-categories-pages-and.html</guid>
      <description>&lt;p&gt;Some more work on Weiro. Much of it is pretty mundane, mainly to get it to feature parity with other CMS&amp;rsquo;s out there. Yes, I know the existence of those other CMS&amp;rsquo;s make the entire project pointless. Doubly so when you consider that much of what I&amp;rsquo;m going to talk about was largely done by coding agents. It made me wonder whether it was worth writing this update at all. Well, it&amp;rsquo;s drafted up already so I may as well finish it off. At least one thing will get finished.Okay, enough wallowing in my self-doubt. What was added? Well, categories are now a thing. These can be defined in a new categories section and consist of a label, a slug, and a description. Going to &lt;code&gt;/categories/&amp;lt;slug&amp;gt;&lt;/code&gt; will list all the posts with that category. Posts can be in zero or more categories, which can be selected from the edit post screen. Pretty simple stuff.&lt;figure&gt;&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/m7rczzbd0bdam8jj.png&#34; width=&#34;600&#34; height=&#34;417&#34; alt=&#34;Auto-generated description: A webpage displays a list of categories with their names, slugs, and post counts, along with navigation options like New Category and settings.&#34;&gt;&lt;figcaption&gt;The Categories section, where the user can manage categories.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&gt;&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/cslhwkomkhk8rycc.png&#34; width=&#34;600&#34; height=&#34;417&#34; alt=&#34;Auto-generated description: An online interface is displayed, showing fields to edit a category, including Name, Slug, and Description, with Save and Delete buttons.&#34;&gt;&lt;figcaption&gt;Editing a category.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&gt;&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/62aswp-h8po2jaa1.png&#34; width=&#34;600&#34; height=&#34;417&#34; alt=&#34;Auto-generated description: A blog post interface is displayed with a text editor on the left featuring a draft update, and categories on the right labeled Well Read.&#34;&gt;&lt;figcaption&gt;Selecting categories from within the edit post screen.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&gt;&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/nuoc2szdzv-3qalp.png&#34; width=&#34;600&#34; height=&#34;417&#34; alt=&#34;Auto-generated description: A webpage titled Devlog displays categories with links to Weiro and Well Read, indicating it&#39;s under construction.&#34;&gt;&lt;figcaption&gt;The categories page as it appears on the published site.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&gt;&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/nnljqeqf1-gd7lav.png&#34; width=&#34;600&#34; height=&#34;417&#34; alt=&#34;Auto-generated description: A webpage titled Devlog features a blog post about Weiro, a blogging CMS for hosting via Netlify, with an update dated 6th March 2026.&#34;&gt;&lt;figcaption&gt;The single category page.&lt;/figcaption&gt;&lt;/figure&gt;Another small thing added was pages. This allows the user to define &amp;ldquo;slash&amp;rdquo; pages, with the option of appearing in the nav bar, and can also be used to replace the home page, by setting the slug to &lt;code&gt;/&lt;/code&gt; (the posts are still available at &lt;code&gt;/posts&lt;/code&gt;). I do need to spend some more time figuring out how to organise the nav bar as I would also like to include things like redirects. I thinking of making that an extension of the pages model, but I&amp;rsquo;m not sure.&lt;figure&gt;&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/oe-1ncpvxjkhlomh.png&#34; width=&#34;600&#34; height=&#34;417&#34; alt=&#34;Auto-generated description: A webpage is displayed showing a simple layout with options for creating a new page and a table with columns labeled Title, Slug, and Nav.&#34;&gt;&lt;figcaption&gt;List of pages of a site.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&gt;&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/tbd-bczthswmloip.png&#34; width=&#34;600&#34; height=&#34;417&#34; alt=&#34;Auto-generated description: A webpage editor is displaying text input fields for creating an About page with options for page settings and navigation.&#34;&gt;&lt;figcaption&gt;Editing a page. There&amp;rsquo;s only a single page type at the moment.&lt;/figcaption&gt;&lt;/figure&gt;Those two features were mainly done with the help of Claude Code, but I did build some stuff manually. The largest addition was the ability to do some edits on uploaded images. I&amp;rsquo;ve never been a fan of how Apple produces the shadows around windows: the margins are just too large. So I added a way to do this within Weiro itself.&lt;figure&gt;&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/mkmzi73t1xlfyv7j.png&#34; width=&#34;600&#34; height=&#34;439&#34; alt=&#34;Auto-generated description: A web interface is displaying settings for editing a category in a blogging CMS.&#34;&gt;&lt;figcaption&gt;The edit upload feature.&lt;/figcaption&gt;&lt;/figure&gt;It&amp;rsquo;s pretty simple. Just imagine the filter section from any image editor, then remove all the other features of that editor. Yeah, it&amp;rsquo;s &lt;em&gt;that&lt;/em&gt; simple. There&amp;rsquo;s no cropping, rotating, or anything else of that nature: just a bunch of &amp;ldquo;processors&amp;rdquo; that you can add to the image, with the sole one being a drop shadow.This is actually done server side using a simple file-based approach. When opening an image upload to edit, you spawn a session. Each session has a JSON file maintaining the processors, plus a series of cached image files of all the intermediate steps along the processing chain. When a new processor is added, a hash is computed with the processor&amp;rsquo;s properties, and if they change, the cached image will be regenerated. The processor includes the hash of the previous step too, so that if processors further up the chain are modified or remove, they will force a recompute of subsequent images. The user is then served the last image in the chain.
This differs from the image processor in Blogging Tools, which performed the transformations within the browser itself. The motivation there was to avoid the need of a slow upload of the image, but it came with the performance cost associated with doing the processing within the browser itself. WASM might be fast, but it&amp;rsquo;s not &lt;em&gt;that&lt;/em&gt; fast[^fast]. Since the upload in Weiro is already uploaded, I figured it would be quicker to just do the image processing server side. My hope is that the computing power the server has access to would offset the time it takes to download the image. So far this seems to be the case.Finally, I did some minor work around the UI, adding a site chooser and a much needed way to open the published site from the admin section.So this project is still coming along, surprisingly. It&amp;rsquo;s probably the furthest I&amp;rsquo;ve got in a blogging CMS that I actually want to use. I do have a large list of things I want to add to it, and I certainly need to do something with the design of the actual site. It&amp;rsquo;s all a question of whether I&amp;rsquo;m interested in spending time on it.[^fast]: Although to be fair, I think the slow down comes from encoding the processed image as a data URI and setting it as the source of an &lt;code&gt;img&lt;/code&gt; tag.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Well Read - One Week Later</title>
      <link>https://lmika.org/2026/03/17/devlog-well-read-one-week.html</link>
      <pubDate>Tue, 17 Mar 2026 20:34:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/03/17/devlog-well-read-one-week.html</guid>
      <description>&lt;p&gt;It&amp;rsquo;s been a week since I learnt about Inkwell&amp;rsquo;s API and got an agent to start work on an RSS reader. Since then, Well Read has been in a state of flux, as I ask for agents to make changes to the interaction and layout. I think I&amp;rsquo;ve got it in a pretty good state now, certainly in a state that works well for me.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/rggwaorys0kcw-bm.png&#34; width=&#34;600&#34; height=&#34;402&#34; alt=&#34;Auto-generated description: Three phone screens display different interfaces, including email and note-taking applications with visible text and icons.&#34;&gt;
&lt;figcaption&gt;The earliest screenshots of Well Read, taken on 11 March 2026&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The Today and Recent tabs still retain their original behaviour, although the idea of using 6:00 pm of the previous evening as the cutoff for &amp;ldquo;todays&amp;rsquo; posts&amp;rdquo; was a good one. Being where I&amp;rsquo;m located in the world, many people I follow publish posts while I&amp;rsquo;m asleep. I recently added a tab for bookmarks which uses Micro.blog&amp;rsquo;s bookmarking feature. Any post can be added as a bookmark, and since there&amp;rsquo;s no real way to get to a post once it drops out of the Recent tab (well, apart from opening it up in Inkwell proper), I have in mind this feature to act as a place to stash posts for later. So I tried making bookmarks as easy to add as possible. Sliding aside the item in the feed list will reveal a bookmark action, as will the overflow menu in the feed viewer.&lt;/p&gt;
&lt;p&gt;The app now has an icon too, which was made with the help of ChatGPT. I haven&amp;rsquo;t consulted the agents about a better name though.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/pyge70epfdvskrrd.png&#34; width=&#34;148&#34; height=&#34;148&#34; alt=&#34;Auto-generated description: An open book with an ink bottle and a Wi-Fi signal icon on its pages is depicted against a gradient background.&#34;&gt;
&lt;figcaption&gt;App logo of Well Read&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I did get my hands a little dirty to try and speed up the Webviewer. I think I made some success with removing the &lt;code&gt;max-resizing&lt;/code&gt; from the &lt;code&gt;viewport&lt;/code&gt; meta tag aded to the HTML the Webviewer opens. It could be psychosomatic, though. It feels a little different after I added it, but only if I&amp;rsquo;m using the actual phone. I see no performance issues in the simulator. I even tried comparing two screen recordings I made before an after the change, but I couldn&amp;rsquo;t see a different. Since then, the scrolling has become choppy again. Sigh. That&amp;rsquo;s one downside with choosing Flutter for this: you&amp;rsquo;re bound by the speed of the language runtime. Maybe if I got the coding agent to port it to plain Android… 🤔&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/bvycgp7ect8jftfx.png&#34; width=&#34;600&#34; height=&#34;402&#34; alt=&#34;Auto-generated description: Three screenshots showcasing different layouts of an email application, each displaying a list of emails, email details, or related interactions.&#34;&gt;
&lt;figcaption&gt;Screenshots of Well Read taken this evening&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;But the proof of any app is in the pudding: am I actually using it to read my RSS feeds? And the answer is yes, I am. It doesn&amp;rsquo;t do everything a full-featured RSS app does, but what it does it does reasonably well — jankiness Webviewer scrolling aside — and I find myself turning to it at lunch or during my commutes. And even though there aren&amp;rsquo;t any unread counts anywhere, it&amp;rsquo;s slightly ironic to find that this river-based pattern has turned me into a bit of a completeness, wanting to get through today&amp;rsquo;s feed before they drop over to recents. Oh I still save the odd post by keeping it marked as unread (the &amp;ldquo;Mark as Unread&amp;rdquo; action is optimised for this, in that it actually kicks you back to the post list when you use it), but not a frequently as I have been.&lt;/p&gt;
&lt;p&gt;Anyway, my need for changes for this app might settle a little as I just go about simply using it for a while. But with the use of these agents, it&amp;rsquo;s pretty amazing what they can be accomplish in a week.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Well Read - An Inkwell Client for Android</title>
      <link>https://lmika.org/2026/03/12/devlog-well-read-an-inkwell.html</link>
      <pubDate>Thu, 12 Mar 2026 06:22:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/03/12/devlog-well-read-an-inkwell.html</guid>
      <description>&lt;p&gt;When Manton mentioned that &lt;a href=&#34;https://help.micro.blog/t/feeds-syncing-with-inkwell/4255&#34;&gt;Inkwell has an API&lt;/a&gt;, I… um… may have vibe-coded an Android client.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/gdyts0xjbn7-uheb.png&#34; width=&#34;600&#34; height=&#34;639&#34; alt=&#34;Auto-generated description: Two smartphone screenshots display a reading list with articles and a detailed view of a specific article quoting John Carmack.&#34;&gt;
&lt;p&gt;It&amp;rsquo;s called &amp;ldquo;Well Read&amp;rdquo;, which is not a great name but better than &amp;ldquo;Inkwell Client&amp;rdquo; which was the working title. Much like Inkwell, it follows the river approach to RSS. There&amp;rsquo;s a Today tab and Recent tab, each showing a portion of the entries in reverse chronological order. Today shows all the posts from today, plus the last 6 hours of yesterday. The motivation here is that this will be the tab you&amp;rsquo;ll be spending your time, with posts aging out to recent over time. There&amp;rsquo;s no Fading tab: all posts older than about half a week fall out of the app. And I&amp;rsquo;m not sure if I&amp;rsquo;m going to add one. There&amp;rsquo;s only a few feeds that I want to catch up on if I miss their post, and since I&amp;rsquo;m using NetNewsWire and Inkwell, I&amp;rsquo;m pretty sure I&amp;rsquo;ll catch them later.&lt;/p&gt;
&lt;p&gt;At the moment this is pretty bare bones. There&amp;rsquo;s no bookmarks (yet), you can&amp;rsquo;t add feeds, there&amp;rsquo;s no search, and only a few actions you can perform on a post: copy the link, mark it as unread, and open it in the external browser. The webview itself is a little janky too, which is not great, and I need to do something about that as it does impact the reading experience.&lt;/p&gt;
&lt;p&gt;But so far it&amp;rsquo;s been pretty good. Feels the time when all I have is a phone to read stuff, like during lunch or while commuting.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Weiro - Update 6th March 2026</title>
      <link>https://lmika.org/2026/03/09/devlog-weiro-update-th-march.html</link>
      <pubDate>Mon, 09 Mar 2026 09:48:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/03/09/devlog-weiro-update-th-march.html</guid>
      <description>&lt;p&gt;A small update on Weiro. I&amp;rsquo;ve been working on it over the past week, trying to get it in a state that is pleasant to use. I&amp;rsquo;m been trying to get something halfway usable before doubt scuppers my motivation and this project appears on the growing list of aborted attempts at making a CMS. There&amp;rsquo;ve been one or two close calls, but it hasn&amp;rsquo;t caused me to stop yet.&lt;/p&gt;
&lt;p&gt;A large part of that was a feature I knew I wanted but was daunting to implement: uploads. The thought of writing the logic to manage large files, make sure EXIF data is stripped, and serve and manage them always seems like a pain. It&amp;rsquo;s the reason why I&amp;rsquo;ve abandoned CMS projects in the past. And I want something that support uploads: I&amp;rsquo;ve tried CMSes that didn&amp;rsquo;t have them and I never stayed long.&lt;/p&gt;
&lt;p&gt;But this time, I rolled up my sleeves, cracked open Claude, and told it to… no, I&amp;rsquo;m kidding. Much of the feature was hand rolled, although Claude helped with some of the plumbing of getting uploaded files. But it&amp;rsquo;s done: uploads are now supported. Which means screenshots:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/wrdzcj60vx1nkyui.png&#34; width=&#34;600&#34; height=&#34;423&#34; alt=&#34;Auto-generated description: A screenshot of a website interface showing a blog post titled Weiro - Update 6th March 2026 with a draft status indicator.&#34;&gt;
&lt;figcaption&gt;Main post screen, with this post shown as draft. The &#39;rebuild site&#39; is temporary at the moment.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/kavv7u9ugrazfpzk.png&#34; width=&#34;600&#34; height=&#34;423&#34; alt=&#34;Auto-generated description: A webpage is displayed featuring a blog post about a CSV Editor, including links, text, and a screenshot of a CSV tool.&#34;&gt;
&lt;figcaption&gt;The post editor. Pretty basic at the moment.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/ibatq9w6yc38hmor.png&#34; width=&#34;600&#34; height=&#34;423&#34; alt=&#34;Auto-generated description: A browser window displays an online platform featuring in-progress upload thumbnails and an Upload button.&#34;&gt;
&lt;figcaption&gt;The uploads list.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/29hzeboaipdylaou.png&#34; width=&#34;600&#34; height=&#34;423&#34; alt=&#34;Auto-generated description: A computer screen displays a web browser with a table showing various individuals&#39; names, emails, phones, and other personal details.&#34;&gt;
&lt;figcaption&gt;A single upload, allowing the user to copy the HTML to paste in the post.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Along with this are some updates to the published site. This I&amp;rsquo;m hand-rolling too, rather than relying on an existing SSG. Probably a mistake in the long run, but it does keep things flexible. There&amp;rsquo;s nothing flashy about the HTML version of the site right now. I&amp;rsquo;m using &lt;a href=&#34;https://simplecss.org&#34;&gt;Kev Quirk&amp;rsquo;s awesome Simple.css&lt;/a&gt;, that I use as a base style for most things nowadays. But I have added an RSS and JSON feed. Two, in fact. One is a standard RSS feed for the site, and another is designed from crossposting to Micro.blog. The main difference between the two is that the crossposted version will have &amp;ldquo;Devlog&amp;rdquo; prepended to each of the post titles, as they&amp;rsquo;ll be imported as full posts in a blog that has other content.&lt;/p&gt;
&lt;p&gt;This is all hard-coded at the moment but I&amp;rsquo;m hoping to extend this to support other sorts of feeds, allowing the author to modify, include, and exclude posts based on their desires. The motivation here is to make it easier to integrate with services that cross-post, while recognising that not all of these &amp;ldquo;views&amp;rdquo; of a post would be the same. Cross-post to a service that supports at-mentions and maybe you&amp;rsquo;d like to ensure their handle is properly written out. But display it on the site, and maybe you&amp;rsquo;d prefer that the handle is a link to their homepage. These should all derived from the post source, and expanded in the various feeds based on who&amp;rsquo;s consuming it.&lt;/p&gt;
&lt;p&gt;Anyway, that&amp;rsquo;s the idea. We&amp;rsquo;ll see if any of this comes about.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: CSVTool - A Vibe-coded CSV Editor</title>
      <link>https://lmika.org/2026/03/08/devlog-csvtool-a-vibecoded-csv.html</link>
      <pubDate>Sun, 08 Mar 2026 06:11:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/03/08/devlog-csvtool-a-vibecoded-csv.html</guid>
      <description>&lt;p&gt;One of the fun aspects of these new code agents is seeing what they&amp;rsquo;re capable of producing just form the prompt, so called &amp;ldquo;vibe-coding.&amp;rdquo; There are some that are definitely all in on the concept: I&amp;rsquo;m thinking of Steve Yeggie and his Gas Town work. As for myself, I still prefer to be a bit more hands on. But it&amp;rsquo;s still amusing to see what these agents are capable of just from the prompt.&lt;/p&gt;
&lt;p&gt;I got inspired by &lt;a href=&#34;http://scripting.com/2026/03/03.html#a145455&#34;&gt;Dave Winder&amp;rsquo;s post&lt;/a&gt; about how he asked Claude Code to make a spreadsheet app for him. I use a TUI app I made for myself called &lt;a href=&#34;https://lmika.dev/cmd/ted&#34;&gt;Ted&lt;/a&gt; that I use to edit CSV files, but seeing how good Claude was in making the spreadsheet, I thought I&amp;rsquo;d ask Claude to make me a GUI version. This is what it came up with:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/egpa1ybj7ftpzmez.png&#34; width=&#34;600&#34; height=&#34;386&#34; alt=&#34;Auto-generated description: A table displays a list of names, contact details, and job information for various individuals, formatted like a spreadsheet or CSV file.&#34;&gt;
&lt;p&gt;The results were pretty decent, at least on the surface. I haven&amp;rsquo;t put it quite through it&amp;rsquo;s paces for editing large CSV files, but what it managed to do out of the box was pretty impressive. Not that it&amp;rsquo;s groundbreaking in any technical sense: think spreadsheet without the ability to define expressions. Most of the supportable commands are available via a command palette, invokable using Cmd+P. There are a few copy-and-paste options for getting table data out into usable formats, something I find quite frustrating with the spreadsheets I use. You can paste table data as CSV, a Markdown table, and Jira markup. There&amp;rsquo;s the usual load, save, and commands to insert rows; all pretty standard.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/t6eu6lo8pfu-7avl.png&#34; width=&#34;600&#34; height=&#34;386&#34; alt=&#34;Auto-generated description: A spreadsheet-like interface displays a list of names, emails, and other information, with a context menu open showing options related to row and column operations.&#34;&gt;
&lt;p&gt;It isn&amp;rsquo;t perfect though. Despite two attempts to instruct it to make the header row fixed such that it won&amp;rsquo;t scroll off the top, Claude was unable to achieve this. The techniques of making such a header are pretty hacky: the one I&amp;rsquo;m aware of is making a second table with just the header, placing it above the first, setting the position to absolute, then adding some JavaScript to keep the column widths in sync. Granted, this worked a good 18 years ago, and for tables that didn&amp;rsquo;t scroll horizontally. Maybe there&amp;rsquo;s a better way of doing this nowadays? Oh and speaking of the columns, despite providing affordances for resizing the columns, Claude didn&amp;rsquo;t implement the code to actually resize them. This makes me wonder if HTML tables are probably not the best approach for such a control. I guess I probably should&amp;rsquo;ve told Claude that.&lt;/p&gt;
&lt;p&gt;In any case, I&amp;rsquo;m not sure I&amp;rsquo;ll put this to any real use. One thing I like about Ted is that it&amp;rsquo;s modal, like Vim. Being able to move around using the keyboard (I, J, K, and L are mapped to up, left, down, and right, respectively) is something I&amp;rsquo;d miss. So I think Ted will still be useful for me. I may revisit this in the future though, and see if Claude could make a GUI version of Ted. That could be interesting.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll finish up by talking about some of the other features I asked to be added. Some of them were pretty ill conceived, as they came while I had a flurry of needs for CSV data. I ended up using to tool more frequently than I expected.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sorting:&lt;/strong&gt; Sort the table (the actual model, not just the view) either alphanumeric ascending, or alphanumeric descending based on the values of the current cell. There was also a &amp;ldquo;Sort Advanced&amp;rdquo; that prompted the user to enter the columns to sort in priority order (although not the direction, it&amp;rsquo;s always alphanumeric ascending).&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/rddp-wu2pcj1j8ij.png&#34; width=&#34;600&#34; height=&#34;386&#34; alt=&#34;Auto-generated description: A computer screen displays a CSV editing tool with a dataset of names, contact information, and occupations, featuring a Sort Advanced pop-up for organizing columns.&#34;&gt;
&lt;p&gt;&lt;strong&gt;Match Cell:&lt;/strong&gt; Select cells that had a value matching the current cell. This was an attempt of selecting rows to delete, but it never really worked well.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Set Where:&lt;/strong&gt; Prompts the user for a source column, a set of match values, and a target value. Whenever the source column has one of the match values, it will set the current column to the target value. This acts like a poor man&amp;rsquo;s inner join, and was made as I needed to interleave values I had from a shell command in the table I was working on.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: 3rd March 2026</title>
      <link>https://lmika.org/2026/03/03/devlog-rd-march.html</link>
      <pubDate>Tue, 03 Mar 2026 06:15:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/03/03/devlog-rd-march.html</guid>
      <description>&lt;p&gt;Oof! Everyone&amp;rsquo;s building blogging CMS&amp;rsquo;s now, apparently.&lt;/p&gt;
&lt;p&gt;Since starting work on this project, I saw one other announce their own CMS that was vibe-coded with Claude. No shame in that: making something that works for you is part of the joy of participating in the Indie-web. I did take a brief look at it, and dismissed it because it was written in PHP. Yes, I am a snooty developer that looks down on those using PHP (it&amp;rsquo;s just so annoying to deploy; although credit to this person, they did prepare a Docker container).&lt;/p&gt;
&lt;p&gt;Weiro is not vibe coded. Much of it is written by hand. Not all of it, mind you: I am using agents to help with the more mundane stuff. But the models, DB schema, and much of the UI is hand-rolled. And I&amp;rsquo;m conflicted as to whether that&amp;rsquo;s the right balance. Progress is slower: these vibe-coders are whipping up CMS&amp;rsquo;s in the same time it takes me to add a single feature to it. And there are probably better things I could be doing other than adding one more CMS into the world (although when my mind whispers that doubt, it usually suggests watching TV or doomscrolling as an alternative, so there are certainly worse things I could be doing).&lt;/p&gt;
&lt;p&gt;I haven&amp;rsquo;t stopped working on it yet, so here&amp;rsquo;s a brief update. I&amp;rsquo;ve got a version of it up and running in Coolify. It currently supports posts at the moment: both draft and published via Netlify. Much of the work was just making the writing experience feel natural, given that working on posts is probably the core feature of any CMS. So there&amp;rsquo;s a very large window for the post body (maybe a little too large), and there&amp;rsquo;s a Cmd+S keyboard shortcut for saving updates. I would like to add an auto-save feature, but I&amp;rsquo;m not entirely sure how best to do that server side, so I may settle for something that&amp;rsquo;s browser only, just to save work for when the browser crashes or has no network connection. I&amp;rsquo;m also working on uploads, so there shouldn&amp;rsquo;t be too much time before I can start sharing screenshots.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Hello</title>
      <link>https://lmika.org/2026/02/28/devlog-hello.html</link>
      <pubDate>Sat, 28 Feb 2026 10:28:24 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/02/28/devlog-hello.html</guid>
      <description>&lt;p&gt;This is the inaugural post of Devlog, where I&#39;m planning to write on what I&#39;m working on. This post was created using Weiro, a new blogging CMS I&#39;ve been working on. This post is little more than a test to see if the deployed version of Weiro is working.  I&#39;ll post more about Weiro in later posts. For now, I just want to make sure this is being published correctly.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Micro apps - Some Score Cards</title>
      <link>https://lmika.org/2026/02/01/devlog-microapps-some-score-cards.html</link>
      <pubDate>Sun, 01 Feb 2026 09:05:02 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/02/01/devlog-microapps-some-score-cards.html</guid>
      <description>&lt;p&gt;In the spirit of maintaining a document of what I&amp;rsquo;ve been working on, and being somewhat inspired by &lt;a href=&#34;https://birchtree.me/tag/micro-apps/&#34;&gt;Matt Birchler&amp;rsquo;s posts about his micro apps&lt;/a&gt;, I&amp;rsquo;d thought I&amp;rsquo;d document on some of the small apps I&amp;rsquo;ve worked on recently.&lt;/p&gt;
&lt;p&gt;The fact of the matter is that I&amp;rsquo;ve been building quite a few of these apps over the course of the summer, primarily in response to a specific need I have at the time. I haven&amp;rsquo;t written about them before, mainly because there didn&amp;rsquo;t feel like there was much to say. Some were vibe coded, and saying that I &amp;ldquo;made&amp;rdquo; them didn&amp;rsquo;t feel correct. Others were made by hand, but they were super simple and there was no real challenge in making them at all. In either case, being able to say &amp;ldquo;I made it&amp;rdquo; is difficult as the amount of effort spent in making it is quite low, and that doesn&amp;rsquo;t make for interesting blog posts. Would a post saying that &amp;ldquo;I went to work today&amp;rdquo; be worth reading if that&amp;rsquo;s what I do every weekday?&lt;/p&gt;
&lt;p&gt;But regardless of what my feelings are, the scales were tipped ever so slightly into wanting to write about them. So that&amp;rsquo;s what I&amp;rsquo;ll do. And there is some leeway in writing about the small things if I were to put them in the Devlog category, which could be seen as my public workbook.&lt;/p&gt;
&lt;p&gt;So, let&amp;rsquo;s set expectations by talking about a series of score-keeping apps. These are among the simplest micro-apps I have, yet they do have their uses. You&amp;rsquo;re always left high and dry with a need to keep score of some ad-hoc game made in the moment (or, at least, I am). Notebooks and spreadsheets could work here, but I don&amp;rsquo;t carry a paper notebook, and use a digital notebook for this is always a little cumbersome. And a spreadsheet just feel a little overkill. Hence, the want for having a simple web-app for keeping scores:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/out-20260201-100150.png&#34; width=&#34;600&#34; height=&#34;409&#34; alt=&#34;Auto-generated description: A scorecard interface displays scores for four players over nine rounds, with options to undo or reset.&#34;&gt;
&lt;p&gt;Using it couldn&amp;rsquo;t be simpler: enter the score under the specific player, and it will be recorded in a table, alongside a running total. If you make a mistake, tap &amp;ldquo;Undo&amp;rdquo;. If you want to clear the scorecard, tap &amp;ldquo;Reset&amp;rdquo;. The scores are stored in local-storage so there&amp;rsquo;s nothing server side that&amp;rsquo;s running (although it does mean they&amp;rsquo;re tied to your browser). Focus automatically moves to the player to the right, but any entry could be entered at any time.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a &lt;a href=&#34;https://tools.lmika.app/scorecard-2p/&#34;&gt;two player&lt;/a&gt; and &lt;a href=&#34;https://tools.lmika.app/scorecard-4p/&#34;&gt;four player&lt;/a&gt; variant. I was considering having a &amp;ldquo;player entry&amp;rdquo; screen to select the number of players, but that felt like adding too much complexity for something that&amp;rsquo;s designed to be quick to implement.&lt;/p&gt;
&lt;p&gt;In fact, much of this was already implemented in the form of a &lt;a href=&#34;https://tools.lmika.app/finska/&#34;&gt;Finska scorecard&lt;/a&gt;.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/out-20260201-100202.png&#34; width=&#34;600&#34; height=&#34;409&#34; alt=&#34;Auto-generated description: A Finska scorecard displaying scores for Team A and Team B, with Team A at 12 points and Team B at 25 points.&#34;&gt;
&lt;p&gt;This is not a new project, I just moved it from a dedicated domain name and changed the styling. It&amp;rsquo;s been a while since I needed it for a game of Finska: I was more often than not using it for generic scorekeeping. But there were some aspects of scoring Finska which made it useful for that, and less useful for anything generic. For those who never played Finska, the goal is to get to exactly 50 points. If you go over 50, you go back to 25 points. There&amp;rsquo;s also a mode where all the zero point entries are highlighted to indicate a foul (3 fouls means you loose the game).&lt;/p&gt;
&lt;p&gt;And, that&amp;rsquo;s it! Really, there&amp;rsquo;s nothing more to say about these apps. I had a need and some free time, so I put these together. There is one other scoring app that I&amp;rsquo;ve made, but that&amp;rsquo;s a little more complicated and warrants it&amp;rsquo;s own post.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2026/01/31/more-work-on-assets-for.html</link>
      <pubDate>Sat, 31 Jan 2026 09:38:13 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/01/31/more-work-on-assets-for.html</guid>
      <description>&lt;p&gt;More work on assets for my niece&amp;rsquo;s game. This time I tried something organic: a tree. Not the most difficult organic thing, I agree. Need to build up to the hard things.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/out-20260131-103550.png&#34; width=&#34;600&#34; height=&#34;972&#34; alt=&#34;Auto-generated description: A digital illustration displays the progressive creation of a tree design using vector graphics tools, starting with basic shapes and evolving into a detailed tree with fruits or blossoms.&#34;&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2026/01/26/was-not-expecting-to-spend.html</link>
      <pubDate>Mon, 26 Jan 2026 08:36:01 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/01/26/was-not-expecting-to-spend.html</guid>
      <description>&lt;p&gt;Was not expecting to spend yesterday morning working on Dequoter, my Boop clone. Opened it up to do some light work and when I looked up, a couple of hours have passed. Added a few more processors to deal with lines, such as splitting and joining on commas. Added a status bar for processes that return information rather than filter text, such as returning a line count.&lt;/p&gt;
&lt;p&gt;Also integrated UCL, because of course I did. Added two commands: a &lt;code&gt;UCL: Evaluate&lt;/code&gt; which executes the input as a UCL script and displays the response in the status bar, and a &lt;code&gt;UCL: Replace&lt;/code&gt; which replaces the UCL script with it&amp;rsquo;s output. This makes it possible to generate text from scripts, like a bunch of lines to test out the line count processor:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;map (seq 20) { |n| &amp;#34;Line $n&amp;#34; } | strs:join &amp;#34;\n&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/out-20260126-093456.png&#34; width=&#34;600&#34; height=&#34;478&#34; alt=&#34;Auto-generated description: A computer window titled Dequoter displays a list numbered from 0 to 19.&#34;&gt;
&lt;p&gt;It&amp;rsquo;ll also make for a useful scratchpad for testing out some UCL commands.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Game For Niece — More Art and Cropping Ebitengine Images</title>
      <link>https://lmika.org/2026/01/20/devlog-game-for-niece-more.html</link>
      <pubDate>Tue, 20 Jan 2026 22:22:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/01/20/devlog-game-for-niece-more.html</guid>
      <description>&lt;p&gt;Drew some more artwork and started integrating it into the game. Replaced the previous bus image I got from an image search with one I created myself using Affinity Designer:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2026/out-20260120-223806.png&#34; width=&#34;600&#34; height=&#34;359&#34; alt=&#34;Auto-generated description: A cartoon-style bus is depicted in an illustration with the word BUS in green text above it.&#34;&gt;
&lt;p&gt;It&amp;rsquo;s orange, just like the busses round here, although this one has a plainer livery.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve not integrated the car just yet: I&amp;rsquo;m hoping to finish off the animations for the bus first. But I&amp;rsquo;m so glad I embraced Affinity Designer for this. I&amp;rsquo;m surprise I haven&amp;rsquo;t considered it sooner. The whole image is designed as a whole, then I export each component — body, door, wheels — as a separate PNG and recompose it in the code, giving me the means of animating each component. I&amp;rsquo;d imagine this is a pretty typical workflow: and I&amp;rsquo;m surprise I haven&amp;rsquo;t considered this sooner too.&lt;/p&gt;
&lt;p&gt;And yes, I will admit I did look at AI image generation for this. But I wasn&amp;rsquo;t happy with what it produced, and I wanted each image to have a consistent style. Turns out an &amp;ldquo;elementary vector artist&amp;rdquo; style works perfectly here.&lt;/p&gt;
&lt;p&gt;Anyway, took a bit of time working out how to draw a cropped image in Ebitengine. Spent a little bit of time seeing what was possible with masks. But after looking at the &lt;a href=&#34;https://ebitengine.org/en/examples/tiles.html&#34;&gt;examples&lt;/a&gt;, it looks like using &lt;a href=&#34;https://pkg.go.dev/github.com/hajimehoshi/ebiten/v2#Image.SubImage&#34;&gt;SubImage&lt;/a&gt; for this is viable:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;busDoor&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ebiten&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Image&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;getSpriteImage&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;bus_door.png&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;render&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;screen&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ebiten&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Image&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;doorDX&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;bp&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;sprBusDoor&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Bounds&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;Dx&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;doorDY&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;bp&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;sprBusDoor&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Bounds&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;Dy&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;pos&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ebiten&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;GeoM&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;pos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Translate&lt;/span&gt;(float64(&lt;span style=&#34;color:#a6e22e&#34;&gt;doorDX&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;screen&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;DrawImage&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;busDoor&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;SubImage&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;image&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Rect&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;doorDX&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;doorDY&lt;/span&gt;)).(&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ebiten&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Image&lt;/span&gt;), 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ebiten&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;DrawImageOptions&lt;/span&gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;GeoM&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;pos&lt;/span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As for the other scenes I need to add, I plan to work through vehicles and objects first as they seem easier to draw. Hopefully by doing so I gain some practice before I tackle more organic objects like trees and animals. Tackling those feel a little daunting to me: more curves involved.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2026/01/05/was-hoping-to-prepare-an.html</link>
      <pubDate>Mon, 05 Jan 2026 21:36:02 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/01/05/was-hoping-to-prepare-an.html</guid>
      <description>&lt;p&gt;Was hoping to prepare an account and make a video on using Alto for that interested party, but ended up fixing all the dodgy UI elements that were either dangerous or just plain embarrasing. The evening&amp;rsquo;s over now, and it&amp;rsquo;s still pretty dodgy, but it&amp;rsquo;s probably good enough to share with this person.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2026/01/04/dusted-off-alto-to-give.html</link>
      <pubDate>Sun, 04 Jan 2026 07:09:05 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2026/01/04/dusted-off-alto-to-give.html</guid>
      <description>&lt;p&gt;Dusted off Alto to give access to someone I know. Need to finish off the multi-tenant functionality, along with removing the hardcoded tokens from the mobile app. Manage to start work after 5 or so minutes of waiting for dependencies/builds/whatever, which is a nice change.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: An API For a Keyframe Animation Package</title>
      <link>https://lmika.org/2025/12/30/devlog-an-api-for-a.html</link>
      <pubDate>Tue, 30 Dec 2025 16:42:35 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/12/30/devlog-an-api-for-a.html</guid>
      <description>&lt;p&gt;Spent the day building a key-frame animator for an Ebitengine project. All coded in Go, which should make it usable for some other projects in theory. I don&amp;rsquo;t have a lot of experience with animation frameworks — other than as an end-user of apps that consist of a timeline where I place keyframes — so I&amp;rsquo;m not aware of what constitutes best practice for a programatic API. So I set out to build one with an API that made sense to me. After one major iteration, this is the approach I came up with.&lt;/p&gt;
&lt;p&gt;The core model type is the Var, which represents an animatable value. These are essentially handles describing the type of value the (package) user wants to animate. At the moment there is only a single type, which is a float:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;xPos&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;animator&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewFloatVar&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;yPos&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;animator&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewFloatVar&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To animate the value, the user will need to create an Animation and place any created Vars on a track. Tracks consist of key-frames, which is essentially a time offset, and one or more values a Var should have at that time. These values can be created from the Var itself, by calling &lt;code&gt;Set()&lt;/code&gt;.   This returns a type holding what that Var should be set to at that key-frame (&amp;ldquo;set&amp;rdquo; is probably not the best term for this):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;moveDown&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;animator&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewAnimation&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;posTrack&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;moveDown&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewTrack&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;posTrack&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;KeyFrame&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.0&lt;/span&gt;, []&lt;span style=&#34;color:#a6e22e&#34;&gt;animator&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FloatSet&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;xPos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.0&lt;/span&gt;), &lt;span style=&#34;color:#a6e22e&#34;&gt;yPos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.0&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;posTrack&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;KeyFrame&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;3.0&lt;/span&gt;, []&lt;span style=&#34;color:#a6e22e&#34;&gt;animator&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FloatSet&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;xPos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;100.0&lt;/span&gt;), &lt;span style=&#34;color:#a6e22e&#34;&gt;yPos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;100.0&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Not all Vars need to be included on a particular keyframe, but prior to animating, each of the Vars known to the track will need to be tweened if there isn&amp;rsquo;t a &amp;ldquo;set&amp;rdquo; for them on the key-frame, so it&amp;rsquo;s generally good practice to set all the Vars for each track. Different tracks can have a different of Vars, so if different keyframes are needed, it probably should exist on a separate track.&lt;/p&gt;
&lt;p&gt;Animations are designed to be reusable and simply encode what the Animation is. The motivation here is that all the expensive work should be done once, when the Animation is created. To actually animate the Vars, the user will need to create a timeline and set it to the Animation.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;tl&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;animator&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewTimeline&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;tl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;SetAnimation&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;moveDown&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will initialise all the Var values to what they would be at frame 0 (at the moment, frame 0 must be a time 0, but down the line I&amp;rsquo;d like to remove this to allow for blending between Animations). The value of each Var can now be retrieved, as Vars only have a value within the scope of a Timeline.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;xPos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;tl&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;yPos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;tl&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// x and y == 0, as that is the value they were set to in the first frame&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;drawRect&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The Timeline clock is advanced by calling the &lt;code&gt;Advance()&lt;/code&gt; method, which takes a delta in fractions of a second. This will tween all the Vars based on the current action, and the new value can be retrieved by calling &lt;code&gt;Value()&lt;/code&gt; again. Thus the core use of this package is advancing the clock and querying the values for each tweened frame.&lt;/p&gt;
&lt;p&gt;Within an Ebitengine project, this is done by calling &lt;code&gt;Advance&lt;/code&gt; within &lt;code&gt;Update&lt;/code&gt; function and sampling the values within &lt;code&gt;Draw&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;tl&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;animator&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Timeline&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;xPos&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;animator&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FloatVar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;yPos&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;animator&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FloatVar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Update&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;dt&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; float64(&lt;span style=&#34;color:#a6e22e&#34;&gt;ebiten&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;TPS&lt;/span&gt;())  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;tl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Advance&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;dt&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Draw&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;screen&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ebiten&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Image&lt;/span&gt;) {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;vector&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FillRect&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;screen&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       float32(&lt;span style=&#34;color:#a6e22e&#34;&gt;xPos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;tl&lt;/span&gt;)), float32(&lt;span style=&#34;color:#a6e22e&#34;&gt;yPos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;tl&lt;/span&gt;)),  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;col&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here&amp;rsquo;s an test animation:&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://cdn.uploads.micro.mov/25293/2025/cleanshot-2025-12-30-at-17.14.42/playlist.m3u8&#34; poster=&#34;https://cdn.uploads.micro.blog/25293/2025/frames/1647920-0-36a228.jpg&#34; width=&#34;1232&#34; height=&#34;720&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m quite happy with how this turns out. I like how little &amp;ldquo;magic&amp;rdquo; is involved in the API: values and clocks are not changed from underneath you: you need to explicitly advance the clock yourself, and read the values when you need them.&lt;/p&gt;
&lt;p&gt;Obviously there&amp;rsquo;s room for improvement. Tracks and keyframes cannot be changed; only linear interpolation of float values are supported; and there&amp;rsquo;s no looping, bouncing, or reversing of animations. I&amp;rsquo;d hope to add all this in due time, probably based on the needs of the project I&amp;rsquo;m working on. There are also a few things I&amp;rsquo;m a little wary of, such as a global atomic int responsible for allocating an ID for each newly created Var. And the Timeline holds Var values in a map which I hope is fast enough for rendering on a screen.&lt;/p&gt;
&lt;p&gt;But it&amp;rsquo;s nice to have something like this in my toolbox. It&amp;rsquo;s been something I&amp;rsquo;ve been wishing for a while.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/12/27/just-placed-the-exit-on.html</link>
      <pubDate>Sat, 27 Dec 2025 09:15:38 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/12/27/just-placed-the-exit-on.html</guid>
      <description>&lt;p&gt;Just placed the exit on Level 3-2 on my Godot game. The level still needs prettying up but the critical path is now done. I&amp;rsquo;ll think this will be the last level I build. There&amp;rsquo;s still so much work to do: polishing, backdrops, menus, etc. But no more content. Kind of want to put a pin in this one.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20251227-101258.png&#34; alt=&#34;Auto-generated description: A 2D platformer game level with block structures, steps, and floating platforms.&#34;&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Godot Project - Level 3-2 And Bobbing Water</title>
      <link>https://lmika.org/2025/12/08/devlog-godot-project-level-and.html</link>
      <pubDate>Mon, 08 Dec 2025 21:32:32 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/12/08/devlog-godot-project-level-and.html</guid>
      <description>&lt;p&gt;I spent a lot of time on distractions recently, so I was a little surprised to find myself wanting to get back to working on that Godot game. The level under design is going okay: it took a few attempts to find the right way to start. Turns out writing down how you want the level to progress helps, like having three &amp;ldquo;acts&amp;rdquo; where the first act consists of some jumping puzzles, then introducing one of the gimmick, than a variant of said gimmick, and so on.&lt;/p&gt;
&lt;p&gt;Anyway, I&amp;rsquo;m working on the second &amp;ldquo;act&amp;rdquo; which will feature a low-power mechanic: there&amp;rsquo;s only so many units of power to energise the various zones, and the player needs to juggle it all to get through the act. Because power is involved, I wanted to have a generator sprite to provide some decoration. This is what I came up with:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20251203-221842.png&#34; width=&#34;600&#34; height=&#34;327&#34; alt=&#34;Auto-generated description: A pixelated knight stands next to a large, metal generator in a block-brick environment with blue water below.&#34;&gt;
&lt;p&gt;It&amp;rsquo;s… fine. I did try to get some inspiration by uploaded the tile-set to Google Gemini and asked it to produce something that resembled a generator:&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;Using the tile set images uploaded, please provide some suggested designs for a tile-set representing an electric generator. The suggestion must match the style and colour scheme of the supplied image.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;This helped, although what I have here is a fair bit different to what was generated. I may adjust the colours a bit (the simple colours are by design as I want to maintain the palette used by the existing tile-set) and I may need to add some wire decorations and maybe a sign that says &amp;ldquo;Generator&amp;rdquo; to make it clear that&amp;rsquo;s what it is, and that it&amp;rsquo;s not a cannon, as the alt-text generation suggests. We&amp;rsquo;ll see how it goes, I guess.&lt;/p&gt;
&lt;p&gt;One other thing I found myself wanting to add is water that can be raised and lowered when activated. I came up with something from first principals that looks a little like this (the tile layer is hidden to show the &lt;code&gt;Area2D&lt;/code&gt; shapes):&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20251208-221530.png&#34; width=&#34;600&#34; height=&#34;226&#34; alt=&#34;Auto-generated description: A 2D platformer game editor displays a character, crates, water sections, and terrain blocks.&#34;&gt;
&lt;p&gt;It consists of is a top-level &lt;code&gt;Node2D&lt;/code&gt; which will raise and lower when triggered. This is done in code, just to allow me to configure the speed and displacement on a case-by-base basis. A child of that is a &lt;code&gt;TileMapLayer&lt;/code&gt; containing the water tiles. It&amp;rsquo;s at Z-index 6, which is just in front of the player so that it looks like it engulfs them if they fall in. Four units below the top is the kill-plane — water is a hazard — and four units below that is the &amp;ldquo;bobbing&amp;rdquo; plane. This is a &lt;code&gt;Node2D&lt;/code&gt; that simply oscillates up and down by 2 units every second. A child of that is an &lt;code&gt;Area2D&lt;/code&gt; solid with allows the player to move through, but will collide with a new crate sprite, which also collides with everything else.&lt;/p&gt;
&lt;p&gt;This has the effect of a water plane in which creates would appear buoyant yet slightly submerged, giving the player somewhere to stand. Activating the water layer will allow it to &amp;ldquo;drain,&amp;rdquo; bringing the crates down to rest on any solids. Activate it again and the water level will rise, catching any crates which will begin floating:&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://cdn.uploads.micro.mov/25293/2025/screencast-water-bobbing/playlist.m3u8&#34; poster=&#34;https://cdn.uploads.micro.blog/25293/2025/frames/1633294-2-a7753f.jpg&#34; width=&#34;1158&#34; height=&#34;720&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s probably better ways to do this, and there are some drawbacks. It can&amp;rsquo;t be bundled into a dedicated scene, so each instance will need to be built manually. And if more than one crate is in the scene, they will bob in unison (although that could probably be alleviated with splitting the bobbing plane into areas with distinct offsets). But for something devised from first principals, I&amp;rsquo;m quite happy with it.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Blogging Tools - Image Collections Triage App</title>
      <link>https://lmika.org/2025/11/29/devlog-blogging-tools-image-collections.html</link>
      <pubDate>Sat, 29 Nov 2025 09:54:02 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/11/29/devlog-blogging-tools-image-collections.html</guid>
      <description>&lt;p&gt;Up until now, I&amp;rsquo;ve been using Blogging Tools&amp;rsquo; in-app notification system to sort images to collections. There were quite a few limitations in doing so: only notifications for the first for images were raised, galleries weren&amp;rsquo;t handled, and you could only select one category per image. I also wanted a way to automatically change the header image in this blog. This was done using the image category notification, but it was pretty basic in that the image was chosen as the header if any category was selected.&lt;/p&gt;
&lt;p&gt;I had ideas of improving all of this but I realised quickly that I needed something more sophisticated to do so. So I added another app to Blogging Tools to do this. I also thought to try out using Antigravity to do this. I was curious to know how well Google Gemini would work with a project that already had an established architecture and coding patterns. I was also curious to know how Antigravity felt when I held a tighter grip of the reigns: I wanted to drive the development work and only turning to the agent when I need something done.&lt;/p&gt;
&lt;p&gt;I started simply enough: I built a basic HTML template for this new app and asked Gemini to prepare a stub handler:&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;Please create a new handler that services the template &amp;ldquo;imageposts/show.html&amp;rdquo;. Implement it much like the other handlers found in the &amp;ldquo;handlers&amp;rdquo; directory.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;I forgot to mention to the agent that this was to be a stub, and it ended up using the existing provider for the gallery app. But it was pretty close to what I wanted. I then asked it to suggest a good HTML entity for opening a page in an external window.&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;What&amp;rsquo;s a good HTML entity for indicating that a link will open up into an external window/tab?&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;It suggested &lt;code&gt;↗&lt;/code&gt;, also a good answer. I then went on to build the Go models and wanted to work on the CSS styling next, so I asked Gemini to produce some example data using these models:&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;Please generate 3 stubbed values for this item slice. Use real example image URLs for the source.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;It struggled a little on this one, suggesting a URL that didn&amp;rsquo;t exist. But after pointing this out, it got there in the end. It was at this point that I needed to do some refactoring to some of the existing services, needing to extract out a type that used only the values from another type. Gemini did quite well here too:&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;Note the select call. At the moment this is returning a micropub.Item model. I want to make a new model called &amp;ldquo;ImageCollection&amp;rdquo; that uses only the values of this micropub.Item that the code is currently using now. I don&amp;rsquo;t want the micropub.Item changed, but any code that is using this for the purpose of the selected call should include a map to this new model.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;I guess this is not a novel experience to anyone using Cursor, but what I really liked about this interaction is that I was free to work on other things or move away from the keyboard entirely while the agent was working. This is one annoyance that I have with Claude Code: the exchange pauses and waits for me to approve every change before it proceeds. Having Gemini plan the changes first, then have me approve them once I&amp;rsquo;m back is a much nicer way of working.&lt;/p&gt;
&lt;p&gt;I then turned to the database schema, defining the table schema and SQL queries myself, and asking Gemini to produce the Go code that marshals the data: the so called &amp;ldquo;database providers&amp;rdquo;.&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;Implement the Go DB provider for DB tables for the queries in &amp;ldquo;imageposts.sql&amp;rdquo;. Do it in a similar style to the other providers found in &amp;ldquo;providers/db&amp;rdquo;.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;This is where I get the most use out of using coding agents. This code is not hard, but it&amp;rsquo;s super annoying to write. I think it also helps that there&amp;rsquo;s an established code base for the agent to work on. I was a little unhappy with what the agent produced last week, probably because it was required to produce it from scratch and in a way that wasn&amp;rsquo;t how I would write it. But here, probably because the agent had existing patterns to work on, it&amp;rsquo;s producing something that looks a lot better, to my eyes at least.&lt;/p&gt;
&lt;p&gt;It did struggle with the providers a little, choosing to merge an update to two models in a single method. I did try to correct it, but I just had to go in and fix it manually.&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;We can probably separate SaveImagePost into one that saves the ImagePost, and another that saves ImagePostItems.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;One other thing I discovered is that getting Gemini to write unit tests is important, as it provides a way for the agent to verify it&amp;rsquo;s code. I approved a change Gemini produced without looking too closely, and if I were to test it myself, it would&amp;rsquo;ve failed. But only after I asked it to produce a unit test did I see it fix the errors introduced in the last interaction. Something to think about in the future.&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;Might be a good idea to implement a unit test to verify that the DB methods in imageposts.go work. Do this by creating a new db.Provider instance with a temporary file, and verifying that the provider methods of imageposts.go work as expected. Please use testify.Assert to do the asserts.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;It was now time to build the service layer. This is something I wanted to drive myself, occasionally relying on auto-suggestions. Antigravity fell down (ha ha) here a little, producing suggestions that were pretty far from what I wanted. I guess it was lacking the context to know what I actually wanted, which is somewhat understandable as I wasn&amp;rsquo;t giving that context. It also seems to fight with IntelliSence from VS Code sometimes. I occasionally press tab in response to a type suggestion from the index, only to have it produce a completion for some unknown type. I do wish the suggestions use the code index more in general. There&amp;rsquo;ve been more than a few times where the agent suggested types and methods that just didn&amp;rsquo;t exist. Might be something that can be tuned.&lt;/p&gt;
&lt;p&gt;One quality of life improvement I did make was increase the timeout for when suggestions would appear. The default was something like 50 milliseconds, which is pretty short. Bumping it to 1 second helps, but even so they still get in the way. I may bump it a little higher.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/pasted-image-20251129100529.png&#34; width=&#34;600&#34; height=&#34;449&#34; alt=&#34;Auto-generated description: A web interface for blogging tools features options to categorize or set an image of a scenic landscape as a header.&#34;&gt;
&lt;figcaption&gt;First cut of the collection triage screen at the end of day one.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I worked on this feature over the next couple of evenings, occasionally changing tack which required a change in the database schema. One thing I did like while working on this was getting to the point where I could simply ask the agent to update the provider code whenever I did so, and it knew what I meant:&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;The ImagePost model and table schema now has a PostDate field/column. Please update the providers.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;I do get the feeling that the user interaction in Antigravity needs to be refined a little. For someone who is willing to do most of the driving, it would be nice if the editor was a little less &amp;ldquo;needy&amp;rdquo;. This is probably more to do with VS Code being the underlying editor: lots of annoying popups and mouse overs, all shouting for my attention: LOOK AT THIS, LOOK AT THIS. But the AI suggestions don&amp;rsquo;t help. Instead of displaying the generated code, disrupting my sense of where everything is, maybe a more subtler approach is warranted: a small indicator beside the cursor or in the margin indicating that a suggestion is present. I may miss it, but who cares? Remember, I&amp;rsquo;m the one driving here.&lt;/p&gt;
&lt;p&gt;Today, I got the feature finished. The way it works is that it will poll the RSS feed of this blog, and for every post that has an image, it will add an entry for me to triage. Each entry consists of a brief summary, plus the images found within the post:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/pasted-image-20251129100017.png&#34; width=&#34;600&#34; height=&#34;449&#34; alt=&#34;Auto-generated description: A menu interface displaying various blogging tools and options for managing images, videos, and text.&#34;&gt;
&lt;figcaption&gt;A new &#39;Sort Image Posts&#39; app, with a badge indicating the number of images to triage.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/pasted-image-20251129100346.png&#34; width=&#34;600&#34; height=&#34;449&#34; alt=&#34;Auto-generated description: Blogging interface featuring a post titled Under the big, grey sky-rail with a small image and options to edit or delete.&#34;&gt;
&lt;figcaption&gt;List of posts with images that need triaging.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Clicking through to the post would list the collections, and I would select the ones I&amp;rsquo;d like the image to be filed in. And this would list all the images of the post, not just the first few. It is all or nothing, unfortunately. There are ways to get the images of the collection, but I couldn&amp;rsquo;t see a way for me to get the collections of an image. I don&amp;rsquo;t think this would be a problem, as Blogging Tools is the only way I actually set the collections of an image.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/pasted-image-20251129100432.png&#34; width=&#34;600&#34; height=&#34;449&#34; alt=&#34;Auto-generated description: A screen displays a blogging tool interface with an image of an elevated roadway and options for categorizing an image post.&#34;&gt;
&lt;figcaption&gt;Selecting a post will list the images found within that post, with the collections they should be added to.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Saving the categories will queue the changes, which will be applied every 5 minutes. A first cut of this feature applied the changes immediately, but the way I imagine using this is that I&amp;rsquo;d go in, triage the images in one go, then leave. Applying all the changes then felt inefficient.&lt;/p&gt;
&lt;p&gt;When it comes to setting the header image, I figured I&amp;rsquo;d add a dedicated collection for that. Every hour a cronjob will query the local database for images that is within this category, and write the URLs to a manifest file in the assets Git repository. A Forgejo Action on that repository runs every night, and will check the date-stamps for the next image to display on the header. If one is found, it will fetch the image, save it as a JPEG with reduced quality and upload to Netlify. Since Netlify flushes the CDN whenever a deployment happens, nothing more needs to be done: no MD5 sum hashes, no adjustment to CSS files. Just update &lt;code&gt;header.jpg&lt;/code&gt; and go:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Example of the manifest file.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;images&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;date&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2025-11-22T13:00:00Z&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://lmika.org/uploads/2025/pxl-20251120-083552448.jpg&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;crop&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;bottom&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;date&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2025-11-26T13:00:00Z&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://lmika.org/uploads/2025/a3c74bfebd.jpg&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Deployed all these changes to prod and naturally some rework that&amp;rsquo;s needed. First, I completely forgot about galleries. They do come through as image tags in the generated HTML, but I&amp;rsquo;m operating on the source post, and they appear as Hugo short-codes. Fortunately the way I write galleries is pretty standardised now, so I think a regular expression would work here.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;galleryRegexp&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;regexp&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;MustCompile&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;`[{]{2}[&amp;lt;][ ]?glightbox.*src=&amp;#34;([^&amp;#34;]+)&amp;#34;.*[&amp;gt;][}]{2}`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Oh, I also want to fix the layout of the collection checkboxes and hide some options, such as those that for past years. And finally, will turn off the existing image category notification logic.&lt;/p&gt;
&lt;p&gt;Deployed again, and now I just need to wait for the next time I post a photo. I suspect some bug fixes or usability adjustments will come from this, but more on that when that happens.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Blogging Tools - Chunking File Uploader</title>
      <link>https://lmika.org/2025/11/16/devlog-blogging-tools-chunking-file.html</link>
      <pubDate>Sun, 16 Nov 2025 19:40:43 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/11/16/devlog-blogging-tools-chunking-file.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;m hoping to use Micro.blog more for video hosting, meaning that I would like a fast way to upload them. Because my internet connection is not fast, I&amp;rsquo;m looking at adding some fancy uploading techniques to Blogging Tools. Blogging Tools does have a simple file repository, and I&amp;rsquo;ve already built a reusable upload component, which uploads the contents of a file with a single HTTP request. My goal is to improve the performance of this by using a few of the techniques &lt;a href=&#34;https://dev.to/leapcell/how-to-handle-large-file-uploads-without-losing-your-mind-3dck&#34;&gt;found on this dev.to page&lt;/a&gt;, such as chunking and concurrency.&lt;/p&gt;
&lt;p&gt;But first, I start with some baseline tests. I will simulate a slow upload by throttling the browser speeds at &amp;ldquo;Fast 4G&amp;rdquo;, and test the time it takes to upload a 45.4 MB file.&lt;/p&gt;
&lt;p&gt;Test done. It took 269.174 seconds, about 4.49 minutes.&lt;/p&gt;
&lt;p&gt;So my first goal is add some chunking and concurrency uploads, similar to what was suggested in the linked article. The way I&amp;rsquo;m thinking of doing this is as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Modify the &amp;ldquo;new upload&amp;rdquo; request handler to accept a maximum file size, a chunk size, and the expected number of chunks. The server will respond by creating a empty file of that size, ready to be written to.&lt;/li&gt;
&lt;li&gt;I&amp;rsquo;ll then make multiple fetches requests, each uploading a single chunk, identified by the chunk&amp;rsquo;s starting offset.&lt;/li&gt;
&lt;li&gt;Then, once all chunks are uploaded, I&amp;rsquo;ll send a finalise request with a file hash. The server will also calculate the hash and say whether or not the upload was successful.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I&amp;rsquo;ll ignore the progress indicator for now, but that will need to be updated to reflect the actual upload progress.&lt;/p&gt;
&lt;p&gt;First pass is to simply make sure the chunking works and doesn&amp;rsquo;t corrupt the uploaded file. I&amp;rsquo;ll begin on the server side by creating an empty file of size N. It was difficult to find how to do this in Go, but it turns out the &lt;a href=&#34;https://pkg.go.dev/os#Truncate&#34;&gt;os.Truncate&lt;/a&gt; method can be used to set the file size, once the file has been touched:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// This creates the file and sets it&amp;#39;s length  &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;fileName&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cfs&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;fsProvider&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FilePath&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;SymID&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;os&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Create&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;fileName&lt;/span&gt;); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Close&lt;/span&gt;()  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;os&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Truncate&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;fileName&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Size&lt;/span&gt;); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cfs&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;fsProvider&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;OpenFileFlags&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;SymID&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;os&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;O_RDWR&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cfs&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;fileHandles&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ID&lt;/span&gt;] = &lt;span style=&#34;color:#a6e22e&#34;&gt;uploadSessions&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;mutex&lt;/span&gt;:      &lt;span style=&#34;color:#a6e22e&#34;&gt;sync&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Mutex&lt;/span&gt;{},  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;fileHandle&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;size&lt;/span&gt;:       &lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Size&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;startTime&lt;/span&gt;:  &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Now&lt;/span&gt;(),  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The rest of the Go code is pretty straightforward: requests to upload a file chunk will simply be passed down to the file handle and written at the requested position using &lt;code&gt;WriteAt&lt;/code&gt;. The finalise call will seek to the start of the file, compute the SHA-1, and compare it with the SHA-1 computed by the browser. MD5 would probably have been my preferred hash because of it&amp;rsquo;s speed, but SHA-1 seems to be the one &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest&#34;&gt;supported natively in browsers&lt;/a&gt;. The open files are kept in an in-memory &amp;ldquo;upload session&amp;rdquo; data structure. Finalising will also close the file handle and remove the session, but I do need to add a &amp;ldquo;session reaper&amp;rdquo; to deal with any sessions that died or terminated early.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the first cut of the JavaScript which does the chunking upload:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fileMetadata&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;window.&lt;span style=&#34;color:#a6e22e&#34;&gt;bgtData&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;resPrefix&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/files/new`&lt;/span&gt;, { 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;method&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;POST&amp;#34;&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;headers&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Content-type&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;body&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;JSON&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;stringify&lt;/span&gt;({  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mime_type&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;mimeType&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;size&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;size&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    })  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})).&lt;span style=&#34;color:#a6e22e&#34;&gt;json&lt;/span&gt;();  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;chunkSize&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1024&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1024&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;chunks&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;ceil&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;size&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;chunkSize&lt;/span&gt;);  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;; &lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;chunks&lt;/span&gt;; &lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;++&lt;/span&gt;) {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// TODO: retries  
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;didUpload&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;_uploadFile&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;chunkSize&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fileMetadata&lt;/span&gt;);  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Finalise the upload  
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;_finalizeUpload&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fileMetadata&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Doing a quick test to see how this performs, although I&amp;rsquo;m not expecting any improvements. And indeed, there weren&amp;rsquo;t. In fact, it&amp;rsquo;s slower, at 270.301 seconds (4.51 minutes).&lt;/p&gt;
&lt;p&gt;But this is where the performance boost comes in, in theory: replacing the loop with a call to &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all&#34;&gt;Promise.all&lt;/a&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;chunkSize&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1024&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1024&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;chunks&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;ceil&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;size&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;chunkSize&lt;/span&gt;);  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Prepare promises for each upload chunk, then dispatch them all at once  
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;uploadPromises&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Array(&lt;span style=&#34;color:#a6e22e&#34;&gt;chunks&lt;/span&gt;);  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;; &lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;chunks&lt;/span&gt;; &lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;++&lt;/span&gt;) {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;uploadPromises&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;_uploadFile&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;chunkSize&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fileMetadata&lt;/span&gt;);  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; Promise.&lt;span style=&#34;color:#a6e22e&#34;&gt;all&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;uploadPromises&lt;/span&gt;);  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Finalise the upload  
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;_finalizeUpload&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fileMetadata&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Testing this approach. As expected, the progress bar is jumping around as each upload fights to update it. But we can deal with that later. We&amp;rsquo;re only interested in the upload time.&lt;/p&gt;
&lt;p&gt;And the results are in. And sadly, they&amp;rsquo;re not much better: 269.597 seconds, about 4.49 minutes.&lt;/p&gt;
&lt;p&gt;I am curious if this has something to do with the browser limiting the number of parallel uploads per domain. I hacked a version of the JavaScript which would send each chunk to a separate subdomain, so chunk 0 will go to &lt;code&gt;u0.localhost:3000&lt;/code&gt;, chunk 1 will go to &lt;code&gt;u1.localhost:3000&lt;/code&gt; and so on. This does mean adding &lt;a href=&#34;https://docs.gofiber.io/api/middleware/cors/&#34;&gt;CORS middleware&lt;/a&gt; to Blogging Tools, as this would be considered cross-origin requests. But alas, this didn&amp;rsquo;t help matter: the upload time was still around 269 seconds.&lt;/p&gt;
&lt;p&gt;So the bottleneck must be the connection itself. I was afraid of that, especially when you consider that the browser and OS would be using all the available network bandwidth to do the upload. I can&amp;rsquo;t think of any reason why it would be throttled. Another issue might simply be distance. Blogging Tools is currently hosed in Germany, since Hetzner doesn&amp;rsquo;t offer any hosting locations in Australia. But there&amp;rsquo;s also Singapore, which is quite close. Maybe it&amp;rsquo;s worth moving Blogging Tools there.&lt;/p&gt;
&lt;p&gt;Anyway, it may still be useful to keep the chunking uploader around, if for no other reason just to avoid timeouts due to long running connections. So I undid all that multi-domain work and finishing the feature off with a stalled upload reaper which will close file handles that haven&amp;rsquo;t been written to in the last 5 minutes. I also found that I wasn&amp;rsquo;t doing anything to throttle the fetch requests: a hundred or so were dispatched at once, and I think the stalled upload reaper was having an effect on all the requests fighting for bandwidth: I found uploads failing because  no single request managed to finish after 5 minutes, and the reaper was killing the upload.&lt;/p&gt;
&lt;p&gt;So made a few small changes to only dispatch batches of 5 at a time, each with a 2 MB chunk, and bumped the reaper to wait for 30 minutes. Gave it a test to see how long it took to upload a 469.4 MB file. It was not quick: 7,112 seconds, or around 118.5 minutes, or 1.98 hours. It did work, and I managed to get the file to Blogging Tools. Forwarding it on to Micro.blog failed though: turns out the file was too big anyway. So yeah, may need to do something about that.&lt;/p&gt;
&lt;p&gt;Now, this may have been wasted effort, but I think it&amp;rsquo;s worth keeping. There are some benefits from uploading files this way, such as not having connections die due to timeouts. You also have the opportunity to retry particular chunks that individually fail, without causing the entire upload to be wasted. And I do think they are being uploaded in parallel: seeing the 5 requests in the console end around the same time suggests that they&amp;rsquo;re not entirely sequential. But I think the limiting factor here is that my upload speeds are terrible. It&amp;rsquo;d be probably easier upgrading the connection first before embarking on any more fancy upload techniques.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Laying The Groundwork For Dynamic Header Images</title>
      <link>https://lmika.org/2025/11/08/devlog-laying-the-groundwork-for.html</link>
      <pubDate>Sat, 08 Nov 2025 11:11:26 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/11/08/devlog-laying-the-groundwork-for.html</guid>
      <description>&lt;p&gt;Making some changes to the Card theme I&amp;rsquo;m using for this blog. First think I&amp;rsquo;m considering is a banner image, similar to the one in Scripting News. And like Scripting News, I&amp;rsquo;m hoping for the image to change occasionally. I&amp;rsquo;d like the change to happen when the blog is being built, and in order to do this, I need a way to configure this value. I&amp;rsquo;m hoping to use Blogging Tools to do this, but to actually make use of these values, I&amp;rsquo;m hoping to use &lt;a href=&#34;https://gohugo.io/methods/resource/data/#article&#34;&gt;Hugo&amp;rsquo;s resource data&lt;/a&gt; methods.&lt;/p&gt;
&lt;p&gt;The idea is to setup an assets project which will be hosted on Netlify. When I want to change anything in it, Blogging Tools will push new changes via Git, which will run a pipeline that will deploy the updates to Netlify. Then, next time I add a blog post, Hugo will pick up these data fields and use it to build the site, probably by setting an attribute that will itself be picked by CSS&amp;rsquo;s newly improved &lt;a href=&#34;**https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/attr&#34;&gt;attr()&lt;/a&gt; function to set the banner background.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/assets-lmika-org.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;a9d076a666de56db96237c1af78f6cc3&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/assets-lmika-org.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;assets-lmika-org.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;This will start off as simple data values, stored in JSON, which will include things like the current banner header. One of the reasons why I&amp;rsquo;m keeping the actual data separate is that I don&amp;rsquo;t want any issues with Blogging Tools from impacting my ability to publish blog posts, and I have more confidence in Netlify&amp;rsquo;s ability to keep a service up than I do, especially as a company that is getting paid to do so.&lt;/p&gt;
&lt;p&gt;Being a &amp;ldquo;backend guy&amp;rdquo;, I&amp;rsquo;ll start on the Netlify and Blogging Tools side of things first. Actually, I&amp;rsquo;ll start on the asset repo first, just so I can verify that Micro.blog&amp;rsquo;s Hugo instance can pull data like this.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve added the new Git repo and got it publishing to Netlify on commit to main. At the moment there&amp;rsquo;s a single JSON file, &lt;code&gt;/site/data.json&lt;/code&gt; that has a test message. The goal is now to get that onto my test blog and included in the Hugo template on build. Naturally, because Hugo loves changing their public API so often, I need to see how this is done in Hugo 0.117 as it seems to be completely different in Hugo 0.141, the current version as of this writing.&lt;/p&gt;
&lt;p&gt;So, checking out the Hugo source code and doing a &lt;code&gt;find&lt;/code&gt;/&lt;code&gt;grep&lt;/code&gt; search to find the relevant technique. Turn&amp;rsquo;s out the way to do this in Hugo 0.117.0 is to use the &lt;code&gt;getJSON&lt;/code&gt; function. &lt;a href=&#34;https://github.com/gohugoio/hugo/blob/release-0.117.0/docs/content/en/templates/data-templates.md&#34;&gt;Here&amp;rsquo;s a link to the docs&lt;/a&gt; for anyone else who&amp;rsquo;s interested.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  {{ $siteData := getJSON &amp;#34;https://assets.lmika.org/data/site.json&amp;#34; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;&amp;gt;My data is {{$siteData.data}}&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Okay, that seams to work:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20251108-102005.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;a9d076a666de56db96237c1af78f6cc3&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/out-20251108-102005.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;out-20251108-102005.png&#34; 
        
  /&gt;
&lt;/a&gt;


So, the next thing to do is to add a banner image. I&amp;rsquo;m thinking of a slightly washed out version of the image, with a subtle gradient to the body housing the cards.  I had to adjust the header template a little to move the &lt;code&gt;site-header&lt;/code&gt; to a wrapping &lt;code&gt;div&lt;/code&gt;. This allowed me to add new CSS to allow the header to span the entire width of the page. It&amp;rsquo;s really fortunate that the Card theme styles the CSS classes, and not the HTML elements themselves. It also allowed me to add a new HTML element with a &lt;code&gt;header-end&lt;/code&gt; class to act as the gradient blend from the backing image to the regular background colour:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- Template: layouts/partials/header.html --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;header&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;site-header&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- The original content of &amp;#39;header&amp;#39; --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;header-end&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;header&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With that I could restyle the header:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-css&#34; data-lang=&#34;css&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;/** Header */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;body&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/* The body had a 10px padding which I had to disable to allow
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;     the header to blend all the way to the client sides. */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;padding&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;page-content&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;footer&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;site-footer&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/* Disabling the 10px padding on the body meant I had to move
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;     the padding here.*/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;padding&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;px&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;header&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/* Using &amp;#39;background-blend-mode&amp;#39; to mix the header image with the background
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;     colour. We&amp;#39;re using &amp;#39;color-mix&amp;#39; to make the background color slighty
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;     transparent to do this. */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;background-color&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;color-mix&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    in srgb, 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;var&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;--&lt;/span&gt;body&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;background&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;color&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    rgba(&lt;span style=&#34;color:#ae81ff&#34;&gt;255&lt;/span&gt;,&lt;span style=&#34;color:#ae81ff&#34;&gt;255&lt;/span&gt;,&lt;span style=&#34;color:#ae81ff&#34;&gt;255&lt;/span&gt;,&lt;span style=&#34;color:#ae81ff&#34;&gt;0.75&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;background-image&lt;/span&gt;: url(&lt;span style=&#34;color:#e6db74&#34;&gt;https://lmika.org/uploads/2025/c1745b14d3.jpg&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;background-blend-mode&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;screen&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;background-size&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;cover&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;background-position&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;%&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;margin&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;header-end&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/* A small gradient from transparent to a fully opaque background colour
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;     allows a smooth transition from the header to the body. */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;height&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;px&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;background&lt;/span&gt;: linear-gradient(&lt;span style=&#34;color:#ae81ff&#34;&gt;180&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;deg&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    rgba(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;%&lt;/span&gt;, 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;var&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;--&lt;/span&gt;body&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;background&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;color&lt;/span&gt;) &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;%&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Various links to MDN documentation covering all these changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/background-blend-mode&#34;&gt;background-blend-mode&lt;/a&gt; to blend the header image with the background colour. This is to wash it out some so that the menu items are not illegible.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/color-mix&#34;&gt;color-mix&lt;/a&gt; to set the transparency on the background colour.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/gradient/linear-gradient&#34;&gt;linear-gradient&lt;/a&gt; to generate a linear gradient.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20251108-110434.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;a9d076a666de56db96237c1af78f6cc3&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/out-20251108-110434.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;out-20251108-110434.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Oh, and I naturally forgot dark mode, so added a new style to darken the image in that mode:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-css&#34; data-lang=&#34;css&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@&lt;span style=&#34;color:#66d9ef&#34;&gt;media&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;prefers-color-scheme&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;dark&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;header&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;background-color&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;color-mix&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      in srgb, 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;var&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;--&lt;/span&gt;body&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;background&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;color&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      rgba(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,&lt;span style=&#34;color:#ae81ff&#34;&gt;0.5&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;background-blend-mode&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;darken&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20251108-111245.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;a9d076a666de56db96237c1af78f6cc3&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/out-20251108-111245.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;out-20251108-111245.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Okay, the next thing is to make the image dynamic. I want to try and use CSS &lt;code&gt;attr()&lt;/code&gt;  to do this. Basically the idea is that when the template is built, the image URL is pulled from the data and sent to the CSS via a HTML attribute. Here&amp;rsquo;s the new header template&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- Template: layouts/partials/header.html --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{{ $siteData := getJSON &amp;#34;https://assets.lmika.org/data/site.json&amp;#34; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;header&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data-background-url&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;{{ $siteData.header.imageUrl }}&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the updated CSS:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-css&#34; data-lang=&#34;css&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;header&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;background-image&lt;/span&gt;: url(&lt;span style=&#34;color:#e6db74&#34;&gt;attr(data-background-url raw-string&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ah, that didn&amp;rsquo;t work. Apparently there are &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/attr#limitations_and_security&#34;&gt;some restrictions on attr&lt;/a&gt; which could potentially result in security issues. So, plan B is to simply set the &lt;code&gt;background-image&lt;/code&gt; in a &lt;code&gt;style&lt;/code&gt; a style attribute:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- Template: layouts/partials/header.html --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{{ $siteData := getJSON &amp;#34;https://assets.lmika.org/data/site.json&amp;#34; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;header&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;style&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;background-image: url({{ $siteData.header.imageUrl }})&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Okay, that&amp;rsquo;s working. Now the final test: can I make it dynamic? I&amp;rsquo;ll change the URL to another image:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;header&amp;#34;&lt;/span&gt;:{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;imageUrl&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://lmika.org/uploads/2025/pxl-20251105-200617867.jpg&amp;#34;&lt;/span&gt;}}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And publish a new post to rebuild the template. Let&amp;rsquo;s have a look:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20251108-114024.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;a9d076a666de56db96237c1af78f6cc3&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/out-20251108-114024.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;out-20251108-114024.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Awesome, that&amp;rsquo;s working. Now to apply it to the actual blog. I did make one last change: the &lt;code&gt;header&lt;/code&gt; attribute is used for the header of posts too, so I had to add a class — &lt;code&gt;site-header-wrapper&lt;/code&gt; — to only select the site header. But yeah, it looks good. I like it. For reference, here&amp;rsquo;s the final template changes I&amp;rsquo;ve made:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- Template: layouts/partials/header.html --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{{ $siteData := getJSON &amp;#34;https://assets.lmika.org/data/site.json&amp;#34; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;header&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;site-header-wrapper&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;style&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;background-image: url({{ $siteData.header.imageUrl }})&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;site-header&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- The original content of &amp;#39;header&amp;#39; --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;header-end&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;header&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the final CSS changes:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-css&#34; data-lang=&#34;css&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;/** Header */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;body&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;padding&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;page-content&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;footer&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;site-footer&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;padding&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;px&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;header&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;site-header-wrapper&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;background-color&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;color-mix&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    in srgb, 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;var&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;--&lt;/span&gt;body&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;background&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;color&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    rgba(&lt;span style=&#34;color:#ae81ff&#34;&gt;255&lt;/span&gt;,&lt;span style=&#34;color:#ae81ff&#34;&gt;255&lt;/span&gt;,&lt;span style=&#34;color:#ae81ff&#34;&gt;255&lt;/span&gt;,&lt;span style=&#34;color:#ae81ff&#34;&gt;0.75&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;background-blend-mode&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;screen&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;background-size&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;cover&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;background-position&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;%&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;margin&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@&lt;span style=&#34;color:#66d9ef&#34;&gt;media&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;prefers-color-scheme&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;dark&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;header&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;site-header-wrapper&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;background-color&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;color-mix&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      in srgb, 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;var&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;--&lt;/span&gt;body&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;background&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;color&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      rgba(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,&lt;span style=&#34;color:#ae81ff&#34;&gt;0.5&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;background-blend-mode&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;darken&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;header-end&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/* A small gradient from transparent to a fully opaque background colour
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;     allows a smooth transition from the header to the body. */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;height&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;px&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;background&lt;/span&gt;: linear-gradient(&lt;span style=&#34;color:#ae81ff&#34;&gt;180&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;deg&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    rgba(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;%&lt;/span&gt;, 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;var&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;--&lt;/span&gt;body&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;background&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;color&lt;/span&gt;) &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;%&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I didn&amp;rsquo;t have time to integrate this with Blogging Tools, but the groundwork has now been laid for that now.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Dynamo Browse - Item View Annotations and Asynchronous Tasks</title>
      <link>https://lmika.org/2025/11/04/devlog-dynamo-browse-item-view.html</link>
      <pubDate>Tue, 04 Nov 2025 20:42:40 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/11/04/devlog-dynamo-browse-item-view.html</guid>
      <description>&lt;p&gt;The laundry list of things to do in Dynamo Browse has grown over the last week, as I find myself wanting using it and wanting more from it. I&amp;rsquo;ve knocked off many of the small ones: fixing bugs, making it easier to get the first item from a result set. The time has come to tackle some of the larger ones.&lt;/p&gt;
&lt;p&gt;The first is the ability to annotate fields in the item view, as in add additional information to the right of the value. I would personally find that useful for describing the value of an other table using it&amp;rsquo;s ID. But the point is that it can be anything. I&amp;rsquo;d like users to add these annotations via extensions, using UCL. The way I&amp;rsquo;m thinking of doing this is by &amp;ldquo;installing&amp;rdquo; a field annotator, something along the lines as follows:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;ui:add-item-annotator { |rs attr_path| 
    return &amp;#34;&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will add an annotator which will take a result set, and a path to an attribute. It will return the annotated value. We&amp;rsquo;ll see how well this will work, but first I&amp;rsquo;ll need to build the Go types. Rendering the item view is done using an &lt;code&gt;ItemRenderer&lt;/code&gt; which basically walks through the attributes of an item and renders it as a table. I think I&amp;rsquo;ll change this to take an item annotation. I also realised I need a type to represent the attribute path. I think a simple linked list would work here. Here&amp;rsquo;s what I got:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AttrPathNode&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;Name&lt;/span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;Index&lt;/span&gt;   &lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;IsIndex&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;bool&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;Parent&lt;/span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;AttrPathNode&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The trouble is representing both string attribute names and numerical indicies in the same type. Go doesn&amp;rsquo;t have a great way of doing this, so I&amp;rsquo;ve gone with something simple and added boolean which would be true if the node is actually an integer index to a list or set.&lt;/p&gt;
&lt;p&gt;Actually, that may not be necessary. Poking around the renderer code, I found this type used to represent the sub items of an item:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SubItem&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;Key&lt;/span&gt;   &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Renderer&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So I can simply use strings for the key. Good to know. Okay, added a test annotator and gave it a quick test:
&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-11-03-at-20.37.46.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;8765091e77c1a5bd3340e2f3fa6a373e&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-11-03-at-20.37.46.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-11-03 at 20.37.46.png&#34; 
        
  /&gt;
&lt;/a&gt;


Not bad, although found some glaring issues. It would be nice if the annotation was closer to the value. And laying out the annotations in a dedicated column will cause the entire column to shift as I move through the items. So instead of making it a separate column, I&amp;rsquo;ll try simply concatenating it to the end of the value. I&amp;rsquo;ll also apply the meta-info styling to dim the text a little:
&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-11-03-at-20.39.24.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;8765091e77c1a5bd3340e2f3fa6a373e&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-11-03-at-20.39.24.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-11-03 at 20.39.24.png&#34; 
        
  /&gt;
&lt;/a&gt;


That&amp;rsquo;s much better. I&amp;rsquo;m banking on only a few attributes having annotations, so I&amp;rsquo;m hoping it won&amp;rsquo;t be as busy as it looks here.&lt;/p&gt;
&lt;p&gt;Now to consider the UCL integration. Hmm, how am I going to connect the two? As expected, injecting the annotation directly into &lt;code&gt;ItemRenderer&lt;/code&gt; would introduce a dependency loop. What I could do instead is add a setter allowing the caller of the &lt;code&gt;ItemRenderer&lt;/code&gt; to change the annotation on the fly, and inject the &lt;code&gt;ItemRenderer&lt;/code&gt; as a dependency of the command controller.&lt;/p&gt;
&lt;p&gt;Implemented this, and it was actually easier than I thought.  Now to test this. I settled on the following UCL command:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;ui:set-item-annotator { |rs item path| 
    &amp;#34;annotation&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rs&lt;/code&gt; is the result set&lt;/li&gt;
&lt;li&gt;&lt;code&gt;item&lt;/code&gt; is the item being rendered in the view&lt;/li&gt;
&lt;li&gt;&lt;code&gt;path&lt;/code&gt; is the attribute path, represented as a list.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The attribute path is a new proxy, just to reduce the amount of memory copying between Go and UCL. I kept the same list index semantics, where &amp;gt; 0 starts from the left, and &amp;lt; 0 starts from the right, but it was a little mind bending to reverse this for a linked list.&lt;/p&gt;
&lt;p&gt;A simple annotator which returns the currently displayed item value as an annotation can be implemented as follows:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;ui:set-item-annotator { |rs item path|
    $item.($path.(-1))
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here it is in action:
&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-11-03-at-20.41.25.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;8765091e77c1a5bd3340e2f3fa6a373e&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-11-03-at-20.41.25.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-11-03 at 20.41.25.png&#34; 
        
  /&gt;
&lt;/a&gt;


Brilliant, it&amp;rsquo;s working! Naturally as commands are simple UCL statements, this could be entered on the command line.&lt;/p&gt;
&lt;p&gt;One thing to consider is that unlike normal commands, the UCL block rendering the annotation is running on the UI thread. Not sure I like this, but the amount of effort required to change this would be significant. I guess I&amp;rsquo;ll just tell everyone to keep annotation rendering fast. And as it&amp;rsquo;s currently implement, it is reasonably fast. Or more accurately, it&amp;rsquo;s not noticeably slow, which is good enough.&lt;/p&gt;
&lt;p&gt;The next feature to add to Dynamo Browse is a way to asynchronously schedule blocks. I think it may be worth tapping into the existing command looper in some way.&lt;/p&gt;
&lt;p&gt;Commands in Dynamo Browse are invoked on a dedicated goroutine. Calling &lt;code&gt;execute()&lt;/code&gt; will attempt to send a command via a channel. If that fails, &lt;code&gt;execute()&lt;/code&gt; will return an error indicating that a command is currently running. This keeps running UCL code off the UI thread, and it also makes it possible to implement commands like &lt;code&gt;ui:prompt&lt;/code&gt; synchronously from the UCL code&amp;rsquo;s perspective, when in reality it pauses this goroutine and sends a message to the UI with a callback to resume the thread:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;m&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;uiModule&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;uiPrompt&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ucl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;CallArgs&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;) (&lt;span style=&#34;color:#66d9ef&#34;&gt;any&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;) {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;prompt&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Bind&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;prompt&lt;/span&gt;); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;resChan&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; make(&lt;span style=&#34;color:#66d9ef&#34;&gt;chan&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;cancelChan&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; make(&lt;span style=&#34;color:#66d9ef&#34;&gt;chan&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt;{})  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;go&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;() {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;commandctrl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;PostMsg&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;events&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;PromptForInputMsg&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;Prompt&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;prompt&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;OnDone&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;tea&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Msg&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#a6e22e&#34;&gt;resChan&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            },  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;OnCancel&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;() &lt;span style=&#34;color:#a6e22e&#34;&gt;tea&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Msg&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#a6e22e&#34;&gt;cancelChan&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt;{}{}  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        })  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }()  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;select&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;resChan&lt;/span&gt;:  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cancelChan&lt;/span&gt;:  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Done&lt;/span&gt;():  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Err&lt;/span&gt;()  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So tapping into this event loop would be nice. But how does one do so?&lt;/p&gt;
&lt;p&gt;I think, probably the simplest way to do so is with buffered channels. These are bounded, meaning that there will be an upper limit to the number of pending tasks, but maybe that&amp;rsquo;s not a bad thing. Having too many pending tasks crowding out the user&amp;rsquo;s ability to run commands will probably make for a poor user experience. So let&amp;rsquo;s set the limit to something quite generous, say 50, and integrate it into the event loop:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;select&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cmdChan&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;cmdChan&lt;/span&gt;:  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ExecuteAndWait&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;cmdChan&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;cmd&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;postMessage&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;events&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Error&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;))  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        } &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;postMessage&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;events&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;StatusMsg&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;fmt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Sprint&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;)))  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;execCtx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;requestRefresh&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;postMessage&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;events&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ResultSetUpdated&lt;/span&gt;{})  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// New code here&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;task&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;pendingTaskChan&lt;/span&gt;:  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;task&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;task&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;postMessage&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;events&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Error&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;))  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now for the UCL API. I have an idea of adding a new &lt;code&gt;async&lt;/code&gt; package for this, which will provide commands for running things asynchronously. It&amp;rsquo;s simplest command would be &lt;code&gt;async:do&lt;/code&gt;, which will schedule a block when the command loop is free. So invoking the following:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;ui:command testasync {  
    echo &amp;#34;This&amp;#34;
    async:do {
        echo &amp;#34;Other&amp;#34;
        async:do { echo &amp;#34;Sierra&amp;#34; }
    }
    async:do {
        echo &amp;#34;Romeo&amp;#34;
    }
    echo &amp;#34;That&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Should display:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;This
That
Other
Romeo
Sierra
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the logs. Trying it out and here&amp;rsquo;s how it looks:
&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-11-03-at-21.31.42.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;8765091e77c1a5bd3340e2f3fa6a373e&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-11-03-at-21.31.42.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-11-03 at 21.31.42.png&#34; 
        
  /&gt;
&lt;/a&gt;


Okay, that&amp;rsquo;s pretty good. Now to build on this. The next thing to add is a command that will schedule tasks in the future, similar to &lt;code&gt;window.setTimeout()&lt;/code&gt;. For this, I plan to use the &lt;a href=&#34;https://github.com/go-co-op/gocron&#34;&gt;gocron&lt;/a&gt; package. Lots there, but for my purpose, I plan to use the &lt;a href=&#34;https://pkg.go.dev/github.com/go-co-op/gocron/v2@v2.17.0#OneTimeJob&#34;&gt;OneTimeJob&lt;/a&gt; type. It does feel like bringing in a crane to lift an empty wood pallet, but the alternative is building my own scheduler (or getting AI to vibe-code one) using a heap. Maybe something for later, but I think this is fine for now.&lt;/p&gt;
&lt;p&gt;To make use of this, I&amp;rsquo;ll add &lt;code&gt;async:in&lt;/code&gt;, which takes a timeout in seconds, and a block to run. To test this, I&amp;rsquo;ll do the classic &amp;ldquo;count down&amp;rdquo; tests:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;proc countdown { |from|
    if (le $from 0) {
        echo &amp;#34;Blast off!!&amp;#34;
        return
    }
    echo &amp;#34;$from&amp;#34;
    async:in 1 { countdown (sub $from 1) }
}

ui:testasync {
    countdown 10
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here it is in action:&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-11-03-at-22.12.51.mp4&#34; poster=&#34;https://lmika.org/uploads/2025/72bad33388.png&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;One last thing to add to async, the ability to run a query in the background and execute a block once the results are available, keeping long running queries off the UCL goroutine. I think for this I will add another thread-pool which will execute up to two queries in the background, then schedule a task to run once the results are ready.&lt;/p&gt;
&lt;p&gt;The API for running queries is pretty well established, to the point that I&amp;rsquo;ve actually got a helper functions which deal with the UCL side of things, so it&amp;rsquo;s just a matter of implementing an asynchronous version of this. I&amp;rsquo;m thinking of an interface along the following lines:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;async:query &amp;lt;query&amp;gt; { |rs|
    # do something with the result-set $rs
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It should also support keyword arguments too. Unfortunately, UCL requires keyword arguments to be placed after the positional arguments, so they would need to be placed after the block, which is a little yucky.&lt;/p&gt;
&lt;p&gt;Implemented the code, now for the test. This one&amp;rsquo;s a little contrived. What it does is asynchronously run a query on startup, counting the number of opened and closed offices in the &lt;code&gt;business-addresses&lt;/code&gt; table. Once those results are available, it will install a new column annotation, which will display the count to the right of the &lt;code&gt;officeOpened&lt;/code&gt; fields.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;proc _prep_officeCount {
    openedOffices = 0
    closedOffices = 0

    async:query &amp;#39;officeOpened=true&amp;#39; { |rs|
        openedOffices = len $rs
        async:query &amp;#39;officeOpened=false&amp;#39; { |rs|
            closedOffices = len $rs

            ui:set-item-annotator { |rs item path|
                if (eq $path.(-1) &amp;#34;officeOpened&amp;#34;) {
                    if $item.officeOpened {
                        &amp;#34;Count = ${openedOffices}&amp;#34;
                    } else {
                        &amp;#34;Count = ${closedOffices}&amp;#34;
                    }
                } else {
                    &amp;#34;&amp;#34;
                }
            }
        } -table business-addresses
    } -table business-addresses
}

_prep_officeCount
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It is contrived, but I&amp;rsquo;m hoping to use this in a real setting where I annotate ID fields with a value I retrieve from another table, so a use case like this is pretty close to why I built this feature. Because the queries are running in the background, the goal is to avoid blocking the UI thread, but since this runs on startup before the user selects a table, the table needs to be specified as part of the call. I may have to have a think of how I am to fix that.&lt;/p&gt;
&lt;p&gt;But here it is working. Note the &lt;code&gt;Count = 244&lt;/code&gt; next to the &lt;code&gt;officeOpened&lt;/code&gt; field:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-11-04-at-13.49.19.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;8765091e77c1a5bd3340e2f3fa6a373e&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-11-04-at-13.49.19.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-11-04 at 13.49.19.png&#34; 
        
  /&gt;
&lt;/a&gt;


Excellent. The next thing to do is try this out in a real setting. I finished off making a proper goroutine pool for the task that actually runs the query — which I&amp;rsquo;m calling the &amp;ldquo;aux task pool&amp;rdquo; — and a few other changes, like making the meta information a little easier to see.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Godot Game - Level 3-2 and a Rotating Platform</title>
      <link>https://lmika.org/2025/10/28/devlog-godot-game-level-and.html</link>
      <pubDate>Tue, 28 Oct 2025 21:19:28 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/10/28/devlog-godot-game-level-and.html</guid>
      <description>&lt;p&gt;Okay, time to start level 3-2. This, like level 3-1, is in the mountainous regions. Except this time, the player will be a little higher. So that means jagged platforms, more verticality, lots of gaps: a real sense that care of where one steps must be taken.&lt;/p&gt;
&lt;p&gt;Starting with some platforming to give this sense: a few narrow safe areas, separated by pits, leading into an area with platforms.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-22-at-21.23.18.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;8eeed4ab2dca5262f0cb22b195a74537&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-22-at-21.23.18.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-22 at 21.23.18.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Now, I have two choices of mechanics here: I can have the rotating platform, or I can have an area of the tile map fall away when the player lands on it. I think I prefer the rotating platform for the sole reason that adding the falling away platform may be a little overwhelming for the player.&lt;/p&gt;
&lt;p&gt;Actually, no. Danger and instability is the feeling I want to go for here. So I&amp;rsquo;ll add the falling platform, followed by a large safe area where the player can see the rotating player. Also, since this is a new mechanic, it&amp;rsquo;s only right that the player should play with it in a safe area first.&lt;/p&gt;
&lt;p&gt;Okay, I&amp;rsquo;ve added the falling platform. Well to be honest, I designated an area of the field where the falling platform will go. This leads into a large safe area which would be an ideal place to introduce the rotating platforms I have in mind.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m thinking of a scene with an &lt;code&gt;AnimationPlayer&lt;/code&gt; that will do the actual rotation animation: as in rotate the sprites from 0° to 90°. The question is, do I want the entire sequence timed using the &lt;code&gt;AnimationPlayer&lt;/code&gt;? As in, have a single animation that will rotate the platform all 4 times, with gaps between each stage? Or should I stick with the &lt;code&gt;AnimationPlayer&lt;/code&gt; simply animating a 90° rotation, and having another thing timing the wait time between stages?&lt;/p&gt;
&lt;p&gt;I think I prefer the latter. That way, I have more control over the pause between stage rotations. I can do things like disable it, or making it configurable. All desirable features.&lt;/p&gt;
&lt;p&gt;I guess the next question then is whether I want the &lt;code&gt;AnimationPlayer&lt;/code&gt; at all. Wouldn&amp;rsquo;t it be more flexible to do the animation in code? Probably, but I&amp;rsquo;m not sure I want that flexibility, at least not yet. Maybe in time it would be needed, but I think for now, I&amp;rsquo;ll just stick with the &lt;code&gt;AnimationPlayer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So, let&amp;rsquo;s start building out the animation. I think I&amp;rsquo;ll start with four animations, each one rotate the platform from θ to θ+90°, just from different starting positions. I&amp;rsquo;ll use names that include the start and end angles, like &lt;code&gt;rotate_0_to_90&lt;/code&gt;, to allow for bidirectional rotation, should I decided to add that. Angle 0° will be up, and rotation will be clockwise. I think each rotation will be 0.5 seconds at speed 1, just so I have a consistent speed control. I can adjust the speed of the overall playback on the &lt;code&gt;AnimationPlayer&lt;/code&gt; itself.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-25-at-16.22.23.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;8eeed4ab2dca5262f0cb22b195a74537&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-25-at-16.22.23.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-25 at 16.22.23.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Okay, animations made. Now to think of collision. I don&amp;rsquo;t like the idea of a collision box rotating along with the platforms. I&amp;rsquo;m not too certain about how well this plays with the physics engine, and even if there were no issues, non-orthogonal geometry is just not a thing in this game. So what I&amp;rsquo;m thinking of doing is adding two collision boxes, one horizontal and one vertical, and just toggling between the two as the platform moves into position. While the platform is rotating, both boxes will be turned off.&lt;/p&gt;
&lt;p&gt;Ooh, I didn&amp;rsquo;t know the collision layer could be added as a animatable track. That&amp;rsquo;s pretty cool.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-25-at-16.33.27.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;8eeed4ab2dca5262f0cb22b195a74537&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-25-at-16.33.27.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-25 at 16.33.27.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Now to add a &lt;code&gt;Timer&lt;/code&gt; which will actually dictate the cadence. The included code is nice and neat: basically take the current rotation of the sprites in degrees, add 90 to it, and produce a string which will become the next animation to play:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-gdscript&#34; data-lang=&#34;gdscript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_on_timer_timeout&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; next &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_next_animation&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	animation_player&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;play&lt;/span&gt;(next)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_next_animation&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;String&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; rot &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt;(sprites&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;rotation &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;180&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;PI&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;360&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;rotate_&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;_to_&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; [rot, rot &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;90&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Added it to a test level, and I had to speed up the &lt;code&gt;AnimationPlayer&lt;/code&gt; to 3x normal speed, just so that the movement is snappy enough to justify the loss of collision during the rotation. It may need further adjusting a little later, but so far works pretty well. Although I do think I need to telegraph when the platform is about to move.&lt;/p&gt;
&lt;p&gt;Oh, I remember how I&amp;rsquo;m going to telegraph to the player when the platform is about to rotate! It&amp;rsquo;s to be an indicator panel mounted at the front that will rotate slowly on it&amp;rsquo;s own. When it reaches a new orientation it will pause, then the platform behind it will rotate.&lt;/p&gt;
&lt;p&gt;Okay, new sprite. How the heck did I choose a canvas size with non-even dimensions and still end up with a sprite that doesn&amp;rsquo;t have a definitive centre line? 🤦&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-28-at-21.21.15.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;8eeed4ab2dca5262f0cb22b195a74537&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-28-at-21.21.15.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-28 at 21.21.15.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Turns out it was how I was removing the upper and lower circles. I was using the eraser for that, and I didn&amp;rsquo;t position it properly.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-28-at-21.24.05.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;8eeed4ab2dca5262f0cb22b195a74537&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-28-at-21.24.05.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-28 at 21.24.05.png&#34; 
        
  /&gt;
&lt;/a&gt;


Okay, here&amp;rsquo;s the sprite:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-28-at-21.29.07.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;8eeed4ab2dca5262f0cb22b195a74537&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-28-at-21.29.07.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-28 at 21.29.07.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Now to think about animating it. I&amp;rsquo;m wondering if it would be possible to simply add it to the same animation player as the platform. The thing is, I would like them to be somewhat independent. The indicator should move on it&amp;rsquo;s own accord, even when the platform sprite is rotating to a new orientation. I would also like the indicator to take on the platform rotation cadence, sending it a signal when it reaches a stop (this will remove the need for the &lt;code&gt;Timer&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Can I do that all in a single scene though? Maybe with two animation players? I&amp;rsquo;ll try that and see how viable it is.&lt;/p&gt;
&lt;p&gt;Okay, just tried that, and yes, using two &lt;code&gt;AnimationPlayers&lt;/code&gt; is viable. Now to prepare the animation. The current rotation period is 3 seconds so I&amp;rsquo;ll start with a 6 second animation that would just rotate the indicator over 180° with a slight pause halfway to trigger the platform. One good thing about the indicator&amp;rsquo;s symmetry is that one animation can cover the full rotation. Although be sure to change the loop wrap mode to &amp;ldquo;Clamp&amp;rdquo;, otherwise you&amp;rsquo;ll see the sprite spin round to 0° when the animation repeats.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-28-at-21.58.34.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;8eeed4ab2dca5262f0cb22b195a74537&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-28-at-21.58.34.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-28 at 21.58.34.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the full thing in action. And yeah, adding the indicator was pretty crucial in the end. The platform wouldn&amp;rsquo;t have been fair otherwise.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-28-at-21.56.12.mp4&#34; poster=&#34;https://lmika.org/uploads/2025/12f0aa11e2.png&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: UCL - Adding Some Missing Library Functions</title>
      <link>https://lmika.org/2025/10/27/devlog-ucl-adding-some-missing.html</link>
      <pubDate>Mon, 27 Oct 2025 21:26:14 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/10/27/devlog-ucl-adding-some-missing.html</guid>
      <description>&lt;p&gt;Working on UCL, adding some missing builtins I&amp;rsquo;ve been finding myself wanting. Nothing too interesting. Just functions like &lt;code&gt;strs:has-prefix&lt;/code&gt;, &lt;code&gt;strs:trim-suffix&lt;/code&gt;, and other functions involving strings and lists that are missing. That sort of thing.&lt;/p&gt;
&lt;p&gt;I am facing some decisions around &lt;code&gt;strs:substr&lt;/code&gt;. See, the usual way these builtins are validating arguments is that if there are more arguments than are necessary, they are generally just ignored. One could describe this as the &amp;ldquo;JavaScript&amp;rdquo; approach to argument validation: open Node and evaluate &lt;code&gt;&amp;quot;hello&amp;quot;.substr(1,2,3,4,5,6,7,8,9)&lt;/code&gt; and one will get the same result as if they simply typed in &lt;code&gt;&amp;quot;hello&amp;quot;.substr(1,2)&lt;/code&gt;. But now I&amp;rsquo;m wondering if it&amp;rsquo;s better to assert that the arguments are either one or two positions, along with the string. What if I wanted to add more positional arguments in the future?&lt;/p&gt;
&lt;p&gt;Although in practice, why would I want to do that? Well, okay, I do have an idea of for taking multiple substrings in a single call. So could be useful? Ah, probably doesn&amp;rsquo;t matter: it&amp;rsquo;ll just be me using this for now.&lt;/p&gt;
&lt;p&gt;Anyway, &lt;code&gt;strs:substr&lt;/code&gt;. One deviation from JavaScript I think I would like to do is that if a single number is used, that would be treated as the end position of the substring; as oppose to JavaScript which treats it as the start position. So running &lt;code&gt;strings:sub &amp;quot;hello, world&amp;quot; 5&lt;/code&gt; will return &lt;code&gt;&amp;quot;hello&amp;quot;&lt;/code&gt;, rather than &lt;code&gt;&amp;quot;, world&amp;quot;&lt;/code&gt;. To start from the right, a negative position can be used: &lt;code&gt;strings:sub &amp;quot;hello, world&amp;quot; -5&lt;/code&gt; will return &lt;code&gt;&amp;quot;world&amp;quot;&lt;/code&gt;. Two numbers will be treated as a start (inclusive) and end (exclusive) positions, like most other languages; although both can be negative which will set the position from the right side.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;strs:substr &amp;#34;hello, world&amp;#34; 5   
--&amp;gt; &amp;#34;hello&amp;#34;

strs:substr &amp;#34;hello, world&amp;#34; -5  
--&amp;gt; &amp;#34;world&amp;#34;

strs:substr &amp;#34;hello, world&amp;#34; 3 10
--&amp;gt; &amp;#34;lo, wor&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Okay, that&amp;rsquo;s working for strings. Need to do the same thing for lists. I could almost use the same function.&lt;/p&gt;
&lt;p&gt;Oh, I keep forgetting that I need to worry about nils. How should &lt;code&gt;sublists&lt;/code&gt; work if the passed in list is nil? Maybe it&amp;rsquo;s better to just return a nil. Less hassle than requiring the user to deal with nil values. I&amp;rsquo;ll start with that approach, but I may change that the future.  Should I do the same for &lt;code&gt;strs:substr&lt;/code&gt;? Hmm, no. I think I&amp;rsquo;ll keep the default binding rules for nil to strings, which is basically convert it into an empty string. That saves me from changing the binding logic. Maybe I should lock that decision in with a test.&lt;/p&gt;
&lt;p&gt;Hmm, also, many of the other list builtins support iterators. If I were going to add iterator support to &lt;code&gt;sublist&lt;/code&gt; , then how would I handle the negative numbers? Iterators, by definition, can be boundless. Maybe leave iterator support out for now.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;lists:sublist [a b c] 1
--&amp;gt; [a]

lists:sublist [a b c] -1
--&amp;gt; [c]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It also looks like the argument binder has made my mind up about nils for me. I added the test which testing for nils, and I got an &lt;code&gt;expected listable&lt;/code&gt; error.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;lists:sublist () 1
--&amp;gt; error: expected listable
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I think that&amp;rsquo;s fine. Might be better to fail on nils actually.&lt;/p&gt;
&lt;p&gt;Getting back to the argument question raised earlier, I did run into an issue where I was using &lt;code&gt;strs:split&lt;/code&gt; without a second argument. When one does that, UCL will do something similar to Go, and return a list of each individual rune. I think that&amp;rsquo;s a useful feature, but I think I&amp;rsquo;d prefer it if that option was explicit, and if a second argument were not set, raise an error.&lt;/p&gt;
&lt;p&gt;Okay, a few more items on my todo list. I need a string replace function. I generally prefer such a function to replace all the instances of a string, rather than just the first; but that might be pushing my preference a little too far. So I&amp;rsquo;ll settle with replacing the first instance of a substring and add an &lt;code&gt;-all&lt;/code&gt; flag to replace all instances.&lt;/p&gt;
&lt;p&gt;Actually, no. Go has support for specifying a count, so I&amp;rsquo;ll use that by adding an &lt;code&gt;-n COUNT&lt;/code&gt; option. If N is unset, then all the substrings are replaced.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;strs:replace &amp;#34;hello hello hello&amp;#34; &amp;#34;hello&amp;#34; &amp;#34;world&amp;#34;
--&amp;gt; world world world

strs:replace &amp;#34;hello hello hello&amp;#34; &amp;#34;hello&amp;#34; &amp;#34;world&amp;#34; -n 1
--&amp;gt; world hello hello
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Also, given that this is using Go&amp;rsquo;s builtin &lt;code&gt;strings.Replace&lt;/code&gt; function, setting &amp;ldquo;match&amp;rdquo; to the empty string will cause &lt;code&gt;replace&lt;/code&gt; to match between each rune, plus the start and end of the
string.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;strs:replace &amp;#34;each one&amp;#34; &amp;#34;&amp;#34; &amp;#34;|&amp;#34;
--&amp;gt; |e|a|c|h| |o|n|e|
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Could be useful, so I&amp;rsquo;ll keep this feature in.&lt;/p&gt;
&lt;p&gt;Nearing the end now: adding an &lt;code&gt;-in&lt;/code&gt; switch to &lt;code&gt;os:exec&lt;/code&gt; to set stdin for an command. The original idea was to name the switch &lt;code&gt;-stdin&lt;/code&gt;, and I also considered &lt;code&gt;-input&lt;/code&gt;. But when writing the tests, I naturally settled on &lt;code&gt;-in&lt;/code&gt;.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;os:exec tr &amp;#39;[a-z]&amp;#39; &amp;#39;[A-Z]&amp;#39; -in &amp;#34;hello&amp;#34;
--&amp;gt; HELLO
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It would&amp;rsquo;ve been nice to accept stdin from a pipe. Sadly, that&amp;rsquo;s not possible with how pipes actually work.&lt;/p&gt;
&lt;p&gt;And the last one: adding a &lt;code&gt;nil?&lt;/code&gt; builtin, which will return true if the given item is nil.  I guess in the grand scheme of things, it&amp;rsquo;s probably not strictly necessary, as anything that&amp;rsquo;s not zero, including nil, will be true. And one could always use &lt;code&gt;eq $thing ()&lt;/code&gt; to test if &lt;code&gt;$thing&lt;/code&gt; is nil. So I may leave this one on the backlog.&lt;/p&gt;
&lt;p&gt;Okay, finished. I wanted to add some stuff to Dynamo Browse, but I ran out of time. I will update the version of UCL used by Dynamo Browse, but all that other stuff will need to wait for tomorrow.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Godot Game - Working On Backdrops</title>
      <link>https://lmika.org/2025/10/20/devlog-godot-game-working-on.html</link>
      <pubDate>Mon, 20 Oct 2025 21:01:17 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/10/20/devlog-godot-game-working-on.html</guid>
      <description>&lt;p&gt;Time to focus on backdrops. &lt;em&gt;Aaaaargh!&lt;/em&gt; What it&amp;rsquo;s time for is to face my inability to make artwork. I&amp;rsquo;ve been able to do small things: lifts, signs, even some pickups. But for the larger stuff, I don&amp;rsquo;t know where to begin. And regretfully it fills me with shame that I&amp;rsquo;m even looking at asset packs: despite most of the assets in this game &lt;a href=&#34;https://brackeysgames.itch.io/brackeys-platformer-bundle&#34;&gt;already originating from an asset pack&lt;/a&gt;. But the alternative is drawing it myself. And I&amp;rsquo;m not confident that I&amp;rsquo;ll be able to do a good job here.&lt;/p&gt;
&lt;p&gt;But let&amp;rsquo;s try. If it doesn&amp;rsquo;t work, I&amp;rsquo;ll use the asset pack.&lt;/p&gt;
&lt;p&gt;The first idea is to get some inspiration. Well there are plenty of those in my photo collection. Here&amp;rsquo;s one that seems like a good start:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/pxl-20230624-143911534.jpg&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;6dad51e49e4b55baeeaca0ad769758db&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/pxl-20230624-143911534.jpg&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;PXL_20230624_143911534.jpg&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;In short, foothills are slightly jagged yet mainly consist of sweeping curves at roughly the same vertical height. Colour and shadow provide the sense of distance. The hills in the photo are largely covered in vegetation, which could potentially be simulated by a vertical spray-brush: favouring light and dark green with some light brown on a largely mid-level green colour.&lt;/p&gt;
&lt;p&gt;Also I need to remember a principal of illustrations: go from the foreground to the background. I guess one saving grace with modern art tools is that I&amp;rsquo;ve got layers to play around.&lt;/p&gt;
&lt;p&gt;Okay, new sprite. Let&amp;rsquo;s start with the foothills. Taking inspiration from both the photo and the asset pack. Tracing around the foothills and adding some colour to give us depth yields the following:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-18-at-10.23.02.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;6dad51e49e4b55baeeaca0ad769758db&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-18-at-10.23.02.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-18 at 10.23.02.png&#34; 
        
  /&gt;
&lt;/a&gt;


Okay, not bad. A little plain but a good first start.&lt;/p&gt;
&lt;p&gt;Trying out some ridge-lines.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-18-at-10.27.33.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;6dad51e49e4b55baeeaca0ad769758db&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-18-at-10.27.33.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-18 at 10.27.33.png&#34; 
        
  /&gt;
&lt;/a&gt;


Ugh, that looks awful! Not convincing at all. Okay, forget about the ridge-lines. I also tried out adding a spray to simulate vegetation but that didn&amp;rsquo;t look convincing either. I think given the distance, my skill level, and the fact that this is a backdrop, less might be more here.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s try it out in the game. I&amp;rsquo;ll use the existing blue sky image I have for the desert world. Let&amp;rsquo;s also remove the existing backdrop.&lt;/p&gt;
&lt;p&gt;Hmm, the issue is trying to get the hills to look convincing, and given that the player starts quite high, and given that the camera is position to track the player near the centre of the screen, they won&amp;rsquo;t see this background layer. But this might be okay as I also need to make a distant mountain layer too.&lt;/p&gt;
&lt;p&gt;Okay, after a lot of trial and error, I&amp;rsquo;ve got the foothills positioned in a way that works well:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-19-at-09.11.36.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;6dad51e49e4b55baeeaca0ad769758db&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-19-at-09.11.36.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-19 at 09.11.36.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;In order to do this I had to do the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scale them down by 50% so that they don&amp;rsquo;t dominate the screen or have too large pixel edges.&lt;/li&gt;
&lt;li&gt;Use a horizontal scroll offset of 0.7 and a vertical scroll offset of 0.2 so they look convincing in the background. I did try a vertical scroll offset of 0.0 to keep them anchored at the bottom of the screen. But because of how the camera follows the player vertically as they jump, that just looked disorientating.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A few things I&amp;rsquo;ve learnt about using &lt;code&gt;Parallax2D&lt;/code&gt;: you want the &amp;ldquo;repeat size&amp;rdquo; to be the size of the sprite (after transforms) so that they&amp;rsquo;re laid out seamlessly across the screen. If the scaled size doesn&amp;rsquo;t make it all away across, bump the &amp;ldquo;repeat times&amp;rdquo; up a few times. You will get the automatic repeat, which you can verify during play through by overriding the camera, zooming all the way out, and turning on 2D controls:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-19-at-09.19.24.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;6dad51e49e4b55baeeaca0ad769758db&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-19-at-09.19.24.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-19 at 09.19.24.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Now it&amp;rsquo;s time for the mountains. For this I&amp;rsquo;m going to use &lt;a href=&#34;https://bearbreath.itch.io/parallax-mountain-background&#34;&gt;this asset pack&lt;/a&gt; for inspiration. Obviously much taller than the foothills, with jagged peaks. Less sweeping curves, and more like upside-down Vs, yet with flat tops.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s attempt one:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-19-at-09.39.32.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;6dad51e49e4b55baeeaca0ad769758db&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-19-at-09.39.32.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-19 at 09.39.32.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;No, don&amp;rsquo;t like it at all. For one thing the canvas is too wide in the vertical direction and too short in the horizontal direction. For another, the mountains don&amp;rsquo;t look convincing. The peaks are too pointy. So what I&amp;rsquo;ll do is cut the canvas in half and try again with a steadier brush.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s attempt two:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-19-at-09.50.03.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;6dad51e49e4b55baeeaca0ad769758db&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-19-at-09.50.03.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-19 at 09.50.03.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;A little better. The general shapes look okay. Might look better after colouring it in.&lt;/p&gt;
&lt;p&gt;Okay, here&amp;rsquo;s the mountain layer with the colouring:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-20-at-21.00.18.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;6dad51e49e4b55baeeaca0ad769758db&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-20-at-21.00.18.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-20 at 21.00.18.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s almost convincing. Let&amp;rsquo;s try it out in the scene.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-20-at-21.25.56.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;6dad51e49e4b55baeeaca0ad769758db&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-20-at-21.25.56.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-20 at 21.25.56.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-20-at-21.26.56.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;6dad51e49e4b55baeeaca0ad769758db&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-20-at-21.26.56.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-20 at 21.26.56.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Hmm, I&amp;rsquo;m not sure I like it. It looks a little cartoon-ie, what with it&amp;rsquo;s large, sweeping areas filled with a single colour. I also don&amp;rsquo;t like the outlines: they&amp;rsquo;re a little too bold. It may help if I made the mountain layer a little smaller and reduce the motion a little.&lt;/p&gt;
&lt;p&gt;Okay, after doing that, I like it a little more. The outlines are less pronounced and you see the more of the mountain plate throughout the level. I added back the outline in the foothill plate. I think you can either go with outlines, or you go without: mixing the two looks bad.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-20-at-21.38.12.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;6dad51e49e4b55baeeaca0ad769758db&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-20-at-21.38.12.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-20 at 21.38.12.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll try this for a while. I may play around with the scroll settings a bit but it is difficult coming up with a combination that will show the mountains for more of the level while also hiding the seams between the layers. I may need to adjust the contrast a little, just so that the background doesn&amp;rsquo;t clash with the foreground: even in the screenshot about it&amp;rsquo;s a little muddy. I also need to put in a cloud layer too. Not quite sure how I&amp;rsquo;ll make convincing looking clouds, but I&amp;rsquo;ll think of something.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/10/14/finally-added-some-checkpoints-to.html</link>
      <pubDate>Tue, 14 Oct 2025 20:51:47 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/10/14/finally-added-some-checkpoints-to.html</guid>
      <description>&lt;p&gt;Finally added some checkpoints to level 1-3. Also added some animated water as a new type of hazard. The asset pack I&amp;rsquo;m using came with a single &amp;ldquo;wave&amp;rdquo; tile. I used Aseprite to shift that across 4 cells and configured it as an animation in the tile set. I was planning to add some background waves too, but that made it a little too busy and hard to discern. Less is more here.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-14-at-21.43.35.gif&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;ae947d93f67e658bbd692d98683f393f&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-14-at-21.43.35.gif&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-14 at 21.43.35.gif&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/10/13/a-small-devlog-entry-today.html</link>
      <pubDate>Mon, 13 Oct 2025 21:02:13 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/10/13/a-small-devlog-entry-today.html</guid>
      <description>&lt;p&gt;A small devlog entry today. Just painting the level, adding background tiles and kill-zones. I also added a frame around the lift door, just to define it a little. Discovered that it&amp;rsquo;s probably best not to have sprites with non-even dimensions. It makes positioning them a little awkward, resulting in fractional offsets. I elected to include a transparent row at the top of the frame sprite, just to avoid this.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-13-at-21.54.55.png&#34; width=&#34;600&#34; height=&#34;372&#34; alt=&#34;Auto-generated description: A pixel art-style platform game scene features a character standing near a doorway, surrounded by brick walls and yellow platforms, with spikes visible on the right.&#34;&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Godot Game - A Trigger That Reveals Secrets</title>
      <link>https://lmika.org/2025/10/12/devlog-godot-game-a-trigger.html</link>
      <pubDate>Sun, 12 Oct 2025 10:09:40 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/10/12/devlog-godot-game-a-trigger.html</guid>
      <description>&lt;p&gt;Okay, I think I need something to hide secret areas from the player and reveal it to them when they approach it. I&amp;rsquo;ve lived without this mechanic for a while, placing secret areas a fair distance from the main game area, so that they&amp;rsquo;re not visible. But such distances make it difficult to place time-limited things within those secret areas, like the invulnerability power-up, since travelling back to the main area to use it would eat up into the player&amp;rsquo;s power-up time. I could add ways for the player to return to the main play area quickly, like doors, but that complicates the level geometry and I need to place them out of the way, like in the sky.&lt;/p&gt;
&lt;p&gt;So, I&amp;rsquo;ll add a new element to do this. It will be an &lt;code&gt;Area2D&lt;/code&gt; node with an associated tile layer as a child. When the player enters the &lt;code&gt;Area2D&lt;/code&gt;, the tile layer is hidden away and the player can see the revealed secret. The player leaving the &lt;code&gt;Area2D&lt;/code&gt; will make the tile layer visible again.&lt;/p&gt;
&lt;p&gt;Okay, the new &lt;code&gt;SecretReveal&lt;/code&gt; scene has been built. The root node is an &lt;code&gt;Area2D&lt;/code&gt; which takes, as children when added to the main scene, a &lt;code&gt;CollisionShape2D&lt;/code&gt; to act as the trigger space, and a tile layer to act as the tiles to hide/show. The tile layer needs to be in the foreground, which is Z Index 10 and with collision turned off, so that it&amp;rsquo;ll reveal the mid and background layers. The script simply tracks entering bodies (it monitors layer 2, which is dedicated to the player).&lt;/p&gt;
&lt;p&gt;Now, the next question is how to fade the tile map in and out. I could just toggle the visibility but that&amp;rsquo;s way too jarring. What I hope to achieve is to add an animation player to &lt;code&gt;SecretReveal&lt;/code&gt; which will play a fade transition as the player enters or exits the &lt;code&gt;Area2D&lt;/code&gt;. But how to do that without using scripting? I rather use the primitives offered to me as I&amp;rsquo;m guessing they&amp;rsquo;ll be more efficient than throwing events around in GDSscript.&lt;/p&gt;
&lt;p&gt;One possibility is the &lt;code&gt;Modulate&lt;/code&gt; option in the &lt;code&gt;CanvasItem&lt;/code&gt; properties. After quickly playing around with this in the editor, it looks like it&amp;rsquo;s possible to adjust the alpha channel on this property, and that would adjust the visibility of the tile layer:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-12-at-10.02.17.gif&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;057d625dbf85066aaf4426135761271e&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-12-at-10.02.17.gif&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-12 at 10.02.17.gif&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;So this looks viable. I&amp;rsquo;m actually glad that &lt;code&gt;Area2D&lt;/code&gt; has this property, despite not actually rendering itself in any way (outside of the editor) Let&amp;rsquo;s give it a try.&lt;/p&gt;
&lt;p&gt;Okay, that works. Hooking it up to an animation player playing an animation that fades out and fades in the tiles when the player enters and leaves the trigger area works. It needs some improvement though. If the player were to enter and leave the target area quickly, the visibility would quickly &amp;ldquo;jump&amp;rdquo; from partially visible to fully visible. I suspect the reason for this is that the animation is always starting from the start.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-12-at-10.10.53.gif&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;057d625dbf85066aaf4426135761271e&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-12-at-10.10.53.gif&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-12 at 10.10.53.gif&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;One technique I used in the past is to preserve the current timestamp of the animation when I change it. But the animations here are using non-linear easing as it makes for a better transition.&lt;/p&gt;
&lt;p&gt;So what I&amp;rsquo;ll try is a single animation that will be played forwards and backwards. When there&amp;rsquo;s a need to play the animation, the logic will detect whether the animation player is currently playing, and will resume playback from it&amp;rsquo;s current position in the desired direction.&lt;/p&gt;
&lt;p&gt;Okay, so this is what I settled with:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-gdscript&#34; data-lang=&#34;gdscript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_on_body_entered&lt;/span&gt;(body: &lt;span style=&#34;color:#a6e22e&#34;&gt;Node2D&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	candidate_body &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; body
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; animation_player&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;is_playing&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		animation_player&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;play_section&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;reveal_secret&amp;#34;&lt;/span&gt;, \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		    animation_player&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;current_animation_position)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		animation_player&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;play&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;reveal_secret&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_on_body_exited&lt;/span&gt;(body: &lt;span style=&#34;color:#a6e22e&#34;&gt;Node2D&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; body &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; candidate_body:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		candidate_body &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; animation_player&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;is_playing&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			animation_player&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;play_section&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;reveal_secret&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.0&lt;/span&gt;, \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			    animation_player&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;current_animation_position, &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			animation_player&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;play_backwards&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;reveal_secret&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What I&amp;rsquo;m doing is when the player enters the trigger space, I&amp;rsquo;ll play the &amp;ldquo;reveal secret&amp;rdquo; animation, either from the start or from the current position, depending if the animation player is playing. If the player leaves the trigger space, and the animation player is not playing, I&amp;rsquo;ll play the &amp;ldquo;reveal secret&amp;rdquo; in reverse from the end. When it is playing, I do something similar but I need to set the end position to the current animation position, rather than the start position. I&amp;rsquo;m guessing the animation player will take the initial position as the end when it&amp;rsquo;s playing in reverse, which makes sense.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how the transition looks now:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-12-at-10.27.25.gif&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;057d625dbf85066aaf4426135761271e&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-12-at-10.27.25.gif&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-12 at 10.27.25.gif&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Much smoother.&lt;/p&gt;
&lt;p&gt;Now to try it out in level 3-1. And yeah, this is a much better way to do secrets. I can build them close to the play area and the player no longer needs to navigate them blindly, or walk a million miles just to get to the bonus. I won&amp;rsquo;t overdo it though. I&amp;rsquo;m going to keep some of the large secrets that are naturally out of the way as they were. The player will need to react fast to enter them so I want to make sure they have the opportunity to see them.&lt;/p&gt;
&lt;p&gt;I am wondering whether to merge this with the &amp;ldquo;You found a secret&amp;rdquo; trigger, but I think I&amp;rsquo;ll keep them separate for now.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Godot Game - More on Level 3-1</title>
      <link>https://lmika.org/2025/10/11/devlog-godot-game-more-on.html</link>
      <pubDate>Sat, 11 Oct 2025 20:10:10 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/10/11/devlog-godot-game-more-on.html</guid>
      <description>&lt;p&gt;Starting on the Godot game again. Level 3-1 is nearing its finish. I&amp;rsquo;m building the corridor/cave that&amp;rsquo;ll lead to the lift that the player will travel up to the exit. To add some interest, I&amp;rsquo;m terminating the corridor at a spike section which should be impossible for the player to navigate. But now I&amp;rsquo;m wondering: what if the player managed to navigate this with invulnerability? Maybe a secret where the player picks up a vile given them this power that would allow them to travel through this section and pick up something nice?&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll come back to that. I&amp;rsquo;ll build the lift first and give it a try to see how it feels.&lt;/p&gt;
&lt;p&gt;Need to remember how these lifts work. Okay, yeah, it&amp;rsquo;s all driven by the doors. Added the lift and gave the level a test from the midpoint. Feels good. I do like the lift. I jumped around the spike section a little and yeah I think something like a secret allowing the player to travel through that section for a bonus would be nice.&lt;/p&gt;
&lt;p&gt;First implementation of the invulnerability power-up: simple enough. The current implementation taps into the existing death handler, in which an Area2D trigger will signal to the player to die when they enter a trigger area. Note that this happens when the player &lt;em&gt;enters&lt;/em&gt; the area. If the player is already in the area, nothing happens. This means that if the player enters an area, and stays there when the invulnerability power-up times out, they&amp;rsquo;re still invulnerable until they exit that area. A way around this is to constantly signal to the player to die while they&amp;rsquo;re inside, but I think I&amp;rsquo;ll keep it this way for now. I like the idea of giving the player just a little more time than they have to be invulnerable (although when they leave and enter a new kill zone, they will die).&lt;/p&gt;
&lt;p&gt;Okay, implemented the invulnerability power-up and gave it a quick test in the spiky area. Yeah, it works. I haven&amp;rsquo;t added the kill zones yet so the need for invulnerability is completely unnecessary, but the progress bar has been implemented, and based on how quickly that runs down, there&amp;rsquo;s ample time for the player to get the pickups and get back before the bar runs out.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-11-at-10.17.02.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;86fd78ccac1ad324ea845862fdf12816&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-11-at-10.17.02.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-11 at 10.17.02.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Place the exit and now just playing through the level. It needs checkpoints, and there are some tricky jumps that I&amp;rsquo;m tweaking. But I think it plays rather well. I&amp;rsquo;ll start dressing up the level now.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Godot Game - More On Level 3-1</title>
      <link>https://lmika.org/2025/10/09/devlog-godot-game-more-on.html</link>
      <pubDate>Thu, 09 Oct 2025 21:19:54 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/10/09/devlog-godot-game-more-on.html</guid>
      <description>&lt;p&gt;Working on the Godot game again. Finishing off the top layer of zone 3 in 3-1. Made a floating balloon with a smaller drop-payload trigger area, just to avoid accidental triggers if the player gets too close.&lt;/p&gt;
&lt;p&gt;One thing I didn&amp;rsquo;t appreciate is that if the player misses a jump, it takes a long time for them to fall and recover from that. I&amp;rsquo;m wondering if I need to add some kill zones between each horizontal level of the cross-back, along some checkpoints so that the player doesn&amp;rsquo;t have to spend so much time waiting to fall and redoing large chunks of the zone.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll try this will some barbed-wire graphics. See how I feel about it. Here&amp;rsquo;s how they looks:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-09-at-21.45.49.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;487a0b931c939eca484b5e4434df7187&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-09-at-21.45.49.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-09 at 21.45.49.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Doesn&amp;rsquo;t look inviting, although that&amp;rsquo;s kind of the point. But this may work as a kill-zone between hight levels. Although I&amp;rsquo;m uncertain as to how I&amp;rsquo;m going to introduce them to the player. Maybe they won&amp;rsquo;t need any introduction: some ugly, spiky coils might be deterrent enough.&lt;/p&gt;
&lt;p&gt;Anyway, the elements of the actual jumping part has been filled in. I think adding this was a good idea: feels interesting again. Now it&amp;rsquo;s just a matter of adding the final lift, and then the exit.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Godot Game - More On Level 3-1</title>
      <link>https://lmika.org/2025/10/08/devlog-godot-game-more-on.html</link>
      <pubDate>Wed, 08 Oct 2025 21:07:17 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/10/08/devlog-godot-game-more-on.html</guid>
      <description>&lt;p&gt;Continuing the build out of the zone 3 in level 3-1. I&amp;rsquo;ll start with a platform that is self rising when the player jumps on it. Always a classic; and honestly, I think the start of the level can use that. Lift does feel a little slow. I boosted the speed from 70 to 90. Also started added floating balloons with mines around this part of the level, since those elements were introduced in zone 2.&lt;/p&gt;
&lt;p&gt;Hmm, I wonder if I should make a variant of the red balloon with mine. The mines will kill you, but the player can safely land on the top of the red balloon. I&amp;rsquo;m wondering if a variant that is unsafe for the player could be useful. Maybe a  &amp;ldquo;spiky balloon&amp;rdquo; type. It can be attached to the grey mine I made for level 2-2.&lt;/p&gt;
&lt;p&gt;Alternatively, I can make it a different colour and just make it such that landing on it will pop the balloon. Took a brief look at a colour blindness test and it looks like red and green is the way to go for two distinguished colours.&lt;/p&gt;
&lt;p&gt;Hit a roadblock: I need to import a type, the &lt;code&gt;TextureVariant&lt;/code&gt; enum, from one script (the static mine) into another one (balloon_with_static_mine). &lt;a href=&#34;https://forum.godotengine.org/t/how-do-i-include-a-script-into-another-script/19217&#34;&gt;Quick web search&lt;/a&gt; and it looks like the way to do this is by &amp;ldquo;preloading&amp;rdquo; a script. This gives you access to the script&amp;rsquo;s symbols:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-gdscript&#34; data-lang=&#34;gdscript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; StaticMine &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; preload(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;res://scripts/static_mine.gd&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; mine_texture_variant: StaticMine&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;TextureVariant
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To support popping when the player lands on it, the balloon is an &lt;code&gt;AnimatableBody2D&lt;/code&gt; which can detect collisions as it bobs up and down. I wonder if I can actually test collision that way:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-gdscript&#34; data-lang=&#34;gdscript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; coll &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; animatable_body_2d&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;move_and_collide&lt;/span&gt;(velocity)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; coll &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; coll&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;get_collider&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Player&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Bang!&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No, turns out I can&amp;rsquo;t. Tried this and even played around with layers a little, and it didn&amp;rsquo;t detect when the player landed on it. So moving to plan B, which is adding an Area2D set to layer 2, the same layer as the player. When the player lands in that, it will pop the balloon… eventually. At the moment it just makes the balloon disappear. I&amp;rsquo;ll need to add the pop animation and sound effects. It will also give the player a jump at at about the same distance they would travel in a normal jump.&lt;/p&gt;
&lt;p&gt;Okay, new variant implemented. Here&amp;rsquo;s how it looks:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-08-at-21.51.39.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;0d8f284ff5fe12ea5c49f3cc0c6878b6&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-08-at-21.51.39.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-08 at 21.51.39.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;I was going for something a little deadlier, but I think having the extra jump could make for some interesting mechanics. I guess we&amp;rsquo;ll see. Anyway, I&amp;rsquo;ll leave it there this evening. I&amp;rsquo;ve managed to get 2/3rds of new section built out.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Godot Game - The Back Third of Level 3-1</title>
      <link>https://lmika.org/2025/10/07/devlog-godot-game-the-back.html</link>
      <pubDate>Tue, 07 Oct 2025 21:15:22 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/10/07/devlog-godot-game-the-back.html</guid>
      <description>&lt;p&gt;Working on Blogging Tool was a nice little break but I&amp;rsquo;m back on my Godot game. I&amp;rsquo;m trying to get the layout of level 3-1 finished. I&amp;rsquo;m actually not very happy with the back 60% of this. For you see, world 3 is meant to be a mountainous world, with more pits and more vertical levels. Much of this is to be seen in 3-2, with 3-1 being merely the foothills of the range the player is expected to climb. And part of this is that this level was originally the second one, but some players I shared this with said the difficulty curve ramped up a little too much. So I&amp;rsquo;ve moved it in world 3. making it the 5th level of the game.&lt;/p&gt;
&lt;p&gt;So the first part of the level sort of double backs on itself as the player is to negotiate some pretty careful jumping with moving platforms and enemies:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-07-at-21.10.27.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;831d29443fb026cb0e199d85e4c8bdd3&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-07-at-21.10.27.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-07 at 21.10.27.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;This is largely untouched from when this was the second level. It&amp;rsquo;s a little clunky — mainly due to some of the platforms being mistimed — but it works. This then extends to the second part, which is more linear and traditional:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-07-at-21.12.12.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;831d29443fb026cb0e199d85e4c8bdd3&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-07-at-21.12.12.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-07 at 21.12.12.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s this part I&amp;rsquo;m not happy with. For one thing, it&amp;rsquo;s not challenging enough. Not that it has to be super challenging, but the pacing just sort of peters out here. It&amp;rsquo;s also on the smaller size, especially compare to world 2 and their significantly longer levels.&lt;/p&gt;
&lt;p&gt;But I think I have an idea. The relatively flat section, featuring a bridge and a bit of dodging of balloons, I think I&amp;rsquo;ll keep. But for the third part, I think I&amp;rsquo;ll bring back the double backing jumping challenge, and finish it off with a lift ride. My theory is that the flat section will act as a nice palate cleanser to break up the other two &amp;ldquo;acts&amp;rdquo; of the level, along with reintroducing the floating balloons with mines. Act three will mirror act one, in the precision jumping while giving the sense of ascending difficult terrain. The lift at the end will also add to this which also giving a sense of accomplishment.&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s a lot of words I&amp;rsquo;ve picked up from YouTube videos about game design. Let&amp;rsquo;s see if I can execute on this. I&amp;rsquo;ll start with a pit, designed to to be an allusion to the pit at the start. I wish I can draw convincing organic terrain: it never looks right. Although the pit at the start has straight walls, so maybe this one should too.&lt;/p&gt;
&lt;p&gt;Okay, so tried to draw some floating land masses that the player will need to navigate. Gave it a quick test, and nothing disastrous so far. The player should be able to see where they need to go while they&amp;rsquo;re traversing the horizontal parts. Verticals are another story, but the path should be easy to follow once platforms are added.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-07-at-21.52.12.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;831d29443fb026cb0e199d85e4c8bdd3&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-07-at-21.52.12.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-07 at 21.52.12.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Have jumped around this area a few times and I think this may work. It makes for a more interesting area, and I can see where hazards and platforms would go. Tried it from the start and yeah, I like it. I think I&amp;rsquo;ll go with this.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Blogging Tools - Podcast Clip Favourites</title>
      <link>https://lmika.org/2025/10/06/devlog-blogging-tools-podcast-clip.html</link>
      <pubDate>Mon, 06 Oct 2025 21:29:36 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/10/06/devlog-blogging-tools-podcast-clip.html</guid>
      <description>&lt;p&gt;Back on Blogging Tools. Gave up on trying to set a remote ref: all the ones I&amp;rsquo;ve tried were not working. Looking at the examples it looks like I can call &lt;code&gt;Push&lt;/code&gt; with defaults (apart from authentication which looks like I&amp;rsquo;ll require with my repo). Gave it a try and that looks like it worked. I peeked in the source code and the default ref that work was the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;DefaultPushRefSpec&lt;/span&gt; = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;refs/heads/*:refs/heads/*&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Turns out by adding &lt;code&gt;refs/remote/origin/…&lt;/code&gt; I was using the default pull ref. I suppose I should learn more about how Git works in this respect. But later, it&amp;rsquo;s working now. That&amp;rsquo;s all I need. Finishing off the Blogging Tool side of things by moving all the hard coded values to the config, and I think this is ready to go.&lt;/p&gt;
&lt;p&gt;Turning to the Hugo site. I&amp;rsquo;ve started a new site from scratch, which does little than just push to Netlify. Same template and same changes I&amp;rsquo;ve made for the test site. I don&amp;rsquo;t love the template, but I think it&amp;rsquo;ll work for now.&lt;/p&gt;
&lt;p&gt;Created a new site and installed the theme. Set the menu and cleared the footer as per this Hugo config:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;baseURL&lt;/span&gt; = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;https://example.org/&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;languageCode&lt;/span&gt; = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;en-us&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;title&lt;/span&gt; = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;Podcast Favourites&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;theme&lt;/span&gt; = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;hugo-flex&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;params&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;footer&lt;/span&gt; = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[[&lt;span style=&#34;color:#a6e22e&#34;&gt;menu&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;]]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt; = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Clips&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;pageRef&lt;/span&gt; = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/about&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;weight&lt;/span&gt; = &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;markup&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;goldmark&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;renderer&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;unsafe&lt;/span&gt; = &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Produces this nice clean home page:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-06-21.25.37-localhost-48f16a06173b.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;cd2795880879f7ce0b88795de840a206&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-06-21.25.37-localhost-48f16a06173b.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;2025-10-06 21.25.37 localhost 48f16a06173b.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Added CI/CD to build the site and deploy it to Netlify. All seems to work.&lt;/p&gt;
&lt;p&gt;Merge changes in Blogging Tools, pushed to Forgjo, and redeployed in Coolify. Status is degraded. Reason is that the temp directory, which is where the workspace for the Hugo site&amp;rsquo;s Git repo, doesn&amp;rsquo;t exist:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-06-at-21.48.17.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;cd2795880879f7ce0b88795de840a206&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-06-at-21.48.17.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-06 at 21.48.17.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;I forget that this is deployed in a &lt;code&gt;scratch&lt;/code&gt; container, which has no guest OS. So creating the &lt;code&gt;/tmp&lt;/code&gt; directory is something I&amp;rsquo;ll need to do. Will add a quick bit of logic to Blogging Tools to do this prior to creating the Git provider.&lt;/p&gt;
&lt;p&gt;Actually, no. Change of pace. I&amp;rsquo;ll do it in the Docker file. What I&amp;rsquo;ll do is touch a file in &lt;code&gt;/tmp&lt;/code&gt; during the build phase and just copy it over to scratch (I can&amp;rsquo;t do this in scratch as it doesn&amp;rsquo;t come with executables). This should force the directory creation.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;RUN&lt;/span&gt; touch /tmp/t&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# ---------  &lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;FROM&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;scratch&lt;/span&gt;  &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;COPY&lt;/span&gt; --from&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;builder /tmp/t /tmp&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Redeployed and… what?&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-06-at-21.58.54.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;cd2795880879f7ce0b88795de840a206&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-06-at-21.58.54.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;CleanShot 2025-10-06 at 21.58.54.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Oh, that&amp;rsquo;s meant to be:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;COPY&lt;/span&gt; --from&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;builder /tmp/t /tmp/t&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Copying directly to &lt;code&gt;/tmp&lt;/code&gt; makes a new file with the filename &lt;code&gt;tmp&lt;/code&gt; in &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Okay, Blogging Tools is working again. Let&amp;rsquo;s try it out.&lt;/p&gt;
&lt;p&gt;Ah, error: &lt;code&gt;author field is required&lt;/code&gt;. I need to set this for the commit call. That may be why the commit author was me and not the &lt;code&gt;bloggingtools&lt;/code&gt; user when I was testing this on my own machine.&lt;/p&gt;
&lt;p&gt;Trying again. Hey, success! Blogging Tools added the clip to the repository and pushed the changes as the &lt;code&gt;bloggingtools&lt;/code&gt; user. This kicked off the CI/CD to rebuild the Hugo site and pushes it to Netlify. Did forget to fix the base URL add the Hugo template for making the audio playable, but fixed that and now the clip is playable.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/pasted-image-20251006222338.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;cd2795880879f7ce0b88795de840a206&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/pasted-image-20251006222338.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;Pasted image 20251006222338.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Nice! Calling this feature done. I will need to style the Hugo site, but I&amp;rsquo;ll do that over time. It&amp;rsquo;s getting a bit late and I want to wrap this up.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Blogging Tools - Podcast Clip Favourites</title>
      <link>https://lmika.org/2025/10/05/devlog-blogging-tools-podcast-clip.html</link>
      <pubDate>Sun, 05 Oct 2025 16:52:30 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/10/05/devlog-blogging-tools-podcast-clip.html</guid>
      <description>&lt;p&gt;Inspired by the way &lt;a href=&#34;https://ronjeffries.com/articles/-w025/y/t/&#34;&gt;Ron Jeffries writes&lt;/a&gt; about his work, as well as by &lt;a href=&#34;https://blog.martin-haehnel.de/2025/10/03/whiledo/&#34;&gt;Martin Hähnel&lt;/a&gt; attempt at this, I thought for this instalment of Devlog I&amp;rsquo;ll try a more &amp;ldquo;lab notes&amp;rdquo; approach. This means a potentially more mundane and less satisfactory description of project work today: less showing of what was accomplished, and more of a running commentary. I wanted to see if I liked this style of writing, and if it helped me or slowed me down. Preliminary results were mixed: it did slow me down, but I found it enjoyable. I&amp;rsquo;ll try this a few more times to see how I feel about it.&lt;/p&gt;
&lt;p&gt;Today&amp;rsquo;s coding session is to continue the work of adding saved favourite podcast clips from Blogging Tools. Previously this was a standalone application connected to a database, but along with the tech stack used to build this falling out of active support, along with the feeds no longer working for reasons I rather not look into, I want to replace this with a static site that takes nothing to maintain. One other thing is that I want to move away from referencing links to the podcast clips and saving actual audio. Since I&amp;rsquo;m dealing with copyrighted material, some of it that is for pay, this site will be kept private.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m choosing to use Blogging Tools for this as it has a relatively decent podcast clipper that I use for clips I publish to my site. For the static site itself, I&amp;rsquo;m choosing to look at Hugo, given that I&amp;rsquo;m comfortable with this approach too. Although I do have some concerns about the long term viability of this for sites I don&amp;rsquo;t touch that often. An initial attempt at this used another site I use to track audio that used a template that fell out of maintenance and broke when I moved to Hugo 0.151. The Hugo maintainers really need to clean up their versioning act.&lt;/p&gt;
&lt;p&gt;Anyway, the current status of this is a temporary Hugo site with a new template. At this stage, I&amp;rsquo;ve got Blogging Tools running a new job type that will take the clipped audio and image thumbnail, save them as static data to Hugo, and publish a new post.&lt;/p&gt;
&lt;p&gt;At the moment, the template isn&amp;rsquo;t including the thumbnail or audio HTML. This is now a point of committing to this site and template: moving it out of the temp directory and into a proper workspace. So let&amp;rsquo;s start that now. I&amp;rsquo;m using the &lt;a href=&#34;https://github.com/ldeso/hugo-flex&#34;&gt;Hugo Flex&lt;/a&gt; theme as it provides a nice clean canvas to start with, and it seems to be in active maintenance (something to watch out for when dealing with Hugo themes). Added the git module, and setting Goldmark unsafe to true (once again) and now the new Hugo site is ready:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-09.35.03-localhost-fd2731f7bcc3.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;9ea5d8956f0e4d3138d476569919f9c7&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-09.35.03-localhost-fd2731f7bcc3.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;2025-10-05 09.35.03 localhost fd2731f7bcc3.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Now testing this with Blogging Tools. I&amp;rsquo;ve already got an ATP clip ready to go. And after remembering to change the target directory from the temp Hugo site to the real one, I am ready to test the extract. All that involves is clicking &amp;ldquo;Save&amp;rdquo;:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-09.36.41-localhost-bdf14f391c73.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;9ea5d8956f0e4d3138d476569919f9c7&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-09.36.41-localhost-bdf14f391c73.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;2025-10-05 09.36.41 localhost bdf14f391c73.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Okay, that didn&amp;rsquo;t work for a very stupid reason. I set the target directory to Blogging Tools repo, not the Hugo site. Made the change (Blogging Tools is being run using &lt;a href=&#34;https://github.com/air-verse/air&#34;&gt;Air&lt;/a&gt; which gives me file watching capabilities one would see in the frontend world) and can now see that the new post was written to the new Hugo site.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-09.43.17-localhost-42cb85cfbe26.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;9ea5d8956f0e4d3138d476569919f9c7&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-09.43.17-localhost-42cb85cfbe26.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;2025-10-05 09.43.17 localhost 42cb85cfbe26.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Now to adjust the template a little, just to add the audio and thumbnail. The Markdown files produced by Blogging Tools have all the clip information in the front matter:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;title: &amp;#34;449:  An Unclean Mouse&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;episode:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  guid: some-random-id
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  url: &amp;#34;https://atp.fm/449&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  title: &amp;#34;449:  An Unclean Mouse&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  date: 2021-09-23 12:27:49 +1000 AEST
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  image_src: &amp;#34;https://cdn.atp.fm/artwork-bootleg&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  audio_src: &amp;#34;https://example.com/file.mp3&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;show:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  title: &amp;#34;Accidental Tech Podcast: Unedited Live Stream&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;clip:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  start: 0s
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  duration: 60ms
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;date: 2021-09-23 12:27:49 +1000 AEST
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;thumbnail: images/DQkt_Lk5QRjXVHE0rdR09.jpg
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;audio: audio/5fFX8m0BqqFqTwlVAtb2Y.mp3
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Test thing
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Oh, before I do that. Let me fix the dates should that they&amp;rsquo;re in UTC and formatted as ISO 8601. This is a quick change to the template used by Blogging Tools:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;date: {{.Clip.EpisodeDate.UTC.Format &amp;#34;2006-01-02T15:04:05Z07:00&amp;#34;}}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Another quick test… perfect. The dates are coming through again. Oh, and a quick aside: because the front matter is all included in one Go template, I had to make sure to properly quote the string values, lest they contain colons and other constructs YAML doesn&amp;rsquo;t like. Easy way to do that is pipe them through &lt;code&gt;printf &amp;quot;%q&amp;quot;&lt;/code&gt;, which quotes them as if they&amp;rsquo;re Go strings. May not be perfect but it&amp;rsquo;s good enough for now:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;title: {{.Clip.EpisodeTitle | printf &amp;#34;%q&amp;#34;}}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So, lets fix the template. I&amp;rsquo;ll start by creating a new template in &lt;code&gt;layouts/clips/singles.html&lt;/code&gt; which is a copy-and-paste of the existing theme post. I&amp;rsquo;ll then add details of the front matter, such as the thumbnail and audio player. This I can get using the &lt;a href=&#34;https://gohugo.io/configuration/params/&#34;&gt;Params&lt;/a&gt; template construct. I&amp;rsquo;ll start by just printing it:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-gotemplate&#34; data-lang=&#34;gotemplate&#34;&gt;{{ define &amp;#34;main&amp;#34; }}

  {{ $context := dict &amp;#34;page&amp;#34; . &amp;#34;level&amp;#34; 1 &amp;#34;isdateshown&amp;#34; true }}
  {{ partial &amp;#34;heading.html&amp;#34; $context }}
  
  Audio = {{.Params.audio}}, image = {{.Params.thumbnail}}
  
  {{ .Content }}
  {{ partial &amp;#34;tags.html&amp;#34; . }}
  {{ partial &amp;#34;comments.html&amp;#34; . }}

{{ end }}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-09.54.52-localhost-c59d83c214d9-1.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;9ea5d8956f0e4d3138d476569919f9c7&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-09.54.52-localhost-c59d83c214d9-1.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;2025-10-05 09.54.52 localhost c59d83c214d9 1.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;I had to restart Hugo, but that&amp;rsquo;s coming through. I need to convert these into proper URLs though. I&amp;rsquo;ll start by trying the &lt;a href=&#34;https://gohugo.io/functions/urls/absurl/&#34;&gt;absurl&lt;/a&gt; template function:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-gotemplate&#34; data-lang=&#34;gotemplate&#34;&gt;Audio = {{ absURL .Params.audio}}, image = {{ absURL .Params.thumbnail}}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-09.57.47-localhost-392aecb7bb5f-1.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;9ea5d8956f0e4d3138d476569919f9c7&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-09.57.47-localhost-392aecb7bb5f-1.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;2025-10-05 09.57.47 localhost 392aecb7bb5f 1.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Okay, getting better. Let&amp;rsquo;s try and turn them into proper HTML elements:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;  &amp;lt;img src=&amp;#34;{{ absURL .Params.thumbnail}}&amp;#34;&amp;gt;
  &amp;lt;audio src=&amp;#34;{{ absURL .Params.audio}}&amp;#34; controls&amp;gt;&amp;lt;/audio&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-09.59.35-localhost-56693aeddfc3-1.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;9ea5d8956f0e4d3138d476569919f9c7&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-09.59.35-localhost-56693aeddfc3-1.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;2025-10-05 09.59.35 localhost 56693aeddfc3 1.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Good, the image and audio is coming through. Image looks a little large but I&amp;rsquo;ll fix that at little later. Oh, and it looks like the clips are showing up in the home page already. That&amp;rsquo;s a nice touch. I do want to change the clip paths though. At the moment, they&amp;rsquo;re saved using the auto-incrementing clip ID, which is not ideal. Clip metadata is stored in an Sqlite database and the library I&amp;rsquo;m using — which is &lt;a href=&#34;modernc.org/sqlite&#34;&gt;a Go port of Sqlite3 that&lt;/a&gt; doesn&amp;rsquo;t need CGO — has a nasty habit of reusing row IDs. I really should replace this with an auto-generated primary key. But that&amp;rsquo;s for later.&lt;/p&gt;
&lt;p&gt;Right now, I think I&amp;rsquo;ll use a hash of the episode Guid for the path. ATP seems to have randomly generated hashes but I know that some RSS feeds choose to use URLs, which won&amp;rsquo;t do for a path. Why the Guid? Well, I want to be able to regenerate a clip if need be, replacing the existing one.&lt;/p&gt;
&lt;p&gt;Actually, no, that won&amp;rsquo;t do. I want to be able to create multiple clips from the same episode. So maybe I&amp;rsquo;ll use a hash that includes the Guid and clip timestamp. So let&amp;rsquo;s try that.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;clipPathRaw&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fmt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;%v:%v&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;clip&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;EpisodeGUID&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;clip&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ClipStart&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;clipPathHash&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;md5&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Sum&lt;/span&gt;([]byte(&lt;span style=&#34;color:#a6e22e&#34;&gt;clipPathRaw&lt;/span&gt;))  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;clipPath&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;hex&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;EncodeToString&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;clipPathHash&lt;/span&gt;[:])  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;postFilename&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;filepath&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Join&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;postDir&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fmt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;%v.md&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;clipPath&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;MD5 should be fine: these don&amp;rsquo;t need to be cryptographically safe. Also, &lt;code&gt;md5.Sum&lt;/code&gt; returns an &lt;code&gt;[16]byte&lt;/code&gt; array, and I keep forgetting how to return this as a &lt;code&gt;[]byte&lt;/code&gt; slice. Apparently it&amp;rsquo;s &lt;code&gt;array[:]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Another quick test. Yeah, it works. The clip paths now look like this: &lt;code&gt;http://localhost:1313/clips/210da5b665c38c809eb1ea481b1b22de/&lt;/code&gt; Not great, but I tend to browse this visually so I think I can live with this.&lt;/p&gt;
&lt;p&gt;Okay, next thing is having Blogging Tools generate the clips and commit them to Git. The goal is to have the Hugo site stored in a Git repository and built to a private, undisclosed website. I did this for Nano Journal, which synchronised journal entries to a Git repo after saving it locally. Looking at the code, though, I have no idea how I setup the credentials. There&amp;rsquo;s nothing in the config hinting at a certificate so I may have done it manually.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also wondering if I can use &lt;a href=&#34;https://github.com/go-git/go-git&#34;&gt;Go Git&lt;/a&gt; for this. I didn&amp;rsquo;t use this for Nano Journal, as I wanted to use LFS; which Go Git doesn&amp;rsquo;t support (instead I just shelled out to &lt;code&gt;git&lt;/code&gt;). But I&amp;rsquo;m wondering if I can live without LFS for this.&lt;/p&gt;
&lt;p&gt;Okay, let&amp;rsquo;s try this. I&amp;rsquo;ll push the current Hugo site to a test repo on my Forgejo instance and see whether I can check it out from Blogging Tools. This will be done as a user that I&amp;rsquo;ll configure Blogging Tools to act as.&lt;/p&gt;
&lt;p&gt;Then I added Go Git to blogging tool and created a new provider for Git. Added a method to either pull or clone a remote repository, checkout a branch, and pull updates from the repo. I&amp;rsquo;ve chosen to use the file system for keeping the local workspace so that Blogging Tools doesn&amp;rsquo;t need to keep cloning the repo whenever there&amp;rsquo;s a need to add a clip. This I&amp;rsquo;m just going to keep in ephemeral storage so that when the Docker container gets cycled and the workspace lost, it&amp;rsquo;ll just recreate it.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Provider&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;CloneOrPull&lt;/span&gt;() &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; (  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;repo&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Repository&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;isDir&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;workspace&lt;/span&gt;) {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#75715e&#34;&gt;// Workspace doesn&amp;#39;t exist. Pull from repo.  &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;repo&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;PlainClone&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;workspace&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;CloneOptions&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a6e22e&#34;&gt;URL&lt;/span&gt;:               &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;repoURL&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a6e22e&#34;&gt;RecurseSubmodules&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;DefaultSubmoduleRecursionDepth&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a6e22e&#34;&gt;Auth&lt;/span&gt;: &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;BasicAuth&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;             &lt;span style=&#34;color:#a6e22e&#34;&gt;Username&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;username&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;             &lt;span style=&#34;color:#a6e22e&#34;&gt;Password&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;password&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          },  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       })  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    } &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;repo&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;PlainOpen&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;workspace&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;worktree&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;repo&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Worktree&lt;/span&gt;()  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;worktree&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Pull&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;PullOptions&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;RemoteURL&lt;/span&gt;:         &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;repoURL&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;RecurseSubmodules&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;DefaultSubmoduleRecursionDepth&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;ReferenceName&lt;/span&gt;:     &lt;span style=&#34;color:#a6e22e&#34;&gt;plumbing&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ReferenceName&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;branch&lt;/span&gt;),  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;Auth&lt;/span&gt;: &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;BasicAuth&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a6e22e&#34;&gt;Username&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;username&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a6e22e&#34;&gt;Password&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;password&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       },  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;worktree&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Checkout&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;CheckoutOptions&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;Branch&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;plumbing&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ReferenceName&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;branch&lt;/span&gt;),  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Quick test of this, and I&amp;rsquo;m seeing a &amp;ldquo;repository doesn&amp;rsquo;t exist&amp;rdquo; error.&lt;/p&gt;
&lt;p&gt;Actually, no. That was because of an earlier use of the repository URL when I actually wanted the workspace. But now I&amp;rsquo;m seeing a &amp;ldquo;reference not found&amp;rdquo; error when I try to pull from the origin:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-11.24.51-localhost-87f4f1d9b693.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;9ea5d8956f0e4d3138d476569919f9c7&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-11.24.51-localhost-87f4f1d9b693.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;2025-10-05 11.24.51 localhost 87f4f1d9b693.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Ah, okay. I should&amp;rsquo;ve used &lt;code&gt;plumbing.NewBranchReferenceName&lt;/code&gt; here. That&amp;rsquo;s fix that. Now it&amp;rsquo;s an &lt;code&gt;already up-to-date&lt;/code&gt; error. That&amp;rsquo;s fine, I&amp;rsquo;ll just add an ignore case here:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;worktree&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Pull&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;PullOptions&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;RemoteURL&lt;/span&gt;:         &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;repoURL&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;RecurseSubmodules&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;DefaultSubmoduleRecursionDepth&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;ReferenceName&lt;/span&gt;:     &lt;span style=&#34;color:#a6e22e&#34;&gt;plumbing&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewBranchReferenceName&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;branch&lt;/span&gt;),  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;Auth&lt;/span&gt;: &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;BasicAuth&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;Username&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;username&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;Password&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;password&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;errors&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Is&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NoErrAlreadyUpToDate&lt;/span&gt;) {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Another test:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-11.28.44-localhost-055ec2134222.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;9ea5d8956f0e4d3138d476569919f9c7&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/2025-10-05-11.28.44-localhost-055ec2134222.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;2025-10-05 11.28.44 localhost 055ec2134222.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;And that worked. I managed to pull changes from remote.&lt;/p&gt;
&lt;p&gt;Deleting the workspace and trying a clone. Hmm, that&amp;rsquo;s strange. I&amp;rsquo;m seeing an &lt;code&gt;authentication required&lt;/code&gt; error, yet it looks like the repository was cloned successfully. Let me try and ignore that error.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;repo&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;repo&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;log&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Printf&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;warn: error received alongside repo: %v&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Okay, that worked. It&amp;rsquo;s not pretty, but I think it&amp;rsquo;s fine for now.&lt;/p&gt;
&lt;p&gt;Now to commit the new files to the repository. Although partial failures from staging multiple files — one file is stage but the second one fails for some reason — may introduce some problems where the state is not completely settled, so I may also need a method of ensuring the workspace is pristine: no files should exist after cloning or pulling that is not staged. So need to add that first. I don&amp;rsquo;t see a method to do this on the Workspace type, so basically what I&amp;rsquo;ll do is reset hard, and then iterate over the untracked files and remove them:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Reset&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ResetOptions&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;Mode&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;HardReset&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Status&lt;/span&gt;()  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;s&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;range&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;s&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Worktree&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Untracked&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;os&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Remove&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Well, that&amp;rsquo;s the plan eventually. I&amp;rsquo;ll just log them for now.&lt;/p&gt;
&lt;p&gt;Then it&amp;rsquo;s just a matter of adding files and commit the changes. First attempt at this failed with an &lt;code&gt;entry not found&lt;/code&gt; error. That was just a bad path. Adding files now working, along with committing, and now we&amp;rsquo;re failing at the push. Reason is that unlike all the other Go Git methods, which were pretty high level, this one leaks a bit of the mechanics of Git, requiring the use of a &lt;a href=&#34;https://git-scm.com/book/en/v2/Git-Internals-The-Refspec&#34;&gt;Ref Spec&lt;/a&gt;. I&amp;rsquo;ve just hard coded it to a version which will push the local main branch to the origin main branch.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;repo&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Push&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;PushOptions&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;RemoteURL&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;repoURL&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;Auth&lt;/span&gt;: &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;BasicAuth&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;Username&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;username&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;Password&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;password&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;RefSpecs&lt;/span&gt;: []&lt;span style=&#34;color:#a6e22e&#34;&gt;config&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;RefSpec&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#a6e22e&#34;&gt;config&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;RefSpec&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;fmt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;+refs/heads/%v:refs/remotes/origin/%v&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;branch&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;branch&lt;/span&gt;)),  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Oh, before that, I finding a &lt;code&gt;cannot create empty commit: clean working tree&lt;/code&gt;. I&amp;rsquo;m opening the work tree between staging the files and committing. Is that the problem?&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-05-at-21.57.36.png&#34; 
   class=&#34;glightbox&#34;
   data-gallery=&#34;9ea5d8956f0e4d3138d476569919f9c7&#34;
   
&gt;
  &lt;img src =&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-10-05-at-21.57.36.png&#34; 
       loading=&#34;lazy&#34;
       decoding=&#34;async&#34;
       style=&#34;border-radius: 5px; max-width: 100%&#34;
       alt=&#34;cleanshot-2025-10-05-at-21.57.36.png&#34; 
        
  /&gt;
&lt;/a&gt;

&lt;/p&gt;
&lt;p&gt;Hmm, no. It might be that I&amp;rsquo;m adding the full path of the file to the index. Running &lt;code&gt;git status&lt;/code&gt; in that repository yielded full path entries, which is very uncharacteristic. Furthermore, trying to unstage them did nothing. I&amp;rsquo;ll remove the repo directory and change the logic to get the relative path of the file before they&amp;rsquo;re added:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;range&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;files&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;relPath&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;filepath&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Rel&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;workspace&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Add&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;relPath&lt;/span&gt;); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Commit&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;msg&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;git&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;CommitOptions&lt;/span&gt;{}); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fault&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wrap&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Oh, it was because the files I was trying to add were already added to the repository from earlier in the day, and they weren&amp;rsquo;t being changed by the job. Let me remove them from the repository and try again.&lt;/p&gt;
&lt;p&gt;Okay, it was that. Commits are now working, although pushes don&amp;rsquo;t seem to be synchronising the commits. When I look at recent changes in Forgejo, it only shows my commits, not the ones created by the service user I made for Blogging Tools. Will try adjusting the refspec to push all heads:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;RefSpecs&lt;/span&gt;: []&lt;span style=&#34;color:#a6e22e&#34;&gt;config&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;RefSpec&lt;/span&gt;{  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;+refs/heads/*:refs/remotes/origin/*&amp;#34;&lt;/span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nah, that didn&amp;rsquo;t help. The method is returning no error, yet it doesn&amp;rsquo;t seem to be synchronising the branches. Will need to learn more about how this all works. But I think that&amp;rsquo;s something for later.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/09/30/bit-more-on-lifts-in.html</link>
      <pubDate>Tue, 30 Sep 2025 22:14:06 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/09/30/bit-more-on-lifts-in.html</guid>
      <description>&lt;p&gt;Bit more on lifts in Godot today. Re-engineered how the whole thing works: now everything is driven by the stationary lift doors. Each door has a reference to a lift and a target door. This makes the lift carriage itself rather passive: it will continue to reparent the player as before, but it no longer needs to track activations or have animating doors of it&amp;rsquo;s own. The doors just tell it where and when to go.&lt;/p&gt;
&lt;p&gt;This has got proper targeting working so that a lift can now move between a pair of doors. It also allows for the player to call for the lift when it&amp;rsquo;s not positioned at the doors already. There are a few more dependencies amongst the various entities, but I think it makes for a more robust system.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/09/28/small-change-to-the-thirst.html</link>
      <pubDate>Sun, 28 Sep 2025 11:09:06 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/09/28/small-change-to-the-thirst.html</guid>
      <description>&lt;p&gt;Small change to the thirst mechanic for my Godot project. Switched from discrete thirst levels to a single timer that will tick down if the player is thirsty. This allowed for a change to how I indicate this to the player, replacing text messages that&amp;rsquo;ll be displayed at each thirst level with a gauge that shows up on the HUD.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250928-105703.png&#34; width=&#34;600&#34; height=&#34;381&#34; alt=&#34;Auto-generated description: A retro-style video game features a knight-like character navigating a platform with a floating pink enemy and a water obstacle below.&#34;&gt;
&lt;p&gt;I thought that the messages would be enough, but after playing through with them, they turned out to be more of a hindrance. They didn&amp;rsquo;t communicate the player&amp;rsquo;s thirst level well enough: they show up for a few seconds, then disappear, leaving the player to wonder how much time they have. This information is now always present with the gauge. Plus, I think it gives more of a sense of urgency, in that a gauge that&amp;rsquo;s constantly ticking down will encourage the player to play a little faster in order to reach the next water bottle before they perish from thirst.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/09/27/more-on-that-godot-project.html</link>
      <pubDate>Sat, 27 Sep 2025 11:02:07 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/09/27/more-on-that-godot-project.html</guid>
      <description>&lt;p&gt;More on that Godot project today. Finally bit the bullet and started working on proper backdrops. I&amp;rsquo;ve leaving the artwork until later (i.e. I&amp;rsquo;m procrastinating), today was all about the mechanics, trying to get parallax scrolling working. Fortunately Godot has &lt;a href=&#34;https://docs.godotengine.org/en/stable/tutorials/2d/2d_parallax.html&#34;&gt;built-in nodes to make this easy&lt;/a&gt;, although I did have to upgrade Godot to 4.5.&lt;/p&gt;
&lt;p&gt;This is my first cut, some simple colour banding to represent sky and sand in one of the desert levels:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250927-105652.png&#34; width=&#34;600&#34; height=&#34;381&#34; alt=&#34;Auto-generated description: A pixel-art game interface shows a character in armour on a blocky terrain with a blue sky background and floating coins.&#34;&gt;
&lt;p&gt;These two layers were added to the scene as &lt;a href=&#34;https://docs.godotengine.org/en/stable/classes/class_parallax2d.html#class-parallax2d&#34;&gt;Parallax2D&lt;/a&gt; nodes, each with a single Sprite child. Positioning the sprites was a little tricky. Godot suggests placing them at the origin, but what I didn&amp;rsquo;t understand was that the actual position of the sprite depends a lot on the scroll scale of the Parallax2D node. I added the sky layer without changing the scroll position and it took me a while before I discovered that it was appearing below the camera. Only after reducing the scroll scale to about 0.1 did it start showing up in the viewport (one nice thing about Godot 4.5 is that the &amp;ldquo;play scene&amp;rdquo; window includes debug options, allowing you to hijack the camera and move it around the scene).&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250927-105631.png&#34; width=&#34;600&#34; height=&#34;318&#34; alt=&#34;Auto-generated description: A game development interface is displayed, featuring a pixelated game scene with water and blocks being edited in an application, likely Godot Engine.&#34;&gt;
&lt;p&gt;I will need to introduce some detail in the backdrop layers soon. The bands are too simple and may induce some vertigo when the player is ascending to high platforms. Plus, it just looks boring. Not that they&amp;rsquo;re meant to be eye-catching but something a little more interesting would be nice.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/09/23/small-devlog-update-today-continuing.html</link>
      <pubDate>Tue, 23 Sep 2025 22:10:06 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/09/23/small-devlog-update-today-continuing.html</guid>
      <description>&lt;p&gt;Small DevLog update today. Continuing to work on that Godot game. I got the bulk of the mechanics of level 2-1 built (it still needs dressing up), and I spent this evening mainly just play-testing it, and tweaking it. I think I may need to get someone else to play-test it though, just to make sure it&amp;rsquo;s not too hard. It&amp;rsquo;s significantly long, coming in at 10,560 units horizontally, and given how many new mechanics are involved, I&amp;rsquo;m a little worried about the difficulty curve. But I am happy about the mix of elements I do have. It doesn&amp;rsquo;t feel boring, which was the concern I had. There are some timing challenges, and I the pacing across the level feels fine. But I really should convince someone else to try it out.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Trying OpenAI Codex to Produce Freelens Logo Creator</title>
      <link>https://lmika.org/2025/09/13/devlog-trying-openai-codex-to.html</link>
      <pubDate>Sat, 13 Sep 2025 09:05:30 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/09/13/devlog-trying-openai-codex-to.html</guid>
      <description>&lt;p&gt;Tried a bit of vibe coding using OpenAI Codex yesterday.&lt;/p&gt;
&lt;p&gt;I needed something to produce nice logos for &lt;a href=&#34;https://freelensapp.github.io&#34;&gt;Freelens&lt;/a&gt; so I can tell each cluster apart. The default avatar for Freelens is an abbreviation of the cluster name set atop a random background colour, which doesn&amp;rsquo;t do me a lot of good as the cluster names I work with are all similar, and would produce the same abbreviations. Furthermore, Freelens doesn&amp;rsquo;t have a way to change these default avatars. But it does have a way to upload new avatars as an image.&lt;/p&gt;
&lt;p&gt;This was a perfect candidate for trying out OpenAI Codex. A use-case that I needed but was not important nor interesting enough for me to build manually. I mean, knowing me, I probably would&amp;rsquo;ve built it, but it wouldn&amp;rsquo;t have been good use of my time. So I created a new Github repo, connected it to Codex, and gave it this prompt:&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;I would like a static web-page, consisting of one HTML file, and one JavaScript file. This is to be vanilla JavaScript that does not require NPM or any external dependencies.&lt;/p&gt;
&lt;p&gt;This page is to have a form which accepts the following input:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A text input with the name &amp;ldquo;Upper&amp;rdquo; which accepts a string of any kind.&lt;/li&gt;
&lt;li&gt;A text input with the name &amp;ldquo;Lower&amp;rdquo; which accepts a string of any kind.&lt;/li&gt;
&lt;li&gt;A selection for background colour. Instead of a colour picker, please allow the user to select one of 8 distinct colour choices. Each one should be distinct enough such that the user is able to distinguish them from each other. They should be of medium darkness, such that if white text were to be display atop of it, the text should be legible; yet not too dark as to be hard to distinguish.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The page is also to have a canvas element, of about 80 x 80 points, which will take input of the form above and do the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fill the background with the chosen background colour.&lt;/li&gt;
&lt;li&gt;Draw a thin dividing line — no greater than 6 px wide — in white across the width of the canvas.&lt;/li&gt;
&lt;li&gt;Print the form value of &amp;ldquo;Upper&amp;rdquo; in uppercase, in a san serif font, centred and above the dividing line in white. The printed text should be large enough to fill most of&lt;/li&gt;
&lt;li&gt;Print the form value of &amp;ldquo;Lower&amp;rdquo; in uppercase, in a san serif font, centred and below the dividing line in white.&lt;/li&gt;
&lt;li&gt;Ensure that a suitable margin of 15 pixels padding around the border of the canvas content, and that the printed text and dividing line is not drawn beyond the margin.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The canvas element is to be updated when the form values are changed.&lt;/p&gt;
&lt;p&gt;Below the canvas should be a button which should allow the user to download the canvas image as a PNG.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;And here&amp;rsquo;s what it produced:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250913-090254.png&#34; width=&#34;600&#34; height=&#34;447&#34; alt=&#34;Auto-generated description: A browser window showcases a logo generator tool with a blue logo on the left labeled TEST AP-1 and options for customization above.&#34;&gt;
&lt;p&gt;It pretty much did what it said on the tin: when I changed the value of upper or lower, or changed the background colour, the canvas would redraw showing a different preview. Clicking &amp;ldquo;Download PNG&amp;rdquo; would allow me to save it as an image.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250913-090258.png&#34; width=&#34;600&#34; height=&#34;447&#34; alt=&#34;Auto-generated description: A logo design interface with fields for upper and lower text, a background color menu, and a Download PNG button is displayed in a browser window.&#34;&gt;
&lt;p&gt;On the whole, what I received was pretty good. It&amp;rsquo;s not perfect — there&amp;rsquo;s no styling; orange is a bit too close to amber, as is green and teal; and the alignment of the bottom line is slightly off. But it&amp;rsquo;s a pretty decent start.&lt;/p&gt;
&lt;p&gt;There were some things I wanted to change, so I went in myself. I found a nice looking stylesheet called &lt;a href=&#34;https://picocss.com&#34;&gt;Pico CSS&lt;/a&gt;, which I&amp;rsquo;ve applied. Normally I would&amp;rsquo;ve turned to &lt;a href=&#34;https://simplecss.org&#34;&gt;simple.css&lt;/a&gt;, but I&amp;rsquo;ve been using that a lot and I wanted to try a nice, fresh look. I also adjusted the vertical positioning of the lower row in the icon, added support for single line icons, and adjusted the colours a little.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250913-090305.png&#34; width=&#34;600&#34; height=&#34;435&#34; alt=&#34;Auto-generated description: A logo creation interface is displayed with options to input text, select background color, and download a PNG file.&#34;&gt;
&lt;p&gt;The whole thing looks quite nice now, and I&amp;rsquo;m quite happy with the icons it produces:&lt;/p&gt;
&lt;img class=&#34;block-center&#34; src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250912-220408.png&#34; width=&#34;212&#34; height=&#34;212&#34; alt=&#34;Auto-generated description: Four colored squares labeled Dev, Test, Preprod, and Prod represent different stages of a deployment pipeline.&#34;&gt;
&lt;p&gt;I&amp;rsquo;m still working out how these AI coding agents will fit into my life. I&amp;rsquo;ve been using Claude Code occasionally, and that&amp;rsquo;s been a good fit for projects where I&amp;rsquo;d prefer to be the driver. But there are occasional things that I&amp;rsquo;d like to see exist but are a little too trivial to spend the time on. In those cases, I may not have built it at all; or I would&amp;rsquo;ve built it and then immediately regret spending the time doing so it. And that might be where Codex could be useful: it provides a good starting point, which I can then refine.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re interested in trying this tool out, &lt;a href=&#34;https://tools.lmika.app/freelens-logo/&#34;&gt;you can find a link to it here&lt;/a&gt;. The source &lt;a href=&#34;https://github.com/lmika/webtools&#34;&gt;you can find on GitHub&lt;/a&gt;.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/09/07/ah-macoss-lockeddown-nature-strikes.html</link>
      <pubDate>Sun, 07 Sep 2025 11:14:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/09/07/ah-macoss-lockeddown-nature-strikes.html</guid>
      <description>&lt;p&gt;Ah, MacOS&amp;rsquo;s locked-down nature strikes again! Was testing the CI/CD build for Dequoter and after downloading the artefact and attempting to open it, I got this warning message:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250907-101030.png&#34; width=&#34;308&#34; height=&#34;298&#34; alt=&#34;Auto-generated description: A warning dialog box states that Dequoter is damaged and suggests moving it to the Bin, with options to cancel or proceed.&#34;&gt;
&lt;p&gt;Turn&amp;rsquo;s out it was being quarantined by MacOS, and &lt;a href=&#34;https://apple.stackexchange.com/a/328207&#34;&gt;these instructions resolved the issue&lt;/a&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;xattr -dr com.apple.quarantine &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/Applications/Your Application.app&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The binaries not notarised so I wasn&amp;rsquo;t expecting it to work out of the box. I was hoping that it would do that thing where the app will be listed in settings and I can allow it to launch from there, but I guess there&amp;rsquo;s something about where this file came from that was too much from MacOS. Ah well, I can live with this for the short term.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Dequoter — Something Different Today</title>
      <link>https://lmika.org/2025/09/06/devlog-dequoter-something-different-today.html</link>
      <pubDate>Sat, 06 Sep 2025 16:10:04 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/09/06/devlog-dequoter-something-different-today.html</guid>
      <description>&lt;p&gt;When it comes to hobby projects, I&amp;rsquo;m trying to reduce the number of things I build for work. But I&amp;rsquo;ve decided to revisit one that I&amp;rsquo;ve abandoned, given the frequent need for this. The issue is simple: I have a JSON data structure that is quoted within a string, and I want to &amp;ldquo;unquote&amp;rdquo; it and filter the JSON so that I can read it.&lt;/p&gt;
&lt;p&gt;So I thought I&amp;rsquo;d spend today restarting the Dequoter project. The best way to describe it is &lt;a href=&#34;https://github.com/IvanMathy/Boop&#34;&gt;a Boop clone&lt;/a&gt;. I like Boop, I use Boop. And I know Boop is extendable, so I could implement any processor I need using JavaScript. But I&amp;rsquo;m not a huge fan of JavaScript, and Go has the function I need &lt;a href=&#34;https://pkg.go.dev/strconv@go1.25.1#Unquote&#34;&gt;right there in the standard library&lt;/a&gt;. And with most things I encounter, I want to build my own. I know I shouldn&amp;rsquo;t, and I&amp;rsquo;ve been better at resisting. But I&amp;rsquo;d figured this could be a nice little distraction. And I thought I could get much of what I need done in a morning.&lt;/p&gt;
&lt;p&gt;So I started a new &lt;a href=&#34;https://wails.io&#34;&gt;Wales project&lt;/a&gt; and after about 3 hours, I had a scratchpad with an invokable command palette that I can use to dequote a string and format a JSON object:&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-09-06-at-15.57.26.mp4&#34; poster=&#34;https://lmika.org/uploads/2025/ecf16f805a.png&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Granted, it&amp;rsquo;s not much for 3 hours. While I did use Claude Code a little, much of this was hand rolled. And I spent a lot of time learning the ins and outs of &lt;a href=&#34;https://codemirror.net&#34;&gt;Code Mirror&lt;/a&gt;. This is why I wanted to use Wales this time around. The &lt;a href=&#34;https://lmika.org/2024/07/20/continuing-my-exploration.html&#34;&gt;previous run at this&lt;/a&gt; used &lt;a href=&#34;https://fyne.io/&#34;&gt;Fyne&lt;/a&gt;, which was really nice to use, but the available widgets were quite limited. The nice thing about Wales is that I can build the UI in HTML and it uses the built-in HTML renderer from the OS: no need to bundle Chrome. The frontend is mainly vanilla HTML and JavaScript, although I am using &lt;a href=&#34;https://stimulus.hotwired.dev&#34;&gt;Stimulus&lt;/a&gt; to help with some of the controls. The backend is, of course, Go.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-09-06-at-15.46.43.png&#34; width=&#34;400&#34; height=&#34;572&#34; alt=&#34;Auto-generated description: Window displaying file information for an application named dequoter, including details like size, location, and version.&#34;&gt;
&lt;figcaption&gt;Final build size is 8.4 MB.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;By the end of this morning&amp;rsquo;s session, what I had was not pretty, but it&amp;rsquo;s functional and it already does what I need it to do. I do have some plans for integrating this with UCL, and using that to define processors. That should hopefully leave all the heavy core stuff to be built near the beginning, and that could sit pretty while the extensions are added in a nicer language. Anyway, more on this if this goes anywhere.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/09/03/starting-to-work-on-the.html</link>
      <pubDate>Wed, 03 Sep 2025 21:27:26 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/09/03/starting-to-work-on-the.html</guid>
      <description>&lt;p&gt;Starting to work on the background tiles. This is what I have so far. I hope it&amp;rsquo;s not too busy or distracting.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250903-212503.png&#34; alt=&#34;Auto-generated description: A pixelated video game scene features a character in knight armor navigating a stone brick environment with a wooden crate and a collectible item.&#34;&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Godot Project — Bricks in Level 2-3 Laid</title>
      <link>https://lmika.org/2025/09/01/devlog-godot-project-bricks-in.html</link>
      <pubDate>Mon, 01 Sep 2025 21:31:56 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/09/01/devlog-godot-project-bricks-in.html</guid>
      <description>&lt;p&gt;Just a quick update today. I&amp;rsquo;ve finished all the brickwork in level 2-3. And it didn&amp;rsquo;t go too badly. Made one significant mistake which would&amp;rsquo;ve involved a lot of rework, that I patched up with some single tiles:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250901-212752.png&#34; width=&#34;600&#34; height=&#34;722&#34; alt=&#34;Auto-generated description: Two sections of a pixelated game map featuring stone walls, an arched door, and orange blocks resembling platforms.&#34;&gt;
&lt;figcaption&gt;Top: the mistake. Bottom: the fix.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Doing the rest of it was pretty dreary work. Godot does have some tools to make this easier, but there was no getting around the level of care needed to place the bricks correctly. But it&amp;rsquo;s all pretty much done now. And just for comparison to the &lt;a href=&#34;https://lmika.org/2025/07/26/devlog-godot-project-level-and.html&#34;&gt;before screenshots&lt;/a&gt; I took when I started, here&amp;rsquo;s how how the level looks now:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250901-211644.png&#34; width=&#34;600&#34; height=&#34;362&#34; alt=&#34;Auto-generated description: A pixelated video game scene shows a knight on a platform with lava below, stone blocks, and crates to the right.&#34;&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250901-211652.png&#34; width=&#34;600&#34; height=&#34;362&#34; alt=&#34;Auto-generated description: A pixelated video game scene shows a character in armor navigating a platform level with brick walls, crates, and spiked obstacles.&#34;&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250901-211659.png&#34; width=&#34;600&#34; height=&#34;362&#34; alt=&#34;Auto-generated description: A pixelated character in knight armor stands on a brick platform with a gray background, part of a retro-style video game.&#34;&gt;
&lt;p&gt;There&amp;rsquo;s still plenty of work. The background is not yet done, nor are any of the pickups placed. The HUD needs updating to show the key gems the player has, and I also need to repair some dodgy mechanics around moving platforms. But I guess that&amp;rsquo;s just a matter of plowing ahead.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Shutting Down Nano Journal</title>
      <link>https://lmika.org/2025/08/30/devlog-shutting-down-nano-journal.html</link>
      <pubDate>Sat, 30 Aug 2025 15:34:43 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/08/30/devlog-shutting-down-nano-journal.html</guid>
      <description>&lt;p&gt;With the move to Obsidian for my journalling needs, I shut down my bespoke journalling web-app. I deployed it on 26th August 2024, which makes it just over a year old. I did start using Obsidian on the 20th though, so it didn&amp;rsquo;t quite make it the entire year. Even so, not bad for something hand made and somewhat neglected. Most things I eventually abandon last way less than that.&lt;/p&gt;
&lt;p&gt;Anyway, here are some screenshots of the final version, just before I shut it down:&lt;/p&gt;
&lt;div class=&#34;img-gallery&#34;&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250830-115806.png&#34;
     
        alt=&#34;Auto-generated description: A digital journal interface features a text box with the prompt What&amp;#39;s been happening today and buttons labeled Attach and Save.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  The landing page, with a full-width post text area.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250830-115815.png&#34;
     
        alt=&#34;Auto-generated description: A journal entry interface with a textbox discussing layout issues and an attachment option is shown.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  A post with attachments.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250830-115840.png&#34;
     
        alt=&#34;Auto-generated description: A journal webpage displays a list of dated entries from May to August 2025, with links to each entry.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Clicking &amp;#39;Posts&amp;#39; will bring up a chronological list of posts from the last 6 months. Those with a paperclip are ones with attachments.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250830-115851.png&#34;
     
        alt=&#34;Auto-generated description: A digital note dated May 17, 2025, expresses frustration about relying on others for problem-solving and a desire for improvement.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  An example of a viewing a post. Attachments would appear below the body.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;/div&gt;
&lt;p&gt;You can compare this with some earlier screenshots documented in these posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://lmika.org/2024/08/22/i-enjoyed-reading.html&#34;&gt;22 August 2024&lt;/a&gt;: Development work started&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://lmika.org/2024/09/15/weekly-update-sept.html&#34;&gt;15 September 2024&lt;/a&gt;: Addition of attachments and titles&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://lmika.org/2024/10/04/im-not-really.html&#34;&gt;4 October 2024&lt;/a&gt;: Adding Git synchronisation of entries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I guess you can describe the ascetics of this project as the &amp;ldquo;minimum amount to get working.&amp;rdquo; That was true of the backend too, which was somewhat rushed and difficult to maintain. That might be why I never really gave this a lot of love as other projects like Blogging Tools. But I added some features I that I thought were neat: such as attachments, and keeping a copy of a draft in the browser&amp;rsquo;s local storage before it was sent to the server.&lt;/p&gt;
&lt;p&gt;Anyway, I&amp;rsquo;m now in the middle of migrating the old posts over to Obsidian. One thing I&amp;rsquo;m glad I added was Git synchronisation. And given that entries were plain markdown files, migration was simply a matter of checking out the repository and moving the posts across. Easy enough work, although I am taking my time. There&amp;rsquo;s a bit of reliving involved with moving the posts over, and I&amp;rsquo;m approaching a period where some pretty sad things happened.&lt;/p&gt;
&lt;p&gt;Anyway, that was Nano Journal. Good at what it needed to do.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Godot Project — Level 2-3 Update</title>
      <link>https://lmika.org/2025/08/26/devlog-godot-project-level-update.html</link>
      <pubDate>Tue, 26 Aug 2025 22:27:58 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/08/26/devlog-godot-project-level-update.html</guid>
      <description>&lt;p&gt;Been working on level 2-3 these past few weeks. And it&amp;rsquo;s a large one: 4,720 x 2,928 units. There are longer levels but those tend to be quite horizontal, unlike this one which feels like a mixture of the two dimensions:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250826-214656.png&#34; width=&#34;600&#34; height=&#34;318&#34; alt=&#34;Auto-generated description: A pixel art depiction of a pyramid interior displayed within a game development software interface.&#34;&gt;
&lt;p&gt;Reached a milestone this evening: the critical path has been built. And yeah, it ended up pretty much being a standard key-hunt. I did have an idea for having more puzzle elements, like requiring the player to work out combinations based on key colour, but given the length one has to travel to get the necessary keys, it felt like a hit to the pacing. I figured it would be more interesting to vary the mechanics in each zone the keys are located. They&amp;rsquo;re not complicated mechanics, but they did reach the limit of what I had in my toolbox. It got to the point where I started adding the classics, like dart shooters which fired projectiles.&lt;/p&gt;
&lt;div class=&#34;img-gallery&#34;&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250826-214959.png&#34;
     
        alt=&#34;Auto-generated description: A pixelated video game scene features a knight character standing on a wooden crate within a brick-walled level, avoiding swinging spiked traps.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Pick up these gems…
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250826-214933.png&#34;
     
        alt=&#34;Auto-generated description: A pixel art video game scene features a knight character on a brick platform navigating a level with ladders and a pickaxe-like object.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  …and place them on this holders to activate things. And yes, I stole this directly from Commander Keen.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250826-215012.png&#34;
     
        alt=&#34;Auto-generated description: A pixel art-style video game features a small knight character on green platforms above a wavy brown surface with obstacles and coins on the screen.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Given that this is in the desert world, the sand spinners make an appearance.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250826-214944.png&#34;
     
        alt=&#34;Auto-generated description: A pixelated video game scene features a knight character navigating platforms and obstacles made of spikes and bricks.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  The new dart shooters and the projectiles they fire.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;/div&gt;
&lt;p&gt;There are a few mechanics I wanted the player to learn. Trying to telegraph that via gameplay is not easy, but I&amp;rsquo;m hoping it will comes through. Some approaches I&amp;rsquo;m trying are slight cues in colour and making sure interaction indicators are clear. And also level geometry, such as moving the player through one-way gates and making it obvious that all they have to proceed is right in front of them.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also decided to move away from paid checkpoints, where you need to pay 5 coins to save your progress. I&amp;rsquo;ve replaced these with hidden checkpoints, of which this level has a few. I think given the size, and that some backtracking is involved, it makes more sense being quite generous with checkpoints in this level. I think I&amp;rsquo;ll keep the 5 coin toll gates though for other things.&lt;/p&gt;
&lt;p&gt;Work remaining on this level is prettying it up (it&amp;rsquo;s still rather barebones) plus adding pickups and maybe some bonuses. Painting in the brick tiles will take a while, but could be a nice exercise in calming work when one can switch off their brain somewhat. I also need to make the background tiles and (ugh) the actual level backdrop. Not quite sure what that will be. Something deserty no doubt.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: UCL — Comparing UCL To Some Early Ideas</title>
      <link>https://lmika.org/2025/08/22/devlog-ucl-comparing-it-to.html</link>
      <pubDate>Fri, 22 Aug 2025 11:14:02 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/08/22/devlog-ucl-comparing-it-to.html</guid>
      <description>&lt;p&gt;I was browsing through some very old notes I had when I came across one that contain an idea for a hypothetical shell-like command language, sort of what UCL was designed for. It was designed for a project called Nuget, which was a CLI torrent downloader (now defunct). Much like UCL, it was REPL based with a simple token-based command language, and I was thinking of ways to extend this to including scripting. Here&amp;rsquo;s the note in full:&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;The nuget command language will be something very similar to shell. It will be used to configure nuget (in an RC file), and configure extensions.&lt;/p&gt;
&lt;p&gt;Example - check that the VPN is running&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let piaCtlState = !piactl get connectionstate

if eval &#39;trim(${piaCtlState}) ne &amp;quot;Connected&amp;quot;&#39; {
    notice &amp;quot;Warn: VPN not connected.  Use &#39;piactl&#39; to connect&amp;quot;
}

on newjob {
    on done { echo &amp;quot;Job done: ${job}&amp;quot; }
}
&lt;/code&gt;&lt;/pre&gt;

  &lt;/blockquote&gt;
&lt;p&gt;I never implemented this language, opting to simply use TCL. But it&amp;rsquo;s interesting to see the desire of having an embeddable language for both REPLs and scripting alive and well back in February 2021 (the note&amp;rsquo;s datestamp).&lt;/p&gt;
&lt;p&gt;Assuming that UCL existed at the time, I image the script would&amp;rsquo;ve looked a little less TCL-ly and a little closer to how Go templates work. Maybe something likes the following, assuming that the built-ins were implemented:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;piaCtlState = os:shell &amp;#34;piactl get connectionstate&amp;#34;

if (ne (strs:trim $piaCtlState) &amp;#34;Connected&amp;#34;) {
    notice &amp;#34;Warn: VPN not connected.  Use &amp;#39;piactl&amp;#39; to connect&amp;#34;
}

# I&amp;#39;m guessing &amp;#39;on&amp;#39; in the original idea would have job as a global variable.
# UCL can simply pass it in as an argument.
on newjob { |job|
  on done { echo &amp;#34;Job done: $job&amp;#34; }
}
&lt;/code&gt;&lt;/pre&gt;</description>
    </item>
    
    <item>
      <title>Devlog: Blogging Tools — All About Images</title>
      <link>https://lmika.org/2025/08/10/devlog-blogging-tools-all-about.html</link>
      <pubDate>Sun, 10 Aug 2025 21:30:40 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/08/10/devlog-blogging-tools-all-about.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been working on Blogging Tools on-and-off the last few weeks, mainly around images and image management. They&amp;rsquo;re not super exciting in their own right so I&amp;rsquo;d figure I&amp;rsquo;ll briefly write about them here.&lt;/p&gt;
&lt;p&gt;The first major addition is the ability to save images to the server from the Image Tool app. There are some composition processors available and if one wanted to pre-process the image in some way, they would have to download the pre-processed image and upload it again for composition. This change will hopefully reduce the need for this back-and-forth. The download button has been replaced within the Image Tool app with a Save button, which will upload the processed image to the server.&lt;/p&gt;
&lt;p&gt;Saved images can be recalled from the processor by using a new picker. This replaced the Load image by URL option, which I never really used (although I may add it back within the Saved Image app).&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250810-212554.png&#34; width=&#34;600&#34; height=&#34;368&#34; alt=&#34;Auto-generated description: A grid layout displays images of a pigeon, crow, lion, tiger, and a cartoon fish on a blogging tool interface.&#34;&gt;
&lt;p&gt;There is also a new Saved Images app which allows one to browse the saved image, file them into categories, and download them to the local file system.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250810-212627.png&#34; width=&#34;600&#34; height=&#34;455&#34; alt=&#34;Auto-generated description: A webpage titled Blogging Tools displays five saved images including a pigeon, a crow, a lion, a tiger, and a yellow fish in a grid layout.&#34;&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250810-212648.png&#34; width=&#34;600&#34; height=&#34;455&#34; alt=&#34;Auto-generated description: A stuffed toy of a pigeon is placed on a table, visible within a web interface.&#34;&gt;
&lt;p&gt;Now those with keen eyes may notice the quality of the saved images in the screenshots above. That&amp;rsquo;s because the second change I made was to integrate calls to an image generator. There was no reason for doing this other than to try out the API, and I haven&amp;rsquo;t really used it for anything other than make test images. The API I&amp;rsquo;m using is Flux 1 provided by &lt;a href=&#34;https://together.ai/&#34;&gt;together.ai&lt;/a&gt;, but there&amp;rsquo;s also a partially-functional version of Gemini&amp;rsquo;s API too (the API is region locked and is not available in Germany, which is where Blogging Tools is running).&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250810-212658.png&#34; width=&#34;600&#34; height=&#34;387&#34; alt=&#34;Auto-generated description: A webpage titled Blogging Tools features an Image Generator section with a prompt about a cartoon person leaning out a window in comedic anger and buttons to Generate and Clear Completed.&#34;&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250810-212705.png&#34; width=&#34;600&#34; height=&#34;387&#34; alt=&#34;Auto-generated description: A user interface titled Blogging Tools with options for apps, notices, and settings, includes a request input box, a provider selection, and a generate button.&#34;&gt;
&lt;p&gt;Saved Images are build on the existing Files subsystem, which has been relegated to a new Settings section, the third change I&amp;rsquo;ve made to Blogging Tools. This allows one to manage files, see running jobs, and managed sessions from the system. That section may expand in time as I think of things I&amp;rsquo;d want to change without redeploying the app (most of the config is managed using environment variables).&lt;/p&gt;
&lt;p&gt;The last change was the addition of a new notification type. I&amp;rsquo;ve added &lt;a href=&#34;https://help.micro.blog/t/photo-collections/3366&#34;&gt;photo collections&lt;/a&gt; to my blog a few months ago, but I never remember to actually file photos into any of them. My workflow with making a photo post usually ends at the point I hit &amp;ldquo;publish&amp;rdquo;, and as far as I can tell, there&amp;rsquo;s no way to set the category of the image from the new post screen.&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;ve added a new notification type to help me with this. When the RSS feed of my blog is polled, and a new post with the &amp;ldquo;Photos&amp;rdquo; category is found, the first 4 images are extracted and presented as a notification asking if I want to file the photos into a photo category.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250810-212755.png&#34; width=&#34;600&#34; height=&#34;455&#34; alt=&#34;Auto-generated description: A blogging tools interface displays a notice with an image of a parked red car with its hood open next to a white car.&#34;&gt;
&lt;p&gt;This also worked for posts you add to the &amp;ldquo;Photos&amp;rdquo; category via the notification subsystem itself. Both act on the same RSS feed and setting the category of a post to &amp;ldquo;Photos&amp;rdquo; will clear the fetch history of that item so that it can run through the processors again.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve decided to use notifications for this, rather than add something to do this automatically, as I do want to go through the candidate photos first. But there are some limitations to this approach. For one, the notification system only allows one user selection per notification, making it impossible to add a photo to more than one collection. And to keep the number of notifications low, only the first 4 images are presented, with the rest being ignored. I have some ideas of how this could be improved in the future — probably by adding a separate app — but for now, I think I could live with taking corrective action manually.&lt;/p&gt;
&lt;p&gt;One last think about this new category processor: it&amp;rsquo;s activated by a change to the RSS feed, but it scans for images using the original post retrieved from Micro.blog&amp;rsquo;s micropub API. The reason for that I&amp;rsquo;ve enabled the CDN option, and the image URLs which use the CDN do not work with the collections API. It took me a while before I figured this out, mainly because I wasn&amp;rsquo;t using a CDN on my test blog and that worked perfectly. Something to keep in mind for anyone else using this API.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Godot Project — Level 4-2 And Level 2-3</title>
      <link>https://lmika.org/2025/07/26/devlog-godot-project-level-and.html</link>
      <pubDate>Sat, 26 Jul 2025 15:47:11 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/07/26/devlog-godot-project-level-and.html</guid>
      <description>&lt;p&gt;I can&amp;rsquo;t quite remember how far I got into building of level 4-2 when I wrote my last Dev Log entry. Based on how I described it, I think I was still working on the bones of it: a level with little more than the tile-map. I got a fair bit of the level built since then. It now features three major zones, each with a different camera lock. The central mechanic of a tile layer that can be raised by hitting switches has been built, along with it&amp;rsquo;s variations within each zone. Pickups and enemies have now been placed. And decoration. A lot of decoration.&lt;/p&gt;
&lt;p&gt;I wish I had a screenshot of how it looked when I wrote that last entry, but here&amp;rsquo;s a shot of how it looks now:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250726-154404.png&#34; width=&#34;600&#34; height=&#34;362&#34; alt=&#34;Auto-generated description: A pixelated platform game scene features a character navigating underground, with obstacles and items scattered around.&#34;&gt;
&lt;p&gt;I&amp;rsquo;m quite happy with how the mid-layer tiles add some interest to the aesthetics. But while thinking of the actual background to set the scene, I now feel that simply adding sky-coloured tiles is not enough. I may need to make a proper landscape backdrop, for no other reason than to give the level some place. Not entirely sure how I&amp;rsquo;m going to do that.&lt;/p&gt;
&lt;p&gt;Anyway, that&amp;rsquo;s for later. For now, I had another go at working on world 2. Level 2-1 is still barely started: I haven&amp;rsquo;t returned to it yet, and probably won&amp;rsquo;t until I have an idea of how to put that level together. I do have some ideas for level 2-2 and 2-3 and to ease myself in working on them, I started the week by simply building game elements, like a thirst mechanic (world 2 is a desert, as pre-ordained by &lt;a href=&#34;https://youtu.be/Gvr0qU1kPng?si=k9l60HCw9JMXkVyB&#34;&gt;Yahtzee Croshaw&lt;/a&gt;). More on those in the future I&amp;rsquo;m sure, but it was enough to get me to start work on level 2-3, which I&amp;rsquo;m thinking might be set in an underground pyramid with some windy maze elements (original, I know). Not too many windy maze elements, mind you: I generally don&amp;rsquo;t like playing maze levels. But I think a little would add some variety to what I&amp;rsquo;d imagine would be simply jumping puzzles until then. So far this has meant reverting to &amp;ldquo;playing the hits&amp;rdquo; as it were, by starting off with adding a simple lock-and-key hunt, albeit with gems (shout-out to my fellow Keen 4-6 fans). I do have a twist in mind which I hope to slowly introduce to the play as they progress through the level.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll just finish this update off with some screenshots of how level 2-3 looks now in it&amp;rsquo;s super early state.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250726-154418.png&#34; width=&#34;600&#34; height=&#34;362&#34; alt=&#34;Auto-generated description: A pixelated platformer video game features a character navigating through stone and brick structures with a goal indicator at the top left.&#34;&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250726-154429.png&#34; width=&#34;600&#34; height=&#34;362&#34; alt=&#34;Auto-generated description: A pixelated video game scene features a knight in a maze-like structure with brick walls and wooden crates, aiming to collect a purple gem.&#34;&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250726-154444.png&#34; width=&#34;600&#34; height=&#34;362&#34; alt=&#34;Auto-generated description: A pixel art video game scene shows a knight character on a platform surrounded by brick-like structures and a red arrow above.&#34;&gt;
&lt;p&gt;So that&amp;rsquo;s where things currently stand.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Blogging Tools — Category Fixer</title>
      <link>https://lmika.org/2025/07/20/devlog-blogging-tools-category-fixer.html</link>
      <pubDate>Sun, 20 Jul 2025 23:09:07 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/07/20/devlog-blogging-tools-category-fixer.html</guid>
      <description>&lt;p&gt;I was going to say that this one was pretty quick to implement, but thinking of it now, it took several hours all up, so maybe it wasn&amp;rsquo;t that quick. It&amp;rsquo;s a feature I&amp;rsquo;ve creatively called the &amp;ldquo;category fixer&amp;rdquo;. Yeah, the name&amp;rsquo;s not great, but it&amp;rsquo;s been a feature I&amp;rsquo;ve been wishing for a while, and it&amp;rsquo;s nice to see it added to Blogging Tools.&lt;/p&gt;
&lt;p&gt;I post blog posts of images which could either be a photo or a screenshot, along with the occasional meme. I have categories for all three, and when I create a new post with an image, I try to make sure the appropriate category is set. Only issue is that I usually forget, and most image posts remain without any category at all. Micro.blog does has a feature of automatically adding posts with image tags into a photos category, but I&amp;rsquo;ve turned that off as I don&amp;rsquo;t like the idea of screenshots going there.&lt;/p&gt;
&lt;p&gt;So I needed a way to fix this, and that&amp;rsquo;s what this new feature does. I&amp;rsquo;ve added a RSS feed parser which will poll my blog&amp;rsquo;s RSS feed for any new or updated posts. If there are any, the HTML content will get parsed to find at least one &lt;code&gt;img&lt;/code&gt;. If it does, an in-app notification will be added. I can then go in and triage these posts, selecting the category they should be sorted into.&lt;/p&gt;
&lt;div class=&#34;img-gallery&#34;&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250720-230148.png&#34;
     
        alt=&#34;Auto-generated description: A webpage titled Blogging Tools features notices for posts without categories, each containing expandable text and links for photos, screenshots, and memes.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  The in-app notifications, accessible from a new top-level menu item.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250720-230403.png&#34;
     
        alt=&#34;Auto-generated description: A modern building with red and black exterior illuminated at night, featuring large arched windows displaying the word COBURG.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Tapping the disclosure icon reveals the first image found, along with a link to view the entire post.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250720-230424.png&#34;
     
        alt=&#34;Auto-generated description: A magpie is perched on a wooden fence beside a grassy area with trees in the background.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Selecting an option will update the notice then and there (using HTMX). The notice isn&amp;#39;t cleared until the category change has been made, which does require a page refresh.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;/div&gt;
&lt;p&gt;About half the work was building out these in-app notifications. They&amp;rsquo;re all stored in a &lt;code&gt;sqlite3&lt;/code&gt; table and mainly consist of a notification type, brief message, and an extendable set of properties which are stored as a JSON object.&lt;/p&gt;
&lt;p&gt;For the feed parser, I&amp;rsquo;m using &lt;a href=&#34;https://github.com/mmcdole/gofeed/&#34;&gt;gofeed&lt;/a&gt;, the same library I&amp;rsquo;m using for reading podcast feeds. I can definitely recommend this library for parsing RSS feeds if you&amp;rsquo;re using Golang. There was a little bit I had to add around this library to detect when feed items have previously been read or updated. The way I&amp;rsquo;m detecting this is by recording the GUID, along with a SHA-1 hash of the title, description, and content in the database.  Parsing the feed HTML is done using &lt;a href=&#34;https://github.com/PuerkitoBio/goquery&#34;&gt;goquery&lt;/a&gt;, allowing me to easily select the first &lt;code&gt;img&lt;/code&gt; tag of a HTML fragment. For fetching and setting the categories, I&amp;rsquo;m using a home grown Micropub client.&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s &amp;ldquo;category fixer&amp;rdquo;. A lot of infrastructure was added for this one which could be useful in the future. I do have some other ideas that would follow this sort of pattern: a new post is picked up by the RSS reader, and an in-app notification is raised for me to action. I suspect this may becoming a bit of a pattern.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Godot Project — Some Feelings</title>
      <link>https://lmika.org/2025/07/12/devlog-godot-project-some-feelings.html</link>
      <pubDate>Sat, 12 Jul 2025 15:43:59 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/07/12/devlog-godot-project-some-feelings.html</guid>
      <description>&lt;p&gt;Spent a little more time working on my Godot game, mainly around level 4-2. Got around two-thirds of it done, including one of the more difficult thirds to build. It&amp;rsquo;s nice seeing the ideas I had for this level come together. It&amp;rsquo;s not exactly how I envisioned it, but truth be told, I didn&amp;rsquo;t have many ideas solidified before I started building the level. Just some vague ideas of how I wanted the level to look, then I went where my virtual paintbrush took me.&lt;/p&gt;
&lt;p&gt;Now, some feelings about this entire project. It&amp;rsquo;s hard spending time on this knowing that it&amp;rsquo;s not really going to amount to anything.  I&amp;rsquo;ve been watching a lot of game reviews recently, particularly from &lt;a href=&#34;https://www.youtube.com/playlist?list=PLUBKwq0XD0ueR3CXGUhGpsD1puLcYJPUp&#34;&gt;Yahtzee Croshaw&lt;/a&gt;, who is also a game developer. Seeing his work, along with the work he critiques, is a little discouraging. Here are accomplished game developers doing amazing work, making games that move emotions and look and sound amazing. And here&amp;rsquo;s this project, using assets from a Godot tutorial and is little more than another Mario-esc clone. And I know going in to this that this will be little more than a bit of a time-waster and an excuse to try Godot out: something to work on evenings and weekends.&lt;/p&gt;
&lt;p&gt;But there&amp;rsquo;s always this niggling feeling saying, &amp;ldquo;if you&amp;rsquo;re not expecting anything, why are you wasting your time on this? Why don&amp;rsquo;t you kill this now and focus on something bigger? &amp;lsquo;Make no small plans&amp;rsquo; and all that.&amp;rdquo; And once you found the answer surrounding the underlying question motivating you to start the project in the first place, if you know it won&amp;rsquo;t amount to nothing, why continue?&lt;/p&gt;
&lt;p&gt;And yeah, ultimately might be good advice. But I think I&amp;rsquo;ve abandoned too many projects in the past. I&amp;rsquo;d like to see something through, even if it&amp;rsquo;s something other than this. So maybe I will kill this. But not yet.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: UCL — More About The Set Operator</title>
      <link>https://lmika.org/2025/06/14/devlog-ucl-more-about-the.html</link>
      <pubDate>Sat, 14 Jun 2025 11:35:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/06/14/devlog-ucl-more-about-the.html</guid>
      <description>&lt;p&gt;I made a decision around the set operator in UCL this morning.&lt;/p&gt;
&lt;p&gt;When I added the set operator, I made it such that when setting variables, you had to include the leading dollar sign:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$a = 123
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The reason for this was that the set operator was also to be used for setting pseudo-variables, which had a different prefix character.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;@ans = &amp;#34;this&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I needed the user to include the &lt;code&gt;@&lt;/code&gt; prefix to distinguish the two, and since one variable type required a prefix, it made sense to require it for the other.&lt;/p&gt;
&lt;p&gt;I’ve been trying this for a while, and I’ve deceided I didn’t like it. It felt strange to me. It shouldn&amp;rsquo;t, really, as it&amp;rsquo;s similar to how variable assignments work in Go’s templating language, which I consider an inspiration for UCL. On the other hand, TCL and Bash scripts, which are also inspirations, require the variable name to be written without the leading dollar sign in assignments. Heck, UCL itself still had constructs where referencing a name for a variable is done so without a leading dollar sign, such as block inputs. And I had no interest in changing that:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;proc foo { |x|
    echo $x
}

for [1 2 3] { |v| foo $v }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So I made the decision to remove the need for the dollar sign prefix in the set operator. Now, when setting a variable, only the variable name can be used:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;msg = &amp;#34;Hello&amp;#34;
echo $msg
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In fact, if one were to use the leading dollar sign, the program will fail with an error.&lt;/p&gt;
&lt;p&gt;This does have some tradeoffs. The first is that I still need to use the &lt;code&gt;@&lt;/code&gt; prefix for setting pseudo variables, and this change will violate the likeness of how the two look in assignments:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;@ans = 123
bla = 234
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The second is that this breaks the likeness of how a sub-index looks  when reading it, verses how it looks when it&amp;rsquo;s being modified:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;a = [1 2 3]
a.(1) = 4
$a
--&amp;gt; [1 4 3]
$a.(1)
--&amp;gt; 4
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(One could argue that the dollar sign prefix makes sense here as the evaluator is dereferencing the list in order to modify the specific index. That&amp;rsquo;s a good argument, but it feels a little bit too esoteric to justify the confusion it would add).&lt;/p&gt;
&lt;p&gt;This sucks, but I think they&amp;rsquo;re tradeoffs worth making. UCL is more of a command language than a templating language, so when asked to imagine similar languages, I like to think one will respond with TCL or shell-scripts, rather than Go templates.&lt;/p&gt;
&lt;p&gt;And honestly, I think I just prefer it this way. I feel that I&amp;rsquo;m more likely to set regular variables rather than pseudo-variables and indicies. So why not go with the approach that seems nicer if you&amp;rsquo;re likely to encounter more often.&lt;/p&gt;
&lt;p&gt;Finally, I did try support both prefixed and non-prefixed variables in the set operator, but this just felt like I was shying away from making a decision. So it wasn&amp;rsquo;t long before I scrapped that.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>That Which Didn&#39;t Make the Cut: a Hugo CMS</title>
      <link>https://lmika.org/2025/06/09/that-which-didnt-make-the.html</link>
      <pubDate>Mon, 09 Jun 2025 16:32:49 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/06/09/that-which-didnt-make-the.html</guid>
      <description>&lt;p&gt;You&amp;rsquo;ve probably noticed&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; that I&amp;rsquo;ve stopped posting links to Open Bookmarks, and have started posting them here again. The main reason for this is that I&amp;rsquo;ve abandoned work on the CMS I was working on that powered that bookmarking site. Yes, yes, I know: another one. Open Bookmarks was basically a static Hugo site, hosted on Netlify. But being someone that wanted to make it easy for me to post new links without having to do a Git checkout, or fiddle around YAML front-matter, I thought of building a simple web-service for this.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t want to say too much about it, other than I managed to get the post functionality working. Creating a new link post would involve fetching the page, and pre-populating the link and optional via link with the fetched page title. I&amp;rsquo;d just finish the post with some quotes or quips, then click Post. That&amp;rsquo;ll save the post in a local database, write it to a staged Hugo site, run Hugo to generate the static site, and upload it to Netlify. There was nothing special about link posts per se: they were just a specialisation of the regular posting feature — a template if you will — which would work the same way, just without the pre-fills.&lt;/p&gt;
&lt;p&gt;The other thing I added was support for adding arbitrary pages. This was a dramatic simplification to what is possible in Hugo, in that only one &lt;a href=&#34;https://gohugo.io/content-management/page-bundles/&#34;&gt;page bundle&lt;/a&gt; was supported. And it was pretty compromised: you had to set the page title to &amp;ldquo;Index&amp;rdquo; to modify the home page. But this was enough for that, plus some additional pages at the top level.&lt;/p&gt;
&lt;p&gt;One other feature was that you can &amp;ldquo;preview&amp;rdquo; the site if you didn&amp;rsquo;t have a Netlify site setup and you wanted to see the Hugo site within the app itself. I had an idea of adding support for staging posts prior to publishing them to Netlify, so that one could look at them. But this never got built.&lt;/p&gt;
&lt;p&gt;Finally there were some options for configuring the site properties. Uploads were never implemented.&lt;/p&gt;
&lt;p&gt;Here are some screenshots, which isn&amp;rsquo;t much other than evidence that I was prioritising the backend over the user experience:&lt;/p&gt;
&lt;div class=&#34;img-gallery&#34;&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250609-155850.png&#34;
     
        alt=&#34;Auto-generated description: A webpage displays a list titled Sites with links to good-socials, open-bookmarks, juniper-course-notes, and a Create Site button.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Once authenticated, you&amp;#39;re presented with this: the list of your sites.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250609-155900.png&#34;
     
        alt=&#34;Auto-generated description: A website interface displays a content management system with sections for posts, pages, and various entries related to JavaScript animation, Twitter viewing, and a Kanban board.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Clicking on a site opens up the list of posts. Clicking &amp;#39;New Post&amp;#39; will take you directly to the editor.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250609-155930.png&#34;
     
        alt=&#34;Auto-generated description: A simplified webpage interface for Hugo CMS displays options like Posts, Pages, Uploads, and Settings along with fields for a Link URL and Via URL.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Clicking &amp;#39;New Link Post&amp;#39; will bring up this form, where you can specifying the post link and &amp;#39;via&amp;#39; link.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250609-155909.png&#34;
     
        alt=&#34;Auto-generated description: A webpage showcasing the Hugo CMS with a draft post discussing the GSAP JavaScript animation library.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Clicking through will preload the post title and body with the page title and URL. If a via is set, the page title and URL of the via link will also be included.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250609-155917.png&#34;
     
        alt=&#34;Auto-generated description: A webpage from Hugo CMS with sections for posts, pages, uploads, settings, and a description of Open Bookmarks.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Also supported were the ability to add standalone pages, accessible by clicking the &amp;#39;Pages&amp;#39; section in the side-bar. Most created sites come with an index page (which needed to have the title &amp;#39;index&amp;#39;).
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250609-155923.png&#34;
     
        alt=&#34;Auto-generated description: A Hugo CMS interface is displayed, showing options to edit the site title, theme, URL, and notify site ID, with links for posts, pages, uploads, and settings.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  The &amp;#39;Settings&amp;#39; nav item bought up the site settings, including the site tile, theme, and Netlify site ID.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;/div&gt;
&lt;p&gt;So, why was this killed? Well, apart from the fact that it seemed like remaking prior art — I&amp;rsquo;m sure there are plenty of Hugo CMSes out there — it seemed strange having two systems that relate to blogging. Recognising this, I decided to add this functionality to Blogging Tools:&lt;/p&gt;
&lt;div class=&#34;img-gallery&#34;&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250609-155937.png&#34;
     
        alt=&#34;Auto-generated description: A webpage titled Blogging Tools includes a saved post notification, a button to create a new post, and a linked article titled The Who Cares Era.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  The &amp;#39;Posts&amp;#39; app in Blogging Tools. Unlike Hugo CMS, this is just a databased-backed list of posts.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250609-161952.png&#34;
     
        alt=&#34;Auto-generated description: A webpage titled Blogging Tools offers options for Apps, Files, and Jobs, with a section titled New From Template and the author&amp;#39;s name at the bottom.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  The goal with this app is to easily create posts from templates, so there&amp;#39;s no way to create a blank post. Clicking New will require one to select a template to use. There are currently only two: creating a post with a Mastodon embed, and creating a link post.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250609-155944.png&#34;
     
        alt=&#34;Auto-generated description: A webpage titled Blogging Tools features a form for linking a post, including fields for Post URL and Via URL, with options for a leading emoji and a Create button.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Once the template is selected, the steps are the same: specify the post link, optional via link, and link emoji.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250609-155952.png&#34;
     
        alt=&#34;Auto-generated description: A text editor screenshot features a blog post titled Retirement Day by Brent Simmons, reflecting on his career and retirement.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  These get pre-filled as before. It&amp;#39;s just a matter of adding any commentary after that. Clicking &amp;#39;Save&amp;#39; will only save the post locally: one needs to click &amp;#39;Publish&amp;#39; in the posts list to send it to the Micropub service.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;/div&gt;
&lt;p&gt;This can&amp;rsquo;t really be described as CMS. It&amp;rsquo;s more of a place to create posts from a template which can then be published via a Micropub endpoint. But it does the job I need, which is creating link posts with the links pre-filled.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;Or not. I mean, I&amp;rsquo;m not expecting you to notice. You&amp;rsquo;ve got lives of your own, after all.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/06/05/had-a-reason-to-write.html</link>
      <pubDate>Thu, 05 Jun 2025 21:35:04 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/06/05/had-a-reason-to-write.html</guid>
      <description>&lt;p&gt;Had a reason to write a journal entry today, which meant I had a reason to work on the journaling app. Biggest change was moving the entry list to a separate page and supersizing the text-area to allow for larger entries. Good thing too: today&amp;rsquo;s was going to need all the space it could get.&lt;/p&gt;
&lt;div class=&#34;img-gallery&#34;&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250605-212831.png&#34;
     
        alt=&#34;Auto-generated description: A digital journal entry from May 2025 details a technical issue involving CMSS Webview, its history, and a request for source code location.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Before
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250605-212823.png&#34;
     
        alt=&#34;Auto-generated description: A computer screen displays a journal entry about a software issue and an attempted replacement that failed.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  After
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/06/04/working-on-that-godot-game.html</link>
      <pubDate>Wed, 04 Jun 2025 22:26:41 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/06/04/working-on-that-godot-game.html</guid>
      <description>&lt;p&gt;Working on that Godot game again, mainly coming up with mechanics for a new level 2. This is what I&amp;rsquo;ve got so far: a mine tethered to a balloon. Their idle state is just bobbing up and down, but I am planning a variant which will drop their payload and fly away when the player is nearby.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/cleanshot-2025-06-04-at-22.18.10.gif&#34; width=&#34;480&#34; height=&#34;364&#34; alt=&#34;Auto-generated description: Three red balloons are tethered to spiked balls, hovering above a row of green and brown blocks.&#34;  class=&#34;block-center&#34;&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Dynamo-Browse Now Scanning For UCL Extensions</title>
      <link>https://lmika.org/2025/05/25/devlog-dynamobrowse-now-scanning-for.html</link>
      <pubDate>Sun, 25 May 2025 13:55:49 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/05/25/devlog-dynamobrowse-now-scanning-for.html</guid>
      <description>&lt;p&gt;Almost finished integrating UCL with Dynamo-Browse. As stated earlier, the goal is to use UCL for both the command and scripting language. Today, the goal was closer, with a development version of Dynamo-Browse no longer using old extension scripting language, and now scanning and evaluating UCL-based extensions on launch time.&lt;/p&gt;
&lt;p&gt;This required some changes in how UCL evaluated script files. There&amp;rsquo;s a new &lt;code&gt;ucl.WithSubEnv()&lt;/code&gt; option that can be passed into  &lt;code&gt;inst.Eval()&lt;/code&gt; to instruct the evaluator to fork the environment prior to evaluating the script:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;os&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ReadFile&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;my-file.ucl&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;inst&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ucl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;inst&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Eval&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;bytes&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewReader&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;), &lt;span style=&#34;color:#a6e22e&#34;&gt;ucl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithSubEnv&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Under the hood, this forks the top-level environment frame and marks it as the frame to store all procs and global variables for the evaluated file. This effectively makes it a separate namespace, separating it from all other scripts and preventing cross-contamination when scripts share the same symbol.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m hoping that this will eventually be the basis for UCL modules, but for now, this is use within Dynamo-Browse to isolate each UCL extension. The downside of this is that it&amp;rsquo;s no longer possible to add new commands simply using the &lt;code&gt;proc&lt;/code&gt; keyword. I think this may end up being a good thing though, as extensions will probably not be interested in exposing internal functions defined just for their own use.&lt;/p&gt;
&lt;p&gt;To compensate for this, there is now a new command — &lt;code&gt;ui:command&lt;/code&gt; — which allows the script author to expose a function as a command. In fact, what it does is save the command in the root environment frame, effectively making it work the old way &lt;code&gt;proc&lt;/code&gt; did, and acting like a poor-mans extension export.&lt;/p&gt;
&lt;p&gt;Now with all the extension loading in place, it&amp;rsquo;s time for the inaugural UCL extension. Here it is:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;# Gets the partition key of the currently selected row, and adds it
# to the pasteboard. Bound to the &amp;#39;U&amp;#39; key.

ui:command copy-pk {
    # Get the partition key value from the currently selected item
    $pkKey = @table.Keys.PK
    $pkValue = @item.($pkKey)

    # Place it in the pasteboard
    pb:put $pkValue

    echo &amp;#34;$pkKey copied to clipboard&amp;#34;
}

ui:bind &amp;#34;U&amp;#34; { copy-pk }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;All the features are not quite implemented yet, but I think it&amp;rsquo;s good enough to start using it as part of my normal workflow. That&amp;rsquo;s usually the best way to find the rough edges.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/05/18/all-the-recent-changes-to.html</link>
      <pubDate>Sun, 18 May 2025 14:07:39 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/05/18/all-the-recent-changes-to.html</guid>
      <description>&lt;p&gt;All the recent changes to UCL is in service of unifying the scripting within Dynamo Browse. Right now there are two scripting languages: one for the commands entered after pressing  &lt;code&gt;:&lt;/code&gt;, and one for extensions. I want to replace both of them with UCL, which will power both interactive commands, and extensions.&lt;/p&gt;
&lt;p&gt;Most of the commands used within the in-app REPL loop has been implemented in UCL. I&amp;rsquo;m now in the process of building out the UCL extension support, start with functions for working with result sets, and pseudo-variables for modifying elements of the UI.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a demo of what I&amp;rsquo;ve got so far. This shows the user&amp;rsquo;s ability to control the current result-set, and the selected item programatically. Even after these early changes, I&amp;rsquo;m already seeing much better support for doing such things than what was there before.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://media.lmika.org/videos/2025/dynamo-browse-pvals-demo.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: UCL — Assignment</title>
      <link>https://lmika.org/2025/05/18/devlog-ucl-assignment.html</link>
      <pubDate>Sun, 18 May 2025 09:35:57 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/05/18/devlog-ucl-assignment.html</guid>
      <description>&lt;p&gt;Toying with changing how assignment works in UCL. Up to now, assigning a variable to a value involved calling the &lt;code&gt;set&lt;/code&gt; command, which took a variable name, and the value to assign:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;set a &amp;#34;hello&amp;#34;
$a
--&amp;gt; &amp;#34;hello&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This has been fine, but I&amp;rsquo;m now running into few limitations with this approach. The first is that it doesn’t support setting subscript values within lists or values. Because &lt;code&gt;set&lt;/code&gt; is just a regular command, the parser will evaluate any dot or sub-pipe expressions prior to invoking &lt;code&gt;set&lt;/code&gt;. One way around this is to quoted the variable to assign as a string:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;set list [1 2 3]
set &amp;#34;$list.(1)&amp;#34; 4
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But this looks ugly, and will involve another pass of the parser every time &lt;code&gt;set&lt;/code&gt; is called.&lt;/p&gt;
&lt;p&gt;Another approach is converting &lt;code&gt;set&lt;/code&gt; to a macro, which receives the arguments as the parse tree. This gives &lt;code&gt;set&lt;/code&gt; more control over how to interpret the arguments. But it doesn&amp;rsquo;t resolve the second issue, which is an inconsistency introduced with a new feature I&amp;rsquo;m planning.&lt;/p&gt;
&lt;p&gt;I want to add the notion of pseudo variables, which act like regular variables except that getting or setting them will invoke some hander logic from the embedding system, rather than set a value in memory. Think of them similar to how &lt;code&gt;document&lt;/code&gt; and &lt;code&gt;window&lt;/code&gt; work in a browser&amp;rsquo;s JavaScript runtime. My plan for them is to embed this Dynamo Browse to allow the user to access or modify the result-set or selected item, which will result in the UI changing. I&amp;rsquo;m sure I&amp;rsquo;ll have more to say about that in the future.&lt;/p&gt;
&lt;p&gt;To distinguish them from normal variables, they&amp;rsquo;ll have an at-sign instead of a dollar sign:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;@value
--&amp;gt; something
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But as far as the user is concerned, they should act just like normal variables. And just like normal variables, they should be modifiable using the &lt;code&gt;set&lt;/code&gt; command. I don&amp;rsquo;t want the variable and pseudo-variable to share the same namespace (since they look different, there&amp;rsquo;s no reason for them to share one), so I needed a way to distinguish  between setting a pseudo-variable from a regular variable.&lt;/p&gt;
&lt;p&gt;This first means they needed to be quoted but I hacked a quick version of &lt;code&gt;set&lt;/code&gt; on this feature branch to
support setting them:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;set &amp;#34;@value&amp;#34; &amp;#34;new value&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But compare this with the first example. Notice that when we were setting &lt;code&gt;a&lt;/code&gt;, we didn&amp;rsquo;t need to include the dollar sign. Here we have a case where one sort of variable requires the prefix symbol, and the other doesn&amp;rsquo;t. This form of inconsistency was not appealing to me.&lt;/p&gt;
&lt;p&gt;Since the various modes of assignments has outgrown the ability of one command to do it all, I’ve decided to remove &lt;code&gt;set&lt;/code&gt; and add basic assignments to the language. These look like typical assignments you see in most other languages:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$a = &amp;#34;hello&amp;#34;
$a
--&amp;gt; &amp;#34;hello&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But they’ll be able to fix the issues from using &lt;code&gt;set&lt;/code&gt;, such as assign the value of subsets and showing consistent representation of what you&amp;rsquo;re actually trying to modify:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;@value = &amp;#34;new value&amp;#34;

$list = [1 2 3]
$list.(1) = 4
$list
--&amp;gt; [1 4 3]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It also leads to some improvements of the common case. One small issue with using &lt;code&gt;set&lt;/code&gt; is that it always required one to wrap the result of a sub-expression in parenthesis.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;set h (strs:to-upper &amp;#34;Hello&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But since this is a grammar change, these parenthesis are no longer necessary.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$h = strs:to-upper &amp;#34;Hello&amp;#34;

$h = &amp;#34;Hello&amp;#34; | strs:to-upper
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There may also be room for different forms of assignment, such as ensuring the value you&amp;rsquo;re trying to set is not nil. There was a variant of set, called &lt;code&gt;set!&lt;/code&gt;, which threw an error when attempting to assign a variable to nil. This could be expressed as a different assignment form:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$h = &amp;#34;this is fine&amp;#34;

$h =! ()
--&amp;gt; error: trying to set $h to a nil value
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I will acknowledge that doing this will mean loosing out on some theoretical benefits that came from using &lt;code&gt;set&lt;/code&gt;.  For one thing, it will no longer be possible to indirectly set a value. You can&amp;rsquo;t, for example, do this:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;set b &amp;#34;a&amp;#34;
set $b &amp;#34;hello&amp;#34;
$a
--&amp;gt; &amp;#34;hello&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;ve never really needed to do this, but I could see this being potentially useful. One way to support this might be to &lt;a href=&#34;https://lua.org/manual/5.4/manual.html#2.2&#34;&gt;do something similar to what Lua does&lt;/a&gt;, and expose the environment as a hash. Pseudo-variables could be useful here:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$b = &amp;#34;a&amp;#34;
@_G.($b) = &amp;#34;hello&amp;#34;
$a
--&amp;gt; &amp;#34;hello&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;ll hold off from adding something like this until I absolutely need to.&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s the current idea. I spent around an hour on this so far, just trying it out and seeing how it feels, and I think it&amp;rsquo;s got promise. I&amp;rsquo;ll keep it on the feature branch for now, but I suspect this will eventually become the new way to do assignment in UCL going forward.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Blogging Tools — Finished Podcast Clips</title>
      <link>https://lmika.org/2025/05/15/devlog-blogging-tools-finished-podcast.html</link>
      <pubDate>Thu, 15 May 2025 23:18:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/05/15/devlog-blogging-tools-finished-podcast.html</guid>
      <description>&lt;p&gt;Well, it&amp;rsquo;s done. I&amp;rsquo;ve finally finished adding the podcast clip to Blogging Tools. And I won&amp;rsquo;t lie to you, it took longer than expected, even after enabling some of the AI features my IDE came with. Along with the complexity that came from implementing this feature, that touched on most of the key subsystems of Blogging Tools, the biggest complexity came from designing how the clip creation flow should work. Blogging Tools is at a disadvantage over clipping features in podcast players in that it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Doesn&amp;rsquo;t know what feeds you&amp;rsquo;ve subscribed to,&lt;/li&gt;
&lt;li&gt;Doesn&amp;rsquo;t know what episode you&amp;rsquo;re listening to, and&lt;/li&gt;
&lt;li&gt;Doesn&amp;rsquo;t know where in the episode you are.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Blogging Tools needs to know this stuff for creating a clip, so there was no alternative to having the user input this when they&amp;rsquo;re creating the clip. I tried to streamline this in a few ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Feeds had to be predefined:&lt;/strong&gt; While it&amp;rsquo;s possible to create a clip from an arbitrary feed, it&amp;rsquo;s a bit involved, and the path of least resistence is to set up the feeds you want to clip ahead of time. This works for me as I only have a handful of feeds I tend to make clips from.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prioritise recent episodes:&lt;/strong&gt; The clips I tend to make come from podcasts that touch on current events, so any episode listings should prioritise the more recent ones. The episode list is in the same order as the feed, which is not strictly the same, but fortunately the shows I subscribe to list episodes in reverse chronological order.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy course and fine positioning of clips:&lt;/strong&gt; This means going straight to a particular point in the episode by entering the timestamp. This is mainly to keep the implementation simple, but I&amp;rsquo;ve always found trying to position the clip range on a visual representation of a waveform frustrating. It was always such a pain trying to make fine adjustments to where the clip should end. So I just made this simple and allow you to advance the start time and duration by single second increments by tapping a button.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rather than describe the whole flow in length, or prepare a set of screenshots, I&amp;rsquo;ve decided to record a video of how this all fits together.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://media.lmika.org/videos/2025/podcast-clip-feature.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;The rest was pretty straightforward: the videos are made using &lt;code&gt;ffmpeg&lt;/code&gt; and publishing it on Micro.blog involved the Micropub API. There were some small frills added to the UI using both HTMX and Stimulus.JS so that job status updates could be pushed via web-sockets. They weren&amp;rsquo;t necessary, as it&amp;rsquo;s just me using this, but this project is becoming a bit of a testbed for stretching my skills a little, so I think small frills like this helped a bit.&lt;/p&gt;
&lt;p&gt;I haven&amp;rsquo;t made a clip for this yet or tested out how this will feel on a phone, but I&amp;rsquo;m guessing both will come in time. I also learnt some interesting tidbits, such that the source audio of an &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; tag requires a HTTP response that supports &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests&#34;&gt;range requests&lt;/a&gt;. Seeking won&amp;rsquo;t work otherwise: trying to change the time position will just seek the audio back to the start.&lt;/p&gt;
&lt;p&gt;Anyway, good to see this in prod and moving onto something else. I&amp;rsquo;ve getting excited thinking about the next thing I want to work on. No spoilers now, but it features both Dynamo Browse and UCL.&lt;/p&gt;
&lt;p&gt;Finally, I just want to make the point that this would not be possible without the open RSS podcasting ecosystem. If &lt;a href=&#34;https://justinjackson.ca/youtube-kill-podcasting&#34;&gt;I was listening to podcasts in YouTube&lt;/a&gt;, forget it: I wouldn&amp;rsquo;t have been able to build something like this. I know for myself that I&amp;rsquo;ll continue to listen to RSS podcasts for as long as podcasters continue to publish them. Long may it be so.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Blogging Tools — Ideas For Stills For A Podcast Clips Feature</title>
      <link>https://lmika.org/2025/05/07/devlog-blogging-tools-podcast-clips.html</link>
      <pubDate>Wed, 07 May 2025 23:18:48 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/05/07/devlog-blogging-tools-podcast-clips.html</guid>
      <description>&lt;p&gt;I recently discovered that Pocketcasts for Android have changed their clip feature. It still exists, but instead of producing a video which you could share on the socials, it produces a link to play the clip from the Pocketcasts web player. Understandable to some degree: it always took a little bit of time to make these videos. But hardly a suitable solution for sharing clips of private podcasts: one could just listen to the entire episode from the site. Not to mention relying on a dependent service for as long as those links (or the original podcast) is around.&lt;/p&gt;
&lt;p&gt;So… um, yeah, I&amp;rsquo;m wondering if I could building something for myself that could replicate this.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m thinking of another module for Blogging Tools. I was already using this tool to &lt;a href=&#34;https://lmika.org/2024/11/11/oof-finally-fixed.html&#34;&gt;crop the clip videos&lt;/a&gt; that came from Pocketcasts so it was already in my workflow. It also has &lt;code&gt;ffmpeg&lt;/code&gt; bundled in the deployable artefact, meaning that I could use to produce video. Nothing fancy: I&amp;rsquo;m thinking of a still showing the show title, episode title, and artwork, with the audio track playing. I pretty confident that &lt;code&gt;ffmpeg&lt;/code&gt; can handle such tasks.&lt;/p&gt;
&lt;p&gt;I decided to start with the fun part: making the stills. I started with using &lt;a href=&#34;https://github.com/llgcode/draw2d&#34;&gt;Draw2D&lt;/a&gt; to provide a very simple frame where I could place the artwork and render the text. I just started with primary colours so I could get the layout looking good:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/podcast-background-1.png&#34; width=&#34;600&#34; height=&#34;338&#34; class=&#34;block-center&#34; alt=&#34;Auto-generated description: A date, episode title, and show name are displayed alongside an image of ocean waves against rocks in a colorful border.&#34;&gt;
&lt;p&gt;I&amp;rsquo;m using Roboto Semi-bold for the title font, and Oswald Regular for the date. I do like the look of Oswald, the narrower style contrasts nicely with the neutral Roboto. Draw2D provides methods for measuring text sizes, which I&amp;rsquo;m using to power the text wrapping layout algorithm (it&amp;rsquo;s pretty dumb. It basically adds words to a line until it can&amp;rsquo;t fit the available space)&lt;/p&gt;
&lt;p&gt;The layout I got nailed down yesterday evening. This evening I focused on colour.&lt;/p&gt;
&lt;p&gt;I want the frame to be interesting and close to the prominent colours that come from the artwork. I found &lt;a href=&#34;https://github.com/EdlinOrg/prominentcolor&#34;&gt;this library&lt;/a&gt; which returns the dominant colours of an image using K-means clustering. I&amp;rsquo;ll be honest: I haven&amp;rsquo;t looked at how this actually works. But I tried the library out with some random artwork from &lt;a href=&#34;https://picsum.photos&#34;&gt;Lorem Picsum&lt;/a&gt;, and I was quite happy with the colours it was returning. After adding &lt;a href=&#34;https://github.com/teacat/noire&#34;&gt;this library&lt;/a&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; to calculate the contract for the text colour, plus a slight shadow, and the stills started looking pretty good:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250507-213441.png&#34; width=&#34;600&#34; height=&#34;517&#34; alt=&#34;Auto-generated description: Six rectangular cards each feature a different background image with the date 14 April 2020, text A pretty long episode title, and My test show.&#34;&gt;
&lt;p&gt;I then tried some real podcast artwork, starting with &lt;a href=&#34;https://atp.fm&#34;&gt;ATP&lt;/a&gt;. And that&amp;rsquo;s where things started going off the rails a little:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250507-224100.png&#34; width=&#34;600&#34; height=&#34;342&#34; alt=&#34;Auto-generated description: Four color variations of a promotional card design featuring a logo with rainbow stripes, a date of 14 April 2020, and text stating A pretty long episode title and My test show.&#34;&gt;
&lt;p&gt;The library returns the colours in order of frequency, and I was using the first colour as the border and the second as the card background. But I&amp;rsquo;m guessing since the ATP logo has so few actual colour, the K-means algorithm was finding those of equal prominence and returning them in a random order. Since the first and second are of equal prominence, the results were a little garish and completely random.&lt;/p&gt;
&lt;p&gt;To reduce the effects of this, I finished the evening by trying a variation where the card background was simply a shade of the border. That still produced random results, but at least the colour choices were a little more harmonious:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250507-224112.png&#34; width=&#34;600&#34; height=&#34;342&#34; alt=&#34;Auto-generated description: A series of four visually distinct cards display a logo, date, episode title, and show subtitle, each set against different colored backgrounds.&#34;&gt;
&lt;p&gt;I&amp;rsquo;m not sure what I want to do here. I&amp;rsquo;ll need to explore the library a little, just to see whether it&amp;rsquo;s possible to reduce the amount of randomness. Might be that I go with the shaded approach and just keep it random: having some variety could make things interesting.&lt;/p&gt;
&lt;p&gt;Of course, I&amp;rsquo;m still doing the easy and fun part. How the UI for making the clip will look is going to be a challenge. More on that in the future if I decide to keep working on this. And if not, at least I&amp;rsquo;ve got these nice looking stills.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;The annoying thing about this library is that it doesn&amp;rsquo;t use Go&amp;rsquo;s standard &lt;code&gt;Color&lt;/code&gt; type, nor does it describe the limits of each component. So for anyone using this library: the range for R, G, and B go from 0 to 255, and A goes from 0 to 1.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Dialogues</title>
      <link>https://lmika.org/2025/05/04/added-something-fun-and-potentially.html</link>
      <pubDate>Sun, 04 May 2025 10:45:36 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/05/04/added-something-fun-and-potentially.html</guid>
      <description>&lt;p&gt;Added something fun, and potentially useless, to this blog: styling for dialogues:&lt;/p&gt;
&lt;blockquote class=&#34;dialogue&#34;&gt;
&lt;style&gt;
@scope {
  :scope {
    --dialogue-m1-color: #4C6C8C;
    --dialogue-m1-background: no-repeat center / 105% url(https://avatars.micro.blog/avatars/2024/15/55331.jpg);
    --dialogue-m2-color: #625309;
    --dialogue-m2-background: white no-repeat center / 75% url(https://lmika.org/uploads/2025/rubber-duck.jpg);
  }
}
&lt;/style&gt;
&lt;p class=&#34;member member-m1&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;L:&lt;/b&gt; Hello?&lt;/p&gt;
&lt;p class=&#34;member member-m2&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;🦆:&lt;/b&gt; Oh, hello. What&#39;s up?&lt;/p&gt;
&lt;p class=&#34;member member-m1&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;L:&lt;/b&gt; Just showing how dialogues look on this site.&lt;/p&gt;
&lt;p class=&#34;member member-m2&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;🦆:&lt;/b&gt; Oh, wow. Super nice.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I don&amp;rsquo;t &lt;a href=&#34;https://en.wikipedia.org/wiki/Rubber_duck_debugging&#34;&gt;rubber-duck&lt;/a&gt; often, but I have a few times in the past, and I&amp;rsquo;ve published those &lt;a href=&#34;https://lmika.org/2024/09/10/rubberducking-of-config.html&#34;&gt;as blog posts&lt;/a&gt; &lt;a href=&#34;https://lmika.org/2024/02/09/rubberducking-on-context.html&#34;&gt;on this site&lt;/a&gt;. And it&amp;rsquo;s been fun. Not only has it been helpful to work through the problem this way, there&amp;rsquo;s an element of creativity involved in the whole process: imagining two characters working through a problem like this. The exchange is purely fictional, but the benefits are very real.&lt;/p&gt;
&lt;p&gt;And part of me is wondering whether to do more of this style of writing. Not only when I&amp;rsquo;m facing a programming dilemma, but when I&amp;rsquo;m feeling… I don&amp;rsquo;t know, &amp;ldquo;whimsical.&amp;rdquo;  I&amp;rsquo;ve seen a few other sites having this style of dialogues, both real and imagined; and I&amp;rsquo;ve recently read posts from others &lt;a href=&#34;https://danilafe.com/blog/blog_microfeatures/#dialogues&#34;&gt;wishing more sites had them&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So all these justifications — along with boredom, a desire to work on something novel, and ship something small — accumulated into this silly little change.&lt;/p&gt;
&lt;p&gt;The dialogues themselves are all HTML. If you were to view source, you&amp;rsquo;ll see that they&amp;rsquo;re essentially styled blockquotes. It was important to me to make sure that came through correctly on RSS feeds, with all the styling stripped away. In a browser, they&amp;rsquo;re meant to resemble an imaginary chat experience. I&amp;rsquo;m hoping this is versatile enough: the one concern I have is that this style of interaction seems to encourage short exchanges between the participants, so I may adjust the styling a little in the future. But I&amp;rsquo;ll start with this and see how it goes. If you&amp;rsquo;re curious as to how the CSS looks, &lt;a href=&#34;https://github.com/lmika/lmika.org-extra-assets/blob/main/static/styles/dialogue.css&#34;&gt;you can find it here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Of course, most people would stop here; but because I&amp;rsquo;m me, and will take any opportunity to spend several hours on something if I think it would save me a few minutes, I added a module to Blogging Tools to render dialogs from a mini-language. This uses a small parser written in &lt;a href=&#34;https://github.com/alecthomas/participle&#34;&gt;participal&lt;/a&gt; that will produce the HTML for a dialog that I can copy and paste directly into the Micro.blog editor. There&amp;rsquo;s also a tiny bit of AI that would automatically generate a name for the interaction if I were to leave it blank. For that, I&amp;rsquo;m using &amp;ldquo;Meta Llama 3.1.8B Instruct Turbo&amp;rdquo; made available via &lt;a href=&#34;https://together.ai&#34;&gt;together.ai&lt;/a&gt; (the name is not published, it&amp;rsquo;s just used for listing the dialogue).&lt;/p&gt;
&lt;div class=&#34;img-gallery&#34;&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/dialogue-1.png&#34;
     
      /&gt;

  &lt;figcaption&gt;
  
  The new Dialogues module.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/dialogue-2.png&#34;
     
      /&gt;

  &lt;figcaption&gt;
  
  The dialogue editor. This is essentially a plain-text post editor.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/dialogue-3.png&#34;
     
      /&gt;

  &lt;figcaption&gt;
  
  An example of the mini-language used to styling the dialogue. The real avatar properties are slightly different to what appears here.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/dialogue-4.png&#34;
     
      /&gt;

  &lt;figcaption&gt;
  
  Click Render to generate the HTML. Copy and paste at will.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;/div&gt;
&lt;blockquote class=&#34;dialogue&#34;&gt;
&lt;style&gt;
@scope {
  :scope {
    --dialogue-m1-color: #4C6C8C;
    --dialogue-m1-background: no-repeat center / 105% url(https://avatars.micro.blog/avatars/2024/15/55331.jpg);
    --dialogue-m2-color: #625309;
    --dialogue-m2-background: white no-repeat center / 75% url(https://lmika.org/uploads/2025/rubber-duck.jpg);
  }
}
&lt;/style&gt;
&lt;p class=&#34;member member-m2&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;🦆:&lt;/b&gt; Great work.&lt;/p&gt;
&lt;p class=&#34;member member-m2&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;🦆:&lt;/b&gt; So are you going to use this often?&lt;/p&gt;
&lt;p class=&#34;member member-m1&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;L:&lt;/b&gt; Well, probably not. I think it&#39;s one of those features where most of the fun is building it. Then once you have it, you&#39;re left wondering what to do with it.&lt;/p&gt;
&lt;p class=&#34;member member-m2&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;🦆:&lt;/b&gt; Oh, okay.&lt;/p&gt;
&lt;p class=&#34;member member-m1&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;L:&lt;/b&gt; Yeah, it&#39;s not the first time I did this. I did it before with &lt;a href=&#34;https://lmika.org/2024/06/28/trying-something-new.html&#34;&gt;marginalias&lt;/a&gt;. Built the Hugo short-code, added it to a few posts, then never looked at it again.&lt;/p&gt;
&lt;p class=&#34;member member-m2&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;🦆:&lt;/b&gt; Oh, yeah. I vaguely remember you doing that before.&lt;/p&gt;
&lt;p class=&#34;member member-m1&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;L:&lt;/b&gt; Yeah, I may actually revisit that some time. Might be that doing something similar to what I&#39;ve done here with dialogues, i.e. adding a module in this Blogging Tool, will make it that I&#39;m more likely to use this.&lt;/p&gt;
&lt;p class=&#34;member member-m2&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;🦆:&lt;/b&gt; Well, let&#39;s see. The feature is here now, so there&#39;s no reason why you wouldn&#39;t use this in the future. Maybe next time in one of our rubber-ducking sessions.&lt;/p&gt;
&lt;p class=&#34;member member-m1&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;L:&lt;/b&gt; Yeah, we&#39;ll see.&lt;/p&gt;
&lt;p class=&#34;member member-m1&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;L:&lt;/b&gt; Anyway, I&#39;ll catch you then.&lt;/p&gt;
&lt;p class=&#34;member member-m2&#34;&gt;&lt;b class=&#34;avatar&#34;&gt;🦆:&lt;/b&gt; Yeah, see ya later.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So anyway, that&amp;rsquo;s what I&amp;rsquo;ve been doing the last week.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Devlog: Godot Game Update</title>
      <link>https://lmika.org/2025/04/27/a-brief-status-update-on.html</link>
      <pubDate>Sun, 27 Apr 2025 11:29:47 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/04/27/a-brief-status-update-on.html</guid>
      <description>&lt;p&gt;A brief status update on that Godot game. I think we&amp;rsquo;re pretty close to a finished 4-1 level. The underground section has been built, and the level has been decorated. I&amp;rsquo;ve also added a couple of secrets, which needed a few new mechanics — like doorways, which are used to transport the player around the level — plus some refinement to existing ones. I am a little concerned about the amount of waiting involved near the end of the first half, where the player will need to make their way across a large gap by jumping on the slow cycling &amp;ldquo;layer 2&amp;rdquo; tile layer. I&amp;rsquo;ll see what feedback I get from play-testers about this.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250427-111949.png&#34; width=&#34;600&#34; height=&#34;362&#34; alt=&#34;Auto-generated description: A pixelated, side-scrolling platformer video game scene features a knight character on a cliff with blocks, coins, and a treasure chest in view.&#34;&gt;
&lt;p&gt;I&amp;rsquo;ve also filled in the backdrop of all the levels to date. The grey default background colour has been replaced with a sky made of colour bands that came with the asset set I&amp;rsquo;m using. It&amp;rsquo;s basic, maybe too basic, but as long as it doesn&amp;rsquo;t clash with the interactive elements, and in the interest of &amp;ldquo;merely shipping something,&amp;rdquo; I think it&amp;rsquo;ll do for now.&lt;/p&gt;
&lt;p&gt;Finally, I made some changes from feedback I got from play-testers. Movement can now be made using WASD, along with the inverted-T arrow keys. I&amp;rsquo;ve also bound jump to the Enter key for those that want to use WASD. I made some changes in how secrets are telegraphed with the goal of making them more consistent. This is always a bit of a balancing act, as I don&amp;rsquo;t want to make them too obvious: they&amp;rsquo;re secrets after all. I added a tiny bit of &amp;ldquo;slip&amp;rdquo; to the player&amp;rsquo;s movement, so they don&amp;rsquo;t stop dead whenever the key is released. I didn&amp;rsquo;t want to go too far here: adding too much inertia introduced the risk of the player just falling off a platform after landing from a jump, which I don&amp;rsquo;t think is a great experience. And I fixed a window resizing bug (Godot enables window resizing by default, for anyone else who needs to know this).&lt;/p&gt;
&lt;p&gt;I think the next thing to do is to organise another version for play-testers, just to gauge what they think of level 4-1. Then it&amp;rsquo;s either on to level 4-2, or even introduce a new level between 1-1 and 1-2 to reduce the ramp up in difficulty between the two levels. Fortunately I&amp;rsquo;ve got some ideas on what I want to do for both of theses, so they&amp;rsquo;ll be easy starters. I&amp;rsquo;ve still got nothing for level 2-1, but I have a few ideas on what I want to do for 2-2, so maybe I&amp;rsquo;ll start that too.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/04/18/made-some-more-progress-on.html</link>
      <pubDate>Fri, 18 Apr 2025 14:30:18 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/04/18/made-some-more-progress-on.html</guid>
      <description>&lt;p&gt;Made some more progress on that Godot game. I haven&amp;rsquo;t gotten any further with the first level of world 2, so I&amp;rsquo;ve been spending much of my time making mechanics. One of them was the slow moving &amp;ldquo;level 2&amp;rdquo; mechanic that I stole wholesale from Super Mario World. That mechanic, despite it being frustrating to speed-runners, was always slightly interesting to me. To have areas of a level become accessible or hazardous just due to a layer of it oscillate up and down, it promised to make for some interesting timing challenges. At least in theory.&lt;/p&gt;
&lt;figure&gt;
&lt;video src=&#34;https://media.lmika.org/videos/2025/godot-w4-layer2.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;
&lt;figcaption&gt;Portion of the new level showing all three new mechanics.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I decided to put that theory to the test, and start work on one the later levels. And despite being a little skeptical about whether the mechanic could carry through a level on it&amp;rsquo;s own, I came up with one that I&amp;rsquo;m reasonably happy with. The mechanic is introduce slowly, and in a rather non-threatening way, proving the player the means to get to higher ground. This leads into the second half, which will be a long underground section which will ramp up the difficulty by introducing the risk of getting crushed or missing platforms.&lt;/p&gt;
&lt;p&gt;To compliment this is a new enemy that rushes the player. The player cannot do anything to defeat this enemy: combat is not really a thing in this game. All they could do is evade it before the enemy gives up. I am reusing the same &amp;ldquo;green slime&amp;rdquo; sprite for this but I&amp;rsquo;m hoping that the differing animations provide some hints of how this enemy&amp;rsquo;s behaviour differs from that of the simpler one.&lt;/p&gt;
&lt;p&gt;Finally, it was time to consider checkpoints. While the first few levels were too short to justify adding them in, this one is just that bit too long without one. And given the difficulty ramp-up in the second half, having the player go through the slower first half every time they died would probably lead to frustration. So checkpoints are now a thing. They&amp;rsquo;re not free — costing 5 coins to activate — and they are sometimes mandatory, blocking the player from progressing until they pay the toll. But I think their presence helps with eliminating the areas of the level that would just be boring to play through again and again.&lt;/p&gt;
&lt;p&gt;So yeah, I&amp;rsquo;m quite happy with this level. And I&amp;rsquo;m also happy in realising that I&amp;rsquo;m not bound to building this game in the same progression that the player will experience it. It&amp;rsquo;s better sometimes to just work on the areas that you&amp;rsquo;re ready to. I mean, it&amp;rsquo;s sounds obvious to say that now. Not sure why it took me this long to actually do so.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/04/08/spent-some-time-over-the.html</link>
      <pubDate>Tue, 08 Apr 2025 22:35:06 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/04/08/spent-some-time-over-the.html</guid>
      <description>&lt;p&gt;Spent some time over the last few days working on that Godot game, mainly building new mechanics. This evening I started working on an interceptor, something that would jump out of the quicksand in order to disrupt the player&amp;rsquo;s jump. Here&amp;rsquo;s an example of how they look in the test bed:&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://media.lmika.org/videos/2025/godot-w2-sand-spinners.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;And yeah, they&amp;rsquo;re pretty much a carbon-copy of the Podoboos from Mario. But I think there&amp;rsquo;s a reason they&amp;rsquo;re still making an appearance in games, years after their debut in Super Mario Brothers. They&amp;rsquo;re quite a versatile enemy, making jumping challenges a bit more interesting than just seeing whether the player the clear a gap. Plus they&amp;rsquo;re reasonably easy to make.&lt;/p&gt;
&lt;p&gt;Another mechanic taken from Mario was a switch that revealed coins and tiles for a limited time. Hit it once and the child nodes of this &amp;ldquo;timed_limited_visible&amp;rdquo; scene are displayed and activated for 10 seconds, before they disappear again:&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://media.lmika.org/videos/2025/godot-w2-floor-switch.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Much like the blue P switch this mechanic takes inspiration from, the switch can only be activated once. So it may be only useful for bonuses and areas the player can afford to miss.&lt;/p&gt;
&lt;p&gt;I had to do some special handling for nested &lt;code&gt;TileMap&lt;/code&gt; nodes, since the player could still collide with them even when they&amp;rsquo;re hidden. How I solved this was nothing too spectacular: basically I just walk the child tree looking for &lt;code&gt;TileMap&lt;/code&gt; instances, and when encountering one, just enabling or disabling the first layer:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;func _show_and_activate_children():
    visible = true
    process_mode = Node.PROCESS_MODE_INHERIT
    for tm in find_children(&amp;#34;*&amp;#34;, &amp;#34;TileMap&amp;#34;, false):
        tm.set_layer_enabled(0, true)

func _hide_and_deactivate_children():
    visible = false
    process_mode = Node.PROCESS_MODE_DISABLED
    for tm in find_children(&amp;#34;*&amp;#34;, &amp;#34;TileMap&amp;#34;, false):
        tm.set_layer_enabled(0, false)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Building these elements was fun, but the main problem is that I&amp;rsquo;m struggling to come up with a centrepiece mechanic for level 2-1, something that defines the level in some way. I have an idea for level 2-2 — this world is set in a desert so I&amp;rsquo;m hoping to introduce a thirst mechanic — but level 2-1 I&amp;rsquo;m hoping to keep relatively plain so as to avoid overwhelming the player with too many new things. The fear is to avoid making it little more than what the player encountered in world 1: a series of jumping puzzles over pits. Sure, that&amp;rsquo;s pretty much the entire game in a way, but some variety would be nice.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m hoping one of these mechanics could help here. I guess I&amp;rsquo;ll find one once I&amp;rsquo;ve start seriously building the level.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/04/01/started-working-on-world-and.html</link>
      <pubDate>Tue, 01 Apr 2025 20:52:50 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/04/01/started-working-on-world-and.html</guid>
      <description>&lt;p&gt;Started working on world 2, and one of the main mechanics of this world: quicksand. It won&amp;rsquo;t kill the player directly, but it will make it difficult for them to manoeuvre, and getting too low could cause death. Might be one of the more annoying mechanics in the game, but that&amp;rsquo;s kind of the point.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://media.lmika.org/videos/2025/level1-2-quicksand.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/04/01/the-results-of-my-first.html</link>
      <pubDate>Tue, 01 Apr 2025 20:17:48 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/04/01/the-results-of-my-first.html</guid>
      <description>&lt;p&gt;The results of my first play-test are in. And overall, they were pretty positive: movement was good, hit-boxes were fair, and it was described as &amp;ldquo;quite fun,&amp;rdquo; which was better than I was hoping for.&lt;/p&gt;
&lt;p&gt;One thing I&amp;rsquo;ll need to look out for is telegraphing secrets. The number of secrets is indicated at the end of the level, and based on the play-tester&amp;rsquo;s feedback, they seemed to have spent a lot of time running against walls trying to find them. There is one secret in the level 1-1 that I thought was telegraphed well, and I can confirm that the player found them all. But I will concede the others required the player to make a leap of faith, and fall into an area that the player will usually want to avoid, which is pretty unfair. So I&amp;rsquo;ll need to fix that.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/03/29/a-bit-more-on-the.html</link>
      <pubDate>Sat, 29 Mar 2025 10:36:52 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/03/29/a-bit-more-on-the.html</guid>
      <description>&lt;p&gt;A bit more on the Godot game this morning, this time working on background tiles artwork. Made some grey masonry tiles for the end castle sequences. Also tried some background rocks for underground areas. I&amp;rsquo;m pretty rubbish at anything organic, but they didn&amp;rsquo;t turn out too bad.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250329-113300.png&#34; width=&#34;600&#34; height=&#34;564&#34; alt=&#34;Auto-generated description: Two rectangular pixel art frames with stone textures, one in brown and the other in gray, are displayed with matching filled versions inside them.&#34;&gt;
&lt;figcaption&gt;Right side has the background tiles surrounded with their complementary foreground tiles on the left.&lt;/figcaption&gt;
&lt;/figure&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/03/27/added-a-few-final-things.html</link>
      <pubDate>Thu, 27 Mar 2025 21:12:26 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/03/27/added-a-few-final-things.html</guid>
      <description>&lt;p&gt;Added a few final things to my Godot game, such as a really boring title and end-title screen, before preparing a release for play testers (or play tester, I&amp;rsquo;ve got exactly one lined up). I think we&amp;rsquo;re ready.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250327-220802.png&#34; width=&#34;600&#34; height=&#34;362&#34; alt=&#34;Auto-generated description: A game welcome screen introduces Princess Real-estate, instructing players to collect coins and avoid hazards to reach a castle.&#34;&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/03/24/a-bit-more-on-godot.html</link>
      <pubDate>Mon, 24 Mar 2025 21:39:10 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/03/24/a-bit-more-on-godot.html</guid>
      <description>&lt;p&gt;A bit more on Godot this evening, mainly working on pausing the game, and the end-of-level sequence. Have got something pretty close to what I was looking for: a very Mario-esc sequence where the player enters a castle, it start auto-walking the character, and the level stats show up and &amp;ldquo;spin&amp;rdquo; for a bit. Not too bad, although I may need to adjust the timing and camera a little to keep the stats from being unreadable.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://media.lmika.org/videos/2025/godot-ending-sequence.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/03/17/a-bit-more-godot-work.html</link>
      <pubDate>Mon, 17 Mar 2025 21:14:01 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/03/17/a-bit-more-godot-work.html</guid>
      <description>&lt;p&gt;A bit more Godot work this evening. I wanted to add a foreground layer of tiles that obscured in the player. This is for making false walls to hide secrets around the level. It took me a while to work out how to turn off collision of this foreground layer: there wasn&amp;rsquo;t really any way to do so within the designer.&lt;/p&gt;
&lt;p&gt;Fortunately, &lt;a href=&#34;https://github.com/godotengine/godot-proposals/issues/6881#issuecomment-1547610226&#34;&gt;this Github comment&lt;/a&gt; showing how to do so using a script worked like a charm:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;extends TileMap

const foreground_layer = 1

func _use_tile_data_runtime_update(layer: int, coords: Vector2i) -&amp;gt; bool:
	return layer == foreground_layer
	
func _tile_data_runtime_update(layer: int, coords: Vector2i, tile_data: TileData) -&amp;gt; void:
	tile_data.set_collision_polygons_count(0, 0)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Only limitation seems to be that it will disable collision for the whole layer, but that&amp;rsquo;s perfectly fine with me.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/03/17/spent-more-time-on-my.html</link>
      <pubDate>Mon, 17 Mar 2025 06:42:31 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/03/17/spent-more-time-on-my.html</guid>
      <description>&lt;p&gt;Spent more time on my Godot platformer yesterday, mainly rebuilding the first level from scratch. The previous version was rush and was just not fun (it didn&amp;rsquo;t look good either, but I didn&amp;rsquo;t dress it up yet). This new one is much nicer, and allowed me to use a few new mechanics I&amp;rsquo;ve built.&lt;/p&gt;
&lt;p&gt;I still need to build out the level ending sequence. I&amp;rsquo;ve got less than the basics at the moment: a drawbridge descends and that&amp;rsquo;s pretty much it. I need to add the conditions for when the bridge descends (specifically, a minimum number of coins), stopping the player movement for a brief scripted sequence, then transition to the next level. I think I know how I want to do this, it&amp;rsquo;s just a matter of implementing it. Once level 1 is dressed up and working, I think that&amp;rsquo;ll be the next thing I do.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m enjoying working on this project so far, although part of me is a little afraid that I&amp;rsquo;m spending time on something that just isn&amp;rsquo;t good, like there&amp;rsquo;s some sunk cost with the time I&amp;rsquo;m spending on this that&amp;rsquo;s better put to use on something else. Of course, when I give in to these feelings, I usually just spend the time watching TV. So which activity is the real waste of time? Is producing something that may be crap (or worse, just boring) better than not producing anything at all?&lt;/p&gt;
&lt;p&gt;Anyway, not should how this became a feeling post. This game might be rubbish, but I still enjoy working on it: Godot is actually quite fun to use. Surely that is reason enough to continue.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/03/10/was-looking-at-how-i.html</link>
      <pubDate>Mon, 10 Mar 2025 20:29:31 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/03/10/was-looking-at-how-i.html</guid>
      <description>&lt;p&gt;Was looking at how I could add hazards to my Godot project, such as spikes. My first idea was to find a way to detect collisions with tiles in a &lt;code&gt;TileMap&lt;/code&gt; in Godot. But there was no real obvious way to do so, suggesting to me that this was not the way I should be going about this. Many suggested simply using an &lt;code&gt;Area2D&lt;/code&gt; node to detect when a play touches a hazard.&lt;/p&gt;
&lt;p&gt;I was hesitant to copy and paste the scene node I had which handled the collision signal and kill the player — the so-called &amp;ldquo;kill zone&amp;rdquo; scene —but today I learnt that it&amp;rsquo;s possible to add multiple &lt;code&gt;CollisionShape2D&lt;/code&gt; nodes to an &lt;code&gt;Area2D&lt;/code&gt; node. This meant I needed only a single &amp;ldquo;kill zone&amp;rdquo; scene node, and just draw out the kill zones over the spikes as children. The TileMap simply provides the graphics.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250310-211225.png&#34; width=&#34;600&#34; height=&#34;427&#34; alt=&#34;Auto-generated description: A game development interface displaying level design with a grid layout, tiles, and collision shapes is shown.&#34;&gt;
&lt;p&gt;This discovery may seem a little trivial, but I&amp;rsquo;d prefer to duplicate as few nodes as a can, just so I&amp;rsquo;ve got less to touch when I want to change something.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/03/10/turns-out-my-previous-solution.html</link>
      <pubDate>Mon, 10 Mar 2025 08:49:38 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/03/10/turns-out-my-previous-solution.html</guid>
      <description>&lt;p&gt;Tried opening my Godot project this morning and was greeted with the following error:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250310-094231.png&#34; width=&#34;600&#34; height=&#34;176&#34; alt=&#34;Auto-generated description: A computer screen displays a development environment with an error message pop-up about an invalid or missing scene file.&#34;&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;scene/resources/resource_format_text.cpp:284 - res://scenes/falling_platform.tscn:14 - Parse Error: 
Failed loading resource: res://scenes/falling_platform.tscn. Make sure resources have been imported by opening the project in the editor at least once.
Failed to instantiate scene state of &amp;#34;res://scenes/falling_platform.tscn&amp;#34;, node count is 0. Make sure the PackedScene resource is valid.
Failed to load scene dependency: &amp;#34;res://scenes/falling_platform.tscn&amp;#34;. Make sure the required scene is valid.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Traced it back to the &lt;a href=&#34;https://lmika.org/2025/03/08/adventures-in-godot-respawning-a.html&#34;&gt;technique I was using to respawn the falling platform&lt;/a&gt;. Looks like Godot didn&amp;rsquo;t like the two preloads I included:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;# file: scripts/falling_platform.gd

@onready var FallingPlatform = preload(&amp;#34;res://scenes/falling_platform.tscn&amp;#34;)
@onready var FallingPlatformScript = preload(&amp;#34;res://scripts/falling_platform.gd&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This resulted in a parse error and Godot thinking the level scene was corrupted.  In retrospect, this kinda makes sense. What I doing was technically a circular dependency, where the scene and script was trying to preload itself. I was hoping Godot was smart enough to recognise this, but I guess not.&lt;/p&gt;
&lt;p&gt;So I had to change the respawn code. I modified it to make use the &lt;a href=&#34;https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-duplicate&#34;&gt;duplicate&lt;/a&gt; method. Here&amp;rsquo;s the revised version:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;func respawn():	
    var dup = self.duplicate()
    dup.global_position = init_global_position	

    # Duplicate also copies the velocity so zero it out here
    dup.velocity = Vector2(0, 0)
    get_tree().current_scene.add_child(dup)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After some limited testing, this seems to work. One good thing about this approach is that it looks like duplicate copies the script, so I no longer need to do anything special here.&lt;/p&gt;
&lt;p&gt;So I guess the lesson here is don&amp;rsquo;t try to preload the script within itself.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Adventures In Godot: Respawning A Falling Platform</title>
      <link>https://lmika.org/2025/03/08/adventures-in-godot-respawning-a.html</link>
      <pubDate>Sat, 08 Mar 2025 11:35:07 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/03/08/adventures-in-godot-respawning-a.html</guid>
      <description>&lt;p&gt;My taste of going through a Godot tutorial last week has got me wanting more, so I&amp;rsquo;ve set about building a game with it. Thanks to my limited art skills, I&amp;rsquo;m using &lt;a href=&#34;https://brackeysgames.itch.io/brackeys-platformer-bundle&#34;&gt;the same asset pack&lt;/a&gt; that was used in the video, although I am planning to add a bit of my own here and there.&lt;/p&gt;
&lt;p&gt;But it&amp;rsquo;s the new mechanics I enjoy working on, such as adding falling platforms. If you&amp;rsquo;ve played any platformer, you know what these look like: platforms that are suspended in air, until the player lands on them, at which point gravity takes a hold and they start falling, usually into a pit killing the player in the process:&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://media.lmika.org/videos/2025/falling-platform-1.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;The platforms are built as a &lt;code&gt;CharacterBody2D&lt;/code&gt; with an &lt;code&gt;Area2D&lt;/code&gt; that will detect when a player enters the collision shape. When they do, a script will run which will have the platform &amp;ldquo;slipping&amp;rdquo; for a second, before the gravity is turned on and the platform falls under it&amp;rsquo;s own weight. The whole thing is bundled as a reusable scene which I could drag into the level I&amp;rsquo;m working on.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250308-123202.png&#34; width=&#34;600&#34; height=&#34;391&#34; alt=&#34;Auto-generated description: A game development interface with a sprite and code editor is shown, from a software environment like Godot.&#34;&gt;
&lt;p&gt;I got the basics of this working reasonably quickly, yet today, I had a devil of a time going beyond that.
The issue was that I wanted the platform to respawn after it fell off the map, so that the player wouldn&amp;rsquo;t get soft-locked at at an area where the platform was needed to escape. After a false-start trying to reposition the platform at it&amp;rsquo;s starting point after it fell, I figured it was just easier to respawn the platform when the old one was removed. To do this I had to solve two problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;How do I get the platform starting point?&lt;/li&gt;
&lt;li&gt;How can I actually respawn the platform?&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;getting-the-starting-point&#34;&gt;Getting The Starting Point&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;Node2D&lt;/code&gt; object has the property &lt;a href=&#34;https://docs.godotengine.org/en/stable/classes/class_node2d.html#class-node2d-property-global-position&#34;&gt;global_position&lt;/a&gt;, which returns the position of the object based on the world coordinates. However, it seems like this position is not correct when the &lt;a href=&#34;https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-private-method-init&#34;&gt;_init&lt;/a&gt; function of the attached script is called. I suspect this is because this function is called before the platform is added to the scene tree, when the final world coordinates are known.&lt;/p&gt;
&lt;p&gt;Fortunately, there exists the &lt;code&gt;_ready&lt;/code&gt; notification, which is invoked when the node is added to the scene tree. After some experimentation, I managed to confirm that &lt;code&gt;global_position&lt;/code&gt; properly was correct. So tracking the starting point is a simple as storing that value in a variable:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;var init_global_position = null

func _ready():
	init_global_position = global_position
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Another option is to use the &lt;code&gt;_enter_tree()&lt;/code&gt; notification. From &lt;a href=&#34;https://docs.godotengine.org/en/stable/tutorials/best_practices/godot_notifications.html#ready-vs-enter-tree-vs-notification-parented&#34;&gt;the documentation&lt;/a&gt;, it looks like either would probably work here, with the only difference being the order in which this notification is invoked on parents and children (&lt;code&gt;_enter_tree&lt;/code&gt; is called by the parent first, whereas &lt;code&gt;_ready&lt;/code&gt; is called by the children first).&lt;/p&gt;
&lt;h2 id=&#34;respawning-the-platform&#34;&gt;Respawning The Platform&lt;/h2&gt;
&lt;p&gt;The next trick was finding out how to respawn the platform. The usual technique for doing so, based on the results of my web searching, is to load the platform scene, &lt;a href=&#34;https://docs.godotengine.org/en/stable/classes/class_packedscene.html#description&#34;&gt;instantiate a new instance&lt;/a&gt; of it, and added it to the scene tree.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;@onready var FallingPlatform = preload(&amp;#34;res://scenes/falling_platform.tscn&amp;#34;)

func respawn():	
    var dup = FallingPlatform.instantiate()
    add_child(dup)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Many of the examples I&amp;rsquo;ve seen online added the new scene node as a child of the current node. This wouldn&amp;rsquo;t work for me as I wanted to free the current node at the same time, and doing so would free the newly instantiated child. The fix for this was easy enough: I just added the new node as a child of the current scene.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;@onready var FallingPlatform = preload(&amp;#34;res://scenes/falling_platform.tscn&amp;#34;)


func respawn():	
    var dup = FallingPlatform.instantiate()
    get_tree().current_scene.add_child(dup)
    queue_free()
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I still had to reposition the new node to the spawn point. Fortunately the &lt;code&gt;global_position&lt;/code&gt; property is also settable, so it was simply a matter of setting that property before adding it to the tree (this is so that it&amp;rsquo;s correct when the newly instantiated node receives the &lt;code&gt;_ready&lt;/code&gt; notification).&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;@onready var FallingPlatform = preload(&amp;#34;res://scenes/falling_platform.tscn&amp;#34;)

func respawn():	
    var dup = FallingPlatform.instantiate()
    dup.global_position = init_global_position
    get_tree().current_scene.add_child(dup)
    queue_free()
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This spawned the platform at the desired positioned, but there was a huge problem: when the player jumped on the newly spawn platform, it wouldn&amp;rsquo;t fall. The &lt;code&gt;Area2D&lt;/code&gt; connection was not invoking the script to turn on the gravity:&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://media.lmika.org/videos/2025/falling-platform-2.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;It took me a while to figured out what was going on, but I came to the conclusion that the packed scene was loading properly, but without the script attached. Turns out a &lt;a href=&#34;https://docs.godotengine.org/en/stable/classes/class_script.html&#34;&gt;Script&lt;/a&gt; is a resource separate from the scene, and can be loaded and attached to an object via the &lt;a href=&#34;https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-method-set-script&#34;&gt;set_script&lt;/a&gt; method:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;@onready var FallingPlatform = preload(&amp;#34;res://scenes/falling_platform.tscn&amp;#34;)
@onready var FallingPlatformScript = preload(&amp;#34;res://scripts/falling_platform.gd&amp;#34;)

func respawn():	
    var dup = FallingPlatform.instantiate()
    dup.set_script(FallingPlatformScript)

    dup.global_position = init_global_position
    get_tree().current_scene.add_child(dup)
    queue_free()
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Finally, after figuring all this out, I was able to spawn a new falling platform, have it positioned at the starting position of the old platform, and react to the player standing on it.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://media.lmika.org/videos/2025/falling-platform-3.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;The time it took to work this out is actually a little surprising. I was expecting others to run into the same problem I was facing, where they were trying to instantiate a scene only to have the scripts not do anything. Yet it took me 45 minutes of web searching through Stack Overflow and forum posts that didn&amp;rsquo;t solve my problem. It was only after a bit of experimentation and print-debugging on my own that I realised that I actually had to attached the script after instantiating the node.&lt;/p&gt;
&lt;p&gt;To be fair, I will attribute some of this to not understanding the problem at first: I actually thought the &lt;code&gt;Area2D&lt;/code&gt; wasn&amp;rsquo;t actually being instantiated at all. Yet not one of the Stack Overflow answers or forum post floated the possibility that the script wasn&amp;rsquo;t being loaded alongside the scene.  This does suggest to me that my approach may not be optimal. There does exist a &amp;ldquo;Local to Scene&amp;rdquo; switch in the script inspector that could help, although turning it on doesn&amp;rsquo;t seem to do much. But surely there must be some way to instantiate the script alongside the scene.&lt;/p&gt;
&lt;p&gt;Anyway, that&amp;rsquo;s for later. For now, I&amp;rsquo;m happy that I&amp;rsquo;ve got something that works.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/02/24/prototyped-a-game-i-had.html</link>
      <pubDate>Mon, 24 Feb 2025 21:02:53 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/02/24/prototyped-a-game-i-had.html</guid>
      <description>&lt;p&gt;Prototyped a game I had in mind, sort of a 2D Sokoban-like thing, where you control a robot with a retractable pushing arm that is to push gems to a &amp;ldquo;receiver&amp;rdquo; tile. Not entirely sure if it&amp;rsquo;s fun enough to actually build.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://media.lmika.org/videos/2025/naclbot-prototype.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Used &lt;a href=&#34;https://pixijs.com&#34;&gt;PixiJS&lt;/a&gt; to build it. Not a bad framework.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Making A Small Two-Letter Country Code Lookup Page</title>
      <link>https://lmika.org/2025/02/20/made-a-small-thing-yesterday.html</link>
      <pubDate>Thu, 20 Feb 2025 07:57:14 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/02/20/made-a-small-thing-yesterday.html</guid>
      <description>&lt;p&gt;Made a small thing yesterday.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been seeing links to the &lt;a href=&#34;https://uchu.style&#34;&gt;uchū color pallette&lt;/a&gt; and &lt;a href=&#34;https://www.brailleinstitute.org/freefont/&#34;&gt;Atkinson Hyperlegible font&lt;/a&gt; on Mastodon this past week and I wanted to give them a try on something. With little else to work on, I thought I&amp;rsquo;d spend yesterday evening building a small thing I&amp;rsquo;ve been wishing I had at work for a while.&lt;/p&gt;
&lt;p&gt;My tasks at work has me looking up the two-letter country codes a lot recently. My goto is the &lt;a href=&#34;https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2&#34;&gt;ISO-3166-1 alpha 2&lt;/a&gt; page on Wikipedia, but getting there involves a few clicks, a web-search, and a &amp;ldquo;find on page.&amp;rdquo; I wanted an easier way to get to this list, and a much easier way to filter it. I also wanted something that could work as a Vivaldi side panel, so that I can call it up while I&amp;rsquo;m looking at something else.&lt;/p&gt;
&lt;p&gt;What I ended up building is this, which I&amp;rsquo;ve called 2LCC, which stands for Two Letter Country Codes.&lt;/p&gt;
&lt;div class=&#34;img-gallery&#34;&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250220-083135.png&#34;
     
        alt=&#34;Auto-generated description: A web page displays a country code list, featuring a filter option and countries listed alphabetically with their corresponding codes.&#34; 
     
      /&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250220-083206.png&#34;
     
        alt=&#34;Auto-generated description: A webpage shows a table listing country codes and names, including IO for British Indian Ocean Territory, IN for India, and ID for Indonesia, with Ind typed in a search bar.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Start entering a country name or country code to filter the table.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250220-083226.png&#34;
     
        alt=&#34;Auto-generated description: A selection list on a web page shows two-letter country codes along with their corresponding country names, with AU for Australia highlighted.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  Because two-letter codes will appear as letters within country names, any exact match in either the code or the country name will highlight the row in yellow.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://lmika.org/uploads/2025/out-20250220-083239.png&#34;
     
        alt=&#34;Auto-generated description: A web browser window is open displaying a list of country codes on the left and several website shortcuts on the right, including Booking.com, News.com.au, YouTube, and eBay.&#34; 
     
      /&gt;

  &lt;figcaption&gt;
  
  How it looks in Vivaldi&amp;#39;s sidebar.
  &lt;/figcaption&gt;

&lt;/figure&gt;
&lt;/div&gt;
&lt;p&gt;It was built using Hugo, which some might think may be a bit overkill, but Hugo does has some nice utilites for &lt;a href=&#34;https://gohugo.io/methods/site/data/&#34;&gt;reading JSON data&lt;/a&gt; and running it through a Go template to produce static HTML. And that&amp;rsquo;s all this is: a static HTML page with a big table of country codes with a smattering of CSS and JavaScript. The JavaScript is just vanilla, and is simply used to power the filter, which basically walks the table, hiding rows which don&amp;rsquo;t contain the filter string. I was originally going to have the filter logic test the inner text elements of the table cells, but I decided to have the template spit out the country code as &lt;code&gt;data-&lt;/code&gt; attributes instead. It&amp;rsquo;s easier to fetch in JavaScript using the &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset&#34;&gt;dataset&lt;/a&gt; property list and since the values are spat out by the template in lowercase, it makes case insensitive slightly easier.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll see how often I end up using this: there&amp;rsquo;ve been instances when I&amp;rsquo;ve over-index needing something for my job, only to build it and find myself using it less than I imagined. It probably wasn&amp;rsquo;t a great project to choose for trying out uchū colour scheme, seeing that I only ended up using two colours. But I&amp;rsquo;m happy with how the two colours I used turned out, and the the font looks great. And I do like that I took on more of the layout styling myself, rather than simply defer to &lt;a href=&#34;https://simplecss.org&#34;&gt;simple.css&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a link to it should this be useful to you: &lt;a href=&#34;https://2lcc.lmika.app/&#34;&gt;https://2lcc.lmika.app/&lt;/a&gt;.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/02/15/dusted-off-podcast-favourites-last.html</link>
      <pubDate>Sat, 15 Feb 2025 10:10:08 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/02/15/dusted-off-podcast-favourites-last.html</guid>
      <description>&lt;p&gt;Dusted off Podcast Favourites (last commit 25 April 2022) and fixed a longstanding issue of thumbnails being lost when they&amp;rsquo;re changed in the feed. Editing the feed properties will now force a refresh of the thumbnail URLs. Didn&amp;rsquo;t need to change anything else, which was a nice change.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250215-110539.png&#34; width=&#34;600&#34; height=&#34;537&#34; alt=&#34;Auto-generated description: A podcast favourites list showing episodes with titles and descriptions from the ATP - Members Feed.&#34;&gt;
</description>
    </item>
    
    <item>
      <title>UCL: Some Updates</title>
      <link>https://lmika.org/2025/02/08/ucl-some-updates.html</link>
      <pubDate>Sat, 08 Feb 2025 09:32:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/02/08/ucl-some-updates.html</guid>
      <description>&lt;p&gt;Made a few minor changes to UCL. Well, actually, I made one large change. I&amp;rsquo;ve renamed the &lt;code&gt;foreach&lt;/code&gt; builtin to &lt;code&gt;for&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I was originally planning to have a &lt;code&gt;for&lt;/code&gt; loop that worked much like other languages: you have a variable, a start value, and an end value, and you&amp;rsquo;d just iterate over the loop until you reach the end.  I don&amp;rsquo;t know how this would&amp;rsquo;ve looked, but I imagined something like this:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;for x 0 10 {
    echo $x
}
# numbers 0..9 would be printed.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But this became redundant after adding the &lt;code&gt;seq&lt;/code&gt; builtin:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;foreach (seq 10) { |x|
    echo $x
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This was in addition to all the other useful things you could do with the foreach loop&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;, such as loop over lists and hashes, and consume values from iterators. It&amp;rsquo;s already a pretty versatile loop. So I elected to go the Python way and just made it so that the &lt;code&gt;for&lt;/code&gt; loop is the loop to use to iterate over collections.&lt;/p&gt;
&lt;p&gt;This left an opening for a loop that dealt with guards, so I also added the &lt;code&gt;while&lt;/code&gt; loop. Again, much like most languages, this loop would iterate over a block until the guard becomes false:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;set x 0
while (lt $x 5) {
    echo $x
    set x (add $x 1)
}
echo &amp;#34;done&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Unlike the &lt;code&gt;for&lt;/code&gt; loop, this is unusable in a pipeline (well, unless it&amp;rsquo;s the first component). I was considering having the loop return the result of the guard when it terminates, but I realised that would be either false, nil, or anything else that was &amp;ldquo;falsy.&amp;rdquo; So I just have the loop return nil. That said, you can break from this loop, and if the break call had a value, that would be used as the result of the loop:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;set x 0
while (lt $x 5) {
    set x (add $x 1)
    if (ge $x 3) {
        break &amp;#34;Ahh&amp;#34;
    }
} | echo &amp;#34; was the break&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The guard is optional, and if left out, the &lt;code&gt;while&lt;/code&gt; loop will iterate for ever.&lt;/p&gt;
&lt;h2 id=&#34;the-set-builtin&#34;&gt;The Set! Builtin&lt;/h2&gt;
&lt;p&gt;Many of these changes come from using of UCL for my job, and one thing I found myself doing recently is writing a bunch of migration scripts. This needed to get data from a database, which may or may not be present. If it&amp;rsquo;s not, I want the script to fail immediately so I can check my assumptions.  This usually results in constructs like the following:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;set planID (ls-plans | first { |p| eq $p &amp;#34;Plan Name&amp;#34; } | index ID)
if (not $planID) {
    error &amp;#34;cannot find plan&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And yeah, adding the if block is fine — I do it all the time when writing Go — but it would be nice to assert this when you&amp;rsquo;re trying to set the variable, for no reason other than the fact that you&amp;rsquo;re thinking about nullability while writing the expression to fetch the data.&lt;/p&gt;
&lt;p&gt;So one other change I made was add the &lt;code&gt;set!&lt;/code&gt; builtin. This will basically set the variable only if the expression is not nil. Otherwise, it will raise an error.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;set! planID (ls-plans | first { |p| eq $p &amp;#34;Missing Plan&amp;#34; } | index ID)
# refusing to set! `planID` to nil value
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This does mean that &lt;code&gt;!&lt;/code&gt; and &lt;code&gt;?&lt;/code&gt; are now valid characters to appear in identifiers, just like Ruby. I haven&amp;rsquo;t decided whether I want to start following the Ruby convention of question marks indicating a predicate or bangs indicating a mutation. Not sure that&amp;rsquo;s going to work out now, given that the bang is being used here to assert non-nullability.  In either case, could be useful in the future.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;And the &lt;code&gt;seq&lt;/code&gt; builtin&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>Idea for UCL: Methods </title>
      <link>https://lmika.org/2025/02/02/idea-for-ucl-methods.html</link>
      <pubDate>Sun, 02 Feb 2025 14:46:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/02/02/idea-for-ucl-methods.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;m toying with the idea of adding methods to UCL. This will be similar to the methods that exist in Lua, in that they&amp;rsquo;re essentially functions that pass in the receiver as the first argument, although methods would only be definable by the native layer for the first version.&lt;/p&gt;
&lt;p&gt;Much like Lua though, methods would be invokable using the &lt;code&gt;:&lt;/code&gt; &amp;ldquo;pair&amp;rdquo; operator.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;strs:to-upper &amp;#34;Hello&amp;#34;
--&amp;gt; HELLO
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The idea is to make some of these methods on the types themselves, allowing their use on literals and the result of pipelines, as well as variables:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;set h &amp;#34;Hello&amp;#34;
$hello:to-upper
--&amp;gt; HELLO

&amp;#34;Hello&amp;#34;:to-upper
--&amp;gt; HELLO

(cat &amp;#34;Hello &amp;#34; &amp;#34;world&amp;#34;):to-upper
--&amp;gt; HELLO WORLD
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The use of methods would be purely conveience. One could conceive of a type, like a CSV table, where there&amp;rsquo;s a need to perform a series of operations over it.&lt;/p&gt;
&lt;p&gt;The potential downside of using &lt;code&gt;:&lt;/code&gt; is that it may make defining dictionaries more ambiguous. When the parser sees something that could either be a list or a dictionary, it does a scan to search for any pairs that may exist. If so, it treats the literal as a dictionary. But would this work with using &lt;code&gt;:&lt;/code&gt; as the method separator?&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;[&amp;#34;Hello&amp;#34;:to-upper]
--&amp;gt; Is this a dictionary {&amp;#34;Hello&amp;#34;:&amp;#34;to-upper&amp;#34;}, or the list [&amp;#34;HELLO&amp;#34;]?
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So that would require something. Might be that I need to require method invocations to occur within parenthesis for list literals.&lt;/p&gt;
&lt;p&gt;Ambiguities like this aside, I&amp;rsquo;m planning to keep it simple for now. Methods will not be user definable within UCL out of the gate, but would be effectively another interface available to native types. For the builtin ones that exist now, it&amp;rsquo;ll most likely be little more than syntactic sugar over the standard library functions:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$hello:to-upper

# equivalent to

strs:to-upper $hello
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Speaking of the standard library, the use of &lt;code&gt;:&lt;/code&gt; as a module-function separator may need some changes. A the moment it&amp;rsquo;s a bit of a hack: when a module is loaded, any procs defined within that module is stored in the environment with the operator: &lt;code&gt;strs:to-upper&lt;/code&gt; for example. The parser is sophisticated enough to recognised &lt;code&gt;:&lt;/code&gt; as a token, but when it parses two or more identifiers separated with &lt;code&gt;:&lt;/code&gt;, it just joins them up into a single identifier.&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s needed is something else, maybe using something based on methods. The current idea is to define modules as being some special form of object, where the &amp;ldquo;methods&amp;rdquo; are simply the names of the exported symbol.&lt;/p&gt;
&lt;p&gt;I was curious to know whether the language was capable of doing something similar now. It&amp;rsquo;s conceivable that a similar concept could be drafted with procs returning dictionaries of procs, effectively acting as a namespace for a collection of functions.  So a bit of time in the playground resulted in this experiment:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;proc fns {
  return [
    upper: (proc { |x| strs:to-upper $x })
    lower: (proc { |x| strs:to-lower $x })
  ]
}

(fns).upper &amp;#34;Hello&amp;#34;
--&amp;gt; HELLO

(fns).lower &amp;#34;Hello&amp;#34;
--&amp;gt; hello
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A good first start. This is theoretically possible in the language that exists at the moment. It&amp;rsquo;s not perfect thought. For one thing, the call to &lt;code&gt;fns&lt;/code&gt; needs to be enclosed in parenthesis in order to invoke it. Leaving them out results in an error:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;fns.upper &amp;#34;Hello&amp;#34;
--&amp;gt; error: command is not invokable
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The same is true when using variables instead of procs. I tried this experiment again using variables and it came to the same limitations:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;set fns [
  upper: (proc { |x| strs:to-upper $x })
  lower: (proc { |x| strs:to-lower $x })
]

($fns).upper &amp;#34;Hello&amp;#34;
--&amp;gt; HELLO

$fns.upper &amp;#34;Hello&amp;#34;
--&amp;gt; error: command is not invokable
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Obviously the parser needs to be changed to add additional support for the &lt;code&gt;:&lt;/code&gt; operator, but I also need to fix how strong &lt;code&gt;.&lt;/code&gt; binds to values too.  But I think this may have legs.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>UCL: Iterators</title>
      <link>https://lmika.org/2025/01/31/still-working-on-ucl-in.html</link>
      <pubDate>Fri, 31 Jan 2025 08:41:44 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/01/31/still-working-on-ucl-in.html</guid>
      <description>&lt;p&gt;Still working on UCL in my spare time, mainly filling out the standard library a little, like adding utility functions for lists and CSV files. Largest change made recently was the adding iterators to the mix of core types. These worked a lot like the streams of old, where you had a potentially unbounded source of values that could only be consumed one at a time. The difference with streams is that there is not magic to this: iterators work like any other type, so they could be stored in variables, passed around methods, etc (streams could only be consumed via pipes).&lt;/p&gt;
&lt;p&gt;I augmented the existing high-level functions like &lt;code&gt;map&lt;/code&gt; and &lt;code&gt;filter&lt;/code&gt; to consume and produce iterators, but it was fun discovering other functions which also became useful. For example, there exists a &lt;code&gt;head&lt;/code&gt; function which returned the first value of a list. But I discovered that the semantics also worked as a way to consume the next element from an iterator. So that’s what this function now does. This, mixed with the fact that iterators are truthy if they’re got at least one pending value, means that some of the builtins could now be implemented in UCL itself. Like the example below, which could potentially be used to reimplement &lt;code&gt;itrs:to-list&lt;/code&gt; (this is a contrived example, as &lt;code&gt;foreach&lt;/code&gt; would probably work better here).&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;proc to-list { |itr lst|
   if $itr {
      lists:add $lst (head $itr)
      return (to-list $itr $lst)
   }
   return $lst
}

to-list (itrs:from [1 2 3 4 5]) []
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But the biggest advantage that comes from iterators is querying large data-stores with millions of rows. Being able to write a UCL script which sets up a pipeline of &lt;code&gt;maps&lt;/code&gt; and &lt;code&gt;filters&lt;/code&gt; and just let it churn through all the data in it&amp;rsquo;s own time is the dream.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;list-customers | filter { |x| $x.HasPlan } | map { |x| $x.PlanID } | foreach echo
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;ve got a need for this in the internal backend tool that spurred the development of UCL, and I&amp;rsquo;m looking forward to using iterators to help here.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/01/25/this-weeks-distraction-building-a.html</link>
      <pubDate>Sat, 25 Jan 2025 15:03:08 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/01/25/this-weeks-distraction-building-a.html</guid>
      <description>&lt;p&gt;This week&amp;rsquo;s distraction: building a Wordle clone. No particular reason for doing this other than I felt like building one, although I did miss the small time waster of the original Wordle, and watching a game show with my parents that had a similar concept just made those feelings stronger. Main difference between this and Wordle classic: board randomly selects between 4-letter, 5-letter, and 6-letter words; no daily limit or social-media sharing when you guessed the word correctly; and the biggest one: UK English spelling.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250125-141717.png&#34; width=&#34;460&#34; height=&#34;963&#34; alt=&#34;Auto-generated description: A word puzzle game interface shows a grid with the words HOUSE, ALTAR, and POINT, with colour-coded tiles indicating correct and incorrect letter guesses.&#34; class=&#34;block-center&#34;&gt;
&lt;p&gt;Some remarks on how this was built: I used 11ty to build the static site. It originally started as just a HTML page with some JavaScript, but I wanted to leave the option open for bundling and minifying the JS with Stimulus. The dictionary I got from &lt;a href=&#34;https://archive.netbsd.org/pub/pkgsrc-archive/distfiles/2019Q4/hunspell-dictionaries/en_GB-20061130/&#34;&gt;Hunspell&lt;/a&gt;, which &lt;a href=&#34;https://apple.stackexchange.com/a/402862&#34;&gt;is apparently the spell checker&lt;/a&gt; Apple has based their work on. There is a little bit of Go to filter and sort the dictionary of words. The words are in sorted order for the binary search algorithm to check if a word exists or not. The puzzle order is predetermined and was done by &amp;ldquo;shuffling&amp;rdquo; the indices in a separate array. Base styles are, of course, from &lt;a href=&#34;https://simplecss.org/&#34;&gt;simple.css&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re interested in checking it out, &lt;a href=&#34;https://letterise.lmika.app&#34;&gt;you can find it here&lt;/a&gt;. Just be aware that it may not be as polished as much of the other stuff you find out there. Turns out that I can tolerate a fair few shortcomings in things that I build for my own amusement.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/01/20/learnt-a-very-import-thing.html</link>
      <pubDate>Mon, 20 Jan 2025 21:18:23 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/01/20/learnt-a-very-import-thing.html</guid>
      <description>&lt;p&gt;Learnt a very import thing about Stimulus outlets this evening: the outlet name must match the controller name of the outlet target. If this is not the case, the outlet will not bind and you&amp;rsquo;d be beside yourself struggling to find out why the outlet target cannot be found.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://stimulus.hotwired.dev/reference/outlets#:~:text=The%20outlet%20identifier%20in%20the%20host%20controller%20must%20be%20the%20same%20as%20the%20target%20controller%E2%80%99s%20identifier.&#34;&gt;From the docs&lt;/a&gt;:&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;The outlet identifier in the host controller must be the same as the target controller’s identifier.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;Took me 30 minutes and stepping through with code with the debugger to find this out.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/01/19/started-filling-out-the-ucl.html</link>
      <pubDate>Sun, 19 Jan 2025 09:24:18 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/01/19/started-filling-out-the-ucl.html</guid>
      <description>&lt;p&gt;Started filling out the &lt;a href=&#34;https://ucl.lmika.dev/&#34;&gt;UCL website&lt;/a&gt;, mainly by documenting the core modules. It might be a little unnecessary to have a full website for this, given that the only person who&amp;rsquo;ll get any use from it right now will be myself. But who knows how useful it could be in the future? If nothing else, it&amp;rsquo;s a showcase on what I&amp;rsquo;ve been working on for this project.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/01/15/ive-been-using-ucl-a.html</link>
      <pubDate>Wed, 15 Jan 2025 21:40:42 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/01/15/ive-been-using-ucl-a.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been using UCL a lot recently, which is driving additional development on it. Spent a fair bit of time this evening fixing bugs and adding small features like string interpolation. Fix a number of grammar bugs too, that only popped up when I started writing multi-line scripts with it.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Project Update: DSL Formats For Interactive Fiction</title>
      <link>https://lmika.org/2025/01/12/still-bouncing-around-things-to.html</link>
      <pubDate>Sun, 12 Jan 2025 10:39:47 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/01/12/still-bouncing-around-things-to.html</guid>
      <description>&lt;p&gt;Still bouncing around things to work on at the moment. Most of the little features have been addressed, and I have little need to add anything pressing for the things I&amp;rsquo;ve been working on recently. As for the large features, well apathy&amp;rsquo;s taking care of those. But there is one project that is tugging at my attention. And it&amp;rsquo;s a bit of a strange one, as part of me just wants to kill it. Yet it seems to be resisting.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://lmika.org/2024/07/29/current-project-update.html&#34;&gt;About 6 months ago&lt;/a&gt;, I started working on some interactive fiction using &lt;a href=&#34;https://evergreen.ink/&#34;&gt;Evergreen&lt;/a&gt;. I got most of the story elements squared away but much of the interactive elements were still left to be done. And as good as Evergreen is for crafting the story, I found it a little difficult tracking down all the scripts I needed to write, debug and test.&lt;/p&gt;
&lt;p&gt;So in early November, I had a look at porting this story over to a tool of my own, called Pine Needle (yes, the name is a bit of a rip-off). Much like Evergreen, the artefact of this is a choose-your-own-adventure story implemented as a static webpage. Yet the means of building the story couldn&amp;rsquo;t be more different. Seeing that I&amp;rsquo;m more comfortable working with code and text files, I eschewed building any UI in favour of a tool that simply ran from the command line.&lt;/p&gt;
&lt;p&gt;But this meant that I needed someway to represent the story in text. Early versions simply had the story hard coded in Go, but it wasn&amp;rsquo;t long before I started looking a using a DSL. My first attempt was a hand-built one based on Markdown with some additional meta-elements. The goal was to keep boilerplate to a minimum, with the meta-elements getting out of the way of the prose. Here&amp;rsquo;s a sample of what I had working so far:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;// Three dashes separate pages, with the page ID following on.
// Also these are comments, as the hash is reserved for titles.
--- tulips-laundry-cupboard

You open the cupboard door and look at the shelf
above the brooms. There are a couple of aerosol cans up there,
including a red one that says &amp;#34;Begone Insecticide&amp;#34;.
You bring it out and scan the active ingredients. There are a
bunch of letters and numbers back there, and none of them have
the word &amp;#34;organic.&amp;#34;

\choice &amp;#39;Take Insecticide&amp;#39; to=tulips-take-insecticide
\choice &amp;#39;Leave Insecticide&amp;#39; to=tulips-leave-insecticide

--- tulips-take-insecticide

You return to the tulips with the insecticide, and start 
spraying them. The pungent odour of the spray fills the air, 
but you get the sense that it&amp;#39;s helping a little.

\choice &amp;#39;Continue&amp;#39; to=tulips-end

--- tulips-leave-insecticide

You decide against using the can of insecticide. You put the
can back on the shelf and close the cupboard door.

\choice &amp;#39;Look Under The Trough&amp;#39; to=tulips-laundry-trough
\choice &amp;#39;Exit Laundry&amp;#39; to=tulips-exit-laundry
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The goal was to have the meta-elements look like LaTeX macros — for example, &lt;code&gt;\option{Label}{target-screen}&lt;/code&gt; — but I didn&amp;rsquo;t get far in finishing the parser for this. And I wasn&amp;rsquo;t convinced it had the flexibility I wanted. LaTeX macros relies pretty much on positional arguments, but I knew I wanted key-value pairs to make it easier to rely on defaults, plus easier to extend later.&lt;/p&gt;
&lt;p&gt;I did imagine a fully LaTeX inspired DSL for this, but I quickly dismissed it for how &amp;ldquo;macro-heavy&amp;rdquo; it would be. For reference, here&amp;rsquo;s how I imagined it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-latex&#34; data-lang=&#34;latex&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\screen&lt;/span&gt;{tulips-laundry-cupboard}{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  You open the cupboard door and lo ok at the shelf
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  above the brooms. There are a couple of aerosol cans up there,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  including a red one that says &amp;#34;Begone Insecticide&amp;#34;.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  You bring it out and scan the active ingredients. There are a
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  bunch of letters and numbers back there, and none of them have
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  the word &amp;#34;organic.&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;\choice&lt;/span&gt;{Take Insecticide}{to=tulips-take-insecticide}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;\choice&lt;/span&gt;{Leave Insecticide}{to=tulips-leave-insecticide}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\screen&lt;/span&gt;{tulips-take-insecticide}{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  You return to the tulips with the insecticide, and start 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  spraying them. The pungent odour of the spray fills the air, 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  but you get the sense that it&amp;#39;s helping a little.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;\choice&lt;/span&gt;{Continue}{to=tulips-end}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\screen&lt;/span&gt;{tulips-leave-insecticide}{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  You decide against using the can of insecticide. You put the
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  can back on the shelf and close the cupboard door.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;\choice&lt;/span&gt;{Look Under The Trough}{to=tulips-laundry-trough}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;\choice&lt;/span&gt;{Exit Laundry}{to=tulips-exit-laundry}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I wasn’t happy with the direction of the DSL, so I looked for something else. I briefly had a thought about using JSON. I didn’t go so far as to try it, but the way this could work is something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;screens&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tulips-laundry-cupboard&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;body&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    You open the cupboard door and look at the shelf
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    above the brooms. There are a couple of aerosol cans up 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    there, including a red one that says \&amp;#34;Begone Insecticide\&amp;#34;.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    You bring it out and scan the active ingredients. There
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    are a bunch of letters and numbers back there, and none of 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    them have the word \&amp;#34;organic.\&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;  &amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;options&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;screen&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tulips-take-insecticide&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;label&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Take Insecticide&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;screen&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tulips-leave-insecticide&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;label&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Leave Insecticide&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}, &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tulips-take-insecticide&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;body&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    You return to the tulips with the insecticide, and start 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    spraying them. The pungent odour of the spray fills the air, 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    but you get the sense that it&amp;#39;s helping a little.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;  &amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;options&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;screen&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tulips-end&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;label&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Continue&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I generally like JSON as a transport format, but it didn’t strike me as a format that suited the type of data I wanted to encode. Most of what this format would contain would be prose, which I’d prefer to keep as Markdown. But this would clash with JSON’s need for explicit structure. Setting aside the additional boilerplate this structure would require, all the prose would have to be encoded as one big string, which didn’t appeal to me.
Also no comments, especially within string literals, which is a major deal breaker.&lt;/p&gt;
&lt;p&gt;So, the current idea is to use something based on XML. This has some pretty significant benefits: editors have good support for XML, and Go has an unmarshaller which can read an XML directly into Go structures. JSON has this too, but I think it’s also a pretty decent format for at editing documents by hand, so long as you keep your XML elements to a minimum.&lt;/p&gt;
&lt;p&gt;I think one aspect that turned people off XML back in the day was format designer’s embrace of XML’s ability to represent hierarchical data without leaning into it’s use as a language for documents. The clunky XML documents I had to deal with were  purely used to encode structure, usually in a way that mapped directly to an domain’s class model. You had formats where you need 10 nested elements to encode a single bit of information that were a pain to read or edit by hand. These were usually dismissed by the designers with promises like, “Oh, you won’t be editing this by hand most of the time. You’ll have GUI design tools to help you.” But these were awful to use too, and that’s if they were available, which they usually were not (did you know building GUIs are hard?)&lt;/p&gt;
&lt;p&gt;If you have an XML format that skews closer to HTML rather than something that’s representable in JSON, I think it could be made to work. So yesterday I had a go at seeing whether it this could work for Pine Needle. Here’s what I’ve got so far:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;&amp;lt;&lt;/span&gt;?xml version=&amp;#34;1.0&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;story&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;screen&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;id=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tulips-laundry-cupboard&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  You open the cupboard door and look at the shelf
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  above the brooms. There are a couple of aerosol cans up 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  there, including a red one that says &amp;#34;Begone Insecticide&amp;#34;.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  You bring it out and scan the active ingredients. There
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  are a bunch of letters and numbers back there, and none of 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  them have the word &amp;#34;organic.&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;option&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;screen=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tulips-take-insecticide&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;Take Insecticide&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;option&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;screen=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tulips-leave-insecticide&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;Leave Insecticide&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/screen&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;screen&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;id=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tulips-take-insecticide&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  You return to the tulips with the insecticide, and start 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  spraying them. The pungent odour of the spray fills the air, 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  but you get the sense that it&amp;#39;s helping a little.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;option&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;screen=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tulips-end&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;Continue&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/screen&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;screen&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;id=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tulips-leave-insecticide&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  You decide against using the can of insecticide. You put the
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  can back on the shelf and close the cupboard door.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;option&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;screen=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tulips-laundry-trough&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;Look Under The Trough&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;option&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;screen=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tulips-exit-laundry&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;Exit Laundry&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/screen&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/story&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The idea is that the prose will still be Markdown, so things like blank lines will still be respected (the parser strips all the leading whitespace, allowing one to easily indent the prose). Attributes satisfy the key/value requirement for the elements, and I get the features that make this easy to modify by hand, such as comments and good editor support.&lt;/p&gt;
&lt;p&gt;I think it’s going to work. It would require some custom code, as Go’s unmarshaller doesn’t quite like the mix of prose and declared &lt;code&gt;&amp;lt;option&amp;gt;&lt;/code&gt; elements, but I think it’s got the bones of a decent format for this interactive fiction. Already I’m coming up with ideas of how to add script elements and decompose fragments into sub-files to make it easier to test.&lt;/p&gt;
&lt;p&gt;I’ll talk more about this project in the future if I’m still working on it. I don’t know if the story that started all this  will see the light of day. I’ve gone through it a few times, and it’s not great. But shipping stuff you’re proud of comes from shipping stuff you’re not proud of, and given how far along it is, it probably deserved to be release in one form or another. That’s probably why it’s been saved from the chopping block so far&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;Yes, this is probably just a rationalisation for trying to minimise sunk-costs, but I’ve got nothing else to work on, so why not this?&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/01/05/i-recently-got-a-new.html</link>
      <pubDate>Sun, 05 Jan 2025 16:10:47 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/01/05/i-recently-got-a-new.html</guid>
      <description>&lt;p&gt;I recently got a new phone, a Pixel 9 Pro, which meant I needed to bring Alto Player up to date. I probably could&amp;rsquo;ve gotten away using the version I was using on my Pixel 6. But I didn&amp;rsquo;t have a binary build, and I needed to upgrade Gradle anyway, so I decided to spend a bit of time bringing it up to date to API version 35, the version used in Android 15.0. Fortunately it was only a few hours in total, and once I got it running in the simulator, I side-loaded it onto my phone and started using it.&lt;/p&gt;
&lt;p&gt;It worked, but there were some significant UI issues. The title-bar bled into the status bar, the album image in the Now Playing widget was cropped by the curved corners of the phone, and the media notification didn&amp;rsquo;t display playback controls.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250105-162220.png&#34; width=&#34;600&#34; height=&#34;404&#34; alt=&#34;Auto-generated description: A mobile app displays a list of music tracks, each with a title, duration, and album art symbol.&#34;&gt;
&lt;figcaption&gt;Evolution of the window insets, from left-to-right: before any changes, version with the album cover and margin, final version with no album art.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I set about fixing these issues today, starting with the title-bar and Now Playing widget. These was an issue with the views not respecting the window insets, and after a quick Google search, I found &lt;a href=&#34;https://developer.android.com/develop/ui/views/layout/insets&#34;&gt;this article&lt;/a&gt; showing how one could resolve this by adding a &lt;a href=&#34;https://developer.android.com/reference/androidx/core/view/ViewCompat#setOnApplyWindowInsetsListener%28android.view.View%2Candroidx.core.view.OnApplyWindowInsetsListener%29&#34;&gt;ViewCompat.setOnApplyWindowInsetsListener&lt;/a&gt; and reacting to it by adjusting the margins of the view.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; topLevelLayout = findViewById(&lt;span style=&#34;color:#a6e22e&#34;&gt;R&lt;/span&gt;.id.top_level_layout) &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; CoordinatorLayout
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ViewCompat&lt;/span&gt;.setOnApplyWindowInsetsListener(topLevelLayout) { v, windowInsets &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; insets = windowInsets.getInsets(&lt;span style=&#34;color:#a6e22e&#34;&gt;WindowInsetsCompat&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Type&lt;/span&gt;.systemBars())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    v.updateLayoutParams&amp;lt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ViewGroup&lt;/span&gt;.MarginLayoutParams&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        leftMargin = insets.left
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        bottomMargin = insets.bottom
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        rightMargin = insets.right
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// applying a top margin here would work, but will not have the toolbar
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// background colour &amp;#34;bleed&amp;#34; into the status bar, which is what I want.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; t = v.findViewById&amp;lt;Toolbar&amp;gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;R&lt;/span&gt;.id.toolbar)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    t.updateLayoutParams&amp;lt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ViewGroup&lt;/span&gt;.MarginLayoutParams&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// make the toolbar a little &amp;#34;narrower&amp;#34; than full height
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        topMargin = insets.top * &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt; / &lt;span style=&#34;color:#ae81ff&#34;&gt;4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;WindowInsetsCompat&lt;/span&gt;.CONSUMED
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It took a few attempts, but I managed to get this working. Just using the top inset for the toolbar margin made it a little larger than I liked, so I adjusted the height to be 75% of the inset. This means the toolbar will actually encroach into the area reserved for cut-outs like the front-facing camera. This is arguably not something a &amp;ldquo;real&amp;rdquo; Android apps should do, but this is just for me and my phone so it&amp;rsquo;s fine.&lt;/p&gt;
&lt;p&gt;I went through a few iterations of the album artwork cutoff on the bottom right corner trying to find something I liked. I tried bringing in the horizontal margins a little, but I didn&amp;rsquo;t like the alignment of the album art in the player, particularly compared to the covers that appear in the track list screen. One thing I didn&amp;rsquo;t try was raising the bottom margin so that it would fit &amp;ldquo;above&amp;rdquo; the curve. But those corners curve in quite a bit, and doing this would sacrifice a lot of vertical space. So I settled on hiding the album art altogether. It&amp;rsquo;s a bit of a shame to loose it, but at least it looks neater now.&lt;/p&gt;
&lt;p&gt;The next thing I looked at was fixing the playback controls in the media notification. After some investigating, I found that this was because I was not setting the available actions in the &lt;a href=&#34;https://developer.android.com/reference/android/support/v4/media/session/PlaybackStateCompat.Builder#setActions%28long%29&#34;&gt;PlaybackStateCompat builder&lt;/a&gt;. This, if I understand correctly, is used to communicate to various systems the current state of the playing media: what the track name is, whether it&amp;rsquo;s playing, whether one can skip forward or back. I have my own types for tracking this information — which is probably not correct but I wasn&amp;rsquo;t completely sure as to what I was doing at the time with Android&amp;rsquo;s media stack&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; — and when I needed to convert this to a type understood by the framework, I made instances of this builder without setting the available actions. Earlier versions of Android seemed not to care, and the controls always appeared on the notification. But I guess they changed that.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250105-162302.png&#34; width=&#34;600&#34; height=&#34;404&#34; alt=&#34;Three smartphone screens display a notification panel, a permission request, and a media notification with music controls.&#34;&gt;
&lt;figcaption&gt;Evolution of the playback notification, from left-to-right: before any changes, the request to show notifications upon first launch (this is defined by the system), the playback notifications with controls again.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;One other thing I needed to do was to explicitly ask the user permission to show a notification before I could publish one. This is also relatively new: my experience with Android goes back to the early days where these permissions were disclosed up front when the app was installed. But I can completely understand why they changed it, as it was easy to simply tap through those screens with reading them. I am wondering whether media playback notifications are in some way exempt from these permission checks, as I was actually getting one to appear before I made this changes. But I figured it was probably worth doing anyway, so I added this permission request on first launch. Arguably I should be asking for this permission when playback starts, but again, this is just for me.&lt;/p&gt;
&lt;p&gt;One final thing I needed to address were long album titles. The text view displaying the the album title had a width that autosized to the title itself, and the start and end constraints were set such that it appears centred in the view. This worked for &amp;ldquo;normal&amp;rdquo; length titles but when the length became excessive, the text view would fill the entire width of the screen and the title will appear left justified.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250105-162320.png&#34; width=&#34;600&#34; height=&#34;606&#34; alt=&#34;Auto-generated description: Two smartphone screens display a music player with Leaps And Bounds by Paul Kelly.&#34;&gt;
&lt;figcaption&gt;Before (left) and after (right) shot of the fixed album title.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The fix for this was to set the text width to be calculated by the start and end constraints (setting &lt;code&gt;layout_width&lt;/code&gt; to &lt;code&gt;0dp&lt;/code&gt;), bringing in the margins a little, and making the label text centre justified. I did this already for the track title, so it was easy to do this here too. Not sure why I didn&amp;rsquo;t do it earlier, or why I haven&amp;rsquo;t done it for the artist&amp;rsquo;s name yet.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250105-162740.png&#34; width=&#34;600&#34; height=&#34;391&#34; alt=&#34;Auto-generated description: A screenshot of a mobile app layout design in Android Studio, featuring a user interface editor with text, images, and buttons.&#34;&gt;
&lt;p&gt;This was all rushed, and I&amp;rsquo;ll admit I wasn&amp;rsquo;t 100% sure what I was doing. I was going down the route of trial-and-error to get this working, mixed in with web searches and a trip to ChatGPT. And yeah, the methods I used won&amp;rsquo;t make this a portable Android app that would work on every phone out there. But I&amp;rsquo;m reasonably happy with how it turned out.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;This is still true to this day.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2025/01/04/thanks-for-my-new-found.html</link>
      <pubDate>Sat, 04 Jan 2025 14:25:43 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2025/01/04/thanks-for-my-new-found.html</guid>
      <description>&lt;p&gt;Thanks for my new found fondness of buying mainstream music instead of streaming it, I needed a way to get these albums into Alto Catalogue. There exists a feature for fetching and importing tracks from a Zip referenced by a URL. This works great for albums bought in Bandcamp, but less so for any tracks I may have on my local machine.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2025/out-20250104-151341.png&#34; width=&#34;600&#34; height=&#34;427&#34; alt=&#34;A web interface for uploading a zip file from a URL with fields for artist, album, and default rating.&#34;&gt;
&lt;p&gt;I&amp;rsquo;ve managed to get Alto Catalogue building again after updating Webpack and a few NPM packages, so in theory, I could  add an Upload Zip file action. But there&amp;rsquo;s more to this than simply accepting and unpacking a Zip file. I have to read the metadata, maybe even preview the tracks that will be imported, just in case I&amp;rsquo;m importing something I rather not (I did see this once, where zipping a bunch of tracks in the Finder introduced duplicates). This already exists for Zip files that are downloadable online.&lt;/p&gt;
&lt;p&gt;I had a though about what my options are, until I remembered that I had a &lt;a href=&#34;https://github.com/Forceu/Gokapi/&#34;&gt;Gokapi instance&lt;/a&gt; running in Pikapods. So I tried using that to temporarily host the Zip file with a publicly available URL that could be read by Alto Catalouge.&lt;/p&gt;
&lt;p&gt;The only problem is my internet upload speed is &lt;em&gt;sooooo sloooooow&lt;/em&gt;. The Gokapi instance is hosted in Europe, and I suspect the instance itself is a little underpowered. So uploading 100 MB Zip files would take a fair bit of time: maybe 15-30 minutes. When I tried doing this via the web frontend, the connection timed out.&lt;/p&gt;
&lt;p&gt;Fortunately, &lt;a href=&#34;https://gokapi.readthedocs.io/en/latest/advanced.html#api&#34;&gt;Gokapi has an API&lt;/a&gt; and one of the methods allows you to upload a file in &amp;ldquo;chunks,&amp;rdquo; which Gokapi will assemble back into the original file. Even better is that this chunking can be uploaded in parallel.&lt;/p&gt;
&lt;p&gt;So I built a CLI tool which made of this chunking API to upload the Zip files. Once the upload is complete, the tool will display the hot-link URL, which I can copy-and-paste into Alto Catalogue.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://media.lmika.org/videos/2025/send2gokapi-screencast.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;The whole process isn&amp;rsquo;t fast (again, slow upload speeds). But it works, and I can use this tool to queue a bunch of uploads and let it do its thing while I&amp;rsquo;m doing something else. I really like tools that do this, where you&amp;rsquo;re not forced to babysitting them through the process.&lt;/p&gt;
&lt;p&gt;There are a few limitations with it. It doesn&amp;rsquo;t allow for an awful lot of customisations on the lifecycle of the uploaded file. And the tool stalled out once when my computer went to sleep, and I had to start the upload from scratch. I could probably add something to track the chunks that were successful, allowing one to continue a stalled upload. If this happens frequently, I may look more into adding this.&lt;/p&gt;
&lt;p&gt;But even so, this could be a useful addition to my use of Gokapi for transferring temporary files. If you think this might be useful to you, &lt;a href=&#34;https://lmika.dev/cmd/send2gokapi&#34;&gt;you can find the tool here&lt;/a&gt;.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/12/31/fixed-the-ui-of-alto.html</link>
      <pubDate>Tue, 31 Dec 2024 15:28:19 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/12/31/fixed-the-ui-of-alto.html</guid>
      <description>&lt;p&gt;Fixed the UI of Alto Player, plus addressed some long standing issues I&amp;rsquo;ve been having.&lt;/p&gt;
&lt;p&gt;One was displaying the album covers for playlists instead of the generic &amp;ldquo;missing album&amp;rdquo; image. It&amp;rsquo;s technically possible to set an album cover on a playlist, but I never built the UI to do this in the web-app. So the app now uses the album cover of the first track in the playlist if one isn&amp;rsquo;t specified. Another was getting automated release builds working in GitHub, as per &lt;a href=&#34;https://medium.com/@dcostalloyd90/automating-android-builds-with-github-actions-a-step-by-step-guide-2a02a54f59cd&#34;&gt;these instructions&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But the biggest improvement was finally getting around to storing the position of the album list, so that going back up the navigation stack wouldn&amp;rsquo;t reposition the list to the top. I tried this a way back, but couldn&amp;rsquo;t get it working, probably because I was testing &lt;code&gt;RecyclerView.scrollToPositionWithOffset&lt;/code&gt; by passing last constant numbers, like 100, only to find the list not actually scrolling. It turns out that this method actually takes the index of the item to position at the top, not a pixel offsets. So the view wouldn&amp;rsquo;t scroll if you happen to have a list with less than 100 items. It only started working after I tried smaller numbers, like 5.&lt;/p&gt;
&lt;p&gt;So all in all, a good day.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/12/31/only-took-two-hours-to.html</link>
      <pubDate>Tue, 31 Dec 2024 10:51:11 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/12/31/only-took-two-hours-to.html</guid>
      <description>&lt;p&gt;Only took two hours to uplift Alto Player from Android SDK version 30 to 35. Fought an upgrade to Gradle (because of-course), skirted around a migration from ExoPlayer to Media 3, and battled a NullPointerException due to my inability to properly my own navigation args. All in all, not bad.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/12/28/more-fun-today-working-on.html</link>
      <pubDate>Sat, 28 Dec 2024 10:30:36 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/12/28/more-fun-today-working-on.html</guid>
      <description>&lt;p&gt;More fun today working on Blogging Tools. Finished a feature for uploading larger videos to object storage so they can be added to a post using the standard video tag, as opposed to an embedded video player. If you see the screencast below, that means it&amp;rsquo;s working.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://media.lmika.org/videos/2024/blogging-tools-video-upload.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/12/17/exploring-godot-to.html</link>
      <pubDate>Tue, 17 Dec 2024 21:00:25 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/12/17/exploring-godot-to.html</guid>
      <description>&lt;p&gt;Exploring Godot to see if I could use it to make a card game. I got this far:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/out-20241217-215736.png&#34; width=&#34;600&#34; height=&#34;407&#34; alt=&#34;Auto-generated description: A computer window titled SolitaireGolf (DEBUG) is displayed with a blank dark screen.&#34;&gt;
&lt;p&gt;Yep, I&amp;rsquo;m on a roll. 😄&lt;/p&gt;
&lt;p&gt;Might need to work through a couple Godot tutorials first, just so that I understand the basics.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/12/12/i-plan-to.html</link>
      <pubDate>Thu, 12 Dec 2024 07:48:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/12/12/i-plan-to.html</guid>
      <description>&lt;p&gt;I plan to integrate &lt;a href=&#34;https://workpad.dev/categories/tcl&#34;&gt;UCL&lt;/a&gt; into another tool at work, so I spent last night improving it&amp;rsquo;s use as a REPL. Added support for onboard help and setting up custom type printing, which is useful for displaying tables of data. I started working on the tool today and it&amp;rsquo;s already feeling great.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/out-20241212-084242.png&#34; width=&#34;600&#34; height=&#34;465&#34; alt=&#34;A command line interface is displayed, showing help-related commands, usage, arguments, and details.&#34;&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/12/07/in-other-buildingsmallthingsformyself.html</link>
      <pubDate>Sat, 07 Dec 2024 13:19:05 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/12/07/in-other-buildingsmallthingsformyself.html</guid>
      <description>&lt;p&gt;In other building-small-things-for-myself news, I spent a bit of time this morning on the image processor for Blogging Tools. The big new change was adding support for working with multiple source images, instead of just one. This made way for a new &amp;ldquo;Phone Shot&amp;rdquo; processor, which arranges multiple screenshots of phone apps in a row, while also downscaling them and proving some quick crops and distribution options.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://media.lmika.org/cleanshot-2024-12-07-11.52.08.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;This should reduce the vertical size of mobile app screenshots I post here, something that&amp;rsquo;s been bothering me a little.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/12/05/some-more-work.html</link>
      <pubDate>Sat, 07 Dec 2024 10:05:54 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/12/05/some-more-work.html</guid>
      <description>&lt;p&gt;Some more work on that feed reader app. You know how I said that I wanted to hold off looking at appearances? Well, I abandoned that approach when I installed the app on my phone and tried viewing a few articles. And oof! There&amp;rsquo;s work to be done there.&lt;/p&gt;
&lt;p&gt;First, it&amp;rsquo;s slow. Very slow. It&amp;rsquo;s taking a good second or two to pull feeds and entries from Feedbin. I was expecting this, and I&amp;rsquo;ve got a few plans on how to speed this up. But the biggest concern is the janky list scrolling. I mean, I wasn&amp;rsquo;t expecting the buttery smoothness of iPhone list scrolling, but I expected Flutter to be better than what I was experiencing. I&amp;rsquo;m hoping that it&amp;rsquo;s just because I was running a debug build, but part of me fears that Flutter is just not optimised for smooth list scrolling, favouring ease of development and a common runtime. I rather not change frameworks now, especially after spending an evening dealing with all the build issues, but I don&amp;rsquo;t want to live with this for ever.&lt;/p&gt;
&lt;p&gt;But speed is not the biggest issue. The biggest offender was the feed reader view. The embedded web-view was only lightly styled, and it felt like it. The margins were all off, and I didn&amp;rsquo;t like the default font or colours. It made reading the article a bad experience to a surprising degree. I&amp;rsquo;ve dealt with rushed or poorly designed UIs in the past, but I didn&amp;rsquo;t have much tolerance for this. Not sure why this is, but I suspect it&amp;rsquo;s because I&amp;rsquo;ve been using feed readers that put some effort into the design of their reader screen.&lt;/p&gt;
&lt;p&gt;In any case, a couple of evenings ago, I decided to put some effort into the styling. I replace the body text font with Noto Sans and the fixed-font with Ubuntu Mono. I dropped the font-size a little to 1.05 em (it was previously 1.1 em, with felt a little big, and 1.0 em felt a little small). I bought the margins in a little. And I styled the block-quote, figure, and pre elements to an appearance that, despite being a little overused, felt quite modern.&lt;/p&gt;
&lt;p&gt;The results look much better, at least to my eye (and my emulator). Here are some side-to-side comparison shots of the changes (left side is the unstyled version, while the right side has the new styling changes):&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/out-20241207-105808.png&#34; width=&#34;600&#34; height=&#34;401&#34; alt=&#34;Auto-generated description: Two smartphone screens display an article titled Apple Intelligence by Bitsplitting, discussing Apple&#39;s upcoming product announcement.&#34;&gt;
&lt;figcaption&gt;Demonstration of the new font and link colouring choices.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/out-20241207-105928.png&#34; width=&#34;600&#34; height=&#34;401&#34; alt=&#34;Auto-generated description: Side-by-side comparison of two smartphone screens displaying an email titled From the Department of Bringing Receipts to the Interview, featuring text from a Stanford Review interview with President Levin.&#34;&gt;
&lt;figcaption&gt;Demonstration of the changes to block-quotes. Having a line down the left-sided is a pretty common style, but it&#39;s one I like.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/out-20241207-110005.png&#34; width=&#34;600&#34; height=&#34;401&#34; alt=&#34;Auto-generated description: Two mobile screens showing a website with a summer dark theme, including CSS code for customizing the theme&#39;s appearance.&#34;&gt;
&lt;figcaption&gt;Demonstration of code blocks and figures with captions.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I still need to actually install this on my phone and try it out. I&amp;rsquo;m wondering whether I should do so after a bit more work syncing the read status with Feedbin. That&amp;rsquo;s a feature that&amp;rsquo;s keeping me on Feedbin&amp;rsquo;s PWA for now.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/12/03/ran-into-a.html</link>
      <pubDate>Tue, 03 Dec 2024 21:45:18 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/12/03/ran-into-a.html</guid>
      <description>&lt;p&gt;Spent the last few evenings continuing work on a Flutter-based RSS feed reader. This project is now further along then my previous attempts at doing this. I&amp;rsquo;m at the point where feeds and feeds items are being fetch from Feedbin and displayed in a list view:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/screenshot-20241203-212954.png&#34; width=&#34;600&#34; height=&#34;1266&#34; alt=&#34;Auto-generated description: A smartphone screen displays a list of feeds with titles like And now it’s all this, App Defaults, and Articles on Jose M.&#34; class=&#34;block-center&#34;&gt;
&lt;figcaption&gt;The beginning of the feed list&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/screenshot-20241203-213022.png&#34; width=&#34;600&#34; height=&#34;1266&#34; alt=&#34;Auto-generated description: A screenshot of a social media app shows posts by Manton Reece discussing topics like media coverage, book covers, custom emojis, and Micro.blog features.&#34; class=&#34;block-center&#34;&gt;
&lt;figcaption&gt;The feed item list. Title-less posts are to be fully supported, with a bulk of the summary shown in the list. Titled posts should have a smaller summary.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The aesthetics are taking a bit of a back seat in favour of functionality for now; I haven&amp;rsquo;t even changed the default accent colour. But the infrastructure is there: tapping a feed will bring up the entries for that feed. This is using &lt;a href=&#34;https://docs.flutter.dev/cookbook/navigation/named-routes&#34;&gt;named routes&lt;/a&gt; for navigation, and &lt;a href=&#34;https://bloclibrary.dev/flutter-bloc-concepts/&#34;&gt;cubits&lt;/a&gt; for state management. It&amp;rsquo;s a bit more work upfront, but it does make for a neater codebase.&lt;/p&gt;
&lt;p&gt;The biggest challenge so far was getting the actual reader view working. I hoping to use the &lt;a href=&#34;https://pub.dev/packages/webview_flutter&#34;&gt;webview plugin&lt;/a&gt;, but when I tried adding it, I ran into a bunch of Gradle errors. These were either class version errors or dependency errors, depending on what I tried to fix it (I didn&amp;rsquo;t get screenshots, sorry). I eventually stumbled upon this &lt;a href=&#34;https://docs.flutter.dev/release/breaking-changes/flutter-gradle-plugin-apply&#34;&gt;Flutter Gradle plugin migration guide&lt;/a&gt;, and following this, along with upgrading Java to OpenJDK 25, got Gradle working again.&lt;/p&gt;
&lt;p&gt;But I was still getting build errors. The first couple were Gradle plugins  &lt;code&gt;com.android.application&lt;/code&gt; and &lt;code&gt;org.jetbrains.kotlin.android&lt;/code&gt; that needed to be updated. Easy stuff. And then I got this error message:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Execution failed for task &amp;#39;:webview_flutter_android:compileDebugJavaWithJavac&amp;#39;.
&amp;gt; Could not resolve all files for configuration &amp;#39;:webview_flutter_android:androidJdkImage&amp;#39;.
   &amp;gt; Failed to transform core-for-system-modules.jar to match attributes {artifactType=_internal_android_jdk_image, org.gradle.libraryelements=jar, org.gradle.usage=java-runtime}.
      &amp;gt; Execution failed for JdkImageTransform: /Users/leonmika/Library/Android/sdk/platforms/android-34/core-for-system-modules.jar.
         &amp;gt; Error while executing process /Users/leonmika/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/jlink with arguments {--module-path /Users/leonmika/.gradle/caches/8.10.2/transforms/575ccd1a7426c0be21d9fe3a81898be3-05a021da-a1a7-409f-a30a-bba769b57371/transformed/output/temp/jmod --add-modules java.base --output /Users/leonmika/.gradle/caches/8.10.2/transforms/575ccd1a7426c0be21d9fe3a81898be3-05a021da-a1a7-409f-a30a-bba769b57371/transformed/output/jdkImage --disable-plugin system-modules}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Running a web search on the error revealed &lt;a href=&#34;https://stackoverflow.com/a/79095064&#34;&gt;this Stack Overflow answer&lt;/a&gt;, which resolve it. There were still a few complaints about the the NDK version but after all that, the app finally launched with the web-viewer.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/screenshot-20241203-212901.png&#34; width=&#34;600&#34; height=&#34;1266&#34; alt=&#34;Auto-generated description: A smartphone screen displays a simple app interface with the title Read The Feed and placeholder text saying Content goes here.&#34; class=&#34;block-center&#34;&gt;
&lt;figcaption&gt;The web-viewer, with a &#34;hello world&#34;-ish test HTML document&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I still need to actually render the entry, plus style the HTML a bit. The immediate goal after this, once the reader view, is getting this on my phone to start playing with it. It&amp;rsquo;s just barebones for now, but I find that the sooner I can start using a project myself, the more likely I am to keep at it.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/11/25/looking-for-my.html</link>
      <pubDate>Mon, 25 Nov 2024 05:53:42 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/11/25/looking-for-my.html</guid>
      <description>&lt;p&gt;Looking for my next project to work on. I have a few ideas but my mind keeps wandering back to an RSS reader for Android. I read RSS feeds quite frequently on my phone and Feedbin&amp;rsquo;s web app is great, but I think I prefer a native app.&lt;/p&gt;
&lt;p&gt;I just need to get over the hump of setting up my Android Studios. There&amp;rsquo;s something about starting a new project there that just sucks the life out of you.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/11/24/playing-around-with.html</link>
      <pubDate>Sun, 24 Nov 2024 05:41:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/11/24/playing-around-with.html</guid>
      <description>&lt;p&gt;Playing around with some possible UI design choices for that Android RSS Feed Reader. I think I will go with Flutter for this, seeing that I generally like the framework and it has decent (although not perfect) support for native Material styling.&lt;/p&gt;
&lt;p&gt;Started looking at the feed item view. This is what I have so far:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/screenshot-20241125-221642.png&#34; width=&#34;600&#34; height=&#34;1266&#34; alt=&#34;Auto-generated description: A smartphone screen displays a Flutter demo app with a list of text items including an example domain, a note about using &#39;T&#39; RDS instance types, and a comment about a blog post.&#34; class=&#34;block-center&#34;&gt;
&lt;p&gt;Note that this is little more than a static list view. The items comes from nowhere and tapping an item doesn&amp;rsquo;t actually do anything yet. I wanted to get the appearance right first, as how it feels is downstream from how it works.&lt;/p&gt;
&lt;p&gt;The current plan is to show most of the body for items without titles, similar to what other social media apps would show. It occurred to me that in doing so, people wouldn&amp;rsquo;t see links or formatting in the original post, since they&amp;rsquo;ll be less likely to click through. So it might be necessary to bring this formatting to the front. Not all possible formatting, mind you: probably just strong, emphasis, and links. Everything else should result with an ellipsis, encouraging the user to open the actual item.&lt;/p&gt;
&lt;p&gt;Anyway, still playing at the moment.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/11/16/its-done-cyber.html</link>
      <pubDate>Sat, 16 Nov 2024 13:43:04 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/11/16/its-done-cyber.html</guid>
      <description>&lt;p&gt;It&amp;rsquo;s done! Cyber Burger, the Pico-8 arcade game I&amp;rsquo;ve been working on for the last few months, is finished and can now be played online in a (desktop) browser. &lt;a href=&#34;https://cyberburger.lmika.app&#34;&gt;Check it out here&lt;/a&gt;.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/cyber-burger-social.png&#34; width=&#34;600&#34; height=&#34;315&#34; alt=&#34;&#34;&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/11/09/i-also-decided.html</link>
      <pubDate>Sat, 09 Nov 2024 12:44:59 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/11/09/i-also-decided.html</guid>
      <description>&lt;p&gt;I also decided to put the documentation &amp;ldquo;on-board&amp;rdquo;, as opposed to putting it on the web. Yes, it breaks from what was typical during the 8-bit gaming period, but I&amp;rsquo;ve got the space, and it makes adding illustrations easier.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/out-20241109-134707.png&#34; width=&#34;600&#34; height=&#34;537&#34; alt=&#34;Auto-generated description: Instructions for a retro-style video game involving making burgers by shooting ingredients and catching them in a basket.&#34;&gt;
&lt;p&gt;Also forces me to keep it brief, which is no bad thing.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/11/09/building-out-the.html</link>
      <pubDate>Sat, 09 Nov 2024 12:37:44 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/11/09/building-out-the.html</guid>
      <description>&lt;p&gt;Building out the meta elements of Cyber Burger, including the &amp;ldquo;menu du jour&amp;rdquo; a.k.a. the main menu. I&amp;rsquo;ve used food-service terms for the menu items to maintain the theme, but there is a button to switch them over to more conventional names should it be too unclear.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/out-20241109-132922.png&#34; width=&#34;600&#34; height=&#34;537&#34; alt=&#34;Auto-generated description: A retro-style game menu displays options like Start Shift and Employee Handbook under the title CYBER BURGER.&#34;&gt;
</description>
    </item>
    
    <item>
      <title>Weekly Update - 3 Nov 2024</title>
      <link>https://lmika.org/2024/11/03/weekly-update-nov.html</link>
      <pubDate>Sun, 03 Nov 2024 20:39:41 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/11/03/weekly-update-nov.html</guid>
      <description>&lt;p&gt;I probably should stop calling these &amp;ldquo;weekly updates,&amp;rdquo; seeing that
they come up a lot less frequently than once a week. Maybe I should
switch to something like &amp;ldquo;Sunday updates,&amp;rdquo; or maybe something closer
to what this is, which is an excuse to procrastinate by writing about
what I&amp;rsquo;ve been working on, rather than just working on it.&lt;/p&gt;
&lt;p&gt;But I&amp;rsquo;m sure you&amp;rsquo;re not interested in my willowing about the frequency
of these updates, so let&amp;rsquo;s just get straight to the meat of it.&lt;/p&gt;
&lt;h2 id=&#34;cyber-burger&#34;&gt;Cyber Burger&lt;/h2&gt;
&lt;p&gt;All the logic, graphics, and sound-effects for power-up/power-downs are
now finished. They now spawn in randomly, with a frequency and specific
types dictated by the current stage. I also added the notion of a
&amp;ldquo;milkshake bonus&amp;rdquo; which awards the player a bonus multiplier for a
short amount of time.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also made a few balancing changes around demerits. Based on my own
testing, I was pretty blasé about loosing demerits, as you could recover
a demerit every time you finish a burger. I wanted to discourage that,
so I changed things around a little. You still loose demerits if you
screw up the burger your trying to build — such as making it too high or
not catching the correct item — but you no longer recover demerits for
every burger you complete. Instead, you recover one demerit for every
$50 you&amp;rsquo;re awarded. This is now every three to four burgers, depending
on how sophisticated they are, which I hope would make loosing demerits
something the player would want to avoid.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s still the difficulty curve stuff left to do, but I think I&amp;rsquo;ll
start working on the meta elements, like the title screen, main menu and
high score tables. I can probably leave out the addition of stages and a
difficulty curve if I&amp;rsquo;m honest, but I would like to have a decent title
and menu screen.&lt;/p&gt;
&lt;p&gt;The other thing to do is write the manual. I made a start the other day,
but there&amp;rsquo;s much left to do on this front. Part of me wonders whether
it make sense adding &amp;ldquo;on-board documentation.&amp;rdquo;  But part of the fun of
using Pico-8 on this project is to imagine a time where this game came
out during the late 70&amp;rsquo;s and early 80&amp;rsquo;s and the 8-bit era of home
consoles. And &lt;em&gt;those&lt;/em&gt; games didn&amp;rsquo;t have on-board documentation. That
said, I might add a quick start guide for those that didn&amp;rsquo;t RTFM.&lt;/p&gt;
&lt;h2 id=&#34;ucl&#34;&gt;UCL&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been using that tool I&amp;rsquo;ve written for work quite often so there
was a need to add some additional features to UCL. The biggest one was
adding exceptions, but there&amp;rsquo;ve been a lot of little things like
rounding out the standard library.&lt;/p&gt;
&lt;p&gt;All the code written in a rush is starting to weigh this project down
though, and I do think I&amp;rsquo;ll need to do some refactoring to tidy things
up a little. I may need to work on documentation too, just for my own
sake more than anything else. I doubt this would be anything more than
the toy language it currently is, but it does have it&amp;rsquo;s uses, and
whenever I need to reference a built-in, I&amp;rsquo;m always going to the source
code. Which is fine, but I think I can do better on this front.&lt;/p&gt;
&lt;h2 id=&#34;other-projects&#34;&gt;Other Projects&lt;/h2&gt;
&lt;p&gt;A few other things I worked on during the last fortnight:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I spent some time last weekend playing with Htmgo by making a simple
world clock that would update every second. I ended up remaking this
as a &lt;a href=&#34;https://clocks.lmika.app/&#34;&gt;static web page&lt;/a&gt; backed by some WASM code. Knowing the time
in UTC and some American cities could come in handy for my job. At
least, that&amp;rsquo;s the theory: I haven&amp;rsquo;t had a need for it yet.&lt;/li&gt;
&lt;li&gt;I also made some changes to Nano Journal to add support for uploading
multiple attachments at once.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So that&amp;rsquo;s the update for this past fortnight.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/11/02/title-design-this.html</link>
      <pubDate>Sat, 02 Nov 2024 09:39:26 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/11/02/title-design-this.html</guid>
      <description>&lt;p&gt;Title design this morning. Trying to get as close as I can to the &lt;a href=&#34;https://indieground.net/blog/80s-fonts-roundup&#34;&gt;Cyberspace Raceway font&lt;/a&gt; as my pixel art skills will allow for.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/out-20241102-103516.png&#34; width=&#34;600&#34; height=&#34;537&#34; alt=&#34;Auto-generated description: The map editor in Pico-8 depicting a retro-style screen displays CYBER BURGER, with the toolbox showing the sprites depicting the word CYBERUG and a pixelated burger icon.&#34;&gt;
</description>
    </item>
    
    <item>
      <title>Try-Catch In UCL - Some Notes</title>
      <link>https://lmika.org/2024/10/21/trycatch-in-ucl.html</link>
      <pubDate>Mon, 21 Oct 2024 15:49:09 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/10/21/trycatch-in-ucl.html</guid>
      <description>&lt;p&gt;Stared working on a &lt;code&gt;try&lt;/code&gt; command to UCL, which can be used to trap
errors that occur within a block. This is very much inspired by
try-blocks in Java and Python, where the main block will run, and if any
error occurs, it will fall through to the catch block:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try {
  echo &amp;quot;Something bad can happen here&amp;quot;
} catch {
  echo &amp;quot;It&#39;s all right. I&#39;ll run next&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is all I&amp;rsquo;ve got working at the moment, but I want to quickly write
some notes on how I&amp;rsquo;d like this to work, lest I forget it later.&lt;/p&gt;
&lt;p&gt;First, much like everything in UCL, these blocks should return a value.
So it should be possible to do something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set myResults (try {
  result-of-something-that-can-fail
} catch {
  &amp;quot;My default&amp;quot;
})
--&amp;gt; (result of the thing)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is kind of like using &lt;code&gt;or&lt;/code&gt; in Lua to fallback to a default, just
that if the result fails with an error, the default value can be
returned from the &lt;code&gt;catch&lt;/code&gt; block. In might even be possible to simply
this further, and have &lt;code&gt;catch&lt;/code&gt; just return a value in cases where an
actual block of code is unnecessary:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set myResults (try { result-of-something-that-can-fail } catch &amp;quot;My default&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One other thing to consider is how to represent the error.  Errors are
just treated out-of-band at the moment, and are represented as regular
Go &lt;code&gt;error&lt;/code&gt; types. It might be necessary to add a new &lt;code&gt;error&lt;/code&gt; type to
UCL, so that it can be passed through to the &lt;code&gt;catch&lt;/code&gt; block for logging
or switching:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try {
  do-something
} catch { |e|
  echo (cat &amp;quot;The error is &amp;quot; $e)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This could also be used as the return value if there is no &lt;code&gt;catch&lt;/code&gt;
block:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set myResult (try { error &amp;quot;my error&amp;quot; })
--&amp;gt; error: my error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another idea I have is successive catch blocks, that would cascade one
after the other if the one before it fails:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try {
  do-something
} catch {
  this-may-fail-also
} catch {
  echo &amp;quot;Always passes&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unlike JavaScript or Python, I don&amp;rsquo;t think the idea of having &lt;code&gt;catch&lt;/code&gt;
blocks switching based on the error type would be suitable here. UCL is
dynamic in nature, and having this static type checking feels a little
wrong here. The &lt;code&gt;catch&lt;/code&gt; blocks will only act as isolated blocks of
execution, where an error would be caught and handled.&lt;/p&gt;
&lt;p&gt;Finally, there&amp;rsquo;s &lt;code&gt;finally&lt;/code&gt;, which would run regardless of which try or
catch block was executed. I think, unlike the other two blocks, that the
return value of a &lt;code&gt;finally&lt;/code&gt; block will always be swallowed. I think this
will work as the finally block should mainly be used for clean-up, and
it&amp;rsquo;s the result of the &lt;code&gt;try&lt;/code&gt; or &lt;code&gt;catch&lt;/code&gt; blocks that are more important.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set res (try {
  &amp;quot;try&amp;quot;
} catch {
  &amp;quot;catch&amp;quot;
} finally {  
  &amp;quot;finally&amp;quot;
})
--&amp;gt; &amp;quot;try&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anyway, this is the idea I have right now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update —&lt;/strong&gt; I just realised that the idea of the last successful try
block return an error, rather than letting it climb up the stack defeats
the purpose of exceptions. So having something like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try { error &amp;quot;this will fail&amp;quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Should just unroll the stack and not return an error value. Although if
there is a need to have an error value returned, then the following
should work:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try { error &amp;quot;this will fail&amp;quot; } catch { |err| $err }
--&amp;gt; error: this will fail
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Weekly Update - 20 Oct 2024</title>
      <link>https://lmika.org/2024/10/20/weekly-update-oct.html</link>
      <pubDate>Sun, 20 Oct 2024 20:57:17 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/10/20/weekly-update-oct.html</guid>
      <description>&lt;p&gt;Yeah, I know, it&amp;rsquo;s been a while… again. A lot has been happening in
life and there&amp;rsquo;ve been many days that I haven&amp;rsquo;t done any work on
anything. Things are starting to settle down now, although I am
expecting a few more bumpy days ahead, so we&amp;rsquo;ll see how we go with
project work.&lt;/p&gt;
&lt;h2 id=&#34;cyber-burger&#34;&gt;Cyber Burger&lt;/h2&gt;
&lt;p&gt;Yeah, I&amp;rsquo;m getting pretty tired of this one. I&amp;rsquo;m right in the trough of
despair here, where the initial excitement has worn off and I just want
to finish it. All the remaining work for it has been defined, which I
think helps, and now it&amp;rsquo;s just a matter of plowing through the tasks.&lt;/p&gt;
&lt;p&gt;Recent work done on this is the addition of power-ups and power-downs,
which does things like bump the remain time remaining, give the player a
&amp;ldquo;tractor beam&amp;rdquo; weapon, and clearing the play-area of items, amongst
other things. I&amp;rsquo;m about 2/3 through this one: most of the these
power-ups have been implemented, including the tractor beam. After that,
the remaining work on this project is adjusting the difficulty curve,
improving the main menu, and finally finishing off the website.&lt;/p&gt;
&lt;p&gt;Despite how fatigued I feel about this, it has been fun learning how to
build for the Pico-8. The expect fidelity of the pixel art is within my
skillset, and it&amp;rsquo;s quite impressive what it offers in terms of audio
effects. I haven&amp;rsquo;t explored the music options, and I probably won&amp;rsquo;t
for this game, but something to look at for the next one (if there will
be a next one).&lt;/p&gt;
&lt;h2 id=&#34;coasters&#34;&gt;Coasters&lt;/h2&gt;
&lt;p&gt;This was a bit of a side tangent to get my mind off working on Cyber
Burger. It was a long weekend, and I was passively scrolling through the
sample of Vintage Logos when I thought about recreating a game that was
published in the weekend newspaper we read while I was growing up.
You&amp;rsquo;ll be presented with two images and a clue, and you&amp;rsquo;d have to
guess the word or phrase these alluded to.  For example, if the clue was
&amp;ldquo;insect&amp;rdquo; and the images were:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/a893a53f97.jpg&#34;&gt;
&lt;p&gt;Then one possible answer would be &amp;ldquo;butterfly&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The newspaper used stock photos, like the example above, but I was
hoping to creates logos for fake companies as they would appear as if
printed on cardboard coasters you&amp;rsquo;d find in pubs. The idea is that a
new puzzle would be shown to the user a day, much like the word games
you find online.&lt;/p&gt;
&lt;p&gt;I coded up most of this in a single day and had a working version by the
end of the weekend. I had a very simple Go server which served up a new
puzzle every day at 00:00 UTC. The web frontend was mainly HTML, with
some JavaScript for handling the player&amp;rsquo;s guesses. The puzzles
themselves were defined in a CSV file which was imported into a Sqlite
DB on server startup. This was a strange way of doing things, and was
more of an accident as I was deploying this as a Docker container and I
didn&amp;rsquo;t setup a persistent volume that survived restarts. I had plans of
adding an admin section to the Go server so that I could design puzzles
in advanced.&lt;/p&gt;
&lt;p&gt;Fortunately I resisted this as I ended up only producing twelve puzzles.
Once the original run was completed, I basically bundled them all on a
static site. I was able to keep most of the original HTML and JavaScript
as it was, with only the Go backend being junked. &lt;a href=&#34;https://coasters.lmika.app/&#34;&gt;It&amp;rsquo;s still
online&lt;/a&gt;, and maybe I&amp;rsquo;ll come back to it in the future, maybe using
stock images in place of logos I have to create myself.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/a5bdd76798.jpg&#34;&gt;
&lt;figcaption&gt;The new home page, made after the daily puzzles were at an end.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/a01808911e.jpg&#34;&gt;
&lt;figcaption&gt;Example of one of the puzzles.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So although this was not a long lived side tangent, it was good to be
able to get something finished. I was feeling quite dispirited with
projects that I was just not finishing or releasing. Doing something
small like this, and seeing it through, helped a lot. Maybe that&amp;rsquo;s the
secret in how I can see through something from conception to completion:
get the bulk of it done quickly, deploy it as soon as you can, and keep
it small. That would limit the types of project I can deliver, but hey,
at least I would deliver them.&lt;/p&gt;
&lt;h2 id=&#34;other-stuff&#34;&gt;Other Stuff&lt;/h2&gt;
&lt;p&gt;I added a few things to Ted, my terminal CSV editor, this past week.
Like many of the other features of this project, this was driven mainly
by needs that arise at work. I finally bit the bullet and embraced the
support for headers. The first row of the CSV file is now treated as a
header and is now fixed above the column, instead of the column numbers
which were much less helpful. It should, in theory, be possible to read
a CSV file without headers, but I hardly deal with those files that
it&amp;rsquo;s probably just better to make headers the default.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/b8d691581b.jpg&#34;&gt;
&lt;figcaption&gt;TED with a very simple CSV file showing the headers.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I am also exploring ways to do more actions in bulk, such as add columns
which are derived from the value of other columns. These are all quite
experimental at the moment, and are more or less added in a quick and
dirty fashion; just the bare minimum for what I&amp;rsquo;m trying to achieve. So
not much to say here. A few other changes to other projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nano Journal now keeps the current draft post in browser local
storage, so that it&amp;rsquo;ll no longer get clobbered when you&amp;rsquo;re
redirected to the login screen when trying to post an entry. This was
a source of anxiety for me and meant I was less likely to write
something off the cuff, so I&amp;rsquo;m glad this is now fixed.&lt;/li&gt;
&lt;li&gt;Blogging Tool now has a new app which takes a clip videos produced by
Pocketcasts — which is currently generated in an Instagram-style
vertical video — and crops it as either a square or a letterbox.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/e61945c4f7.jpg&#34;&gt;
&lt;figcaption&gt;The new &#34;Post Podcast Clip&#34; app in Blogging Tools.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/6975b1efac.jpg&#34;&gt;
&lt;figcaption&gt;Also, the app list has been styled a little better. Much more interesting than the bulleted list.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So, a lot of little things done this past month. Nothing worthy of a
report on it&amp;rsquo;s own, but still, it&amp;rsquo;s something.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/10/06/ive-spent-the.html</link>
      <pubDate>Sun, 06 Oct 2024 10:14:31 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/10/06/ive-spent-the.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve spent the last week working on a small puzzle game called Coasters, where you presented with two images and a clue, and you need to guess the word or phrase. One puzzle a day, sort of like Wordle. I&amp;rsquo;ve got 10 puzzles ready to go and I may add more but no promises. &lt;a href=&#34;https://coasters.lmika.app&#34;&gt;Check it out if you like.&lt;/a&gt;&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/out-20241006-110751.png&#34; width=&#34;600&#34; height=&#34;449&#34; alt=&#34;Auto-generated description: Two coasters are shown, one with the text &#39;happiness is attractive&#39; and the other displaying the logo &#39;crust bakery&#39;, with a prompt to guess a species of bird.&#34;&gt;
</description>
    </item>
    
    <item>
      <title>Weekly Update - 22 Sept 2024</title>
      <link>https://lmika.org/2024/09/22/weekly-update-sept.html</link>
      <pubDate>Sun, 22 Sep 2024 12:23:09 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/09/22/weekly-update-sept.html</guid>
      <description>&lt;p&gt;No preface today. Let&amp;rsquo;s move on to the update.&lt;/p&gt;
&lt;h2 id=&#34;cyber-burger&#34;&gt;Cyber Burger&lt;/h2&gt;
&lt;p&gt;Cyber Burger now has sound!&lt;/p&gt;
&lt;video src=&#34;https://media.lmika.org/cyber-burger-with-sounds.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;
&lt;/video&gt;
&lt;p&gt;I started added some basic sound effects to the laser and the items
flying across the screen.  They may change, depending on how I find them
after a while, but it&amp;rsquo;s a start. I do like how Pico-8 makes these easy
to make: select a waveform, then just draw out the pitch and volume
graphically:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/0e5504a1d9.jpg&#34;&gt;
&lt;figcaption&gt;The laser fire sound effect, shown in the Pico-8 sound editor.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I also improved how items are spawned. Instead of the item types being
completely random, they&amp;rsquo;re now taken from a list, which gets refilled
and shuffled once it&amp;rsquo;s empty. This smoothes out the chance of seeing a
particular item — no more waiting around for a bun bottoms to start your
burger — while still making things random enough to be fun. The item
types are distributed evenly, so every type will show up once per cycle.
Maybe I&amp;rsquo;ll change this, but probably not, as it seems to be working
reasonably well at the moment.&lt;/p&gt;
&lt;p&gt;The Y location is now quantised a little so that items don&amp;rsquo;t completely
overlap each other by a pixel or two. They can still be located on
exactly the same Y position which… is not great — I may need to take a
look at that. But the slice spacing between each item looks good. Also,
items now spawn in from the left as well as the right.&lt;/p&gt;
&lt;p&gt;Lastly, I changed how demerits work. The previous version was not adding
any when the player made a mistake. This made them pretty useless, and
the game felt quite easy. So now, whenever the player catches an item in
the basket that they weren&amp;rsquo;t suppose to, they get a demerit. If their
burger gets too large, then get 2 demerits. To balance things out, the
player now has 6 demerit points instead of 3, and every completed burger
would remove one demerit until the player has zero again. I think this
will require some more rebalancing, but it makes play a little more
interesting.&lt;/p&gt;
&lt;p&gt;Not sure what I&amp;rsquo;ll work on next. I&amp;rsquo;ll need to finish (or redo) the
sounds, and then come up with some more tasks to do. I&amp;rsquo;m thinking
either working on the difficulty curve, making things harder as the game
progresses; adding power-ups and power-downs; or working on the start
screen and menu. More on either of these in the future.&lt;/p&gt;
&lt;h2 id=&#34;blogging-tool&#34;&gt;Blogging Tool&lt;/h2&gt;
&lt;p&gt;For my &amp;ldquo;extracurricular&amp;rdquo; activity this week, I spend some more time on
Blogging Tool. I added the notion of jobs, which are essentially
background tasks for long running processes. These can be monitored from
within the Jobs section, which is accessible from the main nav:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/5257c5a145.jpg&#34;&gt;
&lt;figcaption&gt;The new Jobs section, showing a job in progress.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/8cfa22fb9e.jpg&#34;&gt;
&lt;figcaption&gt;Once completed, jobs can link to any resource — file or gallery — that they produced.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I&amp;rsquo;m recording jobs in the Sqlite3 database, including the ones that are
currently running. Jobs have the ability to report progress by updating
the &amp;ldquo;summary&amp;rdquo; message, which would update the job record in the
database. I&amp;rsquo;m generally not too keen on having the database act as
storage for something that could be updated quite frequently, but the
alternative was keeping this progress information in memory or deploying
something like Redis, both I felt were worse. Besides, I&amp;rsquo;m curious as
to how well Sqlite3 handles status updates from a running job like this.
I&amp;rsquo;m not expecting jobs to be updating there status too rapidly anyway,
so it should be fine.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/30b822c73c.jpg&#34;&gt;
&lt;figcaption&gt;The Image App now with the Load URL and Send To Blog options.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The Image App got a few changes as well. It&amp;rsquo;s now possible to import an
image from a URL (this is done within the browser so is subject to all
the cross-origin rules that entails). I added this just so that I can
touch up my own images that I already uploaded to Micro.blog, without
having to download the image first. Speaking of, there&amp;rsquo;s now an option
to send a processed image to a blog directly from the app, saving yet
another download step. I&amp;rsquo;ve got this configured to Micro.blog but it
uses Micropub to do this so it could, in theory, be any blogging system
that supports that API.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/7752888c02.jpg&#34;&gt;
&lt;figcaption&gt;The New Upload Zip app.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/5ad41bdf78.jpg&#34;&gt;
&lt;figcaption&gt;Complete with upload progress bar for slow upload connections (like mine).&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The last thing that was added is the ability to upload a Zip file of
images to a Micropub endpoint, and start creating a gallery. This has
been a feature that I wanted for a while. It always took me ages to
prepare a photo gallery in Micro.blog. I know there are apps which help
here, but they&amp;rsquo;re for iOS and just don&amp;rsquo;t fit my workflow. Now,
whenever I want to add a photo gallery, I can select the images from
Google photos, download them as a Zip file, then directly upload them to
Micro.blog in one hit. The upload can take as long as it needs to (this
uses the new jobs system as well), and once they&amp;rsquo;re done, Blogging Tool
will create a gallery for me so I can write the captions. To be able to
do this without clicking around as much as I did is rather exciting.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/2acab5d170.jpg&#34;&gt;
&lt;figcaption&gt;This is not a new screen, but shows the gallery created once the images are uploaded.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Or at least it will be, when I find out how I can get large uploads
working. Uploading a 27 MB Zip file in development was not an issue, but
when I deployed it to my Dokku server and try it for real, I kept
getting 502 errors. I&amp;rsquo;m not sure what&amp;rsquo;s causing these yet: both Nginx
and Blogging Tool have been configured to accept files much larger than
this, and there are no logs in either to suggest a problem. There might
be a timeout somewhere that needs to be raised. In any case, hopefully
this is the last hurdle I need to clear to get this working. Anyway,
that&amp;rsquo;s all for this week. Oh, actually, one more thing: while working
on the UI for the Upload Zip screen, I found that I never got around to
changing the instructions on the Transcode Audio screen after copying
and pasting the HTML from the New Gallery screen:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/c502f4fe6c.jpg&#34;&gt;
&lt;figcaption&gt;That&#39;s not what this screen is for!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Just goes to show how little I pay attention to this copy, even while
working on my own apps. 😛&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Weekly Update - 15 Sept 2024</title>
      <link>https://lmika.org/2024/09/15/weekly-update-sept.html</link>
      <pubDate>Sun, 15 Sep 2024 10:15:41 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/09/15/weekly-update-sept.html</guid>
      <description>&lt;p&gt;Two projects to discuss this week.&lt;/p&gt;
&lt;h2 id=&#34;cyber-burger&#34;&gt;Cyber Burger&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve decided to ditch game mode A, where the player is given a series
of stages they need to clear. Instead, I&amp;rsquo;m changing this to be closer
to an old-school arcade experience. In this mode, you start the game
with a 45 second timer, and you need get as high a score as you can
before the timer runs out. Your score depends roughly on how large and
&amp;ldquo;interesting&amp;rdquo; your burger is. Every burger you make also adds 10
seconds to the clock, so the ultimate aim is to balance making
interesting burgers for a higher score, vs. trying to avoid letting the
clock run down to zero. Or at least that&amp;rsquo;s the idea. I need to do some
rebalancing, as I think the game is a little easy.&lt;/p&gt;
&lt;p&gt;I am wondering whether to award points based on how fast you make the
burger. It was originally my intention, but I&amp;rsquo;m now wondering if that
would be considered unfair, as you don&amp;rsquo;t get to choose the items flying
across the screen. I still do need to fix the random number generator
too, so as to avoid keeping the player from waiting around too long for
a bun base to start building their burger. Resolving these two things
will be my goal for the next week.&lt;/p&gt;
&lt;p&gt;This working version has been &lt;a href=&#34;https://cyberger.lmika.app&#34;&gt;pushed to the web&lt;/a&gt; for anyone to try
out. If you do, please &lt;a href=&#34;https://letterbird.co/lmika&#34;&gt;let me know&lt;/a&gt; if you have any feedback.&lt;/p&gt;
&lt;h2 id=&#34;nano-journal&#34;&gt;Nano Journal&lt;/h2&gt;
&lt;p&gt;The next project I spent some time on was Nano Journal, the web-based
journalling app &lt;del&gt;stolen from&lt;/del&gt; inspired by &lt;a href=&#34;https://kevquirk.com/blog/building-a-simple-journal&#34;&gt;Kev Quirk&amp;rsquo;s
journalling app&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I wasn&amp;rsquo;t intending to work on it this week, but I did have a motive to
do so as I was expecting to do something that I wanted to document (I
ended up not doing that thing).&lt;/p&gt;
&lt;p&gt;The biggest changes I made were adding paging and post titles, plus
making some improvements on the backend on how posts are stored and
retrieved.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/6107e3b636.jpg&#34;&gt;
&lt;figcaption&gt;Current version of the index page. Note the posts with titles, which will appear beside the date. The paperclip indicates a post with attachments (usually images).&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Setting the title is done using a slash-commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/title This is my post title
This is my post body
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also added an &lt;code&gt;/append&lt;/code&gt; slash-command to add to the latest post,
rather than create a new one. This also works for attachments too,
allowing a somewhat clunky way of adding multiple attachments to a
single post.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/98f758bc29.jpg&#34;&gt;
&lt;figcaption&gt;Viewing a post, now with paging.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I am still considering this a prototype, but I have found myself writing
here more often than Day One. I definitely need to start thinking about
the long-term storage of posts if I want to &amp;ldquo;productionise&amp;rdquo; this.
Ultimately I want to get them saved in a private Git repository. Each
post is just a markdown file stored on the file-system, which will make
this easy to do, but I’m somewhat procrastinating on relearning how to
use the various Go Git clients that are available.&lt;/p&gt;
&lt;p&gt;One other thing that I&amp;rsquo;d like to do is improve instances where a post
couldn&amp;rsquo;t be saved due to network issues. The way I&amp;rsquo;m thinking of doing
this is to store the current draft in browser local storage, at least
until it&amp;rsquo;s saved on the server. I&amp;rsquo;m also wondering if it&amp;rsquo;s worth
adding the concept of draft posts. Not sure at this stage whether that
juice is worth the squeeze, at least not yet. This &lt;em&gt;was&lt;/em&gt; meant to be a
relatively simple side-track. But I guess that&amp;rsquo;s the danger (beauty?)
of starting something and finding it useful. 🤷&lt;/p&gt;
&lt;p&gt;Anyway, that&amp;rsquo;s all for this week.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Project Updates</title>
      <link>https://lmika.org/2024/09/08/project-updates.html</link>
      <pubDate>Sun, 08 Sep 2024 17:14:51 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/09/08/project-updates.html</guid>
      <description>&lt;p&gt;Well, it&amp;rsquo;s been three weeks since my last post here, and as hard as it
was to write this update, not writing it would&amp;rsquo;ve been harder. So
let&amp;rsquo;s just skip the preamble and go straight to the update.&lt;/p&gt;
&lt;h2 id=&#34;cyber-burger-that-pico-8-game&#34;&gt;Cyber Burger (That Pico-8 Game)&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m terrible at being coy, I&amp;rsquo;ll just spill the beens. That game I&amp;rsquo;ve
been working on is call Cyber Burger. It&amp;rsquo;s based on a DOS game I saw on
YouTube, and it seemed like a fun project to try and work on, with some
tweaks to the gameplay that I think would make it more forgiving.&lt;/p&gt;
&lt;p&gt;In the weeks since my last update, I finished with the prototypes and
started working on the game itself. The initial set of art is done and a
very simple &amp;ldquo;Game A&amp;rdquo; mode has been implemented. In this mode, you are
shown a burger you&amp;rsquo;ll need to make in your basket. You do so by
shooting down the items flying across the screen and catching them in
order. When you do, you get a &amp;ldquo;tip&amp;rdquo;, which basically amounts to
points. If you make a mistake, you&amp;rsquo;re given a demerit. There are five
rounds in total, and once the round is complete, you move on to the next
one, with maybe slightly different items, or different item speeds,
etc.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/9041d44143.jpg&#34;&gt;
&lt;figcaption&gt;An example play session, with the new graphics.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I managed to make a HTML version of this which plays through round 1. I
gave it to someone at work to play test and the results were… well, they
weren&amp;rsquo;t bad but it didn&amp;rsquo;t set the world on fire.&lt;/p&gt;
&lt;p&gt;I think I&amp;rsquo;m okay with that, but I do need to keep working on it. I
think one thing that would help is adding sound. And I think it might
help me deliver this earlier if I abandoned Mode A and start working on
Mode B, which is closer to an arcade style of game that would allow for
continuous play. These two things, I&amp;rsquo;ll aim to work on this next week.&lt;/p&gt;
&lt;p&gt;Oh, and I&amp;rsquo;ll need to fix the item spawner. Waiting ages for an item you
need is no good.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re interested in giving it a try, you can do so by &lt;a href=&#34;https://cyberger.lmika.app&#34;&gt;following
this link&lt;/a&gt; (it runs in the browser). Feel free to &lt;a href=&#34;https://letterbird.co/lmika&#34;&gt;send any
feedback&lt;/a&gt; you may have.&lt;/p&gt;
&lt;h2 id=&#34;ucl&#34;&gt;UCL&lt;/h2&gt;
&lt;p&gt;The other thing I&amp;rsquo;ve been spending some time on over the last week or
so was UCL. I&amp;rsquo;ve been using that work tool which has this language
quite often recently and I&amp;rsquo;ve been running against a number of bugs and
areas where quality of life changes could be made. Just small things,
such as allowing the &lt;code&gt;foreach&lt;/code&gt; command to be called with a proc name
instead of requiring a block, much like the &lt;code&gt;map&lt;/code&gt; function. Those have
been addressed.&lt;/p&gt;
&lt;p&gt;But the biggest thing I&amp;rsquo;ve been worked on was building out the core
library. I added the following functions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The `seq` function, used for generating a sequence of integers. I
like how I built this: it&amp;rsquo;s effectively a virtual list — that can be
indexed, iterated over, or calculated the length of — but does not
take up linear space.&lt;/li&gt;
&lt;li&gt;Comparator functions, like `eq`, `ne`, `gt`, etc. plus settling
on a type system much like Python, where values are strongly type
(can&amp;rsquo;t compare ints to strings) but are also dynamic.&lt;/li&gt;
&lt;li&gt;Arithmetic functions, like `add`, `sub`, etc. which operate on
integers, and the `cat` function use to concatenate strings (these
functions do try to cohere values to the correct type)&lt;/li&gt;
&lt;li&gt;Logical functions, like `and`, `or`, and `not`.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Along with this, I&amp;rsquo;ve started working through the strings package,
which would add the various string functions you see, like trimming
whitespace, splitting, joining, etc. I&amp;rsquo;ve got trimming and converting
to upper and lower case, and my goal for next week is to add splitting
to and joining from string lists. Once that&amp;rsquo;s done I&amp;rsquo;ll probably put
this on the back-burner again so I can finish off Cyber Burger or work
on something else.&lt;/p&gt;
&lt;p&gt;Just a reminder that there&amp;rsquo;s also a &lt;a href=&#34;https://ucl.lmika.dev&#34;&gt;playground for this too&lt;/a&gt;,
although I apologise for the lack of documentation. I&amp;rsquo;ll need to get
onto that as well.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/08/22/i-enjoyed-reading.html</link>
      <pubDate>Thu, 22 Aug 2024 07:41:13 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/08/22/i-enjoyed-reading.html</guid>
      <description>&lt;p&gt;I enjoyed reading Kev Quirk&amp;rsquo;s post about &lt;a href=&#34;https://kevquirk.com/blog/building-a-simple-journal&#34;&gt;building a simple journal&lt;/a&gt;. I&amp;rsquo;m still using Day One, but I am still thinking of moving off it. So I was inspired to build a prototype similar to Kev&amp;rsquo;s, just to see if something similar works for me. Built using Go instead of PHP, but it also uses Simple CSS.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/out-20240822-073836.png&#34; width=&#34;600&#34; height=&#34;481&#34; alt=&#34;Screenshot of a journal web-page with a text box with the contents saying &#39;Thanks, Kev, for the idea&#39;.&#34;&gt;
</description>
    </item>
    
    <item>
      <title>Project Seed - A Pico-8 Prototype</title>
      <link>https://lmika.org/2024/08/18/project-seed-a.html</link>
      <pubDate>Sun, 18 Aug 2024 09:57:20 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/08/17/project-seed-a.html</guid>
      <description>&lt;p&gt;Oof, another long stretch between updates. This has not been a
productive winter.&lt;/p&gt;
&lt;p&gt;Much of the project I&amp;rsquo;ve been writing about here are, shall we say,
&amp;ldquo;on ice&amp;rdquo;. &lt;a href=&#34;https://scribbles.page/posts/categories/62&#34;&gt;UCL&lt;/a&gt; is still being used for the project it&amp;rsquo;s been
built for, but there&amp;rsquo;s been no further work done on it recently. I
think we can safely say &lt;a href=&#34;https://scribbles.page/posts/categories/217&#34;&gt;Photo Bucket&lt;/a&gt; is dead, at least for now.
&lt;a href=&#34;https://scribbles.page/posts/categories/467&#34;&gt;Blogging Tool&lt;/a&gt; and that &lt;a href=&#34;https://scribbles.page/write/5697&#34;&gt;interactive fiction&lt;/a&gt; project is still
ongoing, but both are running on a slow burn. I was hoping to get the
fiction thing done by the end of winter, but it&amp;rsquo;s likely that timeline
will slip. Maybe some time in spring.&lt;/p&gt;
&lt;p&gt;What have I been doing in the meantime? Watching &lt;a href=&#34;https://www.youtube.com/@Pixelmusement&#34;&gt;YouTube videos&lt;/a&gt; on
&lt;a href=&#34;https://www.youtube.com/@LGR&#34;&gt;old DOS games&lt;/a&gt;, actually. Hardly an activity worth writing about
here. But it did get me wanting to try working on a game again. And I
did get an idea for one while watching videos of someone going through a
&lt;a href=&#34;https://www.youtube.com/playlist?list=PLCIZNtotF3Xh0dfQjJx8XcP_73UGtjqN0&#34;&gt;collection of shovelware titles&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This project is designated the codename &amp;ldquo;Project Seed&amp;rdquo;. I&amp;rsquo;m going to
be a little cagey about the details, at least for now. But I will say
that I&amp;rsquo;m planning to use Pico-8 for this. I bought a license for Pico-8
about 2 years ago (oof, did not expect it to have been that long ago)
and I watched a few videos on how to use it, but I didn&amp;rsquo;t have a good
idea for a project then. It is a fascinating bit of software, and I know
it&amp;rsquo;s quite popular amongst hobbyists. One thing I like about it is that
the games made with it are not expected to have great art. As someone
who can&amp;rsquo;t draw to save himself, this works in my favour. So don&amp;rsquo;t
expect anything resembling Celeste from me! 😄  Pico-8 also targets
HTML5, which works for me.&lt;/p&gt;
&lt;p&gt;Anyway, I have this idea, and I thought about starting a prototype to
see how it feels. I downloaded Pico-8, spun up a new project, and
started drawing some of the graphics. I&amp;rsquo;ve got the bare minimum so far:
a user-controlled paddle, called the &amp;ldquo;basket&amp;rdquo;; a laser bullet, and a
thing that needs to be shot.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/747d295a3f.jpg&#34;&gt;
&lt;figcaption&gt;The first few sprites. Must say I really like the Pico-8 palette.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Next was coding up the Lua code. Using the Pico-8 builtin editor was
fine for a bit, but I eventually switch to Nova just for the screen
size. I am still trying to adhere to the whole retro-style approach to
Pico-8. The code I write is still bound to the 8192 token limit, and
I&amp;rsquo;m trying to avoid using too much virtual memory, capping elements to
only a handful. But, yeah, using Nova to write the logic is so much
better.&lt;/p&gt;
&lt;p&gt;Anyway, the first milestone was allowing the player to move the basket
around and shoot laser bullets. Then it was to get one of the shootable
items moving across the field. The idea is that the player will need to
fire the laser to hit the shootable item. When it&amp;rsquo;s hit, it begins to
fall, and the player needs to catch it in the
basket.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/41756d6408.jpg&#34;&gt;
&lt;figcaption&gt;Shooting the laser and catching the item in the basket.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This took about an hour or so, and already the glimpse of the core game
mechanics are starting to show through. They&amp;rsquo;re just ridiculously
primitive at this stage. I mean, the item really shouldn&amp;rsquo;t fall through
the basket like that. But given that it&amp;rsquo;s a prototype, I&amp;rsquo;m okay with
this so far.&lt;/p&gt;
&lt;p&gt;Next experiment was spawning multiple items onto the field. This got off
to an interesting start:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/c4b62eb9a4.jpg&#34;&gt;
&lt;figcaption&gt;An &#34;item&#34; train.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;But adding a bit of randomness to the Y position and the spawn delay
managed to make things a little more natural
again:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/5402c3c9f4.jpg&#34;&gt;
&lt;figcaption&gt;A more natural item spawner.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;One thing I&amp;rsquo;m considering is whether to add some randomness to the item
X velocity, and even have items move from right to left. But this will
do for now.&lt;/p&gt;
&lt;p&gt;At this stage, items were just being added to an array, which grew
without bounds, and were not being released when they left the screen.
Obviously not a good use of memory (even though this is running on an M2
Mac Mini and not retro hardware from the 1980&amp;rsquo;s, but that&amp;rsquo;s hardly the
point of this exercise). Furthermore, the player is only able to shoot
one bullet at a time, and those bullets weren&amp;rsquo;t being released either.
So I set about resolving this, trying to do so in the spirit of a Pico-8
game. I&amp;rsquo;ve setup a fixed Lua array, which will grow up to a max size,
and added a simple allocator which will search for an empty slot to put
the next item in.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;next_slot&lt;/span&gt;(tbl,n)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;\&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;#&lt;/span&gt;tbl &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; n &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;\&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;#&lt;/span&gt;tbl&lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;,n &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; tbl[i] &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; i
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This makes releasing items really easy: just set that slot to &lt;code&gt;nil&lt;/code&gt;. It
does mean that I can&amp;rsquo;t use &lt;code&gt;ipairs&lt;/code&gt; to iterate over items, though.
Instead I have to use the following construct:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i,item &lt;span style=&#34;color:#66d9ef&#34;&gt;in&lt;/span&gt; pairs(items) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; item &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &lt;span style=&#34;color:#75715e&#34;&gt;-- Do thing with item&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;s here that I wished Lua had a &lt;code&gt;continue&lt;/code&gt; statement.&lt;/p&gt;
&lt;p&gt;I used this for both the item and bullets: there can now be up to 8
items and 4 bullets on the screen at a time. After making those changes,
the prototype started to play a little better:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/7734020660.jpg&#34;&gt;
&lt;p&gt;So, a good start. But there are definitely things need to be fixed. The
basket needs to be wider, for one. It&amp;rsquo;s fine for the prototype, and
I&amp;rsquo;m okay with it the collision being pretty lenient, but it&amp;rsquo;s too
narrow to make it fun.&lt;/p&gt;
&lt;p&gt;But the biggest issue is that the collision logic sucks. Bullets are
flying through the items, and items are falling through the basket. I&amp;rsquo;m
using a point-in-rectangle approach to collision detection, with a few
probing points for each item, but they obviously need to be adjusted.&lt;/p&gt;
&lt;p&gt;So collision is what I&amp;rsquo;m hoping to work on next. More on this when I
get around to it.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Current Project Update</title>
      <link>https://lmika.org/2024/07/29/current-project-update.html</link>
      <pubDate>Mon, 29 Jul 2024 07:12:14 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/07/28/current-project-update.html</guid>
      <description>&lt;p&gt;Hmm, another long gap between posts. A little unexpected, but there&amp;rsquo;s
an explanation for this: between &lt;a href=&#34;https://lmika.org/2024/07/09/a-tour-of.html&#34;&gt;setting up Forgejo&lt;/a&gt; and making the
occasional update to Blogging Tools, I haven&amp;rsquo;t been doing any project
work. Well, at least nothing involving code. What I have been doing is
trying my hand at interactive fiction, using &lt;a href=&#34;https://evergreen.ink&#34;&gt;Evergreen&lt;/a&gt; by Big River
Games.&lt;/p&gt;
&lt;p&gt;Well, okay, it&amp;rsquo;s not completely without code: there is a bit of
JavaScript involved for powering the &amp;ldquo;interactive&amp;rdquo; logic part. But a
bulk of the effort is in writing the narrative, albeit a narrative
that&amp;rsquo;s probably closer to a video game rather than a work of pure
fiction.&lt;/p&gt;
&lt;p&gt;Why? The main reason is to try something new. I had the occasional fancy
to try my hand at fiction, like a short story or something. I have an
idea for a novel — I mean, who doesn&amp;rsquo;t? — but the idea of writing it as
a novel seems daunting at the moment (I&amp;rsquo;ve written it as short story
for NanoWriteMo. It&amp;rsquo;s sitting online as an unedited draft somewhere. I
should probably make a backup of it, actually). But the idea of writing
something as interactive fiction seemed intriguing. I was never into
text parser adventures, but I did enjoy the choose-your-own-adventure
books growing up.&lt;/p&gt;
&lt;p&gt;So what&amp;rsquo;s the story about, I hear you saying? Well, believe it or not,
it&amp;rsquo;s about gardening. Yes, something I have zero experience in. And
perhaps that&amp;rsquo;s what made it an interesting subject to explore.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been working on this for about a month now. I&amp;rsquo;m well past the
everything-is-new-and-exciting phase, and I think I just made it through
the oh-no-why-the-heck-am-I-even-doing-this pit of despair. I can see
the finish line in terms of the narrative and the logic, and all that
remains there should just be a matter of cleaning up, editing, and play
testing. The biggest thing left to do is illustrations. I have zero
artistic skills myself so I&amp;rsquo;m not quite sure what I&amp;rsquo;ll do here.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re curious about it, &lt;a href=&#34;https://greenthumbs.lmika.dev&#34;&gt;here&amp;rsquo;s a sample&lt;/a&gt;. It&amp;rsquo;s about the
first third of the story. It&amp;rsquo;s a little rough, and requires editing and
proof-reading, and illustrations. But let me know what you think.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>More Tools For Blogging Tool</title>
      <link>https://lmika.org/2024/06/23/more-tools-for.html</link>
      <pubDate>Sun, 23 Jun 2024 09:54:56 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/06/22/more-tools-for.html</guid>
      <description>&lt;p&gt;Spent the last week working on Blogging Tool. I want to get as much done
as a I can before motivation begins to wain, and it begins languishing
like every other project I&amp;rsquo;ve worked on. Not sure I can stop that, but
I think I can get the big ticket items in there so it&amp;rsquo;ll be useful to
me while I start work on something else.&lt;/p&gt;
&lt;p&gt;I do have plans for some new tools for Blogging Tool: making it easier
to make Lightbox Gallery was just the start. This last week I managed to
get two of them done, along with some cross-functional features which
should help with any other tools I make down the road.&lt;/p&gt;
&lt;h2 id=&#34;move-to-sqlite&#34;&gt;Move To Sqlite&lt;/h2&gt;
&lt;p&gt;First, a bit of infrastructure. I moved away from Rainstorm as the data
store and replaced it with Sqlite 3. I&amp;rsquo;m using &lt;a href=&#34;https://modernc.org/sqlite&#34;&gt;a version of Sqlite
3&lt;/a&gt; that doesn&amp;rsquo;t use CGO as the Docker container this app runs in
doesn&amp;rsquo;t have libc. It doesn&amp;rsquo;t have as much support out there as the
&lt;a href=&#34;https://github.com/mattn/go-sqlite3&#34;&gt;more popular Sqlite 3 client&lt;/a&gt;, but I&amp;rsquo;ve found it to work just as
well.&lt;/p&gt;
&lt;p&gt;One could argue that it would&amp;rsquo;ve been fine sticking with Rainstorm for
this. But as good as Rainstorm&amp;rsquo;s API is, the fact that it takes out a
lock on the database file is annoying. I&amp;rsquo;m running this app using
&lt;a href=&#34;https://dokku.com/&#34;&gt;Dokku&lt;/a&gt;, which takes a zero-downtime approach to deployments. This
basically means that the old and new app container are running at the
same time.  The old container doesn&amp;rsquo;t get shut down for about a minute,
and because it&amp;rsquo;s still holding the lock, I can&amp;rsquo;t use the new version
during that time as the new container cannot access the Rainstorm
database file. Fortunately, this is not an issue with Sqlite 3.&lt;/p&gt;
&lt;p&gt;It took me a couple of evenings to port the logic over, but fortunately
I did this early, while there was no production data to migrate. I&amp;rsquo;m
using &lt;a href=&#34;https://sqlc.dev/&#34;&gt;Sqlc&lt;/a&gt; for generating Go bindings from SQL statements, and a
home grown library for dealing with the schema migrations. It&amp;rsquo;s not as
easy to use as the Rainstorm API but it&amp;rsquo;ll do. I&amp;rsquo;m finding working
with raw SQL again to be quite refreshing so it may end up being better
in the long run.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/0d45955c32.jpg&#34;&gt;
&lt;figcaption&gt;Current landing page.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&#34;imaging-processing&#34;&gt;Imaging Processing&lt;/h2&gt;
&lt;p&gt;Once that&amp;rsquo;s done, I focused on adding those tools I wanted. The first
one to sit alongside the gallery tool, is something for preparing images
for publishing. This will be particularly useful for screenshots. If you
look carefully, you&amp;rsquo;d noticed that the screenshots on this site have a
slightly different shadow than the MacOS default. It&amp;rsquo;s because I
actually take a screenshot without the shadow, then use a CLI tool to
add one prior to upload. I do this because the image margins MacOS
includes with the shadow are pretty wide, which makes the actual
screenshot part smaller than I like. Using the CLI tool is fine, but
it&amp;rsquo;s not always available to me. So it seemed like a natural thing to
add to this blogging tool.&lt;/p&gt;
&lt;p&gt;So I added an image processing &amp;ldquo;app&amp;rdquo; (I&amp;rsquo;m calling these tools
&amp;ldquo;apps&amp;rdquo; to distinguish them from features that work across all of them)
which would take an image, and allows you to apply a processor on it.
You can then download the processed image and use it in whatever you
need.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/e9e1c12756.jpg&#34;&gt;
&lt;figcaption&gt;The image processing tool, being used here to get the crop right for this particular screenshot.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This is all done within the browser, using the Go code from the CLI tool
compiled to WASM. The reason for this is performance. These images can
be quite large, and I&amp;rsquo;d rather avoid the network round-trip. I&amp;rsquo;m
betting that it&amp;rsquo;ll be faster running it in the browser anyway, even if
you consider the amount of time it takes to download the WASM binary
(which is probably around a second or so).&lt;/p&gt;
&lt;p&gt;One addition I did add was to allow processors to define parameters
which are shown to the user as input fields. There&amp;rsquo;s little need for
this now — it&amp;rsquo;s just being used in a simple meme-text processor right
now — but it&amp;rsquo;s one of those features I&amp;rsquo;d like to at least get basic
support for before my interest wains. It wouldn&amp;rsquo;t be the first time I
stopped short of finishing something, thinking to my self that I&amp;rsquo;d add
what I&amp;rsquo;ll need later, then never going back to do so. That said, I do
have some ideas of processors which could use this feature for real,
which I haven&amp;rsquo;t implemented yet. More on that in the future,
maybe.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/d2238bf934.jpg&#34;&gt;
&lt;figcaption&gt;The &#34;Cheeseburger&#34; processor, and it&#39;s use of image parameters.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&#34;audio-transcoding-and-files&#34;&gt;Audio Transcoding And Files&lt;/h2&gt;
&lt;p&gt;The other one I added deals with audio transcoding. I&amp;rsquo;ve gotten into
the habit of narrating the long form posts I write. I usually use
Quicktime Player to record these, but it only exports M4A audio files
and I want to publish them as MP3s.&lt;/p&gt;
&lt;p&gt;So after recording them, I need to do a transcode. There&amp;rsquo;s an &lt;code&gt;ffmpeg&lt;/code&gt;
command line invocation I use to do this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ffmpeg -i in.m4a -c:v copy -c:a libmp3lame -q:a &lt;span style=&#34;color:#ae81ff&#34;&gt;4&lt;/span&gt; out.mp3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But I have to bring up a terminal, retrieve it from the history (while
it&amp;rsquo;s still in there), pick a filename, etc. It&amp;rsquo;s not hard to do, but
it&amp;rsquo;s a fair bit of busy work&lt;/p&gt;
&lt;p&gt;I guess now that I&amp;rsquo;ve written it here, it&amp;rsquo;ll be less work to remember.
But it&amp;rsquo;s a bit late now since I&amp;rsquo;ve added the feature to do this for
me. I&amp;rsquo;ve included &lt;a href=&#34;https://johnvansickle.com/ffmpeg&#34;&gt;a statically linked version&lt;/a&gt; of &lt;code&gt;ffmpeg&lt;/code&gt; in the
Docker container (it needs to be statically linked for the same reason
why I can&amp;rsquo;t use CGO: there&amp;rsquo;s no libc or any other shared objects) and
wrapped it around a small form where I upload my
M4A.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/441063bdcc.jpg&#34;&gt;
&lt;figcaption&gt;The simple form used to transcode an M4A file.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The transcoding is done on the server (seemed a bit much asking for this
to be done in the browser) but I&amp;rsquo;m hoping that most M4A files will be
small enough that it wouldn&amp;rsquo;t slow things down too much. The whole
process is synchronous right now, and I could&amp;rsquo;ve make the file
available then and there, but it wouldn&amp;rsquo;t be the only feature I&amp;rsquo;m
thinking of that would produced files that I&amp;rsquo;d like to do things with
later. Plus, I&amp;rsquo;d like to eventually make it asynchronous so that I
don&amp;rsquo;t have to wait for long transcodes, should there be any.&lt;/p&gt;
&lt;p&gt;So along with this feature, I added a simple file manager in which these
working files will go.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/2fec9c841e.jpg&#34;&gt;
&lt;figcaption&gt;The files list. Click the link to download the file (although this may changed to be preview in the future).&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;They&amp;rsquo;re backed by a directory running in the container with metadata
managed by Sqlite 3. It&amp;rsquo;s not a full file system — you can&amp;rsquo;t do things
like create directories, for example. Nor is it designed to be long term
storage for these files. It&amp;rsquo;s just a central place where any app can
write files out as a result of their processing. The user can download
the files, or potentially upload them to a site, then delete them. This
would be useful for processors which could take a little while to run,
or run on a regular schedule.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t have many uses for this yet, apart from the audio transcoder,
but having this cross-functional facility opens it up to features that
need something like this. It means I don&amp;rsquo;t have to hand-roll it for
each app.&lt;/p&gt;
&lt;p&gt;Anyway, that&amp;rsquo;s the current state of affairs. I have one, maybe two,
large features I&amp;rsquo;d like to work on next. I&amp;rsquo;ll write about them once
they&amp;rsquo;re done.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Blogging Gallery Tool</title>
      <link>https://lmika.org/2024/06/13/blogging-gallery-tool.html</link>
      <pubDate>Thu, 13 Jun 2024 17:19:38 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/06/13/blogging-gallery-tool.html</guid>
      <description>&lt;p&gt;Oof! It&amp;rsquo;s been a while, hasn&amp;rsquo;t it.&lt;/p&gt;
&lt;p&gt;Not sure why I expected my side-project work to continue while I&amp;rsquo;m here
in Canberra. Feels like a waste of a trip to go somewhere — well, not
&amp;ldquo;unique&amp;rdquo;, I&amp;rsquo;ve been here before; but different — and expect to spend
all your time indoors writing code. Maybe a choice I would&amp;rsquo;ve made when
I was younger, but now? Hmm, better to spend my time outdoors,
&amp;ldquo;touching grass&amp;rdquo;. So that&amp;rsquo;s what I&amp;rsquo;ve been doing.&lt;/p&gt;
&lt;p&gt;But I can&amp;rsquo;t do that all the time, and although I still have UCL (I&amp;rsquo;ve
made some small changes recently, but nothing worth writing about) and
Photo Bucket, I spent this past fortnight working on new things.&lt;/p&gt;
&lt;p&gt;The first was an aborted attempt at an RSS reader for Android that works
with Feedbin. I did get something working, but I couldn&amp;rsquo;t get it onto
my mobile, and frankly it was rather ugly. So I&amp;rsquo;ve set that idea aside
for now. Might revisit it again.&lt;/p&gt;
&lt;p&gt;But all my outdoor adventures did motivate me to actually finish
something I&amp;rsquo;ve been wanting to do for a couple of years now. For you
see, I take a lot of photos and I&amp;rsquo;d like to publish them on my
Micro.blog in the form of a &lt;a href=&#34;https://github.com/jsonbecker/plugin-glightbox&#34;&gt;GLightbox gallery&lt;/a&gt; (&lt;a href=&#34;https://lmika.org/2024/06/09/day-trip-to.html&#34;&gt;see this post&lt;/a&gt;
for an example). But making these galleries is a huge pain. Setting
aside that I always forget the short-codes to use, it&amp;rsquo;s just a lot of
work. I&amp;rsquo;m always switching back and forth between the Upload section in
Micro.blog, looking that the images I want to include, and a text file
where I&amp;rsquo;m working on the gallery markup and captions.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been wishing for some tool which would take on much of this work
for me.  I&amp;rsquo;d give it the photos, write the captions, and it would
generate the markup. I&amp;rsquo;ve had a run at building something that would do
this a few times already, including an idea for a feature in Photo
Bucket. But I couldn&amp;rsquo;t get over the amount of effort it would take to
upload, process, and store the photos. It&amp;rsquo;s not that it&amp;rsquo;d would be
hard, but it always seemed like double handling, since their ultimate
destination was Micro.blog. Plus, I was unsure as to how much effort I
wanted to put into this, and the minimum amount of effort needed to deal
with the images seemed like a bit of a hassle.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/a782f701a2.jpg&#34;&gt;
&lt;figcaption&gt;One of the earlier attempts at these. Images were hosted, and were meant to be rearranged by dragging. You can see why this didn&#39;t go anywhere.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;It turns out the answer was in front of me this whole time. The hard
part was preparing the markup so why couldn&amp;rsquo;t I build something that
simply did that? The images would already be in Micro.blog; just use
their URLs. A much simpler approach indeed.&lt;/p&gt;
&lt;p&gt;So I started working on &amp;ldquo;Blogging Tools&amp;rdquo;, a web-app that&amp;rsquo;ll handle
this part of making galleries. First, I upload the images to Micro.blog,
then I copy the image tags into to this tool:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/f84a4a09b9.jpg&#34;&gt;
&lt;figcaption&gt;Create a new gallery by pasting the image tags from Micro.blog.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The tool will parse these tags, preserving things like the &amp;ldquo;alt&amp;rdquo;
attribute, and present the images in the order they&amp;rsquo;ll appear in the
gallery, with text boxes beside each one allowing me to write the
caption.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/4985661ab9.jpg&#34;&gt;
&lt;figcaption&gt;The images get displayed alongside the captions, which you can edit on this screen.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Once I&amp;rsquo;m done, I can then &amp;ldquo;render&amp;rdquo; the gallery, which will produce
the Hugo short-codes that I can simply copy and paste into the post.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/8d01fcadfe.jpg&#34;&gt;
&lt;figcaption&gt;Listing the galleries. Here you select &#34;Render&#34; from the Action column.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/cbe8c2f4d4.jpg&#34;&gt;
&lt;figcaption&gt;Copy and paste the snippet into Micro.blog.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This took me about a few evenings of work. It&amp;rsquo;s a simple Go app, using
&lt;a href=&#34;https://gofiber.io&#34;&gt;Fiber&lt;/a&gt; and &lt;a href=&#34;https://github.com/AndersonBargas/rainstorm&#34;&gt;Rainstorm&lt;/a&gt;, running in Docker. Seeing that the image
files themselves are not managed by the tool, once I got the image
parsing and rendering done, the rest was pretty straight forward. It&amp;rsquo;s
amazing to think that removing the image handling side of things has
turned this once &amp;ldquo;sizeable&amp;rdquo; tool into something that that was quick to
build and, most importantly, finally exists. I do have more ideas for
this &amp;ldquo;Blogging Tool&amp;rdquo;. The next idea is porting various command line
tools that do simple image manipulation to WASM so I can do them in the
browser (these tools were use to crop and produce the shadow of the
screenshot in this post). I&amp;rsquo;m hoping that these would work on the iPad,
so that I can do more of the image processing there rather than give up
and go to a &amp;ldquo;real&amp;rdquo; computer. I should also talk a little about why I
chose Rainstorm over Sqlite, or whether that was a good idea. Maybe be
more on those topics later, but I&amp;rsquo;ll leave it here for now.&lt;/p&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>Bulk Image Selection</title>
      <link>https://lmika.org/2024/05/26/bulk-image-selection.html</link>
      <pubDate>Sun, 26 May 2024 09:16:51 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/05/25/bulk-image-selection.html</guid>
      <description>&lt;p&gt;Some light housekeeping first: this is the 15th post on this blog so I
thought it was time for a proper domain name. Not that buying a domain
automatically means I&amp;rsquo;ll keep at it, but it does feel like I&amp;rsquo;ve got
some momentum writing here now, so I&amp;rsquo;ll take the $24.00 USD risk. I&amp;rsquo;d
also like to organise a proper site favicon too. I&amp;rsquo;ve got some ideas
but I&amp;rsquo;ve yet to crack open Affinity Design just yet.&lt;/p&gt;
&lt;p&gt;Anyway, I&amp;rsquo;ve been spending some time on Photo Bucket on and off this
past week. I&amp;rsquo;ve fully implemented the new page model mentioned &lt;a href=&#34;https://workpad.dev/post/the-site-page-model&#34;&gt;in the
last post&lt;/a&gt;, and hooked it up to the switcher in the &amp;ldquo;Design&amp;rdquo; admin
section. I&amp;rsquo;ve also built the public gallery and gallery item page.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/91f20220ce.jpg&#34;&gt;
&lt;figcaption&gt;Example of the landing page showing galleries (yes a lot of Jeff Wayne&#39;s War of the Worlds album covers in my test images.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/3d75d375a8.jpg&#34;&gt;
&lt;figcaption&gt;Click one of the galleries to view the items within that gallery.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;They&amp;rsquo;re a little on the simplistic side. That&amp;rsquo;s partly due to my
minimalistic design sensibilities, but it&amp;rsquo;s also because I haven&amp;rsquo;t
spent a lot of time on the public pages yet. I probably shouldn&amp;rsquo;t leave
it too late, lest my impressions on how it looks drops to the point
where I loose interest in working on this again. It&amp;rsquo;s a challenge, but
I guess my counter  is that I&amp;rsquo;ll probably be spending more time in the
admin section, so as long as the experience is good enough there, I can
probably go by with a very basic public site for now (but not for ever).
Now that galleries can be shown on the landing page, I&amp;rsquo;d like to
organise another deployment so that I can start showing images in
galleries. But before I do, I&amp;rsquo;ll need an easy way to move all the
existing images into a gallery. Clicking into 25 individual images  just
to select which gallery they should belong to doesn&amp;rsquo;t sound desirable
to me. So I spent some time adding batch operations to the image admin
page. The way it works is that by pressing Shift and clicking the
images, you can now select them and perform batch operations, such as
add them to a gallery (this is the only one I have now).&lt;/p&gt;
&lt;video src=&#34;https://media.lmika.org/demo-multi-image-selection.mp4&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;
&lt;/video&gt;
&lt;p&gt;I do like how the selection indictor came out. It&amp;rsquo;s got some DaVinci
Resolve vibes (I&amp;rsquo;ve been using DaVinci Resolve recently to edit some
videos so I may have been inspired by their design language here) but I
think I might need to use another highlight colour though: I think the
black bleeds too easily into the images. Also, while I was recording the
demo, I realise I broke the ability to rearrange gallery items. I may
need to fix that before redeploying. Clicking &amp;ldquo;Gallery&amp;rdquo; brings up a
model similar to the one used in the individual image page.  It work&amp;rsquo;s
slightly differently though: instead of choosing whether the images
appear in the gallery or not, this one is used to choose which galleries
to &lt;em&gt;add&lt;/em&gt; the selected images to.&lt;br&gt;
I&amp;rsquo;m not sure that this is the best modal for this. It was quick to add,
but I get the feeling that using the same model in slightly different
ways could confuse people. So I might do something else here. An idea I
have is a modal more like the following:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/7cf6a90c1e.jpg&#34;&gt;
&lt;figcaption&gt;A better modal for managing galleries for multiple images.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The idea is that all the galleries will be listed like before, but will
have a three-segmented button to the right. The centre button will be
selected by default, and will show how many of the selected images are
currently within that particular gallery. To the left will be the option
to remove those images from the gallery, and to the right will be the
option to add all the remaining selected images to the gallery. These
are identified with the number of the selected images each gallery will
have when the user clicks &amp;ldquo;Save&amp;rdquo;: 0 for none, and the number of
selected images for all. For good measure is an option to add all the
selected images into a brand new gallery. This will require some backend
work so I haven&amp;rsquo;t started work on this yet. Not sure if this too will
be a bit confusing: may need some additional text explaining how it all
works. I&amp;rsquo;m hoping that users would recognise it as operating similar to
the Gallery model for a single image.&lt;/p&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>The Site Page Model</title>
      <link>https://lmika.org/2024/05/19/the-site-page.html</link>
      <pubDate>Sun, 19 May 2024 10:00:11 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/05/19/the-site-page.html</guid>
      <description>&lt;p&gt;I opened up Photo Bucket this morning and found a bunch of commits
involving pages. I had no idea why I added them, until I launched it and
started poking around the admin section. I tried a toggle on the Design
page which controlled whether the landing page showed a list of photos
or galleries, and after finding that it wasn&amp;rsquo;t connected to anything,
it all came flooding back to me. So while what I&amp;rsquo;m going to describe
here isn&amp;rsquo;t fully implemented yet, I decided to write it down before I
forget it again.&lt;/p&gt;
&lt;p&gt;So here is where the story continues. Now that galleries have been added
to the model, I want to make them available on the public site. For the
first cut, I&amp;rsquo;m hoping to allow the admin (i.e. the site owner) the
ability to switch the landing page between a grid of photos or a grid of
galleries. This is that single toggle on the &amp;ldquo;Design&amp;rdquo; page I was
talking about earlier:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/c682b8290c.jpg&#34;&gt;
&lt;figcaption&gt;The Design section showing the new &#34;Show Gallers on Homepage&#34; toggle.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Oh, BTW: I finally got around to highlighting the active section in the
admin screen. I probably should&amp;rsquo;ve done this earlier, as deferring
these &amp;ldquo;unnecessary aesthetics tasks&amp;rdquo;  does affect how it feels to use
this — and whether or not I&amp;rsquo;m likely to continue working on it.&lt;/p&gt;
&lt;p&gt;Anyway, if this was before &lt;a href=&#34;https://workpad.lmika.org/post/imports-and-the-new-model&#34;&gt;the new model&lt;/a&gt; I would&amp;rsquo;ve implemented
this as a flag on the &lt;code&gt;Design&lt;/code&gt; model. But I&amp;rsquo;d like to start actually
building out how pages on the site are to be modelled. What I&amp;rsquo;m
thinking is an architecture that looks a little like the
following:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/1ad5f4080f.jpg&#34;&gt;
&lt;figcaption&gt;New architecture, with the new &#34;Pages&#34; model.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The site content will be encoded using a new &lt;code&gt;Page&lt;/code&gt; model. These Pages
will be used to define the contents of the landing page (each site will
have at least this page by default) along with any additional pages the
user would like to add. Galleries and photos will automatically have
their own pages, and will not need to have any specific Page models to
be present on the site. How these will look, plus the properties and
stylings of the site itself, will be dictated by the &lt;code&gt;Design&lt;/code&gt; model.&lt;/p&gt;
&lt;p&gt;Each Page instance will have the following properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Slug — the path the page is available on. For the landing page, this
will be `/`.&lt;/li&gt;
&lt;li&gt;Type — what the page will contain.&lt;/li&gt;
&lt;li&gt;other properties like title, description, etc. which have yet to be
defined.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &amp;ldquo;page type&amp;rdquo; is probably the most important property of a Page, as
it will dictate what the contents of the page will be comprised of.  The
following page types will be supported at first:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Photos — grid of all the photos managed on the site.&lt;/li&gt;
&lt;li&gt;Galleries — grid of all the galleries managed on the site.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The user will probably not have much control over how these pages will
look, apart from styling, footers, etc. which will live on the &lt;code&gt;Design&lt;/code&gt;
model. But I&amp;rsquo;m also thinking of adding a page type which would just
produce a standard HTML page from a Markdown body. This could be useful
for About pages or anything else the user may want to add to their site.
I haven&amp;rsquo;t thought about navigation but I think choosing whether to
include the page on the site&amp;rsquo;s nav bar would be another useful
property.&lt;/p&gt;
&lt;p&gt;The result would be a sitemap that could look a little like the
following, where all the user defined pages reference automatically
created pages:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/fe80e653d8.jpg&#34;&gt;
&lt;figcaption&gt;Example sitemap, with the Page instances with solid boarders reference automatically define pages with dotted boarders.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;And this is what that single toggle should do. It should change the page
type of the landing page between photo list and gallery list.&lt;/p&gt;
&lt;p&gt;As you can probably guess, there&amp;rsquo;s currently no way to add additional
pages at the moment. But I&amp;rsquo;m doing this work now so that it should be
easier to do later.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Indexing In UCL</title>
      <link>https://lmika.org/2024/05/10/indexing-in-ucl.html</link>
      <pubDate>Fri, 10 May 2024 11:09:46 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/05/10/indexing-in-ucl.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been thinking a little about how to support indexing in UCL, as in
getting elements from a list or keyed values from a map.  There already
exists an &lt;code&gt;index&lt;/code&gt; builtin that does this, but I&amp;rsquo;m wondering if this can
be, or even should be, supported in the language itself.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve reserved &lt;code&gt;.&lt;/code&gt; for this, and it&amp;rsquo;ll be relatively easy to make use
of it to get map fields. But I do have some concerns with supporting
list element dereferencing using square brackets. The big one being that
if I were to use square brackets the same way that many other languages
do, I suspect (although I haven&amp;rsquo;t confirmed) that it could lead to the
parser treating them as two separate list literals. This is because the
scanner ignores whitespace, and there&amp;rsquo;s no other syntactic indicators
to separate arguments to proc calls, like commas:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo $x[4]      --&amp;gt; echo $x [4]
echo [1 2 3][2] --&amp;gt; echo [1 2 3] [2]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So I&amp;rsquo;m not sure what to do here. I&amp;rsquo;d like to add support for &lt;code&gt;.&lt;/code&gt; for
map fields but it feels strange doing that just that and having nothing
for list elements.&lt;/p&gt;
&lt;p&gt;I can think of three ways to address this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do Nothing&lt;/strong&gt; — the first option is easy: don&amp;rsquo;t add any new syntax to
the language and just rely on the &lt;code&gt;index&lt;/code&gt; builtin. TCL does with
&lt;a href=&#34;https://www.tcl.tk/man/tcl8.4/TclCmd/lindex.htm&#34;&gt;lindex&lt;/a&gt;, as does Lisp with &lt;a href=&#34;https://www.lispworks.com/documentation/HyperSpec/Body/f_nth.htm#nth&#34;&gt;nth&lt;/a&gt;, so I&amp;rsquo;ll be in good company
here.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use Only The Dot&lt;/strong&gt; — the second option is to add support for the dot
and not the square brackets. This is what the Go templating language
does for keys of maps or structs fields. They also have an &lt;code&gt;index&lt;/code&gt;
builtin too, which will work with slice elements.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d probably do something similar but I may extend it to support index
elements. Getting the value of a field would be what you&amp;rsquo;d expect, but
to get the element of a list, the construct &lt;code&gt;.(x)&lt;/code&gt; can be used:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo $x.hello     \# returns the &amp;quot;hello&amp;quot; field
echo $x.(4)       \# returns the forth element of a list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One benefit of this could be that the &lt;code&gt;.(x)&lt;/code&gt; construct would itself be a
pipeline, meaning that string and calculated values could be used as
well:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo $x.(&amp;quot;hello&amp;quot;)
echo $x.($key)
echo $x.([1 2 3] | len)
echo $x.(&amp;quot;hello&amp;quot; | toUpper)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I can probably get away with supporting this without changing the
scanner or compromising the language design too much. It would be nice
to add support for ditching the dot completely when using the
parenthesis, a.la. BASIC, but I&amp;rsquo;d probably run into the same issues as
with the square brackets if I did, so I think that&amp;rsquo;s out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use Parenthesis To Be Explicit&lt;/strong&gt; — the last option is to use square
brackets, and modify the grammar slightly to only allow the use of
suffix expansion within parenthesis. That way, if you&amp;rsquo;d want to pass a
list element as an argument, you have to use parenthesis:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo ($x[4])       \# forth element of $x
echo $x[4]         \# $x, along with a list containing &amp;quot;4&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is what you&amp;rsquo;d see in more functional languages like Elm and I
think Haskell. I&amp;rsquo;ll have  see whether this could work with changes to
the scanner and parser if I were to go with this option. I think it may
be achievable, although I&amp;rsquo;m not sure how.&lt;/p&gt;
&lt;p&gt;An alternative way might be to go the other way, and modify the grammar
rules so that the square brackets would bind closer to the list, which
would mean that separate arguments involving square brackets would need
to be in parenthesis:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo $x[4]         \# forth element of $x
echo $x ([4])      \# $x, along with a list containing &amp;quot;4&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or I could modify the scanner to recognise whitespace characters and use
that as a guide to determine whether square brackets following a value.
At least one space means the square bracket represent a element suffix,
and zero mean two separate values.&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s where I am at the moment. I guess it all comes down to what
works best for the language as whole. I can live with option one but it
would be nice to have the syntax. I rather not go with option three as
I&amp;rsquo;d like to keep the parser simple (I rather not add to all the
new-line complexities I&amp;rsquo;ve have already).&lt;/p&gt;
&lt;p&gt;Option two would probably be the least compromising to the design as a
whole, even if the aesthetics are a bit strange. I can probably get use
to them though, and I do like the idea of index elements being pipelines
themselves. I may give option two a try, and see how it goes.&lt;/p&gt;
&lt;p&gt;Anyway, more on this later.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Tape Playback Site</title>
      <link>https://lmika.org/2024/05/05/tape-playback-site.html</link>
      <pubDate>Sun, 05 May 2024 22:14:02 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/05/05/tape-playback-site.html</guid>
      <description>&lt;p&gt;Thought I&amp;rsquo;d take a little break from UCL today.&lt;/p&gt;
&lt;p&gt;Mum found a collection of old cassette tapes of us when we were kids,
making and recording songs and radio shows. I&amp;rsquo;ve been digitising them
over the last few weeks, and today the first recorded cassette was ready
to share with the family.&lt;/p&gt;
&lt;p&gt;I suppose I could&amp;rsquo;ve just given them raw MP3 files, but I wanted to
record each cassette as two large files — one per side — so as to not
loose much of the various crackles and clatters made when the tape
recorder was stopped and started. But I did want to catalogue the more
interesting points in the recording, and it would&amp;rsquo;ve been a bit &amp;ldquo;meh&amp;rdquo;
simply giving them to others as one long list of timestamps (simulating
the rewind/fast-forward seeking action would&amp;rsquo;ve been a step too far).&lt;/p&gt;
&lt;p&gt;Plus, simply emailing MP3 files wasn&amp;rsquo;t nearly as interesting as what I
did do, which was  to put together a private site where others could
browse and play the recorded tapes:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/6b3473fe29.jpg&#34;&gt;
&lt;figcaption&gt;The landing page, listing the available tapes (of which there&#39;s only one right now.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/b7f3f993a8.jpg&#34;&gt;
&lt;figcaption&gt;Playback of a tape side, with chapter links for seeking.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The site is not much to talk about — it&amp;rsquo;s a Hugo site using the
&lt;a href=&#34;https://themes.gohugo.io/themes/mainroad/&#34;&gt;Mainroad theme&lt;/a&gt; and deployed to Netlify. There is some JavaScript
that moves the playhead when a chapter link is clicked, but the rest is
just HTML and CSS. But I did want to talk about how I got the audio
files into Netlify. I wanted to use `git lfs` for this and have
Netlify fetch them when building the site. Netlify doesn&amp;rsquo;t do this by
default, and I get the sense that Netlify&amp;rsquo;s support for LFS is somewhat
deprecated. Nevertheless, I gave it a try by adding an explicit `git
lfs` step in the build to fetch the audio files. And it could&amp;rsquo;ve been
that I was using the LFS command incorrectly, or maybe it was invoked at
the wrong time. But whatever the reason, the command errored out and the
audio files didn&amp;rsquo;t get pulled. I tried a few more times, and I probably
could&amp;rsquo;ve got it working if I stuck with it, but all those deprecation
warnings in Netlify&amp;rsquo;s documentation gave me pause.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/33b0e90cfa.jpg&#34;&gt;
&lt;p&gt;So what I ended up doing was turning off builds in Netlify and using a
Github Action which built the Hugo site and publish it to Netlify using
the CLI tool. Here&amp;rsquo;s the Github Action in full:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Publish to Netify&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;push&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;branches&lt;/span&gt;: [&lt;span style=&#34;color:#ae81ff&#34;&gt;main]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;jobs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;build&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Build&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;runs-on&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;ubuntu-latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#f92672&#34;&gt;uses&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;actions/checkout@v4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;submodules&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;fetch-depth&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;lfs&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Setup Hugo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;uses&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;peaceiris/actions-hugo@v3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;hugo-version&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;0.119.0&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Build Site&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;run&lt;/span&gt;: |&lt;span style=&#34;color:#e6db74&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;          npm install
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;          hugo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Deploy&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;env&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;NETLIFY_SITE_ID&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;${{ secrets.NETLIFY_SITE_ID }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;NETLIFY_AUTH_TOKEN&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;${{ secrets.NETLIFY_AUTH_TOKEN }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;run&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;|          &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#ae81ff&#34;&gt;netlify deploy --dir=public --prod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This ended up working quite well: the audio files made it to Netlify and
were playable on the site. The builds are also quite fast; around 55
seconds (an earlier version involved building Hugo from source, which
took 5 minutes). So for anyone else interested in trying to serve LFS
files via Netlify, maybe try turning off the builds and going straight
to using Github Action and the CLI tool. That is… if you can swallow the
price of LFS storage in Github. Oof! A little pricy. Might be that I&amp;rsquo;ll
need to use something else for the audio files.&lt;/p&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>UCL: Brief Integration Update and Modules</title>
      <link>https://lmika.org/2024/05/04/ucl-brief-integration.html</link>
      <pubDate>Sat, 04 May 2024 21:14:27 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/05/04/ucl-brief-integration.html</guid>
      <description>&lt;p&gt;A brief update of where I am with UCL and integrating it into
Dynamo-browse. I did managed to get it integrated, and it&amp;rsquo;s now serving
as the interpreter of commands entered in during a session.&lt;/p&gt;
&lt;p&gt;It works… okay. I decided to avoid all the complexities I mentioned in
the last post — all that about continuations, etc. — and simply kept the
commands returning &lt;code&gt;tea.Msg&lt;/code&gt; values. The original idea was to have the
commands return usable values if they were invoked in a non-interactive
manner. For example, the &lt;code&gt;table&lt;/code&gt; command invoked in an interactive
session will bring up the table picker for the user to select the table.
But when invoked as part of a call to another command, maybe it would
return the current table name as a string, or something.&lt;/p&gt;
&lt;p&gt;But I decided to ignore all that and simply kept the commands as they
are. Maybe I&amp;rsquo;ll add support for this in a few commands down the line?
We&amp;rsquo;ll see. I guess it depends on whether it&amp;rsquo;s necessary.&lt;/p&gt;
&lt;p&gt;Which brings me up to why this is only working &amp;ldquo;okay&amp;rdquo; at the moment.
Some commands return a &lt;code&gt;tea.Msg&lt;/code&gt; which ask for some input from the user.
The &lt;code&gt;table&lt;/code&gt; command is one; another is &lt;code&gt;set-attr&lt;/code&gt;, which prompts the
user to enter an attribute value. These are implemented as a message
which commands the UI to go into an &amp;ldquo;input mode&amp;rdquo;, and will invoke a
callback on the message when the input is entered.&lt;/p&gt;
&lt;p&gt;This is not an issue for single commands, but it becomes one when you
start entering multiple commands that prompt for input, such as two
&lt;code&gt;set-attr&lt;/code&gt; calls:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set-attr this -S ; set-attr that -S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What happens is that two messages to show the prompt are sent, but only
one of them is shown to the user, while the other is simply swallowed.&lt;/p&gt;
&lt;p&gt;Fixing this would require some re-engineering, either with how the
controllers returning these messages work, or the command handlers
themselves. I can probably live with this limitation for now — other
than this, the UCL integration is working well — but I may need to
revisit this down the line.&lt;/p&gt;
&lt;h2 id=&#34;modules&#34;&gt;Modules&lt;/h2&gt;
&lt;p&gt;As for UCL itself, I&amp;rsquo;ve started working on the builtins. I&amp;rsquo;m planning
to have a small set of core builtins for the most common stuff, and the
rest implemented in the form of &amp;ldquo;modules&amp;rdquo;. The idea is that the core
will most likely be available all the time, but the modules can be
turned on and off by the language embedder based on what they need or
are comfortable having.&lt;/p&gt;
&lt;p&gt;Each module is namespaces with a prefix, such as &lt;code&gt;os&lt;/code&gt; for operating
system operations, or &lt;code&gt;fs&lt;/code&gt; for file-system operations. I&amp;rsquo;ve chosen the
colon as the namespace separator, mainly so I can reserve the dot for
field dereferencing, but also because I think TCL uses the colon as a
namespace separator as well (I think I saw it in some sample code). The
first implementation of this was simply adding the colon to the list of
characters that make up the IDENT token. This broke the parser as the
colon is also use as the map key/value separator, and the parser
couldn&amp;rsquo;t resolve maps anymore. I had to extend the &amp;ldquo;indent&amp;rdquo; parse
rule to support multiple IDENT tokens separated by colons. The module
builtins are simply added to the environment with there fully-qualified
name, complete prefix and colon, and invoking them with one of these
idents will just &amp;ldquo;flatten&amp;rdquo; all these colon-separated tokens into a
single string. Not sophisticated, but it&amp;rsquo;ll work for now.&lt;/p&gt;
&lt;p&gt;There aren&amp;rsquo;t many builtins for these modules at the moment: just a few
for reading environment variables and getting files as list of strings.
Dynamo-browse is already using this in a feature branch, and it&amp;rsquo;s
allows me to finally add a long-standing feature I&amp;rsquo;ve been meaning to
add for a while: automatically enabling read-only mode when accessing
DynamoDB tables in production. With modules, this construct looks a
little like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (eq (os:env &amp;quot;ENV&amp;quot;) &amp;quot;prod&amp;quot;) {
    set-opt ro
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It would&amp;rsquo;ve been possible to do this with the scripting language
already used by Dynamo-browse. But this is the motivation of integrating
UCL: it makes these sorts of constructs much easier to do, almost as one
would do writing a shell-script over something in C.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>UCL: Breaking And Continuation</title>
      <link>https://lmika.org/2024/05/01/ucl-breaking-and.html</link>
      <pubDate>Wed, 01 May 2024 08:01:36 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/04/30/ucl-breaking-and.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve started trying to integrate UCL into a second tool: Dynamo Browse.
And so far it&amp;rsquo;s proving to be a little difficult. The problem is that
this will be replacing a dumb string splitter, with command handlers
that are currently returning a &lt;a href=&#34;https://pkg.go.dev/github.com/charmbracelet/bubbletea#Msg&#34;&gt;tea.Msg&lt;/a&gt; type that change the UI in
some way.&lt;/p&gt;
&lt;p&gt;UCL builtin handlers return a &lt;code&gt;interface{}&lt;/code&gt; result, or an &lt;code&gt;error&lt;/code&gt;
result, so there&amp;rsquo;s no reason why this wouldn&amp;rsquo;t work. But &lt;code&gt;tea.Msg&lt;/code&gt; is
also an &lt;code&gt;interface{}&lt;/code&gt; types, so it will be difficult to tell a UI
message apart from a result that&amp;rsquo;s usable as data.&lt;/p&gt;
&lt;p&gt;This is a Dynamo Browse problem, but it&amp;rsquo;s still a problem I&amp;rsquo;ll need to
solve. It might be that I&amp;rsquo;ll need to return &lt;a href=&#34;https://pkg.go.dev/github.com/charmbracelet/bubbletea#Cmd&#34;&gt;tea.Cmd&lt;/a&gt; types — which
are functions returning &lt;code&gt;tea.Msg&lt;/code&gt; — and have the UCL caller detect these
and dispatch them when they&amp;rsquo;re returned. That&amp;rsquo;s a lot of function
closures, but it might be the only way around this (well, the
alternative is returning an interface type with a method that returns a
&lt;code&gt;tea.Msg&lt;/code&gt;, but that&amp;rsquo;ll mean a lot more types than I currently have).&lt;/p&gt;
&lt;p&gt;Anyway, more on this in the future I&amp;rsquo;m sure.&lt;/p&gt;
&lt;h2 id=&#34;break-continue-return&#34;&gt;Break, Continue, Return&lt;/h2&gt;
&lt;p&gt;As for language features, I realised that I never had anything to exit
early from a loop or proc. So I added &lt;code&gt;break&lt;/code&gt;, &lt;code&gt;continue&lt;/code&gt;, and &lt;code&gt;return&lt;/code&gt;
commands. They&amp;rsquo;re pretty much what you&amp;rsquo;d expect, except that &lt;code&gt;break&lt;/code&gt;
can optionally return a value, which will be used as the resulting value
of the &lt;code&gt;foreach&lt;/code&gt; loop that contains it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo (foreach [5 4 3 2 1] { |n|
  echo $n
  if (eq $n 3) {
    break &amp;quot;abort&amp;quot;
  }
})
--&amp;gt; 5
--&amp;gt; 4
--&amp;gt; 3
--&amp;gt; abort
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are implemented as error types under the hood. For example,
&lt;code&gt;break&lt;/code&gt; will return an &lt;code&gt;errBreak&lt;/code&gt; type, which will flow up the chain
until it is handled by the &lt;code&gt;foreach&lt;/code&gt; command (&lt;code&gt;continue&lt;/code&gt; is also an
&lt;code&gt;errBreak&lt;/code&gt; with a flag indicating that it&amp;rsquo;s a continue). Similarly,
&lt;code&gt;return&lt;/code&gt; will return an &lt;code&gt;errReturn&lt;/code&gt; type that is handled by the &lt;code&gt;proc&lt;/code&gt;
object.&lt;/p&gt;
&lt;p&gt;This fits quite naturally with how the scripts are run. All I&amp;rsquo;m doing
is walking the tree, calling each AST node as a separate function call
and expecting it to return a result or an error. If an error is return,
the function bails, effectively unrolling the stack until the error is
handled or it&amp;rsquo;s returned as part of the call to &lt;code&gt;Eval()&lt;/code&gt;. So leveraging
this stack unroll process already in place makes sense to me.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure if this is considered idiomatic Go. I get the impression
that using error types to handle flow control outside of adverse
conditions is frowned upon. This reminds me of all the arguments against
using expressions for flow control in Java. Those arguments are good
ones: following executions between &lt;code&gt;try&lt;/code&gt; and &lt;code&gt;catch&lt;/code&gt; makes little sense
when the flow can be explained more clearly with an &lt;code&gt;if&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But I&amp;rsquo;m going to defend my use of errors here. Like most Go projects,
the code is already littered with all the &lt;code&gt;if err != nil { return err }&lt;/code&gt;
to exit early when a non-nil error is returned. And since Go developers
preach the idea of errors simply being values, why not use errors here
to unroll the stack? It&amp;rsquo;s better than the alternatives: such as
detecting a sentinel result type or adding a third return value which
will just be yet another &lt;code&gt;if bla { return res }&lt;/code&gt; clause.&lt;/p&gt;
&lt;h2 id=&#34;continuations&#34;&gt;Continuations&lt;/h2&gt;
&lt;p&gt;Now, an idea is brewing for a feature I&amp;rsquo;m calling &amp;ldquo;continuations&amp;rdquo;
that might be quite difficult to implement. I&amp;rsquo;d like to provide a way
for a user builtin to take a snapshot of the call stack, and resume
execution from that point at a later time.&lt;/p&gt;
&lt;p&gt;The reason for this is that I&amp;rsquo;d like all the asynchronous operations to
be transparent to the UCL user. Consider a UCL script with a &lt;code&gt;sleep&lt;/code&gt;
command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;Wait here&amp;quot;
sleep 5
echo &amp;quot;Ok, ready&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sleep&lt;/code&gt; could simply be a call to &lt;code&gt;time.Sleep()&lt;/code&gt; but say you&amp;rsquo;re running
this as part of an event loop, and you&amp;rsquo;d prefer to do something like
setup a timer instead of blocking the thread. You may want to hide this
from the UCL script author, so they don&amp;rsquo;t need to worry about
callbacks.&lt;/p&gt;
&lt;p&gt;Ideally, this can be implemented by the builtin using a construct
similar to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func sleep(ctx context.Context, arg ucl.CallArgs) (any, error) {
  var secs int
  if err := arg.Bind(&amp;amp;secs); err != nil {
    return err
  }

  // Save the execution stack
  continuation := args.Continuation()

  // Schedule the sleep callback
  go func() {
    &amp;lt;- time.After(secs * time.Seconds)

    // Resume execution later, yielding `secs` as the return value
    // of the `sleep` call. This will run the &amp;quot;ok, ready&amp;quot; echo call
    continuation(ctx, secs)
  })()

  // Halt execution now
  return nil, ucl.ErrHalt
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The only trouble is, I&amp;rsquo;ve got no idea how I&amp;rsquo;m going to do this. As
mentioned above, UCL executes the script by walking the parse tree with
normal Go function calls. I don&amp;rsquo;t want to be in a position to create a
snapshot of the Go call stack. That a little too low level for what I
want to achieve here.&lt;/p&gt;
&lt;p&gt;I suppose I could store the visited nodes in a list when the &lt;code&gt;ErrHalt&lt;/code&gt;
is raised; or maybe replace the Go call stack with an in memory stack,
with AST node handlers being pushed and popped as the script runs. But
I&amp;rsquo;m not sure this will work either. It would require a significant
amount of reengineering, which I&amp;rsquo;m sure will be technically
interesting, but will take a fair bit of time. And how is this to work
if a continuation is made in a builtin that&amp;rsquo;s being called from another
builtin? What should happen if I were to run &lt;code&gt;sleep&lt;/code&gt; within a &lt;code&gt;map&lt;/code&gt;, for
example?&lt;/p&gt;
&lt;p&gt;So it might be that I&amp;rsquo;ll have to use something else here. I could
potentially do something using Goroutines: the script is executed on
Goroutine and &lt;code&gt;args.Continuation()&lt;/code&gt; does something like pauses it on a
channel. How that would work with a builtin handler requesting the
continuation not being paused themselves I&amp;rsquo;m not so sure. Maybe the
handlers could be dispatched on a separate Goroutine as well?&lt;/p&gt;
&lt;p&gt;A simpler approach might be to just offload this to the UCL user, and
have them run &lt;code&gt;Eval&lt;/code&gt; on a separate Goroutine and simply sleeping the
thread. Callbacks that need input from outside could simply be sent
using channels passed via the &lt;code&gt;context.Context&lt;/code&gt;. At least that&amp;rsquo;ll lean
into Go&amp;rsquo;s first party support for synchronisation, which is arguably a
good thing.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>UCL: The Simplifications Paid Off</title>
      <link>https://lmika.org/2024/04/26/ucl-the-simplifications.html</link>
      <pubDate>Fri, 26 Apr 2024 11:13:49 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/04/26/ucl-the-simplifications.html</guid>
      <description>&lt;p&gt;The UCL simplifications have been implemented, and they seem to be
largely successful.&lt;/p&gt;
&lt;p&gt;Ripped out all the streaming types, and changed pipes to simply pass the
result of the left command as first argument of the right.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;Hello&amp;quot; | echo &amp;quot;, world&amp;quot;
--&amp;gt; &amp;quot;Hello, world&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This has dramatically improved the use of pipes. Previously, pipes could
only be used to connect streams. But now, with pretty much anything
flowing through a pipe, that list of commands has extended to pretty
much every builtins and user-defined procs. Furthermore, a command no
longer needs to know that it&amp;rsquo;s being used in a pipeline: whatever flows
through the pipe is passed transparently via the first argument to the
function call. This has made pipes more useful, and usable in more
situations.&lt;/p&gt;
&lt;p&gt;Macros can still know whether there exist a pipe argument, which can
make for some interesting constructs. Consider this variant of the
&lt;code&gt;foreach&lt;/code&gt; macro, which can &amp;ldquo;hang off&amp;rdquo; the end of a pipe:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[&amp;quot;1&amp;quot; &amp;quot;2&amp;quot; &amp;quot;3&amp;quot;] | foreach { |x| echo $x }
--&amp;gt; 1
--&amp;gt; 2
--&amp;gt; 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not sure if this variant is useful, but I think it could be. It seems
like a natural way to iterate items passed through the pipe. I&amp;rsquo;m
wondering if this could extend to the &lt;code&gt;if&lt;/code&gt; macro as well, but that
variant might not be as natural to read.&lt;/p&gt;
&lt;p&gt;Another simplification was changing the &lt;code&gt;map&lt;/code&gt; builtin to accept
anonymous blocks, as well as an &amp;ldquo;invokable&amp;rdquo; commands by name.
Naturally, this also works with pipes too:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[a b c] | map { |x| toUpper $x }
--&amp;gt; [A B C]

[a b c] | map toUpper
--&amp;gt; [A B C]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As for other language features, I finally got around to adding support
for integer literals. They look pretty much how you expect:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set n 123
echo $n
--&amp;gt; 123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One side effect of this is that an identifier can no longer start with a
dash followed by a digit, as that would be parsed as the start of a
negative integer. This probably isn&amp;rsquo;t a huge deal, but it could affect
command switches, which are essentially just identifiers that start with
a dash.&lt;/p&gt;
&lt;p&gt;Most of the other work done was behind the scenes trying to make UCL
easier to embed. I added the notion of &amp;ldquo;listable&amp;rdquo; and &amp;ldquo;hashable&amp;rdquo;
proxies objects, which allow the UCL user to treat a Go slice or a Go
struct as a list or hash respectively, without the embedder doing
anything other than return them from a function (I&amp;rsquo;ve yet to add this
support to maps just yet).&lt;/p&gt;
&lt;p&gt;A lot of the native API is still a huge mess, and I really need to tidy
it up before I&amp;rsquo;d be comfortable opening the source. Given that the
language is pretty featureful now to be useful, I&amp;rsquo;ll probably start
working on this next. Plus adding builtins. Really need to start adding
useful builtins.&lt;/p&gt;
&lt;p&gt;Anyway, more to come on this topic I&amp;rsquo;m sure.&lt;/p&gt;
&lt;p&gt;Oh, one last thing: I&amp;rsquo;ve put together an online playground where you
can try the language out in the browser. It&amp;rsquo;s basically a WASM build of
the language running in a &lt;a href=&#34;https://xtermjs.org&#34;&gt;JavaScript terminal emulator&lt;/a&gt;. It was a
little bit of a rush job and there&amp;rsquo;s no reason for building this other
than it being a fun little thing to do.&lt;/p&gt;
&lt;p&gt;You can &lt;a href=&#34;https://ucl.lmika.dev/&#34;&gt;try it out here&lt;/a&gt;, if you&amp;rsquo;re curious.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Simplifying UCL</title>
      <link>https://lmika.org/2024/04/23/simplifying-ucl.html</link>
      <pubDate>Tue, 23 Apr 2024 08:21:44 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/04/22/simplifying-ucl.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been using UCL for several days now in that work tool I mentioned,
and I&amp;rsquo;m wondering if the technical challenge that comes of making a
featureful language is crowding out what I set out to do: making a
useful command language that is easy to embed.&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;m thinking of making some simplifications.&lt;/p&gt;
&lt;p&gt;The first is to expand the possible use of pipes. To date, the only
thing that can travel through pipes are streams. But many of the
commands I&amp;rsquo;ve been adding simply return slices. This is probably
because there&amp;rsquo;s currently no &amp;ldquo;stream&amp;rdquo; type available to the embedder,
but even if there was, I&amp;rsquo;m wondering if it make sense to allow the
embedder to pass slices, and other types, through pipes as well.&lt;/p&gt;
&lt;p&gt;So, I think I&amp;rsquo;m going to take a page out of Go&amp;rsquo;s template book and
simply have pipes act as syntactic sugar over sequential calls. The goal
is to make the construct &lt;code&gt;a | b&lt;/code&gt; essentially be the same as &lt;code&gt;b (a)&lt;/code&gt;,
where the first argument of &lt;code&gt;b&lt;/code&gt; will be the result of &lt;code&gt;a&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As for streams, I&amp;rsquo;m thinking of removing them as a dedicated object
type. Embedders could certainly make analogous types if they need to,
and the language should support that, but the language will no longer
offer first class support for them out of the box. &lt;/p&gt;
&lt;p&gt;The second is to remove any sense of &amp;ldquo;purity&amp;rdquo; of the builtins. You may
recall the indecision I had regarding using anonymous procs with the
&lt;code&gt;map&lt;/code&gt; command:&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;I&amp;rsquo;m not sure how I can improve this. I don&amp;rsquo;t really want to add
automatic dereferencing of identities: they&amp;rsquo;re very useful as
unquoted string arguments. I suppose I could add another construct
that would support dereferencing, maybe by enclosing the identifier in
parenthesis.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;I think this is the wrong way to think of this. Again, I&amp;rsquo;m not here to
design a pure implementation of the language. The language is meant to
be easy to use, first and foremost, in an interactive shell, and if that
means sacrificing purity for a &lt;code&gt;map&lt;/code&gt; command that supports blocks,
anonymous procs, and automatic dereferencing of commands just to make it
easier for the user, then I think that&amp;rsquo;s a trade work taking.&lt;/p&gt;
&lt;p&gt;Anyway, that&amp;rsquo;s the current thinking as of now.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Imports And The New Model</title>
      <link>https://lmika.org/2024/04/20/imports-and-the.html</link>
      <pubDate>Sat, 20 Apr 2024 15:53:06 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/04/20/imports-and-the.html</guid>
      <description>&lt;p&gt;Well, I dragged Photo Bucket out today to work on it a bit.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s fallen by the wayside a little, and I&amp;rsquo;ve been wondering if it&amp;rsquo;s
worth continuing work on it. So many things about it that need to be
looked at: the public site looks ugly, as does the admin section;
working with more than a single image is a pain; backup and restore
needs to be added; etc.&lt;/p&gt;
&lt;p&gt;I guess every project goes through this &amp;ldquo;trough of discontent&amp;rdquo; where
the initial excitement has warn off and all you see is a huge laundry
list of things to do.  Not to mention the wandering eye &lt;a href=&#34;https://blog.jim-nielsen.com/2024/netlify-image-cdn/&#34;&gt;looking at the
alternatives&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But I do have to stop myself from completely junking it, since it&amp;rsquo;s
actually being used to host the &lt;a href=&#34;https://gallery.folio.red/&#34;&gt;Folio Red Gallery&lt;/a&gt;.  I guess my push
to deploy it has entrapped me (well, that was the idea of pushing it out
there in the first place).&lt;/p&gt;
&lt;p&gt;Anyway, it&amp;rsquo;s been a while (last update &lt;a href=&#34;https://lmika.org/2024/03/09/photo-bucket-update.html&#34;&gt;is here&lt;/a&gt;) and the move to
the new model is progressing. And it&amp;rsquo;s occurred to me that I haven&amp;rsquo;t
actually talked about the new model (well, maybe I have but I forgot
about it).&lt;/p&gt;
&lt;p&gt;Previously, the root model of this data structure is the &lt;code&gt;Store&lt;/code&gt;. All
images belong to a &lt;code&gt;Store&lt;/code&gt;, which is responsible for managing the
physical storage and retrieval of them. These stores can have
sub-stores, which are usually used to hold the images optimised for a
specific use (serving on the web, showing as a thumbnails, etc).
Separate to this was the public site &lt;code&gt;Design&lt;/code&gt; which handed properties of
the public site: how it should look, what the title, and description is,
etc.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/1355eacebd.jpg&#34;&gt;
&lt;figcaption&gt;The &#34;Store&#34; model&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;There were some serious issues with this approach: images were owned by
stores, and two images can belong to two different stores, but they all
belonged to the same site. This made uploading confusing: which store
should the image live on? I worked around this by adding the notion of a
&amp;ldquo;primary store&amp;rdquo; but this was just ignoring the problem and defeated
the whole multiple-store approach.&lt;/p&gt;
&lt;p&gt;This is made even worse when one considers which store to use for
serving the images. Down the line I was hoping to support virtual domain
hosting, where one could setup different image sites on different
domains that all pointed to the same instance. So imagine how that would
work: one wanted to view images from &lt;code&gt;alpha.example.com&lt;/code&gt; and another
wanted to view images from &lt;code&gt;beta.example.com&lt;/code&gt;. Should the domains live
on the store? What about the site designs? Where should they live?&lt;/p&gt;
&lt;p&gt;The result was that this model could only really ever support one site
per Photo Bucket instance, requiring multiple deployments for different
sites if one wanted to use a single host for separate photo sites.&lt;/p&gt;
&lt;p&gt;So I re-engineered the model to simplify this dramatically.  Now, the
route object is the &lt;code&gt;Site&lt;/code&gt;:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/a927075635.jpg&#34;&gt;
&lt;figcaption&gt;The &#34;Site&#34; model&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Here, the Site owns everything. The images are associated with sites,
not stores. Stores still exist, but their role is now more in-line with
what the sub-stores did. When an image is uploaded, it is stored in
every Store of the site, and each Store will be responsible for
optimising it for a specific use-case. The logic used to determine which
Store to use to fetch the image is still in place but now it can be
assumed that any Store associated with a site will have the image.&lt;/p&gt;
&lt;p&gt;Now the question of which Store an image should be added to is easy: all
the them.&lt;/p&gt;
&lt;p&gt;Non-image data, such as Galleries and Designs now live off the Site as
well, and if virtual hosting is added, so would the domain that serves
that Site.&lt;/p&gt;
&lt;p&gt;At least one site needs to be present at all time, and it&amp;rsquo;s likely most
instances will simply have a single Site for now. But this assumption
solves the upload and hosting resolution issues listed above. And if
multiple site support is needed, a simple site picker can be added to
the admin page (the public pages can will rely on the request hostname).&lt;/p&gt;
&lt;p&gt;This has been added a while ago, and as of today, has been merged to
&lt;code&gt;main&lt;/code&gt;. But I didn&amp;rsquo;t want to deal with writing the data migration logic
for this, so my plan is to simply junk the existing instance and replace
it with the brand new one. But in order to do so, I needed to export the
photos from the old instance, and import them into the new one.&lt;/p&gt;
&lt;p&gt;The export logic has been deployed and I&amp;rsquo;ve made an export it this
morning. Today, the import logic was finished and merged.  Nothing
fancy: like the &lt;code&gt;export&lt;/code&gt; it&amp;rsquo;s only invokable from the command line. But
it&amp;rsquo;ll do the job for now.&lt;/p&gt;
&lt;p&gt;Next steps is to actually deploy this, which I guess will be the
ultimate test. Then, I&amp;rsquo;m hoping to add support for galleries in the
public page so I can separate images on the Folio Red Gallery into
projects. There&amp;rsquo;s still no way to add images in bulk to a gallery.
Maybe this will give me an incentive to do that next.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>UCL: Procs and Higher-Order Functions</title>
      <link>https://lmika.org/2024/04/19/ucl-procs-and.html</link>
      <pubDate>Fri, 19 Apr 2024 16:10:59 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/04/19/ucl-procs-and.html</guid>
      <description>&lt;p&gt;More on UCL yesterday evening.  Biggest change is the introduction of
user functions, called &amp;ldquo;procs&amp;rdquo; (same name used in TCL):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proc greet {
    echo &amp;quot;Hello, world&amp;quot;
}

greet
--&amp;gt; Hello, world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Naturally, like most languages, these can accept arguments, which use
the same block variable binding as the &lt;code&gt;foreach&lt;/code&gt; loop:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proc greet { |what|
    echo &amp;quot;Hello, &amp;quot; $what
}

greet &amp;quot;moon&amp;quot;
--&amp;gt; Hello, moon
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The name is also optional, and if omitted, will actually make the
function anonymous.  This allows functions to be set as variable values,
and also be returned as results from other functions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proc makeGreeter { |greeting|
    proc { |what|
        echo $greeting &amp;quot;, &amp;quot; $what
    }
}

set helloGreater (makeGreeter &amp;quot;Hello&amp;quot;)
call $helloGreater &amp;quot;world&amp;quot;
--&amp;gt; Hello, world

set goodbye (makeGreeter &amp;quot;Goodbye cruel&amp;quot;)
call $goodbye &amp;quot;world&amp;quot;
--&amp;gt; Goodbye cruel, world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&amp;rsquo;ve added procs as a separate object type. At first glance, this may
seem a little unnecessary. After all, aren&amp;rsquo;t blocks already a specific
object type?&lt;/p&gt;
&lt;p&gt;Well, yes, that&amp;rsquo;s true, but there are some differences between a proc
and a regular block. The big one being that the proc will have a defined
scope. Blocks adapt to the scope to which they&amp;rsquo;re invoked whereas a
proc will close over and include the scope to which it was defined, a
lot like closures in other languages.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not a perfect implementation at this stage, since the &lt;code&gt;set&lt;/code&gt;
command only sets variables within the immediate scope. This means that
modifying closed over variables is currently not supported:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\# This currently won&#39;t work
proc makeSetter {
    set bla &amp;quot;Hello, &amp;quot;
    proc appendToBla { |x|
        set bla (cat $bla $x)
        echo $bla
    }
}

set er (makeSetter)
call $er &amp;quot;world&amp;quot;
\# should be &amp;quot;Hello, world&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;higher-order-functions&#34;&gt;Higher-Order Functions&lt;/h2&gt;
&lt;p&gt;The next bit of work is finding out how best to invoke these procs in
higher-order functions. There are some challenges here that deal with
the language grammar.&lt;/p&gt;
&lt;p&gt;Invoking a proc by name is fine, but since the grammar required the
first token to be a command name, there was no way to invoke a proc
stored in a variable. I quickly added a new &lt;code&gt;call&lt;/code&gt; command — which takes
the proc as the first argument — to work around it, but after a while,
this got a little unwieldy to use (you can see it in the code sample
above).&lt;/p&gt;
&lt;p&gt;So I decided to modify the grammar to allow any arbitrary value to be
the first token. If it&amp;rsquo;s a variable that is bound to something
&amp;ldquo;invokable&amp;rdquo; (i.e. a proc), and there exist at-least one other
argument, it will be invoked. So the above can be written as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set helloGreater (makeGreeter &amp;quot;Hello&amp;quot;)
$helloGreater &amp;quot;world&amp;quot;
--&amp;gt; Hello, world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At-least one argument is required, otherwise the value will simply be
returned. This is so that the value of variables and literal can be
returned as is, but that does mean lambdas will simply be dereferenced:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;just, this&amp;quot;
--&amp;gt; just, this

set foo &amp;quot;bar&amp;quot;
$foo
--&amp;gt; bar

set bam (proc { echo &amp;quot;BAM!&amp;quot; })
$bam
--&amp;gt; (proc)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To get around this, I&amp;rsquo;ve added the notion of the &amp;ldquo;empty sub&amp;rdquo;, which
is just the construct &lt;code&gt;()&lt;/code&gt;. It evaluates to nil, and since a function
ignores any extra arguments not bound to variables, it allows for
calling a lambda that takes no arguments:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set bam (proc { echo &amp;quot;BAM!&amp;quot; })
$bam ()
--&amp;gt; BAM!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It does allow for other niceties, such as using a falsey value:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if () { echo &amp;quot;True&amp;quot; } else { echo &amp;quot;False&amp;quot; }
--&amp;gt; False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With lambdas now in place, I&amp;rsquo;m hoping to work on some higher order
functions. I&amp;rsquo;ve started working on &lt;code&gt;map&lt;/code&gt; which accepts both a list or a
stream. It&amp;rsquo;s a buggy mess at the moment, but some basic constructs
currently work:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;map [&amp;quot;a&amp;quot; &amp;quot;b&amp;quot; &amp;quot;c&amp;quot;] (proc { |x| toUpper $x }) 
--&amp;gt; stream [&amp;quot;A&amp;quot; &amp;quot;B&amp;quot; &amp;quot;C&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Oh, by the way, when setting a variable to a stream using &lt;code&gt;set&lt;/code&gt;, it
will now collect the items as a list. Or at least that&amp;rsquo;s the idea.
It&amp;rsquo;s currently not working at the moment.)&lt;/p&gt;
&lt;p&gt;A more refined approach would be to treat commands as lambdas. The
grammar supports this, but the evaluator doesn’t. For example, you
cannot write the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\# won&#39;t work
map [&amp;quot;a&amp;quot; &amp;quot;b&amp;quot; &amp;quot;c&amp;quot;] toUpper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is because &lt;code&gt;makeUpper&lt;/code&gt; will be treated as a string, and not a
reference to an invokable command. It will work for variables. You can
do this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set makeUpper (proc { |x| toUpper $x })
map [&amp;quot;a&amp;quot; &amp;quot;b&amp;quot; &amp;quot;c&amp;quot;] $makeUpper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&amp;rsquo;m not sure how I can improve this. I don&amp;rsquo;t really want to add
automatic dereferencing of identities: they&amp;rsquo;re very useful as unquoted
string arguments. I suppose I could add another construct that would
support dereferencing, maybe by enclosing the identifier in parenthesis:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\# might work?
map [&amp;quot;a&amp;quot; &amp;quot;b&amp;quot; &amp;quot;c&amp;quot;] (toUpper)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anyway, more on this in the future I&amp;rsquo;m sure.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>UCL: First Embed, and Optional Arguments</title>
      <link>https://lmika.org/2024/04/18/ucl-first-embed.html</link>
      <pubDate>Thu, 18 Apr 2024 08:39:12 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/04/17/ucl-first-embed.html</guid>
      <description>&lt;p&gt;Came up with a name: Universal Control Language: UCL. See, you have TCL;
but what if instead of being used for tools, it can be more universal?
Sounds so much more… universal, am I right? 😀&lt;/p&gt;
&lt;p&gt;Yeah, okay. It&amp;rsquo;s not a great name. But it&amp;rsquo;ll do for now.&lt;/p&gt;
&lt;p&gt;Anyway, I&amp;rsquo;ve started integrating this language with the admin tool I&amp;rsquo;m
using at work. This tool I use is the impetus for this whole endeavour.
Up until now, this tool was just a standard CLI command usable from the
shell. But it&amp;rsquo;s not uncommon for me to have to invoke the tool multiple
times in quick succession, and each time I invoke it, it needs to
connect to backend systems, which can take a few seconds. Hence the
reason why I&amp;rsquo;m converting it into a REPL.&lt;/p&gt;
&lt;p&gt;Anyway, I added UCL to the tool, along with a &lt;a href=&#34;https://github.com/chzyer/readline&#34;&gt;readline library&lt;/a&gt;, and
wow, did it feel good to use. So much better than the simple quote-aware
string splitter I&amp;rsquo;d would&amp;rsquo;ve used. And just after I added it, I got a
flurry of requests from my boss to gather some information, and although
the language couldn&amp;rsquo;t quite handle the task due to missing or
unfinished features, I can definitely see the potential there.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m trying my best to only use what will eventually be the public API
to add the tool-specific bindings. The biggest issue is that these
&amp;ldquo;user bindings&amp;rdquo; (i.e. the non-builtins) desperately need support for
producing and consuming streams. They&amp;rsquo;re currently producing Go slices,
which are being passed around as opaque &amp;ldquo;proxy objects&amp;rdquo;, but these
can&amp;rsquo;t be piped into other commands to, say, filter or map. Some other
major limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No commands to actually filter or map. In fact, the whole standard
library needs to be built out.&lt;/li&gt;
&lt;li&gt;No ability to get fields from hashes or lists, including proxy objects
which can act as lists or hashes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One last thing that would be nice is the ability to define optional
arguments. I actually started work on that last night, seeing that it&amp;rsquo;s
relatively easy to build. I&amp;rsquo;m opting for a style that looks like the
switches you&amp;rsquo;d find on the command line, with option names starting
with dashes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;join &amp;quot;a&amp;quot; &amp;quot;b&amp;quot; -separator &amp;quot;,&amp;quot; -reverse
--&amp;gt; b, a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each option can have zero or more arguments, and boolean options can be
represented as just having the switch. This does mean that they&amp;rsquo;d have
to come after the positional arguments, but I think I can live with
that.  Oh, and no syntactic sugar for single-character options: each
option must be separated by whitespace (the grammar actually treats them
as identifiers). In fact, I&amp;rsquo;d like to discourage the use of
single-character option names for these: I prefer the clarity that comes
from having the name written out in full (that said, I wouldn&amp;rsquo;t rule
out support for aliases). This eliminates the need for double dashes, to
distinguish long option names from a cluster of single-character
options, so only the single dash will be used.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll talk more about how the Go bindings look later, after I&amp;rsquo;ve used
them a little more and they&amp;rsquo;re a little more refined.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Tool Command Language: Lists, Hashs, and Loops</title>
      <link>https://lmika.org/2024/04/17/tool-command-language.html</link>
      <pubDate>Wed, 17 Apr 2024 21:04:22 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/04/17/tool-command-language.html</guid>
      <description>&lt;p&gt;A bit more on TCL (yes, yes, I&amp;rsquo;ve gotta change the name) last night.
Added both lists and hashes to the language. These can be created using
a literal syntax, which looks pretty much looks how I described it a few
days ago:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set list [&amp;quot;a&amp;quot; &amp;quot;b&amp;quot; &amp;quot;c&amp;quot;]
set hash [&amp;quot;a&amp;quot;:&amp;quot;1&amp;quot; &amp;quot;b&amp;quot;:&amp;quot;2&amp;quot; &amp;quot;c&amp;quot;:&amp;quot;3&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I had a bit of trouble working out the grammar for this, I first went
with something that looked a little like the following, where the key of
an element is optional but the value is mandatory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;list_or_hash  --&amp;gt; &amp;quot;[&amp;quot; &amp;quot;]&amp;quot;        \# empty list
                | &amp;quot;[&amp;quot; &amp;quot;:&amp;quot; &amp;quot;]&amp;quot;    \# empty hash
                | &amp;quot;[&amp;quot; elems &amp;quot;]&amp;quot;  \# elements

elems --&amp;gt; ((arg &amp;quot;:&amp;quot;)? arg)*      \# elements of a list or hash

arg --&amp;gt; &amp;lt;anything that can be a command argument&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But I think this confused the parser a little, where it was greedily
consuming the key arg and expecting the &lt;code&gt;:&lt;/code&gt; to be present to consume the
value.&lt;/p&gt;
&lt;p&gt;So I flipped it around, and now the &amp;ldquo;value&amp;rdquo; is the optional part:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;elems --&amp;gt; (arg (&amp;quot;:&amp;quot; arg)?)*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So far this seems to work. I renamed the two fields &amp;ldquo;left&amp;rdquo; and
&amp;ldquo;right&amp;rdquo;, instead of key and value.  Now a list element will use the
&amp;ldquo;left&amp;rdquo; part, and a hash element will use &amp;ldquo;left&amp;rdquo; for the key and
&amp;ldquo;right&amp;rdquo; for the value.&lt;/p&gt;
&lt;p&gt;You can probably guess that the list and hash are sharing the same AST
types. This technically means that hybrid lists are supported, at least
in the grammar. But I&amp;rsquo;m making sure that the evaluator throws an error
when a hybrid is detected. I prefer to be strict here, as I don&amp;rsquo;t want
to think about how best to support it. Better to just say either a
&amp;ldquo;pure&amp;rdquo; list, or a &amp;ldquo;pure&amp;rdquo; hybrid.&lt;/p&gt;
&lt;p&gt;Well, now that we have collections, we need some way to iterate over
them. For that, I&amp;rsquo;ve added a &lt;code&gt;foreach&lt;/code&gt; loop, which looks a bit like the
following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\# Over lists
foreach [&amp;quot;a&amp;quot; &amp;quot;b&amp;quot; &amp;quot;c&amp;quot;] { |elem|
  echo $elem
}

\# Over hashes
foreach [&amp;quot;a&amp;quot;:&amp;quot;1&amp;quot; &amp;quot;b&amp;quot;:&amp;quot;2&amp;quot;] { |key val|
  echo $key &amp;quot; = &amp;quot; $val
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What I like about this is that, much like the &lt;code&gt;if&lt;/code&gt; statement, it&amp;rsquo;s
implemented as a macro. It takes a value to iterate over, and a block
with bindable variables: one for list elements, or two for hash keys and
values. This does mean that, unlike most other languages, the loop
variable appears within the block, rather than to the left of the
element, but after getting use to this form of block from my Ruby days,
I can get use to it.&lt;/p&gt;
&lt;p&gt;One fun thing about hashes is that they&amp;rsquo;re implemented using Go&amp;rsquo;s
&lt;code&gt;map&lt;/code&gt; type. This means that the iteration order is random, by design.
This does make testing a little difficult (I&amp;rsquo;ve only got one at the
moment, which features a hash of length one) but I rarely depend on the
order of hash keys so I&amp;rsquo;m happy to keep it as is.&lt;/p&gt;
&lt;p&gt;This loop is only the barest of bones at the moment. It doesn&amp;rsquo;t support
flow control like &lt;code&gt;break&lt;/code&gt; or &lt;code&gt;continue&lt;/code&gt;, and it also needs to support
streams (I&amp;rsquo;m considering a version with just the block that will accept
the stream from a pipe). But I think it&amp;rsquo;s a reasonably good start.&lt;/p&gt;
&lt;p&gt;I also spend some time today integrating this language in the tool I was
building it for. I won&amp;rsquo;t talk about it here, but already it&amp;rsquo;s showing
quite a bit of promise. I think, once the features are fully baked, that
this would be a nice command language to keep in my tool-chest. But more
of that in a later post.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Backlog Proc: A Better JQL</title>
      <link>https://lmika.org/2024/04/15/backlog-proc-a.html</link>
      <pubDate>Mon, 15 Apr 2024 10:47:39 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/04/15/backlog-proc-a.html</guid>
      <description>&lt;p&gt;Backlog Proc is a simple item backlog tracker I built for work. I&amp;rsquo;d
like to link them to Jira tickets, so that I know whether a particular
backlog item actually has tasks written for them, and what the status of
each of those tasks are.  I guess these are meant to be tracked by
epics, but Jira&amp;rsquo;s UI for handling such things is a mess, and I&amp;rsquo;d like
to make notes that are only for my own eyes.&lt;/p&gt;
&lt;p&gt;Anyway, I&amp;rsquo;m was using JQL to select the Jira tickets. And it worked,
but the language is a bit verbose. Plus the tool I&amp;rsquo;m running the
queries in, &lt;code&gt;jira-cli&lt;/code&gt;, requires that I add the project ID along with
the things like the epic or fix version.&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;m started working on a simpler language, one that&amp;rsquo;s just a
tokenised list of ticket numbers. For example, instead of writing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;project = &amp;quot;ABC&amp;quot; and key in (ABC-123 ABC-234 ABC-345)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One could just write:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ABC-123 ABC-234 ABC-345
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And instead of writing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(project = &amp;quot;ABC&amp;quot; and epicLink = &amp;quot;ABC-818&amp;quot;) OR (project = &amp;quot;DEF&amp;quot; and epicLink = &amp;quot;DEF-222&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One could just write:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;epic:(ABC-818, DEF-222)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(note here the use of OR, in that the sets are unionised; I&amp;rsquo;m not sure
how this would scale for the other constructs).&lt;/p&gt;
&lt;p&gt;Key characteristics is that the parser would be able to get the project
ID from the query, instead of having the query writer (i.e. me)
explicitly add it.&lt;/p&gt;
&lt;p&gt;I can also do introspection, such as get the relevant projects, by
&amp;ldquo;unparsing&amp;rdquo; the query. An advantage of controlling the parser and
language. Can&amp;rsquo;t do that with JQL.&lt;/p&gt;
&lt;p&gt;But, of-course, I can&amp;rsquo;t cover all possible bases with this language
just yet, so I&amp;rsquo;ll need a way to include arbitrary JQL.. So I&amp;rsquo;ve also
added a general &amp;ldquo;escape&amp;rdquo; clause to do this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jql:&amp;quot;project in (bla)&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;a-few-other-things&#34;&gt;A Few Other Things&lt;/h2&gt;
&lt;p&gt;A few other things that is needed for Backlog Proc:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The landing screen needs to show all the active items regardless of
what project they belong to. Bouncing around projects is a bit of a
hassle (to be fair, I haven&amp;rsquo;t spent any time styling the screens
beyond what Buffalo generated).&lt;/li&gt;
&lt;li&gt;Add back ticket drafts. I enjoyed the flow of drafting all the tickets
in one hit, then clicking &amp;ldquo;Publish&amp;rdquo; and seeing them come out as a
fully specified Jira epic.&lt;/li&gt;
&lt;li&gt;Add the notion of logs, with a &amp;ldquo;log type&amp;rdquo;. Such types can include
things like &amp;ldquo;todo&amp;rdquo; (i.e thing for me to work on), &amp;ldquo;decision&amp;rdquo; (a
product decision that was agreed on), and &amp;ldquo;log&amp;rdquo; (work that was
done). &lt;/li&gt;
&lt;li&gt;A way to quickly view the tickets in Jira so I can do batch operations
over them.&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Tool Command Language: Macros And Blocks</title>
      <link>https://lmika.org/2024/04/13/tool-command-language.html</link>
      <pubDate>Sat, 13 Apr 2024 16:44:15 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/04/13/tool-command-language.html</guid>
      <description>&lt;p&gt;More work on the tool command language (of which I need to come up with
a name: I can&amp;rsquo;t use the abbreviation TCL), this time working on getting
multi-line statement blocks working. As in:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;Here&amp;quot;
echo &amp;quot;There&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I got a little wrapped up about how I can configure the parser to
recognise new-lines as statement separators. I tried this in the past
with a hand rolled lexer and ended up peppering &lt;code&gt;NL&lt;/code&gt; tokens all around
the grammar. I was fearing that I needed to do something like this here.
After a bit of experimentation, I think I&amp;rsquo;ve come up with a way to
recognise new-lines as statement separators without making the grammar
too messy. The unit tests verifying this so far seem to work.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Excerpt of the grammar showing all the &#39;NL&#39; token matches.
// These match a new-line, plus any whitespace afterwards.

type astStatements struct {
    First *astPipeline   `parser:&amp;quot;@@&amp;quot;`
    Rest  []*astPipeline `parser:&amp;quot;( NL+ @@ )*&amp;quot;`
}

type astBlock struct {
    Statements []*astStatements `parser:&amp;quot;LC NL? @@ NL? RC&amp;quot;`
}

type astScript struct {
    Statements *astStatements `parser:&amp;quot;NL* @@ NL*&amp;quot;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&amp;rsquo;m still using a stateful lexer as it may come in handy when it comes
to string interpolation. Not sure if I&amp;rsquo;ll add this, but I&amp;rsquo;d like the
option.&lt;/p&gt;
&lt;p&gt;Another big addition today was macros. These are much like commands, but
instead of  arguments being evaluated before being passed through to the
command, they&amp;rsquo;re deferred and the command can explicitly request their
evaluation whenever. I think Lisp has something similar: this is not
that novel.&lt;/p&gt;
&lt;p&gt;This was used to implement the &lt;code&gt;if&lt;/code&gt; command, which is now working:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set x &amp;quot;true&amp;quot;
if $x {
  echo &amp;quot;Is true&amp;quot;
} else {
  echo &amp;quot;Is not true&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Of course, there are actually no operators yet, so it doesn&amp;rsquo;t really do
much at the moment.&lt;/p&gt;
&lt;p&gt;This spurred the need for blocks. which is a third large addition made
today. They&amp;rsquo;re just a group of statements that are wrapped in an object
type.  They&amp;rsquo;re &amp;ldquo;invokable&amp;rdquo; in that the statements can be executed and
produce a result, but they&amp;rsquo;re also a value that can be passed around.
It jells nicely with the macro approach.&lt;/p&gt;
&lt;p&gt;Must say that I like the idea of using macros for things like &lt;code&gt;if&lt;/code&gt; over
baking it into the language. It can only add to the &amp;ldquo;embed-ability&amp;rdquo; of
this, which is what I&amp;rsquo;m looking for.&lt;/p&gt;
&lt;p&gt;Finally, I did see something interesting in the tests. I was trying the
following test:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;Hello&amp;quot;
echo &amp;quot;World&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And I was expecting a &lt;code&gt;Hello&lt;/code&gt; and &lt;code&gt;World&lt;/code&gt; to be returned over two lines.
But only &lt;code&gt;World&lt;/code&gt; was being returning. Of course! Since &lt;code&gt;echo&lt;/code&gt; is
actually producing a stream and not printing anything to stdout, it
would only return &lt;code&gt;World&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I decided to change this. If I want to use &lt;code&gt;echo&lt;/code&gt; to display a message,
then the above script should display both &lt;code&gt;Hello&lt;/code&gt; and &lt;code&gt;World&lt;/code&gt; in some
manner. The downside is that I don&amp;rsquo;t think I&amp;rsquo;ll be able to support
constructs like this, where echo provides a source for a pipeline:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\# This can&#39;t work anymore
echo &amp;quot;Hello&amp;quot; | toUpper 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I mean, I could probably detect whether &lt;code&gt;echo&lt;/code&gt; is connected to a pipe
(the parser can give that information). But what about other commands
that output something? Would they need to be treated similarly?&lt;/p&gt;
&lt;p&gt;I think it&amp;rsquo;s probably best to leave this out for now, and have a new
construct for providing literals like this to a pipe. Heck, maybe just
having the string itself would be enough:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;hello&amp;quot; | toUpper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anyway, that&amp;rsquo;s all for today.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Tool Command Language</title>
      <link>https://lmika.org/2024/04/12/tool-command-language.html</link>
      <pubDate>Fri, 12 Apr 2024 07:45:30 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/04/11/tool-command-language.html</guid>
      <description>&lt;p&gt;I have this idea for a tool command language. Something similar to TCL,
in that it&amp;rsquo;s chiefly designed to be used as an embedded scripting
language and chiefly in an interactive context.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s been an idea I&amp;rsquo;ve been having in my mind for a while, but I&amp;rsquo;ve
got the perfect use case for it. I&amp;rsquo;ve got a tool at work I use to do
occasional admin tasks. At the moment it&amp;rsquo;s implemented as a CLI tool,
and it works. But the biggest downside is that it needs to form
connections to the cluster to call internal service methods, and it
always take a few seconds to do so. I&amp;rsquo;d like to be able to use it to
automate certain actions, but this delay would make doing so a real
hassle.&lt;/p&gt;
&lt;p&gt;Some other properties that I&amp;rsquo;m thinking off:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It should be able to support structured data, similar to how Lisp
works&lt;/li&gt;
&lt;li&gt;It should be able to support something similar to pipes, similar to
how the shell and Go&amp;rsquo;s template language works.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some of the trade-offs that come of it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It doesn&amp;rsquo;t have to be fast. In fact, it can be slow so long as the
work embedding and operating it can be fast.&lt;/li&gt;
&lt;li&gt;It may not be completely featureful. I&amp;rsquo;ll go over the features I&amp;rsquo;m
thinking of below, but I say upfront that you&amp;rsquo;re not going to be
building any cloud services with this. Administering cloud servers,
maybe; but leave the real programs to a real language.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;some-notes-on-the-design&#34;&gt;Some Notes On The Design&lt;/h2&gt;
&lt;p&gt;The basic concept is the statement. A statement consists of a command,
and zero or more arguments. If you&amp;rsquo;ve used a shell before, then you can
imagine how this&amp;rsquo;ll look:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;firstarg &amp;quot;hello, world&amp;quot;
--&amp;gt; hello, world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each statement produces a result. Here, the theoretical &lt;code&gt;firstarg&lt;/code&gt; will
return the first argument it receives, which will be the string &lt;code&gt;&amp;quot;hello, world&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Statements are separated by new-lines or semicolons. In such a sequence,
the return value of the last argument is returned:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;firstarg &amp;quot;hello&amp;quot; ; firstarg &amp;quot;world&amp;quot;
--&amp;gt; world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&amp;rsquo;m hoping to have a similar approach to how Go works, in that
semicolons will be needed if multiple statements share a line, but will
otherwise be unnecessary. I&amp;rsquo;m using the &lt;a href=&#34;https://github.com/alecthomas/participle&#34;&gt;Participal&lt;/a&gt; parser library
for this, and I&amp;rsquo;ll need to know how I can configure the scanner to do
this (or even if using the scanner is the right way to go).&lt;/p&gt;
&lt;p&gt;The return value of statements can be used as the arguments of other
statements by wrapping them in parenthesis:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo (firstarg &amp;quot;hello&amp;quot;) &amp;quot; world&amp;quot;
--&amp;gt; hello world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is taken directly from TCL, except that TCL uses the square
brackets. I&amp;rsquo;m reserving the square brackets for data structures, but
the parenthesis are free. It also gives it a bit of a Lisp feel.&lt;/p&gt;
&lt;h2 id=&#34;pipelines&#34;&gt;Pipelines&lt;/h2&gt;
&lt;p&gt;Another way for commands to consume the output of other commands is to
build pipelines. This is done using the pipe &lt;code&gt;|&lt;/code&gt; character:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;hello&amp;quot; | toUpper
--&amp;gt; HELLO
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pipeline sources, that is the command on the left-most side, can be
either commands that produce a single result, or a command that produces
a &amp;ldquo;stream&amp;rdquo;. Both are objects, and there&amp;rsquo;s nothing inherently special
about a stream, other than there some handling when used as a pipeline.
Streams are also designed to be consumed once.&lt;/p&gt;
&lt;p&gt;For example, one can consider a command which can read a file and
produce a stream of the contents:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;quot;taleOfTwoCities.txt&amp;quot;
--&amp;gt; It was the best of times,
--&amp;gt; it was the worst of times,
--&amp;gt; …
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not every command is &amp;ldquo;pipe savvy&amp;rdquo;. For example, piping the result of a
pipeline to &lt;code&gt;echo&lt;/code&gt; will discard it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;hello&amp;quot; | toUpper | echo &amp;quot;no me&amp;quot;
--&amp;gt; no me
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Of course, this may differ based on how the builtins are implemented.&lt;/p&gt;
&lt;h2 id=&#34;variables&#34;&gt;Variables&lt;/h2&gt;
&lt;p&gt;Variables are treated much like TCL and shell, in that referencing them
is done using the dollar sign:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set name &amp;quot;josh&amp;quot;
--&amp;gt; &amp;quot;Josh&amp;quot;
echo &amp;quot;My name is &amp;quot; $name
--&amp;gt; &amp;quot;My name is Josh&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not sure how streams will be handled with variables but I&amp;rsquo;m wondering
if they should be condensed down to a list.  I don&amp;rsquo;t like the idea of
assigning a stream to a variable, as streams are only consumed once, and
I feel like some confusion will come of it if I were to allow this.&lt;/p&gt;
&lt;p&gt;Maybe I can take the Perl approach and use a different variable
&amp;ldquo;context&amp;rdquo;, where you have a variable with a &lt;code&gt;@&lt;/code&gt; prefix which will
reference a stream.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set file (cat &amp;quot;eg.text&amp;quot;)

echo @file
\# Echo will consume file as a stream

echo $file
\# Echo will consume file as a list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The difference is subtle but may be useful. I&amp;rsquo;ll look out for instances
where this would be used.&lt;/p&gt;
&lt;p&gt;Attempting to reference an unset variable will result in an error. This
may also change.&lt;/p&gt;
&lt;h2 id=&#34;other-ideas&#34;&gt;Other Ideas&lt;/h2&gt;
&lt;p&gt;That&amp;rsquo;s pretty much what I have at the moment. I do have some other
ideas, which I&amp;rsquo;ll document below.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Structured Data Support:&lt;/strong&gt; Think lists and hashes. This language is to
be used with structured data, so I think it&amp;rsquo;s important that the
language supports this natively. This is unlike TCL which principally
works with strings and the notion of lists feels a bit tacked on to some
extent.&lt;/p&gt;
&lt;p&gt;Both lists and hashes are created using square brackets:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\# Lists. Not sure if they&#39;ll have commas or not
set l [1 2 3 $four (echo &amp;quot;5&amp;quot;)]

\# Maps
set m [a:1 &amp;quot;b&amp;quot;:2 &amp;quot;see&amp;quot;:(echo &amp;quot;3&amp;quot;) (echo &amp;quot;dee&amp;quot;):$four]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Blocks:&lt;/strong&gt; Yep, containers for a groups of statements. This will be
used for control flow, as well as for definition of functions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set x 4
if (eq $x 4) {
  echo &amp;quot;X == 4&amp;quot;
} else {
  echo &amp;quot;X != 4&amp;quot;
}

foreach [1 2 3] { |x|
  echo $x
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here the blocks are just another object type, like strings and stream,
and both &lt;code&gt;if&lt;/code&gt; and &lt;code&gt;foreach&lt;/code&gt; are regular commands which will accept a
block as an argument. In fact, it would be theoretically possible to
write an &lt;code&gt;if&lt;/code&gt; statement this way (not sure if I&amp;rsquo;ll allow setting
variables to blocks):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set thenPart {
  echo &amp;quot;X == 4&amp;quot;
}

if (eq $x 4) $thenPart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The block execution will exist in a context, which will control whether
a new stack frame will be used. Here the &lt;code&gt;if&lt;/code&gt; statement will simply use
the existing frame, but a block used in a new function can push a new
frame, with a new set of variables:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proc myMethod { |x|
  echo $x
}

myMethod &amp;quot;Hello&amp;quot;
--&amp;gt; &amp;quot;Hello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also note the use of &lt;code&gt;|x|&lt;/code&gt; at the start of the block.  This is used to
declare bindable variables, such as function arguments or for loop
variables.  This will be defined as part of the grammar, and be a
property of the block.&lt;/p&gt;
&lt;p&gt;Anyway, that&amp;rsquo;s the current idea.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Photo Bucket Update: Exporting To Zip</title>
      <link>https://lmika.org/2024/03/09/photo-bucket-update.html</link>
      <pubDate>Sat, 09 Mar 2024 09:33:57 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/03/09/photo-bucket-update.html</guid>
      <description>&lt;p&gt;Worked a little more on Photo Bucket this week. Added the ability to export the contents of an instance to a Zip file. This consist of both images and metadata.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/screenshot-2024-03-09-at-9.58.38-am.png&#34; width=&#34;600&#34; height=&#34;304&#34; alt=&#34;Screenshot of a finder window showing the contents of the exported Zip file&#34;&gt;
&lt;p&gt;I&amp;rsquo;ve went with lines of JSON file for the image metadata. I considered a CSV file briefly, but for optional fields like captions and custom properties, I didn&amp;rsquo;t like the idea of a lot of empty columns. Better to go with a format that&amp;rsquo;s a little more flexible, even if it does mean more text per line.&lt;/p&gt;
&lt;p&gt;As for the images, I&amp;rsquo;m hoping the export to consist of the &amp;ldquo;best quality&amp;rdquo; version. What that means will depend on the instance. The idea is to have three tiers of image quality managed by the store: &amp;ldquo;original&amp;rdquo;, &amp;ldquo;web&amp;rdquo;, and &amp;ldquo;thumbnail&amp;rdquo;. The &amp;ldquo;original&amp;rdquo; version is the untouched version uploaded to the store. The &amp;ldquo;web&amp;rdquo; version is re-encoded from the &amp;ldquo;original&amp;rdquo; and will be slightly compressed with image metadata tags stripped out. The &amp;ldquo;thumbnail&amp;rdquo; version will be a small, highly compressed version suitable for the thumbnail. There is to be a decision algorithm in place to get an image given the desired quality level. For example, if something needed the &amp;ldquo;best quality&amp;rdquo; version of an image, and the &amp;ldquo;original&amp;rdquo; image is not available, the service will default to the &amp;ldquo;web&amp;rdquo; version (the idea is that some of these tiers will be optional depending on the need of the instances).&lt;/p&gt;
&lt;p&gt;This is all partially working at the moment, and I&amp;rsquo;m hoping to rework all this when I replace how stores and images relate to each other (This is what I&amp;rsquo;m starting on now, and why I built export now since this will be backwards incompatible). So for the moment the export simply consists of the &amp;ldquo;web&amp;rdquo; version.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve got unit tests working for this as well. I&amp;rsquo;m trying a new approach for unit testing in this project. Instead of using mocks, the tests are actually running against a fully instantiated versions of the services. There exists a &lt;code&gt;servicestest&lt;/code&gt; package which does all the setup (using temporary directories, etc) and tear down of these services. Each individual unit test gets the services from this package and will run tests against a particular one.&lt;/p&gt;
&lt;p&gt;This does mean all the services are available and exercised within the tests, making them less like unit tests and more like integrations tests. But I think I prefer this approach. The fact that the dependent services are covered gives me greater confidence that they&amp;rsquo;re working. It also means I can move things around without changing mocks or touching the tests.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s not to say that I&amp;rsquo;m not trying to keep each service their own component as much as I can. I&amp;rsquo;m still trying to follow best practice of component design: passing dependencies in explicitly when the services are created, for example. But setting them all up as a whole in the tests means I can exercise them while they serve the component being tested. And the dependencies are explicit anyway (i.e. no interfaces) so it makes sense keeping it that way for the tests as well. And it&amp;rsquo;s just easier anyway. 🤷&lt;/p&gt;
&lt;p&gt;Anyway, starting rework on images and stores now. Will talk more about this once it&amp;rsquo;s done.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Photo Bucket Update: More On Galleries</title>
      <link>https://lmika.org/2024/03/05/photo-bucket-update.html</link>
      <pubDate>Tue, 05 Mar 2024 21:43:05 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/03/05/photo-bucket-update.html</guid>
      <description>&lt;p&gt;Spent a bit more time working on Photo Bucket this last week&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;, particularly around galleries. They&amp;rsquo;re progressing quite well. I&amp;rsquo;m made some strides in getting two big parts of the UI working now: adding and removing images to galleries, and re-ordering gallery items via drag and drop.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll talk about re-ordering first. This was when I had to bite the bullet and start coding up some JavaScript. Usually I&amp;rsquo;d turn to &lt;a href=&#34;https://stimulus.hotwired.dev&#34;&gt;Stimulus&lt;/a&gt; for this but I wanted to give &lt;a href=&#34;https://blog.jim-nielsen.com/2023/html-web-components/&#34;&gt;HTML web components&lt;/a&gt; a try. And so far, they&amp;rsquo;ve been working quite well.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://cdn.uploads.micro.blog/25293/2024/moving-gallery-images.mov&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;The gallery page is generated server-side into the following HTML:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;main&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;pb-draggable-imageset&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;href&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/_admin/galleries/1/items&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;image-grid&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;pb-draggable-image&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;position&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;item-id&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;7&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;a&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;href&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/_admin/photos/3&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;img&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;src&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/_admin/img/web/3&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;a&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;pb-draggable-image&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;pb-draggable-image&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;position&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;1&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;item-id&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;4&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;a&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;href&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/_admin/photos/4&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;img&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;src&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/_admin/img/web/4&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;a&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;pb-draggable-image&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;pb-draggable-image&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;position&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;item-id&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;8&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;a&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;href&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/_admin/photos/1&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;img&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;src&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/_admin/img/web/1&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;a&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;pb-draggable-image&lt;/span&gt;&amp;gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;pb-draggable-imageset&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;main&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each &lt;code&gt;&amp;lt;pb-draggable-image&amp;gt;&lt;/code&gt; node is a direct child of an &lt;code&gt;&amp;lt;pb-draggable-imageset&amp;gt;&lt;/code&gt;. The idea is that the user can rearrange any of the &lt;code&gt;&amp;lt;pb-draggable-image&amp;gt;&lt;/code&gt; elements within a single &lt;code&gt;&amp;lt;pb-draggable-imageset&amp;gt;&lt;/code&gt; amongst themselves. Once the user has moved an image onto to another one, the image will signal its new position by firing a custom event. The containing &lt;code&gt;&amp;lt;pb-draggable-imageset&amp;gt;&lt;/code&gt; element is listening to this event and will respond by actually repositioning the child element and sending a JSON message to the backend to perform the move in the database.&lt;/p&gt;
&lt;p&gt;A lot of this was based on the MDN documentation for &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API&#34;&gt;drag and drop&lt;/a&gt; and it follows the examples quite closely. I did find a few interesting things though. My first attempt at this was to put it onto the &lt;code&gt;&amp;lt;pb-draggable-image&amp;gt;&lt;/code&gt; element, but I wasn&amp;rsquo;t able to get any drop events when I did. Moving the &lt;code&gt;draggable&lt;/code&gt; attribute onto the &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; element seemed to work. I not quite sure why this is. Surely I can&amp;rsquo;t think of any reason as to why it wouldn&amp;rsquo;t work. It may had something else, such as how I was initialising the HTTP components.&lt;/p&gt;
&lt;p&gt;Speaking of HTML components, there was a time where the custom component&amp;rsquo;s &lt;code&gt;connectedCallback&lt;/code&gt; method was being called before the child &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; elements were present in the DOM. This was because I had the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag in the the HTML head and configured to be evaluated during parsing. Moving it to the end of the body and loading it as a module fixed that issue. Also I found that moving elements around using &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/API/Element/before&#34;&gt;element.before&lt;/a&gt; and &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/API/Element/after&#34;&gt;element.after&lt;/a&gt; would actually call &lt;code&gt;connectedCallback&lt;/code&gt; and &lt;code&gt;disconnectedCallback&lt;/code&gt; each time, meaning that any event listeners registered within &lt;code&gt;connectedCallback&lt;/code&gt; would need to be de-registered, otherwise events would be handled multiple times. This book-keeping was slightly annoying, but it worked.&lt;/p&gt;
&lt;p&gt;Finally, there was moving the items with the database. I&amp;rsquo;m not sure how best to handle this, but I have that method that seems to work. What I&amp;rsquo;m doing is tracking the position of each &amp;ldquo;gallery item&amp;rdquo; using a &lt;code&gt;position&lt;/code&gt; field. This field would be 1 for the first item, 2 for the next, and so on for each item in the gallery. The result of fetching items would just order using this field, so as long as they&amp;rsquo;re distinct, they don&amp;rsquo;t need to be a sequence incrementing by 1, but I wanted to keep this as much as possible.&lt;/p&gt;
&lt;p&gt;The actual move involves two update queries. The first one will update the positions of all the items that are to shift left or right by one to &amp;ldquo;fill the gap&amp;rdquo;. The way it does this is that when an item is moved from position X to position Y, the value of &lt;code&gt;position&lt;/code&gt; between X and Y would be changed by +1 if X &amp;gt; Y, or by –1 if Y &amp;gt; X. This is effectively the same as setting position X to X + 1, and so on, but done using one &lt;code&gt;UPDATE&lt;/code&gt; statement. The second query just sets the position of item X to Y.&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s moving gallery items. I&amp;rsquo;m not sure how confident I am with this approach, but I&amp;rsquo;ve been testing this, both manually and by writing unit tests. It&amp;rsquo;s not quite perfect yet: I&amp;rsquo;m still finding bugs (I found some while coming up with these screencasts). Hopefully, I&amp;rsquo;ll be able to get to the bottom of them soon.&lt;/p&gt;
&lt;p&gt;The second bit of work was to actually add and remove images in the gallery themselves. This, for the moment, is done using a &amp;ldquo;gallery picker&amp;rdquo; which is available in the image details. Clicking &amp;ldquo;Gallery&amp;rdquo; while viewing an image will show the list of galleries in the system, with toggles on the left. The galleries an image already belongs to is enabled, and the user can choose the galleries they want the image to be in by switching the toggles on and off. These translate to &lt;code&gt;inserts&lt;/code&gt; and &lt;code&gt;remove&lt;/code&gt; statements behind the scenes.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://lmika.org/uploads/2024/adding-removing-images-to-galleries.mov&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;The toggles are essentially just HTML and CSS, and a bulk of the code was &lt;a href=&#34;https://www.w3schools.com/howto/howto_css_switch.asp&#34;&gt;taken from this example&lt;/a&gt;, with some tweaks. They look good, but I think I may need to make them slightly smaller for mouse and keyboard.&lt;/p&gt;
&lt;p&gt;I do see some downside with this interaction. First, it reverses the traditional idea of adding images to a gallery: instead of doing that, your selecting galleries for an image. I&amp;rsquo;m not sure if this would be confusing for others (it is modelled on how Google Photos works). Plus, there&amp;rsquo;s no real way to add images in bulk. Might be that I&amp;rsquo;ll need to add a way to select images from the &amp;ldquo;Photos&amp;rdquo; section and have a dialog like this to add or remove them all from a gallery. I think this would go far in solving both of these issues.&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s where things are. Not sure what I&amp;rsquo;ll work on next, but it may actually be import and export, and the only reason for this is that I screwed up the base model and will need to make some breaking changes to the DB schema. And I want to have a version of export that&amp;rsquo;s compatible with the original schema that I can deploy to the one and only production instance of Photo Bucket so that I can port the images and captions over to the new schema. More on this in the future, I&amp;rsquo;m sure.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;Apparently I&amp;rsquo;m more than happy to discuss work in progress, yet when it comes to talking about something I&amp;rsquo;ve finished, I freeze up. 🤷&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>Photo Bucket Galleries and Using the HTML Popover API</title>
      <link>https://lmika.org/2024/02/19/spent-a-bit.html</link>
      <pubDate>Mon, 19 Feb 2024 22:05:48 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/02/19/spent-a-bit.html</guid>
      <description>&lt;p&gt;Spent a bit more on Photo Bucket this evening. Tonight I started working on galleries, which&amp;rsquo;ll work more or less like albums.&lt;/p&gt;
&lt;p&gt;At the moment you can create a gallery and add a photo to it. Most of the work so far has been backend so the UI is pretty rough. Eventually you&amp;rsquo;ll be able to do things like rearrange photos within galleries, but for the moment they&amp;rsquo;ll just be added to the end.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://cdn.uploads.micro.blog/25293/2024/galleries.mov&#34; controls=&#34;controls&#34; preload=&#34;metadata&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;I did need to add some modals to the UI, such as the New Gallery model that&amp;rsquo;s shown for the gallery name. This gave me the opportunity to try out the new &lt;a href=&#34;https://12daysofweb.dev/2023/popover-api/&#34;&gt;popover API&lt;/a&gt;. And yeah, it does exactly what it says on the tin: add the &lt;code&gt;popover&lt;/code&gt; attribute to an element and it becomes a working popover (at least in the latest version of Vivaldi). Must say it&amp;rsquo;s impressive that this is now possible with HTML alone.&lt;/p&gt;
&lt;p&gt;The initial version of the modals used a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; for the popover target. And while that worked, there were some small annoyances. First was that the form within the popover didn&amp;rsquo;t get focus when the popover was displayed. It would be nice to click &amp;ldquo;New&amp;rdquo; and start typing out the gallery name. But this is a small thing that&amp;rsquo;s easily solvable with JavaScript, so it&amp;rsquo;s no big deal.&lt;/p&gt;
&lt;p&gt;The second, slightly larger one, was that dismissing the popover by clicking outside of it will not eat the input. If you were to click a sidebar link while the New Gallery model is opened, you&amp;rsquo;ll end up on that newly selected page. I&amp;rsquo;m not a fan of this. Dismissing the popover feels like its own user gesture, and I fear the user accidentally activating things when all they&amp;rsquo;re trying to do is dismiss the popover (it&amp;rsquo;s not in place now, but I am planning to dim the background when the Create Gallery modal is visible).&lt;/p&gt;
&lt;p&gt;Fortunately, there&amp;rsquo;s a simple solution to this. It turns out that replacing the &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; element with a &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element would solves both problems. It works seamlessly with the new popover attributes, yet showing the dialog will give focus to the form, and will eat the click when the user dismisses it.&lt;/p&gt;
&lt;p&gt;Perfect. Looks like I&amp;rsquo;ve delayed the need for JavaScript a little longer (it will come eventually; it always will).&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/02/12/spent-some-time.html</link>
      <pubDate>Mon, 12 Feb 2024 21:25:51 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/02/12/spent-some-time.html</guid>
      <description>&lt;p&gt;Spent some time this evening working on my image hosting tool. It&amp;rsquo;s slowly coming along, but wow do I suck at UI design (the &amp;ldquo;Edit Photo&amp;rdquo; screen needs some rebalancing).&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/screenshot-2024-02-12-at-10.24.08-pm.png&#34; width=&#34;600&#34; height=&#34;450&#34; alt=&#34;Screenshot of a browser showing an image admin section with a grid of images&#34;&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/screenshot-2024-02-12-at-10.24.16-pm.png&#34; width=&#34;600&#34; height=&#34;450&#34; alt=&#34;Screenshot of a browser showing a single image form with an image on the right and two text fields on the left&#34;&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/01/26/you-know-that.html</link>
      <pubDate>Fri, 26 Jan 2024 15:08:32 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/01/26/you-know-that.html</guid>
      <description>&lt;p&gt;Working on one of the admin sections of the project I was &lt;a href=&#34;https://lmika.org/2024/01/25/075025.html&#34;&gt;alluding to yesterday&lt;/a&gt;. Here&amp;rsquo;s a screencast of how it&amp;rsquo;s looking so far.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://cdn.uploads.micro.blog/25293/2024/photo-bucket-admin-v1.mov&#34; height=&#34;360&#34; width=&#34;640&#34; controls poster=&#34;https://lmika.org/uploads/2024/photo-bucket-admin-v1-poster.png&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;The styling and layout is not quite final. I&amp;rsquo;m focusing more on functionality, and getting layout and whitespace looking good always takes time. But compared to how it looked before I started working on it this morning, I think it&amp;rsquo;s a good start.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/01/10/in-the-end.html</link>
      <pubDate>Wed, 10 Jan 2024 20:49:46 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/01/10/in-the-end.html</guid>
      <description>&lt;p&gt;In the end it took significantly more time to &lt;a href=&#34;https://lmika.org/2024/01/05/for-reasons-that.html&#34;&gt;write about it&lt;/a&gt; then to actually do it, but the dot product approach seems to work.&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://cdn.uploads.micro.blog/25293/2024/hex-tracking.mov&#34; poster=&#34;https://lmika.org/uploads/2024/hex-tracking-poster.png&#34; controls&gt;&lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2024/hex-tracking.mov&#34;&gt;Download video&lt;/a&gt;&lt;/video&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2024/01/09/so-i-guess.html</link>
      <pubDate>Tue, 09 Jan 2024 06:55:23 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2024/01/09/so-i-guess.html</guid>
      <description>&lt;p&gt;So I guess today&amp;rsquo;s beginning with a game of &amp;ldquo;guess the secret password requirements.&amp;rdquo; 😒&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2024/34d3f95cad.jpg&#34; width=&#34;600&#34; height=&#34;600&#34; alt=&#34;A macOS login reset password modal, partially filled in, with a prompt saying that &#39;your password does not meet the requirements of this server&#39; without saying what the requirements are.&#34;&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/12/22/making-some-progress.html</link>
      <pubDate>Fri, 22 Dec 2023 12:29:15 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/12/22/making-some-progress.html</guid>
      <description>&lt;p&gt;Making some progress in learning &lt;a href=&#34;https://elm-lang.org/&#34;&gt;Elm&lt;/a&gt; for building frontends. Started working on a Connections clone, which I&amp;rsquo;m calling &amp;ldquo;Clonections&amp;rdquo;. This is what I&amp;rsquo;ve got so far:&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://cdn.uploads.micro.blog/25293/2023/clonections-screencast.mov&#34; controls width=&#34;1280&#34; height=&#34;720&#34; poster=&#34;https://lmika.org/uploads/2023/clonections-screencast-poster.png&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s been fun using Elm to build this. So far I&amp;rsquo;m liking the language. Of course, now I&amp;rsquo;ll have to come up with puzzles for this. 😐&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/12/01/spent-a-little.html</link>
      <pubDate>Sun, 03 Dec 2023 20:09:27 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/12/01/spent-a-little.html</guid>
      <description>&lt;p&gt;Spent a little more time working on my idea for Dynamo-Browse this week. Managed to get it somewhat feature complete this weekend:&lt;/p&gt;
&lt;video src=&#34;https://cdn.uploads.micro.blog/25293/2023/rel-item-example-1.mp4&#34; poster=&#34;https://lmika.org/uploads/2023/rel-item-example-1-poster.png&#34; width=&#34;1440&#34; height=&#34;1080&#34; controls&gt;
  &lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2023/rel-item-example-1.mp4&#34;&gt;Download Video&lt;/a&gt; 
&lt;/video&gt;
&lt;p&gt;I probably should say a few words on what it actually is. The idea is to make it quick and easy to run pre-canned queries based on the currently selected item and table.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s say you&amp;rsquo;ve got a table with customer information, and another table with subscription information, and they&amp;rsquo;re linked with some form of customer ID. If you wanted to see the subscriptions of a customer, up until now, you&amp;rsquo;d have to copy the customer ID to the paste-board, change the currently viewed table, then run a query to select the subscription with that customer ID. It&amp;rsquo;s not difficult but it&amp;rsquo;s extremely tedious.&lt;/p&gt;
&lt;p&gt;This change is meant to streamline this. Now, in a script function, you can define a &amp;ldquo;related item&amp;rdquo; provider which, if matched against the currently displayed table, will be given the currently selected item, and will return a list of queries that will display items related to the current item (depending on whatever definition of &amp;ldquo;related&amp;rdquo; will be). This will be presented to the user as a list. When the user chooses the item, the query will run and the results will be displayed.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example of the script used for the screencasts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ext.related_items(&amp;quot;business-addresses&amp;quot;, func(item) {
    return [
        {&amp;quot;label&amp;quot;: &amp;quot;Customer&amp;quot;, &amp;quot;query&amp;quot;: `city=$city`, &amp;quot;args&amp;quot;: {&amp;quot;city&amp;quot;: &amp;quot;Austin&amp;quot;}},
        {&amp;quot;label&amp;quot;: &amp;quot;Payment&amp;quot;, &amp;quot;query&amp;quot;: `address^=&amp;quot;3&amp;quot;`},
        {&amp;quot;label&amp;quot;: &amp;quot;Thing&amp;quot;, &amp;quot;table&amp;quot;: &amp;quot;inventory&amp;quot;, 
            &amp;quot;query&amp;quot;: `pk=$pk`, &amp;quot;args&amp;quot;: {&amp;quot;pk&amp;quot;: &amp;quot;01fca33a-5817-4c27-8a8f-82380584e69c&amp;quot;}},
    ]
})

ext.related_items(&amp;quot;inventory&amp;quot;, func(item) {
    sk := string(item.attr(&amp;quot;sk&amp;quot;))
    return [
        {&amp;quot;label&amp;quot;: &amp;quot;SK: &amp;quot; + sk, &amp;quot;table&amp;quot;: &amp;quot;business-addresses&amp;quot;, 
            &amp;quot;query&amp;quot;: `pk^=$sk`, &amp;quot;args&amp;quot;: {&amp;quot;sk&amp;quot;: sk}},
    ]
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice how the last &lt;code&gt;business-addresses&lt;/code&gt; item specifies the &amp;ldquo;inventory&amp;rdquo; table, and that the &amp;ldquo;inventory&amp;rdquo; provider actually uses an attribute of the item. Here&amp;rsquo;s a screencast of that working:&lt;/p&gt;
&lt;video src=&#34;https://cdn.uploads.micro.blog/25293/2023/rel-item-example-2.mp4&#34; poster=&#34;https://lmika.org/uploads/2023/rel-item-example-2-poster.png&#34; width=&#34;1440&#34; height=&#34;1080&#34; controls&gt;
  &lt;a href=&#34;https://cdn.uploads.micro.blog/25293/2023/rel-item-example-2.mp4&#34;&gt;Download Video&lt;/a&gt; 
&lt;/video&gt;
&lt;p&gt;This feature has been on the idea board for a while. I was trying to work out how best to handle the pre-canned queries, especially considering that they will likely be different for each item and table. Some ideas I had were adding additional UI elements that the user could use to configure these queries. These would go into the workspace file, a sort of an embedded database which is created for each session. This was pretty crappy, especially when you consider that workspaces usually only last until the user exists. It was only a few weeks ago when I considered using the scripting facilities to implement this (which, honestly, shows how much it remains under-utilised).&lt;/p&gt;
&lt;p&gt;Anyway, I&amp;rsquo;ve only just finished development of it. I&amp;rsquo;d still like to try it for the various related items I tend to use during my day-to-day. We&amp;rsquo;ll see how well it works out.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Idea For Mainboard Mayhem: A Remote Pickup</title>
      <link>https://lmika.org/2023/11/19/sort-of-inbetween.html</link>
      <pubDate>Sun, 19 Nov 2023 20:21:56 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/11/19/sort-of-inbetween.html</guid>
      <description>&lt;p&gt;Sort of in-between projects at the moment so I&amp;rsquo;m doing a bit of light stuff on Mainboard Mayhem. I had an idea for a new element: a remote control which, when picked up, will allow the player to toggle walls and tanks using the keyboard, much like the green and blue buttons.&lt;/p&gt;
&lt;p&gt;I used ChatGGT to come up with some artwork, and it produced something that was pretty decent.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/red-pixel-button-generated-by-dalle.png&#34; width=&#34;600&#34; height=&#34;600&#34; alt=&#34;Image generated from DALL-E with the prompt: pixel art of a remote control with a single red button styled like the tiles found in Chips Challange, rotated 45 degrees to the right.&#34;&gt;
&lt;figcaption&gt;Prompt: pixel art of a remote control with a single red button styled like the tiles found in Chips Challange, rotated 45 degrees to the right.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Only issue was that the image was huge — 1024 x 1024 — and the tiles in Mainboard Mayhem were only 32 x 32.&lt;/p&gt;
&lt;p&gt;I tried shrinking it down in Acorn, using various scaling algorithms. The closest that worked was bringing it down slowly to about 128 x 128 using Nearest Neighbour, than trying to go all the way down  to 32 x 32 using Lanczos. That worked, but it required true 32 bit colour to be recognisable, and I wanted to preserve the 16 colour palette used by the original Chips Challenge.&lt;/p&gt;
&lt;p&gt;So using the original image as a reference, I bit the bullet and drew my own in Acorn. You can see it here in this test level:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/mainboard-mayhem-example-levem.png&#34; width=&#34;600&#34; height=&#34;315&#34; alt=&#34;Example Mainboard Mayhem level showing the green and blue remote controls. The controls have 4 small buttons and one large bulbous button that is either blue or green, with a bit of phong and shadow applied&#34;&gt;
&lt;figcaption&gt;They&#39;re the elements that look like remote controls.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;It turn out okay. At least it&amp;rsquo;s recognisable. Anyway, I coded it up and gave it a bit of a try:&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://cdn.uploads.micro.blog/25293/2023/mainboard-mayhem-demo.mov&#34; poster=&#34;https://lmika.org/uploads/2023/mainboard-mayhem-demo-poster.png&#34; controls=&#34;controls&#34; playsinline=&#34;playsinline&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Yeah, it works well. When the player has the appropriate colour remote, they can hit either &lt;kbd&gt;Z&lt;/kbd&gt; or &lt;kbd&gt;X&lt;/kbd&gt; to toggle the green walls or blue tanks respectively. I really should add some indicators in the status bar to show which button to press.&lt;/p&gt;
&lt;p&gt;Not sure what I&amp;rsquo;ll do after this. The fun part was coming up with the element. But I guess I&amp;rsquo;ll have to come up with a few puzzles that use it.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/10/15/more-work-on.html</link>
      <pubDate>Sun, 15 Oct 2023 15:52:50 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/10/15/more-work-on.html</guid>
      <description>&lt;p&gt;More work on &lt;a href=&#34;https://mainboard.lmika.dev&#34;&gt;Mainboard Mayhem&lt;/a&gt; today.  Had a bit more success getting the Windows build into a releasable state.&lt;/p&gt;
&lt;p&gt;First thing was the app icon. &lt;a href=&#34;https://hjr265.me/blog/adding-icons-for-go-built-windows-executable/&#34;&gt;That blog post&lt;/a&gt; I talked about yesterday worked: I was able to set the icon of the executable. I did make a slight adjustment though. The post suggested using ImageMagick to produce the ICO file, but I wasn&amp;rsquo;t happy with how they looked. There were a lot of artefacts on the smaller icon sizes.&lt;/p&gt;
&lt;p&gt;So I looked around for an alternative, and found &lt;a href=&#34;https://github.com/leaanthony/winicon&#34;&gt;this package&lt;/a&gt; by Lea Anthony. He&amp;rsquo;s the maintainer of &lt;a href=&#34;https://wails.app&#34;&gt;Wails&lt;/a&gt;, a cross-platform toolkit for making browser-based GUI apps in Go, sort of like Electron but without bundling Chrome. In fact, most of the build for Mainboard Mayhem was put together by reading the Wails source code, so I trust he knows what his doing. And sure enough, his package produced a nicely scaled ICO file from a source PNG image. Better yet, it was distributed as a Go package, so I could no need to install and shell-out to run it: I could just integrated it directly into the project&amp;rsquo;s build tool.&lt;/p&gt;
&lt;p&gt;Using &lt;a href=&#34;github.com/akavel/rsrc&#34;&gt;rsrc&lt;/a&gt; to generate the SYSO file with the icon worked as expected: Go did pick it up and embed it into the executable. I did have some trouble getting the Go compiler to pick up these files at first. In short, they need to be in the same directory as the &lt;code&gt;main&lt;/code&gt; package. So if you&amp;rsquo;re running &lt;code&gt;go build ./cmd/thing&lt;/code&gt;, make sure the SYSO files are in &lt;code&gt;./cmd/thing&lt;/code&gt;. Other than that, no real issues here.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/screenshot-2023-10-15-at-10.43.28-am.png&#34; width=&#34;600&#34; height=&#34;407&#34; alt=&#34;Screenshot of Windows 10 file browser with mainboard.exe shown with the app icon, plus a few sdl DLLs&#34;&gt;
&lt;figcaption&gt;A beautiful site: Mainboard.exe with the embedded app icon&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;One last thing I had to deal with was the console window. Running a Go app in Windows shows the console by default. Perfectly fine for command line tools, but less so for games:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/screenshot-2023-10-15-at-11.10.32-am.png&#34; width=&#34;600&#34; height=&#34;407&#34; alt=&#34;Screenshot of Mainboard Mayhem running with the console window open in the background showing log messages&#34;&gt;
&lt;figcaption&gt;Mainboard Mayhem with that annoying console window. Even the log messages are dull (well, unless you&#39;re working on the app).&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So I had to find a way to hide the console on launch. Since Mainboard Mayhem is using &lt;a href=&#34;http://libsdl.org&#34;&gt;SDL&lt;/a&gt;, I&amp;rsquo;m actually using &lt;a href=&#34;https://www.mingw-w64.org&#34;&gt;MinGW&lt;/a&gt; to cross-compile the Windows release on an Ubuntu build runner. The documentation for MinGW suggests adding &lt;code&gt;-mwindows&lt;/code&gt; as a linker option to hide the console:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# What I was doing before, which didn&#39;t work
CGO_ENABLED=1 \
CC=&amp;quot;x86_64-w64-mingw32-gcc&amp;quot; \
GOOS=&amp;quot;windows&amp;quot; \
CGO_LDFLAGS=&amp;quot;-mwindows -L…&amp;quot; \
go build -o dist/cclm/mainboard.exe ./cmd/cclm&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This didn&amp;rsquo;t actually work when I tried it: launching the app kept bringing up the console. Turns out what I should&amp;rsquo;ve done was follow the advice of many &lt;a href=&#34;https://stackoverflow.com/a/36728885/1505851&#34;&gt;Stack Overflow&lt;/a&gt; answers, and set &lt;code&gt;-ldflags &amp;quot;-H=windowsgui&amp;quot;&lt;/code&gt; on the Go command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# This works
CGO_ENABLED=1 \
CC=&amp;quot;x86_64-w64-mingw32-gcc&amp;quot; \
GOOS=&amp;quot;windows&amp;quot; \
CGO_LDFLAGS=&amp;quot;-L…&amp;quot; \
go build -ldflags &amp;quot;-H=windowsgui&amp;quot; -o dist/cclm/mainboard.exe ./cmd/cclm&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works even without the &lt;code&gt;-mwindows&lt;/code&gt; switch. Not completely sure why though. I guess MinGW is not actually being used for linking? Or maybe &lt;code&gt;-m&lt;/code&gt; only works with C header files? Don&amp;rsquo;t know. 🤷 But doesn&amp;rsquo;t matter: the console no longer shows up on launch.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/screenshot-2023-10-15-at-10.44.06-am.png&#34; width=&#34;600&#34; height=&#34;407&#34; alt=&#34;Screenshot of Mainboard Mayhem running, but with no console window. File browser running in the background&#34;&gt;
&lt;figcaption&gt;Mainboard Mayhem without the console window. A much nicer experience now.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Finally, there was testing it all, and for this I just bit the bullet and set-up a Windows 10 virtual machine in Azure. The rate is something like $0.16 AUD an hour, an easy decision compared to spending time trying to get a VM with Windows 10 running on my machine.&lt;/p&gt;
&lt;p&gt;One remaining thing that&amp;rsquo;s slightly annoying is Windows Defender refusing to launch it after download, doing effectively the same thing as Gatekeeper on MacOS does:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/screenshot-2023-10-15-at-10.43.38-am.png&#34; width=&#34;600&#34; height=&#34;407&#34; alt=&#34;Screenshot of Windows Defender SmartScreen indicating that it&#39;s refusing to start an unrecognised app. A single button saying &#39;Don&#39;t Run&#39; appears at the bottom of the dialog.&#34;&gt;
&lt;figcaption&gt;Gatekeeper a.la. Microsoft.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I&amp;rsquo;m sure there&amp;rsquo;s a way around it but it&amp;rsquo;s probably not worth learning about it at this stage. It&amp;rsquo;s easy enough to dismiss: click &amp;ldquo;More Info&amp;rdquo; and the click &amp;ldquo;Run Anyway&amp;rdquo;:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/screenshot-2023-10-15-at-10.43.43-am.png&#34; width=&#34;600&#34; height=&#34;407&#34; alt=&#34;Screenshot of Windows Defender SmartScreen indicating that it&#39;s refusing to start an unrecognised app, saying the name of the executable and that the publisher is unknown. Two buttons saying &#39;Run Anyway&#39; and &#39;Don&#39;t Run&#39; appears at the bottom of the dialog.&#34;&gt;
&lt;figcaption&gt;Clicking &#39;More Info&#39; gives you a way to launch the app.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;But other than that, I think the Windows version of Mainboard Mayhem is ready. I&amp;rsquo;ve updated the website to include the Windows archive if anyone&amp;rsquo;s interested.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/10/14/golang-weekly-had.html</link>
      <pubDate>Sat, 14 Oct 2023 21:20:04 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/10/14/golang-weekly-had.html</guid>
      <description>&lt;p&gt;Spent some time today on Mainboard Mayhem, trying to finish the Windows build. I&amp;rsquo;ve actually got Windows version of the game being built for a while now. I just haven&amp;rsquo;t published them, mainly because I haven&amp;rsquo;t got the app icon set-up yet.&lt;/p&gt;
&lt;p&gt;But this week, &lt;a href=&#34;https://golangweekly.com/issues/479&#34;&gt;Golang Weekly&lt;/a&gt; had a link to &lt;a href=&#34;https://hjr265.me/blog/adding-icons-for-go-built-windows-executable/&#34;&gt;a blog post by Mahmud Ridwan&lt;/a&gt; on how to do so. It looked pretty straightforward, so I thought I&amp;rsquo;d give it a try.&lt;/p&gt;
&lt;p&gt;And yeah, the instructions themselves were easy enough, and I wish I could say if they worked or not. But in order to test it, I need a Windows machine. And I don&amp;rsquo;t have one, and I wasn&amp;rsquo;t about to get one just for this.&lt;/p&gt;
&lt;p&gt;So I tried setting up Windows in a VM using &lt;a href=&#34;getutm.app&#34;&gt;UTM&lt;/a&gt;. I got this far:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/screenshot-2023-10-14-at-3.58.00-pm.png&#34; width=&#34;600&#34; height=&#34;484&#34; alt=&#34;A blue Windows install screen within a MacOS window showing a spinner and the message &#39;Just a moment…&#39; underneath&#34;&gt;
&lt;p&gt;Yeah, this infinite spinner has been staring at me pretty much all day. I got a Windows 10 installer ISO using &lt;a href=&#34;https://github.com/TuringSoftware/CrystalFetch&#34;&gt;CrystalFetch&lt;/a&gt;, and it  &lt;em&gt;seemed&lt;/em&gt; to work. But it just doesn&amp;rsquo;t want to boot up for the first time.&lt;/p&gt;
&lt;p&gt;Not actually sure what the problem is. The error message seems to suggest that it&amp;rsquo;s having trouble connecting to the internet. Might be that? Or maybe the installation didn&amp;rsquo;t complete properly? Could be anything. 🤷&lt;/p&gt;
&lt;p&gt;So no luck getting this tested yet. I&amp;rsquo;m wondering if it might be easier to forget virtualisation and just launch a Windows instance in the cloud somewhere instead.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Mainboard Mayhem</title>
      <link>https://lmika.org/2023/08/30/project-update-on.html</link>
      <pubDate>Wed, 30 Aug 2023 22:27:50 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/08/30/project-update-on.html</guid>
      <description>&lt;p&gt;Project update on Mainboard Mayhem, my Chip&amp;rsquo;s Challenge fan game. I didn&amp;rsquo;t get it finished in time for the release deadline, which was last weekend. I blame work for that. We&amp;rsquo;re going through a bit of a crunch at the moment, and there was a need to work on the weekend.&lt;/p&gt;
&lt;p&gt;The good news is that there wasn&amp;rsquo;t much left to do, and after a few more evenings, I&amp;rsquo;m please to say that it&amp;rsquo;s done. The game is finish, and ready for release.&lt;/p&gt;
&lt;p&gt;So here it is: &lt;a href=&#34;https://mainboard.lmika.dev&#34;&gt;Mainboard Mayhem: A Chip&amp;rsquo;s Challenge fan game&lt;/a&gt; (and yes, that&amp;rsquo;s its full title).&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/screenshot-2.png&#34; width=&#34;600&#34; height=&#34;455&#34; alt=&#34;Screenshot of Mainboard Mayhem&#34;&gt;
&lt;p&gt;At the moment it&amp;rsquo;s only available for MacOS. It should work on both Intel and Apple Silicon Macs, although I&amp;rsquo;ve only tested on my M2 Mac Mini running Ventura.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s good to finally see this project done. It&amp;rsquo;s been in development for about last ten years, and I spent half of that time wondering whether it was worth getting it finished it at all. Not committing to anything meant any work I did do on it was pretty aimless, and I always felt like I was wasting my time. Giving myself three weeks to either kill it, or release it helped a lot. I&amp;rsquo;ll start making deadlines for all the other unfinished projects I&amp;rsquo;m working on.&lt;/p&gt;
&lt;p&gt;As to what that next project will be, I not sure at this stage. Part of me wants to wait until this crunch time ends, but I suspect I&amp;rsquo;ll get antsy before then and start work on something else. I&amp;rsquo;ll keep you posted one way or the other.&lt;/p&gt;
&lt;p&gt;But for now, if you happen to give it a try, thank you and I hope you enjoy it.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/avatar.png&#34; width=&#34;100&#34; height=&#34;100&#34; alt=&#34;The app icon of Mainboard Mayhem&#34; class=&#34;block-center&#34;&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/08/20/project-update-for.html</link>
      <pubDate>Sun, 20 Aug 2023 11:00:37 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/08/20/project-update-for.html</guid>
      <description>&lt;p&gt;Project update for Mainboard Madness. Well, today&amp;rsquo;s the deadline for getting the thing code complete, and what a surprised, it&amp;rsquo;s not finished.&lt;/p&gt;
&lt;p&gt;To be fair, it&amp;rsquo;s pretty close. All the levels are more or less done, and the beats of the in-game lore have been added. It all just needs tightening up a little. I spent today working on the end-game phase, which mainly involved coding up the credit sequence, and making sure I include credits for those involved in the original game (and who&amp;rsquo;s artwork I lifted).&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/mainboard-mayhem-credit-sequence.png&#34; width=&#34;600&#34; height=&#34;455&#34; alt=&#34;Mainboard mayhem credit sequence showing the final credit message &#39;Thanks for playing&#39;&#34;&gt;
&lt;p&gt;The work remaining is to finish one or two game elements, adding a proper app icon, and finishing off the website. I&amp;rsquo;m wondering whether to add sound, but I feel bad enough taking the artwork from the original game, I rather not take the sound effects as well. That will mean the game will remain silent for the time being, but I can probably live with that for now.&lt;/p&gt;
&lt;p&gt;I think we&amp;rsquo;re still on track for getting this finished by this time next week. Last dash to the finish line, then I can put this 9 year project to rest for a while.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/08/13/small-project-update.html</link>
      <pubDate>Sun, 13 Aug 2023 13:36:44 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/08/13/small-project-update.html</guid>
      <description>&lt;p&gt;Small project update on my Chips Challenge fan game.&lt;/p&gt;
&lt;p&gt;Started working on the final level. I was dreading this a little, thanks to my awful level design skills, but I made a great start to it this morning and it&amp;rsquo;s actually coming along pretty well. It&amp;rsquo;s a good opportunity to use all the elements that I didn&amp;rsquo;t get a chance to use in any of the other puzzles, and it&amp;rsquo;s also shaping up to be one that has a bit of climax.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also started working on the website, which is little more than just a landing page. This meant finally coming up with a name. I&amp;rsquo;ve chosen &amp;ldquo;Mainboard Mayhem&amp;rdquo; which is… okay, but it&amp;rsquo;s one that&amp;rsquo;s been rattling around in my head for a while, and I really couldn&amp;rsquo;t use anything close to &amp;ldquo;Chips Challenge&amp;rdquo;. I&amp;rsquo;m already using the tile-set from the original game, I rather not step on any more intellectual property.&lt;/p&gt;
&lt;p&gt;Anyway, one more week of development left to go. Still need to setup the app icon, finish all the levels, and maybe add a menu. Then I think we&amp;rsquo;re code complete.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/08/05/working-on-my.html</link>
      <pubDate>Sat, 05 Aug 2023 13:43:19 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/08/05/working-on-my.html</guid>
      <description>&lt;p&gt;Working on my Chips Challenge &amp;ldquo;fan game&amp;rdquo; this morning. Added the notion of &amp;ldquo;lower thirds,&amp;rdquo; which will show text at the bottom of the play field. I&amp;rsquo;m hoping to use it for narrative or way-finding, like here in this hub level:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/screenshot-2023-08-05-at-1.38.39-pm.png&#34; width=&#34;600&#34; height=&#34;455&#34; alt=&#34;Demonstration of the lower third indicating the direction of movement towards tutorial levels in a hub map&#34;&gt;
&lt;p&gt;Also working on puzzle design. There&amp;rsquo;s about 19 or so &amp;ldquo;real&amp;rdquo; puzzles but I&amp;rsquo;m wondering if it&amp;rsquo;s worth adding a few tutorial ones for those that have never played the original Chip Challenge before. I&amp;rsquo;ve done about 5 such puzzles and I think I need to add maybe 3 or 4 more to cover everything I&amp;rsquo;m hoping to demonstrate. I wish I liked puzzle design more than I like tinkering on the engine.&lt;/p&gt;
&lt;p&gt;Of course, the big question is why I&amp;rsquo;m working on this at all. There is, for lack of a better word, a vision for this, in terms of narrative and structure, but this project has been in development on and off for about 9 years or so, and I&amp;rsquo;m wondering if it&amp;rsquo;s time to just stop working on it altogether. I really am starting to get sick of it, in a way. And yet, this project has shown remarkable staying power over that time that I feel like if I don&amp;rsquo;t actually wrap it up, it&amp;rsquo;ll just continued to be worked on. It feels like the only way to end this project is to finish it, in one way or another.&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;ll set myself a dead-line: something releasable in two weeks, and actually released a week after that. After that, no more! I&amp;rsquo;ll work on something else.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/07/29/attempting-app-icon.html</link>
      <pubDate>Sat, 29 Jul 2023 10:28:48 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/07/29/attempting-app-icon.html</guid>
      <description>&lt;p&gt;Attempting to design an app icon for a Chips Challenge fan game I&amp;rsquo;m working on. Going for something that looks like the fireball sprite in the original game with a hint more realism and tinted in the colour blue. For reference, here&amp;rsquo;s the original fireball sprite:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/screenshot-2023-07-29-at-10.13.14-am.png&#34; width=&#34;160&#34; height=&#34;160&#34; alt=&#34;Fireball sprite in Chips Challenge&#34;&gt;
&lt;p&gt;And here&amp;rsquo;s my attempt:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/blue-plasma.png&#34; width=&#34;384&#34; height=&#34;384&#34; alt=&#34;Blue plasma shaped like the fireball sprite in Chips Challenge&#34;&gt;
&lt;p&gt;I started with Stable Diffusion to get the base image:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/unknown.jpg&#34; width=&#34;384&#34; height=&#34;384&#34; alt=&#34;Stable Diffusion production of the image described in the caption&#34;&gt;
&lt;figcaption&gt;Prompt: a blue plasma fireball shaped like a throwing star with four points on a white background, pixel art&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Then imported into Acorn to rotate it, colourise it, and distort it to look a bit closer to the original sprite.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/screenshot-2023-07-29-at-10.16.09-am.png&#34; width=&#34;600&#34; height=&#34;478&#34; alt=&#34;Screenshot of Acorn showing the image produced by Stable Diffusion rotated and the effects applied to produced the final image.&#34;&gt;
&lt;p&gt;Desaturating the original image got rid of the purple centre, then applying the Glowhoo and Hue Adjust effect recolourised it to the blue I was looking for (I&amp;rsquo;m not sure what the Glowhoo effect does, but it seems to adjust the colour based on the pixel intensity, so it was good enough for what I wanted). Finally, I added a Twirl Distortion effect to achieve the slight warp in the star.&lt;/p&gt;
&lt;p&gt;And yeah, it&amp;rsquo;s not going to win any design awards, but it&amp;rsquo;s good enough for now.&lt;/p&gt;
&lt;p&gt;Oh, and just for kicks, here was my first attempt of producing the sprite using Affinity Designer.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/plasma.png&#34; width=&#34;400&#34; height=&#34;400&#34; alt=&#34;First attempt at the blue plasma logo, crafted within Affinity Designer&#34;&gt;
&lt;p&gt;That&amp;rsquo;s &lt;em&gt;definitely&lt;/em&gt; not going to win any design awards. 😂&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/07/26/224328.html</link>
      <pubDate>Wed, 26 Jul 2023 22:43:28 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/07/26/224328.html</guid>
      <description>&lt;p&gt;Success! Managed to get a Go app built, signed, and notarised all from within a GitHub Action. It even cross-compiles to ARM, which is something considering that it&amp;rsquo;s using &lt;a href=&#34;https://www.libsdl.org&#34;&gt;SDL&lt;/a&gt;. Here&amp;rsquo;s the test app being downloaded and launched in a VM (ignore the black window, the interesting part is the title).&lt;/p&gt;
&lt;p&gt;&lt;video controls=&#34;controls&#34; src=&#34;https://cdn.uploads.micro.blog/25293/2023/cclm-download.mov&#34; poster=&#34;https://lmika.org/uploads/2023/4144e2fc7b.png&#34; alt=&#34;Video of an app being downloaded, and launched successfully. The app is a blank screen&#34; width=&#34;600&#34; height=&#34;422&#34;&gt;&lt;/video&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/07/23/spent-most-of.html</link>
      <pubDate>Sun, 23 Jul 2023 09:33:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/07/23/spent-most-of.html</guid>
      <description>&lt;p&gt;Spent most of the weekend going down various rabbit holes to get a Go application signed and notarised as a MacOS app. I&amp;rsquo;m trying to do this in a way that would make this easy to automate using GitHub Actions. This means things like no implicit access to the system keychain: I want to make a temporary keychain, add my secret stuff to it, then delete it once signing and notarisation is done.&lt;/p&gt;
&lt;p&gt;It also means no XCode GUI either: command line stuff only. Not that I had much hope of using XCode here anyway, since this is a Go application.&lt;/p&gt;
&lt;p&gt;But that&amp;rsquo;s fine, preferable even. I&amp;rsquo;ve never liked all the manual steps needed to get code signing work with XCode. What are you going to do when you change systems? Would you remember all the steps you took several years ago, when you last setup developer certificates?&lt;/p&gt;
&lt;p&gt;So this is why I&amp;rsquo;m trying to get it working with the terminal. But it&amp;rsquo;s not easy. Lots of esoteric commands that I need to learn and be made aware of. Just hope it&amp;rsquo;s not a huge waste of time.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/05/21/ive-been-working.html</link>
      <pubDate>Sun, 21 May 2023 21:52:03 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/05/21/ive-been-working.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been working on Micropub Checkins over the last week. It&amp;rsquo;s been a bit of a rush trying to get it into a usable state for an upcoming trip. And by &amp;ldquo;usable&amp;rdquo;, I mean a form that I can tolerate, and when it comes to projects like this, I can tolerate quite a lot. It can have a really dodgy UI (which this does) and miss some really important features that are annoying to work around; but if it works, and doesn&amp;rsquo;t loose data, I&amp;rsquo;ll be fine with it.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/1977e67e66.jpg&#34; width=&#34;200&#34; height=&#34;422&#34; alt=&#34;The main screen showing three check-ins&#34; /&gt;
&lt;figcaption&gt;The main screen showing the recent check-ins. Note the lock next to some of them. These won&#39;t be published until the locks are removed.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The last week was dedicated to making the act of checking in distinct from publishing it. Until now, check-ins were published the minute they were entered, meaning that you cannot check-in somewhere unless you&amp;rsquo;re comfortable with people knowing where you are the minute you do. Yes, some people like it that way, but not me. And I&amp;rsquo;m aware that this&amp;rsquo;ll only be the case if people are following my check-in blog, which I&amp;rsquo;m doubtful of.&lt;/p&gt;
&lt;p&gt;So pressing the floating action button and choosing a check-in type now starts the flow of a new check-in that will get saved in an SQLite database. You can edit the check-in whenever you like, so long as it&amp;rsquo;s not published. Currently there&amp;rsquo;s no real way of deleting a check-in unless it&amp;rsquo;s been published. This is a bit dodgy, but it&amp;rsquo;s a good example of how tolerant I am with working around these feature gaps for the moment.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/c083b6b602.jpg&#34; width=&#34;200&#34; height=&#34;422&#34; alt=&#34;The new styled edit screen with a title, description, rating, and a ready for check-in switch&#34; /&gt;
&lt;figcaption&gt;The newly styled edit screen. Notice the rating field, which will appear for eateries.&lt;/figcpation&gt;
&lt;/figure&gt;
&lt;p&gt;Check-ins can be published by tapping the upward facing button on the main screen. Any check-in with a lock is private and will not be published until you toggle the &amp;ldquo;Ready to publish&amp;rdquo; switch in the properties. Doing so will not change the date of the check-in: it will still have the date and time that check-in was created.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/41ce8e540e.jpg&#34; width=&#34;200&#34; height=&#34;422&#34; alt=&#34;The target list screen, showing two targets: one test target, and one for my real check-in blog&#34; /&gt;
&lt;figcaption&gt;The targets are currently hard-coded but they can be turned on or off. I had a bit of trouble publishing a check-in to two targets, so I&#39;m not sure if I&#39;ll keep multi-target publishing.
&lt;/figure&gt;
&lt;p&gt;On the subject of publishing, I had some issues with Dart&amp;rsquo;s date and time methods. The method on the DateTime class used to produce an ISO-8501 date-stamp don&amp;rsquo;t include the time-zone if the date and time is not in UTC. This is important as I want the post date and time to be as close to the check-in time as possible, and in the time-zone of the phone. DateTime knows all this, including what the time-zone we&amp;rsquo;re in. So why didn&amp;rsquo;t the developers include it in the ISO-8501 date-time string?&lt;/p&gt;
&lt;p&gt;This is really strange. Fortunately, ChatGPT stepped in to help out, writing a function which will add the time-zone offset to the ISO-8501 date-time string:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String formatTimeZoneOffset(Duration offset) {
  String sign = offset.isNegative ? &#39;-&#39; : &#39;+&#39;;
  int hours = offset.inHours.abs();
  int minutes = (offset.inMinutes.abs() % 60);

  return &#39;$sign${_padZero(hours)}:${_padZero(minutes)}&#39;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Honestly, ChatGPT has been so helpful over the past week with this project, I probably should give it a credit if I get this polished enough to release.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/05/13/back-working-on.html</link>
      <pubDate>Sat, 13 May 2023 12:25:17 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/05/13/back-working-on.html</guid>
      <description>&lt;p&gt;Back working on Micropub Checkin.  Re-engineered the home page to now include a list of what would eventually be check-ins — both historical and soon to be published — complete with the check-in type emoji as the icon:&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/843ca0bc72.jpg&#34; width=&#34;200&#34; height=&#34;444.33&#34; alt=&#34;Main screen for Micropub Checkin&#34; /&gt;
&lt;figcaption&gt;Main screen for Micropub Checkin&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The same list of emoji icons now adorn the check-in type picker as well (except for the airplane one which seems to always be shown as what I can only describe as the “Wingding” representation):&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/941486fd3a.jpg&#34; width=&#34;200&#34; height=&#34;444.33&#34; alt=&#34;The check-in type picker&#34; /&gt;
&lt;figcaption&gt;The check-in type picker&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I went around a bit trying to work out how best to use these emojis icons in the &lt;code&gt;leading&lt;/code&gt; slot of the &lt;code&gt;ListTile&lt;/code&gt; widget. I expored trying to convert them to &lt;code&gt;IconData&lt;/code&gt;, but it turns out just using a &lt;code&gt;Text&lt;/code&gt; widget with a large font worked well. I wrapped in in a &lt;code&gt;Widget&lt;/code&gt; type with a fixed font-size and so far it looks quite good, at least in the emulator:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class EmojiIcon extends StatelessWidget {
  final String emoji;

  const EmojiIcon({super.key, required this.emoji});

  Widget build(BuildContext context) {
    return Text(emoji, style: TextStyle(fontSize: 26.0));
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also started working on a &lt;a href=&#34;https://pub.dev/documentation/bloc/latest/bloc/Cubit-class.html&#34;&gt;Cubit&lt;/a&gt; to handle state for the main page. I had a bit of trouble working ont where the soon-to-be database call to get the list of checkins should go in the cubit. After asking ChatGPT, it looks like the initializer is the best place for it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class CheckinListCubit extends Cubit&amp;lt;CheckinListState&amp;gt; {

  CheckinListCubit(): super(LoadingCheckinListState()) {
    loadCheckinList();
  }

  void loadCheckinList() async {
    var listOfCheckins = await read_database(); 
    emit(FoundCheckinListState(checkins));
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I’ve got some scaffolding code in place to simulate this, and so far it seems to work.&lt;/p&gt;
&lt;p&gt;I need to start working on the database layer and having the ability to edit and delete check-ins before they&amp;rsquo;re published. I think I&amp;rsquo;ll tackle that next.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Building F5 To Run</title>
      <link>https://lmika.org/2023/04/16/building-f-to.html</link>
      <pubDate>Sun, 16 Apr 2023 10:36:18 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/04/16/building-f-to.html</guid>
      <description>&lt;p&gt;At the risk of talking about something that I&amp;rsquo;ve only just started, I&amp;rsquo;d thought today I write about what I&amp;rsquo;m working on right now.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been going through my digital archives this weekend, trying to get it into something more permenant than the portable USB drives it&amp;rsquo;s currently stored on. Amongst all that stuff is a bunch of QBasic apps and games I wrote way back when I was a kid. Over the years it&amp;rsquo;s laid dormant but I do like revising them from time to time.&lt;/p&gt;
&lt;p&gt;Is it a form of nostalgia? An attempts to live past glories? Maybe? I was pretty proud of them at the time, much like anyone else that&amp;rsquo;s proud of their early stuff while they&amp;rsquo;re leaning to code. And I know about the risk of living in the past at the expense of the present. But I also know that if I get rid of them, I&amp;rsquo;d regret it. I already regret loosing the things from the archive so far, due to bad disks or just missing things while copying them from portable hard-drive to portable hard-drive. I don&amp;rsquo;t want to loose any more.&lt;/p&gt;
&lt;p&gt;So in an act of posterity, I&amp;rsquo;d figured it&amp;rsquo;s time to coat them in amber and put them online. So that&amp;rsquo;s what I&amp;rsquo;m doing now.&lt;/p&gt;
&lt;p&gt;These apps run without issue in DosBox, and hearing about how the Wayback Machine has managed to make a bunch of DOS games playable within the browser, I wondered if I could do something similar. Anything that deals with virtualisation is always going to be a little bit involved. I guess one thing going for these is that they were written for a pretty slow machine and a pretty thin OS that would be trivial for modern hardware to emulate. The apps themselves, even compiled to an EXE file, are not very taxing on the hardware back then either. But I still expected to do a bit of heavy lifting myself.&lt;/p&gt;
&lt;p&gt;How wrong I was! After a tiny bit of research — and by tiny I mean one &lt;a href=&#34;https://www.ecosia.org/&#34;&gt;Ecosia&lt;/a&gt; search — I managed to find a JavaScript library called &lt;a href=&#34;https://js-dos.com/&#34;&gt;JS-Dos&lt;/a&gt; which provides a DosBox emulator that&amp;rsquo;s runnable from a browser. All I need to do is prepare a bundle on what I want to run (more on that below) and with a bit of JavaScript, I can start a DosBox machine in the browser and mount it to a HTML element. The library does all the work.&lt;/p&gt;
&lt;h2 id=&#34;how-to-use-js-dos&#34;&gt;How To Use JS-Dos&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s still early days, but here&amp;rsquo;s what I learnt about using the library so far.&lt;/p&gt;
&lt;p&gt;First, the library comes as a NPM package, or can be loaded from their CDN in the form of a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; import.
I first tried using the NPM package, but I didn&amp;rsquo;t know the appropriate &lt;code&gt;import&lt;/code&gt; statement to use, and the documentation was not forthcoming on this front.&lt;/p&gt;
&lt;p&gt;So I went with the CDN approach. I&amp;rsquo;m using &lt;a href=&#34;https://gohugo.io/hugo-pipes/&#34;&gt;Hugo Pipes&lt;/a&gt; to fetch the remote JavaScript file and make a local bundle so I can host it from the site itself. It comes with some CSS which I also need to get (note, I&amp;rsquo;m using parenthesis instead of curly braces here as I&amp;rsquo;m not sure how to include two curly braces in a code-block).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(( $jsDosCSS := resources.GetRemote &amp;quot;https://js-dos.com/v7/build/releases/latest/js-dos/js-dos.css&amp;quot; ))
&amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;(( $jsDosCSS.RelPermalink ))&amp;quot;&amp;gt;
        
(( $jsDosJS := resources.GetRemote &amp;quot;https://js-dos.com/v7/build/releases/latest/js-dos/js-dos.js&amp;quot; ))
&amp;lt;script src=&amp;quot;(( $jsDosJS.RelPermalink ))&amp;quot; defer&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also needed to get an appropriate wdosbox emulator. This comes in the form of a WASM file, plus a small JavaScript file which
I assume is some simple bootstrapper.  I&amp;rsquo;ve downloaded these and stored them in the &lt;code&gt;static/emulators&lt;/code&gt; directory of my Hugo project.
The JSDos library loads them on demand and I needed to set the URL path prefix for these two files so that JSDos knows where to get them:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emulators.pathPrefix = &#39;/emulators/&#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, I needed to build a bundle. These are the DOS programs that are launched with DosBox. They&amp;rsquo;re effectively just Zip files holding some metadata, the DOS executable, and any files needed for the program to run.  There&amp;rsquo;s some &lt;a href=&#34;https://js-dos.com/v7/build/docs/jsdos-bundle&#34;&gt;basic information&lt;/a&gt; about how to make them, and there&amp;rsquo;s &lt;a href=&#34;https://dos.zone/studio&#34;&gt;even an online tool&lt;/a&gt; which will take on a lot of the tedious work. I&amp;rsquo;ve used it to make a couple of test bundles and it works quite well. I&amp;rsquo;d like to eventually make my bundles myself but I&amp;rsquo;ll stick with the tool for the time being, at least until I&amp;rsquo;ve got a DosBox configuration that I&amp;rsquo;m happy with. One thing the tool does is give you the ability to define an overlay so that these DOS apps are usable from within a mobile browsers. I&amp;rsquo;ll see if I can get away from needing these overlays at this stage. I&amp;rsquo;m not expecting anyone with a mobile app to try these out.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;The contents of .jsdos/dosbox.conf for the test bundle `logo-2.jsdos`&lt;/summary&gt;
&lt;pre&gt;&lt;code&gt;[sdl]
autolock=false

fullscreen=false
fulldouble=false
fullresolution=original
windowresolution=original
output=surface
sensitivity=100
waitonerror=true
priority=higher,normal
mapperfile=mapper-jsdos.map
usescancodes=true
vsync=false
[dosbox]
machine=svga_s3

language=
captures=capture
memsize=16
[cpu]
core=auto
cputype=auto
cycles=max

cycleup=10
cycledown=20
[mixer]
nosound=false
rate=44100

blocksize=1024
prebuffer=20

[render]
# frameskip: How many frames DOSBox skips before drawing one.
#    aspect: Do aspect correction, if your output method doesn&#39;t support scaling this can slow things down!.
#    scaler: Scaler used to enlarge/enhance low resolution modes.
#              If &#39;forced&#39; is appended, then the scaler will be used even if the result might not be desired.
#            Possible values: none, normal2x, normal3x, advmame2x, advmame3x, advinterp2x, advinterp3x, hq2x, hq3x, 2xsai, super2xsai, supereagle, tv2x, tv3x, rgb2x, rgb3x, scan2x, scan3x.

frameskip=0
aspect=false
scaler=none

[midi]
#     mpu401: Type of MPU-401 to emulate.
#             Possible values: intelligent, uart, none.
# mididevice: Device that will receive the MIDI data from MPU-401.
#             Possible values: default, win32, alsa, oss, coreaudio, coremidi, none.
# midiconfig: Special configuration options for the device driver. This is usually the id of the device you want to use.
#               See the README/Manual for more details.

mpu401=intelligent
mididevice=default
midiconfig=

[sblaster]
#  sbtype: Type of Soundblaster to emulate. gb is Gameblaster.
#          Possible values: sb1, sb2, sbpro1, sbpro2, sb16, gb, none.
#  sbbase: The IO address of the soundblaster.
#          Possible values: 220, 240, 260, 280, 2a0, 2c0, 2e0, 300.
#     irq: The IRQ number of the soundblaster.
#          Possible values: 7, 5, 3, 9, 10, 11, 12.
#     dma: The DMA number of the soundblaster.
#          Possible values: 1, 5, 0, 3, 6, 7.
#    hdma: The High DMA number of the soundblaster.
#          Possible values: 1, 5, 0, 3, 6, 7.
# sbmixer: Allow the soundblaster mixer to modify the DOSBox mixer.
# oplmode: Type of OPL emulation. On &#39;auto&#39; the mode is determined by sblaster type. All OPL modes are Adlib-compatible, except for &#39;cms&#39;.
#          Possible values: auto, cms, opl2, dualopl2, opl3, none.
#  oplemu: Provider for the OPL emulation. compat might provide better quality (see oplrate as well).
#          Possible values: default, compat, fast.
# oplrate: Sample rate of OPL music emulation. Use 49716 for highest quality (set the mixer rate accordingly).
#          Possible values: 44100, 49716, 48000, 32000, 22050, 16000, 11025, 8000.

sbtype=sb16
sbbase=220
irq=7
dma=1
hdma=5
sbmixer=true
oplmode=auto
oplemu=default
oplrate=44100

[gus]
#      gus: Enable the Gravis Ultrasound emulation.
#  gusrate: Sample rate of Ultrasound emulation.
#           Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
#  gusbase: The IO base address of the Gravis Ultrasound.
#           Possible values: 240, 220, 260, 280, 2a0, 2c0, 2e0, 300.
#   gusirq: The IRQ number of the Gravis Ultrasound.
#           Possible values: 5, 3, 7, 9, 10, 11, 12.
#   gusdma: The DMA channel of the Gravis Ultrasound.
#           Possible values: 3, 0, 1, 5, 6, 7.
# ultradir: Path to Ultrasound directory. In this directory
#           there should be a MIDI directory that contains
#           the patch files for GUS playback. Patch sets used
#           with Timidity should work fine.

gus=false
gusrate=44100
gusbase=240
gusirq=5
gusdma=3
ultradir=C:\ULTRASND

[speaker]
# pcspeaker: Enable PC-Speaker emulation.
#    pcrate: Sample rate of the PC-Speaker sound generation.
#            Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
#     tandy: Enable Tandy Sound System emulation. For &#39;auto&#39;, emulation is present only if machine is set to &#39;tandy&#39;.
#            Possible values: auto, on, off.
# tandyrate: Sample rate of the Tandy 3-Voice generation.
#            Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
#    disney: Enable Disney Sound Source emulation. (Covox Voice Master and Speech Thing compatible).

pcspeaker=true
pcrate=44100
tandy=auto
tandyrate=44100
disney=true

[joystick]
# joysticktype: Type of joystick to emulate: auto (default), none,
#               2axis (supports two joysticks),
#               4axis (supports one joystick, first joystick used),
#               4axis_2 (supports one joystick, second joystick used),
#               fcs (Thrustmaster), ch (CH Flightstick).
#               none disables joystick emulation.
#               auto chooses emulation depending on real joystick(s).
#               (Remember to reset dosbox&#39;s mapperfile if you saved it earlier)
#               Possible values: auto, 2axis, 4axis, 4axis_2, fcs, ch, none.
#        timed: enable timed intervals for axis. Experiment with this option, if your joystick drifts (away).
#     autofire: continuously fires as long as you keep the button pressed.
#       swap34: swap the 3rd and the 4th axis. can be useful for certain joysticks.
#   buttonwrap: enable button wrapping at the number of emulated buttons.

joysticktype=auto
timed=true
autofire=false
swap34=false
buttonwrap=false

[serial]
# serial1: set type of device connected to com port.
#          Can be disabled, dummy, modem, nullmodem, directserial.
#          Additional parameters must be in the same line in the form of
#          parameter:value. Parameter for all types is irq (optional).
#          for directserial: realport (required), rxdelay (optional).
#                           (realport:COM1 realport:ttyS0).
#          for modem: listenport (optional).
#          for nullmodem: server, rxdelay, txdelay, telnet, usedtr,
#                         transparent, port, inhsocket (all optional).
#          Example: serial1=modem listenport:5000
#          Possible values: dummy, disabled, modem, nullmodem, directserial.
# serial2: see serial1
#          Possible values: dummy, disabled, modem, nullmodem, directserial.
# serial3: see serial1
#          Possible values: dummy, disabled, modem, nullmodem, directserial.
# serial4: see serial1
#          Possible values: dummy, disabled, modem, nullmodem, directserial.

serial1=dummy
serial2=dummy
serial3=disabled
serial4=disabled

[dos]
#            xms: Enable XMS support.
#            ems: Enable EMS support.
#            umb: Enable UMB support.
# keyboardlayout: Language code of the keyboard layout (or none).

xms=true
ems=true
umb=true
keyboardlayout=auto

[ipx]
# ipx: Enable ipx over UDP/IP emulation.

ipx=true
[autoexec]
echo off
mount c .
c:

type jsdos~1/readme.txt
echo on

LOGO.EXE
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;I&amp;rsquo;m keeping the bundles in the &lt;code&gt;static/bundles&lt;/code&gt; directory, which sits alongside the emulator WASM file. They&amp;rsquo;re not huge binaries but I&amp;rsquo;m still using &lt;code&gt;git lfs&lt;/code&gt; to manage them. Best to keep the the Git repository relatively sane.&lt;/p&gt;
&lt;p&gt;Finally, it&amp;rsquo;s just a matter of adding some JavaScript to start DosBox, load the bundle, and mount it onto a HTML element:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Dos(document.querySelector(&amp;quot;#element-to-mount&amp;quot;)).run(&amp;quot;/bundles/bundle-to-load.jsdos&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&amp;rsquo;s pretty much it.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/e6318284ee.jpg&#34; alt=&#34;Safari window with a test webpage with JSDos running an editor. The editor has the line &#39;This is the running Dos program.&#39;&#34;/&gt;
  &lt;figcaption&gt;A test webpage with JSDos running Logo 2, which is one of my Basic programs&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;After a few hours, I&amp;rsquo;ve managed to get a test version of this working. There are a few things that need working on: the app I&amp;rsquo;m trying require the Alt key, which is not readily available of Apple keyboards, so I may need to do something about that (JSDos comes with a virtual keyboard with Ctrl and Alt so it&amp;rsquo;s not a complete show-stopper)&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. And I&amp;rsquo;ll need to get styling and more maintainable JavaScript written for this (I&amp;rsquo;m using &lt;a href=&#34;https://stimulus.hotwired.dev&#34;&gt;StimulusJS&lt;/a&gt; for the JavaScript&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;).  But I&amp;rsquo;m impressed by how well this works, given the minimal amount of effort from my part.  Shoulders of giants and all that.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;I&amp;rsquo;ve since learnt that Alt is bound to the Option key in MacOS.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;One thing I leant about Hugo is that it&amp;rsquo;s bundled with &lt;a href=&#34;https://esbuild.github.io&#34;&gt;ESBuild&lt;/a&gt;, meaning that it&amp;rsquo;s resource pipeline supports NPM packages.  My understanding is that this is invokable using the &lt;a href=&#34;https://gohugo.io/hugo-pipes/js/&#34;&gt;js.Build&lt;/a&gt; construct.  This is super useful to know.&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>Updates To Dynamo-Browse</title>
      <link>https://lmika.org/2023/04/02/updates-to-dynamobrowse.html</link>
      <pubDate>Sun, 02 Apr 2023 12:36:32 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/04/02/updates-to-dynamobrowse.html</guid>
      <description>&lt;p&gt;In the off-chance that anyone other than me is reading this, it&amp;rsquo;s likely that there will be no update next week due to the Easter weekend.  It may not be the only weekend without an update either. If I find that I didn&amp;rsquo;t get much done for a particular week, I probably won&amp;rsquo;t say anything and leave the well to fill up for the next one (although I do have some topics planned for some of those weekends).&lt;/p&gt;
&lt;p&gt;In fact, I was not expecting to say much this week, given that work was going through a bit of a crunch period. But the needs of work finally pushed me to add a few features to Dynamo-Browse that were sorely lacking.  So that&amp;rsquo;s where most of this week&amp;rsquo;s side-project budget went to.&lt;/p&gt;
&lt;h2 id=&#34;dynamo-browse&#34;&gt;Dynamo-Browse&lt;/h2&gt;
&lt;p&gt;A lot of work done to query expressions in Dynamo-Browse this week, some of it touching on a few topics I mentioned in a &lt;a href=&#34;https://workingset.net/2023/03/19/dev-log.html&#34;&gt;previous update&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve finally got around to finishing the &lt;code&gt;between&lt;/code&gt; keyword, so that it works with the query planner and actually produces a DynamoDB query when used with a range[^sort] key.  This means no more falling back on table scans. It&amp;rsquo;s still in a branch as of this post, but I feel much less embarrassed with merging it now, given that this support has been added.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also made a decision about how to deal with multiple index candidates. Now, when Dynamo-Browse finds that multiple indices can apply for a specific query expression, it will produce an error, requesting you to specify which index to use.  This can be done by adding a &lt;code&gt;using&lt;/code&gt; suffix to an expression, which specifies how the query should be evaluated:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;color=&amp;quot;blue&amp;quot; using index(&amp;quot;color-item-index&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can be used at any time to override the index Dynamo-Browse should use, even if only one index candidate was found. It can also be used to force the query to run as a table scan if you don&amp;rsquo;t want to use an index at all:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;color=&amp;quot;blue&amp;quot; using scan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ideally you shouldn&amp;rsquo;t need to use this suffix that often. The whole purpose of query expressions was to eliminate the need for specifying details of how the query should be evaluated. But we don&amp;rsquo;t live in a perfect world, and it makes sense adding this to deal with cases where it helps to be specific.&lt;/p&gt;
&lt;p&gt;This is also in a branch, but I&amp;rsquo;m hoping this would be merged soon as well.&lt;/p&gt;
&lt;h3 id=&#34;unified-expression-types-and-values&#34;&gt;Unified Expression Types and Values&lt;/h3&gt;
&lt;p&gt;A relatively large change made this week was how how values and types are represented within query expressions.&lt;/p&gt;
&lt;p&gt;A query expression, once parsed, can be executed in multiple contexts. It can be used to generate a &lt;a href=&#34;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html&#34;&gt;conditional expression&lt;/a&gt; for a DynamoDB query or scan, or it can be evaluated within the app itself to produce a result or alter the fields of a DynamoDB record in memory. Each of these contexts have a different set of types the expression operates on. When interpreting the expression in order to produce a result, the expression operates on types that implement &lt;a href=&#34;https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/dynamodb/types#AttributeValue&#34;&gt;types.AttributeType&lt;/a&gt;. This fits nicely with what the expression has to work with, which is usually the raw DynamoDB records returned by the Go client. The context used to produces conditional expressions, however, operate on a sort of hybrid type hieararchy, that supports both &lt;code&gt;AttributeType&lt;/code&gt; and Go types. This is because to the client used to build the expression accept native Go values, which are sometimes available — particularly if they show up in the expression as a literal — but sometimes not.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s the problem: I want to be able to add functions to the expression language that can be used in both contexts. I&amp;rsquo;ll get into what sort of functions I&amp;rsquo;m thinking of in a minute, but the issue is that with two sets of type hierarchies, I&amp;rsquo;d have to implement the functions twice.&lt;/p&gt;
&lt;p&gt;Another problem is that an evaluation context operating on AttributeTypes feels very inefficient. Numbers are represented as string, and new attribute values are created on the heap. This is probably not too bad in the grand scheme of things, but it would be nice to use native Go values here, even if it&amp;rsquo;s just to avoid going from strings to numbers constantly.&lt;/p&gt;
&lt;p&gt;So I spent most of yesterday trying to fix this. I built a new private Go interface called &lt;code&gt;exprValue&lt;/code&gt; and added as implementing subtypes all the types supported by DynamoDB — strings, numbers, booleans, lists, etc. Values of these type implement this new interface, and can be converted to Go values or DynamoDB &lt;code&gt;AttributeType&lt;/code&gt; values depending on the need.&lt;/p&gt;
&lt;p&gt;Most of the evaluation logic was changed to use these types, including the builtin functions, and already I&amp;rsquo;m seeing some dramatic improvements of what&amp;rsquo;s possible now. I can define a function once and it can be evaluated both in the evaluation and query building context (provided that it&amp;rsquo;s only operating on constant values in the query building context).  It also addressed some long standing issues I&amp;rsquo;ve had with the expression language, such as adding support for using a list with the &lt;code&gt;in&lt;/code&gt; keyword; something that was not possible before:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pk in $someList
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This could potentially be helpful with the &amp;ldquo;fan-out&amp;rdquo; one I mentioned a few weeks ago.&lt;/p&gt;
&lt;p&gt;This is still early days, but I think it&amp;rsquo;s been a huge improvement to what was there before. And it&amp;rsquo;s super satisfying cleaning out all this tech-debt, especially if it means I can add features easily now.&lt;/p&gt;
&lt;h3 id=&#34;date-functions-in-the-expression-language&#34;&gt;Date Functions In The Expression Language&lt;/h3&gt;
&lt;p&gt;Now with the new type hierarchy in place, the time has come to start adding functions. What existed to date were the operators and  functions that DynamoDB&amp;rsquo;s conditional expression language supported, and little else. It&amp;rsquo;s time to go beyond that. And to be honest, this was always the plan, especially given that operators like &amp;ldquo;begins with&amp;rdquo; (&lt;code&gt;^=&lt;/code&gt;) have been there since the start.&lt;/p&gt;
&lt;p&gt;This first thing I&amp;rsquo;m pondering now is time and date functions. The immediate issue is one of representation: in that I don&amp;rsquo;t want to settle on any specific one. I&amp;rsquo;ve seen dates stored as both string date-stamps, usually in &lt;a href=&#34;https://en.wikipedia.org/wiki/ISO_8601&#34;&gt;ISO 8601&lt;/a&gt;, or as integer seconds from the Unix epoch, and it would be good to operate on both of these, in addition to other possible representations, like milliseconds from the Unix epoch, to some other string encoding scheme.&lt;/p&gt;
&lt;p&gt;So what I&amp;rsquo;m thinking is an abstract date-type, probably something backed by Go&amp;rsquo;s builtin &lt;a href=&#34;https://pkg.go.dev/time#Time&#34;&gt;date.Time&lt;/a&gt; type.  This will neither be a number or a string, but can be converted to one, maybe by using a keyword like &lt;code&gt;as&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;now() as &amp;quot;S&amp;quot;   -- represent date as ISO-8601 timestamp
now() as &amp;quot;N&amp;quot;   -- represent date as Unix timestamp
now()          -- error: need to convert it to something
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or maybe some other mechanism.&lt;/p&gt;
&lt;p&gt;The idea is that all the builtin functions will operate on this type, but will prevent the user from assuming a particular representation, and will force them to choose one.&lt;/p&gt;
&lt;p&gt;I think this is something that will fit nicely with the new type hierarchy system, but for now (pun unintended), I&amp;rsquo;ll stick with Unix timestamp, just so that I can use something that is easy to implement. But to make it crystal clear that this is temporary, any such functions will have an annoying prefix.&lt;/p&gt;
&lt;p&gt;So two new functions were added this week: the &lt;code&gt;_x_now()&lt;/code&gt; function, which returns the current time as seconds from the Unix epoch as a number; and the &lt;code&gt;_x_add()&lt;/code&gt;, which returns the sum of two numbers. Much like the time functions, I&amp;rsquo;d like to eventually add arithmetic  operators like &lt;code&gt;+&lt;/code&gt; to the expression language, but I needed something now and I didn&amp;rsquo;t have much time to work on that.&lt;/p&gt;
&lt;h3 id=&#34;attribute-commands&#34;&gt;Attribute Commands&lt;/h3&gt;
&lt;p&gt;Finally, a few random notes about commands dealing with attribute values.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;set-attr&lt;/code&gt; command can now accept the switch &lt;code&gt;-to&lt;/code&gt;, which can be used to set the attribute to the result of a query expression. No more copying-and-pasting values, and operating on them outside Dynamo-Browse.
The good thing about this is that the previous attribute values are available in the value expression, so you can use this switch to set the value of attribute based on other attributes in a row. This comes in super handy with bulk changes. I&amp;rsquo;ve used this to adjust the value of TTLs in a table I&amp;rsquo;m working in. To set the TTL to be 10 minutes into the future, I just marked the rows, entered the command &lt;code&gt;set-attr -to ttl&lt;/code&gt;, and use the expression &lt;code&gt;_x_add(_x_now(), 600)&lt;/code&gt;. Super useful.&lt;/p&gt;
&lt;p&gt;Also, I&amp;rsquo;ve found a bug where the &lt;code&gt;del-attr&lt;/code&gt; command does not work with marked items. It&amp;rsquo;ll only delete attributes from the item that&amp;rsquo;s selected (i.e. in pink).  I haven&amp;rsquo;t got around to fixing this, but I hope to very soon.&lt;/p&gt;
&lt;p&gt;I think that&amp;rsquo;s all for this week. Until next time.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Updating Bocce Scorecard</title>
      <link>https://lmika.org/2023/03/26/updating-bocce-scorecard.html</link>
      <pubDate>Sun, 26 Mar 2023 10:13:43 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/03/26/updating-bocce-scorecard.html</guid>
      <description>&lt;p&gt;I didn&amp;rsquo;t get to a lot of side-project work this week, but I did have to make a large change to a project we use to track scores for our &amp;ldquo;bocce club&amp;rdquo;. So I&amp;rsquo;d though I&amp;rsquo;d say a few words about that today.&lt;/p&gt;
&lt;p&gt;We had our bocce &amp;ldquo;grand final&amp;rdquo; a few weeks ago, and one of the matches resulted in a tie between two players. Unfortunately, the Bocce Scorecard web-app I build could not properly handle these, which meant that I had to fix it.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll be honest in saying that that this was never really well fleshed out in the code, and there was actually a small bug which didn&amp;rsquo;t handle the ranking of players well. But I was pushing to keep this app as the de-facto source of truth for these matches, and there was a bit riding on this being correct (there&amp;rsquo;s a $4 trophy involved). So I had to get this fix before our next match, which was today.&lt;/p&gt;
&lt;p&gt;Now, I been having trouble coming up with a good description of what the rules should be so I&amp;rsquo;d figured a simple example would suffice.&lt;/p&gt;
&lt;p&gt;Imagine that there are four players: Tom, Dick, Harry, and Sally. They play several bocce matches during a season — which roughly corresponds to one calendar year — plus three &amp;ldquo;grand final&amp;rdquo; matches at the end. Each player would be awarded a number of &amp;ldquo;season points&amp;rdquo; (we informally call them &amp;ldquo;cookies&amp;rdquo;) based on how well they did in the match. The person with the most season points at the end of the last grand final match wins the season, and gets to take home the trophy.&lt;/p&gt;
&lt;p&gt;In regular matches, the wining player is awarded one season point, while the remaining players get nothing:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Player&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Score&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Season Points&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Tom&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;11&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Dick&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;8&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;0&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Sally&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;6&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;0&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Harry&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;3&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;0&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In grand final matches, the winning player is awarded 5 points, the one coming in second gets 2, and the one coming in third gets 1:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Player&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Score&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Season Points&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Tom&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;11&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Dick&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;8&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;2&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Sally&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;6&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Harry&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;3&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;0&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Season points for grand final matches are distributed this way so that a single grand final match is roughly equivalent to an entire season of regular matches. This means that if someone is coming in last during the regular season (fun fact: that person&amp;rsquo;s usually me) they still has a chance to win the trophy if they do well during the grand final.&lt;/p&gt;
&lt;p&gt;Now, let&amp;rsquo;s say that our four players are playing a grand final match, and Dick and Sally tie for second place. What should happen is that both Dick and Sally should be awarded half the season points they would get for both the second and third rank, given that they are evenly match for these two positions. In other words, they should both get 1.5 season points (1 + 2 = 3 / 2 = 1.5).
Harry, who came last, still gets zero.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Player&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Score&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Season Points&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Tom&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;11&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Dick&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;7&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1.5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Sally&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;7&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1.5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Harry&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;3&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;0&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This was the rule that I needed to change.&lt;/p&gt;
&lt;p&gt;What I found when I started working on this is that the rule definitions themselves needed to be closer to how the players are ranked. What &lt;a href=&#34;https://workingset.net/2022/01/13/pgwc-scoring-rules.html&#34;&gt;was previously done&lt;/a&gt; was that the players were sorted based on their match score, and then the rules were applied to each one by checking the win condition and awarding the points if they match it.  But this didn&amp;rsquo;t fit nicely with this new approach to ties.&lt;/p&gt;
&lt;p&gt;So instead of the conditions and awards approach, I simplified the rule definitions such that it simply defines the number of season points based on the players rank. This effectively makes it a simple map between rank and points. For normal matches the mapping would look like this:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Rank&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Season Points&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;and for grand final matches, like this:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Rank&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Season Points&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;2&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;2&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;3&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Now, when a match is over, the logic that awards the season points first sorts the players based on their match score, and then groups the players into buckets such that all the players with same match score are lumped together in the same bucket.  Ranks are then assigned to the players in descending score order. If two players have the same score, they will be given two ranks (e.g. Dick and Sally would have both rank two and three). Finally, season points are awarded with the rule definition and the following formula:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;season_points(player) = sum[over player_ranks](rules.rank_scores[rank]) / no_of_players_in_bucket
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This new logic works for ties between any number of players with any ranks.&lt;/p&gt;
&lt;p&gt;But the introduction of division now means that the season points can be a decimal, and the database row that holds the season points is an integer type. I didn&amp;rsquo;t want to make it a floating point, so I took a page from Stripe and simply changed the representation of the season scores such that 1 season point is represented as 100 in the database. This is exposed in the rules configuration, which now looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;rank_scores&amp;quot;: [
    {
      &amp;quot;points&amp;quot;: 500,
      &amp;quot;rank&amp;quot;: 1
    },
    {
      &amp;quot;points&amp;quot;: 200,
      &amp;quot;rank&amp;quot;: 2
    },
    {
      &amp;quot;points&amp;quot;: 100,
      &amp;quot;rank&amp;quot;: 3
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;although all the non-admin screens properly represents the score as a decimal number.&lt;/p&gt;
&lt;p&gt;I managed to get all finished and pushed to the server, but there was one other thing I think I&amp;rsquo;d like to get done down the line. My friends have been asking me about the outcome of previous seasons recently and I&amp;rsquo;d like to make it easier for them to view it themselves.  The data exists, but it&amp;rsquo;s super hacky to get: you need to &amp;ldquo;open&amp;rdquo; a previous season so that the leader board is shown on the home page, then close it again once the info is seen. This can only be done by the admin user (i.e. me) and the screens to do it leave a lot to be desired:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/57d0145835.jpg&#34; alt=&#34;Screenshot of Bocce Scorecard showing the admin section for seasons&#34;/&gt;
  &lt;figcaption&gt;The current season admin section.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;What I&amp;rsquo;m thinking is adding a &amp;ldquo;Seasons&amp;rdquo; section in the web-app.  Clicking &amp;ldquo;Seasons&amp;rdquo; in the nav will bring up the following screen:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/7b48eade55.jpg&#34; alt=&#34;Mockup of the new end user season browser section&#34;/&gt;
  &lt;figcaption&gt;Mockup of a new season browser section.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The game variant will appear the top as a tab, and below them are all the current and past seasons arranged in descending chronological order.  Clicking the &lt;code&gt;&amp;gt;&lt;/code&gt; will bring up the season results display:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/1722969ad6.jpg&#34; alt=&#34;Mockup of the details of a season&#34;&gt;
  &lt;figcaption&gt;Drilling down into a season brings up the details, complete with a leader board and list of matches played during that season.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This will show the final outcome of the season, any metadata associated with the season, and the matches of the season, along with the winner. Clicking the location will bring up the particular bocce session so that all the matches played that day can be seen.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll see when I get around to building this. It&amp;rsquo;s actually been a while since I&amp;rsquo;ve last touched this project while making such a large feature.&lt;/p&gt;
&lt;p&gt;Oh, and since it&amp;rsquo;s been a while, this usually means I needed to upgrade &lt;a href=&#34;https://gobuffalo.io&#34;&gt;Buffalo&lt;/a&gt;, the framework this app is using. Doing this usually means that you&amp;rsquo;ll need to change your app in some way to handle the new build process. This time, it&amp;rsquo;s moving the &lt;code&gt;main.go&lt;/code&gt; file, previously in the project directory, into a &lt;code&gt;cmd/app&lt;/code&gt; directory.  When you see output like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leonmika@Stark bocce-scorecard % buffalo build -o /tmp/app
Usage:
  buffalo build [flags]

Aliases:
  build, b, bill, install

Flags:
      --build-flags strings        Additional comma-separated build flags to feed to go build
      --clean-assets               will delete public/assets before calling webpack
      --dry-run                    runs the build &#39;dry&#39;
      --environment string         set the environment for the binary (default &amp;quot;development&amp;quot;)
  -e, --extract-assets             extract the assets and put them in a distinct archive
  -h, --help                       help for build
      --ldflags string             set any ldflags to be passed to the go build
      --mod string                 -mod flag for go build
  -o, --output string              set the name of the binary
  -k, --skip-assets                skip running webpack and building assets
      --skip-build-deps            skip building dependencies
      --skip-template-validation   skip validating templates
  -s, --static                     build a static binary using  --ldflags &#39;-linkmode external -extldflags &amp;quot;-static&amp;quot;&#39;
  -t, --tags string                compile with specific build tags
  -v, --verbose                    print debugging information

ERRO[0000] Error: open cmd/app/main.go: no such file or directory 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&amp;rsquo;ll need to create a &lt;code&gt;cmd/app&lt;/code&gt; directory and move &lt;code&gt;main.go&lt;/code&gt; into the &lt;code&gt;cmd/app&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;This will get the build working again but it will break &lt;code&gt;buffalo dev&lt;/code&gt; as it could no longer find the main file in the project directory. To fix &lt;em&gt;that&lt;/em&gt;, you&amp;rsquo;ll need to open up &lt;code&gt;.buffalo.dev.yml&lt;/code&gt; and add the following property:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;build_target_path: &amp;quot;./cmd/app&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will get the dev build working again.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t know why the dev command honours this config, yet the build command chooses to look at a hard coded path. Wouldn&amp;rsquo;t it have been easier to express this in a single configuration file?&lt;/p&gt;
&lt;p&gt;And let&amp;rsquo;s not leave Node out of the cold. If you&amp;rsquo;re trying to run &lt;code&gt;buffalo build&lt;/code&gt; and you&amp;rsquo;re getting this error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#21 12.21 node:internal/crypto/hash:71
#21 12.21   this[kHandle] = new _Hash(algorithm, xofLen);
#21 12.21                   ^
#21 12.21 
#21 12.21 Error: error:0308010C:digital envelope routines::unsupported
#21 12.21     at new Hash (node:internal/crypto/hash:71:19)
#21 12.21     at Object.createHash (node:crypto:133:10)
#21 12.21     at BulkUpdateDecorator.hashFactory (/src/bocce_scorecard/node_modules/webpack/lib/util/createHash.js:145:18)
#21 12.21     at BulkUpdateDecorator.update (/src/bocce_scorecard/node_modules/webpack/lib/util/createHash.js:46:50)
#21 12.21     at RawSource.updateHash (/src/bocce_scorecard/node_modules/webpack/node_modules/webpack-sources/lib/RawSource.js:77:8)
#21 12.21     at NormalModule._initBuildHash (/src/bocce_scorecard/node_modules/webpack/lib/NormalModule.js:888:17)
#21 12.21     at handleParseResult (/src/bocce_scorecard/node_modules/webpack/lib/NormalModule.js:954:10)
#21 12.21     at /src/bocce_scorecard/node_modules/webpack/lib/NormalModule.js:1048:4
#21 12.21     at processResult (/src/bocce_scorecard/node_modules/webpack/lib/NormalModule.js:763:11)
#21 12.21     at /src/bocce_scorecard/node_modules/webpack/lib/NormalModule.js:827:5 {
#21 12.21   opensslErrorStack: [ &#39;error:03000086:digital envelope routines::initialization error&#39; ],
#21 12.21   library: &#39;digital envelope routines&#39;,
#21 12.21   reason: &#39;unsupported&#39;,
#21 12.21   code: &#39;ERR_OSSL_EVP_UNSUPPORTED&#39;
#21 12.21 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&amp;rsquo;ll need to enable the legacy OpenSSL provider using a Node option:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export NODE_OPTIONS=--openssl-legacy-provider
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yeah, building to a framework is always fun. 😏&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s it for this weeks update.  I spent some time on Dynamo-Browse this week as well, but I haven&amp;rsquo;t actually finished that work and this log entry is long enough, so I might say more about that next week.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Updates To Dynamo-Browse And CCLM</title>
      <link>https://lmika.org/2023/03/19/updates-to-dynamobrowse.html</link>
      <pubDate>Sun, 19 Mar 2023 21:46:18 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/03/19/updates-to-dynamobrowse.html</guid>
      <description>&lt;p&gt;I started this week fearing that I&amp;rsquo;d have very little to write today. I actually organised some time off over the weekend where I wouldn&amp;rsquo;t be spending a lot of time on side projects. But the week started with a public holiday, which I guess acted like a bit of a time offset, so some things did get worked on.&lt;/p&gt;
&lt;p&gt;That said, most of the work done was starting or continuing things in progress, which is not super interesting at this stage. I&amp;rsquo;ll hold off on talking about those until there&amp;rsquo;s a little more there.  But there were a few things that are worth mentioning.&lt;/p&gt;
&lt;h2 id=&#34;dynamo-browse&#34;&gt;Dynamo-Browse&lt;/h2&gt;
&lt;p&gt;I found a bug in the query planner. It had to do with which index it chose when planning a query with only a single attribute. If a table has multiple GSIs that have that same attribute as the partition key (with different attributes for sort keys), the index the planner choose became effectively random. Because each index may have different records, running that query could give incomplete results.&lt;/p&gt;
&lt;p&gt;I think the query planner needs to be fixed such that any ambiguity in which index to be use would result in an error. I try to avoid putting an unnecessary need for the user to know that a particular query required a particular index. But I don&amp;rsquo;t think there&amp;rsquo;s any getting around this: the user would have to specify.&lt;/p&gt;
&lt;p&gt;But how to allow the user to specify the index to use?&lt;/p&gt;
&lt;p&gt;The fix for the script API was reasonably simple: just allow the script author to specify the index to use in the form of an option. That&amp;rsquo;s effectively what I&amp;rsquo;ve done by adding an optional &lt;code&gt;index&lt;/code&gt; field to the &lt;code&gt;session.query()&lt;/code&gt; method. When set, the specific index would be used regardless of which index the query planner would choose.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not certain how best to solve this when the user is running a query interactively. My current idea is that a menu should appear, allowing the user to select the index to use from a list. This could also include a &amp;ldquo;scan&amp;rdquo; option if no index is needed. Ideally this information will be stored alongside the query expression so that pressing &lt;kbd&gt;R&lt;/kbd&gt; would rerun the query without throwing up the prompt again.&lt;/p&gt;
&lt;p&gt;Another option is allowing the user to specify the index within the expression in some way.  Maybe in the form of a hint, as in having the user explicitly specify the sort key in a way that does&amp;rsquo;t affect the output. This is a little hacky though — sort of like those optimisations you need to do in SQL queries to nudge the planner in a particular execution plan.&lt;/p&gt;
&lt;p&gt;Another option is having the user specify the index specifically in the query. Maybe as an annotation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;color=&amp;quot;blue&amp;quot; @index(&#39;color-item-index&#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or as a suffix:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;color=&amp;quot;blue&amp;quot; using index(&#39;color-item-index&#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anyway, this will be an ongoing thing I&amp;rsquo;m sure.&lt;/p&gt;
&lt;p&gt;One other thing I started working on in Dynamo-Browse is finally working on support for the &lt;code&gt;between&lt;/code&gt; keyword:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;age between 12 and 24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This maps directly to the &lt;code&gt;between&lt;/code&gt; statement in DynamoDB&amp;rsquo;s query expression language, so getting scan support for this was relatively easy. I do need to make the query planner know of this though, as this operation is supported in queries if it&amp;rsquo;s used with the sort key. So this is still on a branch at the moment.&lt;/p&gt;
&lt;p&gt;Finally, I&amp;rsquo;ve found myself using this tool a lot this last week and I desperately need something akin to what I&amp;rsquo;ve been calling a &amp;ldquo;fanout&amp;rdquo; command. This is a way to take the results of one query and use them in someway in  another query — almost like sub-queries in regular SQL. What I&amp;rsquo;ve been finding myself wishing I could use this for is getting the IDs of the row from a query run over the index, and just running a query for rows with those ID over the main table.  At the moment I&amp;rsquo;m left with copying the ID from the first result set, and just making a large &lt;code&gt;pk in (…)&lt;/code&gt; expression, which is far from ideal.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure whether I&amp;rsquo;d like to do this as a command, or extend the query expression in some way. Both approaches have advantages and disadvantages. That&amp;rsquo;s probably why I haven&amp;rsquo;t made any movement on this front yet.&lt;/p&gt;
&lt;h2 id=&#34;cclm&#34;&gt;CCLM&lt;/h2&gt;
&lt;p&gt;I did spend the Monday working on CCLM. I coded up a small script which took some of the ideas from &lt;a href=&#34;https://cwpat.me/misc/puzzle-level-idea-strategies/&#34;&gt;the blog post on puzzle design&lt;/a&gt; I mention last week that I could run to get some ideas. So far it&amp;rsquo;s only producing suggestions with two game elements, but it&amp;rsquo;s enough of a starting point for making puzzles:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leonmika@Stark cclm % go run ./cmd/puzzleidea
bear trap
directional walls
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After running it on Monday I had a go at starting work on a new level. It became clear reasonably soon after I started that I needed a new game element. So I added one, which I&amp;rsquo;ve called &amp;ldquo;kindling&amp;rdquo;.  By default it looks like a pile of wood, and is perfectively safe to walk on:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/e5c90bfb14.jpg&#34; width=&#34;600&#34; height=&#34;455&#34; alt=&#34;A screenshot of CCLM with a fireball about to hit kindling tiles&#34; /&gt;
&lt;p&gt;But if a fireball runs into it, it catches alight and spreads to any adjacent kindling tiles, turning them into fire tiles.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/b05fa92477.jpg&#34; width=&#34;600&#34; height=&#34;455&#34; alt=&#34;A screenshot of CCLM with kindling tiles catching alight and spreading to adjacent kindling tiles&#34; /&gt;
&lt;p&gt;I had an idea for this for a while. I even went to the extend of producing the graphics for this element. But needing it for this puzzle finally bought me around to finishing the work. I actually manage to make most of the changes without any changes to the Go code at all: the existing tile definition configuration was almost powerful enough to  represent this tile.&lt;/p&gt;
&lt;p&gt;One other minor thing I fixed was the alignment of the info panels on the right side of the screen. Dealing with the unaligned numbers got a bit much eventually. The cursor position, marker position, and tag numbers are properly aligned now.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/9131d0bc33.jpg&#34; width=&#34;600&#34; height=&#34;485&#34; alt=&#34;A screenshot of CCEdit with the cursor position, marker position, and tag numbers now properly aligned&#34; /&gt;
&lt;p&gt;Anyway, that&amp;rsquo;s all for this week.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Dev Log - 2023-03-12</title>
      <link>https://lmika.org/2023/03/12/dev-log.html</link>
      <pubDate>Sun, 12 Mar 2023 09:12:28 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/03/11/dev-log.html</guid>
      <description>&lt;h2 id=&#34;preamble&#34;&gt;Preamble&lt;/h2&gt;
&lt;p&gt;When I moved Working Set over to Micro.blog, I&amp;rsquo;d thought I&amp;rsquo;d be constantly writing micro-posts about what I&amp;rsquo;m working on, as a form of working in public. I found that didn&amp;rsquo;t really work for me, for a few reasons.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve got a strange relationship with this blog. I wanted a place online to write about the projects I&amp;rsquo;ve been working on, but every time I publish something here, I always get the feeling that I&amp;rsquo;m &amp;ldquo;showing off&amp;rdquo; in some way: &lt;em&gt;ooh, look what I&amp;rsquo;ve done, aren&amp;rsquo;t I cleaver?&lt;/em&gt; And okay, I&amp;rsquo;d be lying if there&amp;rsquo;s not a part of me that wants others to see how I spend my time. If I didn&amp;rsquo;t want that, I&amp;rsquo;d be content with these posts existing in a private journal.&lt;/p&gt;
&lt;p&gt;And maybe this is a form of self-justification but I&amp;rsquo;d like to think that there&amp;rsquo;s a bit of that feeling in every developer that keeps a public blog on what they do. Maybe not exactly &amp;ldquo;showing off&amp;rdquo;, but I&amp;rsquo;m sure they feel proud on what they work on and they want to talk about it. And there&amp;rsquo;s really nothing wrong with that.  In fact, the posts I tend to enjoy the most are those from other devs talking about the projects they&amp;rsquo;re working on.&lt;/p&gt;
&lt;p&gt;So yeah, I admit that having others see what I&amp;rsquo;m working on would be nice. They say write what you want to read, and this is my attempt at doing just that.&lt;/p&gt;
&lt;p&gt;But that only explains why I write about it on a public blog instead of a private journal. Why I should want to write these posts at all is that I&amp;rsquo;d like to keep a record of the the projects I work on. Nostalgia is one reason: seeing a project progress over time or remembering projects long since abandoned. But another might be a way to track where I&amp;rsquo;m spending my time. This is theoretical at the moment, but if there ever came a time when I wanted to find this out, I have to have the record written somewhere.&lt;/p&gt;
&lt;p&gt;But not as micro-posts. I think a fixed weekly cadence is more appropriate. I tried this a couple of years ago, and although it worked for a while, I fell out of the habit. But after seeing the &lt;a href=&#34;https://zdaysurvivor.com&#34;&gt;weekly entries&lt;/a&gt; by &lt;a href=&#34;https://jonhays.me/&#34;&gt;Jonathan Hays&lt;/a&gt;, I&amp;rsquo;ve been inspired to try it again.&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s why I&amp;rsquo;m trying these weekly update. They&amp;rsquo;ll be frequent enough to be useful to act like diary entries, but not so frequent that they will bother people who aren&amp;rsquo;t interested. They&amp;rsquo;ll be long enough to warrant a title, making it easy for people to skip it. And they&amp;rsquo;ll be any anything related to a side project I&amp;rsquo;m working on: either current or abandoned, public or completely private.  And I&amp;rsquo;m giving myself permission not to feel bad about it.&lt;/p&gt;
&lt;p&gt;Anyway, we&amp;rsquo;ll see how we go.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update 6th July:&lt;/strong&gt; I&amp;rsquo;ve gone back to just making posts when I have something to write about.&lt;/p&gt;
&lt;h2 id=&#34;dynamo-browse&#34;&gt;Dynamo-Browse&lt;/h2&gt;
&lt;p&gt;Big week for Dynamo-Browse: I finally got v0.2.0 out the door. This is the release with scripting support (yes, it finally shipped). The scripting implementation has been finished for a while. The thing that was blocking it&amp;rsquo;s release was all the documentation I had to write: both the section in the manual and the API reference.&lt;/p&gt;
&lt;p&gt;The build was also a bit of an issue. The release builds are built using GitHub actions. To get them published as Homebrew casks, the actions need to push them to another repository. The secret token used to access this repository expired, and I had to create another one. Not difficult, but the fact that I had to create a whole new secret instead of rotate the existing one was a little annoying. Getting the permissions right, and being forced to choose a different name (&amp;ldquo;Deploy Homebrew formulas v2&amp;rdquo;) didn&amp;rsquo;t help matters either.&lt;/p&gt;
&lt;p&gt;But got there in the end. The v0.2.0 release is now available on &lt;a href=&#34;https://dynamobrowse.app/&#34;&gt;dynamobrowse.app&lt;/a&gt; and &lt;a href=&#34;https://github.com/lmika/dynamo-browse&#34;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll reduce the time I spend on this for a little while. We&amp;rsquo;ll see how long that lasts. I use this tool for work so often and I&amp;rsquo;ve got a whole list of features I&amp;rsquo;d like to see added to it.&lt;/p&gt;
&lt;h2 id=&#34;cclm&#34;&gt;CCLM&lt;/h2&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/61b3684a13.jpg&#34; alt=&#34;The Beach level with the square indicated boxes opened in CCLM Edit&#34; width=&#34;600&#34; height=&#34;481&#34; /&gt;
&lt;p&gt;I got the editor up and running again last week and I spent Saturday designing a level with the working name &amp;ldquo;The Beach&amp;rdquo;.  I&amp;rsquo;m a huge fan of the &lt;a href=&#34;https://www.youtube.com/watch?v=akeVPZLZejY&amp;amp;list=PLc38fcMFcV_uH3OK4sTa4bf-UXGk2NW2n&amp;amp;index=10&#34;&gt;Developing&lt;/a&gt; series on the &lt;a href=&#34;https://www.youtube.com/channel/UCqJ-Xo29CKyLTjn6z2XwYAw&#34;&gt;Game Makers Toolkit&lt;/a&gt; YouTube channel, and the latest video was about how difficult it was for Mark to design levels for his video game. I found I had the exact same problem for designing levels for mine (although I think the lack of effort I put into it doesn&amp;rsquo;t help). He pointed to &lt;a href=&#34;https://cwpat.me/misc/puzzle-level-idea-strategies/&#34;&gt;a blog post&lt;/a&gt; by an indie game designer that had some useful tips to help with puzzle design. The one about using two elements that interact with it.&lt;/p&gt;
&lt;p&gt;The one I worked on was for a custom element that will change boxes with a square indicator to blank tiles when pressing the yellow button. I&amp;rsquo;ve had this element around for a while but I haven&amp;rsquo;t actually used it in a level yet. I&amp;rsquo;d thought I&amp;rsquo;ll be time to do so, but the level I came up with seems a little simple. Not sure what I&amp;rsquo;d do about it. I could either rearrange it so that it appears earlier in the level set, or I can make it a little more difficult in some way.&lt;/p&gt;
&lt;h2 id=&#34;client-project&#34;&gt;Client Project&lt;/h2&gt;
&lt;p&gt;One thing that releasing Dynamo-Browse has given me is the opportunity to do a small client project. I&amp;rsquo;ve talked about this on &lt;a href=&#34;https://lmika.org/2023/03/04/im-facing-a.html&#34;&gt;lmika.org&lt;/a&gt; and the latest update is that I think I&amp;rsquo;ve convinced him to consider a static site, seeing that it would be easier for him to run (don&amp;rsquo;t need to worry about plugins) and would be easier for me to build (I don&amp;rsquo;t know how to use Wordpress, especially not their new block editor).&lt;/p&gt;
&lt;p&gt;This week was basically coming up with a site layout. I had the opportunity to use Figma for the first time. Works reasonably well, but I&amp;rsquo;m wondering if Balsamiq Mockups was probably a better choice for a rough outline of what the site is to look like. But that&amp;rsquo;s all moot: a layout was put together and sent to the client for him to get some feedback.&lt;/p&gt;
&lt;p&gt;Anyway, still early days here. I&amp;rsquo;m looking at possible Hugo templates to build the site in and possible hosting solutions that would work with the client. I&amp;rsquo;m not aware of options for static hosting other than the AWS, Cloudflare or Azures of the world. Not sure it will work for the client, although it&amp;rsquo;s totally possible that I&amp;rsquo;m just not looking in the right places.&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s it. Update one done. Although next week I&amp;rsquo;ll be taking some leave so update two might be slightly shorter (at least there&amp;rsquo;ll be no preamble) so it may be less about current updates. I guess we&amp;rsquo;ll find out together.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/03/08/completed-the-release.html</link>
      <pubDate>Wed, 08 Mar 2023 20:52:12 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/03/08/completed-the-release.html</guid>
      <description>&lt;p&gt;Completed the release of Dynamo-Browse 0.2.0. Most of the work in the last week was updating the manual, especially the scripting API. Some more updates need to be made for the query expressions as well, but I&amp;rsquo;ll publish what I have now and update that over time.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/03/04/heres-a-bit.html</link>
      <pubDate>Sat, 04 Mar 2023 15:55:32 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/03/04/heres-a-bit.html</guid>
      <description>&lt;p&gt;Here&amp;rsquo;s a bit of a blast from the past. I managed to get &lt;em&gt;ccedit&lt;/em&gt; working again. This was the original level editor for &lt;a href=&#34;https://workingset.net/2022/12/28/i-guess-im.html,&#34;&gt;workingset.net/2022/12/2&amp;hellip;&lt;/a&gt; my &lt;a href=&#34;https://workingset.net/2022/12/28/i-guess-im.html&#34;&gt;Chips Challenge &amp;ldquo;fan game&amp;rdquo;&lt;/a&gt; I&amp;rsquo;ve been working on.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been designing a few levels for it recently, but since moving to a new Mac, the level editor I was using was going to be difficult to port. It&amp;rsquo;s QT application and the QT bindings were a pain to setup, and I rather not go through that again. I was using a Mac at the time I started working on it, but I wasn&amp;rsquo;t yet ready to go all in on MacOS. So to hedge my bets, I decided to go with QT as the UI toolkit.&lt;/p&gt;
&lt;p&gt;This was 5 years ago and I&amp;rsquo;m unlikely to go back to Linux, so choosing QT was a bit of a bad decision. I think if I had my time again, I&amp;rsquo;d go with something like AppKit.&lt;/p&gt;
&lt;p&gt;Anyway, the level editor still works but I have to log into a screen share to use it. I&amp;rsquo;d like to be able to edit levels on the machine I&amp;rsquo;m using now.&lt;/p&gt;
&lt;p&gt;The code for the original level editor was still around but it hasn&amp;rsquo;t been touched in ages. It&amp;rsquo;s basically an SDL application — the same graphics library I&amp;rsquo;m using for the actual game itself — and the &lt;a href=&#34;https://github.com/veandco/go-sdl2&#34;&gt;SDL v2 bindings&lt;/a&gt; I&amp;rsquo;m using are still maintained, so updating those were quite easy&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;One thing I did have to pull out was the Lua VM&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;. The editor was using old C Lua bindings. Better Lua VMs written in pure Go are now available, so I didn&amp;rsquo;t want to keep using these old bindings anymore. In fact, I didn&amp;rsquo;t want to use Lua at all. Lua was originally used for the level scripts, but I replaced this in favour of another language (which is no longer maintained 😒, but I&amp;rsquo;m not changing it again).&lt;/p&gt;
&lt;figure&gt;
   &lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/aac89fc5e5.jpg&#34; alt=&#34;The original CCLM Editor&#34; title=&#34;Screenshot 2023-03-04 at 4.39.49 pm.png&#34; width=&#34;599&#34; height=&#34;486&#34; /&gt;
   &lt;figcaption&gt;The original CCLM Editor&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So far the editor boots up, but that&amp;rsquo;s about it. I can move the cursor around but I can&amp;rsquo;t add new tiles or load existing levels. There seems to be some weird things going on with the image name lookup. I originally thought image name were case insensitive, but after looking at the image name lookup logic in the game itself, I&amp;rsquo;m not so sure.&lt;/p&gt;
&lt;p&gt;How much time I&amp;rsquo;d like to spend on this is still a bit of a question. It all depends whether I&amp;rsquo;d like to release the game itself in some fashion. There are still questions about whether I&amp;rsquo;m allowed to, given that the graphics are not my own. Still need to think about that.&lt;/p&gt;
&lt;p&gt;But in any case, good to see the old editor again.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;The level editor was actually using older SDL v1 bindings, but it was relatively easy to port them over to v2, although some gaps are still present.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;Lua was actually the second language used by the editor. The first was a Go native TCL interpretor.&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/02/22/spent-some-time.html</link>
      <pubDate>Wed, 22 Feb 2023 21:45:13 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/02/22/spent-some-time.html</guid>
      <description>&lt;p&gt;Spent some time closing off the &lt;a href=&#34;https://workingset.net/2023/01/21/looking-at-the.html&#34;&gt;Dynamo-Browse shortlist&lt;/a&gt;.  I think I&amp;rsquo;ve got most of the big ticket items addressed. Here&amp;rsquo;s a brief update on each one:&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;Fix the activity indicator that is sometimes not clearing when a long running task is finished.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;How long running tasks are dealt with has been completely overhauled. The previous implementation had many opportunities for race conditions, which was probably the cause of the activity indicator showing up when nothing was happening. I rewrote this using a dedicated goroutine for handling these tasks, and the event bus for sending events to the other areas of the app, including the UI layer. Updates and status changes are handled with mutexes and channels, and it just feels like better code as well.&lt;/p&gt;
&lt;p&gt;It will need some further testing, especially in real world use against a real DynamoDB database. We&amp;rsquo;ll see if this bug rears its unpleasant head once more.&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;Fix a bug in which executing a query expression with just the sort key does nothing.  I suspect this has something to do with the query planner somehow getting confused if the sort key is used but the partition key is not.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;Turns out that this was actually a problem with the &amp;ldquo;has prefix&amp;rdquo; operator. It was incorrectly determining that an expression of the form &lt;code&gt;sort_key ^= &amp;quot;string&amp;quot;&lt;/code&gt; with no partition key could be executed as a query instead of a scan. Adding a check to see if the partition key also existed in the expression fixed the problem.&lt;/p&gt;
&lt;p&gt;Also made a number of other changes to the query expression. Added the ability to use indexed references, like &lt;code&gt;this[1]&lt;/code&gt; or &lt;code&gt;that[&amp;quot;thing&amp;quot;]&lt;/code&gt;. This has been a long time coming so it&amp;rsquo;s good to see it implemented. Unfortuntly this only works reliably when a single level is used, so &lt;code&gt;this[1][2]&lt;/code&gt; will result in an error. The cause of this is a bug in the Go SDK I&amp;rsquo;m using to produce the query expressions that are run against the database. If this becomes a problem I look at this again.&lt;/p&gt;
&lt;p&gt;I also realised that &lt;code&gt;true&lt;/code&gt; and &lt;code&gt;false&lt;/code&gt; were not treated as boolean literals, so I fixed that as well.&lt;/p&gt;
&lt;p&gt;Finally, the query planner now consider GSIs when it&amp;rsquo;s working out how to run a query expression. If the expression can be a query over a GSI, it will be executed as one.  Given the types of queries I need to run, I&amp;rsquo;ll be finding this feature useful.&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;Fix a bug where &lt;code&gt;set default-limits&lt;/code&gt; returns a bad value.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;This was a pretty simple string conversion bug.&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;Add a way to describe the table, i.e. show keys, indices, etc.  This should also be made available to scripts.
Add a way to &amp;ldquo;goto&amp;rdquo; a particular row, that is select rows just by entering the value of the partition and optionally the sort key.&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;These I did not do. The reason is that they&amp;rsquo;ll make good candidates for scripts and it would be a good test to see if they can be written as one. I think the &amp;ldquo;goto&amp;rdquo; feature would be easy enough. I added the ability to get information about the current table in the script, and also for scripts to add new key bindings, so I don&amp;rsquo;t force any issues here.&lt;/p&gt;
&lt;p&gt;The table description would be trickier. There&amp;rsquo;s currently no real way to display a large block of text (except the status bar, but even there it&amp;rsquo;s a little awkward). So a full featured description might be difficult. But the information is there, at least to a degree, so maybe something showing the basics would work.&lt;/p&gt;
&lt;p&gt;Anyway, the plan now is to use this version for a while to test it out. Then cut a release and update the documentation. That&amp;rsquo;s a large enough task in and of itself, but I&amp;rsquo;d really like to get this finished so I can move onto something else.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/01/21/looking-at-the.html</link>
      <pubDate>Sat, 21 Jan 2023 08:18:18 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/01/20/looking-at-the.html</guid>
      <description>&lt;p&gt;Looking at the &amp;ldquo;backlog&amp;rdquo; of things to work on for Dynamo-Browse before I set it aside.  I&amp;rsquo;ll fix a few bugs and add a few small features that I&amp;rsquo;ve found myself really wanting.  The short list is as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fix the activity indicator that is sometimes not clearing when a long running task is finished.&lt;/li&gt;
&lt;li&gt;Fix a bug in which executing a query expression with just the sort key does nothing.  I suspect this has something to  do with the query planner somehow getting confused if the sort key is used but the partition key is not.&lt;/li&gt;
&lt;li&gt;Fix a bug where &lt;code&gt;set default-limits&lt;/code&gt; returns a bad value.&lt;/li&gt;
&lt;li&gt;Add a way to describe the table, i.e. show keys, indices, etc.  This should also be made available to scripts.&lt;/li&gt;
&lt;li&gt;Add a way to &amp;ldquo;goto&amp;rdquo; a particular row, that is select rows just by entering the value of the partition and optionally the sort key.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;ll start with these and see how I go.&lt;/p&gt;
&lt;p&gt;Oh, and one more thing: I will need to kill my darlings, namely the other commands in the &amp;ldquo;audax&amp;rdquo; repository that I&amp;rsquo;ve hacked togeather. They&amp;rsquo;re mildly useful — one of them is used to browse SSM parameters and another is used to view JSON log files — but they&amp;rsquo;re unloved and barely functional. I&amp;rsquo;ll move them out of the &amp;ldquo;audax&amp;rdquo; repository and rename this repo to &amp;ldquo;dynamo-browse&amp;rdquo;, just to make it less confusing for everyone.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/01/20/i-think-ill.html</link>
      <pubDate>Fri, 20 Jan 2023 15:19:26 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/01/20/i-think-ill.html</guid>
      <description>&lt;p&gt;I think I&amp;rsquo;ll take a little break from Dynamo-Browse. There&amp;rsquo;s a list of small features that are on my TODO list. I might do one or two of them over the next week, then cut and document a release, and leave it for a while.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m still using Dynamo-Browse pretty much every day at work, but it feels a little demotivating being the only person that&amp;rsquo;s using it. Even those at work seem like they&amp;rsquo;ve moved on. And I can understand that: it&amp;rsquo;s not the most intuitive bit of software out there.  And I get the sense that it&amp;rsquo;s time to do something new.  Maybe an online service or something. 🤔&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2023/01/14/finally-bit-the.html</link>
      <pubDate>Sat, 14 Jan 2023 08:21:55 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2023/01/13/finally-bit-the.html</guid>
      <description>&lt;p&gt;Finally bit the bullet and got scripting working in Dynamo-Browse.  It&amp;rsquo;s officially in the tool, at least in the latest development version. It&amp;rsquo;s finally good to see this feature implemented. I&amp;rsquo;ve been waffling on this for a while, as the last several posts can attest, and it&amp;rsquo;s good to see some decisions made.&lt;/p&gt;
&lt;p&gt;In the end I went with &lt;a href=&#34;https://cloudcmds.github.io/tamarin/&#34;&gt;Tamarin&lt;/a&gt; as the scripting language.  It was fortunate that the maintainer released version 1.0 just as I was about to merge the scripting feature branch into &lt;code&gt;main&lt;/code&gt;.  I&amp;rsquo;ve been trying out the scripting feature at work and so far I&amp;rsquo;ve been finding it to work pretty well. It helps that the language syntax is quite close to Go, but I also think that the room to hide long-running tasks from the user (i.e. no promises everywhere) dramatically simplifies how scripts are written.&lt;/p&gt;
&lt;p&gt;As for the runtime, I decided to have scripts run in a separate go-routine. This means they don&amp;rsquo;t block the main thread and the user can still interact with the tool.  This does mean that the script will need to indicate when a long running process is occurring — which they can do by displaying a message in the status line — but I think this is a good enough tradeoff to avoid having a running script lock-up the app. I still need to add a way for the user to kill long-running scripts (writing a GitHub ticket to do this now).&lt;/p&gt;
&lt;p&gt;At the moment, only one script can run at any one time, sort of like how JavaScript in the browser works.  This is also intentional, as it will prevent a whole bunch of scripts launching go-routines and slowing down the user experience. I think it will help in not introducing any potential synchronisation issues with parallel running scripts accessing the same memory space. No need to build methods in the API to handle this. Will this mean that script performance will be a problem?  Not sure at this stage.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also keeping the API intentionally small at this stage. There are methods to query a DynamoDB table, get access to the result set and the items, and do some basic UI and OS things. I&amp;rsquo;m hoping it&amp;rsquo;s small enough to be useful, at least at the start, without overwhelming script authors or locking me into an API design. I hope to add methods to the API over time.&lt;/p&gt;
&lt;p&gt;Anyway, good to see this committed to.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Poking Around The Attic Of Old Coding Projects</title>
      <link>https://lmika.org/2022/12/28/poking-around-the.html</link>
      <pubDate>Wed, 28 Dec 2022 15:27:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/12/28/poking-around-the.html</guid>
      <description>&lt;p&gt;I guess I&amp;rsquo;m in a bit of a reflective mood these pass few days because I spent the morning digging up an old project that was lying dormant for several years.  It&amp;rsquo;s effectively a clone of Chips Challenge, the old strategy game that came with the Microsoft Entertainment Pack. I was a fan of the game when I was a kid, even though I didn&amp;rsquo;t get through all the levels, and I&amp;rsquo;ve tried multiple times to make a clone of it.&lt;/p&gt;
&lt;p&gt;The earliest successful clone I can think of was back when I was using Delphi, which I think was my teens.  It&amp;rsquo;s since been lost but I do recall having a version that work and was reasonably true to the original game as possible.  It wasn&amp;rsquo;t a particularly accurate clone: I do recall some pretty significant bugs, and the code itself was pretty awful.  But it was nice to be able to do things like design my own levels (I wasn&amp;rsquo;t as internet savvy back then and I didn&amp;rsquo;t go looking for level editors for the Microsoft&amp;rsquo;s release of Chips Challenge).  Eventually I stopped working on it, and after a few updates to the family computer, plus a lack of backups or source control, there came a time where I lost it completely.&lt;/p&gt;
&lt;p&gt;Years later, I made another attempt at building a clone. I was dabbling in .Net at the time and I think I was working on it as an excuse to learn C#.  I think I got the basics of the game and associated level editor working but I didn&amp;rsquo;t get much further than that.  Either I got bored and stopped working on it.&lt;/p&gt;
&lt;p&gt;I started the latest clone nine years ago. I can&amp;rsquo;t remember the original motivation. I was just getting into Go at the time and I think it was both to learn how to build something non-trivial in the language, and to determine how good Go was for building games.  Although this is probably just a rationalisation: I&amp;rsquo;m sure the real reason was to work on something fun on the side.&lt;/p&gt;
&lt;figure&gt;
   &lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/465368360c.jpg&#34; width=&#34;600&#34; height=&#34;455&#34; alt=&#34;Screenshot of CCLM, the latest clone&#34; /&gt;
   &lt;figcaption&gt;Screenshot of CCLM, the latest clone.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Over the first five years of its life or so, I worked on it on and off, adding new game elements (tiles, sprites, etc.) and capabilities like level scripts.  One thing I am particularly proud of was building a mini-language for selecting game elements using something akin to CSS selectors.  Want to select all wall tiles?  Use the selector &lt;code&gt;SOLD&lt;/code&gt;.  How about configuring a water tile to only allow gliders and the player but only if they have the flipper?  Set the &lt;code&gt;Immune&lt;/code&gt; attribute of the water tile to &lt;code&gt;GLID,PLYR:holding(flippers)&lt;/code&gt;.  This was particularly powerful when working on tile and sprite definitions.&lt;/p&gt;
&lt;figure&gt;
   &lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/45b1352421.jpg&#34; width=&#34;600&#34; height=&#34;401&#34; alt=&#34;Text editor showing tile definitions with sample selectors&#34; /&gt;
   &lt;figcaption&gt;A sample of how some of the tiles are defined, including usage of the selectors.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I didn&amp;rsquo;t put as much effort into content however.  As of today, there are only 18 or so unique levels, and about half of them are ones that I consider good.  I certainly put little effort into the graphics.  Many of the tile images were just taken from the original tile-set and any additional graphics were basically inspirations from that.  This blatant copyright violation is probably why this project won&amp;rsquo;t see the light of day.&lt;/p&gt;
&lt;figure&gt;
   &lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/33046b877c.jpg&#34; width=&#34;600&#34; height=&#34;418&#34; alt=&#34;Screenshot of the level editor&#34; /&gt;
   &lt;figcaption&gt;The level editor, and one of the rare times when it&#39;s not crashing.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I&amp;rsquo;m impressed on how Go maintains its backwards capability: moving from 1.13 to 1.19 was just a matter of changing the version number in the .mod file.  I haven&amp;rsquo;t updating any of the libraries, and I&amp;rsquo;m sure the only reason why it still builds is because I haven&amp;rsquo;t dared to try.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll probably shouldn&amp;rsquo;t spend a lot of time on this.  But it was fun to revisit this for a while.&lt;/p&gt;
&lt;p&gt;One final thing: I might write more about projects I&amp;rsquo;ve long since abandoned or have worked on and haven&amp;rsquo;t released, mainly for posterity reasons but also because I like reflecting on them later.  You never know what you&amp;rsquo;d wish you documented until you&amp;rsquo;ve lost the chance to do so.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2022/12/27/spent-the-day.html</link>
      <pubDate>Tue, 27 Dec 2022 15:15:06 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/12/27/spent-the-day.html</guid>
      <description>&lt;p&gt;Spent the day restyling the &lt;a href=&#34;https://dynamobrowse.app&#34;&gt;Dynamo-Browse&lt;/a&gt; website. The &lt;a href=&#34;https://themes.gohugo.io/themes/hugo-theme-terminal/&#34;&gt;Terminal&lt;/a&gt; theme was fun, but over time I found the site to be difficult to navigate. And if you consider that Dynamo-Browse is not the most intuitive tool out there, an easy to navigate user manual was probably important. So I replaced that theme with &lt;a href=&#34;https://themes.gohugo.io/themes/hugo-book/&#34;&gt;Hugo-Book&lt;/a&gt;, which I think is a much cleaner layout.  After making the change, and doing a few small style fixes, I found it to be a significant improvement.&lt;/p&gt;
&lt;p&gt;I also tried my hand at designing a logo for Dynamo-Browse. The blue box that came with the Terminal theme was fine for a placeholder, but it felt like it was time for a proper logo now.&lt;/p&gt;
&lt;p&gt;I wanted something which gave the indication of a tool that worked on DynamoDB tables while also leaning into it&amp;rsquo;s TUI characteristics.  My first idea was a logo that looked like the DynamoDB icon in ASCII art. So after attempting to design something that looks like it in Affinity Designer, and passing it through an online tool which generated ASCII images from PNG, this was the result:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/1d55041cde.jpg&#34; width=&#34;251&#34; height=&#34;228&#34; alt=&#34;First attempt at the Dynamo-Browse logo&#34; /&gt;
&lt;p&gt;I tried adjusting the colours of final image, and doing a few things in Acorn to thicken the ASCII characters themselves, but there was no getting around the fact that the logo just didn&amp;rsquo;t look good.  The ASCII characters were too thin and too much of the background was bleeding through.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/ff82b12ee9.jpg&#34; width=&#34;600&#34; height=&#34;158&#34; alt=&#34;Other attempts at the Dynamo-Browse logo&#34; /&gt;
&lt;p&gt;So after a break, I went back to the drawing board.  I remembered that there were actually Unicode block characters which could produce filled-in rectangles of various heights, and I wondered if using them would be a nice play on the DynamoDB logo.  Also, since the Dynamo-Browse screen consists of three panels, with only the top one having the accent colour, I thought having a similar colour banding would make a nice reference.  So I came up with this design:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/1fd153bc15.jpg&#34; width=&#34;250&#34; height=&#34;250&#34; alt=&#34;Final design of the Dynamo-Browse logo&#34; /&gt;
&lt;p&gt;And I must say, I like it.  It does look a little closer to low-res pixel art than ASCII art, but what it&amp;rsquo;s trying to allude to is clear.   It looks good in both light mode and dark mode, and it also makes for a nice favicon.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s all the updates for the moment. I didn&amp;rsquo;t get around to updating the screenshots, which are in dark-mode to blend nicely with the dark Terminal theme. They actually look okay on a light background, so I can probably hold-off on this until the UI is changed in some way.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2022/12/23/ive-been-resisting.html</link>
      <pubDate>Fri, 23 Dec 2022 10:25:27 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/12/23/ive-been-resisting.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been resisting using mocks in the unit tests of Dynamo-Browse, but today I finally bit the bullet and started adding them. There would have just been too much scaffolding code that I needed to write without them. I guess we&amp;rsquo;ll see if this was a wise decision down the line.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Thinking About Scripting In Dynamo-Browse, Again</title>
      <link>https://lmika.org/2022/12/22/thinking-about-scripting.html</link>
      <pubDate>Thu, 22 Dec 2022 14:39:16 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/12/22/thinking-about-scripting.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;m thinking about scripting in Dynamo-Browse. &lt;a href=&#34;https://workingset.net/2022/08/31/thinking-about-scripting.html&#34;&gt;Yes, again&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For a while I&amp;rsquo;ve been using a version of Dynamo-Browse which included a JavaScript interpreter.  I&amp;rsquo;ve added it so that I could extend the tool with a few commands that have been useful for me at work.  That branch has fallen out of date but the idea of a scripting feature has been useful to me and I want to include it in the mainline in some way.&lt;/p&gt;
&lt;p&gt;The scripting framework works, but there are a few things that I&amp;rsquo;m unhappy about.  The first is around asynchronicity and scheduling.  I built the scripting API around the JS event-loop included in interpreter.  Much like a web-browser or Node.js, this event-loop allows the use of promises for operations that can be dispatched asynchronously.  The interpreter, however, is not ES6 compatible, which means no &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; keywords. The result is that many of the scripts that I&amp;rsquo;ve been writing are littered with all these &lt;code&gt;then()&lt;/code&gt; chains.  Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const session = require(&amp;quot;audax:dynamo-browse/session&amp;quot;);
const ui = require(&amp;quot;audax:dynamo-browse/ui&amp;quot;);
const exec = require(&amp;quot;audax:x/exec&amp;quot;);

plugin.registerCommand(&amp;quot;cust&amp;quot;, () =&amp;gt; {
    ui.prompt(&amp;quot;Enter UserID: &amp;quot;).then((userId) =&amp;gt; {
        return exec.system(&amp;quot;lookup-customer-id.sh&amp;quot;, userId);
    }).then((customerId) =&amp;gt; {
        let userId = output.replace(/\s/g, &amp;quot;&amp;quot;);        
        return session.query(`pk=&amp;quot;CUSTOMER#${customerId}&amp;quot;`, {
            table: `account-service-dev`
        });
    }).then((custResultSet) =&amp;gt; {
        if (custResultSet.rows.length == 0) {
            ui.print(&amp;quot;No such user found&amp;quot;);
            return;            
        }
        session.resultSet = custResultSet;
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yeah, I know; this is nothing new if you&amp;rsquo;ve been doing any web-dev or Node in the past, but it still feels a little clunky exposing the execution details to the script writer.  Should that be something they should be worried about?  Feels like the tool should take on more here.&lt;/p&gt;
&lt;p&gt;The second concern involves modules.  The JavaScript interpreter implements the &lt;code&gt;require()&lt;/code&gt; function which can be used to load a JS module, much like Node.js. But the Node.js standard library is not available.  That&amp;rsquo;s not really the fault of the maintainers, and to be fair to them, they are building out native support for modules.  But that support isn&amp;rsquo;t there now, and I would like to include some modules to do things like access the file system or run commands. Just adding them with non-standard APIs and with name that are the same as equivalent modules in node Node.js feels like a recipe for confusion.&lt;/p&gt;
&lt;p&gt;Further exacerbating this is that script authors may assume that they have access to the full Node standard library, or even NPM repositories, when they won&amp;rsquo;t. I certainly don&amp;rsquo;t want to implement the entire Node standard library from scratch, and even if the full library was available to me, I&amp;rsquo;m not sure the use of JavaScript here warrants that level of support.&lt;/p&gt;
&lt;p&gt;Now, zooming out a little, this could all be a bit of a non-issue.  I&amp;rsquo;ve haven&amp;rsquo;t really shared this functionality with anyone else, so all this could be of no concern to anyone else other than myself.
But even so, I&amp;rsquo;m am thinking of options other than JavaScript.  A viable alternative might be Lua — and Go has a bunch of decent interpreters — but I&amp;rsquo;m not a huge fan of Lua as a language.  Also, Lua&amp;rsquo;s table structure being used for both arrays and structures seems like a source of confusion, especially when dealing with JSONish data structures like DynamoDB items.&lt;/p&gt;
&lt;p&gt;One interpreter that has caught my eye is &lt;a href=&#34;https://github.com/cloudcmds/tamarin&#34;&gt;Tamarin&lt;/a&gt;.  It&amp;rsquo;s early in its development already it&amp;rsquo;s showing some promise.  It offers a Go like syntax, which is nice, along with native literals for lists, sets and maps, which is also nice.  There&amp;rsquo;s a bit of a standard library already in place to do things like string and JSON operations. There&amp;rsquo;s not really anything that interacts with the operating system, but this is actually an advantage as it will mean that I&amp;rsquo;m free to write these modules myself to do what I need.  The implementation looks simple enough which means that it will probably play nicely with Go&amp;rsquo;s GC and scheduler.&lt;/p&gt;
&lt;p&gt;How the example above could look in Tamarin is given below.  This assumes that the asynchronous aspects are completely hidden from the script author, resulting in something a little easier to read:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ext.command(&amp;quot;cust&amp;quot;, func() {
    var userId = ui.prompt(&amp;quot;Enter UserID: &amp;quot;)
    
    var commandOut = exec.system(&amp;quot;lookup-customer-id.sh&amp;quot;, userId).unwrap()
    var userId = strings.trim_space(commandOut)
    
    var custResultSet = session.query(`pk=$1`, {
        &amp;quot;table&amp;quot;: &amp;quot;account-service-dev&amp;quot;,
        &amp;quot;args&amp;quot;: [userId]
    }).unwrap()
    
    if len(custResultSet.items.length) == 0 {
        ui.print(&amp;quot;No such user found&amp;quot;)
        return
    }
    
    session.set_result_set(custResultSet)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So it looks good to me.  However… the massive, massive downside is that it&amp;rsquo;s not a well-known language.  As clunky as JavaScript is, it&amp;rsquo;s at least a language that most people have heard of.  That&amp;rsquo;s a huge advantage, and one that warrants thinking twice about it before saying &amp;ldquo;no, thanks&amp;rdquo;.  I&amp;rsquo;m not expecting Dynamo-Browse to be more popular than sliced bread (I can count on one hand the number of user&amp;rsquo;s I know are using it), but it would be nice if scripting was somewhat approachable.&lt;/p&gt;
&lt;p&gt;One thing in Tamarin&amp;rsquo;s favour is that it&amp;rsquo;s close enough to Go that I think others could pick it up relatively easily.  It&amp;rsquo;s also not a huge language — the features can be describe on &lt;a href=&#34;https://github.com/cloudcmds/tamarin/blob/main/docs/Features.md&#34;&gt;a single Markdown page&lt;/a&gt; — and the fact that the standard library hasn&amp;rsquo;t been fully fleshed out yet can help keep things small.&lt;/p&gt;
&lt;p&gt;I guess the obvious question is why not hide the scheduling aspects from the JavaScript implementation?  Yeah, I think that&amp;rsquo;s a viable thing to do, although it might be a little harder to do than Tamarin.  That will still leave the module exception issue though.&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s where I am at the moment. I&amp;rsquo;m not quite sure what I&amp;rsquo;ll do here, but I might give Tamarin a go, and see if it result in scripts that are easier to read and write.&lt;/p&gt;
&lt;p&gt;One could argue that this pontificating over something that will probably only affect me and a handful of other people is even worth the time at all.  And yeah, you&amp;rsquo;re probably right in thinking that it isn&amp;rsquo;t. To that, I guess the only thing I can respond with is that at least I got a blog post out of it. 🙂&lt;/p&gt;
&lt;p&gt;One last concern I do have, regardless of what language I choose, is how to schedule the scripts.  If I&amp;rsquo;m serious about not exposing the specifics of which thread/goroutine is waiting for what to the script author, I&amp;rsquo;d like to run the script in a separate goroutine from the main event loop.  The concern is around start-up time: launching a goroutine is fast, but if I want to execute a script in response to a keystroke, for example, would it be fast enough?  I guess this is something I should probably test first, but if it is a little unresponsive, the way I&amp;rsquo;m thinking of tackling this is having a single running goroutine waiting on a channel for script scheduling events.  That way, the goroutine will always be &amp;ldquo;warm&amp;rdquo; and they&amp;rsquo;ll be no startup time associated with executing the script.  Something to think about.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Project Exploration: A Check-in App</title>
      <link>https://lmika.org/2022/12/18/project-exploration-a.html</link>
      <pubDate>Sun, 18 Dec 2022 08:22:40 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/12/17/project-exploration-a.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;m in a bit of a exploratory phase at the moment.  I&amp;rsquo;ve set aside Dynamo-Browse for now and looking to start something new.  Usually I need to start two or three things before I find something that grabs me: it&amp;rsquo;s very rare that I find myself something to work on that&amp;rsquo;s exciting before I actually start working on it.  And even if I start something, there&amp;rsquo;s a good chance that I won&amp;rsquo;t actually finish it.&lt;/p&gt;
&lt;p&gt;So it might seem weird that I&amp;rsquo;d write about these explorations at all.  Mainly I do it for prosperity reasons: just something to record so that when look back and think &amp;ldquo;what was that idea I had?&amp;rdquo;, I have something to reflect on.&lt;/p&gt;
&lt;p&gt;With that out of the way, I do have a few ideas that are worth considering.  Yesterday, I&amp;rsquo;ve started a new Flutter project in order to explore an idea for an app that&amp;rsquo;s been rattling around in my head for about a month or two.&lt;/p&gt;
&lt;p&gt;Basically the idea is this: I keep a very simple &lt;a href=&#34;https://lmika.day&#34;&gt;check-in blog&lt;/a&gt; where I post check-ins for locations that seem useful to recall down the line.  At the moment I post these check-ins manually, and usually a few hours after I leave in order to maintain some privacy of my movements.  I&amp;rsquo;ve been entertaining the idea of an app that would do this: I can post a check-in when I arrive but it won&amp;rsquo;t be published several hours after I leave.  The check-ins themselves follow a consistent format so the actual act of filling in the details can be handled by the app by presenting these properties like a regular form.  At the same time, I wanted to try something with the Micropub format and possibly the IndieAuth protocol.&lt;/p&gt;
&lt;p&gt;So I started playing around with a Flutter app that could do this.  I got to the point where I can post new check-ins and they will show up in a test blog.  Here are some screenshots:&lt;/p&gt;
&lt;figure&gt;
   &lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/5787bc38bf.jpg&#34; width=&#34;300&#34; height=&#34;623&#34; alt=&#34;The home screen&#34; /&gt;
   &lt;figcaption&gt;The home screen, which obviously needs a bit of work.  Pressing the floating action button will create a new checkin.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
   &lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/d37fda8ccb.jpg&#34; width=&#34;300&#34; height=&#34;623&#34; alt=&#34;Selecting the check-in type&#34; /&gt;
   &lt;figcaption&gt;Selecting the type of check-in.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
   &lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/6ffba44df5.jpg&#34; width=&#34;300&#34; height=&#34;623&#34; alt=&#34;Entering check-in properties&#34; /&gt;
   &lt;figcaption&gt;Each type will have different properties.  After selecting the check-in type, they will be presented to you in a form.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
   &lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/54d70c1224.jpg&#34; width=&#34;426&#34; height=&#34;185&#34; alt=&#34;How it looks on the blog&#34; /&gt;
   &lt;figcaption&gt;How it will ultimately look on the blog.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;It&amp;rsquo;s still very early stages at this moment.  For one thing, check-ins are published as soon as you press &amp;ldquo;Checkin&amp;rdquo;.  Eventually they should be stored on the device and then published at a later time.  The form for entering checkins should also be slightly different based on the checkin type.  Restaurants and cafes should offer a field to enter a star rating, and airport checkins should provide a way to describe your route.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure if I will pursue this any further.  We&amp;rsquo;ll see how I feel.  But it was good to get something up and running in a couple of hours.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Dynamo-Browse Running With iSH</title>
      <link>https://lmika.org/2022/11/27/dynamobrowse-running-with.html</link>
      <pubDate>Sun, 27 Nov 2022 09:15:30 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/11/26/dynamobrowse-running-with.html</guid>
      <description>&lt;p&gt;Bit of a fun one today. After thinking about how one could go about setting up a small dev environment on the iPad, I remembered that I actually had &lt;a href=&#34;https://github.com/ish-app/ish&#34;&gt;iSH&lt;/a&gt; installed. I&amp;rsquo;ve had for a while but I&amp;rsquo;ve never really used it since I never installed tools that would be particularly useful. Thinking about what tools I could install, I was curious as to whether Dynamo-Browse could run on it.  I guess if Dynamo-Browse was a simple CLI tool that does something and produces some output, it wouldn&amp;rsquo;t be to difficult to achieve this. But I don&amp;rsquo;t think I&amp;rsquo;d be exaggerating if I said Dynamo-Browse is a bit more sophisticated than your run-of-the-mill CLI tool. Part of this is finding out not only whether building it was possible, but whether it will run well.&lt;/p&gt;
&lt;p&gt;Answering the first question involves determining which build settings to use to actually produce a binary that worked.  Dynamo-Browse is a Go app, and Go has some pretty decent &lt;a href=&#34;https://www.digitalocean.com/community/tutorials/building-go-applications-for-different-operating-systems-and-architectures&#34;&gt;cross-compiling facilities&lt;/a&gt; so I had no doubt that such settings existed.  My first thought was a Darwin ARM binary since that&amp;rsquo;s the OS and architecture of the iPad. But one of the features of iSH is that it actually converts the binary through a &lt;a href=&#34;https://github.com/ish-app/ish#a-note-on-the-jit&#34;&gt;JIT&lt;/a&gt; before it runs it. And it turns out, after poking around a few of the included binaries using &lt;code&gt;file&lt;/code&gt;, that iSH expects binaries to be ELF 32-bit Linux binaries.&lt;/p&gt;
&lt;p&gt;Thus, setting &lt;code&gt;GOOS&lt;/code&gt; to &lt;code&gt;linux&lt;/code&gt; and &lt;code&gt;GOARCH&lt;/code&gt; to &lt;code&gt;386&lt;/code&gt; will produced a binary that would run in iSH:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;GOOS=linux GOARCH=386 go build -o dynamo-browse-linux ./cmd/dynamo-browse/
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After uploading it to the iPad using Airdrop and launching in in iSH, success!&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/a8429b22b1.jpg&#34; width=&#34;600&#34; height=&#34;419&#34; alt=&#34;Dynamo-Browse running in iSH on the iPad&#34; /&gt;
&lt;p&gt;So, is runs; but does is run well? Well, sadly no. Loading and scanning the table worked okay, but doing anything in the UI was an exercise in frustration. It takes about two seconds for the key press to be recognised and to move the selected row up or down. I&amp;rsquo;m not sure what the cause of this is but I suspect it&amp;rsquo;s the screen redrawing logic. There&amp;rsquo;s a lot of string manipulation involved which is not the most memory efficient.  I&amp;rsquo;m wondering if building the app using &lt;a href=&#34;https://tinygo.org&#34;&gt;TinyGo&lt;/a&gt; would improve things.&lt;/p&gt;
&lt;p&gt;But even so, I&amp;rsquo;m reasonably impress that this worked at all. Whether it will mean that I&amp;rsquo;ll be using iSH more often for random tools I build remains to be seen, but at least the possibility is there.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; While watching a re:Invent session on how a company moved from Intel to ARM, they mentioned a massive hit to performance around a function that calculates the rune length of a Unicode character. This is something that this application is constantly doing in order to layout the elements on the screen. So I&amp;rsquo;m wondering if this utility function could be the cause of the slowdown.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update 2:&lt;/strong&gt; Ok, after thinking about this more, I think the last update makes no sense. For one thing, the binary iSH is running is an Intel one, and although iSH is interpreting it, I can’t imagine the slowdowns here are a result of the compiled binary. For another, both Intel and ARM (M1) builds of Dynamo-Browse work perfectly well on desktops and laptops (at least on MacOS systems). So the reason for the slowdown must be something else.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Nee Audax Toolset</title>
      <link>https://lmika.org/2022/11/21/nxee-audax-toolset.html</link>
      <pubDate>Mon, 21 Nov 2022 21:38:23 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/11/21/nxee-audax-toolset.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve decided to retire the Audax Toolset name, at least for the moment. It was too confusing to explain what it actually was, and with only a single tool implemented, this complexity was unnecessary.&lt;/p&gt;
&lt;p&gt;The project is now named after the sole tool that is available: Dynamo-Browse. The site is now at &lt;a href=&#34;https://dynamobrowse.app&#34;&gt;dynamobrowse.app&lt;/a&gt;, although the &lt;a href=&#34;https://audax.tools&#34;&gt;old domain&lt;/a&gt; will still work. I haven&amp;rsquo;t renamed the repository just yet so there will still be references to &amp;ldquo;audax&amp;rdquo;, particularly the downloads section. I&amp;rsquo;m hoping to do this soon.&lt;/p&gt;
&lt;p&gt;I think I&amp;rsquo;m happy with this decision. It really now puts focus on the project, and removes the feeling that another tool had to be built just to satisfy the name.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2022/11/13/most-of-whats.html</link>
      <pubDate>Sun, 13 Nov 2022 08:22:38 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/11/12/most-of-whats.html</guid>
      <description>&lt;p&gt;Most of what’s going on with Audax and Dynamo-Browse is “closing the gap” between the possible queries and scans that can be performed over a DynamoDB table, and how they&amp;rsquo;re represented in Dynamo-Browse query expression language.  Most of the constructs of &lt;a href=&#34;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html&#34;&gt;DynamoDB’s conditions expression language&lt;/a&gt; can now be represented. The last thing to add is the &lt;code&gt;size()&lt;/code&gt; function, and that is proving to be a huge pain.&lt;/p&gt;
&lt;p&gt;The reason is that the &lt;a href=&#34;https://workingset.net/2022/09/13/one-other-change.html&#34;&gt;IR representation&lt;/a&gt; is using the &lt;a href=&#34;https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression&#34;&gt;expression builder&lt;/a&gt; package to actually put the expression together. These builders uses Go’s type system to enforce which constructs work with each other one. But this clashes with how I built the IR representation types, which are essentially structs implement a common interface. Without having an overarching type to represent an expression builder, I’m left with either using a very broad type like &lt;code&gt;any&lt;/code&gt;, or completely ditching this package and doing something else to build the expression.&lt;/p&gt;
&lt;p&gt;It feels pretty annoying reaching the brick wall just when I was finishing this off. But I guess them’s the breaks.&lt;/p&gt;
&lt;p&gt;One other thing I’m still considering is spinning out Dynamo-Browse into a separate project.  It currently sits under the “Audax” umbrella, with the intention of releasing other tools as part of the tool set.  These tools actually exist&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; but I haven’t been working on them and they’re not in a fit enough state to release them. So the whole Audax concept is confusing and difficult to explain with only one tool available at the moment.&lt;/p&gt;
&lt;p&gt;I suppose if I wanted to work on the other tools, this will work out in the end.  But I’m not sure that I do, at least not now.  And even if I do, I&amp;rsquo;m now beginning to wonder if building them as TUI tools would be the best way to go.&lt;/p&gt;
&lt;p&gt;So maybe the best course of action is to make Dynamo-Browse a project in it&amp;rsquo;s own right.  I think it&amp;rsquo;s something I can resurrect later should I get to releasing a second tool.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Edit at 9:48:&lt;/strong&gt; I managed to get support for the size function working.  I did it by adding a new interface type with a function that returns a &lt;code&gt;expression.OperandBuilder&lt;/code&gt;.  The existing IR types representing names and values were modified to inherit this interface, which gave me a common type I could use for the equality and comparison expression builder functions.&lt;/p&gt;
&lt;p&gt;This meant that the IR nodes that required a name and literal value operand — which are the only constructs allowed for key expressions — had to be split out into separate types from the &amp;ldquo;generic&amp;rdquo; ones that only worked on any &lt;code&gt;OperandBuilder&lt;/code&gt; node.  But this was not as large a change as I was expecting, and actually made the code a little neater.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;Dynamo-Browse was actually the second TUI tool I made as part of what was  called “awstools”. The first was actually an SQS browser.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>Audax Toolset Version 0.1.0</title>
      <link>https://lmika.org/2022/10/22/audax-toolset-version.html</link>
      <pubDate>Sat, 22 Oct 2022 10:20:36 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/10/22/audax-toolset-version.html</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://github.com/lmika/audax/releases/tag/v0.1.0&#34;&gt;Audax Toolset version 0.1.0&lt;/a&gt; is finally released and is available on GitHub. This version contains updates to Dynamo-Browse, which is still the only tool in the toolset so far.&lt;/p&gt;
&lt;p&gt;Here are some of the headline features.&lt;/p&gt;
&lt;h2 id=&#34;adjusting-the-displayed-columns&#34;&gt;Adjusting The Displayed Columns&lt;/h2&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/80871cfb0d.jpg&#34; width=&#34;600&#34; height=&#34;436&#34; alt=&#34;The Fields Popup&#34; /&gt;
&lt;p&gt;Consider a table full of items that look like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pk           S    00cae3cc-a9c0-4679-9e3a-032f75c2b506
sk           S    00cae3cc-a9c0-4679-9e3a-032f75c2b506
address      S    3473 Ville stad, Jersey , Mississippi 41540
city         S    Columbus
colors       M    (2 items)
  door       S    MintCream
  front      S    Tan
name         S    Creola Konopelski
officeOpened BOOL False
phone        N    9974834360
ratings      L    (3 items)
  0          N    4
  1          N    3
  2          N    4
web          S    http://www.investorgranular.net/proactive/integrate/open-source
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&amp;rsquo;s say you&amp;rsquo;re interested in seeing the city, the door colour and the website in the main table which, by default, would look something like this:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/cb7da879b6.jpg&#34; width=&#34;600&#34; height=&#34;436&#34; alt=&#34;The before table layout&#34; /&gt;
&lt;p&gt;There are a few reasons why the table is laid out this way. The partition and sort key are always the first two columns, followed by any declared fields that may be used for indices.  This is followed by all the other top-level fields sorted in alphabetical order.  Nested fields are not included as columns, and maps and list fields are summarised with the number of items they hold, e.g. &lt;code&gt;(2 items)&lt;/code&gt;.  This makes it impossible to only view the columns you&amp;rsquo;re interested in.&lt;/p&gt;
&lt;p&gt;Version 0.1.0 now allows you to adjust the columns of the table.  This is done using the &lt;a href=&#34;https://audax.tools/docs/dynamo-browse/getting-around/#adjusting-the-displayed-columns&#34;&gt;Fields Popup&lt;/a&gt;, which can be opened by pressing &lt;kbd&gt;f&lt;/kbd&gt;.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/549d22fab0.jpg&#34; width=&#34;600&#34; height=&#34;436&#34; alt=&#34;Adjusting the columns in the table&#34; /&gt;
&lt;p&gt;While this popup is visible you can show columns, hide them, or move them left or right. You can also add new columns by entering a &lt;a href=&#34;https://audax.tools/docs/dynamo-browse/reference/query-expressions/&#34;&gt;Query Expression&lt;/a&gt;, which can be used to reveal the value of nested fields within the main table.  It&amp;rsquo;s now possible to change the columns of the table to be exactly what you&amp;rsquo;re interested in:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/4a02eb72c8.jpg&#34; width=&#34;600&#34; height=&#34;436&#34; alt=&#34;The after table layout&#34; /&gt;
&lt;h2 id=&#34;read-only-mode-and-result-limits&#34;&gt;Read-only Mode And Result Limits&lt;/h2&gt;
&lt;p&gt;Version 0.1.0 also contains some niceties for reducing the impact of working on production tables.  Dynamo-Browse can now be started in read-only mode using the &lt;code&gt;-ro&lt;/code&gt; flag, which will disable all write operations — a useful feature if you&amp;rsquo;re paranoid about accidentally modifying data on production databases.&lt;/p&gt;
&lt;p&gt;Another new flag is &lt;code&gt;-default-limit&lt;/code&gt; which will change the default number of items returned from scans and queries from 1000 to whatever you want.  This is useful to cut down on the number of read capacity units Dynamo-Browse will use on the initial scans of production tables.&lt;/p&gt;
&lt;p&gt;These settings are also changeable from while Dynamo-Browse using the new &lt;a href=&#34;https://audax.tools/docs/dynamo-browse/reference/commands/#set&#34;&gt;set command&lt;/a&gt;:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/1afbded2a3.jpg&#34; width=&#34;600&#34; height=&#34;436&#34; alt=&#34;Using the set command in Dynamo-Browse&#34; /&gt;
&lt;h2 id=&#34;progress-indicators-and-cancelation&#34;&gt;Progress Indicators And Cancelation&lt;/h2&gt;
&lt;p&gt;Dynamo-Browse now indicates running operations, like scans or queries, with a spinner.  This improves the user experience of prior versions of Dynamo-Browse, which gave no feedback of running operations whatsoever and would simply &amp;ldquo;pop-up&amp;rdquo; the result of such operations in a rather jarring way.&lt;/p&gt;
&lt;p&gt;With this spinner visible in the status bar, it is also now possible to cancel an operation by pressing &lt;kbd&gt;Ctrl-C&lt;/kbd&gt;.  You have the option to view any partial results that were already retrieved at the time.&lt;/p&gt;
&lt;h2 id=&#34;other-changes&#34;&gt;Other Changes&lt;/h2&gt;
&lt;p&gt;Here are some of the other bug-fix and improvements that are also included in this release:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Audax toolset is now distributed via Homebrew.  Check out the &lt;a href=&#34;https://audax.tools/download/&#34;&gt;Downloads&lt;/a&gt; page for instructions.&lt;/li&gt;
&lt;li&gt;A new &lt;a href=&#34;https://audax.tools/docs/dynamo-browse/reference/commands/#mark&#34;&gt;mark command&lt;/a&gt; to mark all, unmark all, or toggle marked rows.  The &lt;code&gt;unmark&lt;/code&gt; command is now an alias to &lt;code&gt;mark none&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Query expressions involving the partition and sort key of the main table are now executed as a DynamoDB queries, instead of scans.&lt;/li&gt;
&lt;li&gt;The query expression language now supports conjunction, disjunction, and dot references.&lt;/li&gt;
&lt;li&gt;Fixed a bug which was not properly detecting whether MacOS was in light mode.  This was making some highlighted colours hard to see while in dark mode.&lt;/li&gt;
&lt;li&gt;Fixed the table-selection filter, which was never properly working since the initial release.&lt;/li&gt;
&lt;li&gt;Fixed the back-stack service to prevent duplicate views from being pushed.&lt;/li&gt;
&lt;li&gt;Fixed some conditions which were causing seg. faults.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Full details of the changes can be &lt;a href=&#34;https://github.com/lmika/audax/releases/tag/v0.1.0&#34;&gt;found on GitHub&lt;/a&gt;.  Details about the various features can also be found in the &lt;a href=&#34;https://audax.tools/docs/dynamo-browse/&#34;&gt;user manual&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, although it does not touch on any of the features described above, I recorded a introduction video on the basics of using Dynamo-Browse to view items of a DynamoDB table:&lt;/p&gt;
&lt;div style=&#34;position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;&#34;&gt;
      &lt;iframe allow=&#34;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen&#34; loading=&#34;eager&#34; referrerpolicy=&#34;strict-origin-when-cross-origin&#34; src=&#34;https://www.youtube.com/embed/cQnTIg1_tfg?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0&#34; style=&#34;position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;&#34; title=&#34;YouTube video&#34;&gt;&lt;/iframe&gt;
    &lt;/div&gt;

&lt;p&gt;No promises, but I may record further videos touching on other aspects of the tool in the future.  If that happens, I&amp;rsquo;ll make sure to mention them here.&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;Or you can like, comment or subscribe on YouTube if that&amp;rsquo;s your thing 😛.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2022/10/20/putting-the-final.html</link>
      <pubDate>Thu, 20 Oct 2022 21:01:23 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/10/20/putting-the-final.html</guid>
      <description>&lt;p&gt;Putting the final touches on the website for the upcoming release of Audax Toolset v0.1.0, and I&amp;rsquo;m finding myself a bit unhappy with it.  Given that Dynamo-Browse is the only tool in this &amp;ldquo;suite&amp;rdquo;, it feels weird putting together a landing page with a whole lot of prose about this supposed collection of tools.  There&amp;rsquo;s no great place to talk more about Dynamo-Browse right there on the landing page.&lt;/p&gt;
&lt;p&gt;Part of me is wondering whether it would be better focusing the site solely on Dynamo-Browse, and leave all this Audax Toolset stuff on the back-burner, at least until (or unless) another tool is made available through this collection.  I&amp;rsquo;m wondering if I&amp;rsquo;ll need to rearrange the codebase to do this, and spin out the other commands currently in development into separate repositories.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Bridging The Confidence Gap</title>
      <link>https://lmika.org/2022/10/20/bridging-the-confidence.html</link>
      <pubDate>Thu, 20 Oct 2022 13:06:14 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/10/20/bridging-the-confidence.html</guid>
      <description>&lt;p&gt;I had to do some production work with DynamoDB this morning.  It wasn&amp;rsquo;t particularly complicated work: run a query, get a couple of rows, change two attributes on each one.  I could have used Dynamo-Browse to do this.  But I didn&amp;rsquo;t. Despite building a tool designed for doing these sorts of things, and using it constantly for all sorts of non-prod stuff, I couldn&amp;rsquo;t bring myself to use it on a production database.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m wondering why this is.  It&amp;rsquo;s not like I have any issues with using Dynamo-Browse for the non-prod stuff.  Sure there a bugs, and things that the tool can&amp;rsquo;t do yet, but I haven&amp;rsquo;t (yet) encountered a situation where the tool actually corrupted data.  I also make sure that the code that touches the database is tested against a &amp;ldquo;real&amp;rdquo; instance of DynamoDB (it&amp;rsquo;s actually a mock server running in Docker).&lt;/p&gt;
&lt;p&gt;Yet I still don&amp;rsquo;t have the confidence to use it on a production database.  And okay, part of me understands this to a degree.  I&amp;rsquo;ve only been using this tool for a handful of months, and when it comes to dealing with production data, having a sense of caution, bordering on a small sense of paranoia, is probably healthy.  But if I can&amp;rsquo;t be confident that Dynamo-Browse will work properly when it matters the most, how can I expect others to have that confident?&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;d like to get to the bottom of this.  After a bit of thought, I think there are three primary causes of my lack of confidence here.&lt;/p&gt;
&lt;p&gt;The first is simple: there are features missing in the tool that are needed to do my job.  Things such as running a query over an index, or the ability to impose a conditional check on updates.  This is a legitimate concern in my opinion: if you need to do something, and Dynamo-Browse doesn&amp;rsquo;t support it, then you can&amp;rsquo;t use Dynamo-Browse to do that thing, plain and simple. It&amp;rsquo;s also the easiest concern to address: either add the feature, or say that feature is unsupported and don&amp;rsquo;t expect people to use Dynamo-Browse if they need it.&lt;/p&gt;
&lt;p&gt;I think the second cause is a lack of insight into what the tool is actually doing.  Even though I built the tool and  tested it during development, there&amp;rsquo;s always this feeling of uncertainty in the back of my head while I&amp;rsquo;m using it when the stakes are high.  That feeling of &amp;ldquo;when I do this, although I &lt;em&gt;think&lt;/em&gt; the tool will behave this way, how will the tool &lt;em&gt;actually&lt;/em&gt; behave?&amp;rdquo;  Part of this, I think, comes from a decade and a half of being burned by technology as part of my job, and growing a healthy (?) sense of distrust for it.&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m sure some of this will resolve itself as I continue to use Dynamo-Browse, but the only approach I know of gaining assurance that the tool is working as intended is if the tool tells me what it&amp;rsquo;s doing.  Logging will help here, but I think some features that provide the user the ability to check what&amp;rsquo;s going to happen prior to actually doing it would be useful as well.&lt;/p&gt;
&lt;p&gt;The third cause is probably a lack of controlled end-to-end testing.  There does exists a suite of unit tests with a decent (maybe not good, but decent) level of coverage, but it does not extend to the UI layer, which is a little problematic.  It might be that more testing of the application as a whole would help here.&lt;/p&gt;
&lt;p&gt;This means more manual testing, but it might also be possible to setup some automated testing of the entire tool end-to-end here as well.  What&amp;rsquo;s going for me is the fact that Dynamo-Browse runs in the terminal, and is responsible for rendering the UI and handling the event loop instead of palming this off to the OS.  Looking at some of the features that Bubble Tea offers, it might be possible to run the application headless, and simulate use by pumping in keyboard events.  Verifying that the UI is such may be a little difficult, but what I can test is what is actually read from and written to the database, which is what I&amp;rsquo;m mostly interested in.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m still unsure as to what I&amp;rsquo;ll do to address these three causes of concern.  This is still a hobby project that I do in my spare time, and some of the mundane tasks, like more manual testing, sound unappealing.  On the other hand, I do want to use this tool for my job, and I want others to use it as well.  So I can&amp;rsquo;t really complain that others choose not to use it if I cannot feel comfortable using it when it matters most.  So I probably should do some of each.  And who knows, I may actually get some enjoyment out of doing it.  I certainly would get some enjoyment from knowing that others can rely on it.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;Someone once said that an insane person is one that does the same thing twice and expects different results.  That someone has never worked in technology.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>Things Dynamo-Browse Need</title>
      <link>https://lmika.org/2022/09/29/things-dynamobrowse-need.html</link>
      <pubDate>Thu, 29 Sep 2022 11:17:56 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/09/29/things-dynamobrowse-need.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;m working with dynamo-browse a lot this morning and I&amp;rsquo;m coming up with a bunch of usability shortcomings.  I&amp;rsquo;m listing them here so I don&amp;rsquo;t forget.&lt;/p&gt;
&lt;p&gt;The query language needs an &lt;code&gt;in&lt;/code&gt; operator, such as &lt;code&gt;pk in (&amp;quot;abc&amp;quot;, &amp;quot;123&amp;quot;)&lt;/code&gt;.  This works like &lt;code&gt;in&lt;/code&gt; in all the other database services out there, in that the expression effectively becomes `pk = &amp;ldquo;abc&amp;rdquo; or pk = &amp;ldquo;123&amp;rdquo;.  Is this operator supported natively in DynamoDB? 🤔  Need to check that.&lt;/p&gt;
&lt;p&gt;Likewise, the query language needs something to determine whether an attribute contains a substring.  I believe DynamoDB expressions support this natively, so it probably makes sense to use that.&lt;/p&gt;
&lt;p&gt;Longer term, it would be nice to include the results of the current result set in the expression.  For example, assuming the current result set has these records:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pk    sk    thing     place   category
11    aa    rock      home    geology
22    bb    paper     home    art
33    cc    scissors  home    utensils
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and you want to effectively query for the rows where &lt;code&gt;pk&lt;/code&gt; is equal to the set of &lt;code&gt;pk&lt;/code&gt; in the current result set, having a way to do that in the expression language would save a lot of copy-and-pasting.  An example might be &lt;code&gt;pk in @pk&lt;/code&gt; or something similar, which could produce a result set of the form:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pk    sk    thing      place  category
11    aa    rock       home   geology
11    ab    sand       beach  ~
22    bb    paper      home   art
22    bc    cardboard  shops  ~
33    cc    scissors   home   utensils
33    cd    spoon      cafe   ~
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another way to do this might be to add support for filters, much like the expressions in JQ.  For example, the second result set could be retrieved just by using a query expression of the form &lt;code&gt;place = &amp;quot;home&amp;quot; | fanout pk&lt;/code&gt;, which could effectively do the following pesudo-code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;firstResultSet := query(&amp;quot;place = &#39;home&#39;)
secondResultSet := newResultSet()
for pk in resultSet[&#39;pk&#39;] {
    secondResultSet += query(&amp;quot;pk = ?&amp;quot;, pk)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For dealing with workspaces, a quick way to open up the last workspace that you had opened would be nice, just in case you accidentally close Dynamo-Browse and you want to restore the last session you had opened.  Something like &lt;code&gt;screen -r&lt;/code&gt;.  I think in this case having workspace files stored in the temporary directory might be a problem.  Maybe having some way to set where workspace files are stored by default would work? 🤔&lt;/p&gt;
&lt;p&gt;For dealing with marking rows, commands to quickly mark or unmark rows in bulk.  There&amp;rsquo;s already the &lt;a href=&#34;https://audax.tools/docs/dynamo-browse/reference/#unmark&#34;&gt;unmark&lt;/a&gt; command, but something similar for marking all rows would be good.  This could be extended to only mark (or unmark) certain rows, such as those that match a particular substring (then again, using filters would work here as well).&lt;/p&gt;
&lt;p&gt;It might be nice for refresh to keep your current row and column position, instead of going to the top-left.  And it would also be nice to have a way to refresh credentials (could possiably be handled using the scripting framework).&lt;/p&gt;
&lt;p&gt;And finally, if I have any doubt that the feature to hide or rearrange columns would not be useful, there was an instance today where I wish this feature was around.  So, keep working on that.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m sure there will be more ideas.  If so, I&amp;rsquo;ll post them here.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Overlay Composition Using Bubble Tea</title>
      <link>https://lmika.org/2022/09/24/overlay-composition-using.html</link>
      <pubDate>Sat, 24 Sep 2022 13:58:24 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/09/24/overlay-composition-using.html</guid>
      <description>&lt;p&gt;Working on a new feature for Dynamo-Browse which will allow the user to modify the columns of the table: move them around, sort them, hide them, etc.  I want the feature to be interactive instead of a whole lot of command incantations that are tedious to write.  I also kind of want the table whose columns are being manipulated to be visible, just so that the affects of the change would be apparent to the user while they make them.&lt;/p&gt;
&lt;p&gt;This is also an excuse to try something out using Bubble Tea — the TUI framework I&amp;rsquo;m using — which is to add the ability to display overlays.  These are UI elements (the overlay) that appear above other UI elements (the background).  The two are composed into a single view such that it looks like the overlay is obscuring parts of the background, similar to how windows work in MacOS.&lt;/p&gt;
&lt;p&gt;Bubble Tea doesn&amp;rsquo;t have anything like this built in, and the way views are rendered in Bubble Tea doesn&amp;rsquo;t make this super easy.  The best way to describe how views work is to think of them as &amp;ldquo;scanlines.&amp;rdquo;  Each view produces a string with ANSI escape sequences to adjust the style.  The string can contain newlines which can be used to move the cursor down while rendering.  Thus, there&amp;rsquo;s no easy way position the cursor at an arbitrary position and render characters over the screen.&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;So, I thought I&amp;rsquo;d tackle this myself.&lt;/p&gt;
&lt;h2 id=&#34;attempt-1&#34;&gt;Attempt 1&lt;/h2&gt;
&lt;p&gt;On the surface, the logic for this is simple.  I&amp;rsquo;ll render the background layer up to the top most point of the overlay.  Then for each scan line within the top and bottom border of the overlay, I&amp;rsquo;ll render the background up to the overlay&amp;rsquo;s left border, then the contents of the overlay itself, then the rest of the background from the right border of the overlay.&lt;/p&gt;
&lt;p&gt;My first attempt was something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;line := backgroundScanner.Text()
if row &amp;gt;= c.overlayY &amp;amp;&amp;amp; row &amp;lt; c.overlayY+c.overlayH {
    // When scan line is within top &amp;amp; bottom of overlay
    compositeOutput.WriteString(line[:c.foreX])
    
    foregroundScanPos := row - c.overlayY
    if foregroundScanPos &amp;lt; len(foregroundViewLines) {
        displayLine := foregroundViewLines[foregroundScanPos]
        compositeOutput.WriteString(lipgloss.PlaceHorizontal(
            c.overlayW,
            lipgloss.Left,
            displayLine,
            lipgloss.WithWhitespaceChars(&amp;quot; &amp;quot;)),
        )
    }
    
    compositeOutput.WriteString(line[c.overlayX+c.overlayW:])
} else {
    // Scan line is beyond overlay boundaries of overlay
    compositeOutput.WriteString(line)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&amp;rsquo;s how that looked:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/670f516c6d.jpg&#34; width=&#34;600&#34; height=&#34;428&#34; alt=&#34;Attempt 1&#34; /&gt;
&lt;p&gt;Yeah, not great.&lt;/p&gt;
&lt;p&gt;Turns out I forgot two fundamental things.  One is that the indices of Go strings works on the underlying byte array, not runes.  This means that attempting to slice a string between multi-byte Unicode runes would produce junk.  It&amp;rsquo;s a little difficult to find this in the Go Language Guide apart from this cryptic line:&lt;/p&gt;

  &lt;blockquote&gt;
    &lt;p&gt;A string&amp;rsquo;s bytes can be accessed by integer indices 0 through len(s)-1&lt;/p&gt;

  &lt;/blockquote&gt;
&lt;p&gt;But it&amp;rsquo;s relatively easy to test within the Go playground:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &amp;quot;fmt&amp;quot;

func main() {
    fmt.Println(&amp;quot;世界😀😚🙃😇🥸😎&amp;quot;[1:6])                   // ��界
    fmt.Println(string([]rune(&amp;quot;世界😀😚🙃😇🥸😎&amp;quot;)[1:6]))   // 界😀😚🙃😇
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second issue is that I&amp;rsquo;m splitting half-way through an ANSI escape sequence.  I don&amp;rsquo;t know how long the escape sequence is to style the header of the item view, but I&amp;rsquo;m betting that it&amp;rsquo;s longer than 5 bytes (the overlay is to be position at column 5).  That would explain why there&amp;rsquo;s nothing showing up to the left of the overlay for most rows, and why the sequence &lt;code&gt;6;48;5;188m&lt;/code&gt; is there.&lt;/p&gt;
&lt;h2 id=&#34;attempt-2&#34;&gt;Attempt 2&lt;/h2&gt;
&lt;p&gt;I need to modify the logic so that zero-length escape sequences are preserved.  Fortunately, one of Bubble Tea&amp;rsquo;s dependency is &lt;a href=&#34;github.com/muesli/reflow&#34;&gt;reflow&lt;/a&gt;, which offers a bunch of nice utilities for dealing with ANSI escape sequences.  The function that looks promising is &lt;code&gt;truncate.String&lt;/code&gt;, which will truncate a string at a given width.&lt;/p&gt;
&lt;p&gt;So changing the logic slightly, the solution became this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// When scan line is within top &amp;amp; bottom of overlay
compositeOutput.WriteString(truncate.String(line, uint(c.overlayX)))

foregroundScanPos := r - c.overlayY
if foregroundScanPos &amp;lt; len(foregroundViewLines) {
    displayLine := foregroundViewLines[foregroundScanPos]
    compositeOutput.WriteString(lipgloss.PlaceHorizontal(
        c.overlayW,
        lipgloss.Left,
        displayLine,
        lipgloss.WithWhitespaceChars(&amp;quot; &amp;quot;),
    ))
}

rightStr := truncate.String(line, uint(c.foreX+c.overlayW))
compositeOutput.WriteString(line[len(rightStr):])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The results are a little better.  At least the left side of the overlay looked OK now:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/0a01d51906.jpg&#34; width=&#34;600&#34; height=&#34;428&#34; alt=&#34;Attempt 2&#34; /&gt;
&lt;p&gt;But there are still problems here.  Notice the &lt;code&gt;[0m&lt;/code&gt; at the right side of the overlay on the selected row.  I can assure you that&amp;rsquo;s not part of the partition key; take a look at the item view to see for yourself.  And while you&amp;rsquo;re there, notice the header of the item view?  That should be a solid grey bar, but instead it&amp;rsquo;s cut off at the overlay.&lt;/p&gt;
&lt;p&gt;I suspect that &lt;code&gt;rightStr&lt;/code&gt; does not have the proper ANSI escape sequences.  I&amp;rsquo;ll admit that the calculation used to set &lt;code&gt;rightStr&lt;/code&gt; is a bit of a hack.  I&amp;rsquo;ll need to replace it with a proper way to detect the boundary of an ANSI escape sequence.  But it&amp;rsquo;s more than just that.  If an ANSI escape sequence starts off at the left side of the string, and continues on &amp;ldquo;underneath&amp;rdquo; the overlay, it should be preserved on the right side of the overlay as well.  The styling of the selected row and the item view headers are examples of that.&lt;/p&gt;
&lt;h2 id=&#34;attempt-3&#34;&gt;Attempt 3&lt;/h2&gt;
&lt;p&gt;So here&amp;rsquo;s what I&amp;rsquo;m considering: we &amp;ldquo;render&amp;rdquo; the background underneath the overlay to a null buffer while recording any escape sequences that were previously set on the left, or were changed underneath the overlay.  We also keep track of the number of visible characters that were seen.  Once the scan line position reached the right border of the overlay, we replay all the ANSI escape sequences in the order that were found, and then render the right hand side of the scan line from the first visible character.&lt;/p&gt;
&lt;p&gt;I was originally considering rendering these characters to a null reader, but what I actually did was simply count the length of visible characters in a loop.  The function to do this looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (c *Compositor) renderBackgroundUpTo(line string, x int) string {
    ansiSequences := new(strings.Builder)
    posX := 0
    inAnsi := false

    for i, c := range line {
        if c == ansi.Marker {
            ansiSequences.WriteRune(c)
            inAnsi = true
        } else if inAnsi {
            ansiSequences.WriteRune(c)
            if ansi.IsTerminator(c) {
                inAnsi = false
            }
        } else {
            if posX &amp;gt;= x {
                return ansiSequences.String() + line[i:]
            }
            posX += runewidth.RuneWidth(c)
        }
    }
    return &amp;quot;&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the value set to &lt;code&gt;rightStr&lt;/code&gt; is changed to simply used this function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rightStr := c.renderBackgroundUpTo(line, c.foreX+c.overlayW)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the result:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/9a0dabfcb0.jpg&#34; width=&#34;600&#34; height=&#34;428&#34; alt=&#34;&#34; /&gt;
&lt;p&gt;That looks a lot better.  Gone are the artefacts from splitting in ANSI escape sequences, and the styling of the selected row and item view header are back.&lt;/p&gt;
&lt;p&gt;I can probably work with this.  I&amp;rsquo;m hoping to use this to provide a way to customise the columns with the table view.  It&amp;rsquo;s most likely going to power other UI elements as well.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;Well, there might be a way using ANSI escape sequences, but that goes against the approach of the framework.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>Intermediary Representation In Dynamo-Browse Expressions</title>
      <link>https://lmika.org/2022/09/16/intermediary-representation-in.html</link>
      <pubDate>Fri, 16 Sep 2022 22:37:20 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/09/16/intermediary-representation-in.html</guid>
      <description>&lt;p&gt;One other thing I did in Dynamo-Browse is change how the query AST produced the actual DynamoDB call.&lt;/p&gt;
&lt;p&gt;Previously, the AST produced the DynamoDB call directly.  For example, if we were to use the expression &lt;code&gt;pk = &amp;quot;something&amp;quot; and sk ^= &amp;quot;prefix&amp;quot;&lt;/code&gt;, the generated AST may look something like the following:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/f5b8e16f36.jpg&#34; width=&#34;600&#34; height=&#34;387&#34; alt=&#34;AST from the parse expression&#34; /&gt;
&lt;p&gt;The AST will then be traversed to determine whether this could be handled by either running a query or a scan.  This is called &amp;ldquo;planning&amp;rdquo; and the results of this will determine which DynamoDB API endpoint will be called to produce the result.  This expression may produce a call to DynamoDB that would look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;client.Query(&amp;amp;dynamodb.QueryInput{
    TableName: &amp;quot;my-table-name&amp;quot;,
    KeyConditionExpression: &amp;quot;#0 = :0 and beings_with(#1, :1)&amp;quot;,
    ExpressionAttributeNames: map[string]string{
        &amp;quot;#0&amp;quot;: &amp;quot;pk&amp;quot;,
        &amp;quot;#1&amp;quot;: &amp;quot;sk&amp;quot;,
    },
    ExpressionAttributeValues: map[string]types.AttributeValue{
         &amp;quot;:0&amp;quot;: &amp;amp;types.StringAttributeValue{ Value: &amp;quot;something&amp;quot; },
         &amp;quot;:1&amp;quot;: &amp;amp;types.StringAttributeValue{ Value: &amp;quot;prefix&amp;quot; },
    },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, instead of determining the various calls to DynamoDB itself, the AST will generate an intermediary representation, something similar to the following:&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/0839b39179.jpg&#34; width=&#34;600&#34; height=&#34;514&#34; alt=&#34;Intermediary representation tree&#34; /&gt;
&lt;p&gt;The planning traversal will now happen off this tree, much like it did over the AST.&lt;/p&gt;
&lt;p&gt;For such a simple expression, the benefits of this extra step may not be so obvious.  But there are some major advantages that I can see from doing this.&lt;/p&gt;
&lt;p&gt;First, it simplifies the planning logic quite substantially.  If you compare the first tree with the second, notice how the nodes below the &amp;ldquo;and&amp;rdquo; node are both of type &amp;ldquo;binOp&amp;rdquo;.  This type of node represents a binary operation, which can either be &lt;code&gt;=&lt;/code&gt; or &lt;code&gt;^=&lt;/code&gt;, plus all the other binary operators that may come along.  Because so many operators are represented by this single node type, the logic of determining whether this part of the expression can be represented as a query will need to look something like the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First determine whether the operation is either &lt;code&gt;=&lt;/code&gt; or &lt;code&gt;^=&lt;/code&gt; (or whatever else)&lt;/li&gt;
&lt;li&gt;If it&amp;rsquo;s &lt;code&gt;=&lt;/code&gt; and the field on the left is either a partition or sort key, this can be represented as a query&lt;/li&gt;
&lt;li&gt;If it&amp;rsquo;s &lt;code&gt;^=&lt;/code&gt;, first determine whether the operand is a string, (if not, fail) and then determine whether the field on the left is a sort key.  If so, then this can be query.&lt;/li&gt;
&lt;li&gt;Otherwise, it will have to be a scan.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is mixing various stages of the compilation phase in a single traversal: determining what the operator is, determining whether the operands are valid (&lt;code&gt;^=&lt;/code&gt; must have a string operand), and working out how we can run this as a query, if at all.  You can imagine the code to do this being large and fiddly.&lt;/p&gt;
&lt;p&gt;With the IR tree, the logic can be much simpler.  The work surrounding the operand is done when the AST tree is traverse.  This is trivial: if it&amp;rsquo;s &lt;code&gt;=&lt;/code&gt; then produce a &amp;ldquo;fieldEq&amp;rdquo;; if it&amp;rsquo;s &lt;code&gt;^=&lt;/code&gt; then produce a &amp;ldquo;fieldBeginsWith&amp;rdquo;, and so on.  Once we have the IR tree, we know that when we encounter a &amp;ldquo;fieldEq&amp;rdquo; node, this attribute can be represented as a query if the field name is either the partition or sort key.  And when we encounter the &amp;ldquo;fieldBeginsWith&amp;rdquo; node, we know we can use a query if the field name is the sort key.&lt;/p&gt;
&lt;p&gt;Second, it allows the AST to be richer and not tied to how the actual call is planned out.  You won&amp;rsquo;t find the &lt;code&gt;^=&lt;/code&gt; operator in any of the expressions DynamoDB supports: this was added to Dynamo-Browse&amp;rsquo;s expression language to make it easier to write.  But if we were to add the &amp;ldquo;official&amp;rdquo; function for this as well — &lt;code&gt;begins_with()&lt;/code&gt; — and we weren&amp;rsquo;t using the IR, we would need to have the planning logic for this in two places.  With an IR, we can simply have both operations produce a &amp;ldquo;fieldBeginsWith&amp;rdquo; node.  Yes, there could be more code encapsulated by this IR node (there&amp;rsquo;s actually less) but it&amp;rsquo;s being leverage by two separate parts of the AST.&lt;/p&gt;
&lt;p&gt;And since expressions are not directly mappable to DynamoDB expression types, we can theoretically do things like add arithmetic operations or a nice suite of utility functions.  Provided that these produce a single result, these parts of the expression can be evaluated while the IR is being built, and the literal value returned that can be used directly.&lt;/p&gt;
&lt;p&gt;It felt like a few other things went right with this decision.  I was expecting this to take a few days, but  I was actually able to get it built in a single evening.  I&amp;rsquo;m also happy about how maintainable the code turned out to be.  Although there are two separate tree-like types that need to be managed, both have logic which is much simpler than what we were dealing with before.&lt;/p&gt;
&lt;p&gt;All in all, I&amp;rsquo;m quite happy with this decision.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Letting Queries Actually Be Queries In Dynamo-Browse</title>
      <link>https://lmika.org/2022/09/13/letting-queries-actually.html</link>
      <pubDate>Tue, 13 Sep 2022 22:44:18 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/09/13/letting-queries-actually.html</guid>
      <description>&lt;p&gt;I spent some more time working on dynamo-browse over the weekend (I have to now that I&amp;rsquo;ve got a user-base 😄).&lt;/p&gt;
&lt;p&gt;No real changes to scripting yet.  It&amp;rsquo;s still only me that&amp;rsquo;s using it at the moment, and I&amp;rsquo;m hoping to keep it this way until I&amp;rsquo;m happy enough with the API.  I think we getting close though.  I haven&amp;rsquo;t made the changes discussed in the &lt;a href=&#34;https://workingset.net/2022/08/31/thinking-about-scripting.html&#34;&gt;previous post&lt;/a&gt; about including the builtin &lt;code&gt;plugin&lt;/code&gt; object. I&amp;rsquo;m thinking that instead of a builtin object, I&amp;rsquo;ll use another module instead, maybe something like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ext = require(&amp;quot;audax:ext&amp;quot;);

ext.registerCommand(&amp;quot;thing&amp;quot;, () =&amp;gt; console.log(&amp;quot;Do the thing&amp;quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The idea is that the &lt;code&gt;ext&lt;/code&gt; module will provide access to the extension points that the script can implement, such as registering new commands, etc.&lt;/p&gt;
&lt;p&gt;At this stage, I&amp;rsquo;m not sure if I want to add the concept of namespaces to the &lt;code&gt;ext&lt;/code&gt; module name.  Could help in making it explicit that the user is accessing the hooks of &amp;ldquo;this&amp;rdquo; extension, as opposed to others.  It may leave some room for some nice abilities, like provide a way to send messages to other extensions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// com.example.showTables
// This extension will handle events which will prompt to show tables. 
const ext = require(&amp;quot;audax:ext/this&amp;quot;);

ext.on(&amp;quot;show-tables&amp;quot;, () =&amp;gt; {
    ui.showTablePrompt();
});

// com.example.useShowTables
// This extension will use the &amp;quot;show-tables&amp;quot; message &amp;quot;com.example.showTables&amp;quot;
const showTables = require(&amp;quot;audax:ext/com.example.showTables&amp;quot;);

showTables.post(&amp;quot;show-tables&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then again, this might be unnecessary given that the JavaScript module facilities are there.&lt;/p&gt;
&lt;p&gt;Anyway, what I actually did was start work on making queries actually run as queries against the DynamoDB table.  In the released version, running a query (i.e. pressing &lt;kbd&gt;?&lt;/kbd&gt; and entering a &lt;a href=&#34;https://audax.tools/docs/dynamo-browse/filtering-querying/#querying&#34;&gt;query expression&lt;/a&gt;) will actually perform a table scan.  Nothing against scans: they do what they need to do.  But hardly the quickest way to get rows from DynamoDB if you know the partition and sort key.&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s what I&amp;rsquo;m working on now.  Running a query of the form &lt;code&gt;pk = &amp;quot;something&amp;quot; and sk = &amp;quot;else&amp;quot;&lt;/code&gt; where &lt;code&gt;pk&lt;/code&gt; and &lt;code&gt;sk&lt;/code&gt; are the partition and sort keys of the table will now call the &lt;a href=&#34;https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html&#34;&gt;dynamodb.Query&lt;/a&gt; API.
This also works with the &amp;ldquo;begins with&amp;rdquo; operator: &lt;code&gt;pk = &amp;quot;something&amp;quot; and sk ^= &amp;quot;prefix&amp;quot;&lt;/code&gt;.   Since &lt;code&gt;sk&lt;/code&gt; is the sort key, this will be executed as part of a query to DynamoDB.&lt;/p&gt;
&lt;p&gt;This also works if you were to swap the keys around, as in &lt;code&gt;sk = &amp;quot;else&amp;quot; and pk = &amp;quot;something&amp;quot;&lt;/code&gt;.  Surprisingly the &lt;a href=&#34;https://aws.github.io/aws-sdk-go-v2/docs/&#34;&gt;Go SDK&lt;/a&gt; that I&amp;rsquo;m using does not support allow expressions like this: you must have the partition key before the sort key if you&amp;rsquo;re using &lt;a href=&#34;https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression#KeyAnd&#34;&gt;KeyAnd&lt;/a&gt;.  This touches on one of the design goals I have for queries: the user shouldn&amp;rsquo;t need to care how the expression actually produces the result.  If it can do so by running an actual query against the table, then it will; if not, it will do a scan.  Generally, the user shouldn&amp;rsquo;t care either way: just get me the results in the most efficient way you can think of!&lt;/p&gt;
&lt;p&gt;That said, it might be necessary for the user to control this to an extent, such as requiring the use of a scan if the planner would normally go for a query. I may add some control to this in the expression language to support this. But these should be, as a rule, very rarely used.&lt;/p&gt;
&lt;p&gt;Anyway, that&amp;rsquo;s the biggest change that&amp;rsquo;s happening.  There is something else regarding expressions that I&amp;rsquo;m in the process of working on now. I&amp;rsquo;ll touch on that in another blog post.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2022/09/06/finished-version-of.html</link>
      <pubDate>Tue, 06 Sep 2022 13:28:55 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/09/06/finished-version-of.html</guid>
      <description>&lt;p&gt;Finished version 0.0.3 of Audax Toolset yesterday.  The code has been ready since the weekend, but it took me Sunday morning and yesterday (Monday) evening to finish updating the website.  All done now.&lt;/p&gt;
&lt;p&gt;Now the question is whether to continue working on it, or do something different for a change.  There are a few people using Dynamo-Browse at work now, so part of me feels like I should continue building features for it.  But I also feel like switching to another project, at least for a little while.&lt;/p&gt;
&lt;p&gt;I guess we&amp;rsquo;ll let any squeaky wheels make the decision for me.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2022/09/01/ive-been-using.html</link>
      <pubDate>Thu, 01 Sep 2022 13:39:54 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/09/01/ive-been-using.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been using Dynamo-Browse all morning and I think I&amp;rsquo;ll make some notes about how the experience went.  In short: the command line needs some quality of life improvements.  Changing the values of two attributes on two different items, while putting them to the DynamoDB table each time, currently results in too many keystrokes, especially given that I was simply going back and forth between two different values for these attributes.&lt;/p&gt;
&lt;p&gt;So, in no particular order, here is what I think needs to be improved with the Dynamo-Browse command line:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It needs command line completion.  Typing out a full attribute name is &lt;em&gt;so&lt;/em&gt; annoying, especially considering that you need to be a little careful you got the attribute name right, lest you actually add a new one.&lt;/li&gt;
&lt;li&gt;It needs command line history.  Pressing up a few times is so much better than typing out the full command again and again.  Such a history could be something that lives in the workspace, preserving it across restarts.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;set-attr&lt;/code&gt; and &lt;code&gt;del-attr&lt;/code&gt; commands need a switch to take the value directly, rather than by prompting the user to supply it after entering the command (it can still do that, but have an option to take it as a switch as well).  I think having a &lt;code&gt;-set&lt;/code&gt; switch after the attribute names would suffice.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, I think it might be time to consider adding more language features to the command line.  At the moment the commands are just made up of tokens, coming from a split on whitespace characters (while supporting quotes).  But I think it may be necessary to convert this into a proper language grammar, and add some basic control structures to it, such as entering multiple commands in a single line.  It doesn&amp;rsquo;t need to be a super sophisticated language: something similar to the like TCL or shell would be enough at first.&lt;/p&gt;
&lt;p&gt;It might be that writing a script would have solved this problem, and it would (to a degree at least).  But not everything needs to be a script.  I tried writing a script this morning to do the thing I was working on and it felt just so overkill, especially considering how short-lived this script would actually be.  Having something that you can whip up in a few minutes can be a great help.  It would have probably taken me 15-30 minutes to write the script (plus the whole item update thing hasn&amp;rsquo;t been fully implemented yet).&lt;/p&gt;
&lt;p&gt;Anyway, we&amp;rsquo;ll see which of the above improvements I&amp;rsquo;ll get to soon.  I&amp;rsquo;m kinda thinking of putting this project on hold for a little while, so I could work on something different.  But if this becomes too annoying, I may get to one or two of these.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Thinking About Scripting In Dynamo-Browse</title>
      <link>https://lmika.org/2022/08/31/thinking-about-scripting.html</link>
      <pubDate>Wed, 31 Aug 2022 09:31:28 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/08/30/thinking-about-scripting.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been using the scripting facilities of dynamo-browse for a little while now.  So far they&amp;rsquo;ve been working reasonably well, but I think there&amp;rsquo;s room for improvement, especially in how scripts are structured.&lt;/p&gt;
&lt;p&gt;At the moment, scripts look a bit like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const db = require(&amp;quot;audax:dynamo-browse&amp;quot;);
const exec = require(&amp;quot;audax:x/exec&amp;quot;);

db.session.registerCommand(&amp;quot;cust&amp;quot;, () =&amp;gt; {
    db.ui.prompt(&amp;quot;Enter UserID: &amp;quot;).then((userId) =&amp;gt; {
        return exec.system(&amp;quot;/Users/lmika/projects/accounts/lookup-customer-id.sh&amp;quot;, userId);
    }).then((customerId) =&amp;gt; {
        let userId = output.replace(/\s/g, &amp;quot;&amp;quot;);        
        return db.session.query(`pk=&amp;quot;CUSTOMER#${customerId}&amp;quot;`, {
            table: `account-service-dev`
        });
    }).then((custResultSet) =&amp;gt; {
        if (custResultSet.rows.length == 0) {
            db.ui.print(&amp;quot;No such user found&amp;quot;);
            return;            
        }
        db.session.resultSet = custResultSet;
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example script — modified from one that I&amp;rsquo;m actually using — will create a new &lt;code&gt;cust&lt;/code&gt; command which will prompt the user for a user ID, run a shell script to return an associated customer ID for that user ID, run a query for that customer ID in the &amp;ldquo;account-service-dev&amp;rdquo;, and if there are any results, set that as the displayed result set.&lt;/p&gt;
&lt;p&gt;This is all working fine, but there are a few things that I&amp;rsquo;m a little unhappy about.  For example, I don&amp;rsquo;t really like the idea of scripts creating new commands off the session willy-nilly.  Commands feel like they should be associated with the script that are implementing them, and this is sort of done internally but I&amp;rsquo;d like it to be explicit to the script writer as well.&lt;/p&gt;
&lt;p&gt;At the same time, I&amp;rsquo;m not too keen of requiring the script writer to do things like define a manifest.  That would dissuade &amp;ldquo;casual&amp;rdquo; script writers from writing scripts to perform one-off tasks.&lt;/p&gt;
&lt;p&gt;So, I&amp;rsquo;m thinking of adding an &lt;code&gt;plugin&lt;/code&gt; global object, which provides the hooks that the script writer can use to extend dynamo-browse.  This will kinda be like the &lt;code&gt;window&lt;/code&gt; global object in browser-based JavaScript environments.&lt;/p&gt;
&lt;p&gt;Another thing that I&amp;rsquo;d like to do is split out the various services of dynamo-browse into separate pseudo packages.  In the script above, calling &lt;code&gt;require(&amp;quot;audax:dynamo-browse&amp;quot;)&lt;/code&gt; will return a single &lt;code&gt;dynamo-browse&lt;/code&gt; proxy object, which provides access to the various services like running queries or displaying things to the user.  This results in a lot of &lt;code&gt;db.session.this&lt;/code&gt; or &lt;code&gt;db.ui.that&lt;/code&gt;, which is a little unwieldily.  Separating these into different packages will allow the script writer to associate them to different package aliases at the top-level.&lt;/p&gt;
&lt;p&gt;With these changes, the script above will look a little like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const session = require(&amp;quot;audax:dynamo-browse/session&amp;quot;);
const ui = require(&amp;quot;audax:dynamo-browse/ui&amp;quot;);
const exec = require(&amp;quot;audax:x/exec&amp;quot;);

plugin.registerCommand(&amp;quot;cust&amp;quot;, () =&amp;gt; {
    ui.prompt(&amp;quot;Enter UserID: &amp;quot;).then((userId) =&amp;gt; {
        return exec.system(&amp;quot;/Users/lmika/projects/accounts/lookup-customer-id.sh&amp;quot;, userId);
    }).then((customerId) =&amp;gt; {
        let userId = output.replace(/\s/g, &amp;quot;&amp;quot;);        
        return session.query(`pk=&amp;quot;CUSTOMER#${customerId}&amp;quot;`, {
            table: `account-service-dev`
        });
    }).then((custResultSet) =&amp;gt; {
        if (custResultSet.rows.length == 0) {
            ui.print(&amp;quot;No such user found&amp;quot;);
            return;            
        }
        session.resultSet = custResultSet;
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Is this better?  Worse?  I&amp;rsquo;ll give it a try and see how this goes.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Dynamo-Browse: Using The Back-Stack To Go Forward</title>
      <link>https://lmika.org/2022/08/24/dynamobrowse-using-the.html</link>
      <pubDate>Tue, 23 Aug 2022 23:00:11 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/08/23/dynamobrowse-using-the.html</guid>
      <description>&lt;p&gt;More work on Audax toolset.  Reengineered the entire back-stack in dynamo-browse to support going forward as well as backwards, much like going forward in a web-browser.  Previously, when going back, the current &amp;ldquo;view snapshot&amp;rdquo; was being popped off the stack and lost forever (a &amp;ldquo;view snapshot&amp;rdquo; is information about the currently viewed table, including the current query and filter if any are set).  But I&amp;rsquo;d need to maintain these view snapshot if I wanted to add going forward as well.&lt;/p&gt;
&lt;p&gt;So this evening, I changed it this such that view snapshots were no longer being deleted.  Instead, there is now a pointer to the current view snapshot that will go up or down the stack when the user wants to go backwards or forwards.  This involved changing the back-stack from a single-linked list to a doubly-linked list, but that was pretty straightforward.  The logic to pop snapshots from the stack hasn&amp;rsquo;t been removed: in fact, it has been augmented so that it can now remove the entire head up to the current view snapshot.  This happens when the user goes backwards a few times, then runs a new query or sets a new filter, again much like a web-browser where going backwards a few times, then clicking a link, will remove any chance of going forward from that point.&lt;/p&gt;
&lt;p&gt;Last thing I changed this evening was to restore the last viewed table, query or filter when relaunching dyanmo-browse using an existing workspace.  This works quite nicely with the back-stack as it works now: it just needs to read the view snapshot at the head.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Adding Copy-To-Clipboard And Layout Changes In Dynamo-Browse</title>
      <link>https://lmika.org/2022/08/20/adding-copytoclipboard-and.html</link>
      <pubDate>Sat, 20 Aug 2022 11:56:10 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/08/20/adding-copytoclipboard-and.html</guid>
      <description>&lt;p&gt;Spent some more time on dynamo-browse this morning, just a little bit.&lt;/p&gt;
&lt;p&gt;Got one new feature built and merged to main, which is the ability to copy the displayed item to the pasteboard by pressing &lt;kbd&gt;c&lt;/kbd&gt;.  This is directly inspired by a feature in &lt;a href=&#34;https://k9scli.io&#34;&gt;K9S&lt;/a&gt;, which allows you to copy the logs of a running pod to the pasteboard (I use this feature all the time).  The package I&amp;rsquo;m  using to access the pasteboard is &lt;a href=&#34;https://github.com/golang-design/clipboard&#34;&gt;github.com/golang-design/clipboard&lt;/a&gt;, which seems to offer a nice cross-platform approach to doing this.&lt;/p&gt;
&lt;p&gt;The copied item is not meant for machine use.  It&amp;rsquo;s exactly what&amp;rsquo;s displayed in the item viewport and is intended for tracking items that change over time.  What I&amp;rsquo;ve found myself doing, when I need to track such an item, is to manually select it with Terminal and pasting it to an untitled textedit window.  I do this again when the item changes until I&amp;rsquo;m ready to eye the changes manually.  It&amp;rsquo;s not sophisticated, but it tends to work.  And although this feature is not a full-blown item comparer, it will make this use case a little easier.  Plus, it will copy the entire image, not just the lines displayed when a viewed item is too large for the viewport.&lt;/p&gt;
&lt;p&gt;Speaking of which, the other thing I started working on today was on allowing the user to resize the item viewport to take up more of the screen.  At the moment, the displayed item viewport takes up the bottom 14 lines of the screen, and is not resizable.  Naturally, for large items, this is not large enough, and having the ability to resize this viewport would be a good thing to have.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been thinking about the best way to handle this, and I&amp;rsquo;ve decided that an approach similar to how Vim does this — where you need to press a key-chord followed by a number to move the vertical split between two views — would be a little to annoying here.  Too much fine grain control for when you just need the viewport bigger.  So, I&amp;rsquo;m opting for a more course-grain approach, where you simply press &lt;kbd&gt;w&lt;/kbd&gt; and the viewport size will cycle amongst 5 different sizes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;14 rows at the bottom of the screen (the default)&lt;/li&gt;
&lt;li&gt;Half the screen&lt;/li&gt;
&lt;li&gt;All but the top 6 rows of the screen, so you can still see a bit of the table view&lt;/li&gt;
&lt;li&gt;The entire screen (i.e. hiding the table view)&lt;/li&gt;
&lt;li&gt;Completely hidden (i.e. the table view takes up the entire screen).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Doing this will not necessarily steal focus from the table view: even if shown the entire screen, you can still go through each item by pressing &lt;kbd&gt;Up&lt;/kbd&gt; or &lt;kbd&gt;Down&lt;/kbd&gt;.  But you&amp;rsquo;ll be able to see more of the selected item.&lt;/p&gt;
&lt;p&gt;Anyway, that&amp;rsquo;s the theory.  We&amp;rsquo;ll see how well this works in practice.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Adding A Back-Stack To Dynamo-Browse</title>
      <link>https://lmika.org/2022/08/14/adding-a-backstack.html</link>
      <pubDate>Sun, 14 Aug 2022 10:21:47 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/08/14/adding-a-backstack.html</guid>
      <description>&lt;p&gt;Spent some more time working on dynamo-browse, this time adding a back-stack.  This can be used to go back to the previously viewed table, query or filter by pressing the &lt;kbd&gt;Backspace&lt;/kbd&gt; key, kind of like how a browser back button works.&lt;/p&gt;
&lt;p&gt;This is the first feature that makes use of a workspace, which is a concept that I&amp;rsquo;ve been thinking about since the start of the project.  A workspace is basically a file storing various bits of state  that could be recalled in future launches of the tool.  A workspace is always created, even if one is not explicitly specified by the user.  The workspace filename be set by using the &lt;code&gt;-w&lt;/code&gt; switch on launch, or if one is not specified, then a new workspace filename within the temp directory is generated.  The workspace file itself is a &lt;a href=&#34;https://github.com/coreos/bbolt&#34;&gt;Bolt&lt;/a&gt; database, which is a very simple, embeddable key/value store that uses B-Trees and memory-mapped data access.  I&amp;rsquo;m actually using &lt;a href=&#34;https://github.com/asdine/storm&#34;&gt;StormDB&lt;/a&gt; to access this file, since it provides a nice interface for storing and querying Go structs without having to worry about marshalling or unmarshalling.&lt;/p&gt;
&lt;p&gt;The back-stack consist of view-snapshots, which are essentially records of the currently view table, filter, and query.  The view-snapshots are stored as a linked list, and the back-stack simply pushes new snapshots as the view changes (because this is stored on file, the backlinks are actually ID references and not physical pointers).  I had a bit of trouble getting this right at first.  The initial version actually pushed a snapshot of the previous view before it changed (i.e. a new table was selected or the filter was changed).  But this resulted in a bit of messy implementation, with push calls copied and pasted across the various controllers with various special cases, etc.&lt;/p&gt;
&lt;p&gt;So I went back and reimplemented it so that the top of the back-stack is actually a snapshot of the current view.  Pressing the Backspace key will actually pop the current snapshot, then read the head of the stack and use that to update the view.  This means that there will always be one item in the stack, which bothered me a little at first, but that did make the code easier to implement.&lt;/p&gt;
&lt;p&gt;There is another benefit to this arrangement which just came to me while I was writing this.  Since the current state of the view is always at the top of the stack, I could add the ability to restore the last view when launching dynamo-browse with an existing workspace, saving the user from selecting a table and running their last query.  This will come a bit later.  For the meantime, I need to get this back-stack working with user scripts, so that when a script changes the view, the user can still go back.&lt;/p&gt;
&lt;p&gt;Finally, a shoutout to &lt;a href=&#34;https://github.com/spacewander/boltcli&#34;&gt;boltcli&lt;/a&gt; which is a useful CLI tool for browsing a Bolt database.  This tool has come in handy for debugging this feature.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>More On Scripting In Dynamo-Browse</title>
      <link>https://lmika.org/2022/08/10/more-on-scripting.html</link>
      <pubDate>Wed, 10 Aug 2022 14:45:01 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/08/10/more-on-scripting.html</guid>
      <description>&lt;p&gt;Making some more progress on the adding scripting of dynamo-browse.  It helps being motivated to be able to write some scripts for myself, just to be able to make my own working life easier.  So far the following things can be done within user scripts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Define new commands&lt;/li&gt;
&lt;li&gt;Display messages to, and get input from, the user&lt;/li&gt;
&lt;li&gt;Read the attributes of, and set the attributes of, the currently selected row (only string attributes at this stage)&lt;/li&gt;
&lt;li&gt;Run queries and get result sets, which can optionally be displayed to the user&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s a modified example of a script I&amp;rsquo;ve been working on that does most of what is listed above:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const db = require(&amp;quot;audax:dynamo-browse&amp;quot;);

db.session.registerCommand(&amp;quot;cust&amp;quot;, () =&amp;gt; {
    db.ui.prompt(&amp;quot;Enter user ID: &amp;quot;).then((userId) =&amp;gt; {
        return db.session.query(`userId=&amp;quot;${userId}&amp;quot;`);
    }).then((resultSet) =&amp;gt; {
        db.session.currentResultSet = resultSet;
    });
});

db.session.registerCommand(&amp;quot;sub&amp;quot;, () =&amp;gt; {    
    let currentItem = db.session.selectedRow;

    let currentUserId = currentItem.item[&amp;quot;userId&amp;quot;];
    if (!currentUserId) {
        db.ui.print(&amp;quot;No &#39;userId&#39; on current record&amp;quot;);
        return;
    }

    db.session.query(`pk=&amp;quot;ID#${currentUserId}&amp;quot;`, {
        table: &amp;quot;billing-subscriptions&amp;quot;
    }).then((rs) =&amp;gt; {
        db.session.currentResultSet = rs;
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The process of loading this script is pretty basic at the moment.  I&amp;rsquo;ve added a &lt;code&gt;loadscript&lt;/code&gt; command, which reads and executes a particular JavaScript file taken as an argument.  Scripts can be loaded on startup by putting these &lt;code&gt;loadscript&lt;/code&gt; commands (along with any other commands) in a simple RC file that is read on launch.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure how much I like this approach.  I would like to put some structure around how these scripts are written and loaded, so that they&amp;rsquo;re not just running atop of one another.  However, I don&amp;rsquo;t want too much structure in place either.  To me, having the floor for getting a script written and executed too high is a little demotivating, and sort of goes against the ability to automate these one-off tasks you sometimes come across.  That&amp;rsquo;s the downsides I see with IDEs like GoLand and, to a lesser degree, VS Code: they may have vibrant plugin ecosystems, but the minimum amount of effort to automate something is quite high, that it may not be worth the hassle.&lt;/p&gt;
&lt;p&gt;A nice middleground I&amp;rsquo;m considering is to make it to have two levels of automation: one using JavaScript, and the other closer to something like Bash or TCL.  The JavaScript would be akin to plugins, and would have a rich enough API and runtime to support a large number of sophisticated use-cases, with the cost being a fair degree of effort to get something usable.  The Bash or TCL level would have a lower level of sophistication, but it would be quicker to do these one-off tasks that you can bang out and throw away.  This would mean splitting the effort between the scripting API and command line language, which would fall on me to do, so I&amp;rsquo;m not sure if I&amp;rsquo;d go ahead with this (the command line &amp;ldquo;language&amp;rdquo; is little more than a glorified whitespace split at the moment).&lt;/p&gt;
&lt;p&gt;In the meantime, I&amp;rsquo;ll focus on making sure the JavaScript environment is good enough to use.  This will take some time and dog-fooding so they&amp;rsquo;ll be plenty to do.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also started sharing the tool with those that I work with, which is exciting.  I&amp;rsquo;ve yet to get any feedback from them yet, but they did seem quite interested when they saw me using it.  I said this before, but I&amp;rsquo;ll say it again: I&amp;rsquo;m just glad that I put a bit of polish into the first release and got the website up with a link I can share with them.  I guess this is something to remember for next time: what you&amp;rsquo;re working on doesn&amp;rsquo;t need to be complete, but it helps that it&amp;rsquo;s in a state that is easy enough to share with others.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Scripting In Dynamo-Browse</title>
      <link>https://lmika.org/2022/08/03/scripting-in-dynamobrowse.html</link>
      <pubDate>Wed, 03 Aug 2022 21:34:37 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/08/03/scripting-in-dynamobrowse.html</guid>
      <description>&lt;p&gt;Making some progress with adding scripting capabilities to dynamo-browse.  I&amp;rsquo;ve selected a pretty full-featured JS interpreter called &lt;a href=&#34;https://github.com/dop251/goja&#34;&gt;goja&lt;/a&gt;. It has full support for ECMAScript 5.1 and some pretty good coverage of ES6 as well.  It also has an event loop, which means that features such as &lt;code&gt;setTimeout&lt;/code&gt; and promises come out of the box, which is nice.  Best of all, it&amp;rsquo;s written in Go, meaning a single memory space and shared garbage collector.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve started working the extension interface.  I came up with a bit of a draft during the weekend and now just implementing the various bindings.  I don&amp;rsquo;t want to go too fast here.  I&amp;rsquo;m just thinking about all the cautionary tales from those that were responsible for making APIs, and introducing mistakes that they had to live with.  I imagine one way to avoid this is to play with the extension API for as long as it make sense, smoothing out any rough edges when I find them.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s still early days, but here are some of the test scripts I&amp;rsquo;ve been writing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Define a new &amp;quot;hello&amp;quot; command which will ask for your name and
// print it at the bottom.
const dynamobrowse = require(&amp;quot;audax:dynamo-browse&amp;quot;);

dynamobrowse.session.registerCommand(&amp;quot;hello&amp;quot;, () =&amp;gt; {
    dynamobrowse.ui.prompt(&amp;quot;What is your name? &amp;quot;).then((name) =&amp;gt; {
       dynamobrowse.ui.alert(&amp;quot;Hello, &amp;quot; + name);
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;prompt&lt;/code&gt; method here asks the user for a line of text, and will return the user&amp;rsquo;s input.  It acts a bit like the &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt&#34;&gt;prompt&lt;/a&gt; function in the browser, except that it uses promises.  Unfortunately, Goja does not yet support &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; so that means that anonymous functions will need to do for the time being (at least Goja supports arrow functions).&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s another script, which prints out the name of the table, and set the value of the &lt;code&gt;address&lt;/code&gt; field on the first row:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const dynamobrowse = require(&amp;quot;audax:dynamo-browse&amp;quot;);

dynamobrowse.session.registerCommand(&amp;quot;bla&amp;quot;, () =&amp;gt; {
    let rs = dynamobrowse.session.currentResultSet;
    
    let tableName = rs.table.name;
    dynamobrowse.ui.alert(&amp;quot;Current table name = &amp;quot; + tableName);
    
    rs.rows[0].item.address = &amp;quot;123 Fake St.&amp;quot;;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;rows&lt;/code&gt; array, and &lt;code&gt;item&lt;/code&gt; object, makes use of &lt;a href=&#34;https://pkg.go.dev/github.com/dop251/goja#DynamicArray&#34;&gt;DynamicArray&lt;/a&gt; and &lt;a href=&#34;https://pkg.go.dev/github.com/dop251/goja#DynamicObject&#34;&gt;DyanmicObject&lt;/a&gt;, which is a nice feature in Goja in which you do not need to set the elements in the array, or fields on the object explicitly.  Instead, when a field or element is looked up, Goja will effectively call a method on a Go interface with the field name or element index as a parameter.  This is quite handy, particularly as it cuts down on memory copies.&lt;/p&gt;
&lt;p&gt;These scripts work but they do require some improvements.  For example, some of these names could possibly be shortened, just to reduce the amount of typing involved.  They also need a lot of testing: I have no idea if the updated address in the example above will be saved properly.&lt;/p&gt;
&lt;p&gt;Even so, I&amp;rsquo;m quite happy about how quickly this is coming along.  Hopefully soon I&amp;rsquo;ll be in a position to write the test script I&amp;rsquo;m planning for work.&lt;/p&gt;
&lt;p&gt;Also, I got my first beta user.  Someone at work saw me use dynamo-browse today and asked where he could get a copy.  I&amp;rsquo;m just so glad I got the website finished in time.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Release Preparation &amp; Next Steps</title>
      <link>https://lmika.org/2022/07/30/release-preparation-next.html</link>
      <pubDate>Sat, 30 Jul 2022 10:19:56 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/07/30/release-preparation-next.html</guid>
      <description>&lt;p&gt;Finally finished the website for the &lt;a href=&#34;https://audax.tools&#34;&gt;Audax toolset&lt;/a&gt; and cut an initial release (v0.0.2).  I&amp;rsquo;ve also managed to get binary releases for Linux and Windows made. I&amp;rsquo;ve started to work on binary releases for MacOS with the intention of distributing it via Homebrew.  This is completely new to me so I&amp;rsquo;m not sure what progress I&amp;rsquo;ve made so far: probably nothing.  For one thing, I&amp;rsquo;ll need a separate machine to test with, since I&amp;rsquo;ve just been installing them from source on the machine&amp;rsquo;s I&amp;rsquo;ve been using.  Code signing is going to be another thing that will be fun to deal with.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll slip that into a bit of a background task for the moment.  For now, I&amp;rsquo;d like to start work on a feature for &amp;ldquo;dynamo-browse&amp;rdquo; that I&amp;rsquo;ve been thinking of for a while: the ability for users to extend it with scripts.&lt;/p&gt;
&lt;p&gt;Some might argue that it may be a little early for this feature, but I&amp;rsquo;m motivated to do it now as I have a personal need for this, which is navigating amongst a bunch of DynamoDB tables that all store related data.  This is all work related, so can&amp;rsquo;t say too much here.  But if I were just to say that doing this now is a little annoying.  It involves copying and pasting keys, changing tables, and running filters and queries manually: all possible, but very time consuming.  Having the ability to do all this with a single command or keybinding would be so much better.  And since this is all work related, I can&amp;rsquo;t simply modify the source code directly as it will give too much away.  Perfect candidate for using an embedded scripting language.&lt;/p&gt;
&lt;p&gt;So I think that&amp;rsquo;s what I&amp;rsquo;ll start work on next.&lt;/p&gt;
&lt;p&gt;I also need to think about starting work on &amp;ldquo;sqs-browse&amp;rdquo;.  This would be a complete rewrite of what is currently there: which is something that can be used for polling SQS queues and viewing the messages there.  I&amp;rsquo;m hoping for something a little more sophisticated, involving a workspace for writing and pushing messages to queues, along with pulling, saving and routing them from queues.  I haven&amp;rsquo;t got an immediate need for this yet, but I&amp;rsquo;ve come close on occasion so I can&amp;rsquo;t leave this too late.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>A New Name for Audax Tools (nee AWS Tools)</title>
      <link>https://lmika.org/2022/07/24/a-new-name.html</link>
      <pubDate>Sun, 24 Jul 2022 16:08:58 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/07/24/a-new-name.html</guid>
      <description>&lt;p&gt;I think I&amp;rsquo;ve settled on a name for the project I&amp;rsquo;ve been calling &amp;ldquo;awstools&amp;rdquo;.  &amp;ldquo;Settled&amp;rdquo; is probably a good word for it: I came up with it about a week ago, and dismissed it at first as being pretty ordinary&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.  But over that time, it&amp;rsquo;s been slowly growing on me.  Also, I&amp;rsquo;ve yet to come up with any alternatives that are better.&lt;/p&gt;
&lt;p&gt;Anyway, the name that I&amp;rsquo;ve settled on is the &lt;em&gt;Audax Toolset&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not a great name.  Honestly, anyone with more creativity that me could probably come up with a bunch of names better than this.  I&amp;rsquo;m going to play the &lt;a href=&#34;https://seths.blog/2022/07/getting-stuck-on-the-title/&#34;&gt;name-is-the-thing-you-say-to-talk-about-the-thing&lt;/a&gt; here.&lt;/p&gt;
&lt;p&gt;But the name was not completely random.  &amp;ldquo;Audax&amp;rdquo; was chosen from the second part of the Latin name for a wedged-tail eagle: &lt;em&gt;Aquila audax&lt;/em&gt;.  Translated from Latin it means &lt;em&gt;bold&lt;/em&gt;, and although I would definitely not describe this project as in any way &amp;ldquo;bold&amp;rdquo;, it is a TUI based application which, given the limited palette for building UIs, may have some instances of bold font.  But what I&amp;rsquo;m trying to invoke with this name is the sense of an eagle flying in a cloudless sky.  Cloudless → cloudy → the Cloud → AWS (yeah, like I said it&amp;rsquo;s not a great name).&lt;/p&gt;
&lt;p&gt;Anyway, it&amp;rsquo;s good enough for the moment.  And the domain name &lt;a href=&#34;http://audax.tools&#34;&gt;audax.tools&lt;/a&gt; was available, which was a good thing.  Right now, the domain doesn&amp;rsquo;t point to anything but it will eventually point to the website and user guide.&lt;/p&gt;
&lt;p&gt;Speaking of which, the user guide is still slowly coming along.  I got a very rough draft of the actual guide itself finished.  I&amp;rsquo;ll need to proofread it and flesh it out a little, and also add some screenshots.  The reference, which will list all the commands and key bindings (there isn&amp;rsquo;t many of them), needs to be written as well, but that&amp;rsquo;s probably the easiest thing to write.  I will admit that the guide itself makes for pretty dry reading, and I&amp;rsquo;m not entirely convinced it would be enough for someone coming in cold to download up the tools and start using them.  I may also need to write some form of tutorial, or maybe even record a demo video.  Something to consider once I&amp;rsquo;ve got the rest of the site finished, I guess.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;I use the term &amp;ldquo;ordinary&amp;rdquo; here as a euphemism for &amp;ldquo;not very good&amp;rdquo;.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>AWS Tools: Documentation &amp; The Website</title>
      <link>https://lmika.org/2022/07/21/aws-tools-documentation.html</link>
      <pubDate>Thu, 21 Jul 2022 22:23:41 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/07/21/aws-tools-documentation.html</guid>
      <description>&lt;p&gt;Worked a little more on &amp;ldquo;awstools&amp;rdquo; (still haven&amp;rsquo;t thought of a good alternative name for it).  I think the &amp;ldquo;dynamo-browse&amp;rdquo; tool is close to being in a releasable state.  I&amp;rsquo;ve spent the last couple of days trying to clean up most of the inconsistencies, and making sure that it&amp;rsquo;s being packaged correctly.&lt;/p&gt;
&lt;p&gt;Now it&amp;rsquo;s documentation writing time.  I&amp;rsquo;m working my way through a very basic website and user guide.  It&amp;rsquo;s been a little while since I&amp;rsquo;ve written any form of user-level documentation — most of the documents I write have been for other developers I work closely with — and I admit that it feels like a bit of a slog.  It might be the tone of writing that I&amp;rsquo;ve adopted: a little dry and impersonal, trying to walk that fine line between being informative without swamping the reader with big blocks of words.  I might need to work on that: no real reason why the documentation needs to be boring to the reader.&lt;/p&gt;
&lt;p&gt;The website itself will be a statically generated site using Hugo and will most likely be served using GitHub pages.  I&amp;rsquo;ve settled on the &lt;a href=&#34;https://github.com/panr/hugo-theme-terminal&#34;&gt;terminal&lt;/a&gt; theme, since &amp;ldquo;awstools&amp;rdquo; is a suite of terminal-based apps.  That reasoning might be a little corny, but to be honest, I have grown to actually like the theme itself.  I haven&amp;rsquo;t settled on a domain for it yet.&lt;/p&gt;
&lt;p&gt;While working on the documentation, I ran into a &lt;a href=&#34;https://www.toptal.com/designers/htmlarrows/&#34;&gt;useful website&lt;/a&gt; that contains a comprehensive list of HTML entities, complete with previews.  Good reference for the arrows glyphs I need to use to represent key bindings in the document.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>AWS Tools Dev Diary</title>
      <link>https://lmika.org/2022/07/16/aws-tools-dev.html</link>
      <pubDate>Sat, 16 Jul 2022 11:54:42 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/07/16/aws-tools-dev.html</guid>
      <description>&lt;p&gt;A little more work on &amp;ldquo;awstools&amp;rdquo; today, mainly on a bit of a cleanup spree to make them suitable for others to use.  This generally means fixing up any inconsistencies in how the commands work.  An example of this is the &lt;code&gt;put&lt;/code&gt; command, which now writes all modified items that are marked to the table (or if there are no marked items, all modified items) instead of just the selected one.  This brings it closer to how the &lt;code&gt;delete&lt;/code&gt; command works.&lt;/p&gt;
&lt;p&gt;Also merged the &lt;code&gt;set-s&lt;/code&gt; and &lt;code&gt;set-n&lt;/code&gt; commands into a single &lt;code&gt;set-attr&lt;/code&gt; command, which now has the ability to specify an optional attribute type along with the attribute name.  This still only works with the currently selected item, and I think I&amp;rsquo;ll keep it like that for the moment.  I do want something to modify attributes of all marked items (or even all items), but it might be better if that was a separate command, as it may allow for some potentially useful actions like adding a suffix to the value instead of simply changing it.&lt;/p&gt;
&lt;p&gt;Some of these command names are a bit unwieldy, like &lt;code&gt;set-attr&lt;/code&gt;, but I&amp;rsquo;m hopeful to replace many of these with simple keystrokes down the line.  I&amp;rsquo;m trying not to reserve many generic names like &amp;ldquo;set&amp;rdquo; in the off chance of adding something like TCL or similar to make simple scripts (this is in addition to something closer to JavaScript for more feature-full extensions).  Nothing settled here, but trying to keep that option open.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>WWDC Videos In Broadtail</title>
      <link>https://lmika.org/2022/05/16/wwdc-videos-in.html</link>
      <pubDate>Mon, 16 May 2022 20:20:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/05/16/wwdc-videos-in.html</guid>
      <description>&lt;p&gt;Some more work on Broadtail.  This time, I added the ability to use it to download Apple WWDC videos.&lt;/p&gt;
&lt;p&gt;The way it works is based on the existing RSS feed concept.  In order to get the list of videos for a particular WWDC year, you “subscribe” to that by setting up a feed with the new “Apple Developer Videos” type.  The external ID is taken from the URL slug of the web-site that Apple publishes the session videos.  For example, for &lt;a href=&#34;https://developer.apple.com/videos/wwdc2021&#34;&gt;WWDC 2021&lt;/a&gt;, the external ID would be “wwdc2021”.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/d2c645d1f4.jpg&#34; width=&#34;600&#34; height=&#34;451&#34; alt=&#34;&#34; /&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/9cafe29f43.jpg&#34; width=&#34;600&#34; height=&#34;451&#34; alt=&#34;&#34; /&gt;
&lt;p&gt;Downloading the videos is more or less the same.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/805276c746.jpg&#34; width=&#34;600&#34; height=&#34;451&#34; alt=&#34;&#34; /&gt;
&lt;p&gt;There are a few differences between this feed type, and the YouTube RSS feed.  For instance, it only makes use of what is available from the website, which means details like publishing date or duration are not really available.  This is why the “Publishing” date is displayed as “unknown”.  That’s also why the videos are arranged in alphabetical order and the feed itself is not automatically refreshed (although doing so manually by clicking “Refresh” within the feed page will work).  These are actually properties that can now be applied to all feeds of one wishes, although the YouTube feeds are still arranged in reverse chronological order by default.&lt;/p&gt;
&lt;p&gt;From a coding perspective, this involved a lot of refactoring.  I was hoping to move to a more generic feed and video type, but this was the feature that eventually got me to do so.  Thing is that if I wanted to add more feed and video types in the future, it should be easier to do so.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Feed Rules In Broadtail</title>
      <link>https://lmika.org/2022/05/09/feed-rules-in.html</link>
      <pubDate>Mon, 09 May 2022 20:17:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/05/09/feed-rules-in.html</guid>
      <description>&lt;p&gt;Generally, when there’s a video that I’m interesting in watching, I take a look at Broadtail to see if it’s available.  When it is, I go ahead and download it.&lt;/p&gt;
&lt;p&gt;However, some videos take a long time to download — we’re talking 10 hours or so — and they’re usually published when I’m not looking, like during the night when I’m asleep (thank’s time-zones).  So I’d thought it would be nice for Broadtail to kick off the download for me when the video shows up in the feed.&lt;/p&gt;
&lt;p&gt;So I’ve added Feed Rules to do this.&lt;/p&gt;
&lt;p&gt;Feed rules are very simple automations that happen when new items are found in during the RSS feed poll.  When the video shows up in the feed, and matches the rule condition, Broadtail will perform the rule action for that video.&lt;/p&gt;
&lt;p&gt;Feed Rules are added as a new sub-section in “Settings”, which itself is a new top-level section of the app (the “General” sub-section is empty at this stage).&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/1ce2e30fc0.jpg&#34; width=&#34;600&#34; height=&#34;462&#34; alt=&#34;&#34; /&gt;
&lt;p&gt;Feed Rules consist of a name, whether the rule is active, a set of conditions, and a set of action.  A feed item will need to match all the conditions of the rule in order for the actions to be performed.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/24081e093b.jpg&#34; width=&#34;600&#34; height=&#34;463&#34; alt=&#34;&#34; /&gt;
&lt;p&gt;The conditions of a feed rule touch upon the following properties of a feed item:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The feed in which it appears in. This can be set to “any” to apply the rule to all feed items.&lt;/li&gt;
&lt;li&gt;Whether the title matches a given string. The match rules are similar to the searches in the feed item list views, which are appearance of each of the space separated tokens somewhere in the title (in any case) with phrases appearing as quoted strings.&lt;/li&gt;
&lt;li&gt;Whether the description matches a given string.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a feed item matches all the conditions, Broadtail can perform the following actions for the feed item:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start a download of the video&lt;/li&gt;
&lt;li&gt;Mark the feed item as a favourite&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There might be more conditions or actions added in the future.  So far this seems to be the bare minimum to make the feature usable.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2022/04/04/doing-a-small.html</link>
      <pubDate>Mon, 04 Apr 2022 20:12:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/04/04/doing-a-small.html</guid>
      <description>&lt;p&gt;Doing a small weekend/week-long project at the moment to track favourite moments in a few podcasts I&amp;rsquo;m listening to.  This is something that I&amp;rsquo;ve been thinking about for a while, and I&amp;rsquo;m not entirely sure what compelled me to actually start work on it.  Probably because the system I&amp;rsquo;ve been using so far — a set of timestamped Pocketcast links managed in Pinboard — has been growing quite recently and much of the limitations involved, such as the list being unordered and no skip back 30 seconds available on playback, is start to annoy me.  It&amp;rsquo;s also a chance for a bit of novelty, at least for a few days or so.&lt;/p&gt;
&lt;p&gt;It took roughly a day or so to get a small Buffalo web-app up and running  which does most of what I want.  It just needs some styling and a better way to play the episodes, which is what I&amp;rsquo;m working on now.  I really don&amp;rsquo;t want to spend more than a week working on this — last thing I need are more projects.  But a good thing about this one is that I think the scope is naturally quite small, so no real risk of it blowing out to become too large.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2022/04/01/two-new-awstool.html</link>
      <pubDate>Fri, 01 Apr 2022 20:11:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/04/01/two-new-awstool.html</guid>
      <description>&lt;p&gt;Two new awstool commands: one for browsing SSM parameters and one for simply viewing JSON log files.  The SSM parameter one was especially handy, as I was dealing with parameter subtrees a lot and doing that in the AWS web console is always a pain.  As for the JSON log viewer; well, let&amp;rsquo;s just say there were one too many log files from Kubernete pods I needed to look at this week.&lt;/p&gt;
&lt;p&gt;The pattern for working with state seems to be working.  I may need to be a little careful that the state management doesn&amp;rsquo;t get too unwieldily as I add features and more things that need to be tracked.  But at the moment, it seems to be manageable.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2022/03/29/ive-been-racking.html</link>
      <pubDate>Tue, 29 Mar 2022 20:11:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/03/29/ive-been-racking.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been racking my brain trying to best work out how to organise the code for awstools.  My goals are to make it possible to have view models composable, have state centralised but also localised, and keep controllers from having too much responsibility.  I started another tool, which browses SSM parameters, to try and work this all out.&lt;/p&gt;
&lt;p&gt;I think I&amp;rsquo;ve settled on the following architecture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Providers and Services will remain stateless&lt;/li&gt;
&lt;li&gt;State will be managed by controllers&lt;/li&gt;
&lt;li&gt;Operations in controllers are only available through tea.Cmd implementations.&lt;/li&gt;
&lt;li&gt;Updates from controllers will only be available through tea.Msg implementations.&lt;/li&gt;
&lt;li&gt;View models (i.e. &lt;code&gt;tea.Model&lt;/code&gt;) will only know enough state to be able to render themselves.&lt;/li&gt;
&lt;li&gt;There will be one master model which will coordinate the communication between controllers and view models. This model will react to messages from the controllers and update the views. It will also react to messages from the views and launch operations on the controllers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&amp;rsquo;ll see how this goes and whether it will scale as additional features are added.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2022/03/27/more-work-on.html</link>
      <pubDate>Sun, 27 Mar 2022 20:11:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/03/27/more-work-on.html</guid>
      <description>&lt;p&gt;More work on the AWS tools, mainly rebuilding the UI framework.  Need to rip out all the Operation type stuff, as BubbleTea already does this using messages and commands (see tutorial 2).  Also taking some time building some UI models that I can reuse across the various commands, including a few that deal with layout changes.&lt;/p&gt;
&lt;p&gt;Also tracked down what was causing that delay when trying to create a new list.  It turns out during the call to &lt;code&gt;list.New()&lt;/code&gt;, a bunch of adaptive styles are created, which includes a test to see if the terminal is in light or dark mode. This calls some terminal IO methods which were blocking for a significant amount of time, we’re talking in the order of 10’s of seconds.&lt;/p&gt;
&lt;p&gt;The good thing is that this check is only made once, so what I did was move the check into the main function.  Some preliminary tests indicating that this may work: the lists are consistently being created very quickly again.  We’ll see if this lasts.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2022/03/26/more-work-on.html</link>
      <pubDate>Sat, 26 Mar 2022 20:10:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/03/26/more-work-on.html</guid>
      <description>&lt;p&gt;More work on Broadtail this morning.  Managed to get the new favourite stuff finished.  Favourites are now a dedicated entity, instead of being tied to a feed item.  This means that any video can now be favourited, including ones not from a feed.  The favourite list is now a top-level menu item as well.&lt;/p&gt;
&lt;p&gt;Also found a &lt;a href=&#34;https://github.com/spacewander/boltcli&#34;&gt;useful CLI tool&lt;/a&gt; for browsing BoltDB files.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>New AWS Tools Commands</title>
      <link>https://lmika.org/2022/03/25/new-aws-tools.html</link>
      <pubDate>Fri, 25 Mar 2022 20:05:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/03/25/new-aws-tools.html</guid>
      <description>&lt;p&gt;For a while now, I’ve been wanting some tools which would help manage AWS resources that would also run in the terminal.  I know in most circumstances the AWS console would work, but I know for myself, there’s a lot of benefit from doing this sort of administration from the command line.&lt;/p&gt;
&lt;p&gt;I use the terminal a lot when I’m developing or investigating something.  Much of the time while I’m in the weeds I’ve got a bunch of tabs with tools running and producing output, and I’m switching between them as I try to get something working across a bunch of systems.&lt;/p&gt;
&lt;p&gt;This is in addition to cases when I need to manage an AWS mock running on the local machine.  The AWS console will not work then.&lt;/p&gt;
&lt;p&gt;At the start of the week, I was thinking of at least the following three tools that I would like to see exist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A TUI browser/workspace for DynamoDB tables&lt;/li&gt;
&lt;li&gt;A TUI workspace for monitoring SQS queues&lt;/li&gt;
&lt;li&gt;Some tool for reading JSON style log files.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As of yesterday, I actually got around to building the first two.&lt;/p&gt;
&lt;p&gt;The first is a tool for browsing DynamoDB tables, which I’m calling &lt;code&gt;dynamo-browse&lt;/code&gt; (yes, the names are not great).  This tool does a scan of a DynamoDB table, and shows the resulting items in a table.  Each item can be inspected in full in the lower half of the window by moving the selection.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/91ca4f329a.jpg&#34; width=&#34;600&#34; height=&#34;430&#34; alt=&#34;Dynamo-Browse&#34; /&gt;
&lt;p&gt;At the moment this tool only does a simple scan, and some very lightweight editing of items (duplicate and delete).  But already it’s proven useful with the task I was working on, especially when I came to viewing the contents of test DynamoDB instances running on the local machine.&lt;/p&gt;
&lt;p&gt;The second tool is used for monitoring SQS queues.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/f13d3900a2.jpg&#34; width=&#34;600&#34; height=&#34;430&#34; alt=&#34;SQS-Browse&#34; /&gt;
&lt;p&gt;This will poll for SQS messages from a queue and display them in the table here.  The message itself will be pretty printed in the lower half of the screen.  Messages can also be pushed to another queue.  That’s pretty much it so far.&lt;/p&gt;
&lt;p&gt;There are a bunch of things I’d like to do in these tools.  Here’s a list of them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Queries in DynamoDB:&lt;/strong&gt; There’s currently no way to run queries or filter the resulting items. I’m hoping to design a small query language to do this. I’m already using Participal to power a very simple expression language to duplicate items, so as long as I can design something that is expressive enough and knows how to use particular indices, I think this should work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Putting brand new items in DynamoDB:&lt;/strong&gt; At the moment you can create new items based on existing items in a way, but there’s currently no way to create brand new items or adjust the attributes of existing items. For this, I’d like to see an “edit item” mode, where you can select the attribute and edit the value, change the type, add or remove attributes, etc. This would require some UI rework, which is already a bit tentative at this stage (it was sort of rushed together, and although some architectural changes have been made to the M and the C in MVC, work on the V is still outstanding).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preview item changes before putting them to DynamoDB:&lt;/strong&gt; This sort of extends the point above, where you see the diff between the old item and new item before it’s put into DynamoDB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workspaces in SQS Browse:&lt;/strong&gt; One idea I have for SQS browse is the notion of “workspace”. This is a persistent storage area located on the disk where all the messages from the queue would be saved to. Because SQS browse is currently pulling messages from the SQS queue, I don’t want the user to get into a state where they’ve lost their messages for good. The idea is that a workspace is always created when SQS browse is launch. The user can choose the workspace file explicitly, but if they don’t, then the workspace will be created in the temp directory. Also implicit in this is support for opening existing workspaces to continue work in them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multiple queues in SQS Browse:&lt;/strong&gt; Something that I’d like to see in SQS Browse is the ability to deal with multiple queues. Say you’re pulling from one queue and you’d like to push it to another. You can use a command to add a queue to the workspace. Then, you can do things like poll the queue, push messages in the workspace to the queue, monitor it’s queue length, etc. Queues would be addressable by number or some other way, so you can simply run the command push 2 to push the current message to queue 2.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As for when I’ll actively work on these tools.  It will probably be when I need to use them.  But in the short term, I’m glad I got the opportunity to start working on them.  They’ve already proven quite useful to me.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2022/03/22/even-more-work.html</link>
      <pubDate>Tue, 22 Mar 2022 20:00:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/03/22/even-more-work.html</guid>
      <description>&lt;p&gt;Even more work on Feed Journaler.  Still trying to tune the title removal logic.  Will probably require a lot of testing.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Broadtail 0.0.7</title>
      <link>https://lmika.org/2022/03/15/broadtail.html</link>
      <pubDate>Tue, 15 Mar 2022 19:54:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/03/15/broadtail.html</guid>
      <description>&lt;p&gt;Released Broadtail 0.0.7 about a week ago.  This included some restyling of the job list on the home page, which now includes a progress bar updated using web-sockets (no need for page refreshes anymore).&lt;/p&gt;
&lt;p&gt;For the frontend, the Websocket APIs that come from the browser are used.  There’s not much to it — it’s managed by a &lt;a href=&#34;https://stimulus.hotwired.dev/&#34;&gt;Stimulus&lt;/a&gt; controller which sets up the websocket and listen for updates.  The updates are then pushed as custom events to the main &lt;code&gt;window&lt;/code&gt;, which the Stimulus controllers used to update the progress bar are listening out for.  This allows for a single Stimulus controller to manage the websocket connection and make use of the &lt;code&gt;window&lt;/code&gt; as a message bus.&lt;/p&gt;
&lt;p&gt;Working out the layers of the progress bar took me a bit of time, as I wanted to make sure the text in the progress bar itself was readable as the progress bar filled.  I settled in a HTML tree that looked like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&amp;quot;progressbar&amp;quot;&amp;gt;
  &amp;lt;!-- The filled in layer, at z-index: 10 --&amp;gt;
  &amp;lt;div class=&amp;quot;complete&amp;quot;&amp;gt;
    &amp;lt;span class=&amp;quot;label&amp;quot;&amp;gt;45% complete&amp;lt;/span&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;!-- The unfilled layer --&amp;gt;
  &amp;lt;span class=&amp;quot;label&amp;quot;&amp;gt;45% complete&amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, there’s a base layer and a filled in layer that overlaps it.  Both of these layers have a progress label that contain the same status message.  As the .complete layer fills in, it will hide the unfilled layer and label.  The various CSS properties used to get this effect can be found &lt;a href=&#34;https://github.com/lmika/broadtail/blob/main/assets/css/progress.css&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The backend was a little easier.  There is a &lt;a href=&#34;https://github.com/gorilla/websocket&#34;&gt;nice websocket library&lt;/a&gt; for Go which handles the connection upgrades and provides a nice API for posting JSON messages.  Once the upgrade is complete, a goroutine servicing the connection will just start listening to status updates from the jobs manager and forward these messages as JSON text messages.&lt;/p&gt;
&lt;p&gt;Although this works, it’s not perfect.  One small issue is that updates will not reconnect if there is an error.  I imagine that it’s just a matter of listening out for the relevant events and retrying, but I’ll need to learn more about how this actually works.  Another thing is that the styling of the progress bar relies of fixed widths.  If I get around to reskinning the style of the entire application, that might be the time to address this.&lt;/p&gt;
&lt;p&gt;The second thing this release has is a simple integration with Plex.  If this integration is configured, Broadtail will now send a request to Plex to rescan the library for new files, meaning that there’s no real need to wait for the schedule rescan to occur before the videos are available in the app.  This simply uses Plex’s API, but it needs the Plex token, which can be &lt;a href=&#34;https://www.plexopedia.com/plex-media-server/general/plex-token/&#34;&gt;found using this method&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Anyway, that’s it for this version.  I’m working on re-engineering how favourites work for the next release.  Since this is still in early development, I won’t be putting in any logic to migrate the existing favourites, so just be weary that you may loose that data.  If that’s going to be a problem, feel free to let me know.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Some More Updates of Broadtail</title>
      <link>https://lmika.org/2022/03/05/some-more-updates.html</link>
      <pubDate>Sat, 05 Mar 2022 19:49:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/03/05/some-more-updates.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve made some more changes to Broadtail over the last couple of weeks.&lt;/p&gt;
&lt;p&gt;The home page now shows a list of recently published videos below the currently running jobs.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/a191cab735.jpg&#34; width=&#34;600&#34; height=&#34;460&#34; alt=&#34;&#34; /&gt;
&lt;p&gt;Clicking through to “Show All” displays all the published videos.  A simple filter can be applied to filter them down to videos with titles containing the keywords (note: nothing fancy with the filter, just tokenisation and an OR query).&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/21c680453b.jpg&#34; width=&#34;600&#34; height=&#34;460&#34; alt=&#34;&#34; /&gt;
&lt;p&gt;Finally, items can now be favourited.  This can be used to select videos that you may want to download in the future.  I personally use this to keep the list of “new videos” in the Plex server these videos go to to a minimum.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/6fa1a51a80.jpg&#34; width=&#34;600&#34; height=&#34;453&#34; alt=&#34;&#34; /&gt;
</description>
    </item>
    
    <item>
      <title>PGBC Scoring Rules</title>
      <link>https://lmika.org/2022/01/13/pgbc-scoring-rules.html</link>
      <pubDate>Thu, 13 Jan 2022 22:55:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2022/01/13/pgbc-scoring-rules.html</guid>
      <description>&lt;p&gt;I get a bit of a thrill when there&amp;rsquo;s a need to design a mini-language.  I have one facing me now for a little project I&amp;rsquo;m responsible for, which is maintaining a scoring site for a bocce comp I&amp;rsquo;m involve in with friends.&lt;/p&gt;
&lt;p&gt;How scoring works now is that the winner of a particular bocce match gets one point for the season.  The winner for the season is the person with the most points.  However, we recently discuss the idea of adding &amp;ldquo;final matches,&amp;rdquo; which will give the match winner 7 points, the runner up 2 points, and the person who came in third 1 point.  At the same time I want to add the notion of &amp;ldquo;friendly matches&amp;rdquo; which won&amp;rsquo;t count to the season score.&lt;/p&gt;
&lt;p&gt;It might have been that a simple solution was to encode these rules directly in the app, and have a flag indicating whether a match was normal, final or friendly.  But this was suboptimal as there is another variant of the game we play which do not have the notion of finals, and if we did, we may eventually have different rule for it.  So I opted for a design in which a new &amp;ldquo;match type&amp;rdquo; is added as a new database entity, which will have the scoring rules encoded as a &lt;a href=&#34;https://www.postgresql.org/docs/9.3/datatype-json.html&#34;&gt;PostgreSQL JSON column type&lt;/a&gt;.  Using this as a mechanism of encoding free(ish) structured data when there&amp;rsquo;s no need to query it has worked for me in the past.  There was no need to add the notion of seasons points as it was already present as an easy way to keep track of wins for a season.&lt;/p&gt;
&lt;p&gt;For the scoring rules JSON structure, I&amp;rsquo;m considering the use of an array of conditions.  When a player meets the conditions of a particular array element, they will be awarded the points associated with that condition.  Each player will only be permitted to match one condition, and if they don&amp;rsquo;t match any, they won&amp;rsquo;t get any points.  The fields of the condition that a player can be matched to can be made up of the following attributes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;rank:&lt;/strong&gt; (int) the position the player has in the match just played in accordance with the scoring, with &lt;code&gt;1&lt;/code&gt; being the player with the highest score, &lt;code&gt;2&lt;/code&gt; being the player with the second highest score, and so on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;winner:&lt;/strong&gt; (bool) whether the player is considered the winner of the match.  The person with the highest score usually is, but this is treated as an independent field and so it should be possible to define rules accordingly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;draw:&lt;/strong&gt; (bool) whether the player shares their rank with another player.  When a draw occurs, both winning players will have a rank of &lt;code&gt;1&lt;/code&gt;, with the player of the second highest score having a rank of &lt;code&gt;2&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using this structure, a possible scoring rules definition for a normal match may look like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ &amp;quot;season_score&amp;quot;: [
  { &amp;quot;condition&amp;quot;: { &amp;quot;winner&amp;quot;: true }, &amp;quot;points&amp;quot;: 1 }
]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;whereas a rules definition for the final match may look like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ &amp;quot;season_score&amp;quot;: [
  { &amp;quot;condition&amp;quot;: { &amp;quot;rank&amp;quot;: 1 }, &amp;quot;points&amp;quot;: 7 },
  { &amp;quot;condition&amp;quot;: { &amp;quot;rank&amp;quot;: 2 }, &amp;quot;points&amp;quot;: 2 },
  { &amp;quot;condition&amp;quot;: { &amp;quot;rank&amp;quot;: 3 }, &amp;quot;points&amp;quot;: 1 }
}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, for friendlies, the rules can simply look like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ &amp;quot;season_score&amp;quot;: [] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I think this provides a great deal of flexibility and extensibility without making the rules definition too complicated.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Alto Catalogue Update</title>
      <link>https://lmika.org/2021/11/13/alto-catalogue-update.html</link>
      <pubDate>Sat, 13 Nov 2021 21:55:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2021/11/13/alto-catalogue-update.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve really tied myself up in knots here.  I&amp;rsquo;m spending some time working on Alto Catalogue, trying to streamline the process of uploading individual tracks into a new album.  This is a workflow that is absolutely not user friendly at the moment, and the only way I&amp;rsquo;ve gotten tracks into the catalogue is to run a hacked-together tool to upload the tracks from the command line.  The reason why I&amp;rsquo;m addressing this now is that it&amp;rsquo;s slightly embarrassing to have this open-source project without having a nice way of doing something that, by all accounts, is quite fundamental (a good hint for when you&amp;rsquo;re facing this is when it comes time to write the end-user documentation: if you can&amp;rsquo;t explain how to do something in a way that doesn&amp;rsquo;t include the word &amp;ldquo;hack&amp;rdquo;, &amp;ldquo;complicated&amp;rdquo;, or &amp;ldquo;unsupported&amp;rdquo;, then something is missing).&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;m trying to close this feature gap, but it&amp;rsquo;s proving to be more complicated than I expected.  The main issue relates ID3 tags and how media is arrange in the repository.  Previous versions of the catalogue actually did have a way of uploading track media to the repository, which is essentially an S3 bucket.  The way this work is that the catalogue will issue the browser a pre-signed Put URL, and the browser could upload the track media directly to S3.  But in order to get a pre-signed URL, you need to know the object key, which is a bit like a file path.  The old upload flow had the user enter the object key manually in the upload form.&lt;/p&gt;
&lt;p&gt;This worked but I had some real issues with it.  The first is that I&amp;rsquo;d like the objects within the S3 bucket to be organised in a nice way, for example &amp;ldquo;artist/album/tracknum-title.mp3&amp;rdquo;.  I&amp;rsquo;m hoping that this S3 bucket will be my definitive music collection and I don&amp;rsquo;t want just some random IDs that are completely indecipherable when I browse the objects in the S3 bucket.  That way, if I were ever to shutdown the catalogue down or loose all the metadata, I&amp;rsquo;d still be able to navigate my collection via the object keys alone.&lt;/p&gt;
&lt;p&gt;The second was that this approach did not take into account the track metadata.  Track metadata is managed in a PostgreSQL database and had to be entered in manually; yes, this included the track duration.  The only reason I used the hacked together tool to upload tracks was that it was a tool I was already using to set ID3 tags on MP3 files, and it was trivial to add a HTTP client to do the upload from there.  Obviously, asking users to run a separate tool to do their track uploads is not going to fly.&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;m hoping to improve this.  The ideal flow would be that the user will simply select an MP3 from their file system.  When they click upload, the following things will happen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The ID3 tags of the MP3 will be read.&lt;/li&gt;
&lt;li&gt;That metadata will be used to determine the location of the object in S3.&lt;/li&gt;
&lt;li&gt;A pre-signed URL will be generated and sent to the browser to upload the file.&lt;/li&gt;
&lt;li&gt;The file is uploaded to S3.&lt;/li&gt;
&lt;li&gt;A new track record is created with the same metadata.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The libraries I&amp;rsquo;m using to read the &lt;a href=&#34;https://github.com/bogem/id3v2&#34;&gt;ID3 tags&lt;/a&gt; and &lt;a href=&#34;https://github.com/kgiannakakis/mp3duration&#34;&gt;track duration&lt;/a&gt; requires the track media to be available as a file on the local file system (I assume this is for random access).  Simply uploading the track media to the local file system would be the easiest approach, since it would allow me to read the metadata, upload the media to the repository on the backend, and setup the track metadata all in a single transaction.  But I have some reservations about allowing large uploads to the server, and most of the existing infrastructure already makes use of pre-signed URLs.  So the first run at this feature involved uploading the file to S3 and then downloading it on the server backend to read the metadata.&lt;/p&gt;
&lt;p&gt;But you see the problem here: in order to generate a pre-signed URL to upload the object to S3, I need to know the location of the media, which I want to derive from the track metadata.  So if I don&amp;rsquo;t want uploads to go straight to the file system, I need the object to already be in S3 in order to work out the best location of where to put the object in S3.&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;m wondering what the best ways to fix this would be.  My current thing is this series of events:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a pre-signed URL to a temporary location in the S3 bucket.&lt;/li&gt;
&lt;li&gt;Allow the user to Upload the media directly to that location in the S3 bucket.&lt;/li&gt;
&lt;li&gt;On the server, download that media object to get the metadata and duration.&lt;/li&gt;
&lt;li&gt;From that, derive the objects location and move the object within S3, something I&amp;rsquo;m guessing should be relatively easy if the objects are in the same bucket.&lt;/li&gt;
&lt;li&gt;Create a new track record from the metadata.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The alternative is biting the bullet and allowing track uploads directly to the file system.  That will simplify the crazy workflow above but means that I&amp;rsquo;ll need to configure the server for large uploads.  This is not entirely without precedence though: there is a feature for uploading tracks in a zip file downloaded from a URL which uses the local file system.  So there&amp;rsquo;s not a whole lot stopping me from doing this altogether.&lt;/p&gt;
&lt;p&gt;The third approach might be looking for a JavaScript library to read the ID3 tags.  This is not great as I&amp;rsquo;d need to get the location from the server anyway, as the metadata-derive object location is configured on a per repository basis.  It also means I&amp;rsquo;ll be mixing up different ways to get metadata.&lt;/p&gt;
&lt;p&gt;In any case, not a great set of options here.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Feeds In Broadtail</title>
      <link>https://lmika.org/2021/11/06/feeds-in-broadtail.html</link>
      <pubDate>Sat, 06 Nov 2021 20:32:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2021/11/06/feeds-in-broadtail.html</guid>
      <description>&lt;p&gt;My quest to watch YouTube without using YouTube got a little closer recently with the addition of feeds in Broadtail.  This uses the &lt;a href=&#34;https://mjtsai.com/blog/2020/01/16/youtube-rss-feeds/&#34;&gt;YouTube RSS feed endpoint&lt;/a&gt; to list videos recently added to a channel or playlist.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/1e7e3beeb5.jpg&#34; width=&#34;600&#34; height=&#34;409&#34; alt=&#34;Feed listing, in all it&#39;s 90&#39;s web style glory.&#34; /&gt;
&lt;p&gt;There are a bunch of channels that I watch regularly but I&amp;rsquo;m very hesitant to subscribe to them within YouTube itself (sorry YouTubers, but I choose not to smash that bell icon).  I&amp;rsquo;m generally quite hesitant to give any signal to YouTube about my watching habits, feeding their machine learning models even more information about myself.  But I do want to know when new videos are available, so that I can get them into Plex once they&amp;rsquo;re released.  There is where feeds come in handy.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/1bfefa3950.jpg&#34; width=&#34;600&#34; height=&#34;496&#34; alt=&#34;Recent videos of a feed.&#34; /&gt;
&lt;p&gt;Also improved is the display of video metadata when selecting a feed item or entering a video ID in the quick look bar.  Previously this would immediately start a download of the video, but I prefer knowing more about the video first.  These downloads aren&amp;rsquo;t free, and they usually take many hours to get.  Better to know more about them before committing to it.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/40ca90a92c.jpg&#34; width=&#34;600&#34; height=&#34;569&#34; alt=&#34;Video details page.&#34; /&gt;
&lt;p&gt;Incidentally, I think this mode of watching has a slight benefit.  There are days when I spend the whole evening binging YouTube, not so much following the algorithm but looking at the various channels I&amp;rsquo;m interested in for videos that I haven&amp;rsquo;t seen yet.  Waiting several hours for a video download feels a little more measured, and less likely to send me down the YouTube rabbit hole.  I&amp;rsquo;m sure there will still be evenings when I do nothing else other than watch TV, but hopefully that&amp;rsquo;s more of a choice rather than an accident.&lt;/p&gt;
&lt;p&gt;I think this is enough on Broadtail for the time being.  It&amp;rsquo;s more or less functional for what I want to do with it.  Time to move onto something else.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Some Screenshots Of Broadtail</title>
      <link>https://lmika.org/2021/10/31/some-screenshots-of.html</link>
      <pubDate>Sun, 31 Oct 2021 12:09:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2021/10/31/some-screenshots-of.html</guid>
      <description>&lt;p&gt;I spent some time this morning doing some styling work on Broadtail, my silly little YouTube video download manager I&amp;rsquo;m working on.&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/1fd10c6799.jpg&#34; width=&#34;600&#34; height=&#34;455&#34; alt=&#34;&#34; /&gt;
&lt;p&gt;Now, I think it&amp;rsquo;s fair to say that I&amp;rsquo;m not a designer.  And these designs look a little dated, but, surprisingly, this is sort of the design I&amp;rsquo;m going for: centered pages, borders, etc.  A bit of a retro, tasteless style that may be ugly, but still usable(-ish).&lt;/p&gt;
&lt;img src=&#34;https://cdn.uploads.micro.blog/25293/2023/e0eb3d3bd3.jpg&#34; width=&#34;600&#34; height=&#34;455&#34; alt=&#34;&#34; /&gt;
&lt;p&gt;It&amp;rsquo;s not quite finished — the colours need a bit of work — but it&amp;rsquo;s sort of the style I have in my head.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://lmika.org/2021/10/23/more-work-on.html</link>
      <pubDate>Sat, 23 Oct 2021 19:24:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2021/10/23/more-work-on.html</guid>
      <description>&lt;p&gt;More work on the project I mentioned yesterday, codenamed Broadtail.  Most of the work was around the management of download jobs.  I&amp;rsquo;m using a job management library I&amp;rsquo;ve built for another project and integrated it here so that the video downloads could be observable from the web frontend.  The library works quite well, but at the moment, the jobs are not kept on any sort of disk storage.  They are kept in memory until they&amp;rsquo;re manually cleared, but I&amp;rsquo;m hoping to only keep the active jobs in memory, and store historical jobs onto disk.  So most of today&amp;rsquo;s session was spent on making that possible, along with some screens to list and view job details.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Start of Yet Another Project Because I Can&#39;t Help Myself</title>
      <link>https://lmika.org/2021/10/22/start-of-yet.html</link>
      <pubDate>Fri, 22 Oct 2021 19:16:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2021/10/22/start-of-yet.html</guid>
      <description>&lt;p&gt;One of the reasons why I stopped work on Lorikeet was that I was inspired by those on Micro.blog to setup a Plex server for my YouTube watching needs.  A few years ago, I actually bought an old Intel Nuc for that reason, but I never got around to setting it up.  I managed to do so last Wednesday and so far it&amp;rsquo;s working pretty well.&lt;/p&gt;
&lt;p&gt;The next thing I&amp;rsquo;d like to do is setup RSS subscriptions for certain YouTube channels and automatically download the videos when they are publish.  I plan to use &amp;ldquo;youtube-dl&amp;rdquo; for the actual video downloading part, but I&amp;rsquo;m hoping to build something that would poll the RSS feeds and trigger the download when new videos are published.  I&amp;rsquo;m hoping that this service would have a web-based frontend so I don&amp;rsquo;t have to login via SSH to monitor progress, etc.&lt;/p&gt;
&lt;p&gt;The download&amp;rsquo;s would need to be automatic as the requests made by &lt;code&gt;youtube-dl&lt;/code&gt; seem to be throttled by YouTube and a longish video may take several hours to download.  If this was a manual process, assuming that I would actually remember to start the download myself, the video won&amp;rsquo;t be ready for my evening viewing.  I&amp;rsquo;m hoping that my timezone would work to my advantage here.  The evenings on the US East Coast are my mornings, so if a video download starts at the beginning of the day, hopefully it would be finish when my evening rolls around.  I guess we&amp;rsquo;ll see.&lt;/p&gt;
&lt;p&gt;Anyway, that&amp;rsquo;s what my current coding project will be on: something that would setup RSS subscriptions for YouTube channels, and download new videos when they are published.&lt;/p&gt;
&lt;p&gt;This is probably one of those things that already exist out there.  That may be true, but there are certain things that I&amp;rsquo;m hoping to add down the line.  One such thing might be adding the notion of an &amp;ldquo;interest level&amp;rdquo; to channels which would govern how long a video would be kept around.  For example, a channel that is marked as very interested would have every video downloaded and stored into Plex straight away.  Mildly interested channels would have videos download but kept in a holding place until I choose to watch it, in which case it would be moved to Plex.  If that doesn&amp;rsquo;t happen in 7 days or so, the videos would be removed.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d like to also add some video lifecycle management into the mix as well, just to avoid the disk being completely used up.  I can see instances where I&amp;rsquo;d like to mark videos as &amp;ldquo;keep for ever&amp;rdquo; and all the others will churn away after 14 days or so.  It might be worth checking out what Plex offers for this, just to avoid doubling up on effort.&lt;/p&gt;
&lt;p&gt;But that&amp;rsquo;s all for the future.  For the moment, my immediate goal is to get the basics working.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Abandoning Project Lorikeet</title>
      <link>https://lmika.org/2021/10/18/abandoning-project-lorikeet.html</link>
      <pubDate>Mon, 18 Oct 2021 19:58:00 +1000</pubDate>
      
      <guid>http://lmika.micro.blog/2021/10/18/abandoning-project-lorikeet.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;ll admit it: the mini-project that I have been working on may not have been a good idea.&lt;/p&gt;
&lt;p&gt;The project, which I gave the codename Lorikeet, was to provide a way to stream YouTube videos to a Chromecast without using the YouTube app.  Using the YouTube app is becoming a real pain.  Ads aside, they&amp;rsquo;ve completely replaced the Chromecast experience from a very basic viewing destination to something akin to a Google TV, complete with recommendations of &amp;ldquo;Breaking News&amp;rdquo; from news services that I have no interest in seeing.&lt;/p&gt;
&lt;p&gt;So I spent some time trying to build something to avoid the YouTube app completely, using a mixture of youtube-dl, a Buffalo web-app, and a Flutter mobile app.  I spent the last week on it (it&amp;rsquo;s not pretty so no screenshots), but at this stage I don&amp;rsquo;t see much point continuing to work on it.&lt;/p&gt;
&lt;p&gt;For one, the experience is far from perfect.  Video loading is slow and there are cases when the video pauses due to buffering. I&amp;rsquo;m sure there are ways around this, but I really don&amp;rsquo;t want to spend the time learning how to do this.&lt;/p&gt;
&lt;p&gt;It was also expensive. I have a Linode server running in Sydney which acts as a bit of a hobby server (it&amp;rsquo;s also running Pagepark to serve this site); but in order to be closer to the YouTube CDNs that are closer to me, I had to rent a server that would run in Melbourne.  And there are not many VPS hosting providers that offer hosting here.&lt;/p&gt;
&lt;p&gt;So I went with Google Cloud.&lt;/p&gt;
&lt;p&gt;Now, I&amp;rsquo;m sure there&amp;rsquo;s a lot to like about Google Cloud, but I found its VPS hosting to be quite sub-par.  For just over $10 USD a month, I had a Linux virtual server with 512 MB of RAM, 8 GB of storage, and a CPU which I&amp;rsquo;d imagine is throttled all the way back as trying to do anything of significants slowed it to a crawl.  I had immense issues installing OS updates, getting the Dokku based web-app deployed, and trying to avoid hitting the storage limit.&lt;/p&gt;
&lt;p&gt;For the same amount of money, Linode offers me a virtual server with 2 GB of RAM, 50 GB of storage, and a real virtual CPU.  This server is running 4 Dokku apps, 3 of them with dedicated PostgreSQL databases, and apart from occasionally needing to remove dangling Docker images, I&amp;rsquo;ve had zero issues with it.  None!  (The podcasters were right).&lt;/p&gt;
&lt;p&gt;Where was I?  Oh, yeah.  So, that&amp;rsquo;s the reason why I&amp;rsquo;m abandoning this project and will need to re-evaluate my online video watching experience.  I might give Plex a try, although before doing something like setting up a dedicated media server, I&amp;rsquo;ll probably just use the Mac Mini I&amp;rsquo;ve been using for a desktop in the short term.&lt;/p&gt;
&lt;p&gt;So, yeah, that&amp;rsquo;s it.  It&amp;rsquo;s hard to abandon a project you spent any amount of time on.  I suppose the good thing is that I got to play around with Flutter and learnt how to connect to a Chromecast using Dart, so it&amp;rsquo;s not a complete waste.&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>