Add newed night theme model

This commit is contained in:
slhx agent
2026-06-21 21:31:20 +02:00
parent 9ef2f97423
commit 5652845d2e
2 changed files with 294 additions and 0 deletions
+1
View File
@@ -18,6 +18,7 @@ const socket = @import("socket.zig");
const symbol = @import("symbol.zig");
const syntax = @import("syntax.zig");
const terminal_debug = @import("terminal_debug.zig");
const theme = @import("theme.zig");
const tui = @import("tui.zig");
pub const version = "0.1.0-dev";
+293
View File
@@ -0,0 +1,293 @@
const std = @import("std");
/// Source-patched from newed night:
/// - /opt/repositories/newed/newed-theme/src/default_theme.rs
/// - /opt/repositories/newed/newed-theme/src/adapt.rs
///
/// This is intentionally a tiny local model, not a runtime theme/config system.
pub const Color = struct {
r: u8,
g: u8,
b: u8,
};
pub const Token = enum {
normal,
muted,
accent,
selection,
panel_bg,
panel_border,
cursor,
diagnostic_error,
diagnostic_warning,
diagnostic_info,
diagnostic_hint,
};
pub const Style = struct {
fg: ?Color = null,
bg: ?Color = null,
bold: bool = false,
italic: bool = false,
};
pub const ColorMode = enum {
truecolor,
color256,
color16,
mono,
};
pub const SgrKind = enum { fg, bg };
pub const AdaptedColor = union(enum) {
rgb: Color,
indexed: u8,
reset,
};
pub const Palette = struct {
normal: Style,
muted: Style,
accent: Style,
selection: Style,
panel_bg: Style,
panel_border: Style,
cursor: Style,
diagnostic_error: Style,
diagnostic_warning: Style,
diagnostic_info: Style,
diagnostic_hint: Style,
pub fn style(self: Palette, token: Token) Style {
return switch (token) {
.normal => self.normal,
.muted => self.muted,
.accent => self.accent,
.selection => self.selection,
.panel_bg => self.panel_bg,
.panel_border => self.panel_border,
.cursor => self.cursor,
.diagnostic_error => self.diagnostic_error,
.diagnostic_warning => self.diagnostic_warning,
.diagnostic_info => self.diagnostic_info,
.diagnostic_hint => self.diagnostic_hint,
};
}
};
// RGB approximations of the named OKLCH anchors in newed default_theme.rs.
// The source anchors remain semantic: sumi-ink base, moonlight foreground,
// beni/coral accent, cool water selection, and distinct diagnostics.
pub const newed_night = Palette{
.normal = .{ .fg = rgb(230, 230, 230), .bg = rgb(25, 22, 33) },
.muted = .{ .fg = rgb(103, 93, 122), .bg = rgb(25, 22, 33) },
.accent = .{ .fg = rgb(235, 119, 84), .bold = true },
.selection = .{ .bg = rgb(55, 65, 92) },
.panel_bg = .{ .fg = rgb(230, 230, 230), .bg = rgb(20, 18, 26) },
.panel_border = .{ .fg = rgb(72, 67, 84), .bg = rgb(25, 22, 33) },
.cursor = .{ .fg = rgb(25, 22, 33), .bg = rgb(230, 230, 230) },
.diagnostic_error = .{ .fg = rgb(238, 84, 88), .bold = true },
.diagnostic_warning = .{ .fg = rgb(231, 168, 78), .bold = true },
.diagnostic_info = .{ .fg = rgb(121, 155, 224) },
.diagnostic_hint = .{ .fg = rgb(116, 196, 171) },
};
pub fn rgb(r: u8, g: u8, b: u8) Color {
return .{ .r = r, .g = g, .b = b };
}
pub fn adaptColor(color: Color, mode: ColorMode) AdaptedColor {
return switch (mode) {
.truecolor => .{ .rgb = color },
.color256 => .{ .indexed = nearest256(color) },
.color16 => .{ .indexed = nearest16(color) },
.mono => .reset,
};
}
pub fn appendStyleSgr(writer: *std.Io.Writer, style: Style, mode: ColorMode) !void {
try writer.writeAll("\x1b[0m");
if (mode == .mono) {
if (style.bold) try writer.writeAll("\x1b[1m");
if (style.italic) try writer.writeAll("\x1b[3m");
return;
}
if (style.bold) try writer.writeAll("\x1b[1m");
if (style.italic) try writer.writeAll("\x1b[3m");
if (style.fg) |fg| try appendColorSgr(writer, .fg, fg, mode);
if (style.bg) |bg| try appendColorSgr(writer, .bg, bg, mode);
}
pub fn appendColorSgr(writer: *std.Io.Writer, kind: SgrKind, color: Color, mode: ColorMode) !void {
switch (adaptColor(color, mode)) {
.rgb => |c| try writer.print("\x1b[{d};2;{d};{d};{d}m", .{ sgrBase(kind), c.r, c.g, c.b }),
.indexed => |index| try writer.print("\x1b[{d};5;{d}m", .{ sgrBase(kind), index }),
.reset => {},
}
}
pub fn resetSgr() []const u8 {
return "\x1b[0m";
}
pub fn wrap(allocator: std.mem.Allocator, text: []const u8, style: Style, mode: ColorMode) ![]u8 {
var out: std.Io.Writer.Allocating = .init(allocator);
defer out.deinit();
try appendStyleSgr(&out.writer, style, mode);
try out.writer.writeAll(text);
try out.writer.writeAll(resetSgr());
return out.toOwnedSlice();
}
pub fn contrastRatio(foreground: Color, background: Color) f32 {
const l1 = relativeLuminance(foreground);
const l2 = relativeLuminance(background);
const lighter = @max(l1, l2);
const darker = @min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
fn sgrBase(kind: SgrKind) u8 {
return switch (kind) {
.fg => 38,
.bg => 48,
};
}
fn nearest16(color: Color) u8 {
return nearestInTable(color, ansi16[0..]);
}
fn nearest256(color: Color) u8 {
var best_index: u8 = 0;
var best_distance: u32 = std.math.maxInt(u32);
var i: usize = 0;
while (i < 256) : (i += 1) {
const candidate = xterm256(@intCast(i));
const distance = distanceSquared(color, candidate);
if (distance < best_distance) {
best_distance = distance;
best_index = @intCast(i);
}
}
return best_index;
}
fn nearestInTable(color: Color, table: []const Color) u8 {
var best_index: u8 = 0;
var best_distance: u32 = std.math.maxInt(u32);
for (table, 0..) |candidate, index| {
const distance = distanceSquared(color, candidate);
if (distance < best_distance) {
best_distance = distance;
best_index = @intCast(index);
}
}
return best_index;
}
fn distanceSquared(a: Color, b: Color) u32 {
const dr: i32 = @as(i32, a.r) - @as(i32, b.r);
const dg: i32 = @as(i32, a.g) - @as(i32, b.g);
const db: i32 = @as(i32, a.b) - @as(i32, b.b);
return @intCast(dr * dr + dg * dg + db * db);
}
fn xterm256(index: u8) Color {
if (index < 16) return ansi16[index];
if (index >= 232) {
const level: u8 = 8 + 10 * (index - 232);
return rgb(level, level, level);
}
const cube = index - 16;
const r = cube / 36;
const g = (cube % 36) / 6;
const b = cube % 6;
return rgb(cubeLevel(r), cubeLevel(g), cubeLevel(b));
}
fn cubeLevel(value: u8) u8 {
return if (value == 0) 0 else 55 + value * 40;
}
fn relativeLuminance(color: Color) f32 {
const r = linearize(@as(f32, @floatFromInt(color.r)) / 255.0);
const g = linearize(@as(f32, @floatFromInt(color.g)) / 255.0);
const b = linearize(@as(f32, @floatFromInt(color.b)) / 255.0);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
fn linearize(channel: f32) f32 {
if (channel <= 0.03928) return channel / 12.92;
return std.math.pow(f32, (channel + 0.055) / 1.055, 2.4);
}
const ansi16 = [_]Color{
rgb(0, 0, 0),
rgb(128, 0, 0),
rgb(0, 128, 0),
rgb(128, 128, 0),
rgb(0, 0, 128),
rgb(128, 0, 128),
rgb(0, 128, 128),
rgb(192, 192, 192),
rgb(128, 128, 128),
rgb(255, 0, 0),
rgb(0, 255, 0),
rgb(255, 255, 0),
rgb(0, 0, 255),
rgb(255, 0, 255),
rgb(0, 255, 255),
rgb(255, 255, 255),
};
test "regular: default palette exposes semantic newed night tokens" {
try std.testing.expectEqual(rgb(25, 22, 33), newed_night.normal.bg.?);
try std.testing.expectEqual(rgb(230, 230, 230), newed_night.normal.fg.?);
try std.testing.expectEqual(rgb(235, 119, 84), newed_night.style(.accent).fg.?);
try std.testing.expect(contrastRatio(newed_night.normal.fg.?, newed_night.normal.bg.?) > 10.0);
}
test "regular: color adaptation is deterministic across terminal modes" {
const coral = newed_night.accent.fg.?;
try std.testing.expectEqual(AdaptedColor{ .rgb = coral }, adaptColor(coral, .truecolor));
try std.testing.expectEqual(AdaptedColor{ .indexed = nearest256(coral) }, adaptColor(coral, .color256));
try std.testing.expectEqual(AdaptedColor{ .indexed = nearest16(coral) }, adaptColor(coral, .color16));
try std.testing.expectEqual(AdaptedColor.reset, adaptColor(coral, .mono));
}
test "regular: SGR output resets and does not leak styles" {
const rendered = try wrap(std.testing.allocator, "kw", newed_night.accent, .truecolor);
defer std.testing.allocator.free(rendered);
try std.testing.expect(std.mem.startsWith(u8, rendered, resetSgr()));
try std.testing.expect(std.mem.indexOf(u8, rendered, "\x1b[1m") != null);
try std.testing.expect(std.mem.indexOf(u8, rendered, "\x1b[38;2;235;119;84m") != null);
try std.testing.expect(std.mem.endsWith(u8, rendered, resetSgr()));
}
test "regular: mono preserves emphasis without color" {
var out: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer out.deinit();
try appendStyleSgr(&out.writer, newed_night.accent, .mono);
const rendered = try out.toOwnedSlice();
defer std.testing.allocator.free(rendered);
try std.testing.expectEqualStrings("\x1b[0m\x1b[1m", rendered);
}
test "regular: 256 and 16 color SGR use indexed form" {
var out256: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer out256.deinit();
try appendColorSgr(&out256.writer, .fg, newed_night.diagnostic_warning.fg.?, .color256);
const rendered256 = try out256.toOwnedSlice();
defer std.testing.allocator.free(rendered256);
try std.testing.expect(std.mem.startsWith(u8, rendered256, "\x1b[38;5;"));
var out16: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer out16.deinit();
try appendColorSgr(&out16.writer, .bg, newed_night.selection.bg.?, .color16);
const rendered16 = try out16.toOwnedSlice();
defer std.testing.allocator.free(rendered16);
try std.testing.expect(std.mem.startsWith(u8, rendered16, "\x1b[48;5;"));
}