From 79a8980e46d8ed1f9ef7e3f4e44db7ee58e9f88e Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 5 Aug 2026 16:28:03 -0300 Subject: add the transient message row --- next-steps.txt | 2 +- src/gui/gui.zig | 12 +++ src/lsp/lsp_zls.zig | 10 ++- src/message.zig | 69 +++++++++++++++++ src/pardes.zig | 123 ++++++++++++++++++++++++++++- src/tty/tty.zig | 12 +++ test/snapshot.zig | 8 +- test/snapshots/filewatch.golden | 6 +- test/snapshots/find.golden | 18 ++--- test/snapshots/fsearch.golden | 6 +- test/snapshots/grep.golden | 6 +- test/snapshots/msg.golden | 166 ++++++++++++++++++++++++++++++++++++++++ test/snapshots/msg.snap | 77 +++++++++++++++++++ test/snapshots/pipe.golden | 6 +- test/snapshots/psearch.golden | 6 +- test/snapshots/selregex.golden | 26 +++---- test/snapshots/tagcmd.golden | 2 +- 17 files changed, 508 insertions(+), 47 deletions(-) create mode 100644 src/message.zig create mode 100644 test/snapshots/msg.golden create mode 100644 test/snapshots/msg.snap diff --git a/next-steps.txt b/next-steps.txt index faca8d23..451a25dd 100644 --- a/next-steps.txt +++ b/next-steps.txt @@ -1,4 +1,4 @@ + The selection colors should be based on the theme. + The default layout for when just opening a file should be just the file without the term. + Implement soft line breaks with a builtin to toggle it; the line breaks at min(100 chars, window width) -- A new UI semantic element will be implemented which is the transient message section: it will be the latest row of a pane. When executing any builtin that requires and string argument but none was provided right now it should ask for input at the tagline. now it will be at this transient msg that will be left-aligned on the last row. Also when saving a file or a file is reloaded it should display a message there saying the time and what happened. the message should clear when any key on the keyboard is pressed or any mouse button is moved \ No newline at end of file ++ A new UI semantic element will be implemented which is the transient message section: it will be the latest row of a pane. When executing any builtin that requires and string argument but none was provided right now it should ask for input at the tagline. now it will be at this transient msg that will be left-aligned on the last row. Also when saving a file or a file is reloaded it should display a message there saying the time and what happened. the message should clear when any key on the keyboard is pressed or any mouse button is moved \ No newline at end of file diff --git a/src/gui/gui.zig b/src/gui/gui.zig index b496058e..a4883fa6 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -20,6 +20,7 @@ const ghostty_vt = @import("ghostty-vt"); // 256-color palette for .index cells const pardes = @import("../pardes.zig"); const config = @import("../config.zig"); const look = @import("../look.zig"); +const message = @import("../message.zig"); const deck = @import("deck.zig"); const crt = @import("crt.zig"); const fonts = @import("fonts.zig"); // the Font builtin's half of the seam @@ -816,6 +817,12 @@ fn reloadChanged(core: *pardes.Pardes, gpa: std.mem.Allocator, watches: *[pardes if (h == slot.*.?.hash) continue; slot.*.?.hash = h; core.update(.{ .file_changed = .{ .pane = @intCast(id), .bytes = bytes } }); + // the hash moving IS "someone else wrote this file" (our own Save + // restamped it), so the pane says who did what to it — and it says it + // after the update, which cannot clear a message because only a key or + // a mouse event does + var mbuf: [256]u8 = undefined; + core.setMessage(id, message.stamp(&mbuf, "reloaded", f.path)); } } @@ -2623,6 +2630,11 @@ fn drainEffects( // our own write is about to come back as a watch event: restamp // from the bytes we just put there so it reads as "no change" if (watches[sf.pane]) |*w| w.hash = std.hash.Wyhash.hash(0, f.content); + // ...and say so on the pane's message row. AFTER the write, not + // beside it: every `continue` above is a save that did not happen + // and must not be reported as one. + var mbuf: [256]u8 = undefined; + core.setMessage(sf.pane, message.stamp(&mbuf, "saved", f.path)); }, .new_file => |request| { var path_buf: [4096:0]u8 = undefined; diff --git a/src/lsp/lsp_zls.zig b/src/lsp/lsp_zls.zig index 6090af1a..02b72f10 100644 --- a/src/lsp/lsp_zls.zig +++ b/src/lsp/lsp_zls.zig @@ -140,11 +140,13 @@ fn record(req: lsp.Req, us: u64, rows: usize, err: []const u8) void { } /// A duration cannot go in a snapshot golden — it differs every run. The -/// snapshot harness sets PARDES_LSP_NOTIME so the two introspection views -/// print `-` instead, the same way it pins PARDES_DUMP for the dump tests. -/// Nothing else reads it, and unset (i.e. every real run) shows real numbers. +/// snapshot harness sets PARDES_NOTIME so the two introspection views print `-` +/// instead, the same way it pins PARDES_DUMP for the dump tests. ONE flag, +/// meaning "no wall clock in user-visible text": message.zig reads the same one +/// to blank the transient message row's `HH:MM:SS`. Unset — i.e. every real run +/// — shows real numbers. fn hideTime() bool { - const v = std.c.getenv("PARDES_LSP_NOTIME") orelse return false; + const v = std.c.getenv("PARDES_NOTIME") orelse return false; return std.mem.span(v).len != 0; } diff --git a/src/message.zig b/src/message.zig new file mode 100644 index 00000000..0935ce96 --- /dev/null +++ b/src/message.zig @@ -0,0 +1,69 @@ +//! Native-shell ownership of the transient message row's one clock read. +//! +//! The core owns the row, the buffer and when it goes away (Pardes.setMessage); +//! what it does not own is a clock, and its header says so. So what happened is +//! narrated HERE — at the moment the IO it narrates actually finished, which is +//! also the only moment it is true: a save that failed says nothing — and handed +//! over as finished text. Both native shells post the same two events (a save, +//! an external reload), so the wording sits in one place instead of once per +//! shell, where the two copies would drift the first time either was reworded. +//! Same split, and the same reason, as temp_file.zig owning `New`'s mkstemp. +//! +//! LOCAL time, not UTC: this row is read by a person sitting in front of the +//! terminal. dump.zig stamps filenames in UTC because those are sorted, not +//! read. +const std = @import("std"); +const libc = std.c; + +/// glibc/musl/macOS `struct tm`. Only the first three fields are ever read; the +/// rest are declared because localtime_r writes the whole struct. +const Tm = extern struct { + sec: c_int, + min: c_int, + hour: c_int, + mday: c_int, + mon: c_int, + year: c_int, + wday: c_int, + yday: c_int, + isdst: c_int, + gmtoff: c_long, + zone: ?[*:0]const u8, +}; +extern "c" fn localtime_r(timep: *const libc.time_t, result: *Tm) ?*Tm; + +/// `HH:MM:SS ` into `buf`, e.g. `14:32:07 saved /etc/hosts`. +/// Written into a caller buffer rather than allocated, because the core's own +/// message buffer is fixed too and a message wider than a pane is one nobody +/// reads. A subject too long for the room left is cut from the FRONT: the tail +/// of a path is the part that identifies it. +/// +/// $PARDES_NOTIME blanks the DIGITS and nothing else. The snapshot harness sets +/// it (the same flag hides the language backend's durations) because a wall +/// clock cannot live in a golden — but a message that vanished under the +/// harness could not be pinned at all, so the row still renders, still says +/// what happened, and still occupies the same columns. +pub fn stamp(buf: []u8, verb: []const u8, subject: []const u8) []const u8 { + var clock: [8]u8 = "--:--:--".*; + const notime = if (libc.getenv("PARDES_NOTIME")) |v| std.mem.span(v).len != 0 else false; + if (!notime) { + var ts: libc.timespec = undefined; + _ = libc.clock_gettime(.REALTIME, &ts); + const secs: libc.time_t = ts.sec; + var tm: Tm = undefined; + if (localtime_r(&secs, &tm) != null) { + // unsigned on purpose: Zig 0.16 prints a `+` for a positive SIGNED + // int, and `{d:0>2}` on a c_int would spell 14:32:07 as +1:+3:+7 + _ = std.fmt.bufPrint(&clock, "{d:0>2}:{d:0>2}:{d:0>2}", .{ + @as(u32, @intCast(tm.hour)), + @as(u32, @intCast(tm.min)), + @as(u32, @intCast(tm.sec)), + }) catch {}; + } + } + const head = std.fmt.bufPrint(buf, "{s} {s} ", .{ &clock, verb }) catch return buf[0..0]; + const room = buf.len - head.len; + const tail = if (subject.len <= room) subject else subject[subject.len - room ..]; + @memcpy(buf[head.len..][0..tail.len], tail); + return buf[0 .. head.len + tail.len]; +} diff --git a/src/pardes.zig b/src/pardes.zig index 3acf5765..7ff5ba1f 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -3537,6 +3537,14 @@ pub const Pane = struct { /// into write effects. Bounded: replies are tiny escape sequences. reply: [256]u8 = undefined, reply_len: u16 = 0, + /// THE TRANSIENT MESSAGE: what just happened to this pane, drawn on its + /// LAST row until the next key or mouse event wipes it (see update). Fixed + /// and inline like `reply` above — a message is one short line, so a pane + /// that never sees one still costs nothing to carry it, and there is no + /// allocation to fail at the moment something is trying to be reported. + /// Written in exactly one place, Pardes.setMessage, by a SHELL. + msg: [256]u8 = undefined, + msg_len: u16 = 0, pub fn cwdSlice(p: *const Pane) []const u8 { return p.cwd[0..p.cwd_len]; @@ -4292,6 +4300,23 @@ pub const Pardes = struct { return true; } + /// Post the transient message on `id`'s last row. Called by a SHELL once + /// the IO it narrates has actually happened, exactly as openNewFile above + /// completes a `new_file` effect: the core neither writes files nor owns a + /// clock, so both the outcome and the wall time in `text` come from there + /// (message.zig spells it, once, for both native shells). + /// + /// Nothing here decides WHEN it goes away — update does, on the next key or + /// mouse event — and nothing here knows whether the row is free: an armed + /// prompt simply outranks a message at render time, so a message posted + /// under one is stored and invisible rather than refused. + pub fn setMessage(p: *Pardes, id: usize, text: []const u8) void { + if (id >= MAX_PANES) return; + const pane = p.panes[id] orelse return; + pane.msg_len = @intCast(@min(text.len, pane.msg.len)); + @memcpy(pane.msg[0..pane.msg_len], text[0..pane.msg_len]); + } + pub fn emit(p: *Pardes, e: Effect) void { p.effects.append(p.gpa, e) catch {}; } @@ -4308,6 +4333,24 @@ pub const Pardes = struct { } pub fn update(p: *Pardes, ev: Event) void { + // A transient message is exactly as old as your last input: touch the + // keyboard or the mouse and it is gone, on every pane, because a + // message is a report and you have just proved you are back. Only + // INPUT counts — a resize, pty output, a watch or an answering worker + // all repaint without you, and a message that a background shell could + // wipe would be one you never got to read. + // + // An ARMED PROMPT is a different occupant of the same row and is not + // touched here: it lives in tag_tail, it is what the keystroke is being + // typed INTO, and it ends at Enter or Esc. So clearing here can never + // fight one — at worst it clears something the prompt was already + // hiding. + switch (ev) { + .key, .mouse => for (p.panes) |slot| { + if (slot) |pane| pane.msg_len = 0; + }, + else => {}, + } switch (ev) { .resize => |sz| { if (comptime pdf_enabled) { @@ -11602,6 +11645,67 @@ pub const Pardes = struct { try p.renderPane(arena, pane, p.rects[id], id == p.active); } + // ---- the transient message row: a pane's LAST row, left-aligned ---- + // + // An OVERLAY, not geometry: no rect moves, no pane shrinks, and a pane + // with neither a message nor an armed prompt is not touched at all. + // Drawn after every pane's body so it lands OVER whatever that row was + // showing, and painted in the tag bar's own colours — it is the + // tagline's twin at the other end of the pane, and reading as chrome is + // what keeps it from being mistaken for content. Out here rather than + // at the end of renderPane because renderPane returns early for a + // native PDF page and for an image, and a `/` on a PDF is a real search + // whose prompt has to be visible like any other. + // + // Exactly two things can occupy the row and an ARMED PROMPT beats a + // MESSAGE, because they are not the same kind of thing: a message is a + // report of what already happened and the next keystroke wipes it, a + // prompt is what that keystroke is being typed into and it lives until + // Enter or Esc. + // + // ponytail: the row is draw-only. A click on it lands wherever the body + // under it says (tag clicks map to tag_col on the TAG row), so a prompt + // that moved down here cannot be clicked into or swept the way one on + // the tagline could — the keyboard still edits it in full. Upgrade path + // is a hit test here that maps a press on this row to tag_col + the + // marker offset, i.e. the tag row's own mapping with a constant added. + for (&p.panes, 0..) |*slot, id| { + const pane = slot.* orelse continue; + const r = p.rects[id]; + // no body row (a one-row pane, or one squeezed out entirely): the + // tag row is not ours to overwrite, so the message just waits + if (r.w <= config.GUTTER or r.h <= BOX_H) continue; + // the same one input model renderPane cut off the tagline; only one + // of the two can ever be armed (a body key arms one, exitTagEdit + // clears both) + const prompt_at: ?u16 = if (pane.search_edit) pane.search_at else if (pane.pipe_edit) pane.pipe_at else null; + const text = if (prompt_at) |at| + pane.tag_tail.items[@min(at, pane.tag_tail.items.len)..] + else + pane.msg[0..pane.msg_len]; + if (text.len == 0) continue; + const tx = r.x + config.GUTTER; + const tw = r.w - config.GUTTER; + const row = r.y + r.h - 1; + // the WHOLE row, the way the tagline fills its own before printing: + // a message is a section and not a stamp, and print writes only the + // cells it needs — so without the fill the body row shows through + // to the right of a short message and reads as one garbled line. + s.fill(tx, row, tw, 1, .{ .bg = .{ .rgb = chrome.tag_bg } }); + _ = s.print(tx, row, tw, text, .{ .fg = .{ .rgb = chrome.tag_fg }, .bg = .{ .rgb = chrome.tag_bg } }); + // ...and the cursor follows the text it edits. tag_col is a + // rendered-tag column, so the prompt's own column is it minus where + // the marker starts; a cursor LEFT of that is still over the part + // of the tag that stayed on the tagline, and the tag cursor + // renderPane already placed there is the right one. + if (id != p.active) continue; + const at = prompt_at orelse continue; + const prompt0 = (p.tagPrefix(pane) catch continue).len + at; + const col = @as(usize, pane.tag_col); + if (col >= prompt0 and col - prompt0 < tw) + s.cursor = .{ .x = tx + @as(u16, @intCast(col - prompt0)), .y = row, .bar = pane.mode == .insert }; + } + // global tagbar: full width, top row s.fill(0, 0, s.cols, TOPBAR_H, .{ .bg = .{ .rgb = chrome.tag_bg } }); var tb_buf: [1200]u8 = undefined; @@ -11791,7 +11895,24 @@ pub const Pardes = struct { const tag_style: CellStyle = .{ .fg = .{ .rgb = chrome.tag_fg }, .bg = .{ .rgb = chrome.tag_bg } }; s.fill(tx, r.y, tw, BOX_H, .{ .bg = .{ .rgb = chrome.tag_bg } }); const tag = try p.tagText(arena, pane); - _ = s.print(tx, r.y, tw, tag, tag_style); + // An armed input — `/`, Find, Grep, Rename, WsSymbols, Select/Split, + // `|` — is still TYPED INTO the tag tail: same buffer, same offsets, + // same one-line modal editor, same Enter and same Esc. Only where it is + // DRAWN moved. It is cut off the tagline here and printed on the pane's + // message row instead (see render), so the builtins in the tail stay + // readable while you type instead of being pushed off the right edge by + // a long pattern. + // + // search_at/pipe_at are TAIL offsets and tag_col is a rendered-tag + // column, so the cut is the offset plus the prefix the tail is missing + // — which is what tag.len - tail.len is, without formatting the prefix + // a second time. + const prompt_at: ?u16 = if (pane.search_edit) pane.search_at else if (pane.pipe_edit) pane.pipe_at else null; + const tag_cut = if (prompt_at) |at| + @min(tag.len, tag.len - @min(tag.len, pane.tag_tail.items.len) + at) + else + tag.len; + _ = s.print(tx, r.y, tw, tag[0..tag_cut], tag_style); // pending SPC leader path, on the ACTIVE pane's tag at its RIGHT edge: // the same rule as the `/` input — state you can see, nothing that // disappears — but right-aligned, so it never collides with the diff --git a/src/tty/tty.zig b/src/tty/tty.zig index 6047e5a2..b5b4ff75 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -12,6 +12,7 @@ const pardes = @import("../pardes.zig"); const tracy = @import("../tracy.zig"); const look = @import("../look.zig"); const temp_file = @import("../temp_file.zig"); +const message = @import("../message.zig"); const selection_pipe = @import("../selection_pipe.zig"); extern "c" fn forkpty(amaster: *c_int, name: ?[*:0]u8, termp: ?*const anyopaque, winp: ?*const posix.winsize) c_int; @@ -648,6 +649,12 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { if (h == slot.*.?.hash) continue; slot.*.?.hash = h; core.update(.{ .file_changed = .{ .pane = @intCast(id), .bytes = bytes } }); + // the hash moving IS "someone else wrote this file" (our own + // Save restamped it above), so the pane says who did what to it + // — and it says it after the update, which cannot clear a + // message because only a key or a mouse event does + var mbuf: [256]u8 = undefined; + core.setMessage(id, message.stamp(&mbuf, "reloaded", f.path)); } } @@ -882,6 +889,11 @@ fn drainEffects( // our own write is about to come back as a watch event: restamp // from the bytes we just put there so it reads as "no change" if (watches[sf.pane]) |*w| w.hash = std.hash.Wyhash.hash(0, f.content); + // ...and say so on the pane's message row. AFTER the write, not + // beside it: every `continue` above is a save that did not happen + // and must not be reported as one. + var mbuf: [256]u8 = undefined; + core.setMessage(sf.pane, message.stamp(&mbuf, "saved", f.path)); }, .new_file => |request| { var path_buf: [4096:0]u8 = undefined; diff --git a/test/snapshot.zig b/test/snapshot.zig index 0a2b299d..39350bee 100644 --- a/test/snapshot.zig +++ b/test/snapshot.zig @@ -313,9 +313,11 @@ fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const _ = setenv("XDG_CONFIG_HOME", config_home, 1); _ = setenv("TERM", "xterm-256color", 1); _ = setenv("LC_ALL", "C", 1); - // durations cannot live in a golden; the lsp introspection views print - // `-` for them when this is set (see lsp_zls.zig hideTime) - _ = setenv("PARDES_LSP_NOTIME", "1", 1); + // a wall clock cannot live in a golden; everything that would print one + // prints fixed characters instead when this is set — the lsp introspection + // views' durations (lsp_zls.zig hideTime) and the transient message row's + // time of day (message.zig stamp) + _ = setenv("PARDES_NOTIME", "1", 1); // Pin the dump path: live runs use a timestamped file under the user data // dir (dump.outPath), which would make the dump/load/restore goldens // nondeterministic. Per-SCRIPT, not one shared /tmp/pardes-dump.zon: four diff --git a/test/snapshots/filewatch.golden b/test/snapshots/filewatch.golden index 650c290d..3224dd27 100644 --- a/test/snapshots/filewatch.golden +++ b/test/snapshots/filewatch.golden @@ -45,7 +45,7 @@ | | | -| +| --:--:-- reloaded /tmp/pardes-snap/filewatch/cw | /tmp/pardes-snap/filewatch/cwd/sub/s.txt Save Ne /tmp/pardes-snap/filewatch/cwd New Del | 1 line 1 | 2 line 2 sub w.txt @@ -76,7 +76,7 @@ | | | -| +| --:--:-- reloaded /tmp/pardes-snap/filewatch/cw | /tmp/pardes-snap/filewatch/cwd/sub/s.txt Save Ne /tmp/pardes-snap/filewatch/cwd New Del | 1 RENAMED | 2 sub w.txt @@ -90,7 +90,7 @@ | | | -| +| --:--:-- reloaded /tmp/pardes-snap/filewatch/cw == snap undone grid=150x30 cursor=15,2 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/filewatch/cwd/w.txt Save New De /tmp/pardes-snap/filewatch/cwd New Del /tmp/pardes-snap/filewatch/cwd New Del diff --git a/test/snapshots/find.golden b/test/snapshots/find.golden index 6217c428..6419cba0 100644 --- a/test/snapshots/find.golden +++ b/test/snapshots/find.golden @@ -39,9 +39,9 @@ | | | -== snap armed grid=120x40 cursor=56,1 +== snap armed grid=120x40 cursor=9,39 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/find/cwd/here.txt Save New Del Find / +| /tmp/pardes-snap/find/cwd/here.txt Save New Del | 1 Find | | @@ -79,10 +79,10 @@ | | | -| -== snap typing grid=120x40 cursor=59,1 +| Find / +== snap typing grid=120x40 cursor=12,39 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/find/cwd/here.txt Save New Del Find /zqm +| /tmp/pardes-snap/find/cwd/here.txt Save New Del | 1 Find | | @@ -120,7 +120,7 @@ | | | -| +| Find /zqm == snap results grid=120x40 cursor=7,2 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/find/cwd/here.txt Save New Del @@ -367,13 +367,13 @@ | 1 sub/ZQMARK-beta.txt | 2 sub/nest/zqmark-gamma.txt | 3 zqmark-alpha.txt -== snap word grid=120x40 cursor=56,1 +== snap word grid=120x40 cursor=9,5 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/find/cwd/here.txt Save New Del Find / +| /tmp/pardes-snap/find/cwd/here.txt Save New Del | 1 Find | | -| +| Find / | /tmp/pardes-snap/find/cwd/zqmark-alpha.txt Save New Del | 1 alpha body | diff --git a/test/snapshots/fsearch.golden b/test/snapshots/fsearch.golden index 61866b75..08132e28 100644 --- a/test/snapshots/fsearch.golden +++ b/test/snapshots/fsearch.golden @@ -29,9 +29,9 @@ | | | -== snap typing grid=180x30 cursor=55,1 +== snap typing grid=180x30 cursor=8,29 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/fsearch/cwd/g.txt Save New Del /MARK /tmp/pardes-snap/fsearch/cwd New Del /tmp/pardes-snap/fsearch/cwd New Del +| /tmp/pardes-snap/fsearch/cwd/g.txt Save New Del /tmp/pardes-snap/fsearch/cwd New Del /tmp/pardes-snap/fsearch/cwd New Del | 1 l1 | 2 MARK a g.txt g.txt | 3 l3 @@ -59,7 +59,7 @@ | | | -| +| /MARK == snap results grid=180x30 cursor=7,2 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/fsearch/cwd/g.txt Save New Del /tmp/pardes-snap/fsearch/cwd New Del /tmp/pardes-snap/fsearch/cwd New Del diff --git a/test/snapshots/grep.golden b/test/snapshots/grep.golden index 01761d87..f64119a8 100644 --- a/test/snapshots/grep.golden +++ b/test/snapshots/grep.golden @@ -29,9 +29,9 @@ | | | -== snap typing grid=180x30 cursor=59,1 +== snap typing grid=180x30 cursor=15,29 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/grep/cwd/a.txt Save New Del Grep /TARGET /tmp/pardes-snap/grep/cwd New Del /tmp/pardes-snap/grep/cwd New Del +| /tmp/pardes-snap/grep/cwd/a.txt Save New Del /tmp/pardes-snap/grep/cwd New Del /tmp/pardes-snap/grep/cwd New Del | 1 hello TARGET one | 2 nothing here a.txt noise.txt sub a.txt noise.txt sub | 3 last TARGET line @@ -59,7 +59,7 @@ | | | -| +| Grep /TARGET == snap results grid=180x30 cursor=7,2 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/grep/cwd/a.txt Save New Del /tmp/pardes-snap/grep/cwd New Del /tmp/pardes-snap/grep/cwd New Del diff --git a/test/snapshots/msg.golden b/test/snapshots/msg.golden new file mode 100644 index 00000000..67ab94ea --- /dev/null +++ b/test/snapshots/msg.golden @@ -0,0 +1,166 @@ +== snap opened grid=70x14 cursor=7,2 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/msg/cwd/a.txt Save New Del +| 1 line 1 +| 2 line 2 +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 +| +| +| +| +| +== snap saved grid=70x14 cursor=7,2 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/msg/cwd/a.txt Save New Del +| 1 line 1 +| 2 line 2 +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 +| +| +| +| +| --:--:-- saved /tmp/pardes-snap/msg/cwd/a.txt +== snap repainted grid=70x16 cursor=7,2 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/msg/cwd/a.txt Save New Del +| 1 line 1 +| 2 line 2 +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 +| +| +| +| +| +| +| --:--:-- saved /tmp/pardes-snap/msg/cwd/a.txt +== snap keycleared grid=70x16 cursor=7,3 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/msg/cwd/a.txt Save New Del +| 1 line 1 +| 2 line 2 +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 +| +| +| +| +| +| +| +== snap saved-again grid=70x16 cursor=7,3 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/msg/cwd/a.txt Save New Del +| 1 line 1 +| 2 line 2 +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 +| +| +| +| +| +| +| --:--:-- saved /tmp/pardes-snap/msg/cwd/a.txt +== snap mousecleared grid=70x16 cursor=7,3 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/msg/cwd/a.txt Save New Del +| 1 line 1 +| 2 line 2 +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 +| +| +| +| +| +| +| +== snap reloaded grid=70x16 cursor=7,3 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/msg/cwd/a.txt Save New Del +| 1 EXTERNAL +| 2 +| +| +| +| +| +| +| +| +| +| +| +| --:--:-- reloaded /tmp/pardes-snap/msg/cwd/a.txt +== snap prompt grid=70x16 cursor=7,15 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/msg/cwd/a.txt Save New Del +| 1 EXTERNAL +| 2 +| +| +| +| +| +| +| +| +| +| +| +| /EXT +== snap prompt-wins grid=70x16 cursor=7,15 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/msg/cwd/a.txt Save New Del +| 1 AGAIN +| 2 +| +| +| +| +| +| +| +| +| +| +| +| /EXT +== snap esc grid=70x16 cursor=7,3 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/msg/cwd/a.txt Save New Del +| 1 AGAIN +| 2 +| +| +| +| +| +| +| +| +| +| +| +| diff --git a/test/snapshots/msg.snap b/test/snapshots/msg.snap new file mode 100644 index 00000000..640e9a0c --- /dev/null +++ b/test/snapshots/msg.snap @@ -0,0 +1,77 @@ +# The transient message row: the LAST row of a pane, left-aligned at the text +# area's origin, saying what just happened to that pane and when. An OVERLAY, +# not geometry — nothing shrinks to make room, so a pane with nothing to say is +# not touched at all, which is why the other 71 goldens only moved where a +# prompt or a message actually appears. +# +# The clock reads `--:--:--` here and only here: $PARDES_NOTIME (set by +# snapshot.zig, the same flag that hides the language backend's durations) +# blanks the DIGITS rather than the row, because a message that disappeared +# under the harness could not be pinned at all. +# +# The row has exactly two possible occupants and they are not the same kind of +# thing. A MESSAGE is a report of something that already happened, and the next +# key or mouse event wipes it. An armed PROMPT is what that keystroke is being +# typed INTO, it lives until Enter or Esc, and it WINS. +lines a.txt 6 +start 14 70 a.txt +wait 8000 New Newcol +wait 8000 a.txt +stable 700 20000 +snap opened +# Save (SPC f s) writes the file; the SHELL is where the write and the clock +# both are, so the shell is what posts the message once the bytes are down. +key space f s +stable 700 15000 +snap saved +# A REPAINT is not input. A resize redraws every cell in the window from +# scratch and the message is still there on the other side of it — the row is +# state, not one frame's flash. (Different rows, or the kernel sends no +# SIGWINCH and nothing repaints.) +resize 16 70 +stable 700 15000 +snap repainted +# ...but the next KEY takes it away. `j` is a plain cursor motion and changes +# nothing else on screen, so the vanishing message is the whole diff. +key j +stable 700 15000 +snap keycleared +# Again, and this time a bare mouse MOTION — no button, nothing clicked — +# clears it: any mouse event counts, the rule is "you are back", not "you did +# something". +key space f s +stable 700 15000 +snap saved-again +motion 20 6 +stable 700 15000 +snap mousecleared +# The other message: an external write lands in the pane (filewatch.snap owns +# that contract) and the row says which of the two things happened. `run` is a +# child of the RUNNER, so the app receives no keystroke here and nothing but +# the watch could have posted this. +run printf 'EXTERNAL\n' > a.txt +wait 10000 EXTERNAL +stable 700 15000 +snap reloaded +# `/` still types into the tag tail — same buffer, same offsets, same one-line +# editor, same Enter and Esc — but it is DRAWN here now, so the tagline keeps +# its builtins instead of losing them to a long pattern. Note the tag row above +# ends at `Del`: the marker and the pattern are no longer on it at all. (The +# `/` keystroke also cleared the reload message, which is the rule above.) +key / +text EXT +stable 700 15000 +snap prompt +# ...and a message arriving UNDER an armed prompt does not disturb it: a second +# external write reloads the pane and posts one, the row stays the prompt's. +run printf 'AGAIN\n' > a.txt +wait 10000 AGAIN +stable 700 15000 +snap prompt-wins +# Esc abandons the input, and the very key that abandoned it is also the key +# that clears the message queued underneath — so the row goes empty rather than +# handing back a report from before you started typing. +key esc +settle 100 +stable 700 15000 +snap esc diff --git a/test/snapshots/pipe.golden b/test/snapshots/pipe.golden index 42e12959..0cd57642 100644 --- a/test/snapshots/pipe.golden +++ b/test/snapshots/pipe.golden @@ -1,6 +1,6 @@ -== snap prompt grid=180x12 cursor=61,1 +== snap prompt grid=180x12 cursor=14,11 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/pipe/cwd/pipe.txt Save New Del |tr a-z A-Z +| /tmp/pardes-snap/pipe/cwd/pipe.txt Save New Del | 1 alpha beta | 2 gamma delta | @@ -10,7 +10,7 @@ | | | -| +| |tr a-z A-Z == snap filtered grid=180x12 cursor=17,2 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/pipe/cwd/pipe.txt Save New Del diff --git a/test/snapshots/psearch.golden b/test/snapshots/psearch.golden index a3e65037..0ce9f2ee 100644 --- a/test/snapshots/psearch.golden +++ b/test/snapshots/psearch.golden @@ -29,9 +29,9 @@ | | | -== snap typing grid=140x30 cursor=44,1 +== snap typing grid=140x30 cursor=8,29 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/psearch/cwd New Del /MARK +| /tmp/pardes-snap/psearch/cwd New Del | | x MARK a | zz @@ -59,7 +59,7 @@ | | | -| +| /MARK == snap results grid=140x30 cursor=4,8 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/psearch/cwd New Del diff --git a/test/snapshots/selregex.golden b/test/snapshots/selregex.golden index b66d0d1e..8317a2a6 100644 --- a/test/snapshots/selregex.golden +++ b/test/snapshots/selregex.golden @@ -1,6 +1,6 @@ -== snap prompt-armed grid=160x24 cursor=59,1 +== snap prompt-armed grid=160x24 cursor=11,23 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/selregex/cwd/m.zig Save New Del Select / +| /tmp/pardes-snap/selregex/cwd/m.zig Save New Del | 1 const one = 1; | 2 const two = 2; | 3 const three = 3; @@ -22,10 +22,10 @@ | | | -| -== snap preview grid=160x24 cursor=64,1 +| Select / +== snap preview grid=160x24 cursor=16,23 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/selregex/cwd/m.zig Save New Del Select /const +| /tmp/pardes-snap/selregex/cwd/m.zig Save New Del | 1 const one = 1; | 2 const two = 2; | 3 const three = 3; @@ -47,10 +47,10 @@ | | | -| +| Select /const == style preview |0: 0-56 #949494,#121212, 57-159 d,#121212, -|1: 0-1 #ffffff,#626262, 2-63 #949494,#121212, 64-159 d,#121212, +|1: 0-1 #ffffff,#626262, 2-49 #949494,#121212, 50-159 d,#121212, |2: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #080808,#80a0ff, 11-11 #80a0ff,#080808, 12-18 #bdbdbd,#080808, 19-19 #e3c78a,#080808, 20-20 #bdbdbd,#080808, 21-159 d,#080808, |3: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #080808,#445483, 11-11 #80a0ff,#080808, 12-18 #bdbdbd,#080808, 19-19 #e3c78a,#080808, 20-20 #bdbdbd,#080808, 21-159 d,#080808, |4: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #080808,#445483, 11-11 #80a0ff,#080808, 12-20 #bdbdbd,#080808, 21-21 #e3c78a,#080808, 22-22 #bdbdbd,#080808, 23-159 d,#080808, @@ -72,7 +72,7 @@ |20: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-159 d,#080808, |21: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-159 d,#080808, |22: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-159 d,#080808, -|23: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-159 d,#080808, +|23: 0-0 d,#444444, 1-1 d,#080808, 2-15 #949494,#121212, 16-159 d,#121212, == snap three-selected grid=160x24 cursor=11,2 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/selregex/cwd/m.zig Save New Del @@ -123,9 +123,9 @@ |21: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-159 d,#080808, |22: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-159 d,#080808, |23: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-159 d,#080808, -== snap narrowed grid=160x24 cursor=60,1 +== snap narrowed grid=160x24 cursor=12,23 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/selregex/cwd/m.zig Save New Del Select /t +| /tmp/pardes-snap/selregex/cwd/m.zig Save New Del | 1 const one = 1; | 2 const two = 2; | 3 const three = 3; @@ -147,10 +147,10 @@ | | | -| +| Select /t == style narrowed |0: 0-56 #949494,#121212, 57-159 d,#121212, -|1: 0-1 #ffffff,#626262, 2-59 #949494,#121212, 60-159 d,#121212, +|1: 0-1 #ffffff,#626262, 2-49 #949494,#121212, 50-159 d,#121212, |2: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-11 #80a0ff,#080808, 12-18 #bdbdbd,#080808, 19-19 #e3c78a,#080808, 20-20 #bdbdbd,#080808, 21-159 d,#080808, |3: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-11 #80a0ff,#080808, 12-18 #bdbdbd,#080808, 19-19 #e3c78a,#080808, 20-20 #bdbdbd,#080808, 21-159 d,#080808, |4: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-11 #80a0ff,#080808, 12-20 #bdbdbd,#080808, 21-21 #e3c78a,#080808, 22-22 #bdbdbd,#080808, 23-159 d,#080808, @@ -172,7 +172,7 @@ |20: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-159 d,#080808, |21: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-159 d,#080808, |22: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-159 d,#080808, -|23: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-159 d,#080808, +|23: 0-0 d,#444444, 1-1 d,#080808, 2-11 #949494,#121212, 12-159 d,#121212, == snap esc-restores grid=160x24 cursor=11,2 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/selregex/cwd/m.zig Save New Del diff --git a/test/snapshots/tagcmd.golden b/test/snapshots/tagcmd.golden index 6bba1787..57fbd567 100644 --- a/test/snapshots/tagcmd.golden +++ b/test/snapshots/tagcmd.golden @@ -121,7 +121,7 @@ | | | -| +| --:--:-- saved /tmp/pardes-snap/tagcmd/cwd/c.txt == file written ZZEDITline 1 line 2 -- cgit v1.3