mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-19 22:10:29 +00:00
improve light theme detection
This commit is contained in:
committed by
Mitchell Hashimoto
parent
c4c87f8c85
commit
f66a84b18a
@@ -808,9 +808,21 @@ palette: Palette = .{},
|
||||
/// Available since: 1.3.0
|
||||
@"palette-generate": bool = true,
|
||||
|
||||
/// Whether to invert generated light themes colors (see `palette-generate`).
|
||||
/// This helps give the 256-color palette more semantic meaning.
|
||||
///
|
||||
/// Invert the palette colors generated when `palette-generate` is enabled,
|
||||
/// so that the colors go in reverse order. This allows palette-based
|
||||
/// applications to work well in both light and dark mode since the
|
||||
/// palettes are always relatively good colors.
|
||||
///
|
||||
/// This defaults to off because some legacy terminal applications
|
||||
/// hardcode the assumption that palette indices 16–231 are ordered from
|
||||
/// darkest to lightest, so enabling this would make them unreadable.
|
||||
/// This is not a generally good assumption and we encourage modern
|
||||
/// terminal applications to use the indices in a more semantic way.
|
||||
///
|
||||
/// This has no effect if `palette-generate` is disabled.
|
||||
///
|
||||
/// For more information see `palette-generate`.
|
||||
///
|
||||
/// Available since: 1.3.0
|
||||
@"palette-harmonious": bool = false,
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
const std = @import("std");
|
||||
const colorpkg = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const x11_color = @import("x11_color.zig");
|
||||
|
||||
const colorpkg = @This();
|
||||
|
||||
/// The default palette.
|
||||
pub const default: Palette = default: {
|
||||
var result: Palette = undefined;
|
||||
@@ -91,25 +90,32 @@ pub fn generate256Color(
|
||||
skip: PaletteMask,
|
||||
bg: RGB,
|
||||
fg: RGB,
|
||||
harmonious: bool
|
||||
harmonious: bool,
|
||||
) Palette {
|
||||
// Convert the background, foreground, and 8 base theme colors into
|
||||
// CIELAB space so that all interpolation is perceptually uniform.
|
||||
const bg_lab: LAB = .fromRgb(bg);
|
||||
const fg_lab: LAB = .fromRgb(fg);
|
||||
const base8_lab: [8]LAB = base8: {
|
||||
var base8: [8]LAB = .{
|
||||
.fromRgb(bg),
|
||||
LAB.fromRgb(base[1]),
|
||||
LAB.fromRgb(base[2]),
|
||||
LAB.fromRgb(base[3]),
|
||||
LAB.fromRgb(base[4]),
|
||||
LAB.fromRgb(base[5]),
|
||||
LAB.fromRgb(base[6]),
|
||||
.fromRgb(fg),
|
||||
};
|
||||
|
||||
const is_light_theme = bg_lab.l > 50;
|
||||
const invert = is_light_theme and !harmonious;
|
||||
// For light themes (where the foreground is darker than the
|
||||
// background), the cube's dark-to-light orientation is inverted
|
||||
// relative to the base color mapping. When `harmonious` is false,
|
||||
// swap bg and fg so the cube still runs from black (16) to
|
||||
// white (231).
|
||||
const is_light_theme = base8[7].l < base8[0].l;
|
||||
const invert = is_light_theme and !harmonious;
|
||||
if (invert) std.mem.swap(LAB, &base8[0], &base8[7]);
|
||||
|
||||
const base8_lab: [8]LAB = .{
|
||||
if (invert) fg_lab else bg_lab,
|
||||
LAB.fromRgb(base[1]),
|
||||
LAB.fromRgb(base[2]),
|
||||
LAB.fromRgb(base[3]),
|
||||
LAB.fromRgb(base[4]),
|
||||
LAB.fromRgb(base[5]),
|
||||
LAB.fromRgb(base[6]),
|
||||
if (invert) bg_lab else fg_lab,
|
||||
break :base8 base8;
|
||||
};
|
||||
|
||||
// Start from the base palette so indices 0–15 are preserved as-is.
|
||||
@@ -937,7 +943,7 @@ test "generate256Color: base16 preserved" {
|
||||
|
||||
const bg = RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
const fg = RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const palette = generate256Color(default, .initEmpty(), bg, fg);
|
||||
const palette = generate256Color(default, .initEmpty(), bg, fg, false);
|
||||
|
||||
// The first 16 colors (base16) must remain unchanged.
|
||||
for (0..16) |i| {
|
||||
@@ -950,7 +956,7 @@ test "generate256Color: cube corners match base colors" {
|
||||
|
||||
const bg = RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
const fg = RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const palette = generate256Color(default, .initEmpty(), bg, fg);
|
||||
const palette = generate256Color(default, .initEmpty(), bg, fg, false);
|
||||
|
||||
// Index 16 is cube (0,0,0) which should equal bg.
|
||||
try testing.expectEqual(bg, palette[16]);
|
||||
@@ -959,12 +965,43 @@ test "generate256Color: cube corners match base colors" {
|
||||
try testing.expectEqual(fg, palette[231]);
|
||||
}
|
||||
|
||||
test "generate256Color: cube corners black/white with harmonious=false" {
|
||||
const testing = std.testing;
|
||||
|
||||
const black = RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
const white = RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
|
||||
// Dark theme: bg=black, fg=white.
|
||||
const dark = generate256Color(default, .initEmpty(), black, white, false);
|
||||
try testing.expectEqual(black, dark[16]);
|
||||
try testing.expectEqual(white, dark[231]);
|
||||
|
||||
// Light theme: bg=white, fg=black. The bg/red swap ensures
|
||||
// the cube still runs from black (16) to white (231).
|
||||
const light = generate256Color(default, .initEmpty(), white, black, false);
|
||||
try testing.expectEqual(black, light[16]);
|
||||
try testing.expectEqual(white, light[231]);
|
||||
}
|
||||
|
||||
test "generate256Color: light theme cube corners with harmonious=true" {
|
||||
const testing = std.testing;
|
||||
|
||||
const white = RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const black = RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
|
||||
// harmonious=true skips the bg/fg swap, so the cube preserves the
|
||||
// original orientation: (0,0,0)=bg=white, (5,5,5)=fg=black.
|
||||
const palette = generate256Color(default, .initEmpty(), white, black, true);
|
||||
try testing.expectEqual(white, palette[16]);
|
||||
try testing.expectEqual(black, palette[231]);
|
||||
}
|
||||
|
||||
test "generate256Color: grayscale ramp monotonic luminance" {
|
||||
const testing = std.testing;
|
||||
|
||||
const bg = RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
const fg = RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const palette = generate256Color(default, .initEmpty(), bg, fg);
|
||||
const palette = generate256Color(default, .initEmpty(), bg, fg, false);
|
||||
|
||||
// The grayscale ramp (232–255) should have monotonically increasing
|
||||
// luminance from near-black to near-white.
|
||||
@@ -988,7 +1025,7 @@ test "generate256Color: skip mask preserves original colors" {
|
||||
skip.set(100);
|
||||
skip.set(240);
|
||||
|
||||
const palette = generate256Color(default, skip, bg, fg);
|
||||
const palette = generate256Color(default, skip, bg, fg, false);
|
||||
try testing.expectEqual(default[20], palette[20]);
|
||||
try testing.expectEqual(default[100], palette[100]);
|
||||
try testing.expectEqual(default[240], palette[240]);
|
||||
@@ -997,6 +1034,73 @@ test "generate256Color: skip mask preserves original colors" {
|
||||
try testing.expect(!palette[21].eql(default[21]));
|
||||
}
|
||||
|
||||
test "generate256Color: dark theme harmonious has no effect" {
|
||||
const testing = std.testing;
|
||||
|
||||
// For a dark theme (fg lighter than bg), harmonious should not change
|
||||
// the output because the inversion is only relevant for light themes.
|
||||
const bg = RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
const fg = RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const normal = generate256Color(default, .initEmpty(), bg, fg, false);
|
||||
const harmonious = generate256Color(default, .initEmpty(), bg, fg, true);
|
||||
|
||||
for (16..256) |i| {
|
||||
try testing.expectEqual(normal[i], harmonious[i]);
|
||||
}
|
||||
}
|
||||
|
||||
test "generate256Color: light theme harmonious skips inversion" {
|
||||
const testing = std.testing;
|
||||
|
||||
// For a light theme (fg darker than bg), harmonious=true skips the
|
||||
// bg/red swap, producing different cube colors than harmonious=false.
|
||||
const bg = RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const fg = RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
const inverted = generate256Color(default, .initEmpty(), bg, fg, false);
|
||||
const harmonious = generate256Color(default, .initEmpty(), bg, fg, true);
|
||||
|
||||
// Cube origin (0,0,0) at index 16: without harmonious, bg and red are
|
||||
// swapped so it becomes the red base; with harmonious it stays as bg.
|
||||
try testing.expectEqual(bg, harmonious[16]);
|
||||
try testing.expect(!inverted[16].eql(bg));
|
||||
|
||||
// At least some cube colors should differ between the two modes.
|
||||
var differ: usize = 0;
|
||||
for (16..232) |i| {
|
||||
if (!inverted[i].eql(harmonious[i])) differ += 1;
|
||||
}
|
||||
try testing.expect(differ > 0);
|
||||
}
|
||||
|
||||
test "generate256Color: light theme harmonious grayscale ramp" {
|
||||
const testing = std.testing;
|
||||
|
||||
const bg = RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const fg = RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
|
||||
// harmonious=false swaps bg/fg, so the ramp runs black→white (increasing).
|
||||
{
|
||||
const palette = generate256Color(default, .initEmpty(), bg, fg, false);
|
||||
var prev_lum: f64 = 0.0;
|
||||
for (232..256) |i| {
|
||||
const lum = palette[i].luminance();
|
||||
try testing.expect(lum >= prev_lum);
|
||||
prev_lum = lum;
|
||||
}
|
||||
}
|
||||
|
||||
// harmonious=true keeps original order, so the ramp runs white→black (decreasing).
|
||||
{
|
||||
const palette = generate256Color(default, .initEmpty(), bg, fg, true);
|
||||
var prev_lum: f64 = 1.0;
|
||||
for (232..256) |i| {
|
||||
const lum = palette[i].luminance();
|
||||
try testing.expect(lum <= prev_lum);
|
||||
prev_lum = lum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "LAB.toRgb" {
|
||||
const testing = std.testing;
|
||||
|
||||
|
||||
@@ -184,13 +184,7 @@ pub const DerivedConfig = struct {
|
||||
break :generate;
|
||||
}
|
||||
|
||||
break :palette terminalpkg.color.generate256Color(
|
||||
config.palette.value,
|
||||
config.palette.mask,
|
||||
config.background.toTerminalRGB(),
|
||||
config.foreground.toTerminalRGB(),
|
||||
config.@"palette-harmonious"
|
||||
);
|
||||
break :palette terminalpkg.color.generate256Color(config.palette.value, config.palette.mask, config.background.toTerminalRGB(), config.foreground.toTerminalRGB(), config.@"palette-harmonious");
|
||||
}
|
||||
|
||||
break :palette config.palette.value;
|
||||
|
||||
Reference in New Issue
Block a user