diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 87 |
1 files changed, 74 insertions, 13 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index 4fb11362..dc479909 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1826,6 +1826,16 @@ test "a restored terminal's tag names neither the REPL it was nor the shell it h try std.testing.expect(std.mem.indexOf(u8, shown, "Tty+fish") == null); } +test "a Tty started by a shell's path keeps the path in its Tty+ word" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + try std.testing.expect(p.executeBuiltinLine(0, "Tty /bin/sh")); + try std.testing.expect(p.active != 0); + p.acknowledgeShell(p.active, "/bin/sh", false); + try std.testing.expect(std.mem.startsWith(u8, tagline.curTail(p.panes[p.active].?), " Tty+/bin/sh ")); +} + test "Tty+fish, one word a tag can hold, opens a terminal on that shell" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); @@ -1843,7 +1853,7 @@ test "Tty+fish, one word a tag can hold, opens a terminal on that shell" { const caller = p.panes[0].?; try std.testing.expect(p.executeBuiltinLine(0, "Tty /nonexistent")); try std.testing.expect(p.active == before and caller.shell == null); - try std.testing.expect(std.mem.indexOf(u8, caller.msg[0..caller.msg_len], "no shell \"/nonexistent\"") != null); + try std.testing.expect(std.mem.indexOf(u8, caller.msg[0..caller.msg_len], "shell \"/nonexistent\" not found") != null); try std.testing.expect(p.executeBuiltinLine(0, "Tty fsh-not-a-shell")); try std.testing.expect(caller.shell == null); // A directory passes access(X_OK); it is no shell either. @@ -4553,6 +4563,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, @@ -4778,6 +4792,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); @@ -5022,7 +5037,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) @@ -5030,10 +5046,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; } @@ -5054,6 +5070,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 { @@ -5187,7 +5205,9 @@ pub const Pardes = struct { // The tag's Tty word names the shell the host ran, and a click // on it (`Tty+fish`) opens another of the same. if (pane.command == null) { - const base = std.fs.path.basename(executable); + // A shell given as a path keeps it: its bare name would find + // nothing, or another program, on the usual paths. + const base = if (pane.shell) |given| (if (std.mem.indexOfScalar(u8, given, '/') != null) given else std.fs.path.basename(executable)) else std.fs.path.basename(executable); const rest = tagline.terminal_pane_tail[" Tty".len..]; if (std.fmt.bufPrint(&pane.shell_tail, " Tty+{s}{s}", .{ base, rest })) |tail| { pane.shell_tail_len = @intCast(tail.len); @@ -5327,6 +5347,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 { @@ -5566,6 +5592,8 @@ pub const Pardes = struct { if (saved.saved_revision != saved.revision) return; saved.disk_gone = false; ctlfs.events.noteLog(p, .save, saved_pane); + // On disk now, a new file too: Recent has it, open. + if (saved.output == null and recentKeeps(saved.path)) p.recent.opened(p.recentGpa(), saved.path); if (!saved.watch_after_save) return; saved.watch_after_save = false; if (filesystem.localPath(saved.path) != null) @@ -6606,10 +6634,10 @@ pub const Pardes = struct { const none: ?[]const u8 = switch (w.kind) { .completion, .format, .rename => null, .diagnostics, .workspace_diagnostics => return p.setMessage(w.pane, std.fmt.bufPrint(&said, "{s}: none", .{lspWord(w.kind)}) catch "none"), - .incoming_calls => "no callers, not found", - .outgoing_calls => "no callees, not found", - .hover => "nothing to show, not found", - else => "nothing found, not found", + .incoming_calls => "callers not found", + .outgoing_calls => "callees not found", + .hover => "nothing to show: not found", + else => "not found", }; if (none) |text| p.reportFailure(w.pane, std.fmt.bufPrint(&said, "{s}: {s}", .{ lspWord(w.kind), text }) catch "nothing found"); return; @@ -6750,7 +6778,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. @@ -6792,7 +6822,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; } @@ -6863,7 +6893,9 @@ pub const Pardes = struct { pub fn jumpBy(p: *Pardes, delta: i32) void { const next = @as(i64, @intCast(p.jcur)) + delta; - if (p.njumps == 0 or next < 0 or next >= p.njumps) return; + // At an end of the list: said, not a step that silently does nothing. + if (p.njumps == 0 or next < 0 or next >= p.njumps) + return p.setMessage(p.active, if (delta < 0) "Back: nothing further back" else "Forward: nothing further on"); const j = p.jumps[@intCast(next)]; if (j.pane >= MAX_PANES) return; // A closed file's entry: opened again at its dot, placed as a look @@ -8398,6 +8430,7 @@ test "jump history clears selections and lands at the recorded cursor" { const selected = pane.body.primaryRange(pane.file.?.content, 0); p.jumpBy(1); // No forward entry: selection stays untouched. try std.testing.expectEqualDeep(selected, pane.body.primaryRange(pane.file.?.content, 0)); + try std.testing.expect(std.mem.indexOf(u8, p.panes[p.active].?.msg[0..p.panes[p.active].?.msg_len], "nothing further on") != null); for ([_]Loc{ .{ .pane = 15, .serial = pane.serial, .line = 1, .col = 1 }, .{ .pane = 0, .serial = pane.serial + 1, .line = 1, .col = 1 }, @@ -8508,3 +8541,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()); +} |
