summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/macos.zig4
-rw-r--r--src/main.zig11
-rw-r--r--src/pardes.zig69
3 files changed, 77 insertions, 7 deletions
diff --git a/src/macos.zig b/src/macos.zig
index d263cfc6..83b5eac9 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -709,6 +709,10 @@ fn initCore(runtime: ?*const Runtime, cols_arg: u16, rows_arg: u16) !void {
var opts: pardes.Options = .{
.tty_only = true,
+ // The app is a real bare-tty startup, like the terminal and SDL
+ // shells, so it names the same layout rather than the bare shell a
+ // core booted for a test gets.
+ .boot = .tty,
.frame_allocator = allocs.frame,
.image_allocator = allocs.image,
.pdf_allocator = allocs.pdf,
diff --git a/src/main.zig b/src/main.zig
index 1cbdf24c..9abc04bf 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -270,10 +270,6 @@ fn nativeMain(init: std.process.Init) !void {
}
}
opts.tty_only = explicit_tty or session_only;
- // The real bare-tty startup wants the editor half present from the first
- // frame, so it names that layout; a core booted for a test names none and
- // gets the raw shell by itself.
- if (opts.tty_only) opts.boot = .tty;
opts.mounts = mounts[0..mounts_len];
if (detach != null and attach != null)
badArgs(init.io, "--detach and --attach are opposites: one runs the session, the other joins one", .{});
@@ -370,6 +366,13 @@ fn nativeMain(init: std.process.Init) !void {
}
}
}
+ // The real bare-tty startup wants the editor half present from the first
+ // frame, so it names that layout; a core booted for a test names none and
+ // gets the raw shell by itself. This has to come AFTER the positional is
+ // resolved: naming a layout short-circuits `Boot.of`, so choosing it any
+ // earlier threw away a file argument, and the error pane for one that does
+ // not exist, without a word.
+ if (opts.tty_only and opts.file == null and opts.missing == null) opts.boot = .tty;
const found = pardes.config.User.load(init.io, arena, init.environ_map);
opts.startup_config = found.bytes;
opts.startup_config_path = found.path;
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 });