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