summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 12:24:57 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit3921bfe63980552eddeaa930d5d921d9a2868b37 (patch)
treed6b280abf3848f29b9a65a19aa2d010cc2d23660
parentff0e9832570d9d73b7964a275b605567a0e412b4 (diff)
parentd6195b4ecc89a6506985e17313b580d570b03b77 (diff)
downloadpardes-3921bfe63980552eddeaa930d5d921d9a2868b37.tar.gz
pardes-3921bfe63980552eddeaa930d5d921d9a2868b37.zip
Release Pardes 0.7
Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--build.zig.zon2
-rw-r--r--docs/themes.md5
-rw-r--r--src/CHANGELOG.md26
-rw-r--r--src/colors.zig19
-rw-r--r--src/gui/gui.zig99
-rw-r--r--src/themes/lapis_plain.zig81
-rw-r--r--test/gui_monkey.py2
-rw-r--r--test/snapshots/theme.golden2
-rw-r--r--test/snapshots/theme.snap7
-rw-r--r--test/snapshots/themesel.golden123
-rw-r--r--test/snapshots/themesel.snap9
11 files changed, 307 insertions, 68 deletions
diff --git a/build.zig.zon b/build.zig.zon
index c052341a..e80961ef 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -1,6 +1,6 @@
.{
.name = .pardes,
- .version = "0.0.6",
+ .version = "0.0.7",
.dependencies = .{
// 9P sessions and transports live in the published cloud9 package, so
// the manifest pins the build: `zig build` fetches this commit into
diff --git a/docs/themes.md b/docs/themes.md
index c7204173..db6349c4 100644
--- a/docs/themes.md
+++ b/docs/themes.md
@@ -6,10 +6,13 @@ terminals together. `orchard` is the initial theme.
Execute `Theme <name>` anywhere, or open `Themes` with `SPC t t` and select
a theme's command. Put the same command in your [startup configuration](config.md)
to keep a preference. `NextColor` cycles through the native themes first,
-then `acme` and `lapis`, the [faithful ports](#faithful-ports), the legacy
+then `acme`, `lapis` and `lapis_plain`, the [faithful ports](#faithful-ports), the legacy
`helix` and `dark`, and the imported palettes. `Themes` shows the same
order in sections: `Pardes themes`, one per ported family, `Legacy themes`
and `Imported themes (helix, zed)`; its navigation skips the section headings.
+`lapis` draws its ornament in the GUI; `lapis_plain` is the same theme on
+flat grounds (no dot grid under the text, no stripes behind the focused
+tag), with lapis's framed and shadowed tags and its file-name shadow.
See the [Agave visual review](ui-review.md) for the original six-palette gallery.
| Theme | Character | Page | Accent |
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 42f2eb6f..3dd7c996 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -1,5 +1,31 @@
# Changelog
+## 0.0.7
+
+- `lapis` draws its cells unblended and each plaque pixel once: about a
+ third less GPU time a frame at 4K, pixel for pixel the same. `lapis_plain`
+ is lapis on flat grounds, keeping its gold frames, plaque shadows and
+ file-name shadow.
+- Fixed a crash at startup when the 9P socket path is longer than the
+ system allows, and a leak when a terminal pane closes.
+- Pane, column and workspace tags share one set of write checks: the 4096
+ byte limit, control characters, and a refused `>` write leaves the tag as
+ it was. Column and workspace tags read without a trailing newline.
+- A control character in a write to any ctl file refuses the whole write.
+- Restore never reuses a column serial; the log announces a pane's `new`
+ before anything else about it.
+- `DumpConfig` prints each setting as the ctl word that sets it, and
+ `unsupported` for one the frontend lacks.
+- `New` and `Tty` run from the workspace and column tags over 9P, with no
+ panes too; `Help` and `Find` there act in the session's directory.
+- `pardes NEWFILE` forwards into a directory that does not exist yet; Save
+ makes it. The Tutor pane is `<dir>/+Tutor`.
+- Long-path errors keep their reason; `name` refuses a component over 255
+ bytes; regex refusals of an unanchored branch say why; acme's `Tab`,
+ `Indent`, `Local`, `Incl` and `Abort` are refused, not run as commands.
+- `Lspwhy` explains the last language-server query, whatever its kind.
+- Bulk body writes over 9P are about 15x faster per 60 KB write.
+
## 0.0.6
- A middle or right sweep in a column tag or the workspace tag runs or looks
diff --git a/src/colors.zig b/src/colors.zig
index b6da0d2c..c78043cc 100644
--- a/src/colors.zig
+++ b/src/colors.zig
@@ -106,7 +106,7 @@ test "every native theme's separators stand off its page, and its focus tint off
}
test "every faithful port meets pardes's chrome floors in its own colours, with nothing left to derive" {
- const first = comptime indexOfTheme("lapis") + 1;
+ const first = comptime indexOfTheme("lapis_plain") + 1;
const last = comptime indexOfTheme("helix");
for (themes[first..last]) |*th| {
const chrome = ChromeTheme.fromTheme(th);
@@ -394,6 +394,7 @@ const curated = struct {
pub const orchard_black = @import("themes/orchard_black.zig");
pub const acme = @import("themes/acme.zig");
pub const lapis = @import("themes/lapis.zig");
+ pub const lapis_plain = @import("themes/lapis_plain.zig");
// Faithful ports of well-known themes, a family at a time (src/themes/
// <name>.zig says where each value came from): `theme_groups`.
pub const dark_plus = @import("themes/dark_plus.zig");
@@ -565,6 +566,22 @@ test "the picker's sections run in ring order and cover it" {
try std.testing.expect(groupAt(native_count) != null);
}
+test "lapis_plain is lapis on flat grounds, framed and shadowed as lapis" {
+ const lapis = themes[indexOfTheme("lapis")];
+ const plain = themes[indexOfTheme("lapis_plain")];
+ try std.testing.expectEqual(indexOfTheme("lapis") + 1, indexOfTheme("lapis_plain"));
+ var same = plain;
+ same.name = lapis.name;
+ same.decor = lapis.decor;
+ try std.testing.expectEqualDeep(lapis, same);
+ const decor = plain.decor.?;
+ try std.testing.expect(decor.page_dots == null and decor.tag_stripe == null);
+ try std.testing.expectEqualDeep(lapis.decor.?.tag_border, decor.tag_border);
+ try std.testing.expectEqualDeep(lapis.decor.?.tag_shadow, decor.tag_shadow);
+ try std.testing.expectEqualDeep(lapis.decor.?.title_shadow, decor.title_shadow);
+ try std.testing.expectEqualDeep(lapis.decor.?.rail_checker, decor.rail_checker);
+}
+
/// Original and classic-inspired Pardes palettes precede retained imports.
pub const native_theme_count = 15;
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 8fd8d360..be961477 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -5872,6 +5872,7 @@ fn renderFrame(
if (surface.chrome.fx & 2 == 0 or surface.panelTracks().len != 0) g.hover_glow = .{ .now_ns = g.sel_glow.now_ns };
if (g.glide.moving()) g.glide.offset = g.glide.spring.value(g.glide.now_ns);
cell_decor = CellDecor.of(chrome);
+ plaque_through_len = 0;
// G13: a body's dots follow its text at a quarter of its speed: its
// first line, and a glide's picture offset, in rows.
if (chrome.fx & 8 != 0 and chrome.decor_dots != null) for (surface.regionList()) |region| {
@@ -6159,6 +6160,25 @@ fn renderFrame(
for (groups.items[0..groups.len], 0..) |group, index|
std.debug.assert(cell_next[index] == group.cell_start + group.cell_count);
shifted = emitScrollRows(g, instances, cell_total, surface, layout, win_w, win_h, page);
+ // Over a theme's plaques a see-through cell with no ink blends to
+ // the plaque under it, so it draws nothing: its quad goes, and the
+ // cells are drawn unblended (a third of the GPU's frame at 4K)
+ // unless one see-through cell does have ink, or the page itself is
+ // see-through.
+ if (cell_decor.nplaques != 0) {
+ const space_u = @as(f32, @floatFromInt(g.space_slot.u)) / @as(f32, @floatFromInt(atlas_w));
+ const space_v = @as(f32, @floatFromInt(g.space_slot.v)) / @as(f32, @floatFromInt(atlas_h));
+ if (page.clear or plaque_through_len > plaque_through.len) cell_decor.inked_clear = true;
+ for (plaque_through[0..@min(plaque_through_len, plaque_through.len)]) |at| {
+ const instance = &instances[at];
+ // A grip's button takes its cell's ground back (emitSurfaceCell).
+ if (instance.effect & clear_bg_bit == 0) continue;
+ if (instance.u0 == space_u and instance.v0 == space_v) {
+ instance.x1 = instance.x0;
+ instance.y1 = instance.y0;
+ } else cell_decor.inked_clear = true;
+ }
+ }
@memcpy(instances[cell_total + shifted ..][0..decor_len], g.decor.items);
c.SDL_UnmapGPUTransferBuffer(g.device, g.vxfer.?);
@@ -6190,7 +6210,7 @@ fn renderFrame(
// Blended over what is under: a theme's plaques show through
// blank tag cells; a dissolve's old cells fade over its new.
const dissolving = if (group.track) |active| active.effect == .dissolve else false;
- c.SDL_BindGPUGraphicsPipeline(rp, if (cell_decor.nplaques != 0 or dissolving) g.pipeline_over else g.pipeline);
+ c.SDL_BindGPUGraphicsPipeline(rp, if (cell_decor.inked_clear or dissolving) g.pipeline_over else g.pipeline);
const samp_binding = c.SDL_GPUTextureSamplerBinding{ .texture = g.atlas_tex, .sampler = g.atlas_sampler };
c.SDL_BindGPUFragmentSamplers(rp, 0, &samp_binding, 1);
if (group.cell_count != 0) {
@@ -6516,7 +6536,7 @@ fn emitTagLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const p
emitInstance(g, instances, next.*, 0, y, .{ .w = band_right - left, .h = bh, .x_off = left, .y_off = slide }, win_w, win_h, track, .body, (&background)[0..1], 0, null, old_layer, false, page);
// A theme's plaque is drawn under the band: the band's own ground
// lets it show (tagPlaques).
- if (cell_decor.nplaques != 0 and cell_decor.inPlaque(left + 0.5, top + 0.5)) instances[next.*].effect |= clear_bg_bit;
+ if (cell_decor.nplaques != 0 and cell_decor.inPlaque(left + 0.5, top + 0.5)) plaqueThrough(instances, next.*);
if (slide != 0) clipInstanceTop(&instances[next.*], top, win_h);
next.* += 1;
const cells = layer.cells[index * layer.cols ..][0..layer.cols];
@@ -6728,7 +6748,7 @@ fn emitSurfaceCell(
const cell_x = @as(f32, @floatFromInt(col)) * body_layout.w + body_layout.x_off;
const cell_y = @as(f32, @floatFromInt(row)) * body_layout.h + body_layout.y_off + 0.5;
if (cell_decor.nplaques != 0 and cell_decor.inPlaque(if (here.grip) cell_x + body_layout.w - 0.5 else cell_x + 0.5, cell_y))
- instances[next.*].effect |= clear_bg_bit;
+ plaqueThrough(instances, next.*);
next.* += 1;
// Tag glyphs keep their font size inside a full physical-width grip.
// A pane's grip cells are the cell shells' form (a dirty `*`): the
@@ -7060,7 +7080,7 @@ fn emitInstance(
if (cell_decor.nplaques != 0 and resolved.role == .tagline and cellCodepoint(&line[at]) == ' ' and
(std.mem.eql(u8, &resolved.bg, &cell_decor.grounds[0]) or std.mem.eql(u8, &resolved.bg, &cell_decor.grounds[1])) and
(cell_decor.inPlaque(px0 + 0.5, py0 + 0.5) or cell_decor.inPlaque(px0 + layout.w - 0.5, py0 + 0.5)))
- instances[idx].effect |= clear_bg_bit;
+ plaqueThrough(instances, idx);
if (cell_decor.dots) |dotted| {
if (!resolved.clear_bg and std.mem.eql(u8, &resolved.bg, &dotted)) {
instances[idx].effect |= dotted_bit;
@@ -9034,6 +9054,17 @@ fn updateDisplayScale(window: *c.SDL_Window) void {
/// it falls on, and pushLayerOpacity hands the shader its colours and
/// sizes. ponytail: one window per process, as display_scale.
var cell_decor: CellDecor = .{};
+/// The cells this frame lets a plaque show through, by instance, for
+/// renderFrame to drop the ones with no ink. Past its length the frame's
+/// cells are blended instead.
+var plaque_through: [16384]u32 = undefined;
+var plaque_through_len: usize = 0;
+
+fn plaqueThrough(instances: [*]CellInstance, at: u32) void {
+ instances[at].effect |= clear_bg_bit;
+ if (plaque_through_len < plaque_through.len) plaque_through[plaque_through_len] = at;
+ plaque_through_len += 1;
+}
const CellDecor = struct {
/// The page's ground, when the theme dots it.
dots: ?[3]u8 = null,
@@ -9048,6 +9079,9 @@ const CellDecor = struct {
nplaques: usize = 0,
grounds: [2][3]u8 = .{ .{ 0, 0, 0 }, .{ 0, 0, 0 } },
plaques_off: bool = false,
+ /// A see-through cell over the plaques has ink of its own, so this
+ /// frame's cells are blended (renderFrame).
+ inked_clear: bool = false,
/// G13: each body's rectangle and how far its dots have moved.
bodies: [pardes.MAX_PANES][4]f32 = undefined,
@@ -9296,12 +9330,15 @@ fn tagPlaques(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, g
const b = plaque.border;
const sh = plaque.shadow;
const fill = if (tag.active) chrome.tag_focus_bg else chrome.tag_bg;
- // The band's own ground, then the shadow, the frame and the fill.
- try addDecor(g, gpa, plaque.band[0], plaque.band[1], plaque.band[2], plaque.band[3], page, 1, track, win_w, win_h);
- const shadow_rgb = chrome.decor_box_shadow orelse border_rgb;
- try addDecor(g, gpa, plaque.box[0] + sh[0], plaque.box[1] + sh[1], plaque.box[2] + sh[0], plaque.box[3] + sh[1], if (pane) shadow_rgb else mixRgb(shadow_rgb, page), 1, track, win_w, win_h);
- try addDecor(g, gpa, plaque.box[0], plaque.box[1], plaque.box[2], plaque.box[3], border_rgb, 1, track, win_w, win_h);
+ // The band's own ground, then the shadow, the frame and the fill,
+ // each only where the ones over it leave it showing: all four are
+ // opaque, so that is the same picture drawn once, not four times.
+ const shadow: [4]f32 = .{ plaque.box[0] + sh[0], plaque.box[1] + sh[1], plaque.box[2] + sh[0], plaque.box[3] + sh[1] };
const inner: [4]f32 = .{ plaque.box[0] + b, plaque.box[1] + b, plaque.box[2] - b, plaque.box[3] - b };
+ try addDecorExcept(g, gpa, plaque.band, &.{ plaque.box, shadow }, page, track, win_w, win_h);
+ const shadow_rgb = chrome.decor_box_shadow orelse border_rgb;
+ try addDecorExcept(g, gpa, shadow, &.{plaque.box}, if (pane) shadow_rgb else mixRgb(shadow_rgb, page), track, win_w, win_h);
+ try addDecorExcept(g, gpa, plaque.box, &.{inner}, border_rgb, track, win_w, win_h);
if (pane and tag.active and chrome.decor_box_stripe != null)
try addPattern(g, gpa, inner[0], inner[1], inner[2], inner[3], chrome.decor_box_stripe.?, fill, dp(@floatFromInt(@max(1, chrome.decor_box_stripe_px))), true, track, win_w, win_h)
else
@@ -9309,6 +9346,50 @@ fn tagPlaques(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, g
};
}
+/// Flat decor over `rect` (window pixels) less each of `holes`: up to four
+/// strips round the first hole's part of it, each less the rest.
+fn addDecorExcept(g: *Gui, gpa: std.mem.Allocator, rect: [4]f32, holes: []const [4]f32, rgb: [3]u8, track: ?pardes.animation.Track, win_w: f32, win_h: f32) std.mem.Allocator.Error!void {
+ if (rect[0] >= rect[2] or rect[1] >= rect[3]) return;
+ if (holes.len == 0) return addDecor(g, gpa, rect[0], rect[1], rect[2], rect[3], rgb, 1, track, win_w, win_h);
+ const hole = holes[0];
+ const x0 = @max(rect[0], hole[0]);
+ const y0 = @max(rect[1], hole[1]);
+ const x1 = @min(rect[2], hole[2]);
+ const y1 = @min(rect[3], hole[3]);
+ if (x0 >= x1 or y0 >= y1) return addDecorExcept(g, gpa, rect, holes[1..], rgb, track, win_w, win_h);
+ for ([_][4]f32{
+ .{ rect[0], rect[1], rect[2], y0 },
+ .{ rect[0], y1, rect[2], rect[3] },
+ .{ rect[0], y0, x0, y1 },
+ .{ x1, y0, rect[2], y1 },
+ }) |strip| try addDecorExcept(g, gpa, strip, holes[1..], rgb, track, win_w, win_h);
+}
+
+test "decor less its holes covers each pixel of the rest once and none of the holes" {
+ const gpa = std.testing.allocator;
+ var g: Gui = undefined;
+ g.cell_w = 1;
+ g.cell_h = 1;
+ g.decor = .empty;
+ defer g.decor.deinit(gpa);
+ const holes = [_][4]f32{ .{ 3, 2, 9, 6 }, .{ 5, 4, 12, 8 }, .{ 30, 30, 40, 40 } };
+ try addDecorExcept(&g, gpa, .{ 1, 1, 11, 10 }, &holes, .{ 1, 2, 3 }, null, 16, 12);
+ for (0..12) |y| for (0..16) |x| {
+ const px: f32 = @as(f32, @floatFromInt(x)) + 0.5;
+ const py: f32 = @as(f32, @floatFromInt(y)) + 0.5;
+ var hits: usize = 0;
+ for (g.decor.items) |item| {
+ const r: [4]f32 = .{ (item.x0 + 1) / 2 * 16, (1 - item.y0) / 2 * 12, (item.x1 + 1) / 2 * 16, (1 - item.y1) / 2 * 12 };
+ hits += @intFromBool(px > r[0] and px < r[2] and py > r[1] and py < r[3]);
+ }
+ const in_rect = px > 1 and px < 11 and py > 1 and py < 10;
+ const in_hole = for (holes) |h| {
+ if (px > h[0] and px < h[2] and py > h[1] and py < h[3]) break true;
+ } else false;
+ try std.testing.expectEqual(@as(usize, @intFromBool(in_rect and !in_hole)), hits);
+ };
+}
+
const Plaque = struct { band: [4]f32, box: [4]f32, border: f32, shadow: [2]f32 };
/// A tag's plaque in window pixels: its band (the rows, from after the
diff --git a/src/themes/lapis_plain.zig b/src/themes/lapis_plain.zig
new file mode 100644
index 00000000..5b4c48dd
--- /dev/null
+++ b/src/themes/lapis_plain.zig
@@ -0,0 +1,81 @@
+//! Lapis plain: lapis (src/themes/lapis.zig) with flat grounds. The page
+//! under the text is lapis-deep with no dot grid, and the focused tag's
+//! plaque is its tint with no gold stripes behind its words; the rest is
+//! lapis's own: every colour, the gold frames and vermilion shadows of the
+//! tag plaques, the gold rules, the checker in the scroll column and the
+//! file name's vermilion offset shadow.
+pub const theme = .{
+ .name = "lapis_plain",
+ // --lapis-deep, --vellum
+ .bg = .{ 0x0a, 0x10, 0x30 },
+ .fg = .{ 0xee, 0xe6, 0xd2 },
+ // A gold highlight in lapis ink, as the directory's current entry.
+ .sel_bg = .{ 0xe8, 0xc4, 0x6a },
+ .sel_fg = .{ 0x0f, 0x1a, 0x4a },
+ // --lapis, --vellum-dim
+ .tag_bg = .{ 0x0f, 0x1a, 0x4a },
+ .tag_fg = .{ 0x9a, 0xa6, 0xd9 },
+ .tag_name_fg = .{ 0xe8, 0xc4, 0x6a },
+ .tag_active_name_fg = .{ 0xf2, 0xd2, 0x80 },
+ // The plaques' hover lapis, taken a step further: 1.5:1 off the tag.
+ .tag_active_bg = .{ 0x25, 0x39, 0x7d },
+ .tag_active_fg = .{ 0xee, 0xe6, 0xd2 },
+ .box = .{ 0xe8, 0xc4, 0x6a },
+ .box_dim = .{ 0x3a, 0x4a, 0x8a },
+ .box_border = .{ 0x9a, 0xa6, 0xd9 },
+ .box_dirty = .{ 0xff, 0x6a, 0x4a },
+ .column_box = .{ 0xff, 0x6a, 0x4a },
+ .column_box_dim = .{ 0x8a, 0x3a, 0x3a },
+ // Rubrics: vermilion keywords, gilded strings, the code window's green
+ // for numbers, vellum-dim comments.
+ .kw = .{ 0xff, 0x6a, 0x4a },
+ .str = .{ 0xe8, 0xc4, 0x6a },
+ .num = .{ 0xe7, 0xff, 0xe0 },
+ .comment = .{ 0x9a, 0xa6, 0xd9 },
+ .lineno = .{ 0x5a, 0x6a, 0xa8 },
+ .lineno_active = .{ 0x6a, 0x7a, 0xb8 },
+ // --lapis-ink under a gold thumb (the page's scrollbar-color).
+ .scroll_track = .{ 0x05, 0x0a, 0x24 },
+ .scroll_thumb = .{ 0xe8, 0xc4, 0x6a },
+ // Gold rules, a vermilion hairline between a tag and its text.
+ .border = .{ 0xe8, 0xc4, 0x6a },
+ // An empty column is the page, not a slab of gold leaf.
+ .empty_col = .{ 0x0a, 0x10, 0x30 },
+ .tag_rule = .{ 0xd4, 0x41, 0x2f },
+ .search_bg = .{ 0x6a, 0x2a, 0x1c },
+ .search_fg = .{ 0xff, 0xd0, 0xb0 },
+ .diagnostic_error = .{ 0xff, 0x6a, 0x4a },
+ .diagnostic_warning = .{ 0xe8, 0xc4, 0x6a },
+ .diagnostic_info = .{ 0x9a, 0xc6, 0xff },
+ .diagnostic_hint = .{ 0xa8, 0xe0, 0xc0 },
+ .decor = .{
+ .rail_checker = .{ 0xe8, 0xc4, 0x6a },
+ .rail_checker_px = 3,
+ // The trail: a 2px gold frame and a 5px vermilion shadow; the
+ // focused plaque filled flat.
+ .tag_border = .{ 0xe8, 0xc4, 0x6a },
+ .tag_border_px = 2,
+ .tag_shadow = .{ 0xd4, 0x41, 0x2f },
+ .tag_shadow_px = 5,
+ .title_shadow = .{ 0xd4, 0x41, 0x2f },
+ .title_shadow_px = 2,
+ },
+ .palette = .{
+ .{ 0x0a, 0x10, 0x30 },
+ .{ 0xff, 0x6a, 0x4a },
+ .{ 0xa8, 0xe0, 0xc0 },
+ .{ 0xe8, 0xc4, 0x6a },
+ .{ 0x9a, 0xc6, 0xff },
+ .{ 0xd8, 0xa0, 0xe8 },
+ .{ 0x8a, 0xd8, 0xd8 },
+ .{ 0xee, 0xe6, 0xd2 },
+ .{ 0x9a, 0xa6, 0xd9 },
+ .{ 0xff, 0x9a, 0x6a },
+ .{ 0xc8, 0xf0, 0xd8 },
+ .{ 0xff, 0xde, 0x90 },
+ .{ 0xc0, 0xdc, 0xff },
+ .{ 0xe8, 0xc0, 0xf4 },
+ .{ 0xb0, 0xec, 0xec },
+ .{ 0xff, 0xf8, 0xe8 },
+ },
+};
diff --git a/test/gui_monkey.py b/test/gui_monkey.py
index 2b32f050..540c377a 100644
--- a/test/gui_monkey.py
+++ b/test/gui_monkey.py
@@ -25,7 +25,7 @@ from fs import session # noqa: E402
MOTIONS = ['off', 'crisp', 'smooth', 'bouncy', 'playful']
LIFTS = ['off', 'shadow', 'rim', 'auto']
BLOOMS = ['0', '2']
-THEMES = ['lapis', 'forge', 'acme', 'dusk', 'daybreak']
+THEMES = ['lapis', 'lapis_plain', 'forge', 'acme', 'dusk', 'daybreak']
W, H = 1280, 840
diff --git a/test/snapshots/theme.golden b/test/snapshots/theme.golden
index 351f8905..cbd5ee61 100644
--- a/test/snapshots/theme.golden
+++ b/test/snapshots/theme.golden
@@ -586,6 +586,8 @@
|28: 0-0 d,#e8c46a, 1-49 d,#0a1030, 50-50 d,#e8c46a, 51-99 d,#0a1030,
|29: 0-0 d,#e8c46a, 1-49 d,#0a1030, 50-50 d,#e8c46a, 51-99 d,#0a1030,
|30: 0-0 d,#e8c46a, 1-49 d,#0a1030, 50-50 d,#e8c46a, 51-99 d,#0a1030,
+== snap lapis-plain grid=100x31 cursor=4,6
+== style lapis-plain grid=100x31
== snap dark-plus grid=100x31 cursor=4,6
== style dark-plus grid=100x31
|0: 0-75 #a7a7a7,#3c3c3c, 76-84 #d4d4d4,#264f78, 85-99 #d4d4d4,#3c3c3c,
diff --git a/test/snapshots/theme.snap b/test/snapshots/theme.snap
index 4cb360d3..2a046ef3 100644
--- a/test/snapshots/theme.snap
+++ b/test/snapshots/theme.snap
@@ -109,11 +109,16 @@ snapstyle lapis
press middle @NextColor 1
release middle @NextColor 1
stable 700 10000
+snap lapis-plain
+snapstyle lapis-plain
+press middle @NextColor 1
+release middle @NextColor 1
+stable 700 10000
snap dark-plus
snapstyle dark-plus
press middle @NextColor 1
release middle @NextColor 1
stable 700 10000
-# After orchard_black: acme and lapis, then the first faithful ports.
+# After orchard_black: acme, lapis and lapis_plain, then the first faithful ports.
snap solarized-dark
snapstyle solarized-dark
diff --git a/test/snapshots/themesel.golden b/test/snapshots/themesel.golden
index a82d88f5..253fc73a 100644
--- a/test/snapshots/themesel.golden
+++ b/test/snapshots/themesel.golden
@@ -482,7 +482,7 @@
|27: 16 Theme orchard_black
|28: 17 Theme acme
|29: 18 Theme lapis
-|30: 19
+|30: 19 Theme lapis_plain
== style step-orchard-black grid=100x31
|0: 0-99 #b2c3b8,#1d2824,
|1: 0-1 d,#887455, 2-2 d,#314738, 3-99 #becfc4,#314738,
@@ -514,7 +514,7 @@
|27: 0-0 d,#111813, 1-1 d,#000000, 2-3 d,#000000,b 4-6 #616f64,#000000,b 7-99 #f2f8ef,#000000,
|28: 0-0 d,#111813, 1-3 d,#000000, 4-6 #59675c,#000000, 7-99 #f2f8ef,#000000,
|29: 0-0 d,#111813, 1-3 d,#000000, 4-6 #59675c,#000000, 7-99 #f2f8ef,#000000,
-|30: 0-0 d,#111813, 1-3 d,#000000, 4-99 #59675c,#000000,
+|30: 0-0 d,#111813, 1-3 d,#000000, 4-6 #59675c,#000000, 7-99 #f2f8ef,#000000,
== snap step-acme grid=100x31 cursor=7,27
|17: 7 Theme atelier Theme
|18: 8 Theme forge
@@ -528,8 +528,8 @@
|26: 16 Theme orchard_black
|27: 17 Theme acme
|28: 18 Theme lapis
-|29: 19
-|30: 20 # Visual Studio Code
+|29: 19 Theme lapis_plain
+|30: 20
== style step-acme grid=100x31
|0: 0-99 #000000,#eaffff,
|1: 0-1 d,#8888cc, 2-2 d,#eaffff, 3-99 #000000,#eaffff,
@@ -560,8 +560,8 @@
|26: 0-0 d,#99994c, 1-3 d,#ffffea, 4-6 #999980,#ffffea, 7-99 #000000,#ffffea,
|27: 0-0 d,#99994c, 1-1 d,#ffffea, 2-3 d,#ffffea,b 4-6 #999980,#ffffea,b 7-99 #000000,#ffffea,
|28: 0-0 d,#99994c, 1-3 d,#ffffea, 4-6 #999980,#ffffea, 7-99 #000000,#ffffea,
-|29: 0-0 d,#99994c, 1-3 d,#ffffea, 4-99 #999980,#ffffea,
-|30: 0-0 d,#99994c, 1-3 d,#ffffea, 4-6 #999980,#ffffea, 7-99 #000000,#ffffea,
+|29: 0-0 d,#99994c, 1-3 d,#ffffea, 4-6 #999980,#ffffea, 7-99 #000000,#ffffea,
+|30: 0-0 d,#99994c, 1-3 d,#ffffea, 4-99 #999980,#ffffea,
== snap step-lapis grid=100x31 cursor=7,27
|17: 8 Theme forge Theme
|18: 9 Theme lagoon
@@ -574,9 +574,9 @@
|25: 16 Theme orchard_black
|26: 17 Theme acme
|27: 18 Theme lapis
-|28: 19
-|29: 20 # Visual Studio Code
-|30: 21 Theme dark_plus
+|28: 19 Theme lapis_plain
+|29: 20
+|30: 21 # Visual Studio Code
== style step-lapis grid=100x31
|0: 0-99 #9aa6d9,#0f1a4a,
|1: 0-1 d,#8a3a3a, 2-2 d,#25397d, 3-99 #eee6d2,#25397d,
@@ -606,24 +606,43 @@
|25: 0-0 d,#050a24, 1-3 d,#0a1030, 4-6 #5a6aa8,#0a1030, 7-99 #eee6d2,#0a1030,
|26: 0-0 d,#050a24, 1-3 d,#0a1030, 4-6 #5a6aa8,#0a1030, 7-99 #eee6d2,#0a1030,
|27: 0-0 d,#050a24, 1-1 d,#0a1030, 2-3 d,#0a1030,b 4-6 #6a7ab8,#0a1030,b 7-99 #eee6d2,#0a1030,
+|28: 0-0 d,#050a24, 1-3 d,#0a1030, 4-6 #5a6aa8,#0a1030, 7-99 #eee6d2,#0a1030,
+|29: 0-0 d,#050a24, 1-3 d,#0a1030, 4-99 #5a6aa8,#0a1030,
+|30: 0-0 d,#050a24, 1-3 d,#0a1030, 4-6 #5a6aa8,#0a1030, 7-99 #eee6d2,#0a1030,
+== snap step-lapis-plain grid=100x31 cursor=7,27
+|17: 9 Theme lagoon Theme
+|18: 10 Theme solarium
+|19: 11 Theme spectrum
+|20: 12 Theme harvest
+|21: 13 Theme clay
+|22: 14 Theme forge_black
+|23: 15 Theme forge_soft
+|24: 16 Theme orchard_black
+|25: 17 Theme acme
+|26: 18 Theme lapis
+|27: 19 Theme lapis_plain
+|28: 20
+|29: 21 # Visual Studio Code
+|30: 22 Theme dark_plus
+== style step-lapis-plain grid=100x31
+|18: 0-0 d,#050a24, 1-3 d,#0a1030, 4-6 #5a6aa8,#0a1030, 7-99 #eee6d2,#0a1030,
|28: 0-0 d,#050a24, 1-3 d,#0a1030, 4-99 #5a6aa8,#0a1030,
|29: 0-0 d,#050a24, 1-3 d,#0a1030, 4-6 #5a6aa8,#0a1030, 7-99 #eee6d2,#0a1030,
-|30: 0-0 d,#050a24, 1-3 d,#0a1030, 4-6 #5a6aa8,#0a1030, 7-99 #eee6d2,#0a1030,
== snap step-dark-plus grid=100x31 cursor=7,27
-|17: 11 Theme spectrum Theme
-|18: 12 Theme harvest
-|19: 13 Theme clay
-|20: 14 Theme forge_black
-|21: 15 Theme forge_soft
-|22: 16 Theme orchard_black
-|23: 17 Theme acme
-|24: 18 Theme lapis
-|25: 19
-|26: 20 # Visual Studio Code
-|27: 21 Theme dark_plus
-|28: 22
-|29: 23 # Solarized
-|30: 24 Theme solarized_dark
+|17: 12 Theme harvest Theme
+|18: 13 Theme clay
+|19: 14 Theme forge_black
+|20: 15 Theme forge_soft
+|21: 16 Theme orchard_black
+|22: 17 Theme acme
+|23: 18 Theme lapis
+|24: 19 Theme lapis_plain
+|25: 20
+|26: 21 # Visual Studio Code
+|27: 22 Theme dark_plus
+|28: 23
+|29: 24 # Solarized
+|30: 25 Theme solarized_dark
== style step-dark-plus grid=100x31
|0: 0-99 #a7a7a7,#3c3c3c,
|1: 0-1 d,#458276, 2-2 d,#1e1e1e, 3-99 #ffffff,#1e1e1e,
@@ -657,20 +676,20 @@
|29: 0-3 d,#1e1e1e, 4-6 #858585,#1e1e1e, 7-99 #d4d4d4,#1e1e1e,
|30: 0-3 d,#1e1e1e, 4-6 #858585,#1e1e1e, 7-99 #d4d4d4,#1e1e1e,
== snap step-solarized-dark grid=100x31 cursor=7,27
-|17: 14 Theme forge_black Theme
-|18: 15 Theme forge_soft
-|19: 16 Theme orchard_black
-|20: 17 Theme acme
-|21: 18 Theme lapis
-|22: 19
-|23: 20 # Visual Studio Code
-|24: 21 Theme dark_plus
-|25: 22
-|26: 23 # Solarized
-|27: 24 Theme solarized_dark
-|28: 25 Theme solarized_light
-|29: 26
-|30: 27 # Dracula
+|17: 15 Theme forge_soft Theme
+|18: 16 Theme orchard_black
+|19: 17 Theme acme
+|20: 18 Theme lapis
+|21: 19 Theme lapis_plain
+|22: 20
+|23: 21 # Visual Studio Code
+|24: 22 Theme dark_plus
+|25: 23
+|26: 24 # Solarized
+|27: 25 Theme solarized_dark
+|28: 26 Theme solarized_light
+|29: 27
+|30: 28 # Dracula
== style step-solarized-dark grid=100x31
|0: 0-99 #01090b,#657b83,
|1: 0-1 d,#758a41, 2-2 d,#93a1a1, 3-99 #073642,#93a1a1,
@@ -737,20 +756,20 @@
|29: 0-3 d,#1e1e1e, 4-99 #858585,#1e1e1e,
|30: 0-3 d,#1e1e1e, 4-6 #858585,#1e1e1e, 7-99 #d4d4d4,#1e1e1e,
== snap tail grid=100x31 cursor=7,29
-|17: 326 Theme wolf_alabaster_dark
-|18: 327 Theme wolf_alabaster_dark_bg
-|19: 328 Theme wolf_alabaster_dark_mono
-|20: 329 Theme wolf_alabaster_light
-|21: 330 Theme wolf_alabaster_light_bg
-|22: 331 Theme wolf_alabaster_light_mono
-|23: 332 Theme yellowed
-|24: 333 Theme yo
-|25: 334 Theme yo_berry
-|26: 335 Theme yo_light
-|27: 336 Theme zed_onedark
-|28: 337 Theme zed_onelight
-|29: 338 Theme zenburn
-|30: 339
+|17: 327 Theme wolf_alabaster_dark
+|18: 328 Theme wolf_alabaster_dark_bg
+|19: 329 Theme wolf_alabaster_dark_mono
+|20: 330 Theme wolf_alabaster_light
+|21: 331 Theme wolf_alabaster_light_bg
+|22: 332 Theme wolf_alabaster_light_mono
+|23: 333 Theme yellowed
+|24: 334 Theme yo
+|25: 335 Theme yo_berry
+|26: 336 Theme yo_light
+|27: 337 Theme zed_onedark
+|28: 338 Theme zed_onelight
+|29: 339 Theme zenburn
+|30: 340
== snap picked grid=100x31 cursor=7,3
|16: 14 line 14
|17: 15 line 15
diff --git a/test/snapshots/themesel.snap b/test/snapshots/themesel.snap
index 01e4dc5e..1d61bf6a 100644
--- a/test/snapshots/themesel.snap
+++ b/test/snapshots/themesel.snap
@@ -11,7 +11,7 @@
# and nothing to cancel: the row you ran is the theme you have.
#
# The whole ring is in the buffer, in sections: the fifteen native themes,
-# acme and lapis, the faithful ports a family at a time, the legacy helix and
+# acme, lapis and lapis_plain, the faithful ports a family at a time, the legacy helix and
# dark, then everything exported from the vendored helix .toml and zed .json
# files, sorted. The colors below are
# read with snapstyle, so this proves the generated themes are real data and
@@ -82,7 +82,7 @@ key tab
stable 700 15000
snap step-clay
snapstyle step-clay
-# The three native contrast variants precede acme and lapis, and then the first
+# The three native contrast variants precede acme, lapis and lapis_plain, and then the first
# ported family's section. Its heading
# and blank separator are not stops in the n/N command walk.
key n
@@ -113,6 +113,11 @@ snapstyle step-lapis
key n
key tab
stable 700 15000
+snap step-lapis-plain
+snapstyle step-lapis-plain
+key n
+key tab
+stable 700 15000
snap step-dark-plus
snapstyle step-dark-plus
# across the Visual Studio Code section into Solarized: a section heading and