summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CHANGELOG.md8
-rw-r--r--src/builtins.zig43
-rw-r--r--src/config.zig8
-rw-r--r--src/gui/gui.zig2
-rw-r--r--src/layout.zig29
-rw-r--r--src/ninep/screen.zig2
-rw-r--r--src/ninep/tree.zig2
-rw-r--r--src/panes.zig6
-rw-r--r--src/pardes.zig136
-rw-r--r--src/tutor.txt5
10 files changed, 210 insertions, 31 deletions
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 0083f97f..f8451baf 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -2,6 +2,14 @@
## 0.0.3
+- `Del` takes a side. `Del k` and `DelAbove` give the closed pane's rows to
+ the expanded pane above it, `Del j` and `DelBelow` to the one below. A bare
+ `Del` from the keyboard on a pane with panes both above and below asks on
+ its notice band, and one key answers; a click, a 9P write or a script
+ still closes it at once, rows going up as before.
+- A prompt (Save's path, a search, a pipe) is drawn in the tagline's pitch
+ like the other notices instead of spread one glyph to a body cell, and in
+ the SDL shell every notice chip is ruled off from the body like a tagline.
- Answer 9P on the connection's task instead of the editor's loop. The core
is single-threaded and `pardes.turn` says whose turn it is with it: the
editor's by default, given up while it waits for input and while a step of
diff --git a/src/builtins.zig b/src/builtins.zig
index 30a5a50f..146789b0 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -460,9 +460,38 @@ pub const Newcol = struct {
}
};
+/// Close the pane. `Del k` gives its rows to the open pane above it and
+/// `Del j` to the one below, each falling back to the other side.
pub const Del = struct {
+ pub const takes_arg = true;
pub fn run(c: Ctx) void {
- c.p.removePane(c.id) catch |err| c.p.reportError(c.id, "close", err);
+ const side = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
+ const at = layout.findPane(c.p, c.id);
+ const above = if (at) |f| layout.expandedNeighbor(c.p, f.col, f.idx, .above) else null;
+ const below = if (at) |f| layout.expandedNeighbor(c.p, f.col, f.idx, .below) else null;
+ const to: ?usize = if (std.mem.eql(u8, side, "k"))
+ above orelse below
+ else if (std.mem.eql(u8, side, "j"))
+ below orelse above
+ else if (side.len > 0)
+ return c.p.reportError(c.id, "Del k|j", error.InvalidArgument)
+ else
+ null;
+ // Someone at the keyboard with a real choice to make is asked, on the
+ // pane's notice band; a click, a 9P write or a script closes it the
+ // way it always has. A folded pane gives back only its tag row, and a
+ // pane with a prompt open closes as it did rather than trade the
+ // prompt for a question.
+ if (side.len == 0 and c.p.can_ask and above != null and below != null and
+ !c.pane.collapsed and c.pane.prompt == .none)
+ {
+ c.pane.prompt = .del_side;
+ c.p.active = c.id;
+ // The question says Del already.
+ if (c.pane.msg_announcement) c.pane.msg_len = 0;
+ return;
+ }
+ c.p.removePane(c.id, to) catch |err| c.p.reportError(c.id, "close", err);
}
};
@@ -500,6 +529,18 @@ pub const Delcol = struct {
}
};
+pub const DelAbove = struct {
+ pub fn run(c: Ctx) void {
+ Del.run(.{ .p = c.p, .pane = c.pane, .id = c.id, .txt = c.txt, .arg = "k" });
+ }
+};
+
+pub const DelBelow = struct {
+ pub fn run(c: Ctx) void {
+ Del.run(.{ .p = c.p, .pane = c.pane, .id = c.id, .txt = c.txt, .arg = "j" });
+ }
+};
+
/// A shell in the calling pane's directory, raw from the first frame. On every
/// pane's tagline: the fast path from wherever you are to a prompt there.
pub const Tty = struct {
diff --git a/src/config.zig b/src/config.zig
index 527c3752..4e46b1d8 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -42,6 +42,8 @@ pub const leader_path = paths: {
.Supertypes = "lt",
.Subtypes = "lT",
.Del = "d",
+ .DelAbove = null,
+ .DelBelow = null,
.Filter = null,
.Mode = null,
.Togglettymode = null,
@@ -269,6 +271,12 @@ pub const pipe_marker_insert = " !";
pub const pipe_marker_append = " !+";
pub const search_submit: []const Chord = &.{.{ .cp = Key.enter }};
+// Del from the keyboard on a pane with open panes both above and below it
+// asks which of them takes its rows. One key answers; any other keeps the pane.
+pub const del_question = "Del: give space to k above j below";
+pub const del_above: []const Chord = &.{ .{ .cp = 'k' }, .{ .cp = Key.up } };
+pub const del_below: []const Chord = &.{ .{ .cp = 'j' }, .{ .cp = Key.down } };
+
pub const wheel_rows: i32 = 1;
pub const wheel_cols: i32 = 4;
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 92ded62c..acb8d729 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -1387,7 +1387,7 @@ test "GUI PTY deletion joins an idle reader and retains only its owned child for
const child = try host_io.forkShell(null, 0, &rcs, "/bin/sh", "", 24, 80, null);
ptys[0] = .{ .fd = child.file.handle, .pid = child.pid, .serial = core.panes[0].?.serial };
try spawnReader(gpa, &ptys[0].?, 0, 1, &queue);
- try core.removePane(0);
+ try core.removePane(0, null);
shell.reconcilePtys();
try std.testing.expect(queue.readers[0] == null);
try std.testing.expectEqual(@as(usize, 0), queue.waiting);
diff --git a/src/layout.zig b/src/layout.zig
index 3637f30e..116a64b8 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -995,7 +995,7 @@ pub fn splitColumn(p: *Pardes, source_id: usize, id: usize, before: bool) bool {
if (needs_rebase) old_weight *= 2;
if (id == source_id) {
if (p.col_n[source_col] <= 1) return false;
- absorbVWeight(p, id);
+ absorbVWeight(p, id, null);
removePane(p, id);
} else if (findPane(p, id) != null) return false;
@@ -1033,13 +1033,16 @@ pub fn snapColWeights(p: *Pardes, c: usize) void {
}
}
-pub fn absorbVWeight(p: *Pardes, id: usize) void {
+/// Hand a closing pane's rows to `to`. With none named they go up: past any
+/// results listing to the nearest pane above that is not one, or to the
+/// pane below when the closing one is first in its column.
+pub fn absorbVWeight(p: *Pardes, id: usize, to: ?usize) void {
const f = findPane(p, id) orelse return;
if (p.col_n[f.col] <= 1) return;
snapColWeights(p, f.col);
- var sib = if (f.idx > 0) p.col_panes[f.col][f.idx - 1] else p.col_panes[f.col][f.idx + 1];
+ var sib = to orelse if (f.idx > 0) p.col_panes[f.col][f.idx - 1] else p.col_panes[f.col][f.idx + 1];
var k = f.idx;
- while (k > 0) : (k -= 1) {
+ while (to == null and k > 0) : (k -= 1) {
sib = p.col_panes[f.col][k - 1];
if (p.panes[sib]) |pp| if (if (pp.file) |ff| panes.Output.fileTraits(ff.output).doc else true) break;
}
@@ -1119,7 +1122,7 @@ pub fn toggleCollapse(p: *Pardes, id: usize) void {
// Convert only visible panes to row weights. The toggle then changes one
// pair, rather than letting the column redistribute the released space.
snapColWeights(p, at.col);
- const neighbor = collapseNeighbor(p, at.col, at.idx);
+ const neighbor = expandedNeighbor(p, at.col, at.idx, .above) orelse expandedNeighbor(p, at.col, at.idx, .below);
if (was_collapsed) {
if (neighbor) |other| {
const wanted: u16 = @intFromFloat(@min(@as(f32, @floatFromInt(std.math.maxInt(u16))), @max(1, @round(remembered_height))));
@@ -1139,15 +1142,17 @@ pub fn toggleCollapse(p: *Pardes, id: usize) void {
compute(p);
}
-fn collapseNeighbor(p: *Pardes, col: usize, index: usize) ?usize {
- var above = index;
- while (above > 0) {
- above -= 1;
- const id = p.col_panes[col][above];
+/// The nearest pane on one side of `index` in its column that is not folded
+/// to its tag: the one Collapse lends rows to and Del k / Del j gives them
+/// to. A folded pane only keeps its weight for later, so rows handed to it
+/// would spread over the whole column instead.
+pub fn expandedNeighbor(p: *const Pardes, col: usize, index: usize, side: enum { above, below }) ?usize {
+ var k = index;
+ while (if (side == .above) k > 0 else k + 1 < p.col_n[col]) {
+ k = if (side == .above) k - 1 else k + 1;
+ const id = p.col_panes[col][k];
if (p.panes[id]) |pane| if (!pane.collapsed) return id;
}
- for (p.col_panes[col][index + 1 .. p.col_n[col]]) |id|
- if (p.panes[id]) |pane| if (!pane.collapsed) return id;
return null;
}
diff --git a/src/ninep/screen.zig b/src/ninep/screen.zig
index 2684aa41..60fb5951 100644
--- a/src/ninep/screen.zig
+++ b/src/ninep/screen.zig
@@ -237,7 +237,7 @@ test "terminal body snapshots are lazy bounded and released after the pane close
try testing.expectEqual(E.INVAL, call(p, .{ .tag = 6, .op = .read, .node = screen_node, .handle = handles[0], .size = 32 }).errno());
_ = call(p, .{ .tag = 7, .op = .release, .node = screen_node, .handle = handles[0] });
try testing.expectEqual(node, p.fs.snapshots[handles[0] - 1].node);
- try p.removePane(0);
+ try p.removePane(0, null);
for (handles) |opened_handle| _ = call(p, .{ .tag = 8, .op = .release, .node = node, .handle = opened_handle });
for (p.fs.snapshots) |snapshot| {
try testing.expectEqual(@as(u64, 0), snapshot.node);
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index b13b1472..aca8d109 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -529,7 +529,7 @@ fn remove(p: *Pardes, req: Req) Reply {
};
if (t.file != .dir) return Reply.fail(req.tag, E.PERM);
const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT);
- p.removePane(id) catch return Reply.fail(req.tag, E.IO);
+ p.removePane(id, null) catch return Reply.fail(req.tag, E.IO);
return .{ .tag = req.tag };
}
diff --git a/src/panes.zig b/src/panes.zig
index 0b565676..0b4204aa 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -94,6 +94,9 @@ pub const Pane = struct {
pipe: struct { at: u16, how: modal.Normal.PipeBehavior },
/// Save on a scratch buffer or a terminal: the tail is a path to write to.
save: u16,
+ /// Del from the keyboard with open panes both above and below: which
+ /// one takes the rows. Nothing is typed; one key answers.
+ del_side,
};
pub const Cwd = union(enum) { none, inherited: *Pane, owned: []u8 };
@@ -338,7 +341,7 @@ pub const Pane = struct {
}
pub fn promptAt(p: *const Pane) ?u16 {
return switch (p.prompt) {
- .none => null,
+ .none, .del_side => null,
.search, .save => |at| at,
.pipe => |pipe| pipe.at,
};
@@ -347,6 +350,7 @@ pub const Pane = struct {
/// What an open prompt shows on its notice band: the one answer every
/// painter of the band reads.
pub fn promptText(p: *const Pane) ?[]const u8 {
+ if (p.prompt == .del_side) return config.del_question;
const at = p.promptAt() orelse return null;
return p.tagSlice()[@min(at, p.tag_tail_len)..];
}
diff --git a/src/pardes.zig b/src/pardes.zig
index 29f937d7..42729d50 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -2104,7 +2104,7 @@ test "New opens an empty scratch below the caller, inheriting its directory" {
try std.testing.expectEqualStrings("/tmp/pardes-scratch-dir", Pardes.paneDir(np));
p.setCwd(source, "/tmp/pardes-moved");
try std.testing.expectEqualStrings("/tmp/pardes-moved", Pardes.paneDir(np));
- try p.removePane(source);
+ try p.removePane(source, null);
p.sync();
try std.testing.expect(np.cwd == .owned);
try std.testing.expectEqualStrings("/tmp/pardes-moved", Pardes.paneDir(np));
@@ -2176,7 +2176,7 @@ test "owned cwd close copies are transactional across inherited chains" {
const active = p.active;
const live_bytes = allocator.allocated_bytes - allocator.freed_bytes;
allocator.fail_index = allocator.alloc_index + failed_copy;
- try std.testing.expectError(error.OutOfMemory, p.removePane(0));
+ try std.testing.expectError(error.OutOfMemory, p.removePane(0, null));
try std.testing.expectEqual(parent, p.panes[0].?);
try std.testing.expectEqual(parent, first.cwd.inherited);
try std.testing.expectEqual(parent, second.cwd.inherited);
@@ -2189,12 +2189,12 @@ test "owned cwd close copies are transactional across inherited chains" {
try std.testing.expectEqual(live_bytes, allocator.allocated_bytes - allocator.freed_bytes);
allocator.fail_index = std.math.maxInt(usize);
- try p.removePane(0);
+ try p.removePane(0, null);
try std.testing.expect(p.panes[0] == null);
try std.testing.expect(first.cwd == .owned and second.cwd == .owned);
try std.testing.expectEqual(first, grandchild.cwd.inherited);
try std.testing.expect(first.cwdSlice().ptr != second.cwdSlice().ptr);
- try p.removePane(first_id);
+ try p.removePane(first_id, null);
try std.testing.expect(grandchild.cwd == .owned);
p.sync();
p.sync();
@@ -5539,7 +5539,7 @@ test "vertical close samples only a canonical baseline containing that pane" {
_ = frame.reset(.retain_capacity);
const opening = try p.render(frame.allocator());
p.acknowledgePanelPresentation(opening.panelTracks());
- try p.removePane(1);
+ try p.removePane(1, null);
p.sync();
try std.testing.expectEqual(@as(usize, 0), p.presentation.closing_len);
@@ -5565,7 +5565,7 @@ test "vertical close samples only a canonical baseline containing that pane" {
try std.testing.expectEqual(@as(usize, 0), canonical.panelTracks().len);
p.acknowledgePanelPresentation(canonical.panelTracks());
const serial = p.panes[1].?.serial;
- try p.removePane(1);
+ try p.removePane(1, null);
p.sync();
try std.testing.expectEqual(@as(usize, 1), p.presentation.closing_len);
@@ -6387,6 +6387,10 @@ pub const Pardes = struct {
chord_arg: ?[]u8 = null,
/// how many `execute` calls are on the stack — see max_exec_depth
exec_depth: u8 = 0,
+ /// A key press is being handled: someone is at the keyboard to answer,
+ /// so a builtin may ask rather than guess. Nothing else sets it -- not
+ /// a click, a 9P write, a startup line or a pane's shell exiting.
+ can_ask: bool = false,
lsp_seq: u32 = 0,
lsp_wait: ?struct {
@@ -7794,14 +7798,18 @@ pub const Pardes = struct {
ctlfs.events.notePtyOutput(p, o.pane, o.bytes);
panes.Terminal.feedOutput(p, pane, o.bytes);
},
- .eof => |e| p.removePane(e.pane) catch |err| {
+ .eof => |e| p.removePane(e.pane, null) catch |err| {
if (p.panes[e.pane]) |pane| pane.mode = .normal;
p.reportError(e.pane, "terminal exited; Del retries close", err);
},
.lsp_resp => |r| p.lspResponse(r.id, r.rows),
.pipe_resp => |r| p.pipeResponse(r.id, r.success, r.outputs, r.failure),
.file_changed => |fc| _ = p.applyWatchedFileChanged(fc.pane, fc.bytes),
- .key => |key| p.handleKey(key),
+ .key => |key| {
+ p.can_ask = true;
+ defer p.can_ask = false;
+ p.handleKey(key);
+ },
.mouse => |m| {
if (m.kind == .release) switch (p.drag) {
.border_v, .border_h, .move, .column_move => p.presentation.snap_once = true,
@@ -8899,6 +8907,16 @@ pub const Pardes = struct {
}
fn handleKey(p: *Pardes, key: Key) void {
+ // A question waiting on a pane takes the next key, whatever mode the
+ // pane or a header edit is in: that key is its answer.
+ for (p.panes, 0..) |slot, id| {
+ const asked = slot orelse continue;
+ if (asked.prompt != .del_side) continue;
+ asked.prompt = .none;
+ if (hit(key, config.del_above)) p.runBuiltin(.Del, id, "", "k");
+ if (hit(key, config.del_below)) p.runBuiltin(.Del, id, "", "j");
+ return;
+ }
if (p.topbar_col != null) return p.topbarKey(key);
const pane = p.panes[p.active] orelse return;
if (pane.isTerminal() and pane.mode == .tty and !pane.tag_edit) {
@@ -8988,7 +9006,7 @@ pub const Pardes = struct {
p.applySelRegex(pane, "", false);
pane.tag_tail_len = @min(at, pane.tag_tail_len);
},
- .none => unreachable,
+ .none, .del_side => unreachable,
}
exitTagEdit(pane);
pane.mode = .normal;
@@ -11955,6 +11973,15 @@ pub const Pardes = struct {
}
fn handleMouse(p: *Pardes, m: Mouse) void {
+ // A click goes somewhere else: the question it leaves unanswered is
+ // taken back, and the pane stays. A wheel only looks around.
+ if (m.kind == .press and switch (m.button) {
+ .left, .middle, .right => true,
+ else => false,
+ }) for (p.panes) |slot| {
+ const pane = slot orelse continue;
+ if (pane.prompt == .del_side) pane.prompt = .none;
+ };
if (m.button == .back or m.button == .forward) {
if (m.kind == .press) p.jumpBy(if (m.button == .back) -1 else 1);
return;
@@ -12768,7 +12795,7 @@ pub const Pardes = struct {
panes.Terminal.scrollbar(tt).total > tt.rows) return;
p.deinitPane(tt) catch |err| return p.reportError(tty_id, "close", err);
layout.compute(p); // a just-stacked doc has no rect yet; absorb snaps to rows
- layout.absorbVWeight(p, tty_id);
+ layout.absorbVWeight(p, tty_id, null);
layout.removePane(p, tty_id);
p.panes[tty_id] = null;
if (p.active == tty_id) p.active = keep_id;
@@ -13621,7 +13648,9 @@ pub const Pardes = struct {
pane.tag_init = true;
}
- pub fn removePane(p: *Pardes, id: usize) !void {
+ /// Close a pane; its rows go to `to`, or where layout.absorbVWeight
+ /// sends them when that is null.
+ pub fn removePane(p: *Pardes, id: usize, to: ?usize) !void {
const pane = p.panes[id] orelse return;
// A column outlives its panes. Closing the last one leaves an empty
// pane standing in its place and in its directory; only Delcol ever
@@ -13647,7 +13676,7 @@ pub const Pardes = struct {
layout.insert(p, f.col, f.idx + 1, free);
break :heir free;
};
- layout.absorbVWeight(p, id);
+ layout.absorbVWeight(p, id, to);
layout.removePane(p, id);
p.panes[id] = null;
if (p.active == id) p.active = heir orelse p.prevFocus(id) orelse {
@@ -15499,6 +15528,87 @@ test "a prompt chip is a band with its caret, still on the grid, and only the ch
for (pane.sel) |selection| try std.testing.expect(selection.state == .none);
}
+test "Del from a key asks which neighbour takes the rows; nothing else asks" {
+ if (platform == .web) return;
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 40 });
+ defer p.deinit();
+ var frame = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer frame.deinit();
+ const top = p.active;
+ p.newScratchBelow(top);
+ const mid = p.active;
+ p.newScratchBelow(mid);
+ const low = p.active;
+ p.sync();
+ var h = [_]u16{ p.rects[top].h, p.rects[mid].h, p.rects[low].h };
+
+ // SPC d on the middle pane asks on its notice band, and closes nothing yet.
+ p.active = mid;
+ p.update(.{ .key = .{ .cp = ' ' } });
+ p.update(.{ .key = .{ .cp = 'd' } });
+ try std.testing.expect(p.panes[mid] != null);
+ try std.testing.expectEqual(Pane.Prompt.del_side, p.panes[mid].?.prompt);
+ _ = try p.render(frame.allocator());
+ try std.testing.expectEqual(Pane.Notices.Kind.prompt, p.panes[mid].?.notices.kinds[p.panes[mid].?.notices.len - 1]);
+ try std.testing.expectEqualStrings(config.del_question, p.panes[mid].?.promptText().?);
+ // j: the pane below takes every row, the one above keeps its own.
+ p.update(.{ .key = .{ .cp = 'j' } });
+ try std.testing.expect(p.panes[mid] == null);
+ try std.testing.expectEqual(h[0], p.rects[top].h);
+ try std.testing.expectEqual(h[1] + h[2], p.rects[low].h);
+
+ // Esc takes the question back and the pane stays; so does any other key,
+ // and so does a click.
+ p.newScratchBelow(top);
+ const again = p.active;
+ p.sync();
+ h = .{ p.rects[top].h, p.rects[again].h, p.rects[low].h };
+ try std.testing.expect(p.executeBuiltinLine(again, "Msg keep"));
+ for ([_]Key{ .{ .cp = Key.escape }, .{ .cp = 'x' } }) |answer| {
+ p.update(.{ .key = .{ .cp = ' ' } });
+ p.update(.{ .key = .{ .cp = 'd' } });
+ try std.testing.expectEqual(Pane.Prompt.del_side, p.panes[again].?.prompt);
+ p.update(.{ .key = answer });
+ try std.testing.expect(p.panes[again] != null);
+ try std.testing.expectEqual(Pane.Prompt.none, p.panes[again].?.prompt);
+ }
+ p.update(.{ .key = .{ .cp = ' ' } });
+ p.update(.{ .key = .{ .cp = 'd' } });
+ const r = p.rects[again];
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = r.x + 5, .row = r.y + 3 } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = r.x + 5, .row = r.y + 3 } });
+ try std.testing.expect(p.panes[again] != null);
+ try std.testing.expectEqual(Pane.Prompt.none, p.panes[again].?.prompt);
+ try std.testing.expectEqual(h[1], p.rects[again].h);
+
+ // A word that is not a side closes nothing.
+ try std.testing.expect(p.executeBuiltinLine(again, "Del x"));
+ try std.testing.expect(p.panes[again] != null);
+ // DelAbove is Del k, and a line that is not a key never asks.
+ try std.testing.expect(p.executeBuiltinLine(again, "DelAbove"));
+ p.sync();
+ try std.testing.expect(p.panes[again] == null);
+ try std.testing.expectEqual(h[0] + h[1], p.rects[top].h);
+ try std.testing.expectEqual(h[2], p.rects[low].h);
+
+ // Bare Del from anything but a key closes at once and gives the rows up,
+ // as it always has.
+ p.newScratchBelow(top);
+ const third = p.active;
+ p.sync();
+ h = .{ p.rects[top].h, p.rects[third].h, p.rects[low].h };
+ try std.testing.expect(p.executeBuiltinLine(third, "Del"));
+ p.sync();
+ try std.testing.expect(p.panes[third] == null);
+ try std.testing.expectEqual(h[0] + h[1], p.rects[top].h);
+
+ // With nothing open below, there is nothing to ask: SPC d closes at once.
+ p.active = low;
+ p.update(.{ .key = .{ .cp = ' ' } });
+ p.update(.{ .key = .{ .cp = 'd' } });
+ try std.testing.expect(p.panes[low] == null);
+}
+
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 });
@@ -15513,7 +15623,7 @@ test "closing the last pane in a column leaves an empty one in its place" {
const had = try std.testing.allocator.dupe(u8, p.panes[lone].?.cwdSlice());
defer std.testing.allocator.free(had);
- try p.removePane(lone);
+ try p.removePane(lone, null);
p.sync();
// The column outlives the pane: still two columns, and the one that was
diff --git a/src/tutor.txt b/src/tutor.txt
index ab73be33..2f5b521c 100644
--- a/src/tutor.txt
+++ b/src/tutor.txt
@@ -127,7 +127,10 @@
Tty new terminal in the calling pane's directory
Togglettymode in a terminal tag: switch editor/raw mode (Ctrl-B)
Joincol delete this column, move its panes to the one right
- Del in a pane's own tag: close it
+ Del in a pane's own tag: close it. From the keyboard, between
+ two panes, it asks which one gets the space: k or j
+ DelAbove close it, giving the space to the pane above (Del k)
+ DelBelow close it, giving the space to the pane below (Del j)
A new file opens in a new COLUMN only if both panes would still get
100 columns of width. Otherwise it stacks in the one you are in.