summaryrefslogtreecommitdiff
path: root/src/exec.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 18:53:37 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitb8c5e8772e646fa21f6afdd2a875a1bd52848bd5 (patch)
tree70bca3d554501c49107b27396c7756e0e8ad62a3 /src/exec.zig
parentc4ab29bce30f427decf3e199eebecfd415714a96 (diff)
downloadpardes-b8c5e8772e646fa21f6afdd2a875a1bd52848bd5.tar.gz
pardes-b8c5e8772e646fa21f6afdd2a875a1bd52848bd5.zip
A column can be empty, as acme's can; new panes go where acme's makenewwindow puts them
Newcol makes an empty column; closing a column's last pane leaves it empty with the keyboard on its tag; Delcol and Joincol alone take a column away; the session's last pane closing quits. The +New stand-in and replaceStillborn / stillborn_joiner are gone. Every new pane goes through exec.placeNew, which follows acme's makenewwindow (active column, empty column whole, blank space, halving the biggest) or, with Placement pardes, the old rules. ColumnTags is gone: column tags are always shown. A grip drag shows acme's box cursor. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/exec.zig')
-rw-r--r--src/exec.zig160
1 files changed, 106 insertions, 54 deletions
diff --git a/src/exec.zig b/src/exec.zig
index ee7c31e9..117970d9 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -322,12 +322,7 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane {
nt.greet = !v9fs;
nt.v9fs_on_spawn = v9fs;
nt.body.mode = .tty;
- const parent = layout.splitParent(p, from);
- const f = layout.findPane(p, parent).?;
- layout.insert(p, f.col, f.idx + 1, free);
- layout.splitBelow(p, parent, nt);
- p.active = free;
- p.stillborn_joiner = nt.serial;
+ placeNew(p, from, from, free, .shell);
return nt;
}
@@ -357,29 +352,6 @@ pub fn evictLonePristineTty(p: *Pardes, col: usize, keep_id: usize) void {
if (p.active == tty_id) p.active = keep_id;
}
-/// A pane that just joined a column whose only other pane is stillborn —
-/// an empty scratch nobody typed into, body or tag (what Newcol makes and
-/// what a closed column's last pane leaves behind) — takes the column
-/// whole: the placeholder was only holding the space for it. Run at the
-/// end of a step: the pane that asked for the joiner is often the
-/// placeholder itself, and its caller is still holding it.
-pub fn replaceStillborn(p: *Pardes) void {
- const serial = p.stillborn_joiner orelse return;
- p.stillborn_joiner = null;
- const new_id = p.paneBySerial(serial) orelse return;
- const f = layout.findPane(p, new_id) orelse return;
- if (p.col_n[f.col] != 2) return;
- const old_id = p.col_panes[f.col][1 - f.idx];
- const old = p.panes[old_id] orelse return;
- const file = old.file orelse return;
- const out = file.output orelse return;
- if (out.from != .cmd or out.from.cmd != .New) return;
- if (file.revision != 0 or file.content.len != 0) return;
- if (old.tag.own) |own| if (!std.mem.eql(u8, own, tagline.defaultTail(old))) return;
- layout.compute(p); // the joiner may have no rect yet; absorb snaps to rows
- p.removePane(old_id, new_id) catch |err| return p.reportError(old_id, "close", err);
-}
-
// ---- REPLs ----
//
// A terminal is bound as a language's REPL by `Repl python` (builtins.zig).
@@ -552,7 +524,7 @@ pub fn executeFrom(p: *Pardes, id: usize, txt: []const u8, from_body: bool) ?usi
/// Runs `line` in a command pane for `from`'s directory: `from` itself if
/// it is one whose command is done, else the directory's command pane that
-/// is done, else a new one below the last pane of the rightmost column.
+/// is done, else a new one, placed as placeNew places a command pane.
/// A pane reused keeps what it showed: the new command runs below a line
/// naming it, as acme appends to +Errors and never clears it (util.c:213).
fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
@@ -607,13 +579,7 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
return null;
};
echoCommand(p, pane, line);
- const rc = if (p.ncol > 0) p.ncol - 1 else 0;
- if (p.col_n[rc] > 0) {
- const parent = layout.splitParent(p, p.col_panes[rc][p.col_n[rc] - 1]);
- const f = layout.findPane(p, parent).?;
- layout.splitBelow(p, parent, pane);
- layout.insert(p, f.col, f.idx + 1, free);
- } else layout.insert(p, rc, p.col_n[rc], free);
+ placeNew(p, from, from, free, .command);
noteRun(p, pane, "run", line);
return free;
}
@@ -691,7 +657,24 @@ pub fn applyStartupConfig(p: *Pardes) void {
p.announce = false;
defer p.announce = true;
var lines = std.mem.splitScalar(u8, p.opts.startup_config orelse return, '\n');
- while (lines.next()) |line| _ = executeBuiltinLine(p, p.active, line);
+ while (lines.next()) |line| {
+ if (executeBuiltinLine(p, p.active, line)) continue;
+ // A setting pardes no longer has is said, once, and changes nothing.
+ const word = commandText(line);
+ if (std.mem.eql(u8, word[0 .. std.mem.indexOfAny(u8, word, " \t") orelse word.len], "ColumnTags"))
+ p.setMessage(p.active, "ColumnTags: column tags are always shown now; the init line is ignored");
+ }
+}
+
+test "an init file's retired ColumnTags line is said and ignored" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
+ defer p.deinit();
+ p.opts.startup_config = "ColumnTags\nWrap off\n";
+ applyStartupConfig(p);
+ try std.testing.expect(!p.settings.wrap);
+ try std.testing.expect(p.columnBarHeight() > 0);
+ const pane = p.panes[p.active].?;
+ try std.testing.expectEqualStrings("ColumnTags: column tags are always shown now; the init line is ignored", pane.msg[0..pane.msg_len]);
}
pub fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]const u8) void {
@@ -718,7 +701,6 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[]
const previous_context_style = p.settings.tree_context_tag_style;
const previous_transition = p.settings.panel_transition;
const previous_tagline_percent = p.settings.font.tagline_percent;
- const previous_column_tags = p.settings.column_tags;
switch (setting.action) {
.theme => {
const want = std.mem.trim(u8, arg orelse return, " \t\r\n");
@@ -768,20 +750,98 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[]
p.settings.panel_transition != previous_transition;
const tagline_metrics_changed = setting.action == .tagline_size and
p.settings.font.tagline_percent != previous_tagline_percent;
- if (p.settings.column_tags != previous_column_tags) {
- if (p.header_column != null) tagline.exitHeader(p);
- }
// Idle frames keep no frozen grid while transitions are off; the one
// just drawn is the baseline the first transition starts from.
if (transition_changed and previous_transition == .off)
p.presentation.capturePrevious(p.gpa, &p.surface);
- if (transition_changed or tagline_metrics_changed or p.settings.column_tags != previous_column_tags) {
+ if (transition_changed or tagline_metrics_changed) {
p.abandonPanelAnimations();
}
}
-pub fn placeDoc(p: *Pardes, from_id: usize, free: usize, nt: *Pane) void {
- p.stillborn_joiner = nt.serial;
+/// What a new pane is, and who asked: each rule places them differently.
+/// `scratch` is New's, `fs` 9P's pane/new, `errors` a +Errors pane.
+pub const NewPane = enum { scratch, fs, shell, doc, command, errors };
+
+/// Put new pane `id` in the layout: every new pane comes through here, by
+/// the `Placement` setting, and every one but a command pane takes the
+/// keyboard. `from` is the pane that asked, `origin` the text it asked
+/// 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 {
+ const asked = if (p.exec_column != null) null else origin;
+ switch (p.settings.placement) {
+ .acme => switch (kind) {
+ // New: the bottom half of the last pane in its own column
+ // (look.c:921-923, et->col).
+ .scratch => layout.acmePlace(p, p.exec_column orelse columnOf(p, from), null, id),
+ // +Errors, a command's output: the last column (util.c:94-98).
+ .command, .errors => layout.acmePlace(p, p.ncol - 1, null, id),
+ // makenewwindow(nil) for 9P's new (acme.c:877), else (t).
+ .fs => layout.acmePlace(p, acmeColumn(p, from), null, id),
+ .shell, .doc => layout.acmePlace(p, acmeColumn(p, from), asked, id),
+ },
+ .pardes => pardesPlace(p, from, id, kind),
+ }
+ if (kind == .command) return;
+ tagline.exitHeader(p);
+ p.active = id;
+ if (layout.findPane(p, id)) |f| p.setActiveColumn(f.col);
+}
+
+/// acme's makenewwindow column (util.c:456-467): the column a command came
+/// from (its tag's), else the active column, else the keyboard's pane's,
+/// else the asking pane's.
+fn acmeColumn(p: *Pardes, from: usize) usize {
+ if (p.exec_column) |c| return c;
+ if (p.activeColumn()) |c| return c;
+ if (layout.findPane(p, p.active)) |f| return f.col;
+ return columnOf(p, from);
+}
+
+fn columnOf(p: *Pardes, id: usize) usize {
+ return if (layout.findPane(p, id)) |f| f.col else p.ncol - 1;
+}
+
+/// The placements pardes had before it took acme's: an empty column whose
+/// tag asked, or has the keyboard, is filled; a scratch goes under the
+/// asking pane, a shell under it or the nearest pane with room, a document
+/// beside the last one read or in a column of its own, and a command pane
+/// at the foot of the last column.
+fn pardesPlace(p: *Pardes, from: usize, id: usize, kind: NewPane) void {
+ const empty = if (p.exec_column orelse if (p.header_focus) p.header_column else null) |c|
+ (if (c < p.ncol and p.col_n[c] == 0) c else null)
+ else
+ null;
+ if (empty) |c| {
+ layout.insert(p, c, 0, id);
+ p.panes[id].?.vweight = 1;
+ return;
+ }
+ const nt = p.panes[id].?;
+ switch (kind) {
+ .scratch, .fs => if (layout.findPane(p, from)) |sf| {
+ layout.insert(p, sf.col, sf.idx + 1, id);
+ layout.splitBelow(p, from, nt);
+ },
+ .shell => belowParent(p, from, id),
+ .doc, .errors => pardesDoc(p, from, id),
+ .command => {
+ const rc = p.ncol - 1;
+ if (p.col_n[rc] > 0) belowParent(p, p.col_panes[rc][p.col_n[rc] - 1], id) else layout.insert(p, rc, 0, id);
+ },
+ }
+}
+
+fn belowParent(p: *Pardes, from: usize, id: usize) void {
+ const parent = layout.splitParent(p, from);
+ const f = layout.findPane(p, parent).?;
+ layout.insert(p, f.col, f.idx + 1, id);
+ layout.splitBelow(p, parent, p.panes[id].?);
+}
+
+fn pardesDoc(p: *Pardes, from_id: usize, free: usize) void {
+ const nt = p.panes[free].?;
const doc = if (nt.file) |f| panes.Output.fileTraits(f.output).doc else true; // an image is a doc
var src_id: ?usize = null;
if (p.panes[from_id]) |src| if (if (src.file) |f| panes.Output.fileTraits(f.output).doc else src.image != null or src.hasPdf()) {
@@ -808,22 +868,14 @@ pub fn placeDoc(p: *Pardes, from_id: usize, free: usize, nt: *Pane) void {
if (!doc) if (layout.findPane(p, from_id)) |sf| {
layout.insert(p, sf.col, sf.idx + 1, free);
layout.splitBelow(p, from_id, nt); // NOT splitParent: no bystander pays
- p.active = free;
return;
};
if (src_id == null and doc and p.ncol < MAX_COLS and
layout.columnFitsHalves(p, from_id, 100) and layout.splitColumn(p, from_id, free, true))
- {
- p.active = free;
return;
- }
// Without room for a first-document column, split the Look origin
// just like Tty. Never displace (or evict) a top-left bystander.
- const src = layout.splitParent(p, src_id orelse from_id);
- const sf = layout.findPane(p, src).?;
- layout.insert(p, sf.col, sf.idx + 1, free);
- layout.splitBelow(p, src, nt);
- p.active = free;
+ belowParent(p, src_id orelse from_id, free);
}
test "the acme chords act once per selection, not once on the primary" {