summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 19:06:01 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit242467eb9f066f80164d534be55c2c4ebc9b5604 (patch)
treee3ec33330d13c63c61d51cdd406d9788a67d947b
parent8f0ac9d1f4aaffa7e264cab80b949040a88f0f08 (diff)
downloadpardes-242467eb9f066f80164d534be55c2c4ebc9b5604.tar.gz
pardes-242467eb9f066f80164d534be55c2c4ebc9b5604.zip
acme looks like acme; every theme gets acme's button, scroll column and 2px rules
acme takes plan9port's own colours (acme.c iconinit, draw.h): selections #eeee9e in the body and #9eeeee in the tag, black ink; exec and look sweeps #aa0000 and #006600, white ink; a #99994c scrollbar under a #ffffea thumb; black rules; a #8888cc tag/body rule and column button. Every theme: the grip is acme's button, its tag row's full height, in one scroll column with the scrollbar (rail_px, 12 at a 17px tagline, scaled, times GripWidth, a percent, default 150; same x0/x1 to the pixel): focused clean = solid box; focused dirty = box_dirty filled to a box ring (acme's modbutton); unfocused clean = a box_border ring round the tag's ground; unfocused dirty = box_dirty a pixel inside that ring. The grid, which has no ring, marks dirty with a bold * on box_dirty in the grip's second cell and fills an unfocused grip halfway to its ring. Regions carry a dirty flag. Column buttons are the same shape, solid. 2px rules between columns and stacked panes (inside the tag band's slack), none at the window edge; a 1px tag/body rule halfway between tag and page. Theme data: tag_sel_bg, sweep_bg/fg, tag_rule, rule_px, rail_px, box_border, box_dirty. Chrome: rule_px, tag_rule, rail_px, grip_border, grip_focus_ring; a comptime guard on Chrome's layout forces a wire bump once v8 ships. Structural rules are exempt from the focus-indicator rule (docs/effects.md). Tests: acme widths; stacked rule in the slack at two tagline sizes; every grip-column pixel is band, button, ring or rule, and the button shares the scrollbar's x0/x1 (4 metrics x top/bottom x focus x dirty); the four grip states distinct in every native theme with a non-colour dirty cue in the grid; wire round-trip of the new fields. Shared files: src/colors.zig, src/surface.zig, src/body_layer.zig, src/tagline.zig, src/detached/wire.zig, docs/themes.md, docs/config.md, docs/effects.md, 21 snapshot goldens (the grip cells only), test/gui-goldens.txt. Motion flavours differ by design, and drive notice drops and pane moves
-rw-r--r--docs/config.md8
-rw-r--r--docs/effects.md5
-rw-r--r--docs/themes.md6
-rw-r--r--src/body_layer.zig8
-rw-r--r--src/builtins.zig2
-rw-r--r--src/colors.zig25
-rw-r--r--src/config.zig20
-rw-r--r--src/detached/wire.zig24
-rw-r--r--src/draw.zig124
-rw-r--r--src/gui/gui.zig461
-rw-r--r--src/ninep/ctl.zig1
-rw-r--r--src/surface.zig15
-rw-r--r--src/tagline.zig8
-rw-r--r--src/themes/acme.zig33
-rw-r--r--test/gui-goldens.txt36
-rw-r--r--test/snapshots/chordcut.golden10
-rw-r--r--test/snapshots/delask.golden6
-rw-r--r--test/snapshots/fileedit.golden2
-rw-r--r--test/snapshots/filewatch.golden4
-rw-r--r--test/snapshots/lsp-client-edit.golden2
-rw-r--r--test/snapshots/lsp-rename.golden2
-rw-r--r--test/snapshots/lspcomplete.golden10
-rw-r--r--test/snapshots/lspcompletemoved.golden2
-rw-r--r--test/snapshots/lsprelpath.golden12
-rw-r--r--test/snapshots/msg.golden2
-rw-r--r--test/snapshots/multicursor.golden4
-rw-r--r--test/snapshots/ninep.golden16
-rw-r--r--test/snapshots/pipe.golden2
-rw-r--r--test/snapshots/savepane.golden2
-rw-r--r--test/snapshots/selregex.golden2
-rw-r--r--test/snapshots/tagbottom.golden2
-rw-r--r--test/snapshots/tagcmd.golden2
-rw-r--r--test/snapshots/tagpath.golden10
-rw-r--r--test/snapshots/theme.golden8
-rw-r--r--test/snapshots/themesel.golden14
-rw-r--r--test/snapshots/yankpaste.golden2
36 files changed, 749 insertions, 143 deletions
diff --git a/docs/config.md b/docs/config.md
index 83fbd0eb..324b54e9 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -315,8 +315,10 @@ name, thirteen required RGB roles (`sel_bg`, `sel_fg`, `tag_bg`, `tag_fg`,
`scroll_thumb`), nullable `bg`/`fg`, and either a 16-color RGB `palette` or
`null`. Optional nullable RGB roles extend this format:
`tag_active_bg`, `tag_active_fg`, `tag_name_fg`, `tag_active_name_fg`, `border`, `lineno_active`, `search_bg`, `search_fg`,
-`diagnostic_error`, `diagnostic_warning`, `diagnostic_info`, and
-`diagnostic_hint`. Missing new roles use backward-compatible defaults, so
+`diagnostic_error`, `diagnostic_warning`, `diagnostic_info`,
+`diagnostic_hint`, `tag_sel_bg`, `tag_rule`, `box_border` and `box_dirty`;
+`sweep_bg` and `sweep_fg` take three triples, and `rule_px` and `rail_px` a
+pixel count. Missing new roles use backward-compatible defaults, so
previously exported files remain valid. [Theme customization](themes.md)
describes each role and its fallback. RGB values are three-byte arrays, and
hex literals are accepted. The original fields remain required; there is no
@@ -536,6 +538,8 @@ Lift auto a shadow on a light page; on a dark one the others recede
Lift off
InactiveDim 30 the unfocused panes' text fades 30% toward its ground
Motion smooth off, crisp, smooth, bouncy or playful
+GripWidth 150 the grip's button and the scrollbar under it, percent of the
+ theme's rail_px (12px at a 17px tagline; 50 to 300)
```
`InactiveDim` works everywhere (a grid shows it at once) and never takes a
diff --git a/docs/effects.md b/docs/effects.md
index fec2d13b..eefa52d3 100644
--- a/docs/effects.md
+++ b/docs/effects.md
@@ -6,7 +6,10 @@ selection) or reduce its contrast. The effects are sugar; the indicators are
how a person knows where they are. Every G stage is reviewed against this.
Lift, for one, falls only on pane bodies and rails, never on a tag, a grip or
a header, and its strength is capped so text and the selection keep min(their
-contrast, 4.5) (tests in src/gui/gui.zig). A dim touches only the unfocused
+contrast, 4.5) (tests in src/gui/gui.zig). The rule binds effects, not a
+theme's structure: the rules between panes (2px, `rule_px`) are the theme's
+borders, and the one over a tag band stays in the band's slack, never over
+its text. A dim touches only the unfocused
panes, with the same floor, and the focused pane's text is never at less
contrast than theirs (tests in src/draw.zig).
diff --git a/docs/themes.md b/docs/themes.md
index 8cc4ff9b..6b218b13 100644
--- a/docs/themes.md
+++ b/docs/themes.md
@@ -114,6 +114,12 @@ so existing exported themes remain valid.
| `diagnostic_warning` | Warning text | Same foreground fallback |
| `diagnostic_info` | Informational diagnostic text | Same foreground fallback |
| `diagnostic_hint` | Hint text | Same foreground fallback |
+| `tag_sel_bg` | A tag's selection ground | `sel_bg` |
+| `sweep_bg`, `sweep_fg` | Three triples each: the select, exec and look sweeps' ground and ink | `sel_bg` tinted toward `kw`, `str` and `num`, in `sel_fg` |
+| `tag_rule` | The one-pixel rule between a pane's tag and its body, quieter than the `rule_px` rules between panes and columns | Halfway between `tag_bg` and the page the shell draws |
+| `rule_px` | Pixel shells: the width of the rules between columns, between panes stacked in a column, and under the workspace and column tags, in `border` (the rule between a tag and its body is always one pixel) | 2 |
+| `box_border`, `box_dirty` | The grip is acme's button: `box` filled when focused, else a two-pixel ring of `box_border` round the tag's ground; `box_dirty` fills it inside either ring while its file is unsaved (a grid, which has no ring, shows a bold `*` on `box_dirty` in the grip's second cell) | `box_dim`; `diagnostic_warning`, then `num` |
+| `rail_px` | Pixel shells: the scroll column's width, the grip's button over the scrollbar (thumb a pixel narrower), in pixels at a 17px tagline, scaled with it | 12, acme's Scrollwid |
Native palettes give filenames a distinct hue at approximately the same
brightness as the surrounding tag text. This also applies to output names
diff --git a/src/body_layer.zig b/src/body_layer.zig
index a0195fb8..875f2e36 100644
--- a/src/body_layer.zig
+++ b/src/body_layer.zig
@@ -156,6 +156,7 @@ pub fn paintPointerSelection(
/// The selection colour of each pointer button's sweep: the select button's
/// and the look and exec buttons', each tinted toward its own accent.
pub fn buttonTints(th: *const colors.Theme) [3][3]u8 {
+ if (th.sweep_bg) |own| return own;
const accent2 = if (std.mem.eql(u8, &th.num, &th.str)) th.comment else th.num;
return .{
colors.mix(th.sel_bg, colors.mix(th.sel_bg, th.kw)),
@@ -164,6 +165,11 @@ pub fn buttonTints(th: *const colors.Theme) [3][3]u8 {
};
}
+/// The ink over each button's sweep.
+pub fn buttonInks(th: *const colors.Theme) [3][3]u8 {
+ return th.sweep_fg orelse @splat(th.sel_fg);
+}
+
/// A pane's body, painted once. A file body with compact tree-sitter
/// context rows is painted into its body layer -- more rows than the grid
/// has, the context rows at the tagline's height -- and the grid's rows are
@@ -312,7 +318,7 @@ pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane
// A select sweep over the tag is the tag's selection, painted there
// in the tag's own selection colours.
if (b == pardes.sel_slot and @min(sl.r0, sl.r1) < pane.tag_rows) continue;
- paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sl, pane.tag_rows, sel_btn[b], th.sel_fg);
+ paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sl, pane.tag_rows, sel_btn[b], buttonInks(th)[b]);
}
// modal line selection (helix `x`): whole body rows, normal mode only
if ((pane.body.mode == .normal or modal_hover) and pane.body.msel.active) {
diff --git a/src/builtins.zig b/src/builtins.zig
index e49deb71..c25311e3 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -172,7 +172,7 @@ pub const registry = struct {
if (@intFromEnum(b) == i) return switch (setting.action) {
// a switch flips bare, and DumpDir bare is the default
.toggle, .transition, .scene, .dump_dir, .lift => false,
- .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .shader, .shader_animation, .motion, .inactive_dim => true,
+ .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .shader, .shader_animation, .motion, .inactive_dim, .grip_width => true,
};
unreachable;
}
diff --git a/src/colors.zig b/src/colors.zig
index aeff7b00..25aa838f 100644
--- a/src/colors.zig
+++ b/src/colors.zig
@@ -63,6 +63,29 @@ pub const Theme = struct {
// The current line number is a quiet foreground cue, not a tag-colored
// background block. Older themes fall back to their normal gutter ink.
lineno_active: ?[3]u8 = null,
+ /// A tag's selection, where it differs from the body's (acme's #9eeeee).
+ tag_sel_bg: ?[3]u8 = null,
+ /// Each pointer button's sweep, select, exec and look (Mouse.Button
+ /// order), and the ink over it. Unset, tints of `sel_bg` in `sel_fg`.
+ sweep_bg: ?[3][3]u8 = null,
+ sweep_fg: ?[3][3]u8 = null,
+ /// A pixel shell's rules, in pixels (2 unset); the rule between a tag
+ /// and its body is always one.
+ rule_px: ?u8 = null,
+ /// A grip is acme's button: `box` filled when focused, else a ring of
+ /// `box_border` (unset: `box_dim`) round the tag's ground; `box_dirty`
+ /// (unset: `diagnostic_warning`, then `num`) fills it, in either ring,
+ /// while its file is unsaved.
+ box_border: ?[3]u8 = null,
+ box_dirty: ?[3]u8 = null,
+ /// The rule between a pane's tag and its body, one pixel, quieter than
+ /// the rules between panes (acme's #8888cc under its 2px black ones).
+ /// Unset, halfway between the tag's ground and the page.
+ tag_rule: ?[3]u8 = null,
+ /// A pixel shell's scroll column, the grip's button over the scrollbar,
+ /// in pixels at a 17px tagline, the thumb one narrower (acme's 12px
+ /// Scrollwid, the default).
+ rail_px: ?u8 = null,
};
const curated = struct {
@@ -108,6 +131,8 @@ fn fold(comptime C: type) [@typeInfo(C).@"struct".decls.len]Theme {
}
pub const themes = fold(curated) ++ fold(@import("generated_themes"));
+/// The first `native_count` of `themes` are pardes's own; the rest imported.
+pub const native_count = @typeInfo(curated).@"struct".decls.len;
/// Original and classic-inspired Pardes palettes precede retained imports.
pub const native_theme_count = 15;
diff --git a/src/config.zig b/src/config.zig
index ed7bee92..24e058c7 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -118,6 +118,7 @@ pub const leader_path = paths: {
if (builtins.capabilities.lift) table.set(.Lift, null);
table.set(.Motion, null);
table.set(.InactiveDim, null);
+ if (builtins.capabilities.lift) table.set(.GripWidth, null);
if (builtins.capabilities.panel_transitions) {
table.set(.PanelSlide, "as");
table.set(.PanelZoom, "az");
@@ -629,6 +630,10 @@ pub const Runtime = struct {
/// percent (0 off): the focused pane wins by comparison, its tag tint
/// untouched. Kept above min(each cell's contrast, 4.5).
inactive_dim: u8 = 0,
+ /// The grip's button and the scrollbar under it, one width, as a percent
+ /// of the theme's `rail_px` (acme's 12px Scrollwid at a 17px tagline):
+ /// a pixel shell's (docs/themes.md).
+ grip_width: u16 = 150,
/// How the fx track's animations move (animation.Motion): off for
/// reduced motion.
motion: animation.Motion.Flavour = .smooth,
@@ -862,6 +867,7 @@ pub const Runtime = struct {
motion,
/// A percentage, 0 through 100.
inactive_dim,
+ grip_width,
transition: animation.Transition,
scene: Scene,
/// A Shadertoy file into the post chain or out of it; `off` empties
@@ -899,6 +905,7 @@ pub const Runtime = struct {
.{ .word = "Lift", .action = .lift, .availability = .lift },
.{ .word = "Motion", .action = .motion },
.{ .word = "InactiveDim", .action = .inactive_dim },
+ .{ .word = "GripWidth", .action = .grip_width, .availability = .lift },
.{ .word = "MessageLinger", .action = .{ .message_ms = .linger } },
.{ .word = "MessageFall", .action = .{ .message_ms = .fall } },
.{ .word = "MessageDissolve", .action = .{ .message_ms = .dissolve } },
@@ -960,8 +967,8 @@ pub const Runtime = struct {
@compileError("scene effects must use the scene-shader capability"),
// A toggle is plain state any shell can hold, except one for
// what only some shells draw: that one exists where they do.
- .lift => if (setting.availability != .lift)
- @compileError("Lift must use the lift capability"),
+ .lift, .grip_width => if (setting.availability != .lift)
+ @compileError("Lift and GripWidth must use the lift capability (a pixel shell's)"),
.toggle => |field| if (field == .ligatures) {
if (setting.availability != .ligatures)
@compileError("the ligatures toggle must use the ligatures capability");
@@ -1102,6 +1109,12 @@ pub const Runtime = struct {
if (percent > 100) return false;
state.inactive_dim = percent;
},
+ .grip_width => {
+ const text = std.mem.trim(u8, argument orelse return false, " \t\r\n%");
+ const percent = std.fmt.parseInt(u16, text, 10) catch return false;
+ if (percent < 50 or percent > 300) return false;
+ state.grip_width = percent;
+ },
.theme, .font => return false,
}
return true;
@@ -1277,6 +1290,7 @@ pub const Runtime = struct {
try out.print("{s} unsupported\n", .{setting.word}),
.motion => try out.print("{s} {s}\n", .{ setting.word, @tagName(state.motion) }),
.inactive_dim => try out.print("{s} {d}\n", .{ setting.word, state.inactive_dim }),
+ .grip_width => try out.print("{s} {d}\n", .{ setting.word, state.grip_width }),
};
if (context.startup_config_path) |path|
@@ -1547,6 +1561,7 @@ pub const Runtime = struct {
.lift => "rim",
.motion => "playful",
.inactive_dim => "35",
+ .grip_width => "200",
else => null,
};
try std.testing.expect(apply(&state, setting, argument));
@@ -1600,6 +1615,7 @@ pub const Runtime = struct {
.lift => "Lift rim\n",
.motion => "Motion playful\n",
.inactive_dim => "InactiveDim 35\n",
+ .grip_width => "GripWidth 200\n",
.theme, .font => unreachable,
};
try std.testing.expect(std.mem.indexOf(u8, report, expected) != null);
diff --git a/src/detached/wire.zig b/src/detached/wire.zig
index 2a2ccc6b..c0e17054 100644
--- a/src/detached/wire.zig
+++ b/src/detached/wire.zig
@@ -11,6 +11,19 @@ const limits = @import("../memory.zig").limits;
/// line), and a frame carries the core's placed regions after its layers.
pub const version: u16 = 8;
+/// The Chrome palette is carried field by field as its declaration lists
+/// them, so any change to it changes the wire: bump `version`, then this.
+const chrome_layout: u64 = 0x7e3d0952317bfee;
+comptime {
+ @setEvalBranchQuota(10_000);
+ var hash = std.hash.Fnv1a_64.init();
+ for (@typeInfo(pardes.Chrome).@"struct".fields) |field| {
+ hash.update(field.name);
+ hash.update(@typeName(field.type));
+ }
+ if (hash.final() != chrome_layout) @compileError(std.fmt.comptimePrint("pardes.Chrome changed (layout 0x{x}): bump wire.version, then chrome_layout", .{hash.final()}));
+}
+
pub const Error = error{ Truncated, Overlong, BadTag, BadValue, Trailing, NoSpace };
pub const max_cols: u16 = 512;
@@ -248,7 +261,10 @@ pub const Frame = struct {
region.owner = try r.getByte();
region.serial = try r.getU32();
region.rect = .{ .x = try r.getU16(), .y = try r.getU16(), .w = try r.getU16(), .h = try r.getU16() };
- region.active = try r.getBool();
+ const flags = try r.getByte();
+ if (flags > 3) return error.BadValue;
+ region.active = flags & 1 != 0;
+ region.dirty = flags & 2 != 0;
region.thumb_y = try r.getU16();
region.thumb_h = try r.getU16();
region.notice = try r.getByte();
@@ -807,7 +823,7 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point
try w.putU16(region.rect.y);
try w.putU16(region.rect.w);
try w.putU16(region.rect.h);
- try w.putBool(region.active);
+ try w.putByte(@as(u8, @intFromBool(region.active)) | @as(u8, @intFromBool(region.dirty)) << 1);
try w.putU16(region.thumb_y);
try w.putU16(region.thumb_h);
try w.putByte(region.notice);
@@ -1898,10 +1914,10 @@ test "detached wire carries compact body rows and exact pointer metrics" {
var tag_cells: [3]pardes.Cell = @splat(.{});
tag_cells[2] = logical[10];
const tag: pardes.Layer = .{ .kind = .column, .id = 1, .viewport = .{ .x = 0, .y = 0, .w = 2, .h = 1 }, .cols = 3, .rows = 1, .bg = .{ 11, 22, 33 }, .cells = &tag_cells, .cursor = .{ .x = 2, .y = 0, .bar = true } };
- const placed = [_]pardes.Region{.{ .kind = .rail, .owner = 3, .serial = 42, .rect = .{ .x = 0, .y = 1, .w = 2, .h = 3 }, .active = true, .thumb_y = 1, .thumb_h = 2, .clip = 1, .lift = 0.5 }};
+ const placed = [_]pardes.Region{.{ .kind = .rail, .owner = 3, .serial = 42, .rect = .{ .x = 0, .y = 1, .w = 2, .h = 3 }, .active = true, .dirty = true, .thumb_y = 1, .thumb_h = 2, .clip = 1, .lift = 0.5 }};
const out = try gpa.alloc(u8, frameBound(2, 4) + layersBound(&.{layer}, &.{tag}, &placed));
defer gpa.free(out);
- const palette: pardes.Chrome = .{ .page = .{ 1, 2, 3 }, .border = .{ 4, 5, 6 }, .tag_focus_bg = .{ 7, 8, 9 }, .scroll_thumb = .{ 10, 11, 12 } };
+ const palette: pardes.Chrome = .{ .page = .{ 1, 2, 3 }, .border = .{ 4, 5, 6 }, .tag_focus_bg = .{ 7, 8, 9 }, .scroll_thumb = .{ 10, 11, 12 }, .rule_px = 3, .tag_rule = .{ 13, 14, 15 }, .rail_px = 12, .grip_border = .{ 16, 17, 18 }, .grip_focus_ring = .{ 19, 20, 21 }, .grip_dirty = .{ 22, 23, 24 }, .lift_style = 2 };
const encoded = try encodeFrameLayers(out, 2, 4, null, .link, &cells, &cells, &.{layer}, &.{tag}, &placed, palette);
const packet = (try framed(encoded)).?;
const frame = (try decodeServer(packet.tag, packet.payload)).frame;
diff --git a/src/draw.zig b/src/draw.zig
index 2e608132..1f30a40b 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -58,7 +58,7 @@ pub fn place(p: *Pardes, s: *Surface) void {
const active = id == p.active;
const tag_y = p.tagTop(pane, r);
const body_y = p.bodyTop(pane, r);
- s.addRegion(.{ .kind = .grip, .owner = owner, .serial = pane.serial, .active = active, .rect = .{ .x = r.x, .y = tag_y, .w = config.GUTTER, .h = pane.tag_rows } });
+ s.addRegion(.{ .kind = .grip, .owner = owner, .serial = pane.serial, .active = active, .dirty = tagline.dirty(pane), .rect = .{ .x = r.x, .y = tag_y, .w = config.GUTTER, .h = pane.tag_rows } });
s.addRegion(.{ .kind = .tag, .owner = owner, .serial = pane.serial, .active = active, .rect = .{ .x = r.x + config.GUTTER, .y = tag_y, .w = r.w - config.GUTTER, .h = pane.tag_rows } });
// A folded pane is its tag rows and nothing else.
if (pane.collapsed) continue;
@@ -160,6 +160,29 @@ pub fn place(p: *Pardes, s: *Surface) void {
}
}
+/// A pane's grip in the grid: `box` focused, else halfway between its ring
+/// and `box_dim`, so it shows on its tag without the ring a pixel shell
+/// draws (gui.zig draws its button from the region and Chrome instead).
+fn gripBg(p: *const Pardes, active: bool) [3]u8 {
+ const chrome = p.chromeTheme();
+ if (active) return chrome.box;
+ return colors.mix(boxBorder(p), chrome.box_dim);
+}
+
+/// An unfocused grip's ring, and a dirty one's fill (colors.Theme).
+pub fn boxBorder(p: *const Pardes) [3]u8 {
+ return p.theme().box_border orelse p.chromeTheme().box_dim;
+}
+pub fn boxDirty(p: *const Pardes) [3]u8 {
+ const th = p.theme();
+ return th.box_dirty orelse th.diagnostic_warning orelse th.num;
+}
+
+fn inkOn(bg: [3]u8) [3]u8 {
+ const lum = (@as(u16, bg[0]) * 3 + @as(u16, bg[1]) * 6 + @as(u16, bg[2])) / 10;
+ return if (lum > 140) .{ 0x00, 0x00, 0x00 } else .{ 0xff, 0xff, 0xff };
+}
+
/// The Lift style as drawn: `auto` is a shadow on a light page, and on a
/// dark one, where a shadow has nothing to darken, no lift at all (the other
/// panes recede instead, `inactiveDim`).
@@ -366,6 +389,12 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
.sel_fg = p.theme().sel_fg,
.sel_bg = p.theme().sel_bg,
.lift_style = @intFromEnum(liftStyle(p)),
+ .rule_px = p.theme().rule_px orelse 2,
+ .grip_border = boxBorder(p),
+ .grip_focus_ring = chrome.box,
+ .grip_dirty = boxDirty(p),
+ .tag_rule = p.theme().tag_rule,
+ .rail_px = @intCast(@min(255, @as(u32, p.theme().rail_px orelse 12) * p.settings.grip_width / 100)),
};
place(p, s);
// Separators have their own role; a scrollbar need not frame the page.
@@ -666,21 +695,28 @@ fn renderPane(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r:
if (th.bg) |bg| s.fill(tx, r.y, tw, r.h, .{ .bg = .{ .rgb = bg } });
}
- const box_bg = if (active) chrome.box else chrome.box_dim;
- const box_lum = (@as(u16, box_bg[0]) * 3 + @as(u16, box_bg[1]) * 6 + @as(u16, box_bg[2])) / 10;
- const box_ink: [3]u8 = if (box_lum > 140) .{ 0x00, 0x00, 0x00 } else .{ 0xff, 0xff, 0xff };
+ // The grip is acme's button: the focused pane's filled, the others'
+ // a ring round the tag's ground, a dirty one's the dirty colour inside
+ // its ring. A pixel shell draws the ring (gui.zig gripDecor); a grid,
+ // which cannot, marks dirty in the grip's second cell with a bold `*`
+ // on the dirty colour, a cue that survives any palette.
+ const band = if (active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg;
+ const dirty = tagline.dirty(pane);
+ const box_bg = gripBg(p, active);
const box_style: CellStyle = .{
- .fg = .{ .rgb = box_ink },
+ .fg = .{ .rgb = inkOn(box_bg) },
.bg = .{ .rgb = box_bg },
// Grips keep physical grid dimensions, independently of tag text.
.font_role = .tagline,
};
- // The grip is the first row's alone, as acme's button is; the rows
- // under it are the tag band's, and still grab the pane (the region
- // spans them all, as acme's button column does).
+ // On the first row alone, as acme's button is; the rows under it are
+ // the tag band's, and still grab the pane (the region spans them all,
+ // as acme's button column does).
s.fill(r.x, tag_y, config.GUTTER, @min(1, pane.tag_rows), box_style);
+ if (dirty and pane.tag_rows > 0 and config.GUTTER > 1)
+ s.set(r.x + config.GUTTER - 1, tag_y, "*", .{ .fg = .{ .rgb = inkOn(boxDirty(p)) }, .bg = .{ .rgb = boxDirty(p) }, .bold = true, .font_role = .tagline });
s.fill(r.x, tag_y + 1, config.GUTTER, pane.tag_rows -| 1, .{
- .bg = .{ .rgb = if (active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg },
+ .bg = .{ .rgb = band },
.font_role = .tagline,
});
s.set(r.x, tag_y, switch (pane.body.mode) {
@@ -880,7 +916,7 @@ fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, layer: *pardes.Layer,
var cell = panes.File.rawDisplayCol(shown, bounds.lo) -| scroll;
const end = panes.File.rawDisplayCol(shown, bounds.hi) -| scroll;
while (cell < end and cell < columns) : (cell += 1) {
- surface.at(@intCast(cell), 0).style.bg = .{ .rgb = p.theme().sel_bg };
+ surface.at(@intCast(cell), 0).style.bg = .{ .rgb = p.theme().tag_sel_bg orelse p.theme().sel_bg };
surface.at(@intCast(cell), 0).style.fg = .{ .rgb = p.theme().sel_fg };
}
};
@@ -890,7 +926,7 @@ fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, layer: *pardes.Layer,
var col = panes.File.rawDisplayCol(shown, span.lo) -| scroll;
const end = panes.File.rawDisplayCol(shown, span.hi) -| scroll;
while (col < end and col < columns) : (col += 1) {
- surface.at(@intCast(col), 0).style.bg = .{ .rgb = p.theme().sel_bg };
+ surface.at(@intCast(col), 0).style.bg = .{ .rgb = p.theme().tag_sel_bg orelse p.theme().sel_bg };
surface.at(@intCast(col), 0).style.fg = .{ .rgb = p.theme().sel_fg };
}
}
@@ -1131,7 +1167,7 @@ pub fn paintPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pa
while (col + pane.tag_scroll < end and col < tw) : (col += 1) {
const cell = s.at(tx + @as(u16, @intCast(col)), y);
cell.default = false;
- cell.style.bg = .{ .rgb = th.sel_bg };
+ cell.style.bg = .{ .rgb = th.tag_sel_bg orelse th.sel_bg };
cell.style.fg = .{ .rgb = th.sel_fg };
}
}
@@ -1151,7 +1187,7 @@ pub fn paintPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pa
for (pane.sel, 0..) |selection, button| {
if (selection.state == .none or (selection.state == .done and button != 0)) continue;
if (button != pardes.sel_slot or @min(selection.r0, selection.r1) >= rows) continue;
- body_layer.paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, selection, 0, th.sel_bg, th.sel_fg);
+ body_layer.paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, selection, 0, th.tag_sel_bg orelse th.sel_bg, th.sel_fg);
}
if (p.look_hover_preview) |preview| {
if (preview.pane == id and preview.serial == pane.serial) if (preview.sel) |sel| if (@min(sel.r0, sel.r1) < rows) {
@@ -1160,11 +1196,71 @@ pub fn paintPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pa
};
}
const sel_btn = body_layer.buttonTints(th);
+ const sel_ink = body_layer.buttonInks(th);
for (pane.sel, 0..) |selection, button| {
if (selection.state == .none or (selection.state == .done and button != 0)) continue;
if (button == pardes.sel_slot or @min(selection.r0, selection.r1) >= rows) continue;
- body_layer.paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, selection, 0, sel_btn[button], th.sel_fg);
+ body_layer.paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, selection, 0, sel_btn[button], sel_ink[button]);
+ }
+}
+
+test "an unfocused grip shows on its tag in the grid, in every native theme" {
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 });
+ defer p.deinit();
+ _ = try p.setTestFile("text\n");
+ p.newScratchBelow(0);
+ p.animate_theme_changes = false;
+ var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
+ defer arena.deinit();
+ for (pardes.themes[0..colors.native_count], 0..) |theme, index| {
+ colors.setThemeIndex(p, index);
+ p.active = 1;
+ _ = arena.reset(.retain_capacity);
+ const s = try p.render(arena.allocator());
+ const r = p.rects[0];
+ const grip = s.at(r.x, p.tagTop(p.panes[0].?, r)).style.bg.rgb;
+ const ratio = contrast(grip, p.chromeTheme().tag_bg);
+ // ponytail: dark and helix have always had a faint grip (1.2:1); flagged, not changed here.
+ const floor: f32 = if (std.mem.eql(u8, theme.name, "dark") or std.mem.eql(u8, theme.name, "helix")) 1.2 else 1.5;
+ if (ratio < floor) std.debug.print("{s}: grip {d:.2}\n", .{ theme.name, ratio });
+ try std.testing.expect(ratio >= floor);
+ }
+}
+
+test "a grip's four states, focused or not and clean or dirty, are each its own in every native theme" {
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 });
+ defer p.deinit();
+ const pane = try p.setTestFile("text\n");
+ p.newScratchBelow(0);
+ p.animate_theme_changes = false;
+ var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
+ defer arena.deinit();
+ const State = struct { first: Cell, second: Cell, dirty: bool };
+ for (0..colors.native_count) |index| {
+ colors.setThemeIndex(p, index);
+ var states: [4]State = undefined;
+ for (&states, 0..) |*state, i| {
+ p.active = if (i & 1 == 0) 0 else 1;
+ const saved = pane.file.?.saved_revision;
+ pane.file.?.revision = if (i & 2 != 0) saved + 1 else saved;
+ _ = arena.reset(.retain_capacity);
+ const s = try p.render(arena.allocator());
+ const r = p.rects[0];
+ const y = p.tagTop(pane, r);
+ state.* = .{ .first = s.at(r.x, y).*, .second = s.at(r.x + 1, y).*, .dirty = s.region(.grip, 0).?.dirty };
+ try std.testing.expectEqual(i & 2 != 0, state.dirty);
+ if (i & 2 == 0) continue;
+ // Dirty is more than a shade: the grid prints a bold `*` (one
+ // grid for every shell; a pixel shell draws its button over it).
+ try std.testing.expectEqualStrings("*", state.second.grapheme());
+ try std.testing.expect(state.second.style.bold);
+ }
+ for (states, 0..) |a, i| for (states[i + 1 ..]) |b| {
+ const same = std.meta.eql(a.first.style.bg, b.first.style.bg) and a.second.visuallyEqual(&b.second);
+ try std.testing.expect(!same);
+ };
}
+ pane.file.?.revision = pane.file.?.saved_revision;
}
test "a tag of three rows has its grip on the first, and band under it that still grabs the pane" {
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index d8bae6dc..1a1b1f99 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -2111,7 +2111,13 @@ const Cover = packed struct(u16) {
focus: bool = false,
/// At a column's left edge, where a tag's anchor is inset.
anchor: bool = false,
- _: u7 = 0,
+ /// In a dirty pane's grip.
+ dirty: bool = false,
+ /// In a pane's grip (not a column's).
+ pane_grip: bool = false,
+ /// In a grip's first row, where its button is.
+ grip_top: bool = false,
+ _: u4 = 0,
};
/// Marks the grid cells a body or tag layer covers, which the layers paint
@@ -2143,10 +2149,13 @@ fn coverFrame(cover: []Cover, cols: u16, rows: u16, body_layers: []const pardes.
if (region.kind == .column_grip) bar = region.rect;
}
for (regions) |region| switch (region.kind) {
- .grip => {
+ .grip, .column_grip => {
mark.rect(cover, cols, rows, region.rect, "grip", true);
for (0..@min(region.rect.w, 16)) |offset| mark.rect(cover, cols, rows, .{ .x = region.rect.x +| @as(u16, @intCast(offset)), .y = region.rect.y, .w = 1, .h = region.rect.h }, "grip_offset", @as(u4, @intCast(offset)));
- if (region.active) mark.rect(cover, cols, rows, region.rect, "focus", true);
+ if (region.kind == .grip and region.active) mark.rect(cover, cols, rows, region.rect, "focus", true);
+ if (region.dirty) mark.rect(cover, cols, rows, region.rect, "dirty", true);
+ if (region.kind == .grip) mark.rect(cover, cols, rows, region.rect, "pane_grip", true);
+ mark.rect(cover, cols, rows, .{ .x = region.rect.x, .y = region.rect.y, .w = region.rect.w, .h = @min(1, region.rect.h) }, "grip_top", true);
},
.tag => if (region.active) mark.rect(cover, cols, rows, region.rect, "focus", true),
.column => {
@@ -6022,6 +6031,67 @@ fn anchorInset(g: *const Gui, row: u16, win_h: f32) u32 {
return taglineBandOffset(row, win_h, g.cell_h, g.tagline_height);
}
+/// acme's Scrollwid column at a pane's or column's left edge, in window
+/// pixels: where its button sits over its scrollbar, one width for both
+/// (`rail_px` at acme's 17px font, scaled with the tagline, and always
+/// short of the cell so the text keeps a gap), right of the column's rule.
+fn scrollColumn(g: *const Gui, chrome: *const pardes.Chrome, col: u16, layout: CellLayout) [2]f32 {
+ const rule: f32 = if (col == 0) 0 else rulePx(chrome);
+ const x0 = @round(@as(f32, @floatFromInt(col)) * layout.w + rule);
+ const scaled: f32 = @round(@as(f32, @floatFromInt(@max(1, chrome.rail_px))) * @as(f32, @floatFromInt(g.tagline_height)) / 17);
+ // The rail and the grip are GUTTER cells wide; the text starts after.
+ const w = std.math.clamp(scaled, 4, @max(4, @floor(layout.w * @as(f32, @floatFromInt(config.GUTTER))) - rule - 3));
+ return .{ x0, x0 + w };
+}
+
+/// acme's button for the grip whose first cell is (col, row), in window
+/// pixels: the scroll column wide, its tag row tall, from the rule above to
+/// the rule below, so button and scrollbar read as one strip.
+fn gripButton(g: *const Gui, chrome: *const pardes.Chrome, col: u16, row: u16, layout: CellLayout, win_h: f32) [4]f32 {
+ _ = win_h;
+ const x = scrollColumn(g, chrome, col, layout);
+ const y0 = @round(@as(f32, @floatFromInt(row)) * layout.h);
+ return .{ x[0], y0, x[1], @round(@as(f32, @floatFromInt(row + 1)) * layout.h) };
+}
+
+/// A pane's button fill, from its region's state, not the grid's cells
+/// (which are drawn for cell shells): dirty, focused, or the tag's ground.
+fn gripFill(chrome: *const pardes.Chrome, focus: bool, dirty: bool) [3]u8 {
+ if (dirty) return chrome.grip_dirty;
+ return if (focus) chrome.grip_focus_ring else chrome.tag_bg;
+}
+
+fn inkOn(bg: [3]u8) [3]u8 {
+ const lum = (@as(u16, bg[0]) * 3 + @as(u16, bg[1]) * 6 + @as(u16, bg[2])) / 10;
+ return if (lum > 140) .{ 0x00, 0x00, 0x00 } else .{ 0xff, 0xff, 0xff };
+}
+
+/// Cuts an instance's quad to a pixel rectangle, its atlas window with it so
+/// the glyph neither moves nor stretches; nothing left, an empty quad.
+fn clipInstance(instance: *CellInstance, r: [4]f32, win_w: f32, win_h: f32) void {
+ const x0 = (instance.x0 + 1) / 2 * win_w;
+ const x1 = (instance.x1 + 1) / 2 * win_w;
+ const y0 = (1 - instance.y0) / 2 * win_h;
+ const y1 = (1 - instance.y1) / 2 * win_h;
+ const left = @max(0, r[0] - x0);
+ const right = @max(0, x1 - r[2]);
+ const top = @max(0, r[1] - y0);
+ const bottom = @max(0, y1 - r[3]);
+ if (x1 - right <= x0 + left or y1 - bottom <= y0 + top) {
+ instance.x1 = instance.x0;
+ instance.y1 = instance.y0;
+ return;
+ }
+ instance.x0 += left / win_w * 2;
+ instance.u0 += left / atlas_w;
+ instance.x1 -= right / win_w * 2;
+ instance.u1 -= right / atlas_w;
+ instance.y0 -= top / win_h * 2;
+ instance.v0 += top / atlas_h;
+ instance.y1 += bottom / win_h * 2;
+ instance.v1 -= bottom / atlas_h;
+}
+
fn emitSurfaceCell(
g: *Gui,
chrome: *const pardes.Chrome,
@@ -6054,33 +6124,82 @@ fn emitSurfaceCell(
emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .body, (&band_base)[0..1], 0, null, old_layer, false, page);
next.* += 1;
// Tag glyphs keep their font size inside a full physical-width grip.
- emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .tagline, line, at, cursor, old_layer, is_cursor, page);
+ // A pane's grip cells are the cell shells' form (a dirty `*`): the
+ // button draws its own glyph over a blank of them.
+ var blank = line[at];
+ blank.text[0] = ' ';
+ blank.len = 1;
+ if (here.pane_grip)
+ emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .tagline, (&blank)[0..1], 0, null, old_layer, false, page)
+ else
+ emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .tagline, line, at, cursor, old_layer, is_cursor, page);
// A grip's glyph is the cell at its left edge, and a blank one is
// no glyph: its marks are decor.
const glyph = &line[at - @min(here.grip_offset, at)];
- const grip = here.grip and !glyph.default and glyph.printableAscii() != ' ' and !glyph.style.invisible;
+ const grip = here.grip and here.grip_top and !glyph.default and glyph.printableAscii() != ' ' and !glyph.style.invisible;
const offset: u16 = if (grip) @min(here.grip_offset, at) else 0;
// An anchor starts at its column's rule: its left edge is inset by
// its top margin so the band frames it on the left as above and
// below, and its mark moves right by half that to stay centered.
const inset: u32 = if (line_cover[at - offset].anchor) anchorInset(g, row, win_h) else 0;
+ // A grip is acme's button: its fill is this cell's ground, cut to
+ // the button (and a dirty one's inset a pixel inside its ring, so
+ // the ring reads round it), its glyph centered in it.
+ const button = if (here.grip and here.grip_top) gripButton(g, chrome, col - @min(here.grip_offset, @as(u16, @intCast(at))), row, body_layout, win_h) else null;
if (grip) {
// A grip is one glyph centered across the gutter, not text: it is
// looked up by its codepoint's glyph, never shaped.
const drawn = glyphForCodepoint(g, cellCodepoint(glyph));
const cached = cachedGlyph(g, .{ .face = drawn.face, .glyph = drawn.glyph, .role = .tagline, .decoration = GlyphDecoration.fromCell(glyph), .centered = true });
const slot = cached orelse g.space_slot;
- const shift: f32 = if (cached != null) @as(f32, @floatFromInt(@as(u32, offset) * g.cell_w)) - @as(f32, @floatFromInt(inset / 2)) else 0;
+ const b = button.?;
+ const center = (b[0] + b[2]) / 2 - @as(f32, @floatFromInt(col - offset)) * body_layout.w;
+ const shift: f32 = if (cached != null) @as(f32, @floatFromInt(@as(u32, offset) * g.cell_w)) + @as(f32, @floatFromInt(config.GUTTER * g.cell_w)) / 2 - center else 0;
const resolved = resolveCell(g, glyph[0..1], 0, null, .tagline, false, page);
- instances[next.*].fr = @as(f32, @floatFromInt(resolved.fg[0])) / 255;
- instances[next.*].fg = @as(f32, @floatFromInt(resolved.fg[1])) / 255;
- instances[next.*].fb = @as(f32, @floatFromInt(resolved.fg[2])) / 255;
+ const ink = if (here.pane_grip) inkOn(gripFill(chrome, here.focus, here.dirty)) else resolved.fg;
+ instances[next.*].fr = @as(f32, @floatFromInt(ink[0])) / 255;
+ instances[next.*].fg = @as(f32, @floatFromInt(ink[1])) / 255;
+ instances[next.*].fb = @as(f32, @floatFromInt(ink[2])) / 255;
instances[next.*].u0 = (@as(f32, @floatFromInt(slot.u)) + shift) / atlas_w;
instances[next.*].v0 = @as(f32, @floatFromInt(slot.v)) / atlas_h;
instances[next.*].v1 = @as(f32, @floatFromInt(slot.v + g.tagline_height)) / atlas_h;
}
instances[next.*].u1 = instances[next.*].u0 + @as(f32, @floatFromInt(g.cell_w)) / atlas_w;
- if (inset > 0 and (!grip or offset == 0)) {
+ if (button) |b| {
+ // acme's button fills its tag row from the rule above to the rule
+ // below: the underlay is its fill, cut to its columns; a pane's
+ // from its region's state, not the grid's cell form (whose dirty
+ // `*` is the cell shells'), a column's its cell's own ground.
+ const base = &instances[next.* - 1];
+ const own: [3]f32 = .{ instances[next.*].br, instances[next.*].bg, instances[next.*].bb };
+ if (here.pane_grip) {
+ const fill = gripFill(chrome, here.focus, here.dirty);
+ base.br = @as(f32, @floatFromInt(fill[0])) / 255;
+ base.bg = @as(f32, @floatFromInt(fill[1])) / 255;
+ base.bb = @as(f32, @floatFromInt(fill[2])) / 255;
+ } else {
+ base.br = own[0];
+ base.bg = own[1];
+ base.bb = own[2];
+ }
+ base.effect &= ~clear_bg_bit;
+ clipInstance(base, .{ b[0], -1e9, b[2], 1e9 }, win_w, win_h);
+ // Over it, only its own glyph; the band round it and the ring
+ // are decor (gripDecor).
+ if (here.pane_grip and !grip) {
+ instances[next.*].u0 = @as(f32, @floatFromInt(g.space_slot.u)) / atlas_w;
+ instances[next.*].u1 = instances[next.*].u0 + @as(f32, @floatFromInt(g.cell_w)) / atlas_w;
+ instances[next.*].v0 = @as(f32, @floatFromInt(g.space_slot.v)) / atlas_h;
+ instances[next.*].v1 = @as(f32, @floatFromInt(g.space_slot.v + g.tagline_height)) / atlas_h;
+ }
+ // Its ground is the fill again (a see-through ground would be
+ // written, not blended, in an opaque window).
+ instances[next.*].br = base.br;
+ instances[next.*].bg = base.bg;
+ instances[next.*].bb = base.bb;
+ instances[next.*].effect &= ~clear_bg_bit;
+ clipInstance(&instances[next.*], b, win_w, win_h);
+ } else if (inset > 0) {
instances[next.*].x0 += @as(f32, @floatFromInt(inset)) / win_w * 2;
instances[next.*].u0 += @as(f32, @floatFromInt(inset)) / atlas_w;
}
@@ -6091,6 +6210,102 @@ fn emitSurfaceCell(
next.* += 1;
}
+test "every pixel of a grip column is band, button or ring, and the button sits over the scrollbar" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 60, .rows = 20 });
+ defer core.deinit();
+ const pane = try core.setTestFile("text\n" ** 60);
+ pane.tag.own = try gpa.dupe(u8, "second\nthird");
+ core.newScratchBelow(0);
+ for ([_]struct { w: u32, h: u32, tag: u32 }{ .{ .w = 10, .h = 20, .tag = 12 }, .{ .w = 17, .h = 35, .tag = 30 }, .{ .w = 9, .h = 19, .tag = 15 }, .{ .w = 12, .h = 24, .tag = 24 } }) |m| for ([_]bool{ false, true }) |bottom| for ([_]usize{ 0, 1 }) |focus| for ([_]bool{ false, true }) |dirty| {
+ core.settings.tag_bottom = bottom;
+ core.active = focus;
+ pane.file.?.revision = pane.file.?.saved_revision + @intFromBool(dirty);
+ pardes.test_api.sync(core);
+ const s = try core.render(core.scratch.allocator());
+ var g: Gui = undefined;
+ g.words = .{};
+ g.decor = .empty;
+ defer g.decor.deinit(gpa);
+ g.cell_w = m.w;
+ g.cell_h = m.h;
+ g.tagline_width = m.w;
+ g.tagline_height = m.tag;
+ g.space_slot = .{ .u = 0, .v = 0 };
+ const layout = fixedCellLayout(&g);
+ const win_w: f32 = @floatFromInt(60 * m.w);
+ const win_h: f32 = @floatFromInt(20 * m.h);
+ const cover = try gpa.alloc(Cover, @as(usize, s.cols) * s.rows);
+ defer gpa.free(cover);
+ coverFrame(cover, s.cols, s.rows, s.bodyLayers(), s.tagLayers(), s.regionList());
+ var groups = makeGroups(&.{}, false);
+ try buildDecor(&g, gpa, s, &groups, layout, win_w, win_h, true);
+ const grip = s.region(.grip, 0).?;
+ // The grip column's pixels, band first, then its cells, then decor.
+ const x_px: u32 = grip.rect.x * m.w;
+ const w_px: u32 = config.GUTTER * m.w;
+ const y_px: u32 = grip.rect.y * m.h;
+ const h_px: u32 = grip.rect.h * m.h;
+ const pixels = try gpa.alloc(?[3]f32, w_px * h_px);
+ defer gpa.free(pixels);
+ @memset(pixels, null);
+ const paint = struct {
+ fn quad(into: []?[3]f32, x0n: f32, y0n: f32, x1n: f32, y1n: f32, rgb: [3]f32, ww: f32, wh: f32, ox: u32, oy: u32, pw: u32, ph: u32) void {
+ const px0: i64 = @intFromFloat(@round((x0n + 1) / 2 * ww));
+ const px1: i64 = @intFromFloat(@round((x1n + 1) / 2 * ww));
+ const py0: i64 = @intFromFloat(@round((1 - y0n) / 2 * wh));
+ const py1: i64 = @intFromFloat(@round((1 - y1n) / 2 * wh));
+ var y = @max(py0, oy);
+ while (y < @min(py1, oy + ph)) : (y += 1) {
+ var x = @max(px0, ox);
+ while (x < @min(px1, ox + pw)) : (x += 1) into[@intCast((y - oy) * pw + (x - ox))] = rgb;
+ }
+ }
+ };
+ var instances: [4]CellInstance = undefined;
+ for (0..grip.rect.h) |line| for (0..config.GUTTER) |dx| {
+ const col: u16 = grip.rect.x + @as(u16, @intCast(dx));
+ const row: u16 = grip.rect.y + @as(u16, @intCast(line));
+ const at = @as(usize, row) * s.cols;
+ var next: u32 = 0;
+ emitSurfaceCell(&g, &s.chrome, &instances, &next, col, row, layout, win_w, win_h, null, s.cells[at..][0..s.cols], cover[at..][0..s.cols], col, null, false, false, Ground.opaqueRgb(bg_default));
+ for (instances[0..next]) |i| paint.quad(pixels, i.x0, i.y0, i.x1, i.y1, .{ i.br, i.bg, i.bb }, win_w, win_h, x_px, y_px, w_px, h_px);
+ };
+ for (g.decor.items) |i| if (i.fr >= 1 and i.clip_x1 < i.clip_x0) paint.quad(pixels, i.x0, i.y0, i.x1, i.y1, .{ i.br, i.bg, i.bb }, win_w, win_h, x_px, y_px, w_px, h_px);
+ const rgb = struct {
+ fn of(v: [3]u8) [3]f32 {
+ return .{ @as(f32, @floatFromInt(v[0])) / 255, @as(f32, @floatFromInt(v[1])) / 255, @as(f32, @floatFromInt(v[2])) / 255 };
+ }
+ }.of;
+ const chrome = &s.chrome;
+ const band = if (focus == 0) chrome.tag_focus_bg else chrome.tag_bg;
+ // Band, fills, rings, the rules between stacked panes and under the
+ // tag, and the mode marks' ink (the grip's first cell's).
+ const ink = inkOn(gripFill(chrome, grip.active, grip.dirty));
+ const allowed = [_][3]f32{ rgb(band), rgb(core.chromeTheme().box), rgb(pardes.draw.boxDirty(core)), rgb(chrome.grip_border), rgb(chrome.grip_focus_ring), rgb(chrome.border), rgb(ink), rgb(chrome.tag_rule orelse mixRgb(chrome.tag_bg, chrome.page orelse bg_default)) };
+ for (pixels, 0..) |pixel, n| {
+ const got = pixel orelse return error.TestUnpaintedGripPixel;
+ const known = for (allowed) |a| {
+ if (std.meta.eql(a, got)) break true;
+ } else false;
+ // Under the first row there is no button: band and rules only.
+ if (n / w_px >= m.h and n / w_px < h_px - 1 and !std.meta.eql(got, rgb(band)) and !std.meta.eql(got, rgb(chrome.border)))
+ return error.TestButtonUnderFirstRow;
+ if (!known) std.debug.print("sliver {any} at {d},{d} of {d}x{d} (metrics {d}x{d}/{d}, bottom {}, focus {d}, dirty {}) page {any}\n", .{ got, n % w_px, n / w_px, w_px, h_px, m.w, m.h, m.tag, bottom, focus, dirty, s.chrome.page });
+ try std.testing.expect(known);
+ }
+ // The rule under the tag runs unbroken across the grip column.
+ if (!bottom) for (pixels[(h_px - 1) * w_px ..][0..w_px]) |pixel|
+ try std.testing.expectEqual(rgb(chrome.tag_rule orelse mixRgb(chrome.tag_bg, chrome.page orelse bg_default)), pixel.?);
+ // The button's edges are the scrollbar's.
+ const button = gripButton(&g, chrome, grip.rect.x, grip.rect.y, layout, win_h);
+ const column = scrollColumn(&g, chrome, s.region(.rail, 0).?.rect.x, layout);
+ try std.testing.expectEqual(column[0], button[0]);
+ try std.testing.expectEqual(column[1], button[2]);
+ };
+ pane.file.?.revision = pane.file.?.saved_revision;
+}
+
test "small tagline fonts leave no background gutters between adjacent tags" {
const chrome: pardes.Chrome = .{};
var g: Gui = undefined;
@@ -6778,9 +6993,9 @@ test "pane indicator halves share dissolve changes and safe exhausted atlas" {
try std.testing.expect(gripCellChanged(&surface, &cover, &previous_cover, 1, 0));
try std.testing.expect(!gripCellChanged(&surface, &cover, &previous_cover, 2, 0));
try std.testing.expect(!cover[2].grip);
- // A column's grip has no glyph of a pane's.
+ // A column's grip is a button too, cut the same way (it has no glyph).
coverFrame(&cover, 6, 2, &.{}, &.{}, &.{.{ .kind = .column_grip, .rect = .{ .x = 0, .y = 0, .w = 2, .h = 1 } }});
- try std.testing.expect(!cover[0].grip);
+ try std.testing.expect(cover[0].grip);
coverFrame(&cover, 6, 2, &.{}, &.{}, &grip);
const chrome: pardes.Chrome = .{};
@@ -7714,6 +7929,12 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
for (groups.items[0..groups.len], 0..) |*group, index| {
group.decor_start = @intCast(g.decor.items.len);
defer group.decor_count = @as(u32, @intCast(g.decor.items.len)) - group.decor_start;
+ // The grips' buttons before any rule: the rules over their rows cut
+ // them, and the band round them never covers a rule.
+ if (index <= groups.tracks) {
+ const closing = if (group.track) |active| active.phase == .closing else false;
+ try gripDecor(g, gpa, if (closing) surface.previous_regions else regions, if (closing) surface.previous_cells else surface.cells, groups, index, chrome, layout, surface.cols, surface.rows, win_w, win_h, group.track);
+ }
if (index == 0) {
const top = for (regions) |region| {
if (region.kind == .workspace_tag) break region.rect.h;
@@ -7723,7 +7944,8 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
.grip, .tag, .column_grip, .column_tag => if (region.rect.y == top) break true,
else => {},
} else false;
- const height = topbarPaneBorderPixels(g.cell_h, g.tagline_height);
+ // As wide as the other rules, where the tagline leaves the room.
+ const height: u32 = @max(1, @min(chrome.rule_px, (g.cell_h -| g.tagline_height) * 2));
if (top != 0 and top < surface.rows and tag_under and height != 0) {
const y0: f32 = @floatFromInt(top * g.cell_h - height / 2);
try addDecor(g, gpa, 0, y0, win_w, y0 + @as(f32, @floatFromInt(height)), chrome.topbar_rule, 1, null, win_w, win_h);
@@ -7732,7 +7954,7 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
for (regions) |bar| {
if (bar.kind != .column_grip) continue;
const y = @as(f32, @floatFromInt(bar.rect.y + bar.rect.h)) * layout.h;
- try addDecor(g, gpa, 0, y - 1, win_w, y, chrome.border, 1, null, win_w, win_h);
+ try addDecor(g, gpa, 0, y - rulePx(chrome), win_w, y, chrome.border, 1, null, win_w, win_h);
break;
}
// A tag on the last row runs on through the pixels under the grid.
@@ -7756,7 +7978,6 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
nbodies += 1;
};
const bodies = body_buf[0..nbodies];
- const grid = if (closing) surface.previous_cells else surface.cells;
try paneDecor(g, gpa, placed, groups, index, chrome, layout, surface.cols, surface.rows, win_w, win_h, track);
// A single spine runs down each column, through every tag and body.
if (index == 0) for (regions) |column| {
@@ -7765,9 +7986,10 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
const y0 = @as(f32, @floatFromInt(column.rect.y)) * layout.h;
const grid_y1 = @as(f32, @floatFromInt(surface.rows)) * layout.h;
const y1 = if (win_h - grid_y1 < layout.h) @max(grid_y1, win_h) else grid_y1;
- if (y1 > y0) try addDecor(g, gpa, x, y0, x + 1, y1, chrome.border, 1, null, win_w, win_h);
+ // None at the window's edge: nothing to rule apart there.
+ if (column.rect.x == 0) continue;
+ if (y1 > y0) try addDecor(g, gpa, x, y0, x + rulePx(chrome), y1, chrome.border, 1, null, win_w, win_h);
};
- try gripDecor(g, gpa, placed, grid, groups, index, layout, surface.cols, surface.rows, win_w, win_h, track);
// Cast last in the group, over its cells, images and chrome: a
// lifted pane's shadow darkens its neighbours' rails and rules
// as it darkens their text, and never the pane itself.
@@ -7837,47 +8059,210 @@ fn paneDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, gr
const rule_x1 = if (win_w - x1 < layout.w) @max(x1, win_w) else x1;
const tag_bottom = tag.rect.y > rail.rect.y;
const rule_y = @as(f32, @floatFromInt(if (tag_bottom) tag.rect.y else tag.rect.y + tag.rect.h)) * layout.h;
- try addDecor(g, gpa, x0, if (tag_bottom) rule_y else rule_y - 1, rule_x1, if (tag_bottom) rule_y + 1 else rule_y, chrome.border, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x0, if (tag_bottom) rule_y else rule_y - 1, rule_x1, if (tag_bottom) rule_y + 1 else rule_y, chrome.tag_rule orelse mixRgb(chrome.tag_bg, page), 1, track, win_w, win_h);
+ // Stacked panes are ruled apart as columns are, where one pane's
+ // body meets the next one's tag (acme's Border).
+ {
+ const pane_top = @min(tag.rect.y, rail.rect.y);
+ const stacked = for (regions) |other| {
+ if ((other.kind == .body or other.kind == .tag) and other.owner != rail.owner and other.rect.y + other.rect.h == pane_top and
+ other.rect.x < tag.rect.x + tag.rect.w and tag.rect.x < other.rect.x + other.rect.w) break true;
+ } else false;
+ // Inside the tag band's slack, never over its glyphs: the top
+ // slack of a tag on top, the bottom slack of the one above when
+ // tags are at the bottom; a pixel at least.
+ const y = @as(f32, @floatFromInt(pane_top)) * layout.h;
+ const spare = g.cell_h -| g.tagline_height;
+ if (stacked and !tag_bottom) {
+ const room = taglineBandOffset(pane_top, win_h, g.cell_h, g.tagline_height);
+ try addDecor(g, gpa, x0, y, rule_x1, y + @as(f32, @floatFromInt(@max(1, @min(chrome.rule_px, room)))), chrome.border, 1, track, win_w, win_h);
+ } else if (stacked and pane_top > 0) {
+ const room = spare - taglineBandOffset(pane_top - 1, win_h, g.cell_h, g.tagline_height);
+ try addDecor(g, gpa, x0, y - @as(f32, @floatFromInt(@max(1, @min(chrome.rule_px, room)))), rule_x1, y, chrome.border, 1, track, win_w, win_h);
+ }
+ }
const y0 = @as(f32, @floatFromInt(rail.rect.y)) * layout.h;
const body_end = rail.rect.y + rail.rect.h;
const body_y1 = @as(f32, @floatFromInt(body_end)) * layout.h;
const y1 = if (body_end == rows and win_h - body_y1 < layout.h) @max(body_y1, win_h) else body_y1;
+ // A theme's own track fills its width, the thumb one pixel short
+ // of it so the track edges it (acme's scrollbar); else a thin rail.
+ // acme's scrollbar: the track fills the scroll column, under the
+ // grip's button; the thumb is a pixel short of its right edge, so
+ // the track edges it.
+ const column = scrollColumn(g, chrome, rail.rect.x, layout);
try addDecor(g, gpa, x0, y0, x0 + layout.w, y1, page, 1, track, win_w, win_h);
- try addDecor(g, gpa, x0, y0, x0 + @min(layout.w, 2), y1, chrome.scroll_track, 1, track, win_w, win_h);
+ try addDecor(g, gpa, column[0], y0, column[1], y1, chrome.scroll_track, 1, track, win_w, win_h);
if (rail.thumb_h != 0) {
const thumb_y0 = @as(f32, @floatFromInt(rail.rect.y + rail.thumb_y)) * layout.h;
const thumb_y1 = if (rail.thumb_y + rail.thumb_h == rail.rect.h) y1 else @as(f32, @floatFromInt(rail.rect.y + rail.thumb_y + rail.thumb_h)) * layout.h;
- try addDecor(g, gpa, x0, thumb_y0, x0 + @min(layout.w, @max(2, @floor(layout.w * 0.5))), thumb_y1, chrome.scroll_thumb, 1, track, win_w, win_h);
+ try addDecor(g, gpa, column[0], thumb_y0, column[1] - 1, thumb_y1, chrome.scroll_thumb, 1, track, win_w, win_h);
}
}
}
+/// Halfway between two colours.
+fn mixRgb(a: [3]u8, b: [3]u8) [3]u8 {
+ var out: [3]u8 = undefined;
+ for (&out, a, b) |*o, x, y| o.* = @intCast((@as(u16, x) + y) / 2);
+ return out;
+}
+
+/// A rule's width: the theme's own, or one pixel.
+fn rulePx(chrome: *const pardes.Chrome) f32 {
+ return @floatFromInt(@max(1, chrome.rule_px));
+}
+
/// The two short bars on each row of a grip with no mode glyph there,
/// centered in its anchor. Only the panes of group `index`.
-fn gripDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, cells: []const pardes.Cell, groups: *const Groups, index: usize, layout: CellLayout, cols: u16, rows: u16, win_w: f32, win_h: f32, track: ?pardes.animation.Track) !void {
+fn gripDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, cells: []const pardes.Cell, groups: *const Groups, index: usize, chrome: *const pardes.Chrome, layout: CellLayout, cols: u16, rows: u16, win_w: f32, win_h: f32, track: ?pardes.animation.Track) !void {
if (g.tagline_height < 6 or g.tagline_width < 3) return;
for (regions) |grip| {
- if (grip.kind != .grip or groupOf(groups, grip.serial) != index) continue;
+ if ((grip.kind != .grip and grip.kind != .column_grip) or groupOf(groups, grip.serial) != index) continue;
+ if (grip.rect.h == 0 or grip.rect.y >= rows) continue;
+ const button = gripButton(g, chrome, grip.rect.x, grip.rect.y, layout, win_h);
+ // Round the button, its first row is the tag's band: nothing of the
+ // cells under it shows there.
+ const band = if (grip.active) chrome.tag_focus_bg else chrome.tag_bg;
+ const left = @as(f32, @floatFromInt(grip.rect.x)) * layout.w;
+ const right = @as(f32, @floatFromInt(grip.rect.x + grip.rect.w)) * layout.w;
+ try addDecor(g, gpa, left, button[1], button[0], button[3], band, 1, track, win_w, win_h);
+ try addDecor(g, gpa, button[2], button[1], right, button[3], band, 1, track, win_w, win_h);
+ if (grip.kind != .grip) continue;
+ // acme's button ring, two pixels (ButtonBorder) inside the button:
+ // the focused one's in its own fill. An unfocused dirty one keeps a
+ // pixel of band inside its ring, so the four states differ in shape
+ // whatever a theme's colours (a focused dirty one is acme's
+ // modbutton, filled up to its ring).
+ {
+ // The rules drawn over the row (paneDecor, after this) cut into
+ // it: the ring runs inside them, from rule to rule.
+ const tag = for (regions) |part| {
+ if (part.kind == .tag and part.owner == grip.owner) break part;
+ } else grip;
+ const tag_bottom = for (regions) |part| {
+ if (part.kind == .rail and part.owner == grip.owner) break part.rect.y < tag.rect.y;
+ } else false;
+ const stacked = !tag_bottom and for (regions) |other| {
+ if ((other.kind == .body or other.kind == .tag) and other.owner != grip.owner and other.rect.y + other.rect.h == grip.rect.y and
+ other.rect.x < tag.rect.x + tag.rect.w and tag.rect.x < other.rect.x + other.rect.w) break true;
+ } else false;
+ const top_rule: f32 = if (tag_bottom) 1 else if (stacked) @floatFromInt(@max(1, @min(chrome.rule_px, taglineBandOffset(grip.rect.y, win_h, g.cell_h, g.tagline_height)))) else 0;
+ const bottom_rule: f32 = if (!tag_bottom and grip.rect.h == 1) 1 else 0;
+ const x0 = button[0];
+ const x1 = button[2];
+ const y0 = button[1] + top_rule;
+ const y1 = button[3] - bottom_rule;
+ const ring = if (grip.active) chrome.grip_focus_ring else chrome.grip_border;
+ try addDecor(g, gpa, x0, y0, x1, y0 + 2, ring, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x0, y1 - 2, x1, y1, ring, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x0, y0 + 2, x0 + 2, y1 - 2, ring, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x1 - 2, y0 + 2, x1, y1 - 2, ring, 1, track, win_w, win_h);
+ if (grip.dirty and !grip.active) {
+ try addDecor(g, gpa, x0 + 2, y0 + 2, x1 - 2, y0 + 3, band, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x0 + 2, y1 - 3, x1 - 2, y1 - 2, band, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x0 + 2, y0 + 3, x0 + 3, y1 - 3, band, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x1 - 3, y0 + 3, x1 - 2, y1 - 3, band, 1, track, win_w, win_h);
+ }
+ }
const x = grip.rect.x;
for (0..grip.rect.h) |line| {
const y = grip.rect.y + @as(u16, @intCast(line));
if (x >= cols or y >= rows) continue;
const cell = &cells[@as(usize, y) * cols + x];
if (cell.default or cell.printableAscii() != ' ') continue;
- const rgb = switch (cell.style.fg) {
- .rgb => |rgb| rgb,
- else => continue,
- };
- const center_x = (@as(f32, @floatFromInt(x)) + @as(f32, @floatFromInt(config.GUTTER)) / 2) * layout.w + @as(f32, @floatFromInt(anchorInset(g, y, win_h) / 2));
+ // The marks are in the button's ink, from the region's state.
+ const rgb = inkOn(gripFill(chrome, grip.active, grip.dirty));
+ // The marks are the first row's alone, centered in the button.
+ if (line != 0) continue;
+ const center_x = @floor((button[0] + button[2]) / 2);
const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, win_h, g.cell_h, g.tagline_height))) + @as(f32, @floatFromInt(g.tagline_height)) / 2;
- const half_w = @max(1, @min(3, layout.w - 2));
+ const half_w = @max(1, @min(3, @floor((button[2] - button[0] - 6) / 2)));
try addDecor(g, gpa, @floor(center_x - half_w), @floor(center_y - 2), @floor(center_x + half_w), @floor(center_y - 1), rgb, 1, track, win_w, win_h);
try addDecor(g, gpa, @floor(center_x - half_w), @floor(center_y + 1), @floor(center_x + half_w), @floor(center_y + 2), rgb, 1, track, win_w, win_h);
}
}
}
+test "a theme's own rules and scrollbar are drawn at its widths: acme's 2px black rules and 12px track" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
+ defer core.deinit();
+ _ = try core.newShell(1, "");
+ try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false));
+ try std.testing.expect(core.executeBuiltinLine(0, "Theme acme"));
+ for (0..40) |_| core.update(.tick);
+ pardes.test_api.sync(core);
+ const s = try core.render(core.scratch.allocator());
+ try std.testing.expectEqual(@as(u8, 2), s.chrome.rule_px);
+ // acme's 12px, at GripWidth's 150% default.
+ try std.testing.expectEqual(@as(u8, 18), s.chrome.rail_px);
+ var g: Gui = undefined;
+ g.decor = .empty;
+ defer g.decor.deinit(gpa);
+ g.cell_w = 16;
+ g.cell_h = 20;
+ g.tagline_width = 10;
+ g.tagline_height = 12;
+ var groups = makeGroups(&.{}, false);
+ try buildDecor(&g, gpa, s, &groups, fixedCellLayout(&g), 1280, 480, true);
+ const px = struct {
+ fn w(item: CellInstance) f32 {
+ return (item.x1 - item.x0) / 2 * 1280;
+ }
+ };
+ var spines: usize = 0;
+ var tracks: usize = 0;
+ for (g.decor.items) |item| {
+ const black = item.br == 0 and item.bg == 0 and item.bb == 0;
+ // No rule at the window's left edge; one 2px spine where columns meet.
+ if (black and @abs(px.w(item) - 2) < 0.01 and item.y1 < -0.9) {
+ try std.testing.expect(item.x0 > -1);
+ spines += 1;
+ }
+ const column = scrollColumn(&g, &s.chrome, 40, fixedCellLayout(&g));
+ if (@abs(item.br - @as(f32, 0x99) / 255) < 1e-3 and @abs(item.bb - @as(f32, 0x4c) / 255) < 1e-3 and @abs(px.w(item) - (column[1] - column[0])) < 0.01) tracks += 1;
+ }
+ try std.testing.expectEqual(@as(usize, 1), spines);
+ try std.testing.expect(tracks >= 2);
+}
+
+test "the rule between stacked panes stays in the tag band's slack, at any tagline size" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
+ defer core.deinit();
+ _ = try core.newShell(1, "");
+ core.newScratchBelow(0);
+ pardes.test_api.sync(core);
+ const s = try core.render(core.scratch.allocator());
+ const lower = for (s.regionList()) |region| {
+ if (region.kind == .tag and region.rect.y > 2) break region;
+ } else return error.TestExpectedStackedTag;
+ for ([_]u32{ 12, 20 }) |tagline_h| {
+ var g: Gui = undefined;
+ g.decor = .empty;
+ defer g.decor.deinit(gpa);
+ g.cell_w = 10;
+ g.cell_h = 20;
+ g.tagline_width = 6;
+ g.tagline_height = tagline_h;
+ var groups = makeGroups(&.{}, false);
+ try buildDecor(&g, gpa, s, &groups, fixedCellLayout(&g), 800, 480, true);
+ const top = @as(f32, @floatFromInt(lower.rect.y)) * 20;
+ const slack: f32 = @floatFromInt(@max(1, pardes.taglineBandOffset(lower.rect.y, 480, 20, tagline_h)));
+ var found = false;
+ for (g.decor.items) |item| {
+ const y0 = (1 - item.y0) / 2 * 480;
+ const y1 = (1 - item.y1) / 2 * 480;
+ if (@abs(y0 - top) > 0.01 or @abs(item.br - @as(f32, @floatFromInt(s.chrome.border[0])) / 255) > 1e-3) continue;
+ found = true;
+ try std.testing.expect(y1 - y0 <= slack + 0.01);
+ }
+ try std.testing.expect(found);
+ }
+}
+
test "decor is read from the regions, each pane's in its own group" {
const gpa = std.testing.allocator;
const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
@@ -7905,15 +8290,23 @@ test "decor is read from the regions, each pane's in its own group" {
// workspace tag; a rail two pixels wide at each pane's.
for (core.col_x[0..core.ncol]) |x| {
const left = ndc.x(@as(f32, @floatFromInt(x)) * 10);
+ // Two pixels (rule_px), and none at the window's own edge.
const spine = for (g.decor.items) |item| {
- if (item.x0 == left and @abs(item.x1 - ndc.x(@as(f32, @floatFromInt(x)) * 10 + 1)) < 0.0001) break item;
- } else return error.TestExpectedSpine;
- try std.testing.expectApproxEqAbs(@as(f32, 1 - 20.0 / 480.0 * 2), spine.y0, 0.0001);
- try std.testing.expectEqual(@as(f32, -1), spine.y1);
+ if (item.x0 == left and @abs(item.x1 - ndc.x(@as(f32, @floatFromInt(x)) * 10 + 2)) < 0.0001 and
+ @abs(item.y0 - (1 - 20.0 / 480.0 * 2)) < 0.0001 and std.mem.eql(f32, &.{ item.br, item.bg, item.bb }, &.{ @as(f32, @floatFromInt(s.chrome.border[0])) / 255, @as(f32, @floatFromInt(s.chrome.border[1])) / 255, @as(f32, @floatFromInt(s.chrome.border[2])) / 255 })) break item;
+ } else null;
+ try std.testing.expectEqual(x == 0, spine == null);
+ if (spine) |rule| {
+ try std.testing.expectApproxEqAbs(@as(f32, 1 - 20.0 / 480.0 * 2), rule.y0, 0.0001);
+ try std.testing.expectEqual(@as(f32, -1), rule.y1);
+ }
+ // The scrollbar fills the scroll column right of the rule.
+ const column = scrollColumn(&g, &s.chrome, x, layout);
const rail = for (g.decor.items) |item| {
- if (item.x0 == left and @abs(item.x1 - ndc.x(@as(f32, @floatFromInt(x)) * 10 + 2)) < 0.0001) break item;
+ if (@abs(item.x0 - ndc.x(column[0])) < 0.0001 and @abs(item.x1 - ndc.x(column[1])) < 0.0001 and
+ @abs(item.br - @as(f32, @floatFromInt(s.chrome.scroll_track[0])) / 255) < 0.0001) break item;
} else return error.TestExpectedRail;
- try std.testing.expectApproxEqAbs(@as(f32, s.chrome.scroll_track[0]) / 255, rail.br, 0.0001);
+ _ = rail;
}
try std.testing.expectEqual(@as(u32, 0), groups.items[groups.notices].decor_count);
const still = groups.items[0].decor_count;
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 1c21d314..b28e02fa 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -308,6 +308,7 @@ fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void {
.lift => try w.print("{s} {s}\n", .{ word, @tagName(s.lift) }),
.motion => try w.print("{s} {s}\n", .{ word, @tagName(s.motion) }),
.inactive_dim => try w.print("{s} {d}\n", .{ word, s.inactive_dim }),
+ .grip_width => try w.print("{s} {d}\n", .{ word, s.grip_width }),
}
}
try w.writeAll("LocationsConfig ");
diff --git a/src/surface.zig b/src/surface.zig
index f16993a8..bc0a019d 100644
--- a/src/surface.zig
+++ b/src/surface.zig
@@ -356,6 +356,8 @@ pub const Region = struct {
rect: layout.Rect,
/// The pane or column has the keyboard's focus.
active: bool = false,
+ /// A grip's pane has changes it has not saved.
+ dirty: bool = false,
/// A rail's thumb: rows from the rail's top, and how many. Zero rows is
/// a rail with no thumb (an image). A native PDF's thumb is measured in
/// document pixels and filled in by Pdf.draw while it paints, so read
@@ -403,6 +405,19 @@ pub const Chrome = struct {
sel_bg: [3]u8 = .{ 0, 0, 0 },
/// How a lifted part stands off the page (config.Runtime.LiftStyle).
lift_style: u8 = 0,
+ /// Rule width in pixels (colors.Theme.rule_px).
+ rule_px: u8 = 2,
+ /// The ring round a grip, acme's button: unfocused, and focused.
+ grip_border: [3]u8 = .{ 0, 0, 0 },
+ grip_focus_ring: [3]u8 = .{ 0, 0, 0 },
+ /// A dirty pane's button fill.
+ grip_dirty: [3]u8 = .{ 0, 0, 0 },
+ /// The rule between a pane's tag and its body; null is halfway between
+ /// the tag's ground and the page the shell actually draws.
+ tag_rule: ?[3]u8 = null,
+ /// The scroll column's width, button and scrollbar alike, in pixels at
+ /// a 17px tagline (acme's 12 at its font); a pixel shell scales it.
+ rail_px: u8 = 12,
};
/// The canonical interface: what a frame of pardes IS. The tty shell writes
diff --git a/src/tagline.zig b/src/tagline.zig
index 59c51b4d..d0907cbd 100644
--- a/src/tagline.zig
+++ b/src/tagline.zig
@@ -24,6 +24,12 @@ pub const locations_pane_tail = file_pane_tail ++ " LocationsConfig";
pub const terminal_pane_tail = " " ++ config.terminal_pane_builtins_str;
pub const command_pane_tail = " " ++ config.command_pane_builtins_str;
const dirty_marker = " *";
+
+/// A file with changes it has not saved.
+pub fn dirty(pane: *const panes.Pane) bool {
+ const f = pane.file orelse return false;
+ return panes.Output.fileTraits(f.output).saves and f.revision != f.saved_revision;
+}
pub const legacy_pane_tail = " Del";
pub const legacy_file_pane_tail = " Save Del";
// The defaults from the release before Newtty joined every tagline. Recognized
@@ -136,7 +142,7 @@ pub fn pathPrefix(arena: std.mem.Allocator, pane: *Pane) ![]const u8 {
}
if (pane.file) |f| {
const name = if (pane.prompt == .name) pane.input.own orelse f.path else f.path;
- if (panes.Output.fileTraits(f.output).saves and f.revision != f.saved_revision)
+ if (dirty(pane))
return std.fmt.allocPrint(arena, "{s}{s}", .{ name, dirty_marker });
return name;
}
diff --git a/src/themes/acme.zig b/src/themes/acme.zig
index d91e626b..17ef4224 100644
--- a/src/themes/acme.zig
+++ b/src/themes/acme.zig
@@ -6,20 +6,28 @@
//! in) is a mid grey, and 15 (bright white) is BLACK, because a program asking
//! for the brightest color on a light page means "make this stand out".
//!
-//! The selection is acme's own: plan9's DDarkyellow #eeee9e, the colour acme
-//! sweeps text with, and black ink over it — acme changes the paper under a
-//! selection and never the letters. Out of the same draw.h palette the
-//! scrollbar's #99994c (DYellowgreen) is already taken from.
+//! Every colour below that acme has is acme's own, from plan9port's acme.c
+//! iconinit and draw.h: the tag #eaffff and body #ffffea (each 25% of
+//! DPalebluegreen or DPaleyellow over white), their selections #9eeeee and
+//! #eeee9e with black ink (acme changes the paper under a selection, never
+//! the letters), the exec and look sweeps #aa0000 and #006600 with white ink,
+//! the scrollbar's #99994c track under a #ffffea thumb, 12px wide, the
+//! #8888cc (DPurpleblue) line between a tag and its body and the column
+//! button, and 2px black rules between columns, windows and tags.
+//!
+//! acme has no focus colour; pardes needs one, so the focused pane's button
+//! is filled #8888cc (the column button's look) and every other pane's is
+//! acme's own: #eaffff in a 2px #8888cc ring, #000099 inside it when dirty.
pub const theme = .{
.name = "acme",
.bg = .{ 0xff, 0xff, 0xea },
.fg = .{ 0x00, 0x00, 0x00 },
- .sel_bg = .{ 0xfa, 0xfa, 0x92 },
+ .sel_bg = .{ 0xee, 0xee, 0x9e },
.sel_fg = .{ 0x00, 0x00, 0x00 },
.tag_bg = .{ 0xea, 0xff, 0xff },
.tag_fg = .{ 0x00, 0x00, 0x00 },
- .box = .{ 0x66, 0x3f, 0xe0 },
- .box_dim = .{ 0x35, 0x3a, 0x58 },
+ .box = .{ 0x88, 0x88, 0xcc },
+ .box_dim = .{ 0xea, 0xff, 0xff },
.kw = .{ 0x97, 0x0d, 0xae },
.str = .{ 0x00, 0x77, 0x33 },
.num = .{ 0xa0, 0x52, 0x00 },
@@ -27,6 +35,17 @@ pub const theme = .{
.lineno = .{ 0x99, 0x99, 0x80 },
.scroll_track = .{ 0x99, 0x99, 0x4c },
.scroll_thumb = .{ 0xff, 0xff, 0xea },
+ .border = .{ 0x00, 0x00, 0x00 },
+ .tag_rule = .{ 0x88, 0x88, 0xcc },
+ .rule_px = 2,
+ .box_border = .{ 0x88, 0x88, 0xcc },
+ .box_dirty = .{ 0x00, 0x00, 0x99 },
+ .rail_px = 12,
+ .tag_sel_bg = .{ 0x9e, 0xee, 0xee },
+ .sweep_bg = .{ .{ 0xee, 0xee, 0x9e }, .{ 0xaa, 0x00, 0x00 }, .{ 0x00, 0x66, 0x00 } },
+ .sweep_fg = .{ .{ 0x00, 0x00, 0x00 }, .{ 0xff, 0xff, 0xff }, .{ 0xff, 0xff, 0xff } },
+ .column_box = .{ 0x88, 0x88, 0xcc },
+ .column_box_dim = .{ 0x88, 0x88, 0xcc },
.palette = .{
.{ 0x00, 0x00, 0x00 }, .{ 0xaa, 0x22, 0x22 }, .{ 0x22, 0x80, 0x22 }, .{ 0x88, 0x66, 0x00 },
.{ 0x22, 0x44, 0xbb }, .{ 0x99, 0x33, 0x99 }, .{ 0x11, 0x88, 0x88 }, .{ 0x55, 0x55, 0x55 },
diff --git a/test/gui-goldens.txt b/test/gui-goldens.txt
index 87134609..4c19dc4f 100644
--- a/test/gui-goldens.txt
+++ b/test/gui-goldens.txt
@@ -1,18 +1,18 @@
-01-boot d4e3eb9dba35133df10d46b1d27ab7a90fa9bdefb64bcc304add221326ffdfc9
-02-second-document 235cdf75f3ce1d5800dbbdd7dcfde066af50cde01231a35d00c66c4310213a05
-03-notice 4d59205fe6b89a4589e94c68521257f21b40917c0198ea20248925687663d9f5
-04-context-rows-mid-thumb 7129ee318975f858c430b1e610b26ca756d5bb37553338c0dc6caab2290de2a4
-05-body-selection 39103f3747aae07a12b5151e7e377430b88b96e504670ac2f2d0272197eb827b
-06-tag-focus-selection 6a7f50e292f4ede92df24a817218818b93a01fd15fcf39467cf06c3772077834
-07-prompt-notice d5cdacd74ae6d29834f0b09a607bfce5a2ca8a549c259be50b76a57a2d2d3271
-08-wide-glyph-edges 123a182b82a2a8c455291a5c89ccfa28e1f8ce9566172ca404c0b9c71dd33b48
-09-collapsed-focus-tint ae66076fdfb0cab9a906b052df257deee5ba91a6274712d52a76d8a7adc5b415
-10-tagbottom dd8ba0d3ecabf3420d000b0c9591a57278179cbd2a9060f849d283f3881d1847
-11-column-tags 6b1731d1611404597c96ad77f34c2d5cd5a81c1085ec68c6779388e07ce28614
-12-multiline-tag 0ea8f8a83fecef0e9a72d11f56bb7174739109d15074c56165274046bab48e17
-13-image 33986b288f6e2f4b3dc82113c7890980fc10362be471b3d6bf04eb1df7785e6b
-14-theme-ink 3c435c76370d09c9856a7b481a52206f4ab0314729da79eef8b442d25fa210fa
-15-theme-acme-light 3cf7ed81fc85ec91b5d448cab3d943f4724d754215944b4525e44885f73bccea
-16-debug d2fcf18c15c1cbf771c0141b7e7f680185a099bec9102af147bf7951446afb16
-17-terminal 9b46bf9969382991243aafba88977db3bd276bb2d72dd06552b1683d3ee7d90e
-18-mid-transition 11473788385e248cd7e1cc952629b3ad9a55c2068b51bf78d1494f631509e343
+01-boot 39c940960ccd264a0ed2e9dd99a2c352a47ca64c67d42e3313ae344713c712ae
+02-second-document 9a810fe372fa37bc3d338d7f0bd04b5224957cb57ef63952c8fc02d1e72e705a
+03-notice b52ebd3860694d35a43e56d149d180249b2609d4a3fc4374347d8cd3b5ebc05c
+04-context-rows-mid-thumb e7b9f5d6da06143fff79b9f2686c29050556173f97c759a4119a201542f993ef
+05-body-selection 06b7cbcf368bb4371873b26f70286fb2b5ada4293b758fa73458157a248c7aa6
+06-tag-focus-selection a781c95cebb8c47a5c5168e1b7f9747f44a0aad043cc4eec1338bb1f08d0a340
+07-prompt-notice f2a39f0d1136d818e57d693873256f2134d89f10805d4078eaaf16a59bece1ce
+08-wide-glyph-edges ad7439148a7705aac6f5f3de3f6a47f9863dccffeba561c70a77e9b9838c5847
+09-collapsed-focus-tint 807bd7621d54948b84cf2965ef463d3928d050259e8ee2d27472b1d3d378f91e
+10-tagbottom 03086852528d9c8a76f5926a7e7ee371200da617db279f42cbbb6825b5f41fce
+11-column-tags 28ac090425c28515893f184b8c1fd0ba1dc3f05c0385372bb23a0b9e2abd080e
+12-multiline-tag 72be302d73e36e6d51d940300d557e34b081b94ce7ec4e50e3bbd56968ed0d58
+13-image 162e6404d5578d1a8c34a8abd152b91d54588e8ddba540e88d48396560c06f71
+14-theme-ink 42b756fd9f1fba42004ac40983bc6c45445fd7d039a970c11378cac652e00d89
+15-theme-acme-light 5cecfb32915c217c5d39492b8708532bd826a8a55ad6b8bd49d61ed5cee29ab9
+16-debug 51628eb1d8dce799256e336d081d8510fe70b7a3f28f9efefcbca7126583b348
+17-terminal 65794362c9e76fa2930e85e888f17216d24cace7475e29da3610c0dab411887b
+18-mid-transition 72ce5905098af73e571a1efe009a700940458924502bcf5e96bfafe7c4b8f29c
diff --git a/test/snapshots/chordcut.golden b/test/snapshots/chordcut.golden
index d5266717..bff929b3 100644
--- a/test/snapshots/chordcut.golden
+++ b/test/snapshots/chordcut.golden
@@ -20,7 +20,7 @@
| ls
| f.txt g.txt
== snap cut grid=120x31 cursor=7,8
-|6: /tmp/pardes-snap/chordcut/cwd/f.txt * Save Tty Collapse D
+|6: * /tmp/pardes-snap/chordcut/cwd/f.txt * Save Tty Collapse D
|8: 2 2
== snap copied grid=120x31 cursor=10,9
== snap pointpaste grid=120x31 cursor=16,10
@@ -30,7 +30,7 @@
== snap cancelexec grid=120x31 cursor=25,8
|8: 2 2echo B''OO g.txt
== snap execafter grid=120x31 cursor=25,8
-|6: /tmp/pardes-snap/chordcut/cwd/f.txt * Save Tty Collapse D /tmp/pardes-snap/chordcut/cwd (echo B''OO) exit 0 Kill Sa
+|6: * /tmp/pardes-snap/chordcut/cwd/f.txt * Save Tty Collapse D /tmp/pardes-snap/chordcut/cwd (echo B''OO) exit 0 Kill Sa
|7: 1 line 1 Exec % echo B''OO
|8: 2 2echo B''OO g.txt BOO
|9: 3 line 3 exit 0
@@ -48,13 +48,13 @@
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|5: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-119 d,#0d1410,
-|6: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-37 #d3b0d9,#1d2824, 38-59 #b2c3b8,#1d2824, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-119 #b2c3b8,#1d2824,
+|6: 0-0 #ffffff,#496656, 1-1 #000000,#f9cb70,b 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-37 #d3b0d9,#1d2824, 38-59 #b2c3b8,#1d2824, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-119 #b2c3b8,#1d2824,
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-24 #e5eee5,#0d1410, 25-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-16 #e5eee5,#0d1410, 17-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|11: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|12: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-32 #becfc4,#26362d, 33-37 #dfbce5,#26362d, 38-59 #becfc4,#26362d, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|12: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-32 #becfc4,#26362d, 33-37 #dfbce5,#26362d, 38-59 #becfc4,#26362d, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-10 #f5f7ee,#3c614a, 11-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
@@ -77,7 +77,7 @@
|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
== snap ttyprobe grid=120x31 cursor=42,22
-|12: /tmp/pardes-snap/chordcut/cwd/g.txt * Save Tty Collapse D
+|12: * /tmp/pardes-snap/chordcut/cwd/g.txt * Save Tty Collapse D
|17: $ /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C
|18: $ ls
|20: $ stty -icanon -echo; printf '\x1b[?2004h\x1b[?1000h\x1b[?
diff --git a/test/snapshots/delask.golden b/test/snapshots/delask.golden
index 6b5331ec..35562f00 100644
--- a/test/snapshots/delask.golden
+++ b/test/snapshots/delask.golden
@@ -18,7 +18,7 @@
|
|
|
-| /tmp/pardes-snap/delask/cwd/+New * Save Tty Collapse Del
+| * /tmp/pardes-snap/delask/cwd/+New * Save Tty Collapse Del
| 1 middle-pane
|
|
@@ -27,7 +27,7 @@
|
|
|
-| /tmp/pardes-snap/delask/cwd/+New * Save Tty Collapse Del
+| * /tmp/pardes-snap/delask/cwd/+New * Save Tty Collapse Del
| 1 bottom-pane Up
== snap asked grid=100x36 cursor=18,20
|20: 1 middle-pane Del: give space to k above j below
@@ -40,7 +40,7 @@
|29:
== snap typed grid=100x36 cursor=27,20
|20: 1 bottom-pane DelBelow
-|28: /tmp/pardes-snap/delask/cwd/+New * Save Tty Collapse Del
+|28: * /tmp/pardes-snap/delask/cwd/+New * Save Tty Collapse Del
|29: 1 third-pane
== snap below-took-click grid=100x36 cursor=17,20
|20: 1 third-pane
diff --git a/test/snapshots/fileedit.golden b/test/snapshots/fileedit.golden
index 4d367d72..8a9555e0 100644
--- a/test/snapshots/fileedit.golden
+++ b/test/snapshots/fileedit.golden
@@ -5,7 +5,7 @@
| ls ls
| f.txt f.txt
|
-| /tmp/pardes-snap/fileedit/cwd/f.txt * Save Tty Collapse Del
+| * /tmp/pardes-snap/fileedit/cwd/f.txt * Save Tty Collapse Del
| 1 liEDIT''MEne 1
| 2 line 2
| 3 line 3
diff --git a/test/snapshots/filewatch.golden b/test/snapshots/filewatch.golden
index de44f0ca..ba9bce7c 100644
--- a/test/snapshots/filewatch.golden
+++ b/test/snapshots/filewatch.golden
@@ -1,7 +1,7 @@
== snap edited grid=210x31 cursor=15,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
-| /tmp/pardes-snap/filewatch/cwd/w.txt * Save Tty Collapse Del /tmp/pardes-snap/filewatch/cwd Tty+bash Save Mode Filter Collapse Del
+| * /tmp/pardes-snap/filewatch/cwd/w.txt * Save Tty Collapse Del /tmp/pardes-snap/filewatch/cwd Tty+bash Save Mode Filter Collapse Del
| 1 liMYEDITne 1 ls
| 2 line 2 sub w.txt
| 3 /tmp/pardes-snap/filewatch/cwd/sub/s.txt
@@ -52,7 +52,7 @@
|25:
|26:
== snap undone grid=210x31 cursor=15,3
-|2: /tmp/pardes-snap/filewatch/cwd/w.txt * Save Tty Collapse Del /tmp/pardes-snap/filewatch/cwd Tty+bash Save Mode Filter Collapse Del
+|2: * /tmp/pardes-snap/filewatch/cwd/w.txt * Save Tty Collapse Del /tmp/pardes-snap/filewatch/cwd Tty+bash Save Mode Filter Collapse Del
|3: 1 liMYEDITne 1 ls
|4: 2 line 2 sub w.txt
|5: 3 /tmp/pardes-snap/filewatch/cwd/sub/s.txt
diff --git a/test/snapshots/lsp-client-edit.golden b/test/snapshots/lsp-client-edit.golden
index c75bae4c..7fe973f9 100644
--- a/test/snapshots/lsp-client-edit.golden
+++ b/test/snapshots/lsp-client-edit.golden
@@ -1,7 +1,7 @@
== snap formatted grid=100x25 cursor=7,4
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-| /tmp/pardes-snap/lsp-client-edit/cwd/main.rs * Save Tty Collapse Del TreeContext
+| * /tmp/pardes-snap/lsp-client-edit/cwd/main.rs * Save Tty Collapse Del TreeContext
| 1 // formatted by mock formatted 1 range(s)
| 2 mod lib;
| 3
diff --git a/test/snapshots/lsp-rename.golden b/test/snapshots/lsp-rename.golden
index f6dc8358..9151d1f3 100644
--- a/test/snapshots/lsp-rename.golden
+++ b/test/snapshots/lsp-rename.golden
@@ -1,7 +1,7 @@
== snap renamed grid=110x25 cursor=17,10
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-| /tmp/pardes-snap/lsp-rename/cwd/rename.zig * Save Tty Collapse Del TreeContext
+| * /tmp/pardes-snap/lsp-rename/cwd/rename.zig * Save Tty Collapse Del TreeContext
| 1 const std = @import("std"); renamed 2 range(s)
| 2
| 3 fn renamed_helper(x: u32) u32 {
diff --git a/test/snapshots/lspcomplete.golden b/test/snapshots/lspcomplete.golden
index 8efa7edf..0a58a231 100644
--- a/test/snapshots/lspcomplete.golden
+++ b/test/snapshots/lspcomplete.golden
@@ -16,7 +16,7 @@
| 12 // note. see below
| 13
== snap indent grid=120x41 cursor=11,12
-|2: ^ /tmp/pardes-snap/lspcomplete/cwd/a.zig * Save Tty Collapse Del TreeContext
+|2: ^* /tmp/pardes-snap/lspcomplete/cwd/a.zig * Save Tty Collapse Del TreeContext
|12: 10 }
== snap commentindent grid=120x41 cursor=19,14
|14: 12 // note. see below
@@ -28,17 +28,17 @@
|40: 2 a.zig:3:5-12 cinnabar cinnabar,
== snap refill grid=120x41 cursor=16,10
== snap step grid=120x41 cursor=19,4
-|2: /tmp/pardes-snap/lspcomplete/cwd/a.zig * Save Tty Collapse Del TreeContext
+|2: * /tmp/pardes-snap/lspcomplete/cwd/a.zig * Save Tty Collapse Del TreeContext
== snap txtindent grid=120x41 cursor=15,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-|^ /tmp/pardes-snap/lspcomplete/cwd/notes.txt * Save Tty Collapse Del
+|^* /tmp/pardes-snap/lspcomplete/cwd/notes.txt * Save Tty Collapse Del
| 1 abc. def ghi
| 2
== snap nosemi grid=100x19 cursor=29,9
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-|^ /tmp/pardes-snap/lspcomplete/cwd/c.zig * Save Tty Collapse Del TreeContext
+|^* /tmp/pardes-snap/lspcomplete/cwd/c.zig * Save Tty Collapse Del TreeContext
| 1 const Shade = enum {
| 2 verdigris,
| 3 cinnabar,
@@ -58,7 +58,7 @@
== snap nobrace grid=100x19 cursor=16,10
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-|^ /tmp/pardes-snap/lspcomplete/cwd/d.zig * Save Tty Collapse Del TreeContext
+|^* /tmp/pardes-snap/lspcomplete/cwd/d.zig * Save Tty Collapse Del TreeContext
| 1 const Shade = enum {
| 2 verdigris,
| 3 cinnabar,
diff --git a/test/snapshots/lspcompletemoved.golden b/test/snapshots/lspcompletemoved.golden
index f12b3f0c..797bcd11 100644
--- a/test/snapshots/lspcompletemoved.golden
+++ b/test/snapshots/lspcompletemoved.golden
@@ -1,7 +1,7 @@
== snap typedthrough grid=120x21 cursor=16,14
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-|^ /tmp/pardes-snap/lspcompletemoved/cwd/a.zig * Save Tty Collapse Del TreeContext
+|^* /tmp/pardes-snap/lspcompletemoved/cwd/a.zig * Save Tty Collapse Del TreeContext
| 1 const Shade = enum {
| 2 verdigris,
| 3 cinnabar,
diff --git a/test/snapshots/lsprelpath.golden b/test/snapshots/lsprelpath.golden
index 96dc4c99..e2201d5c 100644
--- a/test/snapshots/lsprelpath.golden
+++ b/test/snapshots/lsprelpath.golden
@@ -1,7 +1,7 @@
== snap absrow grid=100x35 cursor=16,7
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-|^ /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig * Save Tty Collapse Del TreeContext
+|^* /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig * Save Tty Collapse Del TreeContext
| 1 const shade = @import("../shade.zig");
| 2
| 3 pub fn pick(s: shade.Shade) u8 {
@@ -35,7 +35,7 @@
| 1 /tmp/pardes-snap/lsprelpath/cwd/shade.zig:2:5-13 verdigris verdigris,
| 2 /tmp/pardes-snap/lsprelpath/cwd/shade.zig:3:5-12 cinnabar cinnabar,
== snap abslook grid=100x35 cursor=19,19
-|2: /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig * Save Tty Collapse Del TreeContext
+|2: * /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig * Save Tty Collapse Del TreeContext
|17: /tmp/pardes-snap/lsprelpath/cwd/shade.zig Save Tty Collapse Del TreeContext
|18: 1 pub const Shade = enum {
|19: 2 verdigris,
@@ -45,7 +45,7 @@
== style abslook grid=100x35
|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-99 d,#26362d,
-|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-48 #d3b0d9,#1d2824, 49-84 #b2c3b8,#1d2824, 85-99 d,#1d2824,
+|2: 0-0 #ffffff,#496656, 1-1 #000000,#f9cb70,b 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-48 #d3b0d9,#1d2824, 49-84 #b2c3b8,#1d2824, 85-99 d,#1d2824,
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-27 #c9a7ed,#0d1410, 28-28 #e5eee5,#0d1410, 29-42 #b8d88a,#0d1410, 43-44 #e5eee5,#0d1410, 45-99 d,#0d1410,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-12 #c9a7ed,#0d1410, 13-38 #e5eee5,#0d1410, 39-99 d,#0d1410,
@@ -81,7 +81,7 @@
== snap subrow grid=100x35 cursor=16,7
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-|^ /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig * Save Tty Collapse Del TreeContext
+|^* /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig * Save Tty Collapse Del TreeContext
| 1 const tint = @import("inner/tint.zig");
| 2
| 3 pub fn hue(t: tint.Tint) u8 {
@@ -115,7 +115,7 @@
| 1 inner/tint.zig:2:5-9 ochre ochre,
| 2 inner/tint.zig:3:5-10 indigo indigo,
== snap sublook grid=100x35 cursor=15,19
-|2: /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig * Save Tty Collapse Del TreeContext
+|2: * /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig * Save Tty Collapse Del TreeContext
|17: /tmp/pardes-snap/lsprelpath/cwd/sub/inner/tint.zig Save Tty Collapse Del TreeContext
|18: 1 pub const Tint = enum {
|19: 2 ochre,
@@ -125,7 +125,7 @@
== style sublook grid=100x35
|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-99 d,#26362d,
-|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-48 #d3b0d9,#1d2824, 49-84 #b2c3b8,#1d2824, 85-99 d,#1d2824,
+|2: 0-0 #ffffff,#496656, 1-1 #000000,#f9cb70,b 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-48 #d3b0d9,#1d2824, 49-84 #b2c3b8,#1d2824, 85-99 d,#1d2824,
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-26 #c9a7ed,#0d1410, 27-27 #e5eee5,#0d1410, 28-43 #b8d88a,#0d1410, 44-45 #e5eee5,#0d1410, 46-99 d,#0d1410,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-12 #c9a7ed,#0d1410, 13-35 #e5eee5,#0d1410, 36-99 d,#0d1410,
diff --git a/test/snapshots/msg.golden b/test/snapshots/msg.golden
index d9a4a079..293e137c 100644
--- a/test/snapshots/msg.golden
+++ b/test/snapshots/msg.golden
@@ -45,7 +45,7 @@
|3: 1 AGAIN
|4: 2
== snap msgbuiltin grid=70x17 cursor=29,5
-|2: /tmp/pardes-snap/msg/cwd/a.txt * Save Tty Collapse Del
+|2: * /tmp/pardes-snap/msg/cwd/a.txt * Save Tty Collapse Del
|3: 1 AGAIN hand-written report
|5: 3 Msg hand-written report
== snap msgtail grid=70x17 cursor=32,7
diff --git a/test/snapshots/multicursor.golden b/test/snapshots/multicursor.golden
index 96762bd5..010199b1 100644
--- a/test/snapshots/multicursor.golden
+++ b/test/snapshots/multicursor.golden
@@ -52,13 +52,13 @@
|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
== snap edited-at-three grid=150x31 cursor=9,9
-|6: /tmp/pardes-snap/multicursor/cwd/f.txt * Save Tty Collapse Del
+|6: * /tmp/pardes-snap/multicursor/cwd/f.txt * Save Tty Collapse Del
|7: 1 >>line 1
|8: 2 >>line 2
|9: 3 >>line 3
== snap three-selections grid=150x31 cursor=8,9
== style three-selections grid=150x31
-|6: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-35 #becfc4,#26362d, 36-40 #dfbce5,#26362d, 41-64 #becfc4,#26362d, 65-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|6: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-35 #becfc4,#26362d, 36-40 #dfbce5,#26362d, 41-64 #becfc4,#26362d, 65-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #f5f7ee,#243a2d, 8-8 #3c614a,#f5f7ee, 9-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #f5f7ee,#243a2d, 8-8 #3c614a,#f5f7ee, 9-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-8 #f5f7ee,#3c614a, 9-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
diff --git a/test/snapshots/ninep.golden b/test/snapshots/ninep.golden
index 65fb38ec..5391a552 100644
--- a/test/snapshots/ninep.golden
+++ b/test/snapshots/ninep.golden
@@ -73,7 +73,7 @@
|7: 5 return 7;
@7: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-19 #e5eee5,#0d1410,,body 20-89 d,#0d1410,,body
== snap edited-with-colors-off grid=90x15 cursor=7,4
-|2: /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext
+|2: * /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext
|3: 1 // café λ 界
|4: 2 const greeting = "café λ 界 é";
|5: 3
@@ -83,8 +83,8 @@
|9: 7 }
|10: 8
== 9p edited-with-colors-off grid=90x15 cursor=7,4,block
-|2: /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext
-@2: 0-1 #000000,#add38a,,tagline 2-2 d,#26362d,,tagline 3-29 #becfc4,#26362d,,tagline 30-37 #dfbce5,#26362d,,tagline 38-73 #becfc4,#26362d,,tagline 74-89 d,#26362d,,tagline
+|2: * /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext
+@2: 0-0 #000000,#add38a,,tagline 1-1 #000000,#f9cb70,b,tagline 2-2 d,#26362d,,tagline 3-29 #becfc4,#26362d,,tagline 30-37 #dfbce5,#26362d,,tagline 38-73 #becfc4,#26362d,,tagline 74-89 d,#26362d,,tagline
|3: 1 // café λ 界
+3: 13="é" 15="λ" 17="界" 18=""
@3: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-18 #e5eee5,#0d1410,,body 19-89 d,#0d1410,,body
@@ -104,7 +104,7 @@
|10: 8
@10: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
== style colors-on grid=90x15
-|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-73 #becfc4,#26362d, 74-89 d,#26362d,
+|2: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-73 #becfc4,#26362d, 74-89 d,#26362d,
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #b9f480,#0d1410, 18-18 d,d, 19-81 d,#0d1410, 82-82 d,#1d2824, 83-88 #b2c3b8,#1d2824, 89-89 d,#1d2824,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-23 #e5eee5,#0d1410, 24-32 #b8d88a,#0d1410, 33-33 d,d, 34-36 #b8d88a,#0d1410, 37-37 #e5eee5,#0d1410, 38-89 d,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
@@ -128,7 +128,7 @@
== style resized grid=105x20
|0: 0-70 #b2c3b8,#1d2824, 71-104 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-104 d,#26362d,
-|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-73 #becfc4,#26362d, 74-104 d,#26362d,
+|2: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-73 #becfc4,#26362d, 74-104 d,#26362d,
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #b9f480,#0d1410, 18-18 d,d, 19-96 d,#0d1410, 97-97 d,#1d2824, 98-103 #b2c3b8,#1d2824, 104-104 d,#1d2824,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-23 #e5eee5,#0d1410, 24-32 #b8d88a,#0d1410, 33-33 d,d, 34-36 #b8d88a,#0d1410, 37-37 #e5eee5,#0d1410, 38-104 d,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-104 d,#0d1410,
@@ -151,8 +151,8 @@
@0: 0-70 #b2c3b8,#1d2824,,tagline 71-104 d,#1d2824,,tagline
|1: New Tty Find Grep Joincol Delcol
@1: 0-1 #ffffff,#887455,,tagline 2-2 d,#26362d,,tagline 3-34 #becfc4,#26362d,,tagline 35-104 d,#26362d,,tagline
-|2: /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext
-@2: 0-1 #000000,#add38a,,tagline 2-2 d,#26362d,,tagline 3-29 #becfc4,#26362d,,tagline 30-37 #dfbce5,#26362d,,tagline 38-73 #becfc4,#26362d,,tagline 74-104 d,#26362d,,tagline
+|2: * /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext
+@2: 0-0 #000000,#add38a,,tagline 1-1 #000000,#f9cb70,b,tagline 2-2 d,#26362d,,tagline 3-29 #becfc4,#26362d,,tagline 30-37 #dfbce5,#26362d,,tagline 38-73 #becfc4,#26362d,,tagline 74-104 d,#26362d,,tagline
|3: 1 // café λ 界 Colors
+3: 13="é" 15="λ" 17="界" 18=""
@3: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-18 #b9f480,#0d1410,,body 19-96 d,#0d1410,,body 97-97 d,#1d2824,,tagline 98-103 #b2c3b8,#1d2824,,tagline 104-104 d,#1d2824,,tagline
@@ -192,7 +192,7 @@
== snap resized grid=105x20 cursor=7,4
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-| /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext
+| * /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext
| 1 // café λ 界 Colors
| 2 const greeting = "café λ 界 é";
| 3
diff --git a/test/snapshots/pipe.golden b/test/snapshots/pipe.golden
index 14353ebf..208986a8 100644
--- a/test/snapshots/pipe.golden
+++ b/test/snapshots/pipe.golden
@@ -5,7 +5,7 @@
| 1 alpha beta |tr a-z A-Z
| 2 gamma delta
== snap filtered grid=180x13 cursor=17,3
-|2: /tmp/pardes-snap/pipe/cwd/pipe.txt * Save Tty Collapse Del
+|2: * /tmp/pardes-snap/pipe/cwd/pipe.txt * Save Tty Collapse Del
|3: 1 ALPHA BETA
== snap undone grid=180x13 cursor=17,3
|3: 1 alpha beta
diff --git a/test/snapshots/savepane.golden b/test/snapshots/savepane.golden
index 5fcdf656..f1a47696 100644
--- a/test/snapshots/savepane.golden
+++ b/test/snapshots/savepane.golden
@@ -4,7 +4,7 @@
| /tmp/pardes-snap/savepane/cwd Tty+bash Save Mode Filter Collapse Del
| ls
|
-| /tmp/pardes-snap/savepane/cwd/+New * Save Tty Collapse Del
+| * /tmp/pardes-snap/savepane/cwd/+New * Save Tty Collapse Del
| 1 scratch bytes
== snap prompt grid=100x31 cursor=99,7
|6: 1 scratch bytes Save
diff --git a/test/snapshots/selregex.golden b/test/snapshots/selregex.golden
index 8b45ae43..fa29b661 100644
--- a/test/snapshots/selregex.golden
+++ b/test/snapshots/selregex.golden
@@ -51,7 +51,7 @@
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #f5f7ee,#243a2d, 11-11 #3c614a,#f5f7ee, 12-18 #e5eee5,#0d1410, 19-19 #f8c082,#0d1410, 20-20 #e5eee5,#0d1410, 21-159 d,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #f5f7ee,#243a2d, 11-11 #3c614a,#f5f7ee, 12-20 #e5eee5,#0d1410, 21-21 #f8c082,#0d1410, 22-22 #e5eee5,#0d1410, 23-159 d,#0d1410,
== snap commented grid=160x25 cursor=14,3
-|2: /tmp/pardes-snap/selregex/cwd/m.zig * Save Tty Collapse Del TreeContext
+|2: * /tmp/pardes-snap/selregex/cwd/m.zig * Save Tty Collapse Del TreeContext
|3: 1 // const one = 1;
|4: 2 // const two = 2;
|5: 3 // const three = 3;
diff --git a/test/snapshots/tagbottom.golden b/test/snapshots/tagbottom.golden
index 8d770358..637d3143 100644
--- a/test/snapshots/tagbottom.golden
+++ b/test/snapshots/tagbottom.golden
@@ -164,7 +164,7 @@
|8: 7 mmm
|9: 8 nnn
|10: 9 ooo
-|11: /tmp/pardes-snap/tagbottom/cwd/top.txt * Save Tty Collapse Del bottom.txt
+|11: * /tmp/pardes-snap/tagbottom/cwd/top.txt * Save Tty Collapse Del bottom.txt
|12: 1 ZZONE
== file cut-buffer
aaa
diff --git a/test/snapshots/tagcmd.golden b/test/snapshots/tagcmd.golden
index 7853de32..83106d0d 100644
--- a/test/snapshots/tagcmd.golden
+++ b/test/snapshots/tagcmd.golden
@@ -1,7 +1,7 @@
== snap edited grid=100x31 cursor=13,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-| /tmp/pardes-snap/tagcmd/cwd/c.txt * Save Tty Collapse Del
+| * /tmp/pardes-snap/tagcmd/cwd/c.txt * Save Tty Collapse Del
| 1 ZZEDITline 1
| 2 line 2
| 3 line 3
diff --git a/test/snapshots/tagpath.golden b/test/snapshots/tagpath.golden
index ba22a859..2363bcd6 100644
--- a/test/snapshots/tagpath.golden
+++ b/test/snapshots/tagpath.golden
@@ -43,15 +43,15 @@
|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
== snap yanked-path grid=100x31 cursor=41,3
-|2: /tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del
+|2: * /tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del
|3: 1 l/tmp/pardes-snap/tagpath/cwd/c.txtine 1
== snap drag-cross grid=100x31 cursor=41,2
== style drag-cross grid=100x31
-|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-10 #becfc4,#26362d, 11-41 #f5f7ee,#3c614a, 42-60 #becfc4,#26362d, 61-99 d,#26362d,
+|2: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-10 #becfc4,#26362d, 11-41 #f5f7ee,#3c614a, 42-60 #becfc4,#26362d, 61-99 d,#26362d,
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-41 #f5f7ee,#3c614a, 42-46 #e5eee5,#0d1410, 47-99 d,#0d1410,
== snap name-draft grid=100x31 cursor=11,2
-|2: /renamed/tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del
+|2: * /renamed/tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del
== snap name-cancelled grid=100x31 cursor=41,3
-|2: /tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del
+|2: * /tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del
== snap name-committed grid=100x31 cursor=41,3
-|2: /renamed/tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del
+|2: * /renamed/tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del
diff --git a/test/snapshots/theme.golden b/test/snapshots/theme.golden
index 4986eaae..5e7a3dc4 100644
--- a/test/snapshots/theme.golden
+++ b/test/snapshots/theme.golden
@@ -612,9 +612,9 @@
|30: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
== snap acme grid=100x31 cursor=4,5
== style acme grid=100x31
-|0: 0-50 #000000,#eaffff, 51-59 #000000,#fafa92, 60-70 #000000,#eaffff, 71-99 d,#eaffff,
-|1: 0-1 #000000,#c5a87f, 2-2 d,#eaffff, 3-34 #000000,#eaffff, 35-49 d,#eaffff, 50-51 #000000,#c5a87f, 52-52 d,#eaffff, 53-84 #000000,#eaffff, 85-99 d,#eaffff,
-|2: 0-1 #ffffff,#663fe0, 2-2 d,#eaffff, 3-49 #000000,#eaffff, 50-51 #ffffff,#353a58, 52-52 d,#eaffff, 53-99 #000000,#eaffff,
+|0: 0-50 #000000,#eaffff, 51-59 #000000,#9eeeee, 60-70 #000000,#eaffff, 71-99 d,#eaffff,
+|1: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-34 #000000,#eaffff, 35-49 d,#eaffff, 50-51 #000000,#8888cc, 52-52 d,#eaffff, 53-84 #000000,#eaffff, 85-99 d,#eaffff,
+|2: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-49 #000000,#eaffff, 50-51 #000000,#b9c3e5, 52-52 d,#eaffff, 53-99 #000000,#eaffff,
|3: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|4: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|5: 0-99 d,#ffffea,
@@ -629,7 +629,7 @@
|14: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|15: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|16: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
-|17: 0-1 #ffffff,#353a58, 2-2 d,#eaffff, 3-49 #000000,#eaffff, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
+|17: 0-1 #000000,#b9c3e5, 2-2 d,#eaffff, 3-49 #000000,#eaffff, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|18: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|19: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|20: 0-51 d,#ffffea, 52-99 #000000,#ffffea,
diff --git a/test/snapshots/themesel.golden b/test/snapshots/themesel.golden
index 97d3ed41..65a4f563 100644
--- a/test/snapshots/themesel.golden
+++ b/test/snapshots/themesel.golden
@@ -630,8 +630,8 @@
|30: 24 Theme adwaita_dark
== style step-acme grid=100x31
|0: 0-70 #000000,#eaffff, 71-99 d,#eaffff,
-|1: 0-1 #000000,#c5a87f, 2-2 d,#eaffff, 3-34 #000000,#eaffff, 35-99 d,#eaffff,
-|2: 0-1 #ffffff,#353a58, 2-2 d,#eaffff, 3-59 #000000,#eaffff, 60-99 d,#eaffff,
+|1: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-34 #000000,#eaffff, 35-99 d,#eaffff,
+|2: 0-1 #000000,#b9c3e5, 2-2 d,#eaffff, 3-59 #000000,#eaffff, 60-99 d,#eaffff,
|3: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea,
|4: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea,
|5: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea,
@@ -646,7 +646,7 @@
|14: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
|15: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
|16: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
-|17: 0-1 #ffffff,#663fe0, 2-2 d,#eaffff, 3-61 #000000,#eaffff, 62-99 d,#eaffff,
+|17: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-61 #000000,#eaffff, 62-99 d,#eaffff,
|18: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-19 #000000,#ffffea, 20-92 d,#ffffea, 93-93 d,#eaffff, 94-98 #000000,#eaffff, 99-99 d,#eaffff,
|19: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-16 #000000,#ffffea, 17-99 d,#ffffea,
|20: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-23 #000000,#ffffea, 24-99 d,#ffffea,
@@ -709,8 +709,8 @@
== snap step-back grid=100x31 cursor=7,26
== style step-back grid=100x31
|0: 0-70 #000000,#eaffff, 71-99 d,#eaffff,
-|1: 0-1 #000000,#c5a87f, 2-2 d,#eaffff, 3-34 #000000,#eaffff, 35-99 d,#eaffff,
-|2: 0-1 #ffffff,#353a58, 2-2 d,#eaffff, 3-59 #000000,#eaffff, 60-99 d,#eaffff,
+|1: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-34 #000000,#eaffff, 35-99 d,#eaffff,
+|2: 0-1 #000000,#b9c3e5, 2-2 d,#eaffff, 3-59 #000000,#eaffff, 60-99 d,#eaffff,
|3: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea,
|4: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea,
|5: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea,
@@ -725,7 +725,7 @@
|14: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
|15: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
|16: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
-|17: 0-1 #ffffff,#663fe0, 2-2 d,#eaffff, 3-61 #000000,#eaffff, 62-99 d,#eaffff,
+|17: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-61 #000000,#eaffff, 62-99 d,#eaffff,
|18: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-16 #000000,#ffffea, 17-92 d,#ffffea, 93-93 d,#eaffff, 94-98 #000000,#eaffff, 99-99 d,#eaffff,
|19: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-23 #000000,#ffffea, 24-99 d,#ffffea,
|20: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-22 #000000,#ffffea, 23-99 d,#ffffea,
@@ -769,7 +769,7 @@
|29: 27 line 27
|30: 28 line 28
== style picked grid=100x31
-|2: 0-1 #ffffff,#663fe0, 2-2 d,#eaffff, 3-59 #000000,#eaffff, 60-99 d,#eaffff,
+|2: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-59 #000000,#eaffff, 60-99 d,#eaffff,
|3: 0-1 d,#ffffea, 2-6 #999980,#ffffea,b 7-12 #000000,#ffffea, 13-99 d,#ffffea,
|7: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea,
|8: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea,
diff --git a/test/snapshots/yankpaste.golden b/test/snapshots/yankpaste.golden
index 2efcd66d..12e7a74e 100644
--- a/test/snapshots/yankpaste.golden
+++ b/test/snapshots/yankpaste.golden
@@ -1,7 +1,7 @@
== snap linewise-below grid=100x31 cursor=13,6
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-| /tmp/pardes-snap/yankpaste/cwd/f.txt * Save Tty Collapse Del
+| * /tmp/pardes-snap/yankpaste/cwd/f.txt * Save Tty Collapse Del
| 1 line 1
| 2 line 2
| 3 line 3