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

Color-Scheme Aware Faint/Dim Rendering #45

Open
erikgeiser opened this issue Jul 31, 2021 · 0 comments
Open

Color-Scheme Aware Faint/Dim Rendering #45

erikgeiser opened this issue Jul 31, 2021 · 0 comments

Comments

@erikgeiser
Copy link

erikgeiser commented Jul 31, 2021

What's the problem?

Short answer: (Style).Faint() does not change the style in some cases.

Long answer: Currently (Style).Faint() uses the faint ANSI sequence. However, this sequence is not properly supported on many terminals as it is often implemented as RGB*2/3. This does not work well on a bright background, as the foreground text is usually close to black in this case and (0, 0, 0) * 2/3 = (0, 0, 0) resulting in no change of color at all or only a barely noticable change. Depending on the color-scheme and implementation the faint text can also end up more pronounced than regular text.

Examples for this would be gnome-terminal or alacritty (to be fair, alacritty allows users to configure an arbitrary color for dim/faint). On the other hand, some terminals such as iTerm or xterm.js implement it by shifting the color towards the background, ensuring a faint or dim appearance regardless of the background color.

Why should this be fixed in termenv?

Because when the users of termenv call (Style).Faint(), they expect text to appear faintly on the screens of the users of their application. As users of termenv, how else would they go about achieving this? They cannot possibly open PRs for each terminal their users may use.

What can we do to fix this?

A color-scheme aware (Style).Faint() implementation could look like the following listing. I found a blendFactor of 0.5 to be in-line of what iTerm does.

func (t Style) BGAwareFaint() Style {
	blendFactor := 0.5

	bg := termenv.ConvertToRGB(termenv.BackgroundColor())
	fg := termenv.ConvertToRGB(termenv.ForegroundColor())
	faint := termenv.RGBColor((colorful.Color{
		R: (1.-blendFactor)*fg.R + blendFactor*bg.R,
		G: (1.-blendFactor)*fg.G + blendFactor*bg.G,
		B: (1.-blendFactor)*fg.B + blendFactor*bg.B,
	}).Hex())

	return t.Foreground(termenv.ColorProfile().Convert(faint))
}

Obviously this is a naive implementation that does take into account that the color calculation has to be performed after an t.Foreground(...) calls performed by the user. I'm also not a color expert, so maybe someone will know a better way to blend the colors.

With a slight modification of HasDarkBackground() (see #44), an adaptive faint implemntation that only calculates a blend if the terminal has a light background (which is usually the problematic case):

func (t Style) AdaptiveFaint() Style {
    if HasDarkBackground() {
        return t.Faint()
    }

    return t.BGAwareFaint()
}

Demo

Alacritty: Alacritty is used an example of the RGB*2/3 faint implementation.

With a light color scheme the current faint rendering produces an even more pronounced color than the regular text. In contrast, the background aware rendering works as expected:

image

With a dark color scheme the current faint rendering works as intended and the background aware rendering looks the same.
Alacritty Dark BG


iTerm2: iTerm is used as an example of an color scheme aware faint implementation.

With a light color scheme, the effect of the current faint rendering and the background aware rendering is similar, but the actual color differs slightly.

image

In the dark color scheme, iTerm2 seems to brighten bold text slightly. The background aware implementation looks a bit different, but that might be due to naive implementation as mentioned above.
image

Here is some code to test the implementation above:

package main

import (
	"os"

	"github.com/lucasb-eyer/go-colorful"
	"github.com/muesli/termenv"
	"github.com/olekukonko/tablewriter"
)

func hasDarkBackground() bool {
	_, _, fgLightness := termenv.ConvertToRGB(termenv.ForegroundColor()).Hsl()
	_, _, bgLightness := termenv.ConvertToRGB(termenv.BackgroundColor()).Hsl()
	return bgLightness < fgLightness
}

func bgAwareFaint(style termenv.Style) termenv.Style {
	blendFactor := 0.5

	bg := termenv.ConvertToRGB(termenv.BackgroundColor())
	fg := termenv.ConvertToRGB(termenv.ForegroundColor())
	faint := termenv.RGBColor((colorful.Color{
		R: (1.-blendFactor)*fg.R + blendFactor*bg.R,
		G: (1.-blendFactor)*fg.G + blendFactor*bg.G,
		B: (1.-blendFactor)*fg.B + blendFactor*bg.B,
	}).Hex())

	return style.Foreground(termenv.ColorProfile().Convert(faint))
}

func adaptiveFaint(style termenv.Style) termenv.Style {
	if hasDarkBackground() {
		return style.Faint()
	}

	return bgAwareFaint(style)
}

func compare(sample string) {
	writer := tablewriter.NewWriter(os.Stdout)

	bg := "Light BG"
	if hasDarkBackground() {
		bg = "Dark BG"
	}

	writer.SetAutoFormatHeaders(false)
	writer.SetAutoWrapText(false)
	writer.SetHeader([]string{
		termenv.String(bg).Bold().String(),
		termenv.String("Currently").Bold().String(),
		termenv.String("BG Aware").Bold().String(),
		termenv.String("Adaptive").Bold().String(),
	})

	writer.Append([]string{
		termenv.String("Regular").Bold().String(),
		sample,
		sample,
		sample,
	})
	writer.Append([]string{
		termenv.String("Faint").Bold().String(),
		termenv.String(sample).Faint().String(),
		bgAwareFaint(termenv.String(sample)).String(),
		adaptiveFaint(termenv.String(sample)).String(),
	})
	writer.Append([]string{
		termenv.String("Faint+Bold").Bold().String(),
		termenv.String(sample).Faint().Bold().String(),
		bgAwareFaint(termenv.String(sample).Bold()).String(),
		adaptiveFaint(termenv.String(sample).Bold()).String(),
	})

	writer.Render()
}

func main() {
	compare("████ Test")
}

Integration

I would suggest adding (Style).BlendFaint() and (Style).AdaptiveFaint() and the respective template functions.

If you like this suggestion, I can submit a PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant