summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig69
1 files changed, 66 insertions, 3 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 51442034..e9908d65 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -304,7 +304,7 @@ const pre_tty_first_terminal_pane_tail = " Save Tty Del Togglettymode Filter Col
const pre_mode_terminal_pane_tail = " Tty Save Del Togglettymode Filter Collapse";
// ...and the defaults from before the closing word moved to the end of every
// tagline, where a click cannot land on it by overshooting the one before.
-const pre_close_last_pane_tail = " Tty Collapse Del";
+const pre_close_last_pane_tail = " Tty Del Collapse";
const pre_close_last_file_pane_tail = " Save Tty Del Collapse";
const pre_close_last_pdf_pane_tail = " Tty Del PdfSections PdfTint Collapse";
const pre_close_last_terminal_pane_tail = " Tty Save Del Mode Filter Collapse";
@@ -6099,6 +6099,9 @@ pub const Pardes = struct {
fs: filesystem.Namespace = .{},
boot: Boot = .shell,
+ /// Is there a frame worth drawing? Cleared once one is presented, set by
+ /// anything that can change what is on screen. The first frame always is.
+ needs_frame: bool = true,
/// Builtins announce themselves on the message row. Off while startup
/// configuration is applied: those lines are nobody clicking, and should
/// not leave the row holding the name of the last one.
@@ -7137,13 +7140,28 @@ pub const Pardes = struct {
// A quitting frame has already freed what it would draw.
if (p.quit) return;
if (v.poll_frame) |f| f(h.ctx);
+ // Nothing to draw: do not spend a frame, and above all do not block on
+ // the display for one. This is what keeps the frame rate governing
+ // rendering and nothing else.
+ if (!p.needs_frame and !p.animationActive()) return;
_ = p.frame_arena.reset(.retain_capacity);
const surface = try p.render(p.frame_arena.allocator());
if (v.present) |f| f(h.ctx, surface);
if (v.post_present) |f| f(h.ctx);
+ p.needs_frame = false;
}
pub fn update(p: *Pardes, ev: Event) void {
+ // Only two events cannot change the screen: a tick with nothing
+ // animating, and a filesystem request that only reads. The second is
+ // what 9P traffic overwhelmingly is, and answering it used to drag a
+ // whole vsync-blocked frame behind it -- which is why a round trip on
+ // a local socket cost a frame instead of a few microseconds.
+ p.needs_frame = p.needs_frame or switch (ev) {
+ .tick => false,
+ .fs_req => |req| ctlfs.changesPane(req),
+ else => true,
+ };
p.shell_rows.sweep(p.gpa);
switch (ev) {
.tick => {},
@@ -13953,8 +13971,17 @@ pub const Pardes = struct {
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);
+ // A pane only has so many rows to give. When they do not all fit the
+ // LAST ones survive, because the prompt is last and it is the one
+ // carrying the cursor: a prompt you cannot see is a prompt you type
+ // into blind. A two-row pane spends its only body row on one notice,
+ // which is what the single shared row used to do anyway.
+ const room: u8 = @intCast(@min(@as(u16, Pane.Notices.max), r.h - BOX_H));
+ if (pane.notices.len > room) {
+ const drop = pane.notices.len - room;
+ for (0..room) |i| pane.notices.kinds[i] = pane.notices.kinds[i + drop];
+ pane.notices.len = room;
+ }
}
fn renderBodyLayer(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize) !void {
@@ -14603,6 +14630,42 @@ test "Esc back into a tty leaves its view at the prompt" {
try std.testing.expectEqual(live, sp.terminal.?.vt.screens.active.pages.scrollbar().offset);
}
+test "closing the last pane in a column leaves an empty one in its place" {
+ if (platform == .web) return;
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 24 });
+ defer p.deinit();
+ const shell = p.active;
+ p.newScratchColumn(shell);
+ const lone = p.active;
+ try std.testing.expect(lone != shell);
+ try std.testing.expectEqual(@as(usize, 2), p.ncol);
+ const at = layout.findPane(p, lone).?;
+ try std.testing.expectEqual(@as(usize, 1), p.col_n[at.col]);
+ const had = try std.testing.allocator.dupe(u8, p.panes[lone].?.cwdSlice());
+ defer std.testing.allocator.free(had);
+
+ try p.removePane(lone);
+ p.sync();
+
+ // The column outlives the pane: still two columns, and the one that was
+ // emptied holds a fresh empty pane in the same directory.
+ try std.testing.expectEqual(@as(usize, 2), p.ncol);
+ try std.testing.expectEqual(@as(usize, 1), p.col_n[at.col]);
+ const heir = p.col_panes[at.col][0];
+ try std.testing.expect(heir != lone);
+ try std.testing.expect(p.panes[lone] == null);
+ const hp = p.panes[heir].?;
+ try std.testing.expectEqualStrings("", hp.file.?.content);
+ try std.testing.expectEqualStrings(had, hp.cwdSlice());
+ try std.testing.expectEqual(heir, p.active);
+ try std.testing.expect(!p.quit);
+
+ // ...and Delcol is what actually takes the column away.
+ try p.removeColumn(heir);
+ p.sync();
+ try std.testing.expectEqual(@as(usize, 1), p.ncol);
+}
+
test "the bare tty startup opens a text pane under the shell, unfocused" {
if (platform == .web) return;
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .boot = .tty });