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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The primary and the hand-off to it, through the session bus.//!//! The primary owns `dev.jcollie.harrier` and serves//! `org.freedesktop.Application` at `/dev/jcollie/harrier`, which is what a//! launcher calls for an application whose desktop file says//! `DBusActivatable=true`. Every later launch hands off to it and exits: it//! calls `ActivateAction("new-window", …)` with its request — what the//! window should run, where, and with which environment — and the primary//! opens the window. With nothing owning the name, the bus starts the//! service (`harrier --service`) to take the call; without even that, the//! launch keeps its request and becomes the primary itself.//!//! `/dev/jcollie/harrier/SearchProvider` serves GNOME Shell's search,//! `org.gnome.Shell.SearchProvider2`, which finds open panes by title and//! directory. The main loop publishes what panes there are; the search reads//! that, and an activated result goes back through the queue.//!//! The same object serves `org.freedesktop.Terminal1`, the Terminal Intent://! what a launcher calls to run a terminal program — one whose desktop file//! says `Terminal=true` — in the terminal the user chose, rather than//! guessing at which terminal is installed and how its command line works.//!//! `/dev/jcollie/harrier/LauncherEntry` says, through Unity's//! `com.canonical.Unity.LauncherEntry`, how far along the programs in the//! terminals are, for a dock or task manager to show on harrier's icon.//!//! The service runs on a thread of its own, as zig-dbus-service's `Service`//! does. A call's handler only copies what was asked for into a queue and//! wakes the main loop, which opens the window: everything to do with//! windows stays on the main thread.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;const dbus = @import("dbus_service");const mdbus = @import("mdbus");const Application = @import("dbus_interfaces").Application;const Terminal1 = @import("dbus_interfaces").Terminal1;const SearchProvider2 = @import("dbus_interfaces").SearchProvider2;const LauncherEntry = @import("dbus_interfaces").LauncherEntry;
const Wake = @import("os/Wake.zig");const req = @import("request.zig");const desktop_entry = @import("desktop_entry.zig");const search = @import("shell_search.zig");const Portal = @import("Portal.zig");
const log = std.log.scoped(.dbus);
pub const app_id = req.app_id;pub const object_path = "/dev/jcollie/harrier";pub const search_path = object_path ++ "/SearchProvider";pub const launcher_path = object_path ++ "/LauncherEntry";/// How the launcher API names the application: by its desktop file.const app_uri = "application://" ++ app_id ++ ".desktop";
pub const Launcher = req.Launcher;pub const Request = req.Request;const dupeStrings = req.dupeStrings;
/// The session bus's address: `DBUS_SESSION_BUS_ADDRESS`, or the socket/// systemd's user manager puts in the runtime directory.fn sessionAddress(buf: []u8, env: *const std.process.Environ.Map) ?[]const u8 { if (env.get("DBUS_SESSION_BUS_ADDRESS")) |a| return a; const runtime = env.get("XDG_RUNTIME_DIR") orelse return null; return std.fmt.bufPrint(buf, "unix:path={s}/bus", .{runtime}) catch null;}
// ---------------------------------------------------------------------------// The hand-off to the primary, which owns the name// ---------------------------------------------------------------------------
/// Hands `request` off to the primary on the bus — started by the bus if it/// has to be. True if it took it; false if there is no primary to ask, and/// the caller should become the primary and open the window itself.pub fn handOff(gpa: Allocator, io: Io, env: *const std.process.Environ.Map, request: Request) bool { var addr_buf: [std.fs.max_path_bytes + 16]u8 = undefined; const address = sessionAddress(&addr_buf, env) orelse return false; var connection = mdbus.Connection.connectAddress(io, gpa, address) catch |err| { log.debug("no session bus: {t}", .{err}); return false; }; defer connection.deinit();
var arena_state: std.heap.ArenaAllocator = .init(gpa); defer arena_state.deinit(); const arena = arena_state.allocator();
// The window, as one a{sv} in the action's parameter. var entries: std.ArrayList(dbus.Vardict.Entry) = .empty; const command = request.command orelse &[_][]const u8{}; if (request.command != null) entries.append(arena, .{ .key = "command", .value = .of("as", &command) }) catch return false; if (request.cwd) |*c| entries.append(arena, .{ .key = "cwd", .value = .of("s", c) }) catch return false; if (request.environ) |*e| entries.append(arena, .{ .key = "environ", .value = .of("as", e) }) catch return false; if (request.serial_path) |*p| entries.append(arena, .{ .key = "serial-path", .value = .of("s", p) }) catch return false; if (request.baud) |*b| entries.append(arena, .{ .key = "baud", .value = .of("u", b) }) catch return false; if (request.profile) |*e| entries.append(arena, .{ .key = "profile", .value = .of("s", e) }) catch return false; if (request.focus_pane) |*id| entries.append(arena, .{ .key = "focus-pane", .value = .of("t", id) }) catch return false; if (request.focus_primary) |*pid| entries.append(arena, .{ .key = "focus-primary", .value = .of("u", pid) }) catch return false; const window: dbus.Vardict = .{ .entries = entries.items };
var platform: std.ArrayList(dbus.Vardict.Entry) = .empty; if (request.activation_token) |*t| { platform.append(arena, .{ .key = "activation-token", .value = .of("s", t) }) catch return false; platform.append(arena, .{ .key = "desktop-startup-id", .value = .of("s", t) }) catch return false; }
var client: Application.Client = .{ .connection = &connection, .destination = app_id, .path = object_path, // Long enough for the bus to start the service, which scans the // fonts before it can answer. .timeout_ms = 30_000, }; _ = client.call(Application.ActivateAction, arena, .{ .action_name = if (request.new_tab) "new-tab" else "new-window", .parameter = &.{.of("a{sv}", &window)}, .platform_data = .{ .entries = platform.items }, }) catch |err| { if (client.failure) |f| { log.debug("handing the window off: {s}: {s}", .{ f.name, f.message }); } else log.debug("handing the window off: {t}", .{err}); return false; }; return true;}
/// `l` as the launcher API's properties, pointing into `l`.fn launcherProperties(l: *const Launcher, buf: *[3]dbus.Vardict.Entry) dbus.Vardict { buf.* = .{ .{ .key = "progress", .value = .of("d", &l.progress) }, .{ .key = "progress-visible", .value = .of("b", &l.visible) }, .{ .key = "urgent", .value = .of("b", &l.urgent) }, }; return .{ .entries = buf };}
// ---------------------------------------------------------------------------// Owning the name// ---------------------------------------------------------------------------
pub const Server = struct { gpa: Allocator, io: Io, connection: mdbus.Connection, service: dbus.Service, interfaces: [2]dbus.Object.Implementation, search_interfaces: [1]dbus.Object.Implementation, launcher_interfaces: [1]dbus.Object.Implementation, objects: [3]dbus.Object, thread: ?std.Thread = null, /// The service has stopped: the bus went away, and nothing more can be /// asked of this harrier. gone: std.atomic.Value(bool) = .init(false),
/// `$HOME`, where a terminal launched with nowhere else to go starts. /// Owned. home: ?[]const u8,
mutex: Io.Mutex = .init, /// The open panes, as the main loop last published them; under `mutex`, /// with the strings in `panes_arena`. panes: []const search.Pane = &.{}, panes_arena: std.heap.ArenaAllocator, /// The configuration's profiles, likewise, as the main loop last /// published them. profiles: []const search.Profile = &.{}, profiles_arena: std.heap.ArenaAllocator, /// What the launcher entry last said; under `mutex`. launcher: Launcher = .{}, pending: std.ArrayList(Request) = .empty, /// Wakes the main loop when a request arrives. Poll `wake.handle`. wake: Wake,
pub const StartError = error{ NoBus, NameTaken } || Allocator.Error || error{SystemResources};
/// Owns the name and starts serving it. On the heap: the service's /// objects point back into it. pub fn start(gpa: Allocator, io: Io, env: *const std.process.Environ.Map) StartError!*Server { var addr_buf: [std.fs.max_path_bytes + 16]u8 = undefined; const address = sessionAddress(&addr_buf, env) orelse return error.NoBus;
const s = try gpa.create(Server); errdefer gpa.destroy(s); s.* = .{ .gpa = gpa, .io = io, .connection = mdbus.Connection.connectAddress(io, gpa, address) catch return error.NoBus, .service = undefined, .interfaces = undefined, .objects = undefined, .home = if (env.get("HOME")) |h| try gpa.dupe(u8, h) else null, .search_interfaces = undefined, .launcher_interfaces = undefined, .panes_arena = .init(gpa), .profiles_arena = .init(gpa), .wake = try .init(), }; errdefer s.connection.deinit(); errdefer s.wake.deinit(); errdefer if (s.home) |h| gpa.free(h);
s.interfaces = .{ Application.Server(Server).implementation(s), Terminal1.Server(Server).implementation(s), }; s.search_interfaces = .{SearchProvider2.Server(Server).implementation(s)}; s.launcher_interfaces = .{LauncherEntry.Server(Server).implementation(s)}; s.objects = .{ .{ .path = object_path, .interfaces = &s.interfaces }, .{ .path = search_path, .interfaces = &s.search_interfaces }, .{ .path = launcher_path, .interfaces = &s.launcher_interfaces }, }; s.service = dbus.Service.init(gpa, io, &s.connection, &s.objects) catch return error.SystemResources; errdefer s.service.deinit();
s.service.requestName(app_id) catch |err| return switch (err) { error.NameTaken => error.NameTaken, error.OutOfMemory => error.OutOfMemory, else => error.NoBus, }; s.thread = std.Thread.spawn(.{}, serve, .{s}) catch return error.SystemResources; log.info("serving {s} on the session bus", .{app_id}); return s; }
fn serve(s: *Server) void { s.service.run() catch |err| log.info("the session bus service stopped: {t}", .{err}); s.gone.store(true, .release); s.wake.signal(); }
pub fn destroy(s: *Server) void { s.service.stop(); if (s.thread) |t| t.join(); s.service.deinit(); s.connection.deinit(); for (s.pending.items) |*r| r.deinit(s.gpa); s.pending.deinit(s.gpa); s.wake.deinit(); if (s.home) |h| s.gpa.free(h); s.panes_arena.deinit(); s.profiles_arena.deinit(); s.gpa.destroy(s); }
/// The next window asked for, if any. The caller owns it. pub fn take(s: *Server) ?Request { _ = s.wake.drain(); s.mutex.lockUncancelable(s.io); defer s.mutex.unlock(s.io); if (s.pending.items.len == 0) return null; const r = s.pending.orderedRemove(0); // More waiting: make sure the loop comes back for them. if (s.pending.items.len > 0) s.wake.signal(); return r; }
fn queue(s: *Server, request: Request) !void { try s.queueAll(&.{request}); }
/// Several windows, queued together or not at all. fn queueAll(s: *Server, requests: []const Request) !void { { s.mutex.lockUncancelable(s.io); defer s.mutex.unlock(s.io); try s.pending.appendSlice(s.gpa, requests); } s.wake.signal(); }
/// Says `l` on harrier's icon, if it is not what was said last. pub fn publishLauncher(s: *Server, l: Launcher) void { { s.mutex.lockUncancelable(s.io); defer s.mutex.unlock(s.io); if (std.meta.eql(s.launcher, l)) return; s.launcher = l; } var buf: [3]dbus.Vardict.Entry = undefined; LauncherEntry.emitUpdate(&s.service, launcher_path, .{ .app_uri = app_uri, .properties = launcherProperties(&l, &buf), }) catch |err| log.warn("the launcher entry: {t}", .{err}); }
pub fn query(s: *Server, call: *dbus.Call, _: LauncherEntry.Query.In) !LauncherEntry.Query.Out { s.mutex.lockUncancelable(s.io); const l = s.launcher; s.mutex.unlock(s.io); const copy = try call.arena.create(Launcher); copy.* = l; const buf = try call.arena.create([3]dbus.Vardict.Entry); return .{ .properties = launcherProperties(copy, buf) }; }
/// Replaces what the search knows of the open panes with `panes`, copied. pub fn publishPanes(s: *Server, panes: []const search.Pane) Allocator.Error!void { var arena: std.heap.ArenaAllocator = .init(s.gpa); errdefer arena.deinit(); const a = arena.allocator(); const copy = try a.alloc(search.Pane, panes.len); for (panes, copy) |p, *c| c.* = .{ .id = p.id, .title = try a.dupe(u8, p.title), .location = try a.dupe(u8, p.location), }; s.mutex.lockUncancelable(s.io); defer s.mutex.unlock(s.io); s.panes_arena.deinit(); s.panes_arena = arena; s.panes = copy; }
/// Replaces what the search knows of the profiles with `profiles`, /// copied. pub fn publishProfiles(s: *Server, profiles: []const search.Profile) Allocator.Error!void { var arena: std.heap.ArenaAllocator = .init(s.gpa); errdefer arena.deinit(); const a = arena.allocator(); const copy = try a.alloc(search.Profile, profiles.len); for (profiles, copy) |p, *c| c.* = .{ .name = try a.dupe(u8, p.name), .runs = try a.dupe(u8, p.runs), .description = try a.dupe(u8, p.description), .tab = p.tab, }; s.mutex.lockUncancelable(s.io); defer s.mutex.unlock(s.io); s.profiles_arena.deinit(); s.profiles_arena = arena; s.profiles = copy; }
/// The activation token a launcher passes, owned. fn token(s: *Server, platform_data: dbus.Vardict, arena: Allocator) !?[]const u8 { const t = (try platform_data.get("s", []const u8, "activation-token", arena)) orelse (try platform_data.get("s", []const u8, "desktop-startup-id", arena)) orelse return null; return try s.gpa.dupe(u8, t); }
// -- org.freedesktop.Application ------------------------------------
/// What a launcher calls when the desktop file is opened: a window. pub fn activate(s: *Server, call: *dbus.Call, in: Application.Activate.In) !void { try s.queue(.{ .activation_token = try s.token(in.platform_data, call.arena) }); }
/// Files and directories: a window in each directory, or in the /// directory a file is in. pub fn open(s: *Server, call: *dbus.Call, in: Application.Open.In) !void { for (in.uris) |uri| { const prefix = "file://"; if (!std.mem.startsWith(u8, uri, prefix)) continue; const path = try call.arena.dupe(u8, uri[prefix.len..]); const decoded = std.Uri.percentDecodeInPlace(path); const dir = blk: { var d = Io.Dir.cwd().openDir(s.io, decoded, .{}) catch break :blk std.fs.path.dirname(decoded) orelse "/"; d.close(s.io); break :blk decoded; }; try s.queue(.{ .cwd = try s.gpa.dupe(u8, dir), .activation_token = try s.token(in.platform_data, call.arena), }); } }
/// `new-window` or `new-tab`, with what it runs as an a{sv}: `command` /// (as), `cwd` (s), `environ` (as), `serial-path` (s) and `baud` (u), /// all optional. A new tab goes in the window most recently opened or /// focused. /// /// Or `focus-pane`, with a pane's ID (t): what clicking one of /// harrier's notifications does, when the desktop activates the /// application rather than telling the portal. pub fn activateAction(s: *Server, call: *dbus.Call, in: Application.ActivateAction.In) !void { if (std.mem.eql(u8, in.action_name, Portal.focus_action)) { if (in.parameter.len == 0) return error.InvalidArgs; const id = (try in.parameter[0].get("t", u64, call.arena)) orelse return error.InvalidArgs; try s.queue(.{ .focus_pane = id, .activation_token = try s.token(in.platform_data, call.arena) }); return; } const new_tab = std.mem.eql(u8, in.action_name, "new-tab"); if (!new_tab and !std.mem.eql(u8, in.action_name, "new-window")) return error.NotSupported; const arena = call.arena; const window: dbus.Vardict = if (in.parameter.len > 0) (try in.parameter[0].get("a{sv}", dbus.Vardict, arena)) orelse return error.InvalidArgs else .{};
var r: Request = .{ .new_tab = new_tab }; errdefer r.deinit(s.gpa); if (try window.get("as", []const []const u8, "command", arena)) |c| if (c.len > 0) { r.command = try dupeStrings(s.gpa, c); }; if (try window.get("s", []const u8, "cwd", arena)) |c| r.cwd = try s.gpa.dupe(u8, c); if (try window.get("as", []const []const u8, "environ", arena)) |e| r.environ = try dupeStrings(s.gpa, e); if (try window.get("s", []const u8, "serial-path", arena)) |p| r.serial_path = try s.gpa.dupe(u8, p); r.baud = try window.get("u", u32, "baud", arena); if (try window.get("s", []const u8, "profile", arena)) |e| r.profile = try s.gpa.dupe(u8, e); r.focus_pane = try window.get("t", u64, "focus-pane", arena); r.focus_primary = try window.get("u", u32, "focus-primary", arena); r.activation_token = try s.token(in.platform_data, arena); try s.queue(r); }
// -- org.freedesktop.Terminal1 --------------------------------------
/// A window with a tab for each command — or the shell, for an empty /// one or none at all — with each command's `exec`, `env` and /// `working_directory`: the grouping the specification suggests. /// /// A desktop entry names the windows and gives them its `Path` when a /// command has no directory of its own; one that cannot be read refuses /// the call. A command that cannot be started still gets a window, /// saying so, and the rest are launched regardless. pub fn launchCommand(s: *Server, call: *dbus.Call, in: Terminal1.LaunchCommand.In) !void { const arena = call.arena;
var entry: desktop_entry.Entry = .{}; const entry_path = bytestring(in.desktop_entry); if (entry_path.len > 0) { entry = s.readDesktopEntry(arena, entry_path) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return call.fail(dbus.errors.invalid_args, "desktop_entry is not the path of a desktop entry"), }; } const keep_open = (try in.options.get("b", bool, "keep-terminal-open", arena)) orelse false; const token_ = try s.token(in.platform_data, arena); defer if (token_) |t| s.gpa.free(t);
// No commands is one shell. const empty = [_]dbus.Vardict{.{}}; const commands = if (in.commands.len == 0) &empty else in.commands;
var requests: std.ArrayList(Request) = .empty; defer requests.deinit(s.gpa); errdefer for (requests.items) |*r| r.deinit(s.gpa); for (commands, 0..) |command, i| { // The first opens the window the rest are tabs in. var r: Request = .{ .hold = if (keep_open) .always else .failure, .new_tab = i > 0 }; errdefer r.deinit(s.gpa);
if (try command.get("aay", []const []const u8, "exec", arena)) |argv| if (argv.len > 0) { r.command = try dupeStrings(s.gpa, try bytestrings(arena, argv)); }; if (try command.get("aay", []const []const u8, "env", arena)) |env| if (env.len > 0) { r.environ_set = try dupeStrings(s.gpa, try bytestrings(arena, env)); }; const wd = bytestring((try command.get("ay", []const u8, "working_directory", arena)) orelse ""); const cwd: ?[]const u8 = if (wd.len > 0) wd else entry.path orelse s.home; if (cwd) |c| r.cwd = try s.gpa.dupe(u8, c); if (entry.name) |n| r.title = try s.gpa.dupe(u8, n); // A token makes a window active once; the first window takes it. if (i == 0) if (token_) |t| { r.activation_token = try s.gpa.dupe(u8, t); }; try requests.append(s.gpa, r); } try s.queueAll(requests.items); requests.clearRetainingCapacity(); }
/// An `ay` as GLib writes one — `g_variant_new_bytestring`, or `b'…'` /// to gdbus — ends in a NUL that is not part of the string. A NUL can be /// in no path, argument or environment entry, so one at the end is /// always that terminator. fn bytestring(b: []const u8) []const u8 { return if (b.len > 0 and b[b.len - 1] == 0) b[0 .. b.len - 1] else b; }
fn bytestrings(arena: Allocator, list: []const []const u8) Allocator.Error![]const []const u8 { const out = try arena.alloc([]const u8, list.len); for (list, out) |b, *o| o.* = bytestring(b); return out; }
// -- org.gnome.Shell.SearchProvider2 --------------------------------
pub fn getInitialResultSet(s: *Server, call: *dbus.Call, in: SearchProvider2.GetInitialResultSet.In) !SearchProvider2.GetInitialResultSet.Out { return .{ .results = try s.find(call.arena, in.terms) }; }
/// Made afresh, as `search.find` says, rather than among /// `previous_results`. pub fn getSubsearchResultSet(s: *Server, call: *dbus.Call, in: SearchProvider2.GetSubsearchResultSet.In) !SearchProvider2.GetSubsearchResultSet.Out { return .{ .results = try s.find(call.arena, in.terms) }; }
/// What `terms` find, as `search.find` orders it. fn find(s: *Server, arena: Allocator, terms: []const []const u8) ![]const []const u8 { s.mutex.lockUncancelable(s.io); defer s.mutex.unlock(s.io); return search.find(arena, s.panes, s.profiles, terms); }
/// For each result still open: its title as the name, where it is as /// the description, and harrier's icon. pub fn getResultMetas(s: *Server, call: *dbus.Call, in: SearchProvider2.GetResultMetas.In) !SearchProvider2.GetResultMetas.Out { const arena = call.arena; var metas: std.ArrayList(dbus.Vardict) = .empty; s.mutex.lockUncancelable(s.io); defer s.mutex.unlock(s.io); for (in.identifiers) |id_str| { // A variant points at its value until it is sent: every value // lives in the call's arena. const strings = try arena.alloc([]const u8, 4); strings[0] = try arena.dupe(u8, id_str); switch (search.Result.parse(id_str) orelse continue) { .pane => |id| { const p = for (s.panes) |p| { if (p.id == id) break p; } else continue; strings[1] = try arena.dupe(u8, if (p.title.len > 0) p.title else "harrier"); strings[2] = try arena.dupe(u8, p.location); }, .profile => |name| { const p = for (s.profiles) |p| { if (std.mem.eql(u8, p.name, name)) break p; } else continue; strings[1] = try arena.dupe(u8, p.name); strings[2] = try arena.dupe(u8, p.description); }, } strings[3] = app_id; const entries = try arena.alloc(dbus.Vardict.Entry, 4); entries[0] = .{ .key = "id", .value = .of("s", &strings[0]) }; entries[1] = .{ .key = "name", .value = .of("s", &strings[1]) }; entries[2] = .{ .key = "description", .value = .of("s", &strings[2]) }; // The icon by its name in the theme, which is what a GIcon's // string form is for a themed icon. entries[3] = .{ .key = "gicon", .value = .of("s", &strings[3]) }; try metas.append(arena, .{ .entries = entries }); } return .{ .metas = metas.items }; }
pub fn activateResult(s: *Server, _: *dbus.Call, in: SearchProvider2.ActivateResult.In) !void { try s.openResult(search.Result.parse(in.identifier) orelse return error.InvalidArgs); }
/// The Shell's "show me everything" — a click on harrier's icon beside /// its results. harrier has no list of its own to show them in: it /// shows the first pane found, or else starts the first profile found, /// or opens a window when nothing is. pub fn launchSearch(s: *Server, call: *dbus.Call, in: SearchProvider2.LaunchSearch.In) !void { const found = try s.find(call.arena, in.terms); if (found.len > 0) { try s.openResult(search.Result.parse(found[0]).?); } else try s.queue(.{}); }
/// A result picked: a pane shown, or a profile started as it says, in /// a window or in a tab of the last window, which is brought forward. fn openResult(s: *Server, r: search.Result) !void { switch (r) { .pane => |id| try s.queue(.{ .focus_pane = id }), .profile => |name| { const tab = tab: { s.mutex.lockUncancelable(s.io); defer s.mutex.unlock(s.io); for (s.profiles) |p| if (std.mem.eql(u8, p.name, name)) break :tab p.tab; return; }; const copy = try s.gpa.dupe(u8, name); errdefer s.gpa.free(copy); try s.queue(.{ .profile = copy, .new_tab = tab, .raise = true }); }, } }
fn readDesktopEntry(s: *Server, arena: Allocator, path: []const u8) !desktop_entry.Entry { if (!std.mem.endsWith(u8, path, ".desktop")) return error.NotDesktopEntry; const text = try Io.Dir.cwd().readFileAlloc(s.io, path, arena, .limited(1024 * 1024)); return desktop_entry.parse(arena, text); }};
// ---------------------------------------------------------------------------
const testing = std.testing;
test { _ = desktop_entry; _ = search;}
test "a GLib bytestring's terminator is dropped, and nothing else" { try testing.expectEqualStrings("/usr/bin/vim", Server.bytestring("/usr/bin/vim\x00")); try testing.expectEqualStrings("/usr/bin/vim", Server.bytestring("/usr/bin/vim")); try testing.expectEqualStrings("", Server.bytestring("\x00")); try testing.expectEqualStrings("", Server.bytestring(""));}
test "a request copies and frees what it holds" { var r: Request = .{ .command = try dupeStrings(testing.allocator, &.{ "vim", "README.md" }), .cwd = try testing.allocator.dupe(u8, "/tmp"), .environ = try dupeStrings(testing.allocator, &.{"TERM=dumb"}), }; r.deinit(testing.allocator);}