A Markdown hyperlink TextExpander snippet that works in Ulysses

I write a lot of Markdown, and I’ve long had a few simple TextExpander snippets that easily let me insert a markdown link to the front-most window in either Safari or Chrome. They are very simple, for example:

tell application "Safari"
	set t to name of current tab of window 1
	set U to URL of current tab of window 1
end tell
"[" & t & "](" & U & ")"

But… I’ve been recently using Ulysses for writing, which is a Markdown editor, mostly and sort of. It turns out that you cannot simply cut and paste a markdown link into Ulysses and expect it to work. You have to use a special menu command “Paste from Markdown” instead, which will convert Markdown into its internal format.

This breaks TextExpander snippet expansion as well. The way snippet expansion usually works is that it places the expanded text in the clipboard, pastes, and then restores the old content. It’s that second step—pasting—that breaks in Ulysses:

Broken snippet expansion

I worked around this with the below:

-- Insert a markdown link for the active safari window

-- Get the info we need from Safari
tell application "Safari"
	set t to name of current tab of window 1
	set U to URL of current tab of window 1
end tell

set mLink to  "[" & t & "](" & U & ")"

-- Get the name of the fontmost active app
tell application "System Events"
	set appName to name of first application process whose frontmost is true
end tell

if appName is not equal to "UlyssesMac" then
	-- For most apps, just do the normal TextExpander thing and return the text to insert
	return mLink
else
	-- Ulysses doesn't like pasting in Markdown directly, and needs a special Ulysses command
	-- So instead, need to manipulate the clipboard and paste using that command
	set the clipboard to mLink

	delay .5 -- Delay is necessary for this to work
	tell application "System Events"
		-- Use the "Paste from Markdown" command
		keystroke "v" using {command down, option down}
	end tell
end if

The way this works is:

  1. It grabs the necessary information from the browser.
  2. Determines which application is active.
  3. If the current application is not Ulysses, do the normal thing and return the expanded link from the script. TextExpander takes it and pastes it in.
  4. If this is Ulysses, then we have to get creative and short-circuit the usual expansion process:
    1. Place the link text into the system clipboard
    2. Pause for half a second.
    3. Trigger the “Paste from Markdown” command in the menu.
    4. Return an empty string from the snippet itself.

So essentially, for Ulysses, the TextExpander snippet itself expands into nothing, but instead directly manipulates the clipboard and keyboard. It’s hacky but it works:

Working snippet expansion

I’ve shared my Markdown Links TextExpander group, which has a few more examples.

Feedback is information

I don’t like receiving feedback.

But… As an engineering manager I give people feedback all the time. I preach the gospel of feedback every day.

That doesn’t mean that I like it when feedback happens to me. It doesn’t mean that I don’t have an emotional reaction. Or that I don’t feel called out or suddenly vulnerable. Or that I always handle it as gracefully as I’d like.

What’s tough about feedback is that it targets what we are most invested in—ourselves—and for a brief moment it’s brutally clear how someone else sees us, and how it’s different than how we see ourselves and want to be seen. It feels uncomfortable. It feels personal. This is especially true for someone who cares deeply about their work and identifies strongly with it. Humans are not generally wired well to handle feedback gracefully.

But… feedback is important. There’s no way to know if you’re managing to keep your car on the road without the feedback of where the yellow lines are relative to you.

It’s best not to think of feedback as a mechanism for criticism, but as a mechanism of alignment and improvement and adjustment. Feedback is an opportunity to grow. Approach it with a growth mindset. You are receiving useful information that you would have not otherwise received. Deploy it to your benefit.

This is true even if you disagree with it, even if it is wrong, and even if it is rude or ill-intentioned! Any feedback is still a signal to how you are perceived, and new insight into the other person’s perspective. It always contains information.

I am coming to believe that a major differentiator between the good and the great is the ability to face brutal facts and see the world—and ourselves—as it is, and not how we wish it to be, all while maintaining an emotional equilibrium.

Our emotional reactions are often counterproductive to our ability to act and achieve in our own self interest. Feedback feels like an attack, so it’s natural to want to attack back, or withdraw inside ourselves. Neither of those are useful. It’s okay to be frustrated and okay to be angry. But take a breath, focus on your heart rate, and try to at least act calm and receive the feedback gracefully. Don’t push back, don’t be defensive. True calm will return, and then you can act.

So:

  1. Look at feedback as information, and as a mechanism for alignment and improvement. It is information that can be usefully deployed.
  2. Separate the emotional content of the feedback, and your emotional response from the informational content, even if it hurts, and even if you disagree.
  3. Act calm, become calm, act and grow.

Examining the structure of appearances

In ordinary life, we don’t spend very long looking at things or at the natural world or at people, but writers do. It is what literature has in common with painting, drawing, photography. You could say, following John Berger, that civilians merely see, while artists look. In an essay on drawing, Berger writes that ‘To draw is to look, examining the structure of experiences. A drawing of a tree shows, not a tree, but a tree being looked at. Whereas the sight of a tree is registered almost instantaneously, the examination of the sight of a tree (a tree being looked at) not only takes minutes or hours instead of a fraction of a second, it also involves, derives from, and refers back to, much previous experience of looking.’ Berger is saying two things, at least. First, that just as the artist takes pains – and many hours – to examine that tree, so the person who looks hard at the drawing, or reads a description of a tree on the page, learns how to take pains, too; learns how to change seeing into looking. Second, Berger seems to argue that every great drawing of a tree has a relation to every previous great drawing of a tree, since artists learn by both looking at the world and by looking at what other artists have done with the world. Our looking is always mediated by other representations of looking.

James Wood, Serious Noticing

Creating a zoom mic status icon in the menu bar

Like many people lately, I spend half my day in Zoom meetings. And also like many people, I’m working from house noisy from kids running around. So my usual mode of operation on a call is to mute myself except when actually speaking.

On the Mac, Zoom provides a built in global keyboard shortcut (Command-Shift-A) so I can toggle mute/unmute no matter what app I’m using. But that’s awkward to remember and type. And there’s no way to tell when your mic is hot if Zoom isn’t active in the foreground.

So I set out to fix these two things:

  1. A simpler way to toggle mute/unmute in zoom, from any application.
  2. A menu bar icon indicating my mic status, so I know if the mic is hot even if the zoom meeting window is in the background.

And I figured it out! It’s held together by shoestrings and good intentions, but it’s reliable. I’ve written up my notes here.

The steps I followed were:

  1. Set Zoom’s mute/unmute keyboard shortcut as a global keyboard shortcut.
  2. Map Map “eject” key to command-shift-A, command-f13 sequence using Karabiner Elements.
  3. Use Anybar to put a red/green dot in the menu bar indicating mic status, using an AppleScript to set its status.
  4. Write a script to update the menubar icon.

1. Set Zoom’s mute/unmute keyboard shortcut as a global keyboard shortcut.

This is done in the Zoom settings, seen here:

image alt Zoom preference screenshot

2. Map “eject” key to command-shift-A, command-f13 sequence using Karabiner Elements

Karabiner-Elements is a program that can change key mapping on the Mac. You can do really simple things, like map caps-lock to control, or more complex things, like what I’m about to describe.

I chose to map eject to two key sequences: command-shift-A, command-f13. The first simply executes the native Zoom keyboard shortcut for mute, as described above. The second one is more interesting, which I’ll describe in a later step. 

Complex modifications in Karabiner are defined via json.

{
  "title": "Zoom audio toggle modification",
  "rules": [
    {
        "description": "Eject key triggers two keyboard shortcuts",
        "manipulators": [
            {
                "from": {
                    "consumer_key_code": "eject"
                },
                "to": [
                    {
                        "key_code": "a",
                        "modifiers" : ["command", "shift"]
                    },
                    { 
                        "key_code": "f13",
                        "modifiers" : ["command"]
                    }
                ],
                "type": "basic"
            }
        ]
    }]
}

To enable the above:

  1. Place the json file in ~/.config/karabiner/assets/complex_modifications.
  2. Goto Karabiner -> Preferences -> Complex Modifications -> Add rule
  3. Enable your custom rule.

The documentation for complex modifications in Karabiner Elements is a bit hard to come by / nonexistent. A few resources I used:

(You can actually just add a simple modification in Karabiner elements if you don’t care about the status bar indicator, and stop here.)

3. Use Anybar to put a red/green dot in the menu bar indicating mic status, using an AppleScript to set its status.

Anybar is a simple Mac utility that puts a colored dot in the menu bar, which you can control using the command line or AppleScript. I installed it using Homebrew

4. Write a script to update the menubar icon

This is what the second keyboard shortcut sequence, command-f13 is for. When that is triggered, I use an Alfred hotkey to run the script, which reads the current state of audio in Zoom and updates the menu bar accordingly (There are different ways to do this, but I use Alfred to manage my keyboard shortcuts.):

image alt Alfred screenshot

image alt Alfred screenshot

-- Manipulate and get the status of Zoom mute/unmute, and show that status using the app AnyBar
-- Uses AnyBar to show zoom audio status (https://github.com/tonsky/AnyBar)
-- 
-- Takes one argument:
--   - toggle_zoom: Toggles the mute status and updates AnyBar red/green to reflect new status
--   - update_bar:  Grabs the current mute status and updates AnyBar
--
-- Anybar colors:
--   - Green: mic on
--   - Red: mic muted
--   - Hidden: No zoom meeting in progress
--

property btnTitleMute : "Mute audio"
property btnTitleUnMute : "Unmute audio"

on is_running(appName)
	tell application "System Events" to (name of processes) contains appName
end is_running

on set_indicator(indicatorColor)
	tell application "AnyBar" to set image name to indicatorColor
	-- display notification indicatorColor
end set_indicator

-- Return true if zoom meeting is active
on is_zoom_meeting_active()
	-- Is zoom even running?
	if not is_running("zoom.us") then
		return false
	end if
	
	tell application "System Events"
		tell application process "zoom.us"
			if exists (menu bar item "Meeting" of menu bar 1) then
				return true
			end if
			return false
		end tell
	end tell
end is_zoom_meeting_active

-- Return true/false if mic is active or not
on get_zoom_meeting_mic_on()
	if is_zoom_meeting_active() then
		tell application "System Events"
			tell application process "zoom.us"
				if exists (menu item btnTitleMute of menu 1 of menu bar item "Meeting" of menu bar 1) then
					return true
				end if
			end tell
		end tell
	end if
	return false
end get_zoom_meeting_mic_on


-- Update the status bar
on update_status_bar()
	if get_zoom_meeting_mic_on() then
		set_indicator("green")
	else
		set_indicator("red")
	end if
end update_status_bar

-- Toggle the audio state in zoom
on toggle_zoom_audio_state()
	if is_zoom_meeting_active() then
		tell application "System Events"
			tell application process "zoom.us"
				if my get_zoom_meeting_mic_on() then
					-- If unmuted, mute
					click menu item btnTitleMute of menu 1 of menu bar item "Meeting" of menu bar 1
					my set_indicator("red")
				else
					-- If unmuted, mute
					click menu item btnTitleUnMute of menu 1 of menu bar item "Meeting" of menu bar 1
					my set_indicator("green")
				end if
			end tell
		end tell
	end if
end toggle_zoom_audio_state

-- Entry point for script
on run argv
	-- If there is no zoom meeting, quit anybar (no indicator), and quit processing
	if not is_zoom_meeting_active() then
		tell application "AnyBar" to quit
		return
	end if
	
	if (count of argv) > 0 then
		set mode to item 1 of argv
		
		if mode is "toggle_zoom" then
			-- Change state of audio zoom (triggered from keybard shortcut)
			toggle_zoom_audio_state()
		else if mode is "update_bar" then
			-- Just update the menu bar (called via cron)
			update_status_bar()
		end if
	end if
end run

(Note that this script has the ability to set the mute status. I ended up not using that, because it’s significantly slower than doing it via native Zoom keyboard shortcut.)

This actually works really well. But what if I just click the “mute” button in the zoom app? This script won’t be run, and the status bar won’t be updated, and therefore wrong.

I use launchd to run the script every 10 seconds. This also serves to remove the status icon once a video chat has concluded.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>EnvironmentVariables</key>
	<dict>
		<key>PATH</key>
		<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/munki:/usr/local/sbin</string>
	</dict>
	<key>Label</key>
	<string>Periodically check zoom status</string>
	<key>LowPriorityIO</key>
	<true/>
	<key>ProgramArguments</key>
	<array>
		<string>/usr/bin/osascript</string>
		<string>/Users/james.sulak/src/zoom_toggle/zoom_audio.applescript</string>
		<string>update_bar</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
	<key>StartInterval</key>
	<integer>10</integer>
</dict>
</plist>

Place this in ~/Library/LaunchAgents/.

The spirit of the staircase

My former writing teacher, the essayist and cartoonist Timothy Kreider, explained revision to me: “One of my favorite phrases is l’esprit d’escalier, ‘the spirit of the staircase’ — meaning that experience of realizing, too late, what the perfect thing to have said at the party, in a conversation or argument or flirtation would have been. Writing offers us one of the rare chances in life at a do-over: to get it right and say what we meant this time. To the extent writers are able to appear any smarter or wittier than readers, it’s only because they’ve cheated by taking so much time to think up what they meant to say and refining it over days or weeks or, yes, even years, until they’ve said it as clearly and elegantly as they can.”

It’s normal (and even desirable) that the structure of your work will change drastically between drafts; it’s a sign that you’re developing the piece as a whole, rather than just fixing the small problems.

From How to Edit Your Own Writing – The New York Times.

What I’ve been reading

I find myself more willing to buy/read physical books now that social distancing confines me to a small collection of rooms.

What I’ve been reading