How to make your console sing?

Problem:

Sometimes I am running command and I have no idea, how long it will take to execute.
There is this project Makefile, which you think only compiles sources,
but actually it fetches dependencies from git repositories and it takes 1 minute to complete.
Or I start copying things and they are havier than I expected.
So I just go to other windows to do some other work and I am simply forgetting about previous task.

Solution:

Add completion sound after every command.

Additional rationale:

“Pragmatic Thinking and Learning: Refactor Your Wetware” [1]
teaches, that to use your full brain potential, you have to use multisensory input.
That is why mechanical keyboard with nice click sound and button reaction are just nicer to type on.
If you spend a lot of time in terminal, you should make it pretty and maybe even add some sounds.

Preface:

Before you yell at me, that I overcomplicated the final solution, I will walk you through the intermediate steps.
If you are in a hurry, just grep for “Solution2”.
I use Mac OSX, iterm2, oh-my-zsh with bira theme and very often work with tmux over ssh.

First thing, how do you play sounds in terminal on Mac?
afplay /System/Library/Sounds/Submarine.aiff
OSX has couple of nice system sounds.
I decided to use Submarine for succesful command completion and Blow for failure.

Solution1a:

My first idea was to use .zshrc
There is a function called precmd(), that is run always just before displaying prompt.
I could use $? variable to decide, which sound shoud I play.

play_success() { afplay /System/Library/Sounds/Submarine.aiff }
play_failure() { afplay /System/Library/Sounds/Blow.aiff }
precmd() { if [ $? -eq 0 ]; then play_success; else play_failure; fi }

Problems:

The sounds were played in the same thread of execution as the terminal.
This meant, that after typing ls, I had to wait entire second for the sound to stop playing,
before I could write anything.
Not cool!
I have to put it in background.

Solution1b:

play_sound() { afplay $1 }
play_sound_in_background() { play_sound $1 & }
play_success() { play_sound_in_background /System/Library/Sounds/Submarine.aiff }
play_failure() { play_sound_in_background /System/Library/Sounds/Blow.aiff }
precmd() { if [ $? -eq 0 ]; then play_success; else play_failure; fi }

Problems:

Doing something in bacground displayed job notifications in my console:

[1] 14099
[1]  + 14099 done       afplay /System/Library/Sounds/Submarine.aiff

Solution1c:

Surround play sound with “()” makes it run in child console, so there is no output.

play_sound() { afplay $1 }
play_sound_in_background() { ( play_sound $1 & ) }
play_success() { play_sound_in_background /System/Library/Sounds/Submarine.aiff }
play_failure() { play_sound_in_background /System/Library/Sounds/Blow.aiff }
precmd() { if [ $? -eq 0 ]; then play_success; else play_failure; fi }

Problems:

It obviously doesn’t work with ssh.
Even if I install my zsh config on a remote machine.
I don’t have the sounds there.
Abandon solution.

Solution2:

Zsh config is local to the machine, but I always use iTerm2,
so maybe it has some helpful features.
Yes, it does!
Triggers [2] can do something every time, some pattern is printed on terminal.
In bira [3] zsh theme, if command fails, than it displays error code and unicode sign: 1 ↵
Triggers use regular expressions, so it is easy to match on a letter, that rearely appears in normal work.
I went to iTerm2 preferrences -> Profiles -> Advanced -> Triggers -> Edit and put there something like this:

0 ↵$          Run Command    afplay /System/Library/Sounds/Submarine.aiff &
[1-9]d* ↵$    Run Command    afplay /System/Library/Sounds/Blow.aiff &

First pattern catches 0 exit code and the other non zero exit codes.
Mind the & at the end of commands. They are silent, but without it, they run in the same thread
and cause the same problem as in the solution1a.

I also had to modify bira to start displaying the 0 code:

local return_code="%(?.%{$fg[green]%}%? ↵%{$reset_color%}.%{$fg[red]%}%? ↵%{$reset_color%})"

Problems:

In zsh, I can scroll through files in directory using tab.
Every time I did that, the line with status code of previous command is reprinted,
which triggers the sound.

Solution2b:

Bira splits the prompt into two lines, so I simply moved the status code to the first line:

PROMPT="╭─${user_host} ${current_dir} ${rvm_ruby} ${git_branch} ${return_code}
╰─%B$%b "
# RPS1="${return_code}"

Problems:

Now I am quite far, with what I want.
I have sounds, that work even over ssh.
The problem is tmux.
When I scroll in tmux, entire pane gets repainted
and iTerm2 plays all the status codes, that are visible!

Solution2c:

Use iTerm2 tmux integration.
I installed tmux2.0 on the server
(from source, it was the easiest method).
Now I am able to connect to it via ssh with:

ssh user@host -t 'tmux -CC attach'

Problems:

Non yet, TBD.

Wrapping up:

Terminals don’t like sounds.
I can set 256 colors for foregrounds and backgrounds in my terminal emulator,
but I can’t change the beep sound easily.
After couple of days, I think, it was worth it.
I really got used to having this additional feedback,
even when I am listening to music.

Please, comment, if you find it useful.
If you don’t, let me know why.

[1] https://pragprog.com/book/ahptl/pragmatic-thinking-and-learning
[2] https://www.iterm2.com/documentation-triggers.html
[3] https://github.com/robbyrussell/oh-my-zsh/blob/master/themes/bira.zsh-theme

Leave a comment