Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support selection foreground being cell foreground #5219

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
73 changes: 69 additions & 4 deletions src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -454,15 +454,22 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
/// the selection color is just the inverted window background and foreground
/// (note: not to be confused with the cell bg/fg).
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
@"selection-foreground": ?Color = null,
@"selection-background": ?Color = null,
/// Since version 1.1.1, this can also be set to `cell-foreground` to match
/// the cell foreground color, or `cell-background` to match the cell
/// background color.
@"selection-foreground": ?DynamicColor = null,
@"selection-background": ?DynamicColor = null,

/// Swap the foreground and background colors of cells for selection. This
/// option overrides the `selection-foreground` and `selection-background`
/// options.
///
/// If you select across cells with differing foregrounds and backgrounds, the
/// selection color will vary across the selection.
///
/// Warning: This option has been deprecated as of version 1.1.1. Instead,
/// users should set `selection-foreground` and `selection-background` to
/// `cell-background` and `cell-foreground`, respectively.
@"selection-invert-fg-bg": bool = false,

/// The minimum contrast ratio between the foreground and background colors.
Expand Down Expand Up @@ -493,10 +500,17 @@ palette: Palette = .{},

/// The color of the cursor. If this is not set, a default will be chosen.
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
@"cursor-color": ?Color = null,
/// Since version 1.1.1, this can also be set to `cell-foreground` to match
/// the cell foreground color, or `cell-background` to match the cell
/// background color.
@"cursor-color": ?DynamicColor = null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not for this PR (and maybe never), but I wonder if we should unify the terminology for cursor and selection, so that instead of cursor-text and cursor-color we use cursor-foreground and cursor-background.


/// Swap the foreground and background colors of the cell under the cursor. This
/// option overrides the `cursor-color` and `cursor-text` options.
///
/// Warning: This option has been deprecated as of version 1.1.1. Instead,
/// users should set `cursor-color` and `cursor-text` to `cell-foreground` and
/// `cell-background`, respectively.
@"cursor-invert-fg-bg": bool = false,

/// The opacity level (opposite of transparency) of the cursor. A value of 1
Expand Down Expand Up @@ -547,7 +561,10 @@ palette: Palette = .{},
/// The color of the text under the cursor. If this is not set, a default will
/// be chosen.
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
@"cursor-text": ?Color = null,
/// Since version 1.1.1, this can also be set to `cell-foreground` to match
/// the cell foreground color, or `cell-background` to match the cell
/// background color.
@"cursor-text": ?DynamicColor = null,

/// Enables the ability to move the cursor at prompts by using `alt+click` on
/// Linux and `option+click` on macOS.
Expand Down Expand Up @@ -4215,6 +4232,54 @@ pub const Color = struct {
}
};

/// Represents the color values that can be set to a non-static value.
///
/// Can either be a Color or one of the special values
/// "cell-foreground" or "cell-background".
pub const DynamicColor = union(enum) {
color: Color,
@"cell-foreground",
@"cell-background",

pub fn parseCLI(input_: ?[]const u8) !DynamicColor {
const input = input_ orelse return error.ValueRequired;

if (std.mem.eql(u8, input, "cell-foreground")) return .@"cell-foreground";
if (std.mem.eql(u8, input, "cell-background")) return .@"cell-background";

return DynamicColor{ .color = try Color.parseCLI(input) };
}

/// Used by Formatter
pub fn formatEntry(self: DynamicColor, formatter: anytype) !void {
switch (self) {
.color => try self.color.formatEntry(formatter),
.@"cell-foreground", .@"cell-background" => try formatter.formatEntry([:0]const u8, @tagName(self)),
}
}

test "parseCLI" {
const testing = std.testing;

try testing.expectEqual(DynamicColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } }, try DynamicColor.parseCLI("#4e2a84"));
try testing.expectEqual(DynamicColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } }, try DynamicColor.parseCLI("black"));
try testing.expectEqual(DynamicColor{.@"cell-foreground"}, try DynamicColor.parseCLI("cell-foreground"));
try testing.expectEqual(DynamicColor{.@"cell-background"}, try DynamicColor.parseCLI("cell-background"));

try testing.expectError(error.InvalidValue, DynamicColor.parseCLI("a"));
}

test "formatConfig" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);
defer buf.deinit();

var sc: DynamicColor = .{.@"cell-foreground"};
try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try testing.expectEqualSlices(u8, "a = cell-foreground\n", buf.items);
}
};

pub const ColorList = struct {
const Self = @This();

Expand Down
159 changes: 71 additions & 88 deletions src/renderer/Metal.zig
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,7 @@ cursor_color: ?terminal.color.RGB,
/// This is cursor color as set in the user's config, if any. If no cursor color
/// is set in the user's config, then the cursor color is determined by the
/// current foreground color.
default_cursor_color: ?terminal.color.RGB,

/// When `cursor_color` is null, swap the foreground and background colors of
/// the cell under the cursor for the cursor color. Otherwise, use the default
/// foreground color as the cursor color.
cursor_invert: bool,
default_cursor_color: ?configpkg.Config.DynamicColor,

/// The current set of cells to render. This is rebuilt on every frame
/// but we keep this around so that we don't reallocate. Each set of
Expand Down Expand Up @@ -374,16 +369,14 @@ pub const DerivedConfig = struct {
font_thicken_strength: u8,
font_features: std.ArrayListUnmanaged([:0]const u8),
font_styles: font.CodepointResolver.StyleStatus,
cursor_color: ?terminal.color.RGB,
cursor_invert: bool,
cursor_color: ?configpkg.Config.DynamicColor,
cursor_opacity: f64,
cursor_text: ?terminal.color.RGB,
cursor_text: ?configpkg.Config.DynamicColor,
background: terminal.color.RGB,
background_opacity: f64,
foreground: terminal.color.RGB,
selection_background: ?terminal.color.RGB,
selection_foreground: ?terminal.color.RGB,
invert_selection_fg_bg: bool,
selection_background: ?configpkg.Config.DynamicColor,
selection_foreground: ?configpkg.Config.DynamicColor,
bold_is_bright: bool,
min_contrast: f32,
padding_color: configpkg.WindowPaddingColor,
Expand Down Expand Up @@ -419,45 +412,25 @@ pub const DerivedConfig = struct {
config.link.links.items,
);

const cursor_invert = config.@"cursor-invert-fg-bg";

return .{
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
.font_thicken = config.@"font-thicken",
.font_thicken_strength = config.@"font-thicken-strength",
.font_features = font_features.list,
.font_styles = font_styles,

.cursor_color = if (!cursor_invert and config.@"cursor-color" != null)
config.@"cursor-color".?.toTerminalRGB()
else
null,

.cursor_invert = cursor_invert,

.cursor_text = if (config.@"cursor-text") |txt|
txt.toTerminalRGB()
else
null,

.cursor_color = config.@"cursor-color",
.cursor_text = config.@"cursor-text",
.cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")),

.background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(),
.invert_selection_fg_bg = config.@"selection-invert-fg-bg",
.bold_is_bright = config.@"bold-is-bright",
.min_contrast = @floatCast(config.@"minimum-contrast"),
.padding_color = config.@"window-padding-color",

.selection_background = if (config.@"selection-background") |bg|
bg.toTerminalRGB()
else
null,

.selection_foreground = if (config.@"selection-foreground") |bg|
bg.toTerminalRGB()
else
null,
.selection_background = config.@"selection-background",
.selection_foreground = config.@"selection-foreground",

.custom_shaders = custom_shaders,
.links = links,
Expand Down Expand Up @@ -645,7 +618,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.default_background_color = options.config.background,
.cursor_color = null,
.default_cursor_color = options.config.cursor_color,
.cursor_invert = options.config.cursor_invert,

// Render state
.cells = .{},
Expand Down Expand Up @@ -2117,8 +2089,7 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
// Set our new colors
self.default_background_color = config.background;
self.default_foreground_color = config.foreground;
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
self.cursor_invert = config.cursor_invert;
self.default_cursor_color = config.cursor_color;

// Update our layer's opaqueness and display sync in case they changed.
{
Expand Down Expand Up @@ -2598,22 +2569,15 @@ fn rebuildCells(
// The final background color for the cell.
const bg = bg: {
if (selected) {
break :bg if (self.config.invert_selection_fg_bg)
if (style.flags.inverse)
// Cell is selected with invert selection fg/bg
// enabled, and the cell has the inverse style
// flag, so they cancel out and we get the normal
// bg color.
bg_style
else
// If it doesn't have the inverse style
// flag then we use the fg color instead.
fg_style
break :bg if (self.config.selection_background) |selection_color|
// Use the selection background if set, otherwise the default fg color.
switch (selection_color) {
.color => selection_color.color.toTerminalRGB(),
.@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style,
.@"cell-background" => if (style.flags.inverse) fg_style else bg_style,
}
else
// If we don't have invert selection fg/bg set then we
// just use the selection background if set, otherwise
// the default fg color.
break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color;
self.foreground_color orelse self.default_foreground_color;
}

// Not selected
Expand All @@ -2631,20 +2595,26 @@ fn rebuildCells(
};

const fg = fg: {
if (selected and !self.config.invert_selection_fg_bg) {
// If we don't have invert selection fg/bg set
// then we just use the selection foreground if
// set, otherwise the default bg color.
break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color;
}
const final_bg = bg_style orelse self.background_color orelse self.default_background_color;

// Whether we need to use the bg color as our fg color:
// - Cell is selected, inverted, and set to cell-foreground
// - Cell is selected, not inverted, and set to cell-background
// - Cell is inverted and not selected
// - Cell is selected and not inverted
// Note: if selected then invert sel fg / bg must be
// false since we separately handle it if true above.
break :fg if (style.flags.inverse != selected)
bg_style orelse self.background_color orelse self.default_background_color
if (selected) {
// Use the selection foreground if set, otherwise the default bg color.
break :fg if (self.config.selection_foreground) |selection_color|
switch (selection_color) {
.color => selection_color.color.toTerminalRGB(),
.@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style,
.@"cell-background" => if (style.flags.inverse) fg_style else final_bg,
}
else
self.background_color orelse self.default_background_color;
}

break :fg if (style.flags.inverse)
final_bg
else
fg_style;
};
Expand Down Expand Up @@ -2833,19 +2803,25 @@ fn rebuildCells(

// Prepare the cursor cell contents.
const style = cursor_style_ orelse break :cursor;
const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: {
if (self.cursor_invert) {
// Use the foreground color from the cell under the cursor, if any.
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
break :color if (sty.flags.inverse)
// If the cell is reversed, use background color instead.
(sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color)
else
(sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color);
} else {
break :color self.foreground_color orelse self.default_foreground_color;
const cursor_color = self.cursor_color orelse if (self.default_cursor_color) |color| color: {
// If cursor-color is set, then compute the correct color.
// Otherwise, use the foreground color
if (color == .color) {
// Use the color set by cursor-color, if any.
break :color color.color.toTerminalRGB();
}
};

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 bg_style = sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color;
Comment on lines +2814 to +2816
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably not a big deal, but we are computing these styles even if they're not needed when the cursor color is set to an explicit style. I don't think there is a significant runtime cost though. If we wanted to avoid that we could first check to see if the enum has the .color tag and only compute the styles if it does not (this would be a perfect use case for ziglang/zig#12863)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a check before these calls which should skip them if the union has the .color tag


break :color switch (color) {
// If the cell is reversed, use the opposite cell color instead.
.@"cell-foreground" => if (sty.flags.inverse) bg_style else fg_style,
.@"cell-background" => if (sty.flags.inverse) fg_style else bg_style,
else => unreachable,
};
} else self.foreground_color orelse self.default_foreground_color;

self.addCursor(screen, style, cursor_color);

Expand All @@ -2869,18 +2845,25 @@ fn rebuildCells(
.wide, .spacer_tail => true,
};

const uniform_color = if (self.cursor_invert) blk: {
// Use the background color from the cell under the cursor, if any.
const uniform_color = if (self.config.cursor_text) |txt| blk: {
// If cursor-text is set, then compute the correct color.
// Otherwise, use the background color.
if (txt == .color) {
// Use the color set by cursor-text, if any.
break :blk txt.color.toTerminalRGB();
}

const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
break :blk if (sty.flags.inverse)
// If the cell is reversed, use foreground color instead.
(sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color)
else
(sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color);
} else if (self.config.cursor_text) |txt|
txt
else
self.background_color orelse self.default_background_color;
const fg_style = sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_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) {
// If the cell is reversed, use the opposite cell color instead.
.@"cell-foreground" => if (sty.flags.inverse) bg_style else fg_style,
.@"cell-background" => if (sty.flags.inverse) fg_style else bg_style,
else => unreachable,
};
} else self.background_color orelse self.default_background_color;

self.uniforms.cursor_color = .{
uniform_color.r,
Expand Down
Loading