Terminal Palettes
For some recent work, I was switching between OmniFocus and OmniPlan code often. To keep my source directories straight in my head, I tweaked the colors of the two Terminal tabs to use the apps’ branding fonts: purple text for OmniFocus; yellow text for OmniPlan. I mentioned this hack in an engineering meeting and Ken promptly dropped the nerd snipe: “Does it switch automatically?”
It didn’t then. Now it does.
With a three part solution — AppleScript, shell script, and a zsh hook — my Terminal palette now updates whenever I switch between source directories. Yours can too.
ZSH Hook
I added the following code to my .zshrc
file:
if [[ "$TERM_PROGRAM" == "Apple_Terminal" ]] && [[ -z "$INSIDE_EMACS" ]]; then update_terminal_cwd() { # Identify the directory using a "file:" scheme URL, including # the host name to disambiguate local vs. remote paths. # Percent-encode the pathname. # http://superuser.com/questions/313650/resume-zsh-terminal-os-x-lion/328148#328148 local URL_PATH='' { # Use LANG=C to process text byte-by-byte. local i ch hexch LANG=C for ((i = 1; i <= ${#PWD}; ++i)); do ch="$PWD[i]" if [[ "$ch" =~ [/._~A-Za-z0-9-] ]]; then URL_PATH+="$ch" else hexch=$(printf "%02X" "'$ch") URL_PATH+="%$hexch" fi done } local PWD_URL="file://$HOST$URL_PATH" #echo "$PWD_URL" # testing print -Pn "\e]0;%n@%m: %~\a" # get the tab title right: http://www.tldp.org/HOWTO/Xterm-Title-4.html#ss4.1 printf '\e]7;%s\a' "$PWD_URL" # get path restore right ~/bin/terminalPalette.sh "$PWD_URL" # update colors } # Register the function so it is called whenever the working # directory changes. autoload add-zsh-hook add-zsh-hook chpwd update_terminal_cwd # Tell the terminal about the initial directory. update_terminal_cwd fi
This code first declares a local function, update_terminal_cwd()
, then hooks the shell directory change command to run the function. Finally it calls the function so that a new terminal window will trigger the code as well.
Inside the function, we percent-encode the current working directory. Then we set the tab title, save the working directory so it restores on the next launch of Terminal, and finally call the terminalPalette.sh
script to update the color palette.
Shell Script
The terminalPalette.sh
script lives in the bin
subdirectory of my home directory.
The script starts with a fairly standard preamble like so:
#!/bin/zsh func usage() { echo "Usage:" echo "${0} (<file url>|<settings name>)" } PROFILE=${1} if [[ -z ${PROFILE} ]] ; then usage exit 1 fi
The script takes a single argument. That argument can either be a file URL, like
terminalPalette.sh file://localhost/Users/curt/bin
or the name of a Terminal.app profile, like
terminalPalette.sh "Man Page"
The preamble just checks for any single argument and reports the correct usage if the argument is missing.
Next, we bridge the shell argument into AppleScript so we can drive Terminal.app:
osascript - `tty` ${PROFILE} <<SCRIPT
Invoked with -
as the first argument, osascript
will run a script passed in on standard input, feeding the subsequent arguments to the embedded script. Here we pass the current tty — we’ll see why shortly — and the original argument. Then we begin a here doc with the <<SCRIPT
incantation. Until the end of the here doc, we’re coding in AppleScript:
on run argv set theTTY to item 1 of argv set settingsNameOrPath to item 2 of argv if settingsNameOrPath starts with "file://" then set settingsName to my settingsNameForPath(settingsNameOrPath) else set settingsName to settingsNameOrPath end if if settingsName is missing value then return tell application "Terminal" set desiredSettings to first settings set whose name is settingsName if desiredSettings is missing value then return repeat with matchingTab in my tabForTTY(theTTY) set current settings of matchingTab to desiredSettings end repeat end tell end run
The run
handler extracts the tty and the original script’s argument into theTTY
and settingsNameOrPath
respectively. Then it detects what sort of argument was passed to the original script and converts that to a Terminal app settings name. (In grand AppleScript tradition, the UI for Terminal.app calls its color palettes “Profiles” but the AppleScript dictionary calls them “Settings”.) If the argument was a file URL, we call a handler settingsNameForPath
— see below — to get the settings name. Otherwise we use it directly.
Then we drive the Terminal app to look up the desired settings, find the tab that is rendering the current tty, and change the settings of that tab to the desired ones.
Now we just need to define a couple of helpers.
on settingsNameForPath(thePath) if thePath ends with "SourceDirs" then return "Man Page" if thePath contains "SourceDirs/OmniFocus" then return "OmniFocus" if thePath contains "SourceDirs/OmniCurt" then return "OmniCurt" if thePath contains "SourceDirs/OmniPlan" then return "OmniPlan" if thePath contains "SourceDirs/curtclifton.net" then return "curtclifton.net" if thePath contains "SourceDirs" then return "Ocean" return missing value end settingsNameForPath
The settingsNameForPath
handler maps from the path argument to custom palette names. If you’re setting up this script for your own use, you’ll want to tweak this handler and your Terminal profiles. I added some app and project specific profiles, like OmniFocus
and curtclifton.net
. So far I’ve just been tweaking the handler as I add more profiles, though someday it would be nice to make this do a bit more parsing to avoid the repetitive if
statements.
on tabForTTY(theTTY) tell application "Terminal" repeat with aWindow in every window repeat with aTab in every tab of aWindow set currentTTY to tty of aTab if currentTTY is theTTY then return {aTab} end if end repeat end repeat return {} end tell end tabForTTY
This last handler iterates through every Terminal tab looking for the one rendering the script’s tty. This might become too slow with a lot of tabs open, but so far it’s been fine with my usual four or five tabs at once.
Finally, we end the here doc and the script:
SCRIPT
This was a fun bit of hackery. I hope you enjoyed it too. You can download the terminalPalette.sh
script in its entirety here.