Add newed night theme model
This commit is contained in:
@@ -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
@@ -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;"));
|
||||
}
|
||||
Reference in New Issue
Block a user