Fuzz: interactively select Ruby objects with rofi

I often write scripts in which a user needs to choose one of a number of possibilities: maybe they need to select a directory, or a file, or an action, or just an arbitrary object.

rofi is a really cool tool for that! It provides a visual way for a user to use fuzzy searching to choose among a selection of strings.

Unfortunately, though, it does just choose between strings. But my scripts would often be a lot simpler if the user were able to select arbitrary Ruby objects. Fuzz manages the translation between lists of strings that can be selected through rofi and the associated collection of Ruby objects, and thus makes it a bit easier to write interactive and OOP-friendly Ruby scripts.

For example

I'm a fan of the excellent RubyTapas screencasts, and I'd like a script to let me interactively search through their titles to pick one to play. Here's a way I could do that:

require "fuzz"

# Instantiate some File objects:
episodes = Dir.glob("~/ruby_tapas_episodes/*")

# Have the user pick one with rofi:
choice = Fuzz::Selector.new(episodes).pick

# Play the selected file with VLC:
system("vlc \"#{ choice.path }\"")

The call to #pick will call #to_s on every episode, display the results through rofi, get the user's choice, and use that to return the corresponding object.

Caching selections

If you run your script frequently, you may find that you often make the same selections. It's convenient to have those selections appear near the top of the list.

Fuzz can maintain a script-specific record of your previous selections and order your options from most to least popular. Just include an optional :cache argument when creating a Fuzz::Selector:

Fuzz::Selector.new(
  some_objects,
  cache: Fuzz::Cache.new("~/.cache/fuzz/my_script_cache"),
)

You'll want to use a different file for each script. Using the same cache file for different scripts will probably yield weird results.

I think ~/.cache/fuzz/ is a nice directory for your personal script caches, and it complies with the XDG Base Directory Specification, but you can keep 'em wherever you'd like.

Extending fuzz beyond rofi

It's possible to use fuzz without rofi. The Fuzz::Selector constructor takes an optional :picker argument. The supplied object must implement a #pick method, which should take an array of strings and return a string.

Here's a simple example with a silly picker that always chooses the first option:

def PickFirst
  def pick(choices)
    choices.first
  end
end

selector = Fuzz::Selector.new(
  [1, 2, 3, 4, 5],
  picker: PickFirst.new,
)

selector.pick # => 1

Pickers will usually be more interactive than this, I hope! You might shell out to dmenu, pick, or whatever else you'd like.

Installation

Add this line to your application's Gemfile:

gem "fuzz"

And then execute:

$ bundle

Or install it yourself as:

$ gem install fuzz

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/hrs/fuzz. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

Code of Conduct

Everyone interacting in the Fuzz project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.