Something went wrong. Try again.
A GPU terminal emulator for Wayland and Windows on libghostty-vt, drawing with Vulkan, running a program on a pty or a serial port.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Finding things by what is typed, for shell search and the profile//! selector alike, with zf's fuzzy matching.//!//! What is typed is split into words on whitespace, and every word must be//! found: its letters in order, not necessarily together, ignoring case —//! `rtr` finds `router`. A thing is known by two fields, the one it is//! named by and another — a pane's title and its location, a profile's//! name and what it runs — and each word is looked for in either. A word//! found in the first field counts for more than one found only in the//! second, whatever zf ranks them; among those equal in that, zf's rank//! orders them, letters together and at the start of a word counting for//! more. A field that is a path is ranked as one, a match in its last//! component counting for more.
const std = @import("std");const zf = @import("zf");
/// One of the two things a thing is known by.pub const Field = struct { text: []const u8, /// Ranked as a path: a match in its last component counts for more. path: bool = false,};
/// How well what was typed matches a thing.pub const Score = struct { /// How many words were found only in the second field. fallbacks: u32, /// zf's rank, summed over the words: lower is better. rank: f64,
/// Whether `a` matches better than `b`. pub fn better(a: Score, b: Score) bool { if (a.fallbacks != b.fallbacks) return a.fallbacks < b.fallbacks; return a.rank < b.rank; }};
/// The most words a query is split into; more are left out.pub const max_words = 16;
/// `query`'s words, in `buf`.pub fn words(query: []const u8, buf: *[max_words][]const u8) []const []const u8 { var n: usize = 0; var it = std.mem.tokenizeAny(u8, query, " \t\r\n"); while (it.next()) |w| { if (n == buf.len) break; buf[n] = w; n += 1; } return buf[0..n];}
/// How well `ws` match a thing known by `first` and `second`; null when/// one of them is in neither, or there are none.pub fn score(ws: []const []const u8, first: Field, second: Field) ?Score { if (ws.len == 0) return null; var s: Score = .{ .fallbacks = 0, .rank = 0 }; for (ws) |w| { if (rank(w, first)) |r| { s.rank += r; } else if (rank(w, second)) |r| { s.rank += r; s.fallbacks += 1; } else return null; } return s;}
fn rank(word: []const u8, f: Field) ?f64 { if (f.text.len == 0) return null; return zf.rankNeedle(f.text, word, options(word, f));}
fn options(word: []const u8, f: Field) zf.RankNeedleOptions { return .{ .case_sensitive = false, .filename = if (f.path) std.fs.path.basename(f.text) else null, .strict_path = f.path and std.mem.findScalar(u8, word, '/') != null, };}
/// Where in each field the words were found: the byte offsets of the/// letters matched, ascending, in `first_buf` and `second_buf`. A word is/// shown where `score` counted it, in the first field if it is there.pub fn highlight( ws: []const []const u8, first: Field, second: Field, first_buf: []usize, second_buf: []usize,) struct { first: []const usize, second: []const usize } { var nf: usize = 0; var ns: usize = 0; for (ws) |w| { if (rank(w, first) != null) { nf += zf.highlightNeedle(first.text, w, first_buf[nf..], options(w, first)).len; } else if (rank(w, second) != null) { ns += zf.highlightNeedle(second.text, w, second_buf[ns..], options(w, second)).len; } } return .{ .first = sortedUnique(first_buf[0..nf]), .second = sortedUnique(second_buf[0..ns]) };}
fn sortedUnique(xs: []usize) []const usize { std.mem.sortUnstable(usize, xs, {}, std.sort.asc(usize)); var n: usize = 0; for (xs) |x| { if (n > 0 and xs[n - 1] == x) continue; xs[n] = x; n += 1; } return xs[0..n];}
const testing = std.testing;
fn scoreOf(query: []const u8, first: []const u8, second: Field) ?Score { var buf: [max_words][]const u8 = undefined; return score(words(query, &buf), .{ .text = first }, second);}
test "every word must be found, its letters in order, ignoring case" { const loc: Field = .{ .text = "~/dev/harrier", .path = true }; try testing.expect(scoreOf("vim", "vim README.md", loc) != null); try testing.expect(scoreOf("VM rdme", "vim README.md", loc) != null); try testing.expect(scoreOf("hrr", "top", loc) != null); try testing.expect(scoreOf("vim emacs", "vim README.md", loc) == null); try testing.expect(scoreOf("mv", "vim", .{ .text = "" }) == null); try testing.expect(scoreOf("", "vim", loc) == null); try testing.expect(scoreOf(" \t", "vim", loc) == null);}
test "a word found in the first field beats one found only in the second" { // "nas" closely in the second, loosely in the first: the first wins. const loose = scoreOf("nas", "n-a-s", .{ .text = "nas" }).?; const close = scoreOf("nas", "router", .{ .text = "ssh nas" }).?; try testing.expectEqual(@as(u32, 0), loose.fallbacks); try testing.expectEqual(@as(u32, 1), close.fallbacks); try testing.expect(loose.better(close)); try testing.expect(!close.better(loose));}
test "letters together and at the start rank better" { const together = scoreOf("rout", "router", .{ .text = "" }).?; const apart = scoreOf("rout", "rebuild-output", .{ .text = "" }).?; try testing.expect(together.better(apart));}
test "a path's last component counts for more" { const last = scoreOf("har", "sh", .{ .text = "/home/a/dev/harrier", .path = true }).?; const earlier = scoreOf("har", "sh", .{ .text = "/home/a/harbour/notes", .path = true }).?; try testing.expect(last.better(earlier));}
test "matched letters are found where the words were counted" { var buf: [max_words][]const u8 = undefined; const ws = words("rtr ssh", &buf); var fb: [64]usize = undefined; var sb: [64]usize = undefined; const h = highlight(ws, .{ .text = "router" }, .{ .text = "ssh admin@router" }, &fb, &sb); try testing.expectEqualSlices(usize, &.{ 0, 3, 5 }, h.first); try testing.expectEqualSlices(usize, &.{ 0, 1, 2 }, h.second);}
test "words beyond the most are left out" { var buf: [max_words][]const u8 = undefined; try testing.expectEqual(@as(usize, max_words), words("a b c d e f g h i j k l m n o p q r s t", &buf).len);}