diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/exec.zig | 14 | ||||
| -rw-r--r-- | src/layout.zig | 13 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 7 | ||||
| -rw-r--r-- | src/pardes.zig | 56 |
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()); +} |
