rofi: Matching and Sorting

This is the fifth 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.

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:

Code

Overview

For the most part, rofi sorts via Levenshtein distance. In a nutshell, the Levenshtein distance counts the number of changes necessary to go from one string to another. You can view rofi's implementation. It's utf-8-aware, which is very nice.

As for the other matching methods, they seem to be vanilla. While there can be fairly substantial differences in regex and glob implementations across different languages, I haven't been negatively affected by rofi's spin yet (I've been too busy writing other things to really delve deep). rofi's going to be better sometimes and worse other times. Using the Levenshtein distance makes things pretty easy to use.

Comparison

I've made a quick chart to (quite briefly) highlight the differences. I just looked at sorting, Levenshtein distance, and fuzzy matching. regex and glob matching are too specialized to easily throw in a simple chart like this.

I used this script to compile all the information (plus a bunch more).

for matching_state in "${MATCHING[@]}";dofor sort_state in "${FLAG_STATES[@]}";dofor levenshstein_state in "${FLAG_STATES[@]}";do execute_stage "$matching_state""$sort_state""$levenshstein_state"donedonedone

Basic Sort Config

Personally, I find it useful to have things at least minimally sorted as I'm going.

However, the matching method is very much situational. normal should work most of the time. fuzzy might be better, but I don't have any metrics so I'm not sure how it affects performance and all that. glob is amazing when moving files around, but might not be very useful when attempting to drun. You could always run regex but then no one else would want to read your scripts and you'd turn to a life of blogging like me.

defreview_diff():"""Compares the config before and after running"""withNamedTemporaryFile()assuppressed_output:result_pipe=Popen(['diff','--color=always','--unified=0','-t','--tabsize=4',"%s.bak"%CONFIG_FILE,CONFIG_FILE],stdout=suppressed_output,)result_pipe.communicate()suppressed_output.seek(0)result=suppressed_output.read()print(result)

defvalidate_item(item_possibilities,defaults_key,desired_value):""" Validates the passed-in value. On failure, validates the current default. On failure, returns the first possibility. """ifdesired_valueinitem_possibilities:returndesired_valuedesired_value=DEFAULTS[defaults_key]ifdesired_valueinitem_possibilities:returndesired_valuereturnitem_possibilities[0]

defcreate_choices(all_choices,current_choice):"""Creates a list of choices for dmenu"""choices=["%s%s"%(current_choice,ACTIVE_CHOICE_IDENTIFIER)]all_choices.sort()forchoiceinall_choices:ifcurrent_choice!=choice:choices.append(choice)returnchoices

deffind_desired_item(item_possibilities,defaults_key):"""Builds the dmenu list, polls the user, and validates the result"""choices=create_choices(item_possibilities,DEFAULTS[defaults_key])value=pipe_choices_to_rofi(choices,defaults_key)returnvalidate_item(item_possibilities,defaults_key,value)

defparse_and_update_desired_item(item_possibilities,defaults_key,options):""" Attempts to use passed-in values when possible. Otherwise polls the user for each value via dmenu (via rofi) """desired_value=getattr(options,defaults_key,None)if(notdesired_valueordesired_value!=validate_item(item_possibilities,defaults_key,desired_value)):desired_value=find_desired_item(item_possibilities,defaults_key)update_config(defaults_key,desired_value)

defbackup_config():"""Backs up the current config"""copy(CONFIG_FILE,CONFIG_FILE+'.bak')

defcli():"""Bootstraps the app"""load_config()backup_config()options=parse_argv()ifgetattr(options,'only',None):forkey,valueinDEFAULTS.iteritems():ifkey!=options.only:setattr(options,key,value)determine_everything(options)ifnotoptions.skip_diff:review_diff()sys_exit(0)

if'__main__'==__name__:cli()

Change Matching and Sorting Via a modi

After the last post's modi, I wanted to see if I could repeat something similar here. This script is still pretty raw, but it gets the job done. Total GUI, like before.

defprint_and_exit(options):"""Dumps everything to STDOUT for rofi"""foroptioninoptions:print(option)clean_exit()

defspawn_and_die():"""Creates a new process and dies"""runner=create_new_runner()Popen(['bash','-c',"coproc %s"%runner])clean_exit()

defparse_pid():"""Snags the original rofi command"""withopen(DEFAULTS['pid'],'r')aspid_file:process_id=pid_file.read().strip()result=check_output(['ps','--no-headers','-o','command','-p',"%s"%process_id])returnresult.strip()