Using variables in Keyboard Maestro scripts

Having fallen in love with Keyboard Maestro for its flexibility in macOS automation, I began experimenting with scripting in various languages, like my old favourite Perl. That’s when the fun began. How do we access KM variables inside a Perl script.

Let’s see what the documentation says:

So the documentation clearly states that this script

#!/usr/bin/perl

print scalar reverse $KMVAR_MyVar;

should work if I have a KM variable named MyVar. But, you guessed it - it does not.

After digging around in the Keyboard Masestro forums, I found an obscure post that pointed the way. It turns out that the Perl access to KM variables is completely different from what the documentation claims, the format is not $KMVAR_MyVar, it is actually:

$ENV{KMVAR_MyVar}

so the above script works if the variable is accessed from Perl that way:

#!/usr/bin/perl

print scalar reverse $ENV{KMVAR_MyVar};

Hugo cache busting

Justification

Although caching can make page loads notably faster, it comes with a cost. Browsers aren’t always capable of taking note when a cached resource has changed. I’ve noticed recently that Safari utterly refuses to reload .css files even after emptying the browser cache and clearing the web history.

Background

With a lot of help from the a pair of articles written by Ukiah Smith, I’ve developed a workflow for dealing with this problem during the deployment process. He describes two approaches to the problem of static asset caching, one an improvement on the other. I’ve implemented something like what he describes using the git file hash to modify the filename of the css files. When the client browser sees a new filename, it always reloads the resource. So the problem is to figure out how to only change the filename when the contents have changed. Let’s say you tweak a css parameter and want to ensure that client browsers load the correct version. We can use the git file hash, and append it on the filename. Then the only remaining problem is to make sure that the page head template knows how to find the correct version to bake into the pages. Here, our approach is the same as Smith’s.

Where we diverge in the approach described here relates to the fact that I’m using straight CSS and not a pre-processor. The main change is that I’ve moved my custom CSS files to a separate directory css_source at the top level of my site directoy. It’s not parsed in the build process; but it serves as the canonical source of my custom css. During the build process, we run a script that process the contents of the css_source directory, appends the git hash to the filename and creates a JSON manifest that we use in the head template.

The process

First, I just clear the public/css directory of any prior versions of the css files:

import os
import fnmatch

# Clean public/css directory of old CSS files before build process

public_css = "path to my public css directory"
for fname in fnmatch.filter(os.listdir(public_css), '*css'): 
    os.remove(os.path.join(public_css,fname))

Then, we process the files and build the manifest:

import os
import fnmatch
import subprocess
import json
import sys
import random
import re
from shutil import copyfile

def fileHash(filePath):
    process = subprocess.run(['/usr/local/bin/git', 'hash-object',filePath], check=True, stdout=subprocess.PIPE, universal_newlines=True)
    output = process.stdout
    return output
    
def replaceIdentifier(fn,path):
    result = re.match(r'(.+)_[\d\D]{30}\.css', fn)
    try:
        baseName = result.groups()[0]
    except:
        result = re.search(r'(.+)\.css', fn)
        baseName = result.groups()[0]
    identifier = fileHash(os.path.join(path, fn))
    return baseName + "_" + identifier + ".css"

data = {}
path = os.path.abspath(sys.argv[1])
out_path = os.path.abspath(sys.argv[2])
j_data = os.path.abspath(sys.argv[3])
j_data = os.path.join(j_data,"css.json")


# empty the primary css directory
for fname in fnmatch.filter(os.listdir(out_path), '*css'): 
    os.remove(os.path.join(out_path,fname))
    
# apply hash to css filename and move to primary css directory
for fname in fnmatch.filter(os.listdir(path), '*css'):
    newFilename = replaceIdentifier(fname,path)
    data[os.path.splitext(fname)[0]] = newFilename
    copyfile(os.path.join(path,fname), os.path.join(out_path,newFilename))
    
# write the manifest
with open(j_data,'w') as outfile:
    print("Generating css json data file.")
    json.dump(data, outfile, indent = 1)
    print("Done.")

Now in site/data/tonedeaf/ I have a manifest file css.json that we can use to build the stylesheet links.

 {
   "link-page": "link-page_10eb858a5c0a9e255aef9b40baf623d64ee2c29c\n.css",
   "russian-shortcode": "russian-shortcode_65be890c35a165f7ef13a61b1134c0888a0c384f\n.css",
   "photogallery": "photogallery_4d7b91630e0c31a2764ca52d61f3293438d752d0\n.css",
   "tag_page": "tag_page_121f9dc78b4180d309fbac735da704359c335921\n.css",
   "general": "general_191e79d04191f5dd1a8392b0904ee3c8fc4e9669\n.css",
   "hugo-easy-gallery": "hugo-easy-gallery_d78dfec854168b088b061544f8f01b20a5389efc\n.css"
}

Using this data in my head template:

<link href="{{ .Site.BaseURL }}css/{{ index .Site.Data.tonedeaf.css "general" }}" rel="stylesheet">
<link href="{{ .Site.BaseURL }}css/{{ index .Site.Data.tonedeaf.css "russian-shortcode" }}" rel="stylesheet">
<link href="{{ .Site.BaseURL }}css/{{ index .Site.Data.tonedeaf.css "photogallery" }}" rel="stylesheet">
<link href="{{ .Site.BaseURL }}css/{{ index .Site.Data.tonedeaf.css "link-page" }}" rel="stylesheet">

Where Smith handles this all in a Makefile, I just use a Keyboard Maestro macro to link all of pieces together. I hope this helps solve caching problems when working with Hugo.

References

iOS shortcut to clear Safari

iOS

(N.B. The next installment in my obsessional interest in thwarting surveillance capitalism. Read Shoshana Zuboff’s seminal work on the subject and you’ll see.)

Justification

Last week I outlined my evolving comprehensive approach to thwarting surveillance capitalism - that is the extraction, repurposing and selling of online behavioural surplus for the purposes of altering future behaviour.

This is a simple iOS shortcut to the embedded Safari setting for clearing Safari history and website data. It turns out that when iOS Safari is presented with a URL in a certain format, it will execute preference settings on the device. After a little trial and error, I noted that the setting Settings → Safari → Clear History and Website Data has its own URL: prefs:root=SAFARI&path=prefs:root=SAFARI&path=CLEAR_HISTORY_AND_DATA. By loading that URL through an iOS Shortcut, you can quickly sanitize iOS Safari.

My macOS and iOS security setup - Update 2020

(N.B. I am not a security expert. I’ve implemented a handful of reasonable measures to prevent cross-site tracking and limit data collection about my preferences and actions online.)

Surveillance capitalism is a real and destructive force in contemporary economics, politics and culture. Whatever utopian visions that Silicon Valley may have had about the transformative power of ubiquitous network technologies have been overwhelmed by the pernicious and opaque forces that profit from amplifying divisions between people. While I can’t change the system, I can change my own practices and reduce the degree to which surveillance capitalists, state actors and others who have no rights to my data.

A macOS text service for morphological analysis and in situ marking of Russian syllabic stress

Building on my earlier explorations of the UDAR project, I’ve created a macOS Service-like method for in-situ marking of syllabic stress in arbitrary Russian text.

The Keyboard Maestro is simple; we execute the following script, bracketed by Copy and Paste:

#!/Users/alan/.pyenv/shims/python3
import xerox
import udar
import re

rawText = xerox.paste()
doc1 = udar.Document(rawText, disambiguate=True)
searchText = doc1.stressed()
result = re.sub(r'( ,)', ",", searchText)
xerox.copy(result)

This presumes that udar and its prerequisites have already been installed, of course.

So why not build out this idea as an actual macOS text service? In theory, it should be possible. Maybe even ideal but for Python version management I use pyenv. Because some of the UDAR dependencies will not run under my current system Python version of 3.8.5, I use 3.7.4 under pyenv and it appears that the correct Python version run under whatever environment the service launches. Someone with deeper system knowledge could undoubtedly figure it out; but instead I accomplished the same effect via a Keyboard Maestro macro.

Solzhenitsyn on the folly of looking for good/evil dichotomies

Постепенно открылось мне, что линия, разделяющая добро и зло, проходит не между государствами, не между классами, не между партиями, — она проходит через каждое человеческое сердце — и черезо все человеческие сердца. Линия эта подвижна, она колеблется в нас с годами. Даже в сердце, объятом злом, она удерживает маленький плацдарм добра. Даже в наидобрейшем сердце — неискоренённый уголок зла.

Alexander Solzhenitsyn Gulag Archipelago

My rough translation to English:

“Gradually it was revealed to me that the line separating good from evil passes not between States, nor between classes or parties. It passes through every human heart. The line shifts; it oscillates in us with the years. Even in a heart overwhelmed by evil, it retains a small bridgehead of good. Even the kindest hearts, there is an corner of evil not uprooted.”

On not minding what happens

Over-involvement in the future must be our most maladaptive trait.

Back in the 1970’s in Ojai, when Jiddu Krishnamurti drew enormous crowds to his extemporaneous talks, he touched on the liberation that comes from releasing the pointless hold on the future.1

Do you want to know what my secret is? You see, I don’t mind what happens.

Jiddu Krishnamurti Lecture, Ojai,California, USA; late 1970's

That’s it. Of all the teachings from the broad wisdom traditions, his one secret was not minding what happens. Notice that Krishnamurti didn’t say “I don’t care what happens.” It’s an important distinction. I can care about my own wellbeing and that of others in some future. But it’s pointless to hold onto that future in its imaginary state. As Oliver Burkeman put it:

We’re all imposters

Reading Oliver Burkeman’s last advice column in decade-long series in The Guardian, I was struck by his advice on the imposter syndrome:

The solution to imposter syndrome is to see that you are one…Humanity is divided into two: on the one hand, those who are improvising their way through life, patching solutions together and putting out fires as they go, but deluding themselves otherwise; and on the other, those doing exactly the same, except that they know it. It’s infinitely better to be the latter (although too much “assertiveness training” consists of techniques for turning yourself into the former).

The Buddha was a list-maker

Beginning with “The Four Noble Truths”1, “The Noble Eightfold Path”2, and so on, the Buddha was a list-maker. I recently found a wonderful book, now out of print but freely available as a pdf. By David Snyder, Ph.D., it is called “The Complete Book of Buddha’s Lists - Explained”

Snyder does a brilliant job of reinterpreting these lists and framing them in the context of what the social sciences say about how we function individually and in groups.

Beginning to experiement with Stanza for natural language processing

After installing Stanza as dependency of UDAR which I recently described, I decided to play around with what is can do.

Installation

The installation is straightforward and is documented on the Stanza getting started page.

First,

sudo pip3 install stanza

Then install a model. For this example, I installed the Russian model:

#!/usr/local/bin/python3
import stanza
stanza.download('ru')

Usage

Part-of-speech (POS) and morphological analysis

Here’s a quick example of POS analysis for Russian. I used PrettyTable to clean up the presentation, but it’s not strictly-speaking necessary.