diff --git a/build.zig b/build.zig index 5a998bdbd..32fd07eb1 100644 --- a/build.zig +++ b/build.zig @@ -213,6 +213,8 @@ pub fn build(b: *std.Build) !void { exe_options.addOption(renderer.Impl, "renderer", renderer_impl); exe_options.addOption(bool, "libadwaita", libadwaita); + createRGBNames(b); + // Exe if (exe_) |exe| { exe.root_module.addOptions("build_options", exe_options); @@ -220,6 +222,8 @@ pub fn build(b: *std.Build) !void { // Add the shared dependencies _ = try addDeps(b, exe, static); + addRGBNames(exe); + // If we're in NixOS but not in the shell environment then we issue // a warning because the rpath may not be setup properly. const is_nixos = is_nixos: { @@ -461,6 +465,8 @@ pub fn build(b: *std.Build) !void { lib.linkLibC(); lib.root_module.addOptions("build_options", exe_options); + addRGBNames(lib); + // Create a single static lib with all our dependencies merged var lib_list = try addDeps(b, lib, true); try lib_list.append(lib.getEmittedBin()); @@ -490,6 +496,8 @@ pub fn build(b: *std.Build) !void { lib.linkLibC(); lib.root_module.addOptions("build_options", exe_options); + addRGBNames(lib); + // Create a single static lib with all our dependencies merged var lib_list = try addDeps(b, lib, true); try lib_list.append(lib.getEmittedBin()); @@ -603,6 +611,9 @@ pub fn build(b: *std.Build) !void { .root_source_file = .{ .path = "src/main_wasm.zig" }, .target = b.resolveTargetQuery(wasm_crosstarget), }); + + addRGBNames(main_test); + main_test.root_module.addOptions("build_options", exe_options); _ = try addDeps(b, main_test, true); test_step.dependOn(&main_test.step); @@ -638,6 +649,9 @@ pub fn build(b: *std.Build) !void { .target = target, .filter = test_filter, }); + + addRGBNames(main_test); + { if (emit_test_exe) b.installArtifact(main_test); _ = try addDeps(b, main_test, true); @@ -916,6 +930,45 @@ fn addDeps( return static_libs; } +var generate_rgb_names: *std.Build.Step.Run = undefined; +var generate_rgb_names_output: std.Build.LazyPath = undefined; + +fn createRGBNames(b: *std.Build) void { + const gen = b.addExecutable( + .{ + .name = "generate-rgb-names", + .root_source_file = .{ + .path = "src/generate_rgb_names.zig", + }, + .target = b.host, + }, + ); + + const rgb = b.dependency("rgb", .{}); + + gen.root_module.addAnonymousImport( + "rgb", + .{ + .root_source_file = .{ + .path = rgb.builder.pathFromRoot("rgb.txt"), + }, + }, + ); + + generate_rgb_names = b.addRunArtifact(gen); + generate_rgb_names_output = generate_rgb_names.captureStdOut(); +} + +fn addRGBNames(exe: *std.Build.Step.Compile) void { + exe.step.dependOn(&generate_rgb_names.step); + exe.root_module.addAnonymousImport( + "rgb_names", + .{ + .root_source_file = generate_rgb_names_output, + }, + ); +} + fn benchSteps( b: *std.Build, target: std.Build.ResolvedTarget, diff --git a/build.zig.zon b/build.zig.zon index c1472894d..b55b1cba5 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -48,5 +48,9 @@ .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/53acae071801e0de6ed160315869abb9bdaf20fa.tar.gz", .hash = "12201575c5a2b21c2e110593773040cddcd357544038092d18bd98fc5a2141354bbd", }, + .rgb = .{ + .url = "https://gitlab.freedesktop.org/xorg/app/rgb/-/archive/master/rgb-master.tar.gz", + .hash = "12201ecce35845b829edf31f5b1b751b24efe6bdc20a8acf06f5c0f2bd83fdd69158", + }, }, } diff --git a/src/cli/action.zig b/src/cli/action.zig index 1297d640b..f97a5d2cf 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -5,6 +5,7 @@ const list_fonts = @import("list_fonts.zig"); const version = @import("version.zig"); const list_keybinds = @import("list_keybinds.zig"); const list_themes = @import("list_themes.zig"); +const list_colors = @import("list_colors.zig"); /// Special commands that can be invoked via CLI flags. These are all /// invoked by using `+` as a CLI flag. The only exception is @@ -22,6 +23,9 @@ pub const Action = enum { /// List available themes @"list-themes", + /// List named RGB colors + @"list-colors", + pub const Error = error{ /// Multiple actions were detected. You can specify at most one /// action on the CLI otherwise the behavior desired is ambiguous. @@ -62,6 +66,7 @@ pub const Action = enum { .@"list-fonts" => try list_fonts.run(alloc), .@"list-keybinds" => try list_keybinds.run(alloc), .@"list-themes" => try list_themes.run(alloc), + .@"list-colors" => try list_colors.run(alloc), }; } }; diff --git a/src/cli/list_colors.zig b/src/cli/list_colors.zig new file mode 100644 index 000000000..4479c8f68 --- /dev/null +++ b/src/cli/list_colors.zig @@ -0,0 +1,32 @@ +const std = @import("std"); +const inputpkg = @import("../input.zig"); +const args = @import("args.zig"); +const RGBName = @import("rgb_names").RGBName; + +pub const Options = struct { + pub fn deinit(self: Options) void { + _ = self; + } +}; + +/// The "list-colors" command is used to list all the named RGB colors in +/// Ghostty. +pub fn run(alloc: std.mem.Allocator) !u8 { + var opts: Options = .{}; + defer opts.deinit(); + + { + var iter = try std.process.argsWithAllocator(alloc); + defer iter.deinit(); + try args.parse(Options, alloc, &opts, &iter); + } + + const stdout = std.io.getStdOut().writer(); + + inline for (std.meta.fields(RGBName)) |f| { + const rgb = @field(RGBName, f.name).toRGB(); + try stdout.print("{s} = #{x:0>2}{x:0>2}{x:0>2}\n", .{ f.name, rgb.r, rgb.g, rgb.b }); + } + + return 0; +} diff --git a/src/config/Config.zig b/src/config/Config.zig index 6edfe8ea4..e274550f5 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -20,6 +20,7 @@ const Key = @import("key.zig").Key; const KeyValue = @import("key.zig").Value; const ErrorList = @import("ErrorList.zig"); const MetricModifier = fontpkg.face.Metrics.Modifier; +const RGBName = @import("rgb_names").RGBName; const log = std.log.scoped(.config); @@ -2139,7 +2140,12 @@ pub const Color = packed struct(u24) { } pub fn parseCLI(input: ?[]const u8) !Color { - return fromHex(input orelse return error.ValueRequired); + if (input == null) return error.ValueRequred; + if (RGBName.fromString(input.?)) |name| { + const rgb = name.toRGB(); + return Color{ .r = rgb.r, .g = rgb.g, .b = rgb.b }; + } + return fromHex(input.?); } /// Deep copy of the struct. Required by Config. @@ -2188,6 +2194,10 @@ pub const Color = packed struct(u24) { try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("0A0B0C")); try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFFFFF")); } + + test "fromName" { + try std.testing.expectEqual(Color{ .r = 0, .g = 0, .b = 0 }, try Color.parseCLI("black")); + } }; /// Palette is the 256 color palette for 256-color mode. This is still diff --git a/src/generate_rgb_names.zig b/src/generate_rgb_names.zig new file mode 100644 index 000000000..c60aa34da --- /dev/null +++ b/src/generate_rgb_names.zig @@ -0,0 +1,120 @@ +const std = @import("std"); +const rgb = @embedFile("rgb"); + +const RGB = struct { + r: u8, + g: u8, + b: u8, +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var arena = std.heap.ArenaAllocator.init(gpa.allocator()); + const alloc = arena.allocator(); + + const stdout_file = std.io.getStdOut().writer(); + var bw = std.io.bufferedWriter(stdout_file); + const stdout = bw.writer(); + + var set = std.StringHashMap(RGB).init(alloc); + defer set.deinit(); + var list = std.ArrayList([]const u8).init(alloc); + defer list.deinit(); + + try stdout.writeAll( + \\// THIS FILE IS AUTO-GENERATED! DO NOT MAKE ANY CHANGES! + \\ + \\const std = @import("std"); + \\ + \\pub const RGB = struct { + \\ r: u8, + \\ g: u8, + \\ b: u8, + \\}; + \\ + \\/// RGB color names, taken from the X11 rgb.txt file. + \\pub const RGBName = enum { + \\ + \\ const Self = @This(); + \\ + \\ + ); + + var iter = std.mem.splitScalar(u8, rgb, '\n'); + while (iter.next()) |line| { + if (line.len < 12) continue; + const r = try std.fmt.parseInt(u8, std.mem.trim(u8, line[0..3], &std.ascii.whitespace), 10); + const g = try std.fmt.parseInt(u8, std.mem.trim(u8, line[4..7], &std.ascii.whitespace), 10); + const b = try std.fmt.parseInt(u8, std.mem.trim(u8, line[8..11], &std.ascii.whitespace), 10); + var n = try alloc.alloc(u8, line[12..].len); + defer alloc.free(n); + var i: usize = 0; + for (line[12..]) |c| { + if (std.ascii.isWhitespace(c)) continue; + n[i] = std.ascii.toLower(c); + i += 1; + } + const m = try alloc.dupe(u8, n[0..i]); + if (set.get(m) == null) { + try set.put(m, RGB{ .r = r, .g = g, .b = b }); + try list.append(m); + try stdout.print(" {s},\n", .{ + m, + }); + } + } + + try stdout.writeAll( + \\ + \\ pub fn fromString(str: []const u8) ?Self { + \\ const max = 64; + \\ var n: [max]u8 = undefined; + \\ var i: usize = 0; + \\ for (str, 0..) |c, j| { + \\ if (std.ascii.isWhitespace(c)) continue; + \\ n[i] = std.ascii.toLower(c); + \\ i += 1; + \\ if (i == max) { + \\ if (j >= str.len - 1) std.log.warn("color name '{s}' longer than {d} characters", .{str, max}); + \\ break; + \\ } + \\ } + \\ return std.meta.stringToEnum(Self, n[0..i]); + \\ } + \\ + \\ pub fn toRGB(self: Self) RGB { + \\ return switch(self) { + \\ + ); + + for (list.items) |name| { + if (set.get(name)) |value| { + try stdout.print(" .{s} => RGB{{ .r = {d}, .g = {d}, .b = {d} }},\n", .{ name, value.r, value.g, value.b }); + } + } + + try stdout.writeAll( + \\ }; + \\ } + \\}; + \\ + \\test "RGBName" { + \\ try std.testing.expectEqual(null, RGBName.fromString("nosuchcolor")); + \\ try std.testing.expectEqual(RGBName.white, RGBName.fromString("white")); + \\ try std.testing.expectEqual(RGBName.mediumspringgreen, RGBName.fromString("medium spring green")); + \\ try std.testing.expectEqual(RGBName.forestgreen, RGBName.fromString("ForestGreen")); + \\ + \\ try std.testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 0 }, RGBName.black.toRGB()); + \\ try std.testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 0 }, RGBName.red.toRGB()); + \\ try std.testing.expectEqual(RGB{ .r = 0, .g = 255, .b = 0 }, RGBName.green.toRGB()); + \\ try std.testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 255 }, RGBName.blue.toRGB()); + \\ try std.testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, RGBName.white.toRGB()); + \\ try std.testing.expectEqual(RGB{ .r = 124, .g = 252, .b = 0 }, RGBName.lawngreen.toRGB()); + \\ try std.testing.expectEqual(RGB{ .r = 0, .g = 250, .b = 154 }, RGBName.mediumspringgreen.toRGB()); + \\ try std.testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, RGBName.forestgreen.toRGB()); + \\} + \\ + ); + + try bw.flush(); +} diff --git a/src/terminal/color.zig b/src/terminal/color.zig index 732191bff..fb8547315 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -1,5 +1,6 @@ const std = @import("std"); const assert = std.debug.assert; +const RGBName = @import("rgb_names").RGBName; /// The default palette. pub const default: Palette = default: { @@ -216,6 +217,15 @@ pub const RGB = struct { }; } + if (RGBName.fromString(value)) |name| { + const rgb = name.toRGB(); + return RGB{ + .r = rgb.r, + .g = rgb.g, + .b = rgb.b, + }; + } + if (value.len < "rgb:a/a/a".len or !std.mem.eql(u8, value[0..3], "rgb")) { return error.InvalidFormat; } @@ -293,6 +303,16 @@ test "RGB.parse" { try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#ffffff")); try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 16 }, try RGB.parse("#ff0010")); + try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 0 }, try RGB.parse("black")); + try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 0 }, try RGB.parse("red")); + try testing.expectEqual(RGB{ .r = 0, .g = 255, .b = 0 }, try RGB.parse("green")); + try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 255 }, try RGB.parse("blue")); + try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("white")); + + try testing.expectEqual(RGB{ .r = 124, .g = 252, .b = 0 }, try RGB.parse("LawnGreen")); + try testing.expectEqual(RGB{ .r = 0, .g = 250, .b = 154 }, try RGB.parse("medium spring green")); + try testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, try RGB.parse(" Forest Green ")); + // Invalid format try testing.expectError(error.InvalidFormat, RGB.parse("rgb;")); try testing.expectError(error.InvalidFormat, RGB.parse("rgb:"));