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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The configuration as each pane's foreground program sees it: evaluated//! again with `ctx.foreground` set, for a configuration that reads it, and//! kept, so that `fish` to `vim` and back is evaluated once for each.//!//! A pane holds the entry for its foreground program, and an entry no pane//! holds goes once there are more than `max_entries`, the least recently//! used first. Everything goes on a reload, or a change of appearance://! what the configuration says may have changed for every one.//!//! What a pane's evaluation says of a field that is not a pane's is not//! applied, and is said once for each field until the next reload.
const std = @import("std");const Allocator = std.mem.Allocator;const struthio = @import("struthio");
const Config = @import("config/Config.zig");const load = @import("config/load.zig");const bindings_mod = @import("input/bindings.zig");const foreground = @import("os/foreground.zig");
const scoped = std.log.scoped(.pane_configs);/// Quiet under test, where a warning fails the run, and what is said is/// what is tested.const log = if (@import("builtin").is_test) struct { fn warn(comptime _: []const u8, _: anytype) void {} fn err(comptime _: []const u8, _: anytype) void {}} else scoped;
const PaneConfigs = @This();
pub const max_entries = 64;
gpa: Allocator,entries: std.ArrayList(*Entry) = .empty,/// Foreground programs whose evaluation failed, said once each.failed: std.ArrayList(Config.Foreground) = .empty,/// The fields a pane's evaluation was found to give another value than/// the app's, said once each. Names from `Config` itself.warned: std.ArrayList([]const u8) = .empty,/// Counts uses, for least recently used.tick: u64 = 0,
pub const Entry = struct { /// The foreground program it was evaluated for; its own strings. key: Config.Foreground, parsed: struthio.Parsed(Config), theme: Config.Theme, /// The bindings that are not global, defaults and all, as /// `bindings.load` makes them. bindings: []bindings_mod.Binding, /// Panes holding it. refs: u32 = 0, used: u64 = 0,
pub fn config(e: *const Entry) *const Config { return &e.parsed.value; }};
pub fn init(gpa: Allocator) PaneConfigs { return .{ .gpa = gpa };}
pub fn deinit(pc: *PaneConfigs) void { pc.clear(); pc.entries.deinit(pc.gpa); pc.failed.deinit(pc.gpa); pc.warned.deinit(pc.gpa);}
/// Lets go of every entry, and of what has been said: for a new/// configuration. No pane may still hold one.pub fn clear(pc: *PaneConfigs) void { for (pc.entries.items) |e| { std.debug.assert(e.refs == 0); pc.destroy(e); } pc.entries.clearRetainingCapacity(); for (pc.failed.items) |f| foreground.free(pc.gpa, f); pc.failed.clearRetainingCapacity(); pc.warned.clearRetainingCapacity();}
/// The entry for `fg`, held for a pane until `release`: one already made,/// or the configuration `loaded` evaluated again. Null when that fails,/// and then the pane has the app's configuration.pub fn acquire(pc: *PaneConfigs, loaded: *const load.Loaded, fg: Config.Foreground) ?*Entry { pc.tick += 1; for (pc.entries.items) |e| if (foreground.eql(e.key, fg)) { e.refs += 1; e.used = pc.tick; return e; }; for (pc.failed.items) |f| if (foreground.eql(f, fg)) return null;
const e = pc.make(loaded, fg) catch |err| { switch (err) { error.OutOfMemory => log.err("the configuration for {s}: out of memory", .{fg.name}), error.Invalid => {}, } if (foreground.dupe(pc.gpa, fg)) |f| pc.failed.append(pc.gpa, f) catch foreground.free(pc.gpa, f) else |_| {} return null; }; pc.entries.append(pc.gpa, e) catch { pc.destroy(e); return null; }; e.refs = 1; e.used = pc.tick; pc.warnDiffering(loaded.config(), e.config()); pc.evict(); return e;}
pub fn release(pc: *PaneConfigs, e: *Entry) void { _ = pc; std.debug.assert(e.refs > 0); e.refs -= 1;}
fn make(pc: *PaneConfigs, loaded: *const load.Loaded, fg: Config.Foreground) error{ OutOfMemory, Invalid }!*Entry { var diag: struthio.Diagnostic = .{}; var parsed = loaded.evalFor(pc.gpa, fg, &diag) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => { log.warn("the configuration, for a pane running {s} as UID {d}: {f}", .{ fg.name, fg.uid, &diag }); return error.Invalid; }, }; errdefer parsed.deinit(); var field: []const u8 = ""; const theme = parsed.value.theme(&field) catch { log.warn("the configuration, for a pane running {s} as UID {d}: {s}: not a color", .{ fg.name, fg.uid, field }); return error.Invalid; }; const binds = try bindings_mod.load(pc.gpa, parsed.value.keybind); errdefer pc.gpa.free(binds); const key = try foreground.dupe(pc.gpa, fg); errdefer foreground.free(pc.gpa, key); const e = try pc.gpa.create(Entry); e.* = .{ .key = key, .parsed = parsed, .theme = theme, .bindings = binds }; return e;}
fn destroy(pc: *PaneConfigs, e: *Entry) void { foreground.free(pc.gpa, e.key); e.parsed.deinit(); pc.gpa.free(e.bindings); pc.gpa.destroy(e);}
/// Down to `max_entries`, if it can be: only entries no pane holds go.fn evict(pc: *PaneConfigs) void { while (pc.entries.items.len > max_entries) { var oldest: ?usize = null; for (pc.entries.items, 0..) |e, i| if (e.refs == 0) { if (oldest == null or e.used < pc.entries.items[oldest.?].used) oldest = i; }; const i = oldest orelse return; pc.destroy(pc.entries.swapRemove(i)); }}
fn warnDiffering(pc: *PaneConfigs, app: *const Config, pane: *const Config) void { var buf: [64][]const u8 = undefined; for (Config.appFieldsDiffering(app, pane, &buf)) |name| { const said = for (pc.warned.items) |w| { if (std.mem.eql(u8, w, name)) break true; } else false; if (said) continue; log.warn("{s} depends on ctx.foreground, which only a pane's colors, bindings, bell, cursor, clipboard, links, notifications, drag and drop and images can; ignored", .{name}); pc.warned.append(pc.gpa, name) catch {}; }}
const testing = std.testing;
fn loadString(source: []const u8) !load.Loaded { var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(testing.io, .{ .sub_path = "config.struthio", .data = source }); const path = try tmp.dir.realPathFileAlloc(testing.io, "config.struthio", testing.allocator); defer testing.allocator.free(path); var env: std.process.Environ.Map = .init(testing.allocator); defer env.deinit(); var problem: load.Problem = .{}; return load.load(testing.allocator, testing.io, &env, path, .{ .hostname = "test" }, &problem);}
test "a pane's evaluation, by its foreground program, kept and shared" { var loaded = try loadString( \\.{ \\ .colors = .{ .background = if (ctx.foreground) |f| (if (f.uid == 0) "#3a0000" else "#000000") else "#1d1f21" }, \\ .keybind = if (ctx.foreground) |f| (if (f.name == "nvim") .{ .{ .keys = "ctrl+shift+c", .action = .unbind } } else .{}) else .{}, \\} ); defer loaded.deinit(); try testing.expect(loaded.readsForeground()); try testing.expectEqualStrings("#1d1f21", loaded.config().colors.background);
var pc: PaneConfigs = .init(testing.allocator); defer pc.deinit(); const root = pc.acquire(&loaded, .{ .name = "sudo", .uid = 0 }).?; try testing.expectEqual(@as(u8, 0x3a), root.theme.background.r); const nvim = pc.acquire(&loaded, .{ .name = "nvim", .uid = 1000, .user = "jeff" }).?; try testing.expectEqual(@as(u8, 0), nvim.theme.background.r); try testing.expect(nvim.bindings.len != root.bindings.len); // The same program again is the same entry. const again = pc.acquire(&loaded, .{ .name = "sudo", .uid = 0 }).?; try testing.expectEqual(root, again); try testing.expectEqual(@as(u32, 2), root.refs); pc.release(root); pc.release(again); pc.release(nvim);}
test "a configuration that never reads ctx.foreground says so" { var loaded = try loadString(".{ .colors = .{ .background = if (ctx.hostname == \"test\") \"#000000\" else \"#ffffff\" } }"); defer loaded.deinit(); try testing.expect(!loaded.readsForeground());}
test "an evaluation that fails is tried once" { var loaded = try loadString(".{ .colors = .{ .background = if (ctx.foreground) |f| f.user.? else \"#000000\" } }"); defer loaded.deinit(); var pc: PaneConfigs = .init(testing.allocator); defer pc.deinit(); try testing.expectEqual(null, pc.acquire(&loaded, .{ .name = "sh", .uid = 4242 })); try testing.expectEqual(null, pc.acquire(&loaded, .{ .name = "sh", .uid = 4242 })); try testing.expectEqual(@as(usize, 1), pc.failed.items.len); const ok = pc.acquire(&loaded, .{ .name = "sh", .uid = 1000, .user = "#123456" }).?; try testing.expectEqual(@as(u8, 0x12), ok.theme.background.r); pc.release(ok);}
test "only entries no pane holds are let go" { var loaded = try loadString(".{ .colors = .{ .background = if (ctx.foreground != null) \"#000000\" else \"#ffffff\" } }"); defer loaded.deinit(); var pc: PaneConfigs = .init(testing.allocator); defer pc.deinit(); const held = pc.acquire(&loaded, .{ .name = "held", .uid = 1 }).?; var names: [max_entries + 4][8]u8 = undefined; for (&names, 0..) |*n, i| { const name = try std.fmt.bufPrint(n, "p{d}", .{i}); pc.release(pc.acquire(&loaded, .{ .name = name, .uid = 1 }).?); } try testing.expectEqual(@as(usize, max_entries), pc.entries.items.len); const kept = for (pc.entries.items) |e| { if (e == held) break true; } else false; try testing.expect(kept); pc.release(held);}
test "a field that is not a pane's is not taken from one, and is said" { var loaded = try loadString(".{ .font = .{ .size = if (ctx.foreground != null) 20 else 12 } }"); defer loaded.deinit(); var pc: PaneConfigs = .init(testing.allocator); defer pc.deinit(); const e = pc.acquire(&loaded, .{ .name = "x", .uid = 1 }).?; defer pc.release(e); try testing.expectEqual(@as(usize, 1), pc.warned.items.len); try testing.expectEqualStrings("font", pc.warned.items[0]);}