Nathan Grigg

Zsh push-line-or-edit

This week Dr. Drang wrote about a useful feature in recent versions of OS X’s, which allows you to display a man page in its own special window. This is especially useful when you want to look something up but you are in the middle of typing a long command.

It’s not polite to respond to a good solution to a common problem by claiming to have a better solution, and even less so when the better solution requires you to change your shell. But this is the internet, so here goes.

When I need to look at a man page while writing a command, I use zsh’s push-line editing command. This clears the prompt and waits for you to type something else. After executing this new command, it restores your original prompt. The nice thing about this is that it is useful beyond just looking at manuals. Often while typing some command, I realize I need a quick mkdir or cd or even ls before I’m ready to execute.

You can bind push-line to a key (I use Ctrl-B) by putting bindkey '^B' push-line in your .zshrc.

Even better, you can use push-line-or-edit to get the same behavior with one very useful addition. Normally, if you are typing a continuation line of a multi-line command, you cannot make changes to prior lines. But push-line-or-edit redraws the lines as a single block of text, which allows you to edit anything you have typed so far.

More careful TaskPaper automation

When I started using a script to add items to my TaskPaper file, I was a little worried about the script making changes to my file while it was open in TaskPaper. So I used TaskPaper’s preference to save my files every five seconds, and nothing bad happened for a while.

Then I started seeing corrupted files. It seems like OS X autosave is doing something weird. If I poke at it, I can get parts of the file go missing, or sometimes a dialog box pops up to complain. But everything works fine as long as I do an actual “⌘S” save.

To prevent corruption, I added a few lines to my shell script, which use AppleScript to save my TaskPaper file before making the changes. I use pgrep to check if TaskPaper is running, and a heredoc to send the text of the script to the osascript binary.

if pgrep TaskPaper > /dev/null; then
/usr/bin/osascript << EOM
tell application "TaskPaper"
  repeat with Doc in documents whose name is "tasks.taskpaper"
    save Doc
  end repeat
end tell

(It is so much easier to embed AppleScript in a bash script than the other way around.)

Launchd with multiple users

The most widely read post on this site is my 2012 post on scheduling tasks using launchd. But my knowledge of launchd is limited to my experience. In particular, I was mistaken about how to set up a task when your computer has multiple accounts.

(For many years, my wife and I shared an account, mostly because it’s still so difficult to switch between accounts and properly share files. But now, with iPhones and iCloud, it’s even more painful to share an account, so we finally split things up.)

In my post, I wrote:

If you have multiple users and need something to run no matter who is logged in, you should look into putting it in /Library/LaunchAgents.

But this isn’t quite right. For system-wide jobs, there are two folders that can contain your Launch Agent plists: /Library/LaunchAgents and /Library/LaunchDaemons.

The difference is that system-wide Launch Agents run exactly like per-user Launch Agents, except that they run once for each user. If you have two users logged in, the system will run two instances of the Launch Agent job. Each job will run with that user’s permissions. (This may actually cause problems. For example, if you need to write to a file, you must use a different file for each user or use a file that is world-writable.)

Launch Daemons, on the other hand, spawn a single instance, regardless of who is or is not logged in. By default, these run with root permissions (be careful!), although you can (and almost always should) customize this with the UserName key.

Taskpaper Inbox

Here’s my new favorite way to get tasks into TaskPaper. It’s a combination of Drafts, Dropbox, launchd, a Python script, and a shell script.

That sounds convoluted, but once each piece of the pipeline is in place, I just enter one or more tasks into Drafts on my phone, and three seconds later, it is in my TaskPaper file on my Mac. It’s like iCloud, but without the mystery.

Merge new tasks into TaskPaper

I wrote a Python script to insert new tasks in the proper place in my TaskPaper file. Since TaskPaper files are just plain text, this is not too complicated.

My script reads in a text file and interprets each line as a new task. If the task has a project tag, it removes the tag, and then it groups the tasks by project. Anything without a project is assumed to be in the inbox. Next, it reads my main TaskPaper file, and figures out where each project begins and ends. Finally, it inserts each new task at the end of the appropriate project.

A shell script calls the Python script with the correct arguments, merging my inbox.txt file into my tasks.taskpaper file, and deleting the now-redundant inbox.txt file. Update: To avoid corrupting my TaskPaper file, I use some AppleScript within this shell script to first save the file if it is open.

(Of course, the Python script could have done these last steps also, but it’s much better to make the Python script generic, so I can use it for other purposes.)

Watch inbox for changes

The next step is to automate the merging. This is where OS X’s launchd is useful. One solution would be to run the shell script on some kind of timed interval. But launchd is smarter than that.

Using the WatchPaths key, I can have the shell script run whenever my inbox.txt file is modified. Since OS X keeps an eye on all filesystem changes, this actually has a very low overhead and means that my shell script will be run within seconds of any modifications to inbox.txt.

Here is my Launch Agent definition, stored in a plist file in ~/Library/LaunchAgents.

 1: <?xml version="1.0" encoding="UTF-8"?>
 2: <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "">
 3: <plist version="1.0">
 4: <dict>
 5:     <key>Label</key>
 6:     <string>net.nathangrigg.taskpaper-merge-inbox</string>
 7:     <key>Program</key>
 8:     <string>/Users/grigg/bin/</string>
 9:     <key>StandardErrorPath</key>
10:     <string>/Users/grigg/Library/Logs/LaunchAgents/taskpaper_merge_inbox.log</string>
11:     <key>StandardOutPath</key>
12:     <string>/Users/grigg/Library/Logs/LaunchAgents/taskpaper_merge_inbox.log</string>
13:     <key>WatchPaths</key>
14:     <array>
15:         <string>/Users/grigg/Dropbox/Tasks/inbox.txt</string>
16:     </array>
17: </dict>
18: </plist>

Drafts and Dropbox

With the hard work out of the way, I just define a custom Dropbox action in Drafts that appends text to inbox.txt in my Dropbox folder. With no fuss, Drafts sends the new task or tasks off to Dropbox, which dutifully copies them to my Mac, which springs into action, merging them into my TaskPaper file.

With so many applications and services fighting to be the solution to all of our problems, it is refreshing to see tools that are happy solving their portion of a problem and letting you go elsewhere to solve the rest.