This is the fourth 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 looks at changing rofi
’s window location. It also introduces some rofi
dmenu
usage to handle input and ends with a introduction to script
modi
.
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.
You’re going to need a newer version of rofi
, >=1.4
. I’m currently running this:
$ rofi -version |
If you installed from source, you should be good to go.
Code
You can view the code related to this post under the post-04-change-window-location
tag.
Window Location
By default, rofi
launches dead-center of the owning screen.
$ rofi -show run |
There’s a config option, location
, that allows us to change that position. We can instead place the launcher on any of the cardinals, any of the ordinals, or dead center. The locations follow a pattern like this:
1 2 3 |
Manipulating the location doesn’t require much effort.
$ sed \ |
--- $XDG_USER_CONFIG_DIR/rofi/config.rasi.bak |
Scripted
However, manually running sed
every time isn’t that fun. We should write something.
Basic CLI Location Changer
The first thing we’ll need to do is detect the current location for comparison. Once again, awk
is very useful. We’ll need to remove comment characters, if the option isn’t set yet, and we’ll want to strip semicolons to make grabbing easier.
current-location | |
1 2 3 4 5 | /\slocation:/ { |
$ rofi -dump-config \ |
Next we’ll need to enumerate the directions. I spent a massive amount of time thinking about this last night, and I haven’t been able to come up with anything more clever than some half-hearted expansion and associative arrays. It’s a very interesting problem, and I’ll probably come back to it.
directions | |
1 2 3 4 5 6 7 | DIRECTIONS=(c n{w,,e} e s{e,,w} w) |
This will allow us to find the direction with an index via DIRECTIONS
or the index with a direction via DIRECTION_INDICES
.
Somehow we’ve got to pass a location to the script. argv
never hurt anyone, so we’ll go that route. However, if there’s one thing you should never do, it’s trust your users. We’ll need to sanitize and munge the input. Once again, awk
is a great tool.
directions | |
1 2 3 4 | DESIRED_LOCATION_KEY=$( |
The first thing we should do is ensure the string contains only the things we’re interested in.
parse-location-input | |
1 2 3 4 | { |
With a clean input, we should look for easy strings. [ns]o[ru]th
leads six of the compass points, so stripping those is a good idea. awk
’s regex is fairly limited, but we can run basic capture groups via match
. If input
begins with [ns]
, we’ll snag it and clean input
before moving on. If it doesn’t, we’ll set result
to the empty string to make combos easier.
parse-location-input | |
1 2 3 4 5 6 7 8 9 | ... |
The capture group logic is the same for the remaining cardinals. However, we’ve got to glue things together now, as the ordinals look like [ns][ew]
. That’s why we dropped a blank result
above.
parse-location-input | |
1 2 3 4 5 6 | ... |
After attempting to capture the directions, result
will only be empty if
center
was passed, or- we couldn’t process and sanitize the input.
We can kill two birds with one stone by providing a default c
result.
parse-location-input | |
1 2 3 4 5 | ... |
Finally, we need to send off result
.
parse-location-input | |
1 2 3 | END { |
We can easily convert text directions to the proper index via the arrays we built above.
directions | |
1 | DESIRED_LOCATION="${DIRECTION_INDICES[$DESIRED_LOCATION_KEY]}" |
With the new location, we can finally update the config.
directions | |
1 2 3 4 5 6 7 8 9 | sed \ |
CLI Location Changer with GUI
While this will run beautifully, we’ve completely ignored a very useful tool. rofi
can, with minimal config, build very simple menus to make interaction easier.
The first thing we’ll need to do is build a human-readable list of options.
directions-gui | |
1 2 3 4 5 6 7 8 9 10 11 | FULL_DIRECTIONS=( |
It would also be useful if the user knew which location
was currently active. We can modify the DIRECTION_INDICES
for
loop to do just that. On a related note, it would also be much nicer for the active option to be the first in the list in case the user changes their mind quickly. We can accomplish that with a simple swap.
directions-gui | |
1 2 3 4 5 6 7 8 9 10 | for index in "${!DIRECTIONS[@]}"; do |
While we’re building a GUI (sorta), we don’t want to remove the CLI. The goal is to build something that works together in tandem. If the script is called with an argument, we’ll try to parse it. Otherwise, we’ll launch rofi
.
directions-gui | |
1 2 3 4 5 | if [[ -n "$1" ]]; then |
The first thing we have to do is print the array (I use printf
; I can never get echo
to do what I want). rofi
will then consume that (via /dev/stdout
) to construct its GUI list. I’ve added a few style things that you can ignore. You really only need to pipe something into rofi -dmenu
; everything else is just window-dressing.
directions-gui | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | INPUT=$( |
Full Location Changer
location-changer | |
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 | #!/bin/bash |
It’s very simple to use. Like rofi
, it defaults to the center position.
$ scripts/location-changer n |
--- $XDG_USER_CONFIG_DIR/rofi/config.rasi.bak |
$ scripts/location-changer qqq |
--- $XDG_USER_CONFIG_DIR/rofi/config.rasi.bak |
The GUI provides an alternate way to get at things.
$ scripts/location-changer |
--- $XDG_USER_CONFIG_DIR/rofi/config.rasi.bak |
Location Changer modi
Taking what we’ve learned, we should be able to build a script
modi
capable of updating the window location. Essentially, a script
modi
is a never-ending pipe. rofi
launches the script, the user interacts, and the script finishes. Its output is then piped back into the original script to run again. It will run until an external close action (e.g. Esc
) is fired or the script sends nothing out on /dev/stdout
.
Create a script
modi
Like before, we’ll want to start with a list of options. I wanted to include an exit option this time around. We’ll also need to parse the current location
for comparison.
location-changer-modi | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | FULL_DIRECTIONS=( |
I was a bit tidier this time around, and threw the setup into a function. We’ll update the option list and print the options, just like before.
location-changer-modi | |
1 2 3 4 | function rebuild_directions { |
We’ll want to run that no matter what to keep things fresh. However, we won’t want to update the config unless the location changes.
location-changer-modi | |
1 2 3 4 5 6 7 8 9 10 11 12 13 | if [[ ! -z "$@" ]]; then |
This works quite well. As the user interacts, the config gets updated. It does what it says on the tin. Like this:
On the surface, that looks awesome. However, if you look closely, the location
is dead center but rofi
is reporting East
is active. This presents a very interesting problem with script
modi
. Because they’re pipes, rofi
isn’t reloading each time. The modi
can’t call rofi
again, because it’s already running. More importantly, even if it could, it’s going to lose the original command, which could contain extra configuration.
Process-Spawning modi
I spent a decent chunk of time beating my head against this, and then I realized that rofi
stores its pid
. We can access the pid
file via the config, which in turn gives us access to all the information we need. Before I get to the exciting stuff, though, it’s important to mention safety. It’s a really good idea to limit your process count (somehow) in case you create a runaway script. Speaking from experience, it could be half an hour before you can free up enough memory to switch to another tty
and kill everything.
location-changer-respawning-modi | |
1 2 3 4 5 | if [[ 10 -lt $(pgrep -c -f "$0") ]]; then |
Using awk
, we can set up variables that are immediately consumed by eval
.
location-changer-respawning-modi | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | eval $( |
Why exactly do we need the pid
? It’s so we can duplicate the currently running script with all its arguments.
$ ps --no-headers -o command -p $(cat "$ROFI_PID") |
The command by itself isn’t going to do us very much good. Attempting to run rofi
from inside a script
modi
hits the process lock. (I supposed we could unlock it, but that’s a whole new can of bugs to crush.) Happily enough, we can dump the command out to another script and execute in the background to refresh rofi
.
location-changer-respawning-modi | |
1 2 3 4 5 6 7 8 9 10 11 12 | function create_and_spawn_runner { |
Finally, we need to update some of the parsing logic. If the location
changes, we’ll need to spawn a new process and exit instead of continuing along.
location-changer-respawning-modi | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | if [[ ! -z "$@" ]]; then |
Consuming script
modi
script
modi
are listed in config options as <prompt>:<path>
. You can add them to the modi
or combi-modi
lists. I’d recommend creating a directory for script
s to keep things organized. I did this:
$ mkdir -p $XDG_USER_CONFIG_DIR/rofi/scripts |
If you don’t want it in the combi
, strip that logic.
Full window-location
modi
rofi-location-changer | |
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 | #!/bin/bash |