From b8c5e8772e646fa21f6afdd2a875a1bd52848bd5 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 18:53:37 -0300 Subject: 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 --- src/mouse.zig | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'src/mouse.zig') diff --git a/src/mouse.zig b/src/mouse.zig index 22eb3270..5f0a6c4b 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -337,6 +337,9 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { wordAtCol(bar, at); const word = p.scratch.allocator().dupe(u8, selected) catch return; tagline.exitHeader(p); + // A column tag's command acts on that column, empty or not. + p.exec_column = column; + defer p.exec_column = null; if (word.len > 0) { if (m.button == config.exec_button) { const named = std.meta.stringToEnum(Builtin, word); @@ -450,6 +453,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { const on_tag = mrow >= tag_y and mrow < tag_y + pane.tag_rows; if (on_tag and mcol < r.x + TAG_TEXT_INSET) return; p.active = id; + p.noteTyped(); // acme's button 1 sets activecol (acme.c:659) pane.closePrompt(); const pointed = edit.pointerTextSelection(p, id, mcol, mrow, m.body_hit, p.pointer_tag_hit) orelse return; pane.clearPointerSelection(sel_slot); @@ -775,12 +779,15 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { layout.applyRowSplit(p, d.col, d.top_idx, d.cur_y), .column_move => |d| if (layout.columnBySerial(p, d.serial)) |column| switch (layout.columnDrop(p, column, d.cur_x, d.grab)) { .stay => {}, - .reorder => |target| layout.reorderColumn(p, column, target), + .reorder => |target| { + layout.reorderColumn(p, column, target); + p.setActiveColumn(target); // a grip drag sets activecol (acme.c:640) + }, .edge => |x| layout.setColumnPairWidth(p, column - 1, x - p.col_x[column - 1]), }, .move => |d| { layout.movePane(p, d.id, d.cur_x, d.cur_y); - if (p.panes[d.id]) |mp| p.stillborn_joiner = mp.serial; + if (layout.findPane(p, d.id)) |f| p.setActiveColumn(f.col); // a file moved into the left column evicts a lone unused shell if (p.panes[d.id]) |mt| if (mt.file != null) { if (layout.findPane(p, d.id)) |f| if (f.col == 0) -- cgit v1.3 From 0acd945d85887da88f2863219e706000ca738cdd Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 20:13:57 -0300 Subject: A document dragged beside the boot shell leaves it; BootShell replace closes it as before Placement and BootShell are one kind of setting now, a word from an enum, set, flipped bare, and reported by one arm. Co-Authored-By: Claude Opus 5.5 --- docs/config.md | 5 +++++ docs/tags.md | 4 ++++ src/CHANGELOG.md | 2 ++ src/builtins.zig | 2 +- src/config.zig | 45 +++++++++++++++++++++++++++++++++------------ src/exec.zig | 6 ++++-- src/mouse.zig | 3 ++- src/ninep/ctl.zig | 4 +++- src/pardes.zig | 18 ++++++++++++++++++ 9 files changed, 72 insertions(+), 17 deletions(-) (limited to 'src/mouse.zig') diff --git a/docs/config.md b/docs/config.md index c2e0b318..6905446a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -116,6 +116,11 @@ ignored with a message. `Placement acme` (the default) puts new panes where acme would, in the active column; `Placement pardes` brings back pardes's own rules, which open a first document in a column of its own. See [where new panes go](tags.md#where-new-panes-go). +`BootShell keep` (the default) leaves a shell alone when a document is dragged +into the left column beside it, whatever it holds; `BootShell replace` closes +such a shell there when it is the column's only one and nobody has typed +into it (no scrollback, cursor still on the first prompt line), the boot's +placeholder giving its rows to the document. Bare `BootShell` flips it. `Verbose` toggles the message-row announcement every builtin makes of its own name before it runs; it is on by default, and the builtins that own the message row themselves (`Msg`) never announce. Turning it off leaves the row to the diff --git a/docs/tags.md b/docs/tags.md index 26f3dcf0..b8299f22 100644 --- a/docs/tags.md +++ b/docs/tags.md @@ -220,6 +220,10 @@ a shell under it or the nearest pane with room; a document beside the last one read, or in a column of its own on the left when there is none and the column is at least 200 cells wide; a command pane at the foot of the last column. +`BootShell replace` brings back one more placeholder: a document dragged into +the left column closes the column's lone shell if nobody has typed into it. +`BootShell keep`, the default, never closes a pane for another one. + Down from an empty column's tag stays there, and the pane-to-pane keys pass over an empty column; Left and Right from a tag walk every column's tag. diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 874082d8..142287b2 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -13,6 +13,8 @@ the text of a pane with room to spare, else halving the biggest pane; never into a new column. `Placement pardes` brings back the old rules, which open a first document in a column of its own. +- A document dragged into the left column no longer closes the untouched + boot shell there; `BootShell replace` brings that back. - Column tags are always shown: the `ColumnTags` setting is gone, and an init file's `ColumnTags` line is ignored with a message. - Carrying a pane or a column by its grip shows acme's box cursor in the SDL diff --git a/src/builtins.zig b/src/builtins.zig index c2b4d62f..7b34b376 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -169,7 +169,7 @@ pub const registry = struct { inline for (comptime settingList(), manualBuiltinCount()..) |setting, i| if (@intFromEnum(b) == i) return switch (setting.action) { // a switch flips bare, and DumpDir bare is the default - .toggle, .transition, .scene, .dump_dir, .placement => false, + .toggle, .transition, .scene, .dump_dir, .choice => false, .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .shader, .shader_animation => true, }; unreachable; diff --git a/src/config.zig b/src/config.zig index 48f49d50..01a5daf8 100644 --- a/src/config.zig +++ b/src/config.zig @@ -74,6 +74,7 @@ pub const leader_path = paths: { .Delcol = "cd", .Joincol = "cj", .Placement = "cp", + .BootShell = null, .Debug = "td", .Msg = null, .Colors = "tc", @@ -666,9 +667,16 @@ pub const Runtime = struct { /// Where a new pane goes (exec.placeNew): `acme`, acme's makenewwindow, /// or `pardes`, the rules pardes had before it. placement: Placement = .acme, + /// A document dragged into the left column where a lone shell nobody + /// has typed into stands: `keep` leaves the shell, `replace` closes it + /// (exec.evictLonePristineTty). + boot_shell: BootShell = .keep, pub const ShaderAnimation = enum { off, on, always }; pub const Placement = enum { acme, pardes }; + pub const BootShell = enum { keep, replace }; + /// A setting that is one of a few words, each an enum field of Runtime. + pub const Choice = enum { placement, boot_shell }; /// Each pass the bundled Crt or a Shadertoy file. pub const Post = struct { @@ -841,7 +849,7 @@ pub const Runtime = struct { /// it of files. shader, shader_animation, - placement, + choice: Choice, }; pub const Setting = struct { @@ -893,7 +901,8 @@ pub const Runtime = struct { .{ .word = "Crt", .action = .{ .scene = .crt }, .availability = .scene_shaders }, .{ .word = "Shader", .action = .shader, .availability = .scene_shaders }, .{ .word = "ShaderAnimation", .action = .shader_animation, .availability = .scene_shaders }, - .{ .word = "Placement", .action = .placement }, + .{ .word = "Placement", .action = .{ .choice = .placement } }, + .{ .word = "BootShell", .action = .{ .choice = .boot_shell } }, }; pub fn find(name: []const u8) ?Setting { @@ -1052,13 +1061,17 @@ pub const Runtime = struct { const text = std.mem.trim(u8, argument orelse return false, " \t\r\n"); state.shader_animation = std.meta.stringToEnum(ShaderAnimation, text) orelse return false; }, - // Bare, it flips between the two, as a tag word should. - .placement => { - const text = std.mem.trim(u8, argument orelse "", " \t\r\n"); - state.placement = if (text.len == 0) - (if (state.placement == .acme) .pardes else .acme) - else - std.meta.stringToEnum(Placement, text) orelse return false; + // Bare, it steps to the next word, as a tag word should. + .choice => |which| switch (which) { + inline else => |field| { + const now = &@field(state, @tagName(field)); + const T = @TypeOf(now.*); + const text = std.mem.trim(u8, argument orelse "", " \t\r\n"); + now.* = if (text.len == 0) + @enumFromInt((@as(usize, @intFromEnum(now.*)) + 1) % std.enums.values(T).len) + else + std.meta.stringToEnum(T, text) orelse return false; + }, }, .theme, .font => return false, } @@ -1229,7 +1242,9 @@ pub const Runtime = struct { try out.print("{s} {s}\n", .{ setting.word, @tagName(state.shader_animation) }) else try out.print("{s} unsupported\n", .{setting.word}), - .placement => try out.print("{s} {s}\n", .{ setting.word, @tagName(state.placement) }), + .choice => |which| switch (which) { + inline else => |field| try out.print("{s} {s}\n", .{ setting.word, @tagName(@field(state, @tagName(field))) }), + }, }; if (context.startup_config_path) |path| @@ -1481,7 +1496,10 @@ pub const Runtime = struct { .dump_dir => "~/dumps", .shader => "~/crt.glsl", .shader_animation => "always", - .placement => "pardes", + .choice => |which| switch (which) { + .placement => "pardes", + .boot_shell => "replace", + }, else => null, }; try std.testing.expect(apply(&state, setting, argument)); @@ -1531,7 +1549,10 @@ pub const Runtime = struct { }, .shader => "Shader ~/crt.glsl\n", .shader_animation => "ShaderAnimation always\n", - .placement => "Placement pardes\n", + .choice => |which| switch (which) { + .placement => "Placement pardes\n", + .boot_shell => "BootShell replace\n", + }, .theme, .font => unreachable, }; try std.testing.expect(std.mem.indexOf(u8, report, expected) != null); diff --git a/src/exec.zig b/src/exec.zig index 117970d9..22aae10a 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -326,9 +326,11 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane { return nt; } -/// when a doc lands in `col`, a lone pristine shell there is clutter — drop -/// it; absorbVWeight hands its space to the doc +/// With `BootShell replace`, a doc landing in `col` closes a lone pristine +/// shell there, the boot's placeholder, and absorbVWeight hands its space to +/// the doc. With `keep`, the default, the shell stays like any other pane. pub fn evictLonePristineTty(p: *Pardes, col: usize, keep_id: usize) void { + if (p.settings.boot_shell == .keep) return; var n_tty: usize = 0; var tty_id: usize = 0; for (0..p.col_n[col]) |k| { diff --git a/src/mouse.zig b/src/mouse.zig index 5f0a6c4b..ac72481a 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -788,7 +788,8 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { .move => |d| { layout.movePane(p, d.id, d.cur_x, d.cur_y); if (layout.findPane(p, d.id)) |f| p.setActiveColumn(f.col); - // a file moved into the left column evicts a lone unused shell + // BootShell replace: a file moved into the left column closes a + // lone unused shell there if (p.panes[d.id]) |mt| if (mt.file != null) { if (layout.findPane(p, d.id)) |f| if (f.col == 0) exec.evictLonePristineTty(p, 0, d.id); diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index b4ae7231..23d966e8 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -308,7 +308,9 @@ fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void { .tagline_size => try w.print("{s} {d}\n", .{ word, s.font.tagline_percent }), .window_opacity => try w.print("{s} {d}\n", .{ word, s.window_opacity }), .window_blur => try w.print("{s} {d}\n", .{ word, s.window_blur }), - .placement => try w.print("{s} {s}\n", .{ word, @tagName(s.placement) }), + .choice => |which| switch (which) { + inline else => |field| try w.print("{s} {s}\n", .{ word, @tagName(@field(s, @tagName(field))) }), + }, .message_ms => |which| try w.print("{s} {d}\n", .{ word, @field(s, "message_" ++ @tagName(which) ++ "_ms") }), // Bare, it names the default directory. .dump_dir => if (s.dump_dir.len > 0) try w.print("{s} {s}\n", .{ word, s.dump_dir.get() }) else try w.print("{s}\n", .{word}), diff --git a/src/pardes.zig b/src/pardes.zig index e3f4bb60..23341904 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1336,6 +1336,24 @@ test "an empty column shows its theme's empty_col under its tag: acme's white, e } } +test "BootShell keep leaves the untouched boot shell a dragged document joins; replace closes it" { + inline for (.{ "keep", "replace" }) |word| { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 }); + defer p.deinit(); + const shell = p.active; + try std.testing.expect(p.panes[shell].?.isTerminal()); + try std.testing.expect(p.executeBuiltinLine(shell, "BootShell " ++ word)); + _ = try p.openTutorView(1); + layout.insert(p, 0, 1, 1); + p.sync(); + exec.evictLonePristineTty(p, 0, 1); + p.sync(); + const kept = std.mem.eql(u8, word, "keep"); + try std.testing.expectEqual(kept, p.panes[shell] != null); + try std.testing.expectEqual(@as(usize, if (kept) 2 else 1), p.col_n[0]); + } +} + test "dragging a column's only pane to another leaves that column in place, empty" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 32 }); defer p.deinit(); -- cgit v1.3