summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig87
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());
+}