remove hand maintenance of theme data, use comptime introspection
This commit is contained in:
parent
433b83e78f
commit
a2c8d478b6
1 changed files with 34 additions and 50 deletions
|
|
@ -197,56 +197,17 @@ pub const default_theme = Theme{
|
||||||
|
|
||||||
// ── SRF serialization ────────────────────────────────────────
|
// ── SRF serialization ────────────────────────────────────────
|
||||||
|
|
||||||
const field_names = [_]struct { name: []const u8, offset: usize }{
|
|
||||||
.{ .name = "bg", .offset = @offsetOf(Theme, "bg") },
|
|
||||||
.{ .name = "bg_panel", .offset = @offsetOf(Theme, "bg_panel") },
|
|
||||||
.{ .name = "bg_element", .offset = @offsetOf(Theme, "bg_element") },
|
|
||||||
.{ .name = "tab_bg", .offset = @offsetOf(Theme, "tab_bg") },
|
|
||||||
.{ .name = "tab_fg", .offset = @offsetOf(Theme, "tab_fg") },
|
|
||||||
.{ .name = "tab_active_bg", .offset = @offsetOf(Theme, "tab_active_bg") },
|
|
||||||
.{ .name = "tab_active_fg", .offset = @offsetOf(Theme, "tab_active_fg") },
|
|
||||||
.{ .name = "text", .offset = @offsetOf(Theme, "text") },
|
|
||||||
.{ .name = "text_muted", .offset = @offsetOf(Theme, "text_muted") },
|
|
||||||
.{ .name = "text_dim", .offset = @offsetOf(Theme, "text_dim") },
|
|
||||||
.{ .name = "status_bg", .offset = @offsetOf(Theme, "status_bg") },
|
|
||||||
.{ .name = "status_fg", .offset = @offsetOf(Theme, "status_fg") },
|
|
||||||
.{ .name = "input_bg", .offset = @offsetOf(Theme, "input_bg") },
|
|
||||||
.{ .name = "input_fg", .offset = @offsetOf(Theme, "input_fg") },
|
|
||||||
.{ .name = "input_hint", .offset = @offsetOf(Theme, "input_hint") },
|
|
||||||
.{ .name = "accent", .offset = @offsetOf(Theme, "accent") },
|
|
||||||
.{ .name = "positive", .offset = @offsetOf(Theme, "positive") },
|
|
||||||
.{ .name = "negative", .offset = @offsetOf(Theme, "negative") },
|
|
||||||
.{ .name = "warning", .offset = @offsetOf(Theme, "warning") },
|
|
||||||
.{ .name = "info", .offset = @offsetOf(Theme, "info") },
|
|
||||||
.{ .name = "select_bg", .offset = @offsetOf(Theme, "select_bg") },
|
|
||||||
.{ .name = "select_fg", .offset = @offsetOf(Theme, "select_fg") },
|
|
||||||
.{ .name = "border", .offset = @offsetOf(Theme, "border") },
|
|
||||||
.{ .name = "bar_fill", .offset = @offsetOf(Theme, "bar_fill") },
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Schema contract for `theme.srf`. See `srf_lint.validateSchema`.
|
/// Schema contract for `theme.srf`. See `srf_lint.validateSchema`.
|
||||||
///
|
///
|
||||||
/// `Record = Theme` rather than a parallel name list: `loadFromData`
|
/// `Record = Theme`: the loader, `printDefaults` and this lint all take
|
||||||
/// matches keys against the hand-maintained `field_names` table above,
|
/// the key set from `std.meta.fields(Theme)`, so a new color field is
|
||||||
/// but the authoritative set is `std.meta.fields(Theme)`, and deriving
|
/// loadable, printed and linted with no second list to update.
|
||||||
/// from the struct means the lint cannot disagree with the type even if
|
|
||||||
/// `field_names` falls behind it.
|
|
||||||
pub const srf_schema = struct {
|
pub const srf_schema = struct {
|
||||||
pub const Record = Theme;
|
pub const Record = Theme;
|
||||||
pub const file_label: []const u8 = "theme.srf";
|
pub const file_label: []const u8 = "theme.srf";
|
||||||
pub const doc_path: []const u8 = "reference/config/theme-srf.md";
|
pub const doc_path: []const u8 = "reference/config/theme-srf.md";
|
||||||
};
|
};
|
||||||
|
|
||||||
fn colorPtr(theme: *Theme, offset: usize) *Color {
|
|
||||||
const bytes: [*]u8 = @ptrCast(theme);
|
|
||||||
return @ptrCast(@alignCast(bytes + offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn colorPtrConst(theme: *const Theme, offset: usize) *const Color {
|
|
||||||
const bytes: [*]const u8 = @ptrCast(theme);
|
|
||||||
return @ptrCast(@alignCast(bytes + offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn formatHex(c: Color) [7]u8 {
|
fn formatHex(c: Color) [7]u8 {
|
||||||
var buf: [7]u8 = undefined;
|
var buf: [7]u8 = undefined;
|
||||||
_ = std.fmt.bufPrint(&buf, "#{x:0>2}{x:0>2}{x:0>2}", .{ c[0], c[1], c[2] }) catch
|
_ = std.fmt.bufPrint(&buf, "#{x:0>2}{x:0>2}{x:0>2}", .{ c[0], c[1], c[2] }) catch
|
||||||
|
|
@ -276,9 +237,10 @@ pub fn printDefaults(io: std.Io) !void {
|
||||||
try out.writeAll("#\n");
|
try out.writeAll("#\n");
|
||||||
try out.writeAll("# All values are hex RGB: #rrggbb\n");
|
try out.writeAll("# All values are hex RGB: #rrggbb\n");
|
||||||
|
|
||||||
for (field_names) |f| {
|
// Declaration order of `Theme`, which is also the order the file
|
||||||
const c = colorPtrConst(&default_theme, f.offset);
|
// has always been written in.
|
||||||
const hex = formatHex(c.*);
|
inline for (std.meta.fields(Theme)) |f| {
|
||||||
|
const hex = formatHex(@field(default_theme, f.name));
|
||||||
try out.print("{s}::{s}\n", .{ f.name, hex });
|
try out.print("{s}::{s}\n", .{ f.name, hex });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -311,11 +273,10 @@ pub fn loadFromData(data: []const u8) ?Theme {
|
||||||
else => continue,
|
else => continue,
|
||||||
};
|
};
|
||||||
const color = parseHex(str) orelse continue;
|
const color = parseHex(str) orelse continue;
|
||||||
for (field_names) |f| {
|
// Unknown keys fall through untouched; `zfin doctor`
|
||||||
if (std.mem.eql(u8, field.key, f.name)) {
|
// reports them via `srf_schema`.
|
||||||
colorPtr(&theme, f.offset).* = color;
|
inline for (std.meta.fields(Theme)) |f| {
|
||||||
break;
|
if (std.mem.eql(u8, field.key, f.name)) @field(theme, f.name) = color;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -367,3 +328,26 @@ test "default theme has valid colors" {
|
||||||
// Text should be bright
|
// Text should be bright
|
||||||
try std.testing.expect(t.text[0] > 0xc0);
|
try std.testing.expect(t.text[0] > 0xc0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "loadFromData: every Theme field is loadable by its own name" {
|
||||||
|
// Replaces the old hand-maintained name table: a field added to
|
||||||
|
// `Theme` must load from theme.srf with no second list to update.
|
||||||
|
// Each field gets a distinct color, so a key that landed on the
|
||||||
|
// wrong field would show up here too.
|
||||||
|
var aw: std.Io.Writer.Allocating = .init(std.testing.allocator);
|
||||||
|
defer aw.deinit();
|
||||||
|
try aw.writer.writeAll("#!srfv1\n");
|
||||||
|
inline for (std.meta.fields(Theme), 0..) |f, i| {
|
||||||
|
try aw.writer.print("{s}::#0101{x:0>2}\n", .{ f.name, @as(u8, @intCast(i)) });
|
||||||
|
}
|
||||||
|
const t = loadFromData(aw.written()) orelse return error.TestUnexpectedResult;
|
||||||
|
inline for (std.meta.fields(Theme), 0..) |f, i| {
|
||||||
|
try std.testing.expectEqual(Color{ 0x01, 0x01, @intCast(i) }, @field(t, f.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "loadFromData: unknown keys are ignored, not applied to a neighbor" {
|
||||||
|
const t = loadFromData("#!srfv1\nbg_panle::#123456\n") orelse return error.TestUnexpectedResult;
|
||||||
|
try std.testing.expectEqual(default_theme.bg_panel, t.bg_panel);
|
||||||
|
try std.testing.expectEqual(default_theme.bg, t.bg);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue