summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--features.txt3
-rw-r--r--src/builtins.zig3
-rw-r--r--src/panes.zig28
-rw-r--r--src/pardes.zig102
4 files changed, 102 insertions, 34 deletions
diff --git a/features.txt b/features.txt
index 7515d455..e05f6f07 100644
--- a/features.txt
+++ b/features.txt
@@ -136,3 +136,6 @@ gamepad subsystem. Two fields and a branch for nothing, so it went back out.
Conclusion for both platforms: nothing in pardes's own logic is slow at startup. The cost is the Debug build's allocator
tracing and the GPU/driver stack coming up, and the only lever on either is the build mode.
+
+The Last builtin should fall back to a heuristic when the jumplist has nothing to offer, so the bare tty layout -- a shell with an unfocused text pane under it --
+alternates with Last even though that text pane was never focused and is not on the jumplist.
diff --git a/src/builtins.zig b/src/builtins.zig
index aa2b2d6d..30a5a50f 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -701,6 +701,9 @@ pub const Last = struct {
// Restore the cursor without recentering the pane's retained view.
if (j.pane != c.id) return c.p.focusPaneLine(j.pane, .{ .line = j.line, .col = j.col }, .keep);
}
+ // Nothing on the jumplist to go back to, which is the ordinary case
+ // for a pane that opened beside this one and was never focused.
+ if (c.p.neighbourPane(c.id)) |next| c.p.focusPaneLine(next, .{ .line = 0, .col = 0 }, .keep);
}
};
diff --git a/src/panes.zig b/src/panes.zig
index 9a6802d0..e41698bb 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -183,6 +183,34 @@ pub const Pane = struct {
tty_filter: bool = false,
msg: [256]u8 = undefined,
msg_len: u16 = 0,
+ /// The transient lines this pane shows between its body and its tagline:
+ /// a builtin's message, the pending leader chord, a prompt waiting for
+ /// input. Rebuilt every frame from the state that owns each one, so a
+ /// line that goes away is simply not added again and the rest close the
+ /// gap -- there is no second copy of the truth to fall out of step. The
+ /// body layer reserves exactly `len` rows for them, the way it already
+ /// reserves rows for sticky context headers.
+ notices: Notices = .{},
+
+ pub const Notices = struct {
+ /// Painted bottom-up in this order, so a prompt keeps the row nearest
+ /// the tagline that it has always had, and its cursor with it.
+ pub const Kind = enum { message, leader, prompt };
+ pub const max = @typeInfo(Kind).@"enum".fields.len;
+
+ kinds: [max]Kind = undefined,
+ len: u8 = 0,
+
+ pub fn push(n: *Notices, kind: Kind) void {
+ if (n.len >= max) return;
+ n.kinds[n.len] = kind;
+ n.len += 1;
+ }
+
+ pub fn slice(n: *const Notices) []const Kind {
+ return n.kinds[0..n.len];
+ }
+ };
pub fn clearPointerSelection(pane: *Pane, slot: usize) void {
if (pane.pointer_selections[slot]) |selection| {
diff --git a/src/pardes.zig b/src/pardes.zig
index 7e18db59..51442034 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -13090,6 +13090,22 @@ pub const Pardes = struct {
};
}
+ /// The pane Last should fall back to when the jumplist has nothing: the
+ /// one below in this column, wrapping to the top, and any other live pane
+ /// failing that. A pane that opened beside this one and was never focused
+ /// -- the text pane the bare tty layout puts under the shell -- is not on
+ /// the jumplist, and alternating with it is exactly what Last is for.
+ pub fn neighbourPane(p: *Pardes, from: usize) ?usize {
+ if (layout.findPane(p, from)) |at| {
+ const n = p.col_n[at.col];
+ if (n > 1) return p.col_panes[at.col][(at.idx + 1) % n];
+ }
+ for (p.panes, 0..) |slot, id| {
+ if (slot != null and id != from) return id;
+ }
+ return null;
+ }
+
pub fn prevFocus(p: *Pardes, closing: usize) ?usize {
var i = p.njumps;
while (i > 0) {
@@ -13359,50 +13375,54 @@ pub const Pardes = struct {
pane.body_visible_rows = 0;
pane.context_row_limit = null;
s.body_layers[id].rows = 0;
+ p.collectNotices(pane, p.rects[id], id);
try p.renderPane(arena, pane, p.rects[id], id, id == p.active);
try p.renderBodyLayer(arena, pane, p.rects[id], id);
}
for (&p.panes, 0..) |*slot, id| {
const pane = slot.* orelse continue;
+ if (pane.notices.len == 0) continue;
const r = p.rects[id];
- if (r.w <= config.GUTTER or r.h <= BOX_H) continue;
- const prompt_at = pane.promptAt();
- const text = if (prompt_at) |at|
- pane.tagSlice()[@min(at, pane.tag_tail_len)..]
- else
- pane.msg[0..pane.msg_len];
- // The pending SPC path is the third thing that wants this row, and
- // it can want it while the row is otherwise empty.
- const leader_here = id == p.active and p.leader_on;
- if (text.len == 0 and !leader_here) continue;
const tx = r.x + config.GUTTER;
const tw = r.w - config.GUTTER;
- const row = if (p.settings.tag_bottom) r.y + r.h - 1 - BOX_H else r.y + r.h - 1;
const msg_style: CellStyle = .{
.fg = if (th.fg) |c| .{ .rgb = c } else .default,
.bg = if (th.bg) |c| .{ .rgb = c } else .default,
};
- s.fill(tx, row, tw, 1, .{ .bg = msg_style.bg });
- if (text.len > 0) _ = s.print(tx, row, tw, text, msg_style);
- if (leader_here) {
- var ibuf: [16]u8 = @splat(' ');
- @memcpy(ibuf[1..4], "SPC");
- var iw: usize = 4;
- for (p.leader_keys[0..p.leader_n]) |ch| {
- ibuf[iw + 1] = ch;
- iw += 2;
+ // The stack sits directly above the tagline, one line to a row, in
+ // the order collectNotices chose. A single notice therefore lands
+ // on exactly the row the message used to have to itself.
+ const bottom = if (p.settings.tag_bottom) r.y + r.h - 1 - BOX_H else r.y + r.h - 1;
+ for (pane.notices.slice(), 0..) |kind, i| {
+ const row = bottom - @as(u16, @intCast(pane.notices.len - 1 - i));
+ s.fill(tx, row, tw, 1, .{ .bg = msg_style.bg });
+ switch (kind) {
+ .message => _ = s.print(tx, row, tw, pane.msg[0..pane.msg_len], msg_style),
+ .leader => {
+ var ibuf: [16]u8 = @splat(' ');
+ @memcpy(ibuf[1..4], "SPC");
+ var iw: usize = 4;
+ for (p.leader_keys[0..p.leader_n]) |ch| {
+ ibuf[iw + 1] = ch;
+ iw += 2;
+ }
+ const w: u16 = @intCast(iw);
+ if (w < tw) _ = s.print(tx + tw - w, row, w, ibuf[0..iw], msg_style);
+ },
+ .prompt => {
+ const at = pane.promptAt() orelse continue;
+ const text = pane.tagSlice()[@min(at, pane.tag_tail_len)..];
+ _ = s.print(tx, row, tw, text, msg_style);
+ if (id != p.active) continue;
+ const prompt0 = (p.tagPrefix(pane) catch continue).len + at;
+ const col = @as(usize, pane.tag_col);
+ if (col >= prompt0) {
+ const prompt_col = panes.File.displayWidth(text[0..@min(col - prompt0, text.len)]);
+ if (prompt_col < tw)
+ s.cursor = .{ .x = tx + @as(u16, @intCast(prompt_col)), .y = row, .bar = pane.mode == .insert };
+ }
+ },
}
- const w: u16 = @intCast(iw);
- if (w < tw) _ = s.print(tx + tw - w, row, w, ibuf[0..iw], msg_style);
- }
- 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) {
- const prompt_col = panes.File.displayWidth(text[0..@min(col - prompt0, text.len)]);
- if (prompt_col < tw)
- s.cursor = .{ .x = tx + @as(u16, @intCast(prompt_col)), .y = row, .bar = pane.mode == .insert };
}
}
@@ -13921,12 +13941,26 @@ pub const Pardes = struct {
}
}
+ /// The one place that decides which transient lines a pane is showing and
+ /// in what order. Everything downstream reads the answer: the body layer
+ /// so it does not paint under them, and the paint pass so it can stack
+ /// them instead of letting the last one win the row.
+ fn collectNotices(p: *Pardes, pane: *Pane, r: Rect, id: usize) void {
+ pane.notices.len = 0;
+ if (r.w <= config.GUTTER or r.h <= BOX_H) return;
+ if (pane.msg_len > 0) pane.notices.push(.message);
+ if (id == p.active and p.leader_on) pane.notices.push(.leader);
+ if (pane.promptAt()) |at| {
+ if (pane.tagSlice()[@min(at, pane.tag_tail_len)..].len > 0) pane.notices.push(.prompt);
+ }
+ // Never claim so many rows that the body has none left.
+ pane.notices.len = @min(pane.notices.len, r.h - BOX_H -| 1);
+ }
+
fn renderBodyLayer(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize) !void {
if (!p.settings.tree_context_tag_style or pane.context_rows == 0 or pane.file == null or pane.collapsed) return;
if (r.w <= config.GUTTER or r.h <= BOX_H) return;
- const message = if (pane.promptAt()) |at| pane.tagSlice()[@min(at, pane.tag_tail_len)..].len > 0 else pane.msg_len > 0;
- const reserve: u16 = if (message or (id == p.active and p.leader_on)) 1 else 0;
- const viewport_rows = r.h - BOX_H -| reserve;
+ const viewport_rows = r.h - BOX_H -| @as(u16, pane.notices.len);
if (viewport_rows == 0) return;
// The canonical fixed grid remains available to terminal clients. The
// separate body layer reserves enough rows for any compact font size.