charmbracelet/lipgloss

Proposal: pure NewStyle, Lip Gloss v2

aymanbagabas opened this issue · 0 comments

The issue

Currently, *Renderer type and name are misleading. It does not render styles, rather, it's a factory for new styles. The question becomes, do we need a style factory?

On a lower level, type Renderer does two things

  1. Detect the color profile of the terminal
  2. Detect the background color to determine whether it's light or dark

Some context before getting into the issue. A color profile is the terminal profile used to decide the terminal color capability i.e. whether it supports NoTTY (no colors or text decorations), Ascii (no colors), ANSI (4-bit), ANSI256 (16-bit), and TrueColor(RGB 24-bit).

The way Lip Gloss works right now is by having a global *Renderer for os.Stdout that is used for all new global styles i.e. lipgloss.NewStyle(). This may produce different styles on different environments. For example, Apple Terminal only supports the ANSI256 profile and defining a style such as lipgloss.NewStyle().Foreground(lipgloss.Color("#874BFD")) won't produce an RGB color ANSI sequence on Apple Terminal. Instead, Lip Gloss degrades the color to ANSI256 before generating the sequence.

The same goes for HasDarkBackground(), a style needs to know the terminal background color to decide between a light or a dark color for the AdaptiveColor type.

Why this is a problem?

This is not a problem for the average user. However, users who want to use Lip Gloss with Wish, or use it on a different output like os.Stderr might face issues detecting the appropriate profile and background color.

  1. Name confusion, particularly type Renderer
  2. Difficulty debugging things since detection is happening in the background auto-magically
  3. Code complexity using Lip Gloss Renderers

How can we solve this?

// Profile is a color profile: NoTTY, Ascii, ANSI, ANSI256, or TrueColor.
type Profile int

const (
	// TrueColor, 24-bit color profile, produce RGB colors
	TrueColor Profile = iota
	// ANSI256, 8-bit color profile, degrades higher profile colors to 8-bit
	ANSI256
	// ANSI, 4-bit color profile, degrades higher profile colors to 4-bit
	ANSI
	// Ascii, disables style colors
	Ascii // nolint: revive
	// NoTTY, disables style colors and decorations
	NoTTY
)

Given the introduction of color profiles in Lip Gloss, here's a proposal that might simplify this issue and make Lip Gloss "pure".

Embed the color profile and background color in type Style:

type Style struct {
    colorProfile                Profile // Defaults to `TrueColor`
    hasLightBackground bool    // Terminals default to dark background i.e. hasLightBackground = false
    
    ... // other props
}

func (s Style) ColorProfile(p Profile) Style {
    s.colorProfile = p
    return s
}

func (s Style) HasLightBackground(v bool) Style {
    s.hasLightBackground = v
    return s
}

Then we would move the color profile and background detection to their functions

func DetectColorProfile(output io.Writer, environ []string) Profile
func QueryHasLightBackground(in term.File, out term.File) bool

We can also introduce a global default color profile that new styles inherit from and can be overwritten by the above functions and helpers

var (
	// ColorProfile is the color profile used by lipgloss.
	// This is the default color profile used to create new styles.
	// By default, it allows for 24-bit color (TrueColor), decorations, and
	// doesn't do color conversion.
	ColorProfile Profile

	// HasLightBackground is true if the terminal has a light background.
	// This is the default value used to create new styles.
	HasLightBackground bool

	onceStdDefaults sync.Once
)

// UseDefault will set the default color profile and background color detection
// from the given terminal file descriptors and environment variables.
func UseDefault(in term.File, out term.File, env []string) {
	ColorProfile = DetectColorProfile(out, env)
	HasLightBackground = QueryHasLightBackground(in, out)
}

// UseStdDefaults will set the default color profile and background color
// detection from the standard input, output, and OS environment variables.
func UseStdDefaults() {
	UseDefault(os.Stdin, os.Stdout, os.Environ())
}

Then NewStyle can be defined as

// NewStyle returns a new, empty Style. While it's syntactic sugar for the
// Style{} primitive, it's recommended to use this function for creating styles
// in case the underlying implementation changes.
func NewStyle() Style {
	onceStdDefaults.Do(UseStdDefaults)
	return Style{
		profile:            ColorProfile,
		hasLightBackground: HasLightBackground,
	}
}

This users who wants to use a "pure" style can do lipgloss.Style{}.

For more details checkout the proposal-v2-exp branch