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; pub const formatEntry = formatter.formatEntry;
// Field types // Field types
pub const BoldColor = Config.BoldColor;
pub const ClipboardAccess = Config.ClipboardAccess; pub const ClipboardAccess = Config.ClipboardAccess;
pub const Command = Config.Command; pub const Command = Config.Command;
pub const ConfirmCloseSurface = Config.ConfirmCloseSurface; pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;

View File

@ -69,6 +69,10 @@ pub const compatibility = std.StaticStringMap(
// this behavior. This applies to selection too. // this behavior. This applies to selection too.
.{ "cursor-invert-fg-bg", compatCursorInvertFgBg }, .{ "cursor-invert-fg-bg", compatCursorInvertFgBg },
.{ "selection-invert-fg-bg", compatSelectionInvertFgBg }, .{ "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. /// The font families to use.
@ -2804,8 +2808,24 @@ else
/// notifications using certain escape sequences such as OSC 9 or OSC 777. /// notifications using certain escape sequences such as OSC 9 or OSC 777.
@"desktop-notifications": bool = true, @"desktop-notifications": bool = true,
/// If `true`, the bold text will use the bright color palette. /// Modifies the color used for bold text in the terminal.
@"bold-is-bright": bool = false, ///
/// 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. /// 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 /// HACK: We set this with an `xterm` prefix because vim uses that to enable key
@ -3910,6 +3930,23 @@ fn compatSelectionInvertFgBg(
return true; 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 /// 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 /// allocated with the previous config but will have a new arena for
/// any changes or new allocations. The config should have `deinit` /// 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 { pub const ColorList = struct {
const Self = @This(); 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, foreground: terminal.color.RGB,
selection_background: ?configpkg.Config.TerminalColor, selection_background: ?configpkg.Config.TerminalColor,
selection_foreground: ?configpkg.Config.TerminalColor, selection_foreground: ?configpkg.Config.TerminalColor,
bold_is_bright: bool, bold_color: ?configpkg.BoldColor,
min_contrast: f32, min_contrast: f32,
padding_color: configpkg.WindowPaddingColor, padding_color: configpkg.WindowPaddingColor,
custom_shaders: configpkg.RepeatablePath, custom_shaders: configpkg.RepeatablePath,
@ -580,7 +580,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.background = config.background.toTerminalRGB(), .background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(), .foreground = config.foreground.toTerminalRGB(),
.bold_is_bright = config.@"bold-is-bright", .bold_color = config.@"bold-color",
.min_contrast = @floatCast(config.@"minimum-contrast"), .min_contrast = @floatCast(config.@"minimum-contrast"),
.padding_color = config.@"window-padding-color", .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 // the cell style (SGR), before applying any additional
// configuration, inversions, selections, etc. // configuration, inversions, selections, etc.
const bg_style = style.bg(cell, color_palette); const bg_style = style.bg(cell, color_palette);
const fg_style = style.fg( const fg_style = style.fg(.{
color_palette, .default = self.foreground_color orelse self.default_foreground_color,
self.config.bold_is_bright, .palette = color_palette,
) orelse self.foreground_color orelse self.default_foreground_color; .bold = self.config.bold_color,
});
// The final background color for the cell. // The final background color for the cell.
const bg = bg: { const bg = bg: {
@ -2801,10 +2803,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.@"cell-background", .@"cell-background",
=> |_, tag| { => |_, tag| {
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
const fg_style = sty.fg( const fg_style = sty.fg(.{
color_palette, .default = self.foreground_color orelse self.default_foreground_color,
self.config.bold_is_bright, .palette = color_palette,
) orelse self.foreground_color orelse self.default_foreground_color; .bold = self.config.bold_color,
});
const bg_style = sty.bg( const bg_style = sty.bg(
screen.cursor.page_cell, screen.cursor.page_cell,
color_palette, 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 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; const bg_style = sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color;
break :blk switch (txt) { break :blk switch (txt) {

View File

@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const configpkg = @import("../config.zig");
const color = @import("color.zig"); const color = @import("color.zig");
const sgr = @import("sgr.zig"); const sgr = @import("sgr.zig");
const page = @import("page.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( pub fn fg(
self: Style, self: Style,
palette: *const color.Palette, opts: Fg,
bold_is_bright: bool, ) color.RGB {
) ?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) { return switch (self.fg_color) {
.none => null, .none => default: {
.palette => |idx| palette: { if (self.flags.bold) {
if (bold_is_bright and self.flags.bold) { if (opts.bold) |bold| switch (bold) {
const bright_offset = @intFromEnum(color.Name.bright_black); .bright => {},
if (idx < bright_offset) .color => |v| break :default v.toTerminalRGB(),
break :palette palette[idx + bright_offset]; };
} }
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,
}; };
} }