Using zf as a library
November 11, 2025 ยท View on GitHub
zf is offered as both a Zig module and a C library. zf is allocation free and expects the caller to handle any required allocations.
To add to your project run
zig fetch --save git+https://github.com/natecraddock/zf
Then in your build.zig file you can use the dependency.
pub fn build(b: *std.Build) void {
// ... snip ...
const ziglua = b.dependency("zf", .{
.target = target,
.optimize = optimize,
.with_tui = false,
});
// ... snip ...
// add the zf module
exe.root_module.addImport("zf", ziglua.module("zf"));
}
In your code zf will be available with @import("zf")
See the source for documentation on each function.
Usage details
There are a few things that zf expects you to follow when using it as a library
The zf API is designed to offer maximum performance. This means the API leaves some decisions to the caller like allocation and tokenizing the input query.
Function types
The library offers functions two types of functions. One that ranks a slice of needles, and one that ranks a single needle:
rank()andhighlight()rank and highlight a haystack against a slice of needles.rankNeedle()andhighlightNeedle()operate on a single needle
Case sensitivity
Set case_sensitive to false to enable case insensitive ranking.
Plaintext matching
The high-level rank() and highlight() functions accept a boolean plain parameter. When true filename computations are bypassed.
The filename parameter
The rankNeedle() and highlightNeedle functions accept a filename as a parameter. The filename should be set to null when there is no filename. The reason the library functions do not do this for you is again for efficiency reasons. This would be expensive to compute for each given needle. It is expected that the caller will provide the filename when doing the ranking one needle at a time.
To disable filename matching (for plaintext haystacks or for more efficiency) null can be passed as the filename.
Range highlighting
Range highlighting is provided as a separate function and not done in the ranking. The reason it is a separate function is that range calculations are more expensive to compute. Because the normal case is that only a small portion of all input lines are shown in a UI, ranking is done separately to keep things performant.
This also makes ranking more performant for callers who do not need range highlight information.
Example
const std = @import("std");
const zf = @import("zf");
pub fn main() void {
// Strings we want to rank
const strings = [_][]const u8{
"src/tui/EditBuffer.zig",
"src/tui/Previewer.zig",
"src/tui/array_toggle_set.zig",
"src/tui/input.zig",
"src/tui/main.zig",
"src/tui/opts.zig",
"src/tui/ui.zig",
"src/zf/clib.zig",
"src/zf/filter.zig",
"src/zf/zf.zig",
};
// Ranking based on a single needle
const query = "tui";
for (strings) |str| {
const rank = zf.rankNeedle(str, query, .{});
std.debug.print("str: {s} rank: {?}\n", .{ str, rank });
}
std.debug.print("\n", .{});
// Ranking based on multiple needles
const needles = [_][]const u8{ "tui", "Pr" };
for (strings) |str| {
const rank = zf.rank(str, &needles, .{});
std.debug.print("str: {s} rank: {?}\n", .{ str, rank });
}
}
Look at the zf TUI source code for more examples on how to use the module.
C
See the source code of telescipe-zf-native.nvim for a good example of using zf ranking from C.