summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-12 16:33:14 -0300
committerGabriel Schneider <[email protected]>2026-08-12 20:22:18 -0300
commit9280c597b000eed661fd98793e182fdcb640f6cd (patch)
tree1b8e928c052c46ceb9a58c83256d5fb4e11e361a /src/pardes.zig
parentbc89f57cb576e23a58572ec35f96db068367f1b4 (diff)
downloadpardes-9280c597b000eed661fd98793e182fdcb640f6cd.tar.gz
pardes-9280c597b000eed661fd98793e182fdcb640f6cd.zip
shaders: -Dprebuilt-shaders, so a gui build needs no Vulkan SDK
glslc is the one build input that wants a tool a stock machine does not have, and it is also the input that changes least often: eight GLSL files that have outlived several rewrites of everything around them. Asking every machine that wants to run the SDL shell for shaderc is the wrong trade. The SPIR-V is now COMMITTED, under shaders/prebuilt/, and -Dprebuilt-shaders embeds that copy instead of shelling out. The default stays the honest one -- compile the shaders that are actually in the tree -- because the flag trades a dependency for a freshness problem: with it on, the .glsl sources are not build inputs at all, so editing one changes nothing. `zig build shaders` is the other half, and it is deliberately independent of -Dplatform: it recompiles every shader and writes the result back into the tracked directory, so whoever changes a shader refreshes the cache on a machine that has the compiler and commits the diff. `jj diff shaders/prebuilt` after it is the freshness check -- empty means the cache was already current. The shader list is also spelled once now (gui_shaders): the eight embeds, the eight glslc runs and the refresh step all read it, so adding a shader is a name there plus the @embedFile in gui.zig, not three edits in two places. Verified: -Dplatform=gui -Dprebuilt-shaders builds with glslc absent from PATH, and image-harness passes on that binary -- real SDL GPU pipelines built from the committed SPIR-V, 512 source pixels read back. The default gui build still runs the eight glslc steps; tty runs none. The committed bytes are identical to a fresh glslc run, and `zig build shaders` is idempotent.
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig129
1 files changed, 123 insertions, 6 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index da6e540c..3db27b86 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -51,6 +51,14 @@ pub const platform: Platform = @field(Platform, @tagName(@import("pardes_config"
/// disabled so much as meaningless — see builtins.zig.
pub const font_picker = platform == .gui or platform == .macos;
+/// ...and the one fact about that face the core keeps: the name `Font` last
+/// resolved, which the Debug overlay prints. Behind the same comptime shim
+/// builtins.zig and macos.zig import this file with, so a tty or web binary
+/// never analyses a font-directory walk it cannot use.
+const fonts = if (font_picker) @import("fonts.zig") else struct {
+ pub const chosen: []const u8 = "";
+};
+
/// Native PDF quality is a shell property, but the core owns MuPDF and the
/// RGBA cache. Kitty favors wire bandwidth; SDL favors physical-pixel text
/// quality and asks the renderer to cover either fit axis without upscaling.
@@ -2220,6 +2228,71 @@ test "New requests repeat, stay anchored, and reject stale or full completions"
try std.testing.expect(std.mem.indexOf(u8, p.panes[source].?.msg[0..p.panes[source].?.msg_len], "help: NoPaneSlots") != null);
}
+test "Msg writes the transient row by hand, bare or with text, and input ends it" {
+ const p = try Pardes.init(std.testing.allocator, .{ .shells = 3, .cols = 100, .rows = 30 });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ const row = struct {
+ fn of(pp: *Pardes, i: usize) []const u8 {
+ const pane = pp.panes[i].?;
+ return pane.msg[0..pane.msg_len];
+ }
+ }.of;
+
+ // the whole tail, spaces and all: the row is a sentence, not a word
+ try std.testing.expect(p.executeBuiltinLine(2, "Msg saved /etc/hosts"));
+ try std.testing.expectEqualStrings("saved /etc/hosts", row(p, 2));
+ // ...on the pane that ran it, and nowhere else
+ try std.testing.expectEqualStrings("", row(p, 0));
+
+ // Bare, it reports ITSELF through reportError — the `<operation>: <Error>`
+ // every failed save and refused Look arrives in.
+ try std.testing.expect(p.executeBuiltinLine(2, "Msg"));
+ try std.testing.expectEqualStrings("Msg: NoMessage", row(p, 2));
+
+ // A message is exactly as old as your last input.
+ p.update(.{ .key = .{ .cp = Key.escape } });
+ try std.testing.expectEqualStrings("", row(p, 2));
+
+ // Longer than the row's buffer is CUT, never refused and never overrun:
+ // this word is the only way to reach that boundary without a 300-byte path.
+ var long: [600]u8 = @splat('x');
+ var line: [608]u8 = undefined;
+ const cmd = try std.fmt.bufPrint(&line, "Msg {s}", .{&long});
+ try std.testing.expect(p.executeBuiltinLine(2, cmd));
+ try std.testing.expectEqual(p.panes[2].?.msg.len, row(p, 2).len);
+}
+
+test "Font records the face it chose, which is the name the Debug overlay prints" {
+ if (comptime !font_picker) return; // there is no Font builtin on tty or web
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 });
+ defer p.deinit();
+ var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
+ defer arena.deinit();
+ const installed = fonts.list(arena.allocator(), null);
+ if (installed.len == 0) return; // a machine with no monospace face at all
+
+ // module state, and the shell takes it on its next pass: leave neither the
+ // request nor the name behind for whatever test runs after this one
+ defer {
+ fonts.want = null;
+ fonts.chosen = "";
+ }
+ var line: [512]u8 = undefined;
+ const cmd = try std.fmt.bufPrint(&line, "Font {s}", .{installed[0].name});
+ try std.testing.expect(p.executeBuiltinLine(p.active, cmd));
+ // the PATH is what the shell needs and the NAME is what a person reads;
+ // one is the other's stem, so the two can never name different files
+ try std.testing.expect(fonts.want != null);
+ try std.testing.expectEqualStrings(installed[0].name, fonts.chosen);
+ try std.testing.expectEqualStrings(fonts.chosen, std.fs.path.stem(fonts.want.?));
+
+ // ...and a name nothing answers to changes neither, so the overlay goes on
+ // naming the face that is actually on
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Font zzz-no-such-face"));
+ try std.testing.expectEqualStrings(installed[0].name, fonts.chosen);
+}
+
test "startup config overrides restored styling before initFromDump returns" {
const source = try Pardes.init(std.testing.allocator, .{ .startup_config = "Theme dark\n" });
defer source.deinit();
@@ -7824,7 +7897,44 @@ pub const Pardes = struct {
.grep
else
.text;
- p.runSearch(id, armed[slash + 1 ..], kind, .top) catch |err| p.reportError(id, "search", err);
+ p.runSearch(id, armed[slash + 1 ..], kind, .top) catch |err| return p.reportError(id, "search", err);
+ // ...and the bare `/` GOES there. Find and Grep answer with OTHER
+ // files, and opening the first of them on submit would rearrange the
+ // screen before you have read what was found; `/` searched the text
+ // already in front of you, so its first hit is a scroll, not a jump.
+ if (kind == .text) p.lookFirstHit(id);
+ }
+
+ /// Land ON the first hit of a `/`, instead of beside a list of them.
+ ///
+ /// No new motion: the results buffer is FOCUSED and then the two ordinary
+ /// verbs run in the order a hand would run them — the step `n` is, and the
+ /// look Enter is. Focusing is the part that cannot be skipped and the
+ /// reason this is not just a call to the walk: lookWalkPanes deliberately
+ /// leaves the ACTIVE pane out of the ring, so `n` pressed straight after a
+ /// search steps whichever list was looked most recently, which in a
+ /// session with any history at all is not the one that just answered.
+ ///
+ /// Everything the walk needs to be reversible from here is left by the
+ /// step itself (landLookSpot's `look_at`), so `N` afterwards goes back to
+ /// the row above exactly as it would have if you had pressed `n` yourself.
+ fn lookFirstHit(p: *Pardes, id: usize) void {
+ const pane = p.panes[id] orelse return;
+ const rid = pane.search_pane orelse return;
+ const rp = p.panes[rid] orelse return;
+ // Nothing matched: the empty buffer is the answer, and aiming the walk
+ // at a list with no rows would send it round the ring into a NEIGHBOUR
+ // and open a row from some other search.
+ if ((rp.file orelse return).content.len == 0) return;
+ p.active = rid;
+ p.lookWalk(1);
+ if (p.active != rid) return; // walked out of the list; leave it there
+ const txt = p.currentSelText(rp) orelse return;
+ // spent, exactly as the look chord spends it (handleKey)
+ rp.vsel.active = false;
+ rp.msel.active = false;
+ rp.select = false;
+ p.runBuiltin(config.look_cmd, rid, "", txt);
}
/// Fill this pane's results buffer with everything matching `pat_raw`
@@ -13373,14 +13483,21 @@ pub const Pardes = struct {
sb_off = sb.offset;
sb_total = sb.total;
}
- var ov_buf: [192]u8 = undefined;
+ // The face belongs to the SHELL, so the core can only name the one
+ // it was asked for; nothing has asked when this is empty and the
+ // shell is still in whatever it booted in. Clamped because a name
+ // is a file stem and a path component can be as long as a path.
+ const font_name = if (fonts.chosen.len == 0) "default" else fonts.chosen;
+ var ov_buf: [256]u8 = undefined;
const text = std.fmt.bufPrint(
&ov_buf,
- "pardes resizes={d}\nterms {d} cols {d}\nactive #{d} {d}x{d}\nscroll {d}/{d}\npinch {d:6.4}\ntscroll {d:7.4}",
- .{ p.resize_count, nlive, p.ncol, p.active, at.cols, at.rows, sb_off, sb_total, p.ov_pinch_scale, p.ov_touch_scroll_delta },
+ "pardes resizes={d}\ntheme {s}\nfont {s}\nterms {d} cols {d}\nactive #{d} {d}x{d}\nscroll {d}/{d}\npinch {d:6.4}\ntscroll {d:7.4}",
+ .{ p.resize_count, themes[p.theme_idx].name, font_name[0..@min(font_name.len, 64)], nlive, p.ncol, p.active, at.cols, at.rows, sb_off, sb_total, p.ov_pinch_scale, p.ov_touch_scroll_delta },
) catch "";
- const ow: u16 = 30;
- const oh: u16 = 8;
+ // wide enough for the longest theme name compiled in, which is the
+ // only row here whose width is not ours to choose
+ const ow: u16 = 38;
+ const oh: u16 = 10;
const ox: u16 = if (s.cols > ow) s.cols - ow else 0;
if (ox + ow <= s.cols and TOPBAR_H + oh <= s.rows) {
// vaxis single_rounded border ring, default style