summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 19:17:12 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit8429e7bd8aff1ae0c71d3b1a7a6b9f01ed52e691 (patch)
tree9ae9dbe35e371a7b64b7e64bd690c4871e0b705c
parent4cea4751960b359561997cb040f05179923cb80e (diff)
downloadpardes-8429e7bd8aff1ae0c71d3b1a7a6b9f01ed52e691.tar.gz
pardes-8429e7bd8aff1ae0c71d3b1a7a6b9f01ed52e691.zip
Column and workspace tags are one line again, as acme's
acme gives a column tag one font height (cols.c:244) and its row tag one line; pardes let them grow a row per line. A newline typed, pasted, written or restored into a column or workspace tag now becomes a space, the caret and selection kept on the same characters, and the bars are one row each. A dump holding a multi-line header tag comes back with its lines joined by spaces. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/tags.md12
-rw-r--r--src/pardes.zig45
-rw-r--r--src/tagline.zig38
-rw-r--r--test/panes.zig14
4 files changed, 61 insertions, 48 deletions
diff --git a/docs/tags.md b/docs/tags.md
index fdea1472..9b9a23c5 100644
--- a/docs/tags.md
+++ b/docs/tags.md
@@ -71,11 +71,13 @@ are passed over: they only keep a weight for later.
A tag is a text like a pane's body: pane tags, column tags and the
workspace tag all edit with the body's own normal and insert modes, undo
-included, and may hold more than one line. A tag with a newline in it takes
-a row per line, up to eight: a pane's tag leaves its body at least one row
-and a collapsed pane shows only the first line; the column and workspace
-tags take no more than a third of the screen, and the panes below move down
-to make room. The path, the dirty marker and a PDF's page at the start of a
+included. A pane's tag may hold more than one line: one with a newline in
+it takes a row per line, up to eight, and leaves its body at least one row;
+a collapsed pane shows only the first line. The column and workspace tags
+are one line, as acme's (cols.c:244 gives a column tag one font height): a
+newline typed, pasted or written into one becomes a space, and a dump that
+holds a column or workspace tag of several lines, from before, comes back
+with its lines joined by spaces. The path, the dirty marker and a PDF's page at the start of a
pane tag are computed, never stored, and read-only. The keyboard reaches
them as the mouse does: `0` goes to the line's start, the path's, as in
acme, and motions select and yank across the path and the commands. An edit
diff --git a/src/pardes.zig b/src/pardes.zig
index 00843b78..cebd851b 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -3148,7 +3148,7 @@ test "header clicks beyond text append and long UTF-8 tags reveal their caret" {
try std.testing.expectEqual(@as(usize, 0), p.header_scroll);
}
-test "a column tag with a newline in it takes a row per line and pushes its panes down" {
+test "a newline typed into a column tag is a space: the tag stays one row" {
const p = try Pardes.init(std.testing.allocator, .{ .cols = 50, .rows = 12, .tty_only = true });
defer p.deinit();
_ = try p.setTestFile("body\n");
@@ -3158,26 +3158,9 @@ test "a column tag with a newline in it takes a row per line and pushes its pane
p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = 40, .row = TOPBAR_H } });
p.update(.{ .key = .{ .cp = Key.enter } });
p.update(.{ .key = .{ .cp = 'S', .text = "Snarf" } });
- try std.testing.expectEqualStrings("New\nSnarf", tagline.columnTag(p, 0));
- try std.testing.expectEqual(@as(u16, 2), p.columnBarHeight());
- const frame = try p.render(p.scratch.allocator());
- try std.testing.expectEqual(TOPBAR_H + 2, p.rects[0].y);
- try std.testing.expectEqual(TOPBAR_H + 1, frame.cursor.?.y);
- try std.testing.expectEqual(@as(u8, 'S'), frame.at(TAG_TEXT_INSET, TOPBAR_H + 1).text[0]);
-
- // A click on the second row lands on the second line.
- p.update(.{ .key = .{ .cp = Key.escape } });
- p.acknowledgePanelPresentation(&.{});
- p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = TAG_TEXT_INSET + 1, .row = TOPBAR_H + 1 } });
- p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = TAG_TEXT_INSET + 1, .row = TOPBAR_H + 1 } });
- try std.testing.expectEqual(@as(i32, 1), p.col_tags[0].cur_row);
- try std.testing.expectEqual(@as(i32, 1), p.col_tags[0].cur_col);
-
- // Taking the newline back out gives the panes their row again.
- p.update(.{ .key = .{ .cp = Key.home } });
- p.update(.{ .key = .{ .cp = Key.backspace } });
- try std.testing.expectEqualStrings("NewSnarf", tagline.columnTag(p, 0));
_ = try p.render(p.scratch.allocator());
+ try std.testing.expectEqualStrings("New Snarf", tagline.columnTag(p, 0));
+ try std.testing.expectEqual(COLBAR_H, p.columnBarHeight());
try std.testing.expectEqual(TOPBAR_H + COLBAR_H, p.rects[0].y);
}
@@ -4457,24 +4440,21 @@ pub const Pardes = struct {
return a;
}
- /// The workspace tag's height: a row per line of it while the setting
- /// keeps it, zero once a native shell takes its commands over. Layout and
- /// hit testing read this rather than the constant, the way they read
- /// `columnBarHeight`.
+ /// The workspace tag's height: one row while the setting keeps it, as
+ /// acme's row tag is, zero once a native shell takes its commands over.
+ /// Layout and hit testing read this rather than the constant, the way
+ /// they read `columnBarHeight`.
pub fn topBarHeight(p: *const Pardes) u16 {
if (!p.settings.workspace_tag) return 0;
- const lines = if (p.global_tag.own) |own| std.mem.count(u8, own, "\n") + 1 else TOPBAR_H;
- return @intCast(@min(lines, MAX_TAG_ROWS, @max(TOPBAR_H, p.screen_h / 3)));
+ return TOPBAR_H;
}
- /// The column tags' height: a row per line of the tallest one.
+ /// The column tags' height: one row, as acme's (cols.c:244, colresize,
+ /// gives a column tag one font height); a newline in one is a space
+ /// (tagline.flattenHeaders).
pub fn columnBarHeight(p: *const Pardes) u16 {
if (!p.settings.column_tags or p.screen_h < 3) return 0;
- var lines: usize = COLBAR_H;
- for (p.col_tags[0..p.ncol]) |t| if (t.own) |own| {
- lines = @max(lines, std.mem.count(u8, own, "\n") + 1);
- };
- return @intCast(@min(lines, MAX_TAG_ROWS, @max(COLBAR_H, p.screen_h / 3)));
+ return COLBAR_H;
}
fn detachCwds(p: *Pardes, parents: []const *Pane) !void {
@@ -6119,6 +6099,7 @@ pub const Pardes = struct {
/// Recompute geometry, push grid-size changes to each emulator + pty, fire
/// deferred greetings. The mirror of the prototype's loop epilogue.
pub fn sync(p: *Pardes) void {
+ tagline.flattenHeaders(p);
if (p.header_column != null and p.columnBarHeight() == 0) {
tagline.exitHeader(p);
}
diff --git a/src/tagline.zig b/src/tagline.zig
index 59c51b4d..858bb9b1 100644
--- a/src/tagline.zig
+++ b/src/tagline.zig
@@ -453,3 +453,41 @@ pub fn headerSelection(t: *const Text, text: []const u8, row: usize) ?struct { l
if (hi <= start or lo >= end) return null;
return .{ .lo = @max(lo, start) - start, .hi = @min(hi, end) - start };
}
+
+/// Column and workspace tags are one line, as acme's: a newline typed,
+/// pasted or restored from a dump into one becomes a space, the caret and
+/// the selection kept on the same characters.
+pub fn flattenHeaders(p: *Pardes) void {
+ for (p.col_tags[0..p.ncol]) |*t| flatten(t);
+ flatten(&p.global_tag);
+}
+
+fn flatten(t: *Text) void {
+ const own = t.own orelse return;
+ if (std.mem.indexOfScalar(u8, own, '\n') == null) return;
+ const cur = modal.offsetAt(own, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) });
+ const anchor = modal.offsetAt(own, .{ .row = @intCast(@max(0, t.vsel.row)), .col = @intCast(@max(0, t.vsel.col)) });
+ std.mem.replaceScalar(u8, own, '\n', ' ');
+ t.cur_row = 0;
+ t.cur_col = @intCast(cur);
+ t.vsel.row = 0;
+ t.vsel.col = @intCast(anchor);
+ t.nsel = 0;
+}
+
+test "a column or workspace tag is one line: a newline in it is a space" {
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 50, .rows = 12, .tty_only = true });
+ defer p.deinit();
+ p.col_tags[0].own = try p.gpa.dupe(u8, "New\nSnarf");
+ p.col_tags[0].cur_row = 1;
+ p.col_tags[0].cur_col = 2;
+ p.global_tag.own = try p.gpa.dupe(u8, "Newcol\nExit");
+ p.sync();
+ try std.testing.expectEqualStrings("New Snarf", columnTag(p, 0));
+ try std.testing.expectEqualStrings("Newcol Exit", p.global_tag.own.?);
+ try std.testing.expectEqual(@as(i32, 0), p.col_tags[0].cur_row);
+ try std.testing.expectEqual(@as(i32, 6), p.col_tags[0].cur_col);
+ try std.testing.expectEqual(pardes.COLBAR_H, p.columnBarHeight());
+ try std.testing.expectEqual(pardes.TOPBAR_H, p.topBarHeight());
+}
+
diff --git a/test/panes.zig b/test/panes.zig
index 96819728..233e7627 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -267,22 +267,14 @@ test "a tag with more lines than rows scrolls to keep its caret in view" {
try std.testing.expectEqual(tag_y, top.cursor.?.y);
try std.testing.expectEqualStrings("/", top.at(x, tag_y).grapheme());
- // A column tag does the same in its rows.
+ // A column tag is one row, as acme's: its lines are joined.
many.clearRetainingCapacity();
try many.appendSlice(std.testing.allocator, "New");
for (1..21) |n| try many.print(std.testing.allocator, "\nC{d}", .{n});
p.col_tags[0].own = try p.gpa.dupe(u8, many.items);
p.update(.tick);
- const rows = p.columnBarHeight();
- try std.testing.expectEqual(@as(u16, pardes.MAX_TAG_ROWS), rows);
- pardes.tagline.enterHeader(p, 0);
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'e' } });
- const header = try p.render(frame.allocator());
- const header_row = p.topBarHeight() + rows - 1;
- try std.testing.expectEqual(header_row, header.cursor.?.y);
- try std.testing.expectEqualStrings("C", header.at(p.col_x[0] + pardes.TAG_TEXT_INSET, header_row).grapheme());
- try std.testing.expectEqualStrings("2", header.at(p.col_x[0] + pardes.TAG_TEXT_INSET + 1, header_row).grapheme());
+ try std.testing.expectEqual(pardes.COLBAR_H, p.columnBarHeight());
+ try std.testing.expect(std.mem.indexOfScalar(u8, p.col_tags[0].own.?, '\n') == null);
}
const TagNavigationTests = struct {