Getting started with Rofi

Rofi is a Linux app that is a:

[..] window switcher, application launcher and dmenu replacement

I've been using Rofi for about a year but didn't really understand how to write my own scripts for it until recently.

dmenu? Huh?

A key to my lack of apprecaition for Rofi's feature set was that I didn't understand why it billed itself as a "dmenu replacement". What the heck is dmenu?

dmenu is a fast and lightweight dynamic menu for X. It reads arbitrary text from stdin, and creates a menu with one item for each line. The user can then select an item, through the arrow keys or typing a part of the name, and the line is printed to stdout. (emphasis mine)

Essentially dmenu lets you create and present your own menu and then act once a menu item is selected by the user. One of my favorite applications of all time is Alfred, the infinitely customizable launcher utility for macOS. The best part about Alfred was Workflows which (like dmenu) allow you to create custom menus and actions as part of the main launcher interface. How do we create a menu though?

STDIN STDOUT

dmenu will display whatever comes in on STDIN as the menu, e.g. echo "foo\nbar" | dmenu" (if you have a Linux install available install dmenu and give that command a try). Each new line in STDIN becomes a new line in the dmenu menu. The previous command creates a menu with two options: foo and bar. Once the user hits enter on a menu item the value of the selected item gets sent to STDOUT. That's it. dmenu reads in from STDIN and prints out selections to STDOUT. dmenu is super handy to add to workflows and scripts to give them a bit of UI. You can squeeze dmenu anywhere you need feedback from the user, e.g.: echo "Hello $(echo 'World\nPeople' | dmenu)". This prints a hello world message after the users chooses "World" or "People" from dmenu.

Rofi + dmenu

Rofi adds dmenu capability plus lots of other things like selecting open windows, running ssh commands etc. To enable dmenu mode in Rofi pass it as an option -dmenu. Let's port our previous example code snippet to Rofi: echo "foo\nbar" | rofi -dmenu. Easy enough! With the dmenu option Rofi becomes just as useful as Alfred and makes me think Alfred should gain a command line option to pop open Alfred from within scripts like dmenu or Rofi.

My first script

The motivation to write this post came a couple weeks ago when I wrote my first real Rofi script. I use Pop_OS! as my Linux distribution of choice for work and the defaults are really good. However when I want to change my audio output (headphones or external speakers) I've grown annoyed by having to manually mouse up to the menubar and select the desired output. I set out to write a Rofi script to handle changing the audio output. On Linux you can control everything related to the audio system via the pactl utility. The pieces of data I needed for my script were:

  • A list of all available outputs to generate my Rofi menu
  • A way to change to the selected output

pactl list sinks displays all active "sinks" or available outputs ("Sinks" is the Linux audio system term for outputs). We can customize this a bit further wih pactl list short sinks which will cram relevant sink information into one line each instead of multiple lines per sink. This makes parsing the output much easier.

Now that I had a list of available speakers/headphones on the system I needed to change to that output. This turned out to be a two step process because of how the audio system works:

  1. pactl move-sink-input INPUT SINK moves anything currently playing to the specified speaker/headphone.
  2. pactl set-default-sink SINK sets the specified speaker/headphones as the default output going forward, e.g. launching a video call or playing a video.

To accomplish #1 I needed to get the currently playing input which can be done via: pactl list sink-inputs. Again we can specify short to get the relevant information on a single line: pactl list short sink-inputs.

The finished script looks like this:

#!/bin/bash

source="$(pactl list short sinks | cut -f 2 | rofi -dpi 1 -dmenu -p "Change audio:")";
inputs="$(pactl list sink-inputs short | cut -f 1)";

for input in $inputs; do
  pactl move-sink-input "$input" "$source";
done

pactl set-default-sink "$source";

Upon invoking the script, Rofi presents me with a menu that contains all the available outputs. Once I select one the audio changes instantly.

Video showing the change-audio script running via Rofi.

Conclusion

I find Rofi so useful that I have a system-wide hotkey that invokes Rofi as a general launcher like this: rofi -combi-modi run,window,drun -show combi -modi combi -dpi 1. The flags I'm using are documented under "Combi settings" in the Rofi manpage.

Rofi transforms otherwise vanilla shell scripts into poweful transient workflows that allow me to control aspects of my machine without leaving whatever I'm currently working on. It's become an essential part of my toolbox.