- Emily St. John Mandel, The Glass Hotel. I haven’t read many novels yet this year, but this is my favorite. Subtle and engaging.
- Nicole Forsgren and Jez Humble, Accelerate: The Science of Lean Software and DevOps: Building and Scaling High Performing Technology Organizations. I’m finding this to be really good. It’s famously hard to measure software developer productivity, but they make the best arguments I’ve seen for their measures.
- Peter Ackroyd, Dominion: The History of England from the Battle of Waterloo to Victoria’s Diamond Jubiliee. The last in his series.
- Diarmaid MacCulloch, Thomas Cranmer: A Life. This book is, frankly, a bit slow. But… I find this perfect reading for the end of the day. It’s a different world completely removed. It’s an activity that stretches out time, as opposed to an activity that accelerates time, like watching TV or even programming, where at the end the time has disappeared and I don’t know where it went. It’s also a reminder of just how much knowledge there is out there, and how you can keep digging down in an area and discovering more at seemingly the same amount of complexity and detail. This is a 600 page book, well-researched and well-written (taking years of work), about a historical figure that at the first approximation no one cares about or remembers today. I find this comforting and amazing.
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:
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:
- It grabs the necessary information from the browser.
- Determines which application is active.
- 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.
- If this is Ulysses, then we have to get creative and short-circuit the usual expansion process:
- Place the link text into the system clipboard
- Pause for half a second.
- Trigger the “Paste from Markdown” command in the menu.
- 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:
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:
- Look at feedback as information, and as a mechanism for alignment and improvement. It is information that can be usefully deployed.
- Separate the emotional content of the feedback, and your emotional response from the informational content, even if it hurts, and even if you disagree.
- 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:
- A simpler way to toggle mute/unmute in zoom, from any application.
- 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:
- Set Zoom’s mute/unmute keyboard shortcut as a global keyboard shortcut.
- Map Map “eject” key to command-shift-A, command-f13 sequence using Karabiner Elements.
- Use Anybar to put a red/green dot in the menu bar indicating mic status, using an AppleScript to set its status.
- 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:
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:
- Place the json file in
~/.config/karabiner/assets/complex_modifications
. - Goto Karabiner -> Preferences -> Complex Modifications -> Add rule
- 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:
- How to create your own CUSTOM complex modifications? · Issue #1225 · pqrs-org/Karabiner-Elements
- karabiner.json data structure | Karabiner-Elements
- JoshuaManuel/Karabiner-Elements-Key-List: A reference list of keys recognized by Karabiner Elements
- karabiner-elements-complex_modifications
(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.):
-- 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.
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.
- The Mirror and the Light: Hilary Mantel. The final book in the series starting with Wolf Hall and Bring Up the Bodies. Her language is beautiful.
- Britain After Rome: The Fall and Rise, 400 to 1070: Robin Fleming.
- Great by Choice: Jim Collins. This is one of those business books that you can get 90% of the value from by reading the chapter summaries. I’m also annoyed by his ahistorical characterization of the Scott/Amundsen polar race (Roland Huntford I think has been mostly discredited at this point). Good to Great I recommend as the better read.
- Effective Python: Brett Slatkin. We’re embracing Python at FlightAware, and I’m out of practice.
- The Unicorn Project: Gene Kim. Sequel to The Phoenix Project. I recommend the audiobook version. Low density, corny, but some good stuff.
What I’ve been reading
- Foundation: The History of England from Its Earliest Beginnings to the Tudors: Peter Ackroyd and Tudors: The History of England from Henry VIII to Elizabeth I: Peter Ackroyd. I’ve fallen down a bit of a rabbit hole with these.
- Children of Ruin: Adrian Tchaikovsky. Sequel to Children of Time. The first was great. This one I’m having trouble getting through and may stop.
- Managing The Professional Service Firm: David H. Maister. The chapters on managing your career and motivating professionals I found surprisingly good and broadly applicable.
- What You Do Is Who You Are: How to Create Your Business Culture: Ben Horowitz. Culture is a strategic investment in the company doing things the right way when you are not looking.
- The Effective Hiring Manager: Mark Horstman. The purpose of an interview is to find reasons not to hire the candidate.