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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Finding open panes, and profiles, from GNOME Shell's search: what each//! is known by, and which a search's terms find. A pane found is shown; a//! profile found is started.//!//! The Shell splits what was typed into terms on whitespace, and they are//! matched as `match` says: a pane is known by its title and then its//! location, a profile by its name and then what it runs. The location is//! as the title bar's subtitle writes it, host and all, so a host's name//! finds the panes on it. Open panes come before the profiles that would//! open another, each best match first.
const std = @import("std");const Allocator = std.mem.Allocator;const match = @import("match.zig");
/// The most results a search returns.pub const max_results = 20;
/// A pane as the search sees it: copied out of the window it is in, since/// the search runs on the session bus's thread.pub const Pane = struct { id: u64, title: []const u8, /// Its location, as `location.format` writes it; empty when it has /// none. location: []const u8,};
/// A profile as the search sees it, copied out of the configuration.pub const Profile = struct { name: []const u8, /// What it runs, as the profile selector describes it. runs: []const u8, /// How a result shows it, made where the translations are. description: []const u8, /// It opens as a tab rather than a window. tab: bool,};
/// How well `terms` match `pane`, by its title and then its location.pub fn score(pane: Pane, terms: []const []const u8) ?match.Score { return match.score(terms, .{ .text = pane.title }, .{ .text = pane.location, .path = true });}
/// How well `terms` match `profile`, by its name and then what it runs.pub fn scoreProfile(profile: Profile, terms: []const []const u8) ?match.Score { return match.score(terms, .{ .text = profile.name }, .{ .text = profile.runs });}
/// The panes and then the profiles `terms` find, as result identifiers/// allocated in `arena`: a pane already open before a profile that would/// open another, each best first, and among equals in the order given; at/// most `max_results`.////// A search narrowed by more typing is made afresh rather than among what/// it found before: those were cut off at `max_results`, and what more/// typing finds may be among what was cut.pub fn find( arena: Allocator, panes: []const Pane, profiles: []const Profile, terms: []const []const u8,) Allocator.Error![]const []const u8 { var found: std.ArrayList(Found) = .empty; for (panes) |p| { const s = score(p, terms) orelse continue; const id = try std.fmt.allocPrint(arena, "{f}", .{Result{ .pane = p.id }}); try found.append(arena, .{ .id = id, .score = s }); } const n_panes = found.items.len; for (profiles) |p| { const s = scoreProfile(p, terms) orelse continue; const id = try std.fmt.allocPrint(arena, "{f}", .{Result{ .profile = p.name }}); try found.append(arena, .{ .id = id, .score = s }); } // Stable, so that equals keep the order given. std.sort.insertion(Found, found.items[0..n_panes], {}, Found.better); std.sort.insertion(Found, found.items[n_panes..], {}, Found.better); const n = @min(found.items.len, max_results); const out = try arena.alloc([]const u8, n); for (out, found.items[0..n]) |*o, f| o.* = f.id; return out;}
const Found = struct { id: []const u8, score: match.Score,
fn better(_: void, a: Found, b: Found) bool { return a.score.better(b.score); }};
/// What a result's identifier names.pub const Result = union(enum) { pane: u64, profile: []const u8,
/// `pane-` and the pane's ID, or `profile-` and the profile's name. pub fn parse(id: []const u8) ?Result { if (std.mem.startsWith(u8, id, pane_prefix)) return .{ .pane = std.fmt.parseInt(u64, id[pane_prefix.len..], 10) catch return null }; if (std.mem.startsWith(u8, id, profile_prefix) and id.len > profile_prefix.len) return .{ .profile = id[profile_prefix.len..] }; return null; }
pub fn format(r: Result, w: *std.Io.Writer) std.Io.Writer.Error!void { switch (r) { .pane => |id| try w.print(pane_prefix ++ "{d}", .{id}), .profile => |name| try w.print(profile_prefix ++ "{s}", .{name}), } }
const pane_prefix = "pane-"; const profile_prefix = "profile-";};
/// A pane result's identifier, in `buf`.pub fn resultId(buf: []u8, id: u64) []const u8 { return std.fmt.bufPrint(buf, "{f}", .{Result{ .pane = id }}) catch unreachable;}
/// The pane a result's identifier names, or null if it names none.pub fn paneOf(result: []const u8) ?u64 { const r = Result.parse(result) orelse return null; return if (r == .pane) r.pane else null;}
const testing = std.testing;
test "every term must be found, in the title or the location" { const p: Pane = .{ .id = 1, .title = "vim README.md", .location = "~/dev/harrier" }; try testing.expect(score(p, &.{"vim"}) != null); try testing.expect(score(p, &.{ "VIM", "harrier" }) != null); try testing.expect(score(p, &.{"rdme"}) != null); try testing.expect(score(p, &.{ "vim", "emacs" }) == null); try testing.expect(score(.{ .id = 2, .title = "top", .location = "" }, &.{"harrier"}) == null); // Another machine's name finds the panes on it. const there: Pane = .{ .id = 3, .title = "vim", .location = "router:/etc/nginx" }; try testing.expect(score(there, &.{ "router", "nginx" }) != null); // Nothing typed finds nothing. try testing.expect(score(p, &.{}) == null);}
test "panes come before profiles, each best first, equals in order" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); const a = arena.allocator(); const panes = [_]Pane{ .{ .id = 1, .title = "bash", .location = "~/src/router-firmware" }, .{ .id = 2, .title = "router", .location = "~" }, .{ .id = 3, .title = "top", .location = "~" }, .{ .id = 4, .title = "router", .location = "/tmp" }, }; const profiles = [_]Profile{ .{ .name = "nas", .runs = "ssh nas.lan", .description = "", .tab = false }, .{ .name = "router", .runs = "ssh admin@router", .description = "", .tab = false }, }; const got = try find(a, &panes, &profiles, &.{"rtr"}); try testing.expectEqual(@as(usize, 4), got.len); // Found in the title, in order; then only in the location; then the // profile, though it matches as well as the best pane. try testing.expectEqualStrings("pane-2", got[0]); try testing.expectEqualStrings("pane-4", got[1]); try testing.expectEqualStrings("pane-1", got[2]); try testing.expectEqualStrings("profile-router", got[3]);}
test "the best are kept when there are more than can be returned" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); var panes: [max_results + 5]Pane = undefined; for (&panes, 0..) |*p, i| p.* = .{ .id = i, .title = "a-b-c", .location = "" }; panes[max_results + 2].title = "abc"; const got = try find(arena.allocator(), &panes, &.{}, &.{"abc"}); try testing.expectEqual(@as(usize, max_results), got.len); try testing.expectEqualStrings("pane-22", got[0]);}
test "more typing finds what the first words found too many of to return" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); const a = arena.allocator(); var profiles: [max_results + 5]Profile = undefined; for (&profiles, 0..) |*p, i| p.* = .{ .name = try std.fmt.allocPrint(a, "ankeny-b01-stk-{d}", .{i}), .runs = "", .description = "", .tab = false }; profiles[max_results + 2].name = "ankeny-b06-r18-core-1"; try testing.expectEqual(@as(usize, max_results), (try find(a, &.{}, &profiles, &.{"ank"})).len); const got = try find(a, &.{}, &profiles, &.{ "ank", "core" }); try testing.expectEqual(@as(usize, 1), got.len); try testing.expectEqualStrings("profile-ankeny-b06-r18-core-1", got[0]);}
test "result identifiers name panes and profiles" { var buf: [32]u8 = undefined; try testing.expectEqualStrings("pane-42", resultId(&buf, 42)); try testing.expectEqual(@as(?u64, 42), paneOf("pane-42")); try testing.expectEqual(@as(?u64, null), paneOf("pane-")); try testing.expectEqual(@as(?u64, null), paneOf("tab-42")); try testing.expectEqualStrings("admin pwsh", Result.parse("profile-admin pwsh").?.profile); try testing.expectEqual(@as(?u64, null), paneOf("profile-router")); try testing.expect(Result.parse("profile-") == null); try testing.expectEqualStrings("profile-router", try std.fmt.bufPrint(&buf, "{f}", .{Result{ .profile = "router" }}));}
test "a profile is found by its name or what it runs" { const p: Profile = .{ .name = "router", .runs = "ssh admin@router.lan", .description = "", .tab = false }; try testing.expect(scoreProfile(p, &.{"ROUTER"}) != null); try testing.expect(scoreProfile(p, &.{ "ssh", "admin" }) != null); try testing.expect(scoreProfile(p, &.{ "router", "nas" }) == null); try testing.expect(scoreProfile(p, &.{}) == null);}