5ab5traction5 - World Wide and Wonderful

Raku to the Rescue: APL Keyboard Setting Keeper

Outline

  1. First I'll introduce the rationale behind this new series of blog posts called Raku to the Rescue.

  2. Afterwards we will dig into a small Raku script which quickly sets up my keyboard to use the CAPSLOCK key as a toggle switch for inputting APL characters.

  3. The magic sauce of reactive programming is introduced to solve a problem where we need to periodically check via a system command whether (some layer of) the Linux desktop has annoyingly reset my XKB config and then place back our carefully chosen configuration settings.

Introducing.. Raku to the Rescue!

It's been several years since Raku became my primary scripting language of choice. Whenever I have the option to use it, I do. Since it is always installed on my machines and there are always thingies that need to get glued together so Raku ends up getting used a fair bit.

This series aims to publicize some of the successful solutions that I've written while scripting annoying little problems into oblivion.

So follow me, brave reader, if you dare, for the first installment of our serialized adventure series exploring the wild unknown... Raku to the Rescue!

Today's Problem

At some layer of the Linux desktop, something is re-setting the XKB configuration that I've carefully set with setxkbmap.

The kind of periodic checking for this betrayal that best addresses this problem is very easy to do in Raku -- and without feeling like some gross deviant who relies on a while(1) with a sleep call tucked inside. Nice!

APL?

While Raku is still regarded with baseless suspicion by many, I may have recently fallen in love with a language with an even more maligned image amongst the modern programmer: APL.

I won't go into depth about the history of the language here -- one of my absolute favorite aspects of APL (or any language) is removing boiler plate. There are a ton of intro articles on every damn thing in the world and I can't see myself doing it better.

Another thing I love about APL is the rich historical record. For my particular brand of dorky madness, it's downright intoxicating at times. So go forth and read some APL history for yourself.

And if you need a bit of convincing about why array programming is a crucial paradigm to consider for the performance problems we face today, I recommend the excellent talk Rectangles All The Way Down by Martin Thompson.

The Raku Solution

One of my absolute favorite features of Raku is being able to script a command-line program with as little boilerplate as possible (if this guy hates boilerplate so much, why does he keep repeating himself? - ed.).

sub MAIN(:$interval = 30, :$key = 'caps_switch', :v($verbose) = False) {
  react {
    whenever Supply.interval($interval) {
      set-xkbmap-for-apl($key, $verbose)
        if not xkbmap-contains-apl;
    }

    whenever signal(SIGINT) {
      say "Reset a total of $total-resets out of $total-checks checks"
      	if $verbose;
      exit;
    }
  }
}

Example invocation

apl-keyboard-keeper --interval 60 -v >> /tmp/xkb-resets.log &

This slows the checks down to 60 seconds and turns on logging that is then append piped to the file /tmp/xkb-resets.log for later investigation.

Additional logging could be added should I ever bother investigating the underlying issue beyond the simple statistics provided now. However I have rarely used this script since I installed Hikari on Wayland. (Wayland does not use setxkbmap for modifying XKB settings, instead relying on environment variables like a sane UNIX tool).

The MAIN thing

I don't need to reach for a single library. Every feature is in core Raku. I get a short-hand -v toggle just by adjusting the signature declaration ever so slightly. That's because named arguments to your MAIN routine will be interpreted as CLI flags. This allows Raku to provide sugar to adjust the CLI argument parsing by changing the name of the argument slightly.

This is all inter-related to the topics of Signature and Pair, should you wish to dive deeper.

Go ahead, react whenever

The reason I reach for Raku whenever I need a task repeated on a time based manner is because it is exactly as easy as you see above.

The pattern is so easy to write that there is really no reason to ever generalize it in a module -- I just write it every time I need it. One example that had a big impact on my physical health was setting up a timer to stand up and walk around every 30 minutes as I had been instructed by my doctor. I wrote it as a one-liner and set it running every day when I got into work.

In this case we are using Supply.interval to trigger it's whenever block, um, whenever it fires. This checks whether our settings have been erased and resets them if that is the case.

We take care of our exit case by responding to the SIGINT signal with our own codepath. Note that exit is not required, meaning that one can write code to try and untangle whatever state has led a user to send a SIGINT to the process.

Next generation regex

Perl was once the pinnacle of text processing due to it's first class handling of "regexes" (we don't call them "regular expressions" out of fear that academically minded nerds will come for our keyboards).

However, as anyone who has written or edited a PCRE (Perl Compatible Reg Ex) before can attest, it's rarely pretty.

A lot of work has been done in Raku to elevate regex syntax to new levels of usability. An example from my script:

  if $xkb-settings ~~ /^^ "layout:" \s* $<layout>=(<.graph>*) $$/ {
    $layout = $<layout>.Str;
  } else {
    die "Aborting. The xkb settings do not specify any layout:\n$xkb-settings";
  }

Here we use the logical line begin/end matchers, ^^ and $$ respectively. Notice that string literals must be quoted to be matched now. A wonderful knock-on effect of this is way fewer backslash escapes are required to input your match string.

Named regex capture groups are very easy to define and are done in a way that interacts directly with $/, the Match object. This removes the awful implications of $1, $2,... that seeped throughout in Perl 5. (Named captures are not necessary but positionals are accessed through the $/ object as well, eg $/[1,2]).

<.graph> is a handy included character class that includes <alnum> and <punct>. This allows us to slurp in our comma-separated values. The . in <.graph> is to avoid it becoming a named capture (the default behavior of a character class in a regex is to act as a capture group named after that character class).

Conclusion

Today I've shown how Raku came to my rescue and saved me from the frustrations of trying to use my new-found muscle memory for typing APL characters only to find out that my OS is shuffling my config around and forcing me to manually re-invoke setxkbmap.

Below is the full script.

#!/usr/bin/env raku

# Small script to ensure that APL keyboard layout is still set in xkb settings.
# Note that this is a patch for some sort of deranged time-based reset of these
# settings that is happening at a lower level of the Xorg-based Linux user experience.
#
# It's not clear what is causing these resets but this script allows us to more
# or less not care about it and get on with our hacking.
#
# Released under Artistic License by John Longwalker 2020

my $total-checks = 0;
sub xkbmap-contains-apl() {
  $total-checks++;
  so qx{ setxkbmap -query | grep '^layout:.*\<apl\>' }; # shell-out is easy as usual in a Perl
}

my $total-resets = 0;
sub set-xkbmap-for-apl($key, $verbose) {
  say "Reset total is now {++$total-resets} -- {DateTime.now}"
    if $verbose;
  my $xkb-settings = chomp qx{ setxkbmap -query };

  my ($layout, $variant, $options);
  if $xkb-settings ~~ /^^ "layout:" \s* $<layout>=(<.graph>*) $$/ {
    $layout = $<layout>.Str;
  } else {
    die "Aborting. The xkb settings do not specify any layout:\n$xkb-settings";
  }

  if $xkb-settings ~~ /^^ "variant:" \s* $<variant>=(<.graph>*) $$/ {
    $variant = $<variant>.Str;
  }

  if $xkb-settings ~~ /^^ "options:" \s* $<options>=(<.graph>*) $$/ {
    $options = $<options>.Str;
  }

  $layout  = ($layout, 'apl').join(',');
  $variant = $variant ?? ($variant, 'dyalog')
                      !! 'dyalog';
  $options = $options ?? ($options, "grp:$key").join(',') 
                      !! "grp:$key";
  my $invocation = "setxkbmap -layout $layout -variant $variant -option $options";
  say "Invocation: $invocation"
    if $verbose;
  qqx{ $invocation };
}

sub MAIN(:$interval = 30, :$key = 'caps_switch', :v($verbose) = False) {
  react {
    whenever Supply.interval($interval) {
      set-xkbmap-for-apl($key, $verbose)
        if not xkbmap-contains-apl;
    }

    whenever signal(SIGINT) {
      say "Reset a total of $total-resets out of $total-checks checks"
      	if $verbose;
      exit;
    }
  }
}

- 8 toasts