#fzf #bindings #cli-tool #api-bindings #api

fzf-wrapped

A library for integrating the fzf cli tool into your rust program!

4 releases

0.1.3 Nov 11, 2023
0.1.2 Nov 11, 2023
0.1.1 Nov 10, 2023
0.1.0 Nov 3, 2023

#180 in GUI

49 downloads per month
Used in 2 crates

MIT license

39KB
317 lines

fzf-wrapper

A library for integrating the fzf cli tool into your rust program!

This library provides bindings for integrating the fzf cli program into your rust projects.

NOTE this does mean that the end user must have fzf installed on their system.

fzf version

This crate was developed with fzf v0.40.0 in mind, however there should be no reason why it shouldn't work above it, and most of the features should work below it. If your program relies on v0.40.0 features, it might be a good idea to check the version of fzf your program has access to

Example

Say we're wanting to get the user to select their favourite colour using the power of fuzzy finding. First we'd create a list of colours, which will be a collection we'll pass to fzf

let colours = vec!["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

The next step is to construct an instance of [Fzf] and start it:

let mut fzf = Fzf::default();

fzf.run().expect("Failed to start fzf");

The code above fetches the default [Fzf] configuration, which runs fzf with no arguments, and then calls the run() method. This displays fzf to the user.

At the moment all that will be displayed is a blank screen, as we haven't actually told fzf to display anything. There are two ways to do this, the add_item() method, and the add_items() method. They are both nearly identical, with the only difference being that add_items() takes a [Vec] of String's as items, and passes them one by one to add_item().

let mut fzf = Fzf::default();
fzf.run().expect("Failed to start fzf");
fzf.add_items(colours).expect("Failed to add items");

The only thing left to do is to get what the user selected! This will be returned as an Option containing None if the user exited fzf, or Some(String) with the string being the item they selected. To get the output we simply call the output() method, which will blocks execution until the user selects an item with fzf

let mut fzf = Fzf::default();
fzf.run().expect("Failed to start fzf");
fzf.add_items(colours).expect("Failed to add items");
let users_selection = fzf.output().expect("Failed to get the user's output");

The code in it's entirety looks like the following.

use fzf_wrapped::Fzf;

fn main() {
    let colours = vec!["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
    
    let mut fzf = Fzf::default();
    fzf.run().expect("Failed to start fzf");

    fzf.add_items(colours).expect("Failed to add items");

    let users_selection = fzf.output().expect("Failed to get the user's output");
}

This operation of using fzf to select from a predetermined [Vec] of items is so common that a helper function exists to streamline the work involved. Using it, the code looks like the following:

use fzf_wrapped::Fzf;
use fzf_wrapped::run_with_output;

fn main() {
    let colours = vec!["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
    
    let users_selection = run_with_output(Fzf::default(), colours).expect("Something went wrong!");
}

Running fzf with arguments

Now, our favourite color picker program is pretty cool, but our fzf interface is a bit confusing, how are our user's supposed to know what they're picking?

Thankfully fzf provides many different ways to customize it, many of which have been implemented in this library. So far we have been calling the [Fzf] structs default implementation, however we can build more complex instances using FzfBuilder.

We can use two different ways to get an FzfBuilder, either through it's own new() method, or the builder() method on [Fzf]. Let's switch out the default call for a builder call.

let fzf = Fzf::builder().build().unwrap();

let users_selection = run_with_output(fzf, colours).expect("Something went wrong!");

NOTE: It is safe to unwrap the build() call, as every field has a default implementation.

If we run the program now, we'll notice... nothing has changed. This is because the default() method calls the exact line of code we just replaced it with.

Let's make things interesting! First we should probably give the finder a label. Without a border, the label won't show so lets give it a border too!

Adding a border and border label

If you were using fzf straight from the command line, you would pass it the --border flag with the name of one of the supported types of borders, however this can lead to errors if the border doesn't exist. For this reason, fzf-wrapped makes use of enums to ensure that these choices are always valid! For example, to choose a border we'd call the border() method on our FzfBuilder, contain our chosen variant of the Border enum.

Adding a rounded border makes our code look like this:

use fzf_wrapped::Fzf;
use fzf_wrapped::Border;
use fzf_wrapped::run_with_output;

fn main() {
    let colours = vec!["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

    let fzf = Fzf::builder()
        .border(Border::Rounded)
        .build()
        .unwrap();
    
    let users_selection = run_with_output(fzf, colours);
}

And adding a label is even more simple

use fzf_wrapped::Fzf;
use fzf_wrapped::Border;
use fzf_wrapped::run_with_output;

fn main() {
    let colours = vec!["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

    let fzf = Fzf::builder()
        .border(Border::Rounded)
        .border_label("Favourite Colour")
        .build()
        .unwrap();
    
    let users_selection = run_with_output(fzf, colours);
}

Running this should display an fzf finder with a rounded border, and a centered label containing "Favourite Colour".

Changing the layout of fzf

Well, now that we've got a border, we may as well change up the layout. This is almost identical to changing the border, as all possible layout's are mapped to an enum.

All we need to add to our builder is the layout() method with our chosen Layout variant

use fzf_wrapped::Fzf;
use fzf_wrapped::{Border, Layout};
use fzf_wrapped::run_with_output;

fn main() {
    let colours = vec!["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

    let fzf = Fzf::builder()
        .layout(Layout::Reverse)
        .border(Border::Rounded)
        .border_label("Favourite Colour")
        .build()
        .unwrap();
    
    let users_selection = run_with_output(fzf, colours);
}

Choosing colours

fzf lets us pick from a few colour themes, but for this one we're going to keep it simple and use the black and white theme. Similar to borders and the layout, this is selected using an enum. Adding it to our builder results in the following code:

use fzf_wrapped::Fzf;
use fzf_wrapped::{Border, Color, Layout};
use fzf_wrapped::run_with_output;

fn main() {
    let colours = vec!["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

    let fzf = Fzf::builder()
        .layout(Layout::Reverse)
        .border(Border::Rounded)
        .border_label("Favourite Colour")
        .color(Color::Bw)
        .build()
        .unwrap();
    
    let users_selection = run_with_output(fzf, colours);
}

If you run the program now, you'll notice that the ui is in black and white!

Adding a header

Our user might still be confused about what they're picking, so to add some more context, fzf lets us set a header. To do this all we do is call the header() method on our FzfBuilder struct, and pass it anything with the Into<String> trait. We also want the header to appear above our search field, so we'll call the header_first() method with true.

use fzf_wrapped::Fzf;
use fzf_wrapped::{Border, Color, Layout};
use fzf_wrapped::run_with_output;

fn main() {
    let colours = vec!["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

    let fzf = Fzf::builder()
        .layout(Layout::Reverse)
        .border(Border::Rounded)
        .border_label("Favourite Colour")
        .color(Color::Bw)
        .header("Pick your favourite colour")
        .header_first(true)
        .build()
        .unwrap();
    
    let users_selection = run_with_output(fzf, colours);
}

Using an argument not supported by fzf_wrapped

If by chance the argument you want to run with fzf is not a method included on the [Fzf] struct, do not worry! The custom_args() command will let you pass any argument you want! For example, say we wanted to make our colour picker program not take up the full screen, say only 10% of it. fzf has the --height= flag, however the [Fzf] struct doesn't support it! To add it all we'll need to do is to call the custom_args() command on our builder, and the arguments we pass into it will be run with the run() method.

Implementing it would look like:

use fzf_wrapped::Fzf;
use fzf_wrapped::{Border, Color, Layout};
use fzf_wrapped::run_with_output;

fn main() {
    let colours = vec!["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

    let fzf = Fzf::builder()
        .layout(Layout::Reverse)
        .border(Border::Rounded)
        .border_label("Favourite Colour")
        .color(Color::Bw)
        .header("Pick your favourite colour")
        .header_first(true)
        .custom_args(vec!["--height=10".to_string()])
        .build()
        .unwrap();
    
    let users_selection = run_with_output(fzf, colours);

    if let Some(colour) = users_selection {
        println!("{} is an awesome colour!", colour);
    }
}

And with that, our program is almost done!

All we need to do is print some kind of nice message, and while we're at it, we may as well use some proper error handling.

use fzf_wrapped::Fzf;
use fzf_wrapped::{Border, Color, Layout};
use fzf_wrapped::run_with_output;

fn main() {
    let colours = vec!["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

    let fzf = Fzf::builder()
        .layout(Layout::Reverse)
        .border(Border::Rounded)
        .border_label("Favourite Colour")
        .color(Color::Bw)
        .header("Pick your favourite colour")
        .header_first(true)
        .custom_args(vec!["--height=10".to_string()])
        .build()
        .unwrap();
    
    let users_selection = run_with_output(fzf, colours);

    if let Some(colour) = users_selection {
        println!("{} is an awesome colour!", colour);
    }
}

Adding Items at runtime of fzf

With the power of this library, you can use fzf to select from a list of items, even if those items have not been fetched yet. Using the add_item and add_items method adds items to fzf's list, even while fzf is running. This means that if you're calling information from a REST api, you can display result's as they come in straight to fzf, or even hide the slight delay by starting up fzf.

For an example of this, look at my workflows project

Dependencies

~2MB
~44K SLoC