summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-05 16:28:03 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit79a8980e46d8ed1f9ef7e3f4e44db7ee58e9f88e (patch)
tree65ebb32c55b4b91efc6f23c2142a0971a602611c /src/pardes.zig
parent1891a51ed2b64eec3964d3af3807be90d08a14c2 (diff)
downloadpardes-79a8980e46d8ed1f9ef7e3f4e44db7ee58e9f88e.tar.gz
pardes-79a8980e46d8ed1f9ef7e3f4e44db7ee58e9f88e.zip
add the transient message row
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig123
1 files changed, 122 insertions, 1 deletions
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