Open Source Diary C.X.5

You might have noticed from these updates that my open source work isn’t very focused. There’s no single big project that I am committing most of my time to. Instead I jump around between the dozens of lambdaisland projects that have been released, and making contributions to third party libraries along the way as well.

There are two reasons for that, which really point at the two things that motivate me to do open source in the first place. The first is that I address the needs of my dayjob. We constantly dogfood these projects, either on client projects, or for Gaiwan internal projects, and when I run into an issue I will go fix it, release it, and move on. Our tooling is optimized so it doesn’t take more than a few minutes to make a change and get a release out. We rarely make public announcements about these things, which is part of the reason I decided to start keeping these journals, so there’s at least some visibility in the work we’re doing.

The second reason that I do open source is that I find it relaxing. Early in the morning or during the weekend instead of bingeing youtube I like to zone out by poking at an interesting problem. This kind of coding is deliberately unfocused and exploratory, with many loose ends and abandoned pathways. It’s often driven by being unsatisfied with the available options. If there’s an area of the ecosystem that isn’t well addressed, or where I find the existing solutions lacking in elegance or design, then I will pick small pieces of the problem and try to solve them in a clean and elegant way. You’ll notice that many of the libraries we’ve put out in recent years are quite narrow in focus. It can be very liberating to pick a very specific well delineated problem, and try to solve it in the best possible way you know how, giving you a building block that you can leverage in the future to solve bigger problems.

There’s sort of a third category of FOSS work, which has to do with creating a delightful dev experience. Projects like launchpad and Kaocha fit into this category.

Anyway, up to the updates.

lambdaisland/cli - initial context vs flag defaults

The mental model of lambdaisland/cli is that of gradually constructing a map. You start with an intial map you pass into lambdaisland.cli/dispatch, we call this the initial context. Command line flags can assoc values onto this map, or trigger functions that further transform this map. Command handlers add further key-values for parsed arguments, before passing this map into the command handler function itself.

Flags can also be given a default, for example

{:flags ["--env=<env>" {:doc "Environment to execute against"
                        :default "dev"
                        :parse keyword}]}

In this example, if you don’t specify an --env flag on the command line, then the context map will get :env :dev assoc-ed onto it. But what if the initial context already contains an :env key? If we blindly assoc the default value, then this initial value will always get overwritten. So now we check if there’s already a value present, before applying the default. The check uses some?, so both an absent value, or a nil value will be replaced with the default.

Launchpad - back aliases from CLI

I recently switched launchpad over from its own ad-hoc CLI arg handling, to using lambdaisland/cli. It seems somewhere in that migration we lost the ability to provide Clojure CLI aliases on the command line.

For example:

bin/launchpad dev test --go

Will lauch Clojure with -A:dev:test. You could still get these aliases by providing them in deps.local.edn instead.

{:launchpad/aliases [:dev :test]}

But not being able to specify them on the command line directly was a major and unfortunate regression which has since been fixed.

Launchpad – better handling of boolean options

Launchpad has a number of boolean flags that determine how it behaves, for instance

  • --go - invoke (user/go) on startup
  • --cider-nrepl - inject the CIDER nREPL middleware
  • --cider-connect - instruct Emacs/CIDER to connect to nREPL once it’s booted
  • --emacs - same as --cider-nrepl --nrepl-refactor --cider-connect
  • --portal - inject Portal as a dependency, and inject a (user/portal) function for starting it

These are really user preferences. It’s likely you want launchpad to run with the same set of options in each project. That’s why we also let you set these in ~/.clojure/deps.edn. For instance, mine looks something like this.

{:launchpad/options {:emacs true :go true :portal true}}

But what if on some occasions you want to turn these back off. For instance, while playing around with some nREPL middleware I didn’t want launchpad to automatically hook up Emacs, since I was using lambdaisland/nrepl-proxy to inspect the nREPL traffic, and I wanted to manually connect CIDER to that. In that case you should be able to say bin/launchpad --no-cider-connect, and have it temporarily turn off that specific option. This is how it was supposed to work the whole time, but for reasons it wasn’t working. That’s been fixed.

Thonny

You probably didn’t expect to see any Python stuff in here, but here we are. For over a year I’ve been volunteering at my local CoderDojo group, helping kids once a month to get creative, either with scratch or Python. We usually set them up with Thonny, a minimal Python IDE that works well for this use case.

Many of the projects we do use Pygame or Pygame Zero. The latter is a bit more frameworky, eliminating as much boilerplate as possible by automatically importing certain packages, and implicitly creating the main game loop. For that to work, you don’t start the program with the python executable, but with a program called pgzrun.

Thonny has a special mode you can enable for Pygame Zero projects, so you can easily run them inside Thonny as well, but seems this had been broken for newer versions of Python for some time (ah that famous Python stability…).

There was an open issue, that documented exactly the one-line fix that had to happen, but almost a year later no one had actually bothered to fix it. I can’t blame the Thonny author. With 686 open issues at the time of writing there’s clearly too much for one person to keep up with. So I made a small PR, simply applying the fix as documented.

As the maintainer of dozens of projects I know exactly how it is. Even meticulously documented issues just go onto the pile. Maybe I’ll get to them, maybe not. It’s even less likely if the issue doesn’t really affect me. But give me a reasonable PR so I can simply hit the merge button, and I’ll probably be quick to accept it.

And so it went here. Less than a day later my change was merged, and will be part of the next Thonny release.

While on the topic of CoderDojo, I’ve also been working on a Pygame Zero tutorial for a ping-pong game. It’s in Dutch, since the audience is the Flemish teens who come to our CoderDojo workshops. It’s still a work in progress, by the next event I want to add a bunch of graphics and illustrations to make it a bit nicer and more clear, but I’m happy with how it’s coming together. It’ll be good to have a clear project that newcomers can immediately start on.

Ornament-next

I’m a big fan of the web platform and its ever improving open standards, and am always keeping an eye on how browsers are evolving, and how it lets us create better applications.

One of the things that have evolved tremendously over the years is CSS, and so I’ve been looking at ways to incorporate some of that into Ornament, our CSS-in-CLJ(s) component system. You can find this work in the ornament-next. It hasn’t been merged or released, but we’ve used it on several projects. This work really started while working on Compass, the app we made for Heart of Clojure attendees.

Ever since working with Felipe Barros on the Lambda Island redesign I’ve been interested in the concept of design tokens. Since then a JSON format for design tokens has become standardized, and CSS variables have become more widespread, paving the way for projects like Open Props, which is a kind of design system, packaged as a set of design tokens.

Ornament-next adds a few extra functions and macros to Ornament. defprop defines a CSS property (what colloquially is called a CSS variable). defrules lets you define CSS rules that are not tied to a specific component, but that still benefit from Ornament’s Garden/CSS pre-processing, in particular the use of Girouette to provide Tailwind-compatible CSS shorthands.

Finally import-tokens! lets you import a JSON design tokens file directly, generating vars for all the properties within it.

Here’s a snippet from the compass code base.

(ns co.gaiwan.compass.css.tokens
  "Design tokens, partly imported from Open Props, partly defined here"
  {:ornament/prefix ""}
  (:require
   [charred.api :as charred]
   [clojure.java.io :as io]
   [garden.stylesheet :as gs]
   [lambdaisland.ornament :as o]))

(o/import-tokens! (charred/read-json (io/resource "open-props.tokens.json")) {:include-values? false})

(o/defprop --hoc-pink "#e25f7d")
(o/defprop --hoc-pink-1 "#e7879d")
(o/defprop --hoc-pink-2 "#cd4e6a")
(o/defprop --hoc-pink-3 --hoc-pink)
(o/defprop --hoc-pink-4 "#c0415b")

(o/defprop --talk-color)
(o/defprop --workshop-color)

(o/defprop --highlight)

(o/defrules session-colors
  [":where(html)"
   {--talk-color         --blue-2
    --workshop-color     --teal-2
    --highlight          --hoc-pink-1}]

  (gs/at-media
   {:prefers-color-scheme 'dark}
   [":where(html)"
    {--talk-color         --blue-9
     --workshop-color     --teal-8
     --highlight          --hoc-pink-4}]))

As you can see you can use these variable names like --highlight or --blue-9 directly in your Garden/Ornament CSS, and it’ll be smart enough to know if it should get rendered as var(--highlight), or just as --highlight.

Another small but nice thing is that all Ornament created vars now get docstrings. You can add docstrings to your components, and the generated docstring will include the compiled CSS, so you can quickly inspect the result.