Added bold-color option (#7168)

As discussed in https://github.com/ghostty-org/ghostty/discussions/3134

To allow for the option to render bold text in a different colour for
better visibility as an extension of `bold-is-bright`.

This is a feature that is available in other terminals.
This commit is contained in:
Mitchell Hashimoto
2025-07-06 12:59:04 -07:00
committed by GitHub
4 changed files with 187 additions and 25 deletions

View File

@ -14,6 +14,7 @@ pub const entryFormatter = formatter.entryFormatter;
pub const formatEntry = formatter.formatEntry;
// Field types
pub const BoldColor = Config.BoldColor;
pub const ClipboardAccess = Config.ClipboardAccess;
pub const Command = Config.Command;
pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;

View File

@ -69,6 +69,10 @@ pub const compatibility = std.StaticStringMap(
// this behavior. This applies to selection too.
.{ "cursor-invert-fg-bg", compatCursorInvertFgBg },
.{ "selection-invert-fg-bg", compatSelectionInvertFgBg },
// Ghostty 1.2 merged `bold-is-bright` into the new `bold-color`
// by setting the value to "bright".
.{ "bold-is-bright", compatBoldIsBright },
});
/// The font families to use.
@ -2804,8 +2808,24 @@ else
/// notifications using certain escape sequences such as OSC 9 or OSC 777.
@"desktop-notifications": bool = true,
/// If `true`, the bold text will use the bright color palette.
@"bold-is-bright": bool = false,
/// Modifies the color used for bold text in the terminal.
///
/// This can be set to a specific color, using the same format as
/// `background` or `foreground` (e.g. `#RRGGBB` but other formats
/// are also supported; see the aforementioned documentation). If a
/// specific color is set, this color will always be used for all
/// bold text regardless of the terminal's color scheme.
///
/// This can also be set to `bright`, which uses the bright color palette
/// for bold text. For example, if the text is red, then the bold will
/// use the bright red color. The terminal palette is set with `palette`
/// but can also be overridden by the terminal application itself using
/// escape sequences such as OSC 4. (Since Ghostty 1.2.0, the previous
/// configuration `bold-is-bright` is deprecated and replaced by this
/// usage).
///
/// Available since Ghostty 1.2.0.
@"bold-color": ?BoldColor = null,
/// This will be used to set the `TERM` environment variable.
/// HACK: We set this with an `xterm` prefix because vim uses that to enable key
@ -3910,6 +3930,23 @@ fn compatSelectionInvertFgBg(
return true;
}
fn compatBoldIsBright(
self: *Config,
alloc: Allocator,
key: []const u8,
value_: ?[]const u8,
) bool {
_ = alloc;
assert(std.mem.eql(u8, key, "bold-is-bright"));
const set = cli.args.parseBool(value_ orelse "t") catch return false;
if (set) {
self.@"bold-color" = .bright;
}
return true;
}
/// Create a shallow copy of this config. This will share all the memory
/// allocated with the previous config but will have a new arena for
/// any changes or new allocations. The config should have `deinit`
@ -4537,6 +4574,58 @@ pub const TerminalColor = union(enum) {
}
};
/// Represents color values that can be used for bold. See `bold-color`.
pub const BoldColor = union(enum) {
color: Color,
bright,
pub fn parseCLI(input_: ?[]const u8) !BoldColor {
const input = input_ orelse return error.ValueRequired;
if (std.mem.eql(u8, input, "bright")) return .bright;
return .{ .color = try Color.parseCLI(input) };
}
/// Used by Formatter
pub fn formatEntry(self: BoldColor, formatter: anytype) !void {
switch (self) {
.color => try self.color.formatEntry(formatter),
.bright => try formatter.formatEntry(
[:0]const u8,
@tagName(self),
),
}
}
test "parseCLI" {
const testing = std.testing;
try testing.expectEqual(
BoldColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } },
try BoldColor.parseCLI("#4e2a84"),
);
try testing.expectEqual(
BoldColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } },
try BoldColor.parseCLI("black"),
);
try testing.expectEqual(
BoldColor.bright,
try BoldColor.parseCLI("bright"),
);
try testing.expectError(error.InvalidValue, BoldColor.parseCLI("a"));
}
test "formatConfig" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);
defer buf.deinit();
var sc: BoldColor = .bright;
try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try testing.expectEqualSlices(u8, "a = bright\n", buf.items);
}
};
pub const ColorList = struct {
const Self = @This();
@ -8236,3 +8325,23 @@ test "compatibility: removed selection-invert-fg-bg" {
);
}
}
test "compatibility: removed bold-is-bright" {
const testing = std.testing;
const alloc = testing.allocator;
{
var cfg = try Config.default(alloc);
defer cfg.deinit();
var it: TestIterator = .{ .data = &.{
"--bold-is-bright",
} };
try cfg.loadIter(alloc, &it);
try cfg.finalize();
try testing.expectEqual(
BoldColor.bright,
cfg.@"bold-color",
);
}
}

View File

@ -519,7 +519,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
foreground: terminal.color.RGB,
selection_background: ?configpkg.Config.TerminalColor,
selection_foreground: ?configpkg.Config.TerminalColor,
bold_is_bright: bool,
bold_color: ?configpkg.BoldColor,
min_contrast: f32,
padding_color: configpkg.WindowPaddingColor,
custom_shaders: configpkg.RepeatablePath,
@ -580,7 +580,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(),
.bold_is_bright = config.@"bold-is-bright",
.bold_color = config.@"bold-color",
.min_contrast = @floatCast(config.@"minimum-contrast"),
.padding_color = config.@"window-padding-color",
@ -2540,10 +2541,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// the cell style (SGR), before applying any additional
// configuration, inversions, selections, etc.
const bg_style = style.bg(cell, color_palette);
const fg_style = style.fg(
color_palette,
self.config.bold_is_bright,
) orelse self.foreground_color orelse self.default_foreground_color;
const fg_style = style.fg(.{
.default = self.foreground_color orelse self.default_foreground_color,
.palette = color_palette,
.bold = self.config.bold_color,
});
// The final background color for the cell.
const bg = bg: {
@ -2801,10 +2803,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.@"cell-background",
=> |_, tag| {
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
const fg_style = sty.fg(
color_palette,
self.config.bold_is_bright,
) orelse self.foreground_color orelse self.default_foreground_color;
const fg_style = sty.fg(.{
.default = self.foreground_color orelse self.default_foreground_color,
.palette = color_palette,
.bold = self.config.bold_color,
});
const bg_style = sty.bg(
screen.cursor.page_cell,
color_palette,
@ -2852,7 +2855,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
const fg_style = sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color;
const fg_style = sty.fg(.{
.default = self.foreground_color orelse self.default_foreground_color,
.palette = color_palette,
.bold = self.config.bold_color,
});
const bg_style = sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color;
break :blk switch (txt) {

View File

@ -1,5 +1,6 @@
const std = @import("std");
const assert = std.debug.assert;
const configpkg = @import("../config.zig");
const color = @import("color.zig");
const sgr = @import("sgr.zig");
const page = @import("page.zig");
@ -115,24 +116,68 @@ pub const Style = struct {
};
}
/// Returns the fg color for a cell with this style given the palette.
pub const Fg = struct {
/// The default color to use if the style doesn't specify a
/// foreground color and no configuration options override
/// it.
default: color.RGB,
/// The current color palette. Required to map palette indices to
/// real color values.
palette: *const color.Palette,
/// If specified, the color to use for bold text.
bold: ?configpkg.BoldColor = null,
};
/// Returns the fg color for a cell with this style given the palette
/// and various configuration options.
pub fn fg(
self: Style,
palette: *const color.Palette,
bold_is_bright: bool,
) ?color.RGB {
opts: Fg,
) color.RGB {
// Note we don't pull the bold check to the top-level here because
// we don't want to duplicate the conditional multiple times since
// certain colors require more checks (e.g. `bold_is_bright`).
return switch (self.fg_color) {
.none => null,
.palette => |idx| palette: {
if (bold_is_bright and self.flags.bold) {
const bright_offset = @intFromEnum(color.Name.bright_black);
if (idx < bright_offset)
break :palette palette[idx + bright_offset];
.none => default: {
if (self.flags.bold) {
if (opts.bold) |bold| switch (bold) {
.bright => {},
.color => |v| break :default v.toTerminalRGB(),
};
}
break :palette palette[idx];
break :default opts.default;
},
.palette => |idx| palette: {
if (self.flags.bold) {
if (opts.bold) |bold| switch (bold) {
.color => |v| break :palette v.toTerminalRGB(),
.bright => {
const bright_offset = @intFromEnum(color.Name.bright_black);
if (idx < bright_offset) {
break :palette opts.palette[idx + bright_offset];
}
},
};
}
break :palette opts.palette[idx];
},
.rgb => |rgb| rgb: {
if (self.flags.bold and rgb.eql(opts.default)) {
if (opts.bold) |bold| switch (bold) {
.color => |v| break :rgb v.toTerminalRGB(),
.bright => {},
};
}
break :rgb rgb;
},
.rgb => |rgb| rgb,
};
}