A Keyboard Maestro action to save bookmarks to Espial

So this is a little esoteric, but it meets a need I encountered; and it may meet yours if you use Espial, Keyboard Maestro and are on macOS.

For several years I’ve been using Espial a bookmark manager that looks and feels like Pinboard, but is both self-hosted and drama-free1. Espial is easy to setup, stores its data in a comprehensible sqlite database and has an API, which comes in handy when it came to solving the problem I encountered.

Espial offers a JavaScript bookmarklet that can be used to add bookmarks. It works well in Safari, but is tricky in Firefox. In the latter, creating an exception to the popup blockade proved difficult. Even more difficult was getting Firefox to allow me to opt-out of HTTPS-only mode for my Espial server URL which is not served with HTTPS. In lieu of playing around with Firefox settings, I opted to create a Keyboard Maestro macro to save the current page to Espial. I’m going to walk through the prerequisites first, then describe the steps in the macro in details, then finally provide a download link for the macro itself.

Prerequisites

  1. You will need a functioning installation of Espial
  2. Create an API key as described in this pull request. I did stack exec migration -- createapikey --conn espial.sqlite3 --userName my_user_name.
  3. Create a configuration file config.yml at ~/.espial/config.yml:
api_key: your_api_key
server:
  url: example.com
  port: 3000
  1. One of the actions in the macro will need a Ruby gem nokogiri2 so install with gem install nokogiri.

The macro in detail

First we’ll need the currently loaded URL, so we will move the cursor to the address bar, make sure the address is selected and copy it. That involves issues ⌘+L, ⌘+A, and ⌘+C in succession. Next we save the system clipboard to a variable url. The next step is to extract the page title from the URL. I did it using Ruby and nokogiri but there are other choices out there, of course.

#!/usr/bin/env ruby
require 'nokogiri'
require 'open-uri'

def get_page_title(url)
  begin
    doc = Nokogiri::HTML(URI.open(url))
    title = doc.at_css('title')&.text&.strip
    puts title || "No title found"
  rescue OpenURI::HTTPError => e
    puts "HTTP Error: #{e.message}"
  rescue SocketError => e
    puts "Network Error: #{e.message}"
  rescue StandardError => e
    puts "Error: #{e.message}"
  end
end

url = ENV['KMVAR_url']

unless url
  puts "Error: No URL provided in KMVAR_url"
  exit 1
end

get_page_title(url)

We save that output into a variable pageTitle.

Next, we request additional metadata about that bookmark from the user.

The remaining step is to collect the variables from the dialog and formulate and execute the API call to our Espial instance:

#!/usr/bin/env ruby

require 'net/http'
require 'uri'
require 'json'
require 'yaml'

# Read configuration from YAML file
begin
  config = YAML.load_file(File.expand_path('~/.espial/config.yml'))
  api_key = config['api_key']
  server_url = config['server']['url']
  server_port = config['server']['port']
  
  raise "Bad config" unless api_key && server_url && server_port
rescue => e
  puts "Error reading config: #{e.message}"
  exit 1
end

# Get variables from KM
url = ENV['KMVAR_url']
page_title = ENV['KMVAR_pageTitle']
description = ENV['KMVAR_description']
tags = ENV['KMVAR_tags']
private = ENV['KMVAR_private'] == '1'
to_read = ENV['KMVAR_to_read'] == '1'

# Prepare the request
uri = URI("http://#{server_url}:#{server_port}/api/add")
http = Net::HTTP.new(uri.host, uri.port)

# Prepare the request body
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request['Authorization'] = "ApiKey #{api_key}"

# Set the request body
request.body = JSON.generate({
  url: url,
  title: page_title,
  description: description,
  tags: tags,
  private: private,
  toread: to_read
})

# Make the request and get the response
begin
  response = http.request(request)
  puts "Status: #{response.code}"
  puts "Response: #{response.body}"
rescue => e
  puts "Error: #{e.message}"
end

That’s it! The Bookmark in Espial macro which you can download has a little more involved preparation and more dependencies than many others I use, but dramatically simplifies bookmarking to Espial. I’ve tested it with both Safari and Firefox on the macOS desktop.


  1. The End Times have come for the Pinboard.in bookmarking service - Archived version at archive.is, Related discussion on Hacker News ↩︎

  2. Nokogiri Ruby gem - parsing XML and HTML documents in Ruby ↩︎

Louisiana and the Ten Commandments

Recently, the governor of Louisiana signed a bill requiring all public school classrooms in the state to display a poster-sized copy of the Ten Commandments. In the “Beforetimes” (before the current partisan Supreme Court took shape), this would have been struck down immediately as a violation of the Establishment Clause of the First Amendment. This bill is a clear violation of that clause. I imagine that the justices will dance around the cultural and historical significance of the document without stopping to consider the state’s motives in passing this law. While the proponents of the Ten Commandments aren’t wrong about its historical significance, the U.S. Constitution and its Amendments arguably hold more importance from the secular perspective that one must adopt in a public school.

Beyond the Constitutional issues, a reasonable person might question what this adds to our moral philosophy. “You shouldn’t steal,” the document commands. Well, duh. What effect do the proponents of this law expect? Do they believe that a would-be thief will see this edict posted in their classroom and turn from their wicked ways? If their aim is to reduce crime rates and other moral misbehavior, we could test this hypothesis by looking at crime rates, divorce rates, and other metrics in Louisiana in the years to come. I doubt there will be any significant change, but as with any hypothesis, I could be wrong.

What gives me the most discomfort is that the commandments lack nuance. There are myriad ways to harm others in the modern world. By limiting the enumerated restrictions to ten while making them very specific, the commandments lose much of what would actually make it a guiding document. Consider sexual harm, for instance. The prohibition is against adultery, but what about rape, human trafficking, or abuse of minors? Similarly, consider “Thou shalt not steal”—what exactly constitutes theft in modern society? By ignoring climate change, are we not robbing future generations? Can this be understood from a Bronze Age document? What of the theft of labour and rights by our capitalist overlords?

I don’t object to posting a document outlining moral standards, but this one belongs in houses of worship, not in public schools.

Secular authors and organizations have published alternatives to the Ten Commandments, many of which are hard to argue with. While some might raise hackles, here is a cobbled-together list of alternatives from various sources:

  1. Be open-minded and willing to alter your beliefs with new evidence.
  2. Strive to understand what is most likely true, not what you wish to be true.
  3. The scientific method is the most reliable way of understanding the natural world.
  4. Every person has the right to control their body.
  5. Share your time and resources beyond your own personal interests.
  6. Be mindful of the consequences of your actions and recognize your responsibility for them.
  7. Treat others as you would want them to treat you, considering their perspective.
  8. We have a responsibility to consider others, including future generations.
  9. There is no one right way to live.
  10. Leave the world a better place than you found it.

Improving vegetable seed germination with chemical pretreatment

Some vegetable seeds, particularly many exotic chilli pepper varieties and some Asian eggplants are tricky to germinate. After trying the obvious things - cold-induced forced dormancy (cold stratification), abundant moisture, high humidity, and temperatures over 80F, I’ve found that some seeds simply do not germinate with much success at all. But having read a number of articles on this problem, we decided to try an intensive chemical process to see if we could achieve better results.

A quick word on ATtiny 1-series interrupts

The Atmel AVR 8-bit microcontrollers have always been a favourite for tinkering; and the massive popularity of the Arduino based on the ATmega 168 and 328 MCUs introduced a lot of hobbyists to this series. The companion ATtiny series from Atmel were the poor stepchildren of the ATmega controllers to an extent - useful for small projects but often quite limited. However, the acquisition of Atmel by Microchip Technology in 2016 ushered in a new series of MCUs bearing the same moniker of ATtiny, but much more capable and innovative.

FreeRTOS stack size on ESP32 - words or bytes?

Although FreeRTOS1 is an indispensible tool for working on anything more than the simplest application on ESP32, there are some difficulties to master, such as multitasking. Multitasking using FreeRTOS is accomplished by creating tasks with xTaskCreate() or xTaskCreatePinnedToCore(). In both of these calls, one of the parameters is uxStackDepth which is the allocated stack size for the task. The FreeRTOS documentation on the subject is clear about the units for uxStackDepth:

Our vermiculture process: A sustainable contribution

Several people have asked me how we manage a very productive vegetable garden; so I’ve written this post as a brief description of one aspect our our approach - vermiculture. One of our overarching family goals is sustainable living. It’s basically about leaving a small footprint. A practical component of this philosophical stance is dealing with food waste. We deal with kitchen waste with a combination of bokashi composting and vermicomposting (also known as vermiculture) It’s not for the faint-of-heart and some are horrified to learn that I keep thousands - possibly hundreds of thousands - of worms in our basement.

An approach to interleaved and variable musical practice: Tools and techniques

“How do you get to Carnegie Hall” goes the old joke. “Practice, practice, practice.” But of course there’s no other way. If the science of talent development has taught us anything over the last fifty years, it’s that there is no substitute for strategic practice. Some even argue that innate musical abilities don’t exist. Whether it’s nature, nurture, or both, show me a top-notch musician and I’ll show you a person who has learned to practice well.

Telling Hazel not to match locked files

Hazel is a centrepiece of my automation suite on macOS. I rely on it to watch directories and take complex actions on files contained within them. Recently I discovered an issue with files that are locked in the Finder. If files that otherwise match all the rules are locked, then Hazel will attempt to execute the rules. But the locked status may preclude execution. For example, I began seeing frequent Hazel notifications popups such as:

Quickly change playlist view options on macOS

While Apple is slowly coming around to recognizing that some of its users listen to classical music, there is one quirk in the Music app on macOS that betrays its deep bias toward pop music. It’s this: when you create a new playlist, the application defaults to displaying the tracks in its “Playlist” view, which as far as I can tell serves no other function than to consume real-estate in the UI by displaying a thumbnail of the album art.

Obsidian file creation date debacle and a solution

Obsidian is pretty reckless with file creation dates. If you modify a note in Obsidian, it updates the file creation date. This renders Dataview queries that rely on it useless. For an introduction to this issue, see this lengthy thread on the Obsidian forums. Workarounds There are a several solutions to this problem.

  1. YAML-based dates One can include a cdate (or similar) field in the note’s front matter and just direct the Dataview query against that, e.