diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 129 |
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 |
