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.