/ rofi

rofi: Overview and Installation

This is the first in a series of several posts on how to do way more than you really need to with rofi. It's a neat little tool that does so many cool things. I don't have a set number of posts, and I don't have a set goal. I just want to share something I find useful.

This post provides a rofi overview and installation instructions.

Assumptions

I'm running Fedora 27. Most of the instructions are based on that OS. This will translate fairly well to other RHEL derivatives. The Debian ecosystem should also work fairly well, albeit with totally different package names. This probably won't work at all on Windows, and I have no intention of fixing that.

Code

You can view the code related to this post under the post-01-overview-and-installation tag.

Overview

rofi makes it really easy to do things via simple shortcuts. It's super useful if you're trying to up your nerd cred, streamline your workflow, beef up i3, or make Linux that much more pleasant to run.

Rather than make my own shoddy feature list, I'm just going to link the official docs, whose screenshots are already very pretty. rofi can, among other things, do this stuff:

It also does a few more things that I'll probably explore later.

Installation

Instructions were mostly sourced from the official docs. I tweaked a couple of things and made sure everything worked easily on Fedora.

I tested installation from start to finish in a fresh Vagrant box.

$ vagrant init fedora/27-cloud-base

I'm (mostly) certain everything will work there. Unless you configure the box to handle or pass off X events, you won't actually be able to view rofi via Vagrant, but you can ensure the build process works as intended.

Dependencies

I make heavy use of bash's brace expansion. If you're not using bash, I'm sorry. To make things easier, I'll turn on debug mode.

$ set -x

Install build dependencies.

$ sudo dnf install -y gcc make auto{conf,make} pkg-config flex bison git
+ sudo dnf install -y gcc make autoconf automake pkg-config flex bison git

Install external libraries.

$ sudo dnf install -y {pango,cairo,glib2,lib{rsvg2,xkbcommon{,-x11},xcb},startup-notification,xcb-util{,-{w,xr}m},check}{,-devel}
+ sudo dnf install -y pango pango-devel cairo cairo-devel glib2 glib2-devel librsvg2 librsvg2-devel libxkbcommon libxkbcommon-devel libxkbcommon-x11 libxkbcommon-x11-devel libxcb libxcb-devel startup-notification startup-notification-devel xcb-util xcb-util-devel xcb-util-wm xcb-util-wm-devel xcb-util-xrm xcb-util-xrm-devel check check-devel

(Optional) Install debugging dependencies.

$ sudo dnf install -y libasan
+ sudo dnf install -y libasan

There are no more brace expansions.

$ set +x

Source Code

We'll need to get a current copy of the source.

$ cd /desired/path/for/source/code
$ git clone https://github.com/DaveDavenport/rofi --recursive
$ cd rofi

Alternatively, if you've already got a clone of the repo, just update it.

$ cd /desired/path/for/source/code/rofi
$ git reset --hard
$ git pull
$ git submodule init && git submodule update
$ rm -rf build

Finally, rebuild the tooling and create a build directory.

$ cd /desired/path/for/source/code/rofi
$ autoreconf -i
$ mkdir build && cd build

Options

It's useful to check out the current build options to understand what's going on.

$ cd /desired/path/for/source/code/rofi/build
$ ../configure --help

For example, by default, rofi is installed to /usr/local, which means the final product will do this:

$ which rofi
/usr/local/bin/rofi

You can change that in the next steps via

$ ../configure --prefix=/some/other/path

If you're using Fedora 27+, you shouldn't need to adjust any of the options. Older Fedora (and probably CentOS) might require some tweaking. Debian derivatives will also require tweaking.

Standard

$ cd /desired/path/for/source/code/rofi/build
$ ../configure

...

-------------------------------------
Timing output: Disabled
Desktop File drun dialog Enabled
Window Switcher dialog Enabled
Asan address sanitize Disabled
Code Coverage Disabled
Check based tests Enabled
-------------------------------------
Now type 'make' to build

If you're missing some of these options, you'll need to wade through the configure log and figure out which libraries are missing.

$ make
...
$ sudo make install
...
$ which rofi
/usr/local/bin/rofi
$ rofi --help

Debuggable

If you're fighting issues, or want to see how things work, you can also build rofi with some debugging options. You can either replace the existing rofi or you can install them side-by-side using the --program-suffix installation option (which is what I do below). More information can be found in the official debugging docs

$ cd /desired/path/for/source/code/rofi/build
$ ../configure \
--enable-timings \
--enable-asan \
--enable-gcov \
--program-suffix='-debug'

...

-------------------------------------
Timing output: Enabled
Desktop File drun dialog Enabled
Window Switcher dialog Enabled
Asan address sanitize Enabled
Code Coverage Enabled
Check based tests Enabled
-------------------------------------
Now type 'make' to build

If you're missing some of these options, you'll need to wade through the configure log and figure out which libraries are missing.

I'd be lying if I said I fully understand the debug build. I had to first make without debug symbols, then remake with debug symbols. Without the initial make, a couple of important headers aren't built, and I wasn't able to trace how to make just those files (rofi has a fairly involved build process and I'm weak at best with the GNU build system).

$ make
...
$ make CFLAGS='-O0 -g3' clean rofi
...
$ sudo make install
...
$ which rofi
/usr/local/bin/rofi
$ which rofi-debug
/usr/local/bin/rofi-debug
$ rofi-debug --help

Easy Mode

I've collected everything in a simple installation script. Pull requests are absolutely welcome.

$ curl -fLo ./install-from-source https://raw.githubusercontent.com/thecjharries/posts-tooling-rofi/feature/post-01-overview-and-installation/scripts/install-from-source
$ chmod +x ./install-from-source

Its help provides a good overview:

$ ./install-from-source --help
usage: install-from-source [-h] [-d SOURCE] [-b BUILD] [-p INSTALL_PREFIX]
[--dry-run] [--skip-debug | -s DEBUG_SUFFIX]
[-u | -f]

Installs rofi from source

optional arguments:
-h, --help show this help message and exit
-d SOURCE, --source-directory SOURCE
source directory (default /opt/rofi)
-b BUILD, --build-directory BUILD
build directory (default /opt/rofi/build)
-p INSTALL_PREFIX, --prefix INSTALL_PREFIX
root installation directory (default /usr/local)
--dry-run dry run
--skip-debug skip the debuggable build
-s DEBUG_SUFFIX, --debug-suffix DEBUG_SUFFIX
debug suffix, (default -debug, e.g. rofi-debug)
-u, --update update using an existing clone
-f, --force wipes the source directory and clones a fresh copy

I used Python's APIs for directory manipulation, which means you'll have to run it as the user that owns the source and build directories. sudo is required for the default /opt/rofi location. This means the whole script is run as root. For example,

$ stat -c '%A %U %G' /opt
drwxr-xr-x root root
$ sudo ./install-from-source

If that makes you nervous (it should), you can build it in a temporary directory. You'll have to manage the source code yourself (e.g. updates).

$ ./install-from-source -d $(mktemp -d)

I'm a fan of the debug mode, so the script installs both side-by-side. It triples the installation size, bringing the total installation size to ~5M, up from ~1.5M.

$ find /usr/local -type f -name 'rofi*' -exec stat -c %s {} + \
| awk '{ total+=$1 }END{ print total }' \
| numfmt --to=iec
4.8M
$ find /usr/local -type f -name 'rofi*' -not -name '*debug' -exec stat -c %s {} + \
| awk '{ total+=$1 }END{ print total }' \
| numfmt --to=iec
1.5M

If that's too big, or you don't see yourself doing much debugging (honestly you probably won't), the --skip-debug flag will just install plain rofi without debug features.

Full Script

install-from-source
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
#!/usr/bin/env python

# pylint: disable=misplaced-comparison-constant,missing-docstring

from errno import EEXIST, ENOENT
from os import chdir, makedirs
from os.path import join
from shutil import rmtree
from subprocess import CalledProcessError, call
from sys import argv, exit as sys_exit

from argparse import ArgumentParser

# Directory for source code
DEFAULT_SOURCE_DIRECTORY = join('/', 'opt', 'rofi')
# Directory for build artifacts
DEFAULT_BUILD_DIRECTORY = join(DEFAULT_SOURCE_DIRECTORY, 'build')
# Debuggable executable suffix
DEFAULT_DEBUG_SUFFIX = '-debug'
# Root installation directory
DEFAULT_INSTALL_PREFIX = '/usr/local'

# Error codes from git
ERROR_NOT_A_REPO = 128
ERROR_NOT_EMPTY = 128

# Tooling dependencies
BUILD_DEPENDENCIES = [
'gcc',
'make',
'autoconf',
'automake',
'pkg-config',
'flex',
'bison',
'git',
]

# External libraries
EXTERNAL_DEPENDENCIES = [
'pango',
'cairo',
'glib2',
'librsvg2',
'libxkbcommon',
'libxkbcommon-x11',
'libxcb',
'startup-notification',
'xcb-util',
'xcb-util-wm',
'xcb-util-xrm',
'check',
]

# External libraries + headers
DEVEL_DEPENDENCIES = EXTERNAL_DEPENDENCIES + [
dependency + '-devel' for dependency in EXTERNAL_DEPENDENCIES
]

# Debug-only dependencies
DEBUG_DEPENDENCIES = [
'libasan'
]


def run_commands(commands, dry_run=False):
"""Executes a list of commands via call"""
for command in commands:
print ' '.join(command)
if not dry_run:
call(command)


def cd(directory_path, dry_run=False): # pylint: disable=invalid-name
"""Change directory"""
if dry_run:
print "cd %s" % directory_path
else:
chdir(directory_path)


def wipe_directory(directory_path, dry_run=False):
"""Wipes a directory"""
try:
if dry_run:
print "rm -rf %s" % directory_path
else:
rmtree(directory_path)
except OSError as error:
if ENOENT == error.errno:
pass
else:
raise


def create_directory(directory_path, dry_run=False):
"""Creates a directory"""
try:
if dry_run:
print "mkdir -p %s" % directory_path
else:
makedirs(directory_path)
except OSError as error:
if EEXIST == error.errno:
pass
else:
raise


def review_commands(options):
"""Provides a quick review of everything installed"""
commands = [
['which', 'rofi'],
['rofi', '-version']
]
if not options.skip_debug:
commands.append(['which', "rofi%s" % options.debug_suffix])
commands.append(["rofi%s" % options.debug_suffix, '-version'])
run_commands(commands, options.dry_run)


def prep_build(options):
cd(options.source, options.dry_run)
wipe_directory(options.build, options.dry_run)
create_directory(options.build, options.dry_run)


def build(options, is_debuggable=False):
"""Builds the rofi executable"""
prep_build(options)
cd(options.build, options.dry_run)
commands = []
if is_debuggable:
commands.append([
options.configure,
"--prefix='%s'" % (options.install_prefix),
'--enable-timings',
'--enable-asan',
'--enable-gcov',
"--program-suffix='%s'" % (options.debug_suffix),
])
else:
commands.append([
options.configure,
"--prefix='%s'" % (options.install_prefix),
])
commands.append(['make'])
if is_debuggable:
commands.append(['make', 'CFLAGS=\'-O0 -g3\'', 'clean', 'rofi'])
commands.append(['sudo', 'make', 'install'])
run_commands(commands, options.dry_run)


def build_and_install(options):
"""Builds and installs rofi (and possibly rofi-debug)"""
build(options)
if not options.skip_debug:
build(options, True)


def restore_tooling(options):
"""Builds missing build files"""
commands = [['autoreconf', '-i']]
run_commands(commands, options.dry_run)


def clone_repo(dry_run=False):
"""Clones the repo"""
commands = [[
'git',
'clone',
'https://github.com/DaveDavenport/rofi',
'--recursive',
'.'
]]
run_commands(commands, dry_run)


def update_existing_repo(dry_run=False):
"""Attempts to update. Falls back to clone if repo DNE"""
try:
call(['git', 'status'])
except CalledProcessError as error:
if ERROR_NOT_A_REPO == error.returncode:
return clone_repo(dry_run)
else:
raise
commands = [
['git', 'stash'],
['git', 'reset', '--hard'],
['git', 'pull'],
['git', 'submodule', 'init'],
['git', 'submodule', 'update'],
]
return run_commands(commands, dry_run)


def refresh_source(options):
"""Refreshes the source code via update or clone"""
cd(options.source, options.dry_run)
if options.update:
update_existing_repo(options.dry_run)
else:
clone_repo(options.dry_run)


def prep_source_directory(options):
"""Preps the source directory"""
if not options.update:
wipe_directory(options.source, options.dry_run)
create_directory(options.source, options.dry_run)


def install_dependencies(options):
"""Installs necessary system packages"""
commands = [
['sudo', 'dnf', 'install', '-y'] + BUILD_DEPENDENCIES,
['sudo', 'dnf', 'install', '-y'] + DEVEL_DEPENDENCIES,
]
if not options.skip_debug:
commands.append(
['sudo', 'dnf', 'install', '-y'] + DEBUG_DEPENDENCIES
)
run_commands(commands, options.dry_run)


def parse_argv(args=None):
"""Parses CLI args"""
if args is None:
args = argv[1:]
parser = ArgumentParser(
description='Installs rofi from source'
)
parser.add_argument(
'-d', '--source-directory',
dest='source',
default=DEFAULT_SOURCE_DIRECTORY,
help="source directory (default %s)" % DEFAULT_SOURCE_DIRECTORY
)
parser.add_argument(
'-b', '--build-directory',
dest='build',
default=None,
help='build directory (default <source path>/build)'
)
parser.add_argument(
'-p', '--prefix',
dest='install_prefix',
default=DEFAULT_INSTALL_PREFIX,
help="root installation directory (default %s)" % DEFAULT_INSTALL_PREFIX
)
parser.add_argument(
'--dry-run',
dest='dry_run',
action='store_true',
help='dry run'
)
debug_opts = parser.add_mutually_exclusive_group()
debug_opts.add_argument(
'--skip-debug',
dest='skip_debug',
action='store_true',
help='skip the debuggable build'
)
debug_opts.add_argument(
'-s', '--debug-suffix',
dest='debug_suffix',
default=DEFAULT_DEBUG_SUFFIX,
help="debug suffix, (default %s, e.g. rofi%s)" % (
DEFAULT_DEBUG_SUFFIX,
DEFAULT_DEBUG_SUFFIX
)
)
refresh_opts = parser.add_mutually_exclusive_group()
refresh_opts.add_argument(
'-u', '--update',
dest='update',
action='store_true',
help='update using an existing clone'
)
refresh_opts.add_argument(
'-f', '--force',
dest='force',
action='store_true',
help='wipes the source directory and clones a fresh copy'
)
options = parser.parse_args(args)
if options.build is None:
options.build = join(options.source, 'build')
return options


def cli():
"""Bootstraps the script"""
options = parse_argv()
setattr(options, 'configure', join(options.source, 'configure'))
install_dependencies(options)
prep_source_directory(options)
refresh_source(options)
restore_tooling(options)
build_and_install(options)
review_commands(options)
sys_exit(0)

if '__main__' == __name__:
cli()

CJ Harries

I did a thing once. Change "blog." to "cj@" and you've got my email. All these opinions are mine and might not be shared by clients or employers.

Read More