One click

There was an interesting post on Hacker News a few days ago: What is the most impactful thing you’ve built? It reminded me of my earliest memorable programming achievement, one that I’m still proud of and that predates my full-time IT career by a couple of years.

During one of my undergraduate years, I was a part-time AutoCAD technician at a small consulting firm. My primary job was to digitize blueprints. I would take facility blueprints provided by clients, tape them to a large digitizer, and trace them with a special multi-button mouse to create an AutoCAD drawing. Then I had to measure the area of each room in the facility and enter that information into a database. The work required a lot of mouse clicks, and we had a library of custom functions written in AutoLISP to reduce some of the overhead. The buttons on the mouse were programmed to run common commands and functions so that you didn’t have to constantly move your mouse to a toolbar button or switch over to the keyboard to type commands.

When the technician who had been maintaining the custom functions left the firm, my boss tapped me to take over that role in addition to my primary duties. I wasn’t a computer science major, but my computer skills were above average and I had taken one or two programming classes, so I guess he thought I was qualified. I had never seen any LISP dialect before, and it was very confusing at first. Fortunately, AutoCAD came with good documentation, and the firm had subscriptions to two CAD magazines that contained AutoLISP tutorials and sample code. Eventually I learned enough to start enhancing the existing functions and writing new ones.

My boss had told me when I took over the role that his holy grail was a one-click area function. He had wanted it for years, but none of the previous technicians (at least one of whom was a computer science major) had been able to crack it.

For a little background, to measure the area of a space in AutoCAD, you invoked the “area” command and clicked each corner in the space. It wasn’t a big deal in a small rectangular room, but in a large room or a room with a lot of corners, measuring the area required quite a bit of mouse movement across the large digitizer. My boss wanted to be able to click once anywhere inside of a space and have the area measured automatically. It may not seem like a big deal — and I confess that at the time, I didn’t think it was — but when you’re dealing with a large university that may have handed you dozens of blueprints, small time savings add up.

I read a lot of sample code in the CAD magazines, and I scoured the AutoCAD documentation to no avail. Then one day, I remembered (or discovered; I’m not sure which) that area is one of the properties of a hatch. And you can hatch a space by clicking anywhere inside of it. (Hatching a space is like filling a shape with a pattern; the hatch is the fill.) I wrote a function that worked like this: When you clicked inside the space, it would automatically hatch the space, get the area of the hatch, and erase the hatch. It was kludgy and inelegant, and it didn’t work if the space contained an arc. (I never did figure out why.) But about 90 percent of the time, it reduced measuring area to one click.

Filter the Micro.blog Photos page by category

By default, Micro.blog’s Photos page shows all of the JPEGs you’ve uploaded. I wanted to show only images from posts with the category “photo.” I’m still learning how Hugo works (and the Hugo documentation could be better if you ask me), so it took some time to figure it out. This is how you change it in the standard template. Find this line: {{- $list := where .Site.Pages ".Params.photos" "!=" nil -}}. Replace it with {{- $list := .Site.Taxonomies.categories.photo -}} where “photo” is the category.

Migrating from Jekyll to Micro.blog

I recently returned to Micro.blog for the third time (and my old username was even still available). I decided that this time, I would take full advantage of Micro.blog and migrate my Jekyll blog over to Micro.blog. I like that my blog is just static HTML and image files, but it’s kind of inconvenient to actually write. I have to be at my Mac because I need Ruby. I can’t take a photo with my phone and post it without some effort. Micro.blog is just easier, and should the need arise, I can export my site as Markdown files and move back to Jekyll.

There’s not a lot of information on the web about converting a Jekyll site to plain Liquid-free Markdown files. Fortunately, I found this blog post that pointed me to Pandoc.

This is how I migrated my Jekyll site to Micro.blog. If you want to do it the same way, I put the scripts you’ll need in this GitHub repository. The instructions assume some familiarity with the terminal and Python.

Setup

  1. Clone the repository or download the individual files.
  2. Install Python 3.
  3. Install Pandoc.
  4. Open your terminal app.
  5. pip install pypandoc
  6. pip install requests (optional; if you want to check the converted Markdown files for bad links)

Generate a JSON feed of your Jekyll site

  1. Copy feed-all.json to the root of your Jekyll site.
  2. Build your site.

Set variables in json-to-md.py

  1. In the line json_file = '/<Jekyll root>/_site/feed-all.json', change <Jekyll root> to the root of your Jekyll directory.
  2. In the line output_dir = '/<Jekyll root>/_ToMicroBlog', change <Jekyll root> to the root of your Jekyll directory.
  3. In the line static_asset_paths = ['/images/'], add paths to your static assets (e.g., images). Make sure you include leading and trailing slashes. Separate multiple paths with commas, e.g., ['/images/', '/assets/']

Convert JSON to Markdown

  1. cd to the directory that contains json-to-md.py.
  2. Type ./json-to-md.py and press Enter.

Clean up the Markdown files

All files will be output in /<Jekyll root>/_ToMicroBlog.

Review posts-with-img.txt, posts-with-code.txt and posts-with-youtube.txt. Clean up anything that didn’t convert cleanly.

<img> tags

Micro.blog will only import the image file listed in the src attribute of <img> tags. If you have srcset in any <img> tags, you’ll need to remove those from your Markdown files (or manually upload the image files and fix the URLs after importing to Micro.blog). The posts-with-img.txt file will list all Markdown files that contain <img> tags so that you can find them more easily.

Code blocks

If you have code blocks with line numbers in your Jekyll posts, they won’t convert cleanly to Markdown. Even without line numbers, code blocks may not convert with the proper line breaks, spacing and indentation. The posts-with-code.txt will list all files that have code blocks.

YouTube embeds

For YouTube embeds, posts-with-youtube.txt will list the YouTube IDs in each post. Add {{< youtube ID >}} to the Markdown file (replace “ID” with the ID in the file). This is Hugo shortcode for a YouTube embed. Micro.blog will translate this to the embedded video.

Check for bad links (optional)

check-links.py can check for bad links in your converted Markdown files. If you want to check the links, make sure you installed Requests in Setup step 5. Then type ./check-links.py and press Enter. The script will produce a file called bad-links.txt that lists the bad links in each Markdown file. A link is considered “bad” if it doesn’t return HTTP 200 (OK).

Import to Micro.blog

Zip your Markdown files and import the zip file to Micro.blog.

Rebuild the site (maybe)

After importing the Markdown files, all of my imported posts were listed in my account under Posts, but only a few showed up on my blog. I had to rebuild the site by clicking the Rebuild button at the top of this page.

Check for bad links (again)

I used Integrity to check the site for bad links after everything was imported. This helped me find some srcset attributes that I hadn’t cleaned up. To easily find the posts that contained bad links, I downloaded the Micro.blog site as Markdown so that I could just search the directory in Visual Studio Code. Then I manually fixed the posts in the Micro.blog Mac app.

Don’t mark your posts as drafts

I thought I could import all of my posts as drafts (by setting draft: true in the front matter of the Markdown files), clean up formatting issues, and then publish. Don’t do this; you’ll lose your original publish dates. I made this mistake and ended up deleting and reimporting all of my posts.

Can’t wait for December 1

Keyboard Maestro, Shortcuts and the Services menu

I’ve used Shortcuts on iOS since the Workflow days, but I haven’t been motivated to explore Shortcuts on macOS. For Mac automations, I rely mainly on Keyboard Maestro. Recently, I was sitting in front of my Mac when I had an idea for an iPhone shortcut, so I decided to go ahead and build it in the Shortcuts app for Mac. That’s when I discovered a fantastic feature of Shortcuts on macOS: You can add a shortcut to the Services menu. A shortcut can run Keyboard Maestro macros, so this is roundabout way of adding Keyboard Maestro macros to the Services menu.

At work, I’m often logged into our SaaS application in different browsers using different accounts. When I’m testing email notifications and the email contains a link, I need to be sure to open the link in the right browser. My previous solution to this was a set of Keyboard Maestro macros, each one set up to open a copied link in a specific browser. If I needed to open a link in a browser that wasn’t my default, I would copy the link and press the hotkey for that browser’s macro.

If you right-click a link and select a shortcut from the Services menu, the shortcut receives the link as input. I updated my macros to use the shortcut input (accessible in Keyboard Maestro as %TriggerValue%) instead of the clipboard. Since Chrome is my default browser at work, I have shortcuts for Safari, Firefox and a Chrome incognito window. The shortcut naively checks that the input is a valid URL (it just has to start with “http”), and then calls the corresponding macro, passing the URL to it. Now all I have to do is right-click the link and select the appropriate shortcut to open the link in a specific browser.

To make these shortcuts useful to a wider audience, I also made versions that don’t rely on Keyboard Maestro. The non-Keyboard Maestro version of the Safari shortcut uses the Open URLs action in Shortcuts. The shortcuts for the other browsers use AppleScript. Even though I don’t need a shortcut for Chrome, I created one to share here. You can download the shortcuts and macros at the bottom of this post. (Although I thought I had tested it successfully, the non-Keyboard Maestro version of the Chrome incognito shortcut doesn’t work. Shortcuts apparently isn’t allowed to send keystrokes via AppleScript, so there’s no way that I know of to open a Chrome incognito window via Shortcuts.)

The first time you run one of the shortcuts, you’ll see a popup asking for permission to open the URL using the shortcut. Click Always Allow to permit the shortcut to open any URL in the future.

Shortcuts privacy popup
Shortcuts privacy popup
Shortcuts in the Services menu
Shortcuts in the Services menu
Shortcut to open a URL in Safari using the Open URLs action
Shortcut to open a URL in Safari using the Open URLs action
Shortcut to open a URL in Safari using Keyboard Maestro
Shortcut to open a URL in Safari using Keyboard Maestro
Keyboard Maestro macro to open a URL in Safari
Keyboard Maestro macro to open a URL in Safari
Shortcut to open a URL in Chrome using AppleScript
Shortcut to open a URL in Chrome using AppleScript

Downloads

Shortcuts

Keyboard Maestro macros

Bunch, AppleScript and Excel

I work in a professional services job, and most of my time is billable to client projects. Our web-based time tracking software is a little bit inconvenient to use, so I track my time in an Excel spreadsheet throughout the day and enter it in my timesheet at the end of the day. (And since Excel is doing the math, I know I have an accurate accounting of my time, free of the inevitable calculation errors I would make.) The spreadsheet looks like this:

I keep one spreadsheet for each week in a folder structure like this: ~/Documents/Time/<Year>/<Month>.

The nature of time tracking, at least in my case, is that it’s necessary but tedious work with little opportunity for automation. However, I saw a couple of obvious opportunities for automation in this system: creating the weekly spreadsheet and opening it to the correct worksheet every day. I use Bunch to open my usual apps and perform some other setup tasks when I log in to my MacBook in the morning, so I just added this to the same Bunch file.

Step 1: Create the weekly file if it doesn’t exist

I keep a template in ~/Documents/Time/Time.xlsx, so each week I just need to copy that file to the appropriate directory and give it the correct name. This is easily accomplished with a shell script that I named time.sh.

#!/bin/zsh

TIME_DIR="$HOME/Documents/Time"
EXCEL_TEMPLATE="$TIME_DIR/Time.xlsx" # Template for the weekly Excel file

# Get the year and month of the most recent Monday (the Monday of the current week).
# This will be the directory where the current week's file is stored.
YEAR_MONTH=$(date -v -Mon "+%Y/%m")

# Year and month subdirectory, e.g., $HOME/Documents/Time/2022/01
MONTH_DIR="$TIME_DIR/$YEAR_MONTH"

# Get the month and day of the most recent Monday (the Monday of the current week)
# and construct the file name of the weekly file.
WEEKLY_FILE=$(date -v -Mon "+Week of %m-%d.xlsx")

if [ ! -d "$MONTH_DIR" ]; then
   # Create $MONTH_DIR if it doesn't exist.
   mkdir -p "$MONTH_DIR"

   # If $MONTH_DIR doesn't exist, we know the file for this week doesn't 
exist,
   # so we need to create it by copying $EXCEL_TEMPLATE.
   cp "$EXCEL_TEMPLATE" "$MONTH_DIR/$WEEKLY_FILE"
else
   # $MONTH_DIR exists. # Create $WEEKLY_FILE if it doesn't exist.
   if [ ! -f "$MONTH_DIR/$WEEKLY_FILE" ]; then
     cp "$EXCEL_TEMPLATE" "$MONTH_DIR/$WEEKLY_FILE"
   fi
fi

# Run AppleScript script to open the file to today's worksheet.
/usr/bin/osascript "$HOME/Documents/Bunches/OpenTimeSheet.scpt" "$MONTH_DIR/$WEEKLY_FILE"

exit

The line YEAR_MONTH=$(date -v -Mon "+%Y/%m") gets the year and month of the most recent Monday in the format YYYY/MM. The $MONTH_DIR variable is then set to the current month’s directory, for example ~/Documents/Time/2022/02. (It’s really the month of the most recent Monday, which could be the previous month when a week crosses a month boundary.) Then the $WEEKLY_FILE variable is set to the string Week of MM-DD.xlsx, where MM and DD are the month and day of the most recent Monday.

Next the script checks for $MONTH_DIR and creates it if it doesn’t exist. If $MONTH_DIR does exist, the script checks for $WEEKLY_FILE and creates it if it doesn’t exist.

Step 2: Open the file to today’s worksheet

As the final step, the shell script runs an AppleScript script that opens the file and activates today’s worksheet:

/usr/bin/osascript "$HOME/Documents/Bunches/OpenTimeSheet.scpt" "$MONTH_DIR/$WEEKLY_FILE"

This is the AppleScript script:

on run argv
	set inputPath to item 1 of argv
	set filePath to POSIX file inputPath
	set currentDate to (current date)
	set today to (weekday of currentDate) as string
	
	if (today  "Saturday") and (today  "Sunday") then
		tell application "Microsoft Excel"
			open filePath
			set ws to worksheet today
			activate object ws
		end tell
	end if
end run

This script takes the path to this week’s spreadsheet as input. The shell passes the path to AppleScript ($MONTH_DIR/$WEEKLY_FILE). AppleScript gets the path (line 2) and converts it to a POSIX file object (line 3). (The input is the path as a string, which AppleScript can’t work with unless it’s converted to a file object.) Next it gets the weekday of the current date (lines 4 and 5). Then it tells Excel to open the file (line 9) and activate the worksheet with the same name as the current day (lines 10 and 11). (Since the file doesn’t normally contain worksheets for Saturday and Sunday, I added the check in line 7 just in case I log in to my work laptop on the weekend.)

All of this could’ve been done in AppleScript, but the shell is much more efficient at working with files and folders, so I decided to let the shell handle everything except opening the Excel file to the correct worksheet (which only AppleScript can do).

Step 3: Automate it with Bunch

I have a bunch called main.bunch that performs my daily setup, so I added this to that file:

# Open weekly time file in Excel.
$ time.sh

This just runs time.sh every time the bunch runs.