summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/exec.zig14
-rw-r--r--src/layout.zig13
-rw-r--r--src/ninep/ctl.zig7
-rw-r--r--src/pardes.zig56
4 files changed, 79 insertions, 11 deletions
diff --git a/src/exec.zig b/src/exec.zig
index 4785e7a2..06e5d375 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -643,7 +643,12 @@ pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize {
/// such text may be a bound REPL's. Run by name (`Exec ...`, whose line
/// comes back through `execute`) or written as a command word, never.
pub fn executeFrom(p: *Pardes, id: usize, txt: []const u8, from_body: bool) ?usize {
- const pane = p.panes[id] orelse return null;
+ // An empty window runs its session words, and nothing else: no pane to
+ // run a command for.
+ const pane = p.panes[id] orelse {
+ _ = executeBuiltinLine(p, id, commandText(txt));
+ return null;
+ };
const gesture = from_body and config.commandWord(std.mem.trim(u8, txt, " \t\r\n")) == null;
const cmd = commandText(txt);
if (cmd.len == 0) return null;
@@ -891,7 +896,9 @@ pub fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]co
/// `runBuiltin` saying whether a gesture on the pane's body made it.
pub fn runBuiltinFrom(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]const u8, from_body: bool) void {
if (!normal.multiOnce(p)) return; // a builtin is per-keystroke, never per-cursor
- const pane = p.panes[id] orelse return;
+ // No pane at all (the window Delcol emptied): a word runs all the same,
+ // with a stand-in that is no pane (New, Newcol, Exit are what make sense).
+ const pane = p.panes[id] orelse (if (builtins.registry.scope(b) == .session or p.anyPane() == null) p.standIn() orelse return else return);
// A builtin says its own name before it runs, so a tagline click is
// legible even when what it did is invisible. Whatever the builtin
// then says for itself replaces the announcement. It is the message
@@ -1000,6 +1007,9 @@ pub const NewPane = enum { scratch, fs, shell, doc, command, errors };
/// from: null when a column tag, the workspace tag or 9P asked (acme's
/// t->w == nil).
pub fn placeNew(p: *Pardes, from: usize, origin: ?usize, id: usize, kind: NewPane) void {
+ // No column left (the last one Delcol'd): a pane made from the workspace
+ // tag opens one to go in.
+ _ = layout.firstColumn(p);
const asked = if (p.exec_column != null) null else origin;
layout.compute(p);
const before = p.rects;
diff --git a/src/layout.zig b/src/layout.zig
index 16af1ec5..28f567bc 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -635,6 +635,19 @@ fn canSplitCol(p: *Pardes, col: usize) bool {
/// An empty column beside column `col`, with half its width: acme's Newcol
/// (rowadd, rows.c:51). The new column's index, or null with no room.
+/// A window with no column left (Delcol took the last, as acme lets it):
+/// its first one, empty; null when it has columns.
+pub fn firstColumn(p: *Pardes) ?usize {
+ if (p.ncol != 0) return null;
+ p.col_weight[0] = 2;
+ p.col_tags[0] = .{ .what = .column, .gpa = p.gpa };
+ p.col_serial[0] = 0;
+ p.col_n[0] = 0;
+ p.ncol = 1;
+ noteColumn(p, "newcol", columnSerial(p, 0));
+ return 0;
+}
+
pub fn insertColumn(p: *Pardes, col: usize, before: bool) ?usize {
if (!canSplitCol(p, col)) return null;
var old_weight = p.col_weight[col];
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 256b94ad..98899edb 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -52,7 +52,7 @@ fn noteResult(p: *Pardes, serial: u32) void {
/// created; failing that, a look records the pane it focused and an exec
/// the pane it acted on (even one it closed).
pub fn run(p: *Pardes, id: usize, exec: bool, text: []const u8, in_tag: bool) void {
- const context = p.panes[id].?.serial;
+ const context = if (p.panes[id]) |pane| pane.serial else 0;
// A pane an event reader holds hears its clicks instead of acting on
// them (editors/acme/look.c:53 and exec.c:173 send them to winevent),
// and a line written to a look or exec is a click: on that pane, or for
@@ -446,7 +446,7 @@ pub fn runBuiltin(p: *Pardes, req: Req, id: usize, line: []const u8) ?Reply {
defer p.fs.no_prompt = false;
// What a ctl write touched reads back from /look and /exec as a click's
// does (`run`), so a script that made a pane with `Tty` finds it there.
- const context = p.panes[id].?.serial;
+ const context = if (p.panes[id]) |pane| pane.serial else 0;
const before = Serials.snapshot(p);
_ = exec_line.executeBuiltinLine(p, id, line);
const after = Serials.snapshot(p);
@@ -580,7 +580,8 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply {
if (checkBuiltin(p, req, line, .session)) |refusal| return refusal;
continue;
}
- if (p.panes[p.active] == null) return Reply.fail(req.tag, E.NOENT);
+ // An empty window (no pane at all) still takes its session words.
+ if (p.panes[p.active] == null and p.anyPane() != null) return Reply.fail(req.tag, E.NOENT);
p.fs.session_write = true;
defer p.fs.session_write = false;
if (runBuiltin(p, req, p.active, line)) |refusal| return refusal;
diff --git a/src/pardes.zig b/src/pardes.zig
index ec406094..4bc5829b 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4555,6 +4555,10 @@ pub const Pardes = struct {
/// where the shell wrote the last dump (shell reports back after
/// write_dump); shown in the topbar as `Restore <path>`
last_dump: ?[]const u8 = null,
+ /// What a session word (Newcol, New, Exit) runs with when no pane is
+ /// there to run it: the window Delcol emptied. Never installed, made the
+ /// first time it is wanted.
+ stand_in: ?*Pane = null,
/// a Restore builtin wants this dump loaded into the live session; the
/// shell consumes it via takeRestore each frame (restore contents stay host-fed)
restore_req: ?[]const u8 = null,
@@ -4780,6 +4784,7 @@ pub const Pardes = struct {
for (p.panes) |slot| if (slot) |pane| p.noteClosed(pane);
p.saveRecent();
p.recent.deinit(p.recentGpa());
+ if (p.stand_in) |pane| p.teardownPane(pane);
look.cancelLookHover(p);
if (p.repl_ask) |ask| p.gpa.free(ask.text);
for (0..MAX_PANES) |id| p.dropPendingWrite(id);
@@ -5024,7 +5029,8 @@ pub const Pardes = struct {
}
fn newScratch(p: *Pardes, from_id: usize, free: usize, kind: exec.NewPane) !*Pane {
- const src = p.panes[from_id] orelse return error.MissingPane;
+ // None asking (an empty window): the session's, as pane/new's is.
+ const src_or = p.panes[from_id];
const content = try p.gpa.dupe(u8, "");
errdefer p.gpa.free(content);
// A terminal that has not said where it is yet (the boot layout's)
@@ -5032,10 +5038,10 @@ pub const Pardes = struct {
// 9P's pane/new, and a New from a column's or the workspace's tag:
// no pane asked for it, whichever last had the keyboard (acme's new
// window has acme's directory).
- const own = kind != .fs and !p.exec_header;
- const dir = if (own) paneDir(src) else "";
+ const src = if (kind != .fs and !p.exec_header) src_or else null;
+ const dir = if (src) |s| paneDir(s) else "";
const np = try panes.Output.open(p, free, if (dir.len == 0) p.sessionDir() else dir, .{ .cmd = .New }, "", content);
- if (own) np.cwd = .{ .inherited = src };
+ if (src) |s| np.cwd = .{ .inherited = s };
np.body.cur_pinned = true;
return np;
}
@@ -5056,6 +5062,8 @@ pub const Pardes = struct {
/// Newcol: an empty column right of the calling pane's, the keyboard
/// on its tag, for the next pane made to fill.
pub fn newColumn(p: *Pardes, from_id: usize) void {
+ // An empty window (Delcol took the last column): its first column.
+ if (layout.firstColumn(p)) |c| return tagline.enterHeader(p, c);
const col = p.keyboardColumn(from_id) orelse return;
// Said, not silently nothing: "no space" is 9ns's word for ENOSPC.
const c = layout.insertColumn(p, col, false) orelse {
@@ -5331,6 +5339,12 @@ pub const Pardes = struct {
}
}
+ /// The pane a session word runs with in an empty window.
+ pub fn standIn(p: *Pardes) ?*Pane {
+ if (p.stand_in == null) p.stand_in = panes.Terminal.createDoc(p.gpa, p.screen_w, p.screen_h) catch null;
+ return p.stand_in;
+ }
+
/// Drop a pane's parked bytes: its pty is gone, and the slot it occupied
/// may be handed to a different pane next frame.
fn dropPendingWrite(p: *Pardes, id: usize) void {
@@ -6756,7 +6770,9 @@ pub const Pardes = struct {
p.panes[closed] = null;
}
layout.dropColumn(p, col);
- if (p.panes[p.active] == null) p.active = beside orelse p.anyPane() orelse return p.quitEmpty();
+ // The last column gone, the window stays, empty but for the
+ // workspace tag, as acme's does; Newcol starts again from there.
+ if (p.panes[p.active] == null) p.active = beside orelse p.anyPane() orelse return tagline.enterHeader(p, null);
}
/// No pane is left: the session is over.
@@ -6798,7 +6814,7 @@ pub const Pardes = struct {
}
/// Any live pane: the last resort when nothing took the closed one's place.
- fn anyPane(p: *const Pardes) ?usize {
+ pub fn anyPane(p: *const Pardes) ?usize {
for (p.panes, 0..) |slot, k| if (slot != null) return k;
return null;
}
@@ -8517,3 +8533,31 @@ test "a Save the host could not do logs no save record" {
p.perform(.{ .save_file = .{ .pane = 0 } });
try std.testing.expect(th.logHas(p, "\nsave "));
}
+
+test "Delcol on the last column leaves the window, drawn and typed into, and Newcol starts it again" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ while (p.ncol > 0) {
+ p.exec_column = 0;
+ _ = p.executeBuiltinLine(p.active, "Delcol");
+ p.exec_column = null;
+ p.sync();
+ }
+ try std.testing.expect(!p.quit);
+ try std.testing.expect(p.anyPane() == null);
+ // Drawn, and keys go to the workspace tag.
+ _ = try p.render(p.frame_arena.allocator());
+ p.update(.{ .key = .{ .cp = 'x', .text = "x" } });
+ p.update(.{ .key = .{ .cp = Key.enter } });
+ p.sync();
+ _ = try p.render(p.frame_arena.allocator());
+ try std.testing.expect(!p.quit);
+ _ = p.executeBuiltinLine(p.active, "Newcol");
+ p.sync();
+ try std.testing.expectEqual(@as(usize, 1), p.ncol);
+ _ = p.executeBuiltinLine(p.active, "New");
+ p.sync();
+ try std.testing.expect(p.anyPane() != null);
+ _ = try p.render(p.frame_arena.allocator());
+}