Skip to content

Commit

Permalink
feat: lipgloss writer
Browse files Browse the repository at this point in the history
Instead of using fmt.Printf to write out rendered styles, we supply our
own writer and implementation that parses the rendered ansi and degrades
colors as needed.
  • Loading branch information
aymanbagabas committed Jun 14, 2024
1 parent a724723 commit 0723bba
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 1 deletion.
12 changes: 12 additions & 0 deletions examples/writer/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package main

import (
"github.com/charmbracelet/lipgloss"
)

func main() {
// w := lipgloss.Writer{Forward: os.Stdout, Profile: lipgloss.ANSI}
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FF88CC"))

lipgloss.Println(style.Render("Hello, ANSI!"))
}
1 change: 0 additions & 1 deletion style.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ func (p props) has(k propKey) bool {
// 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,
Expand Down
280 changes: 280 additions & 0 deletions writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
package lipgloss

import (
"bytes"
"fmt"
"image/color"
"io"
"os"
"sync"

"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/ansi/parser"
)

var (
output = NewWriter(os.Stdout, os.Environ())
mtx sync.Mutex
)

// Default returns the default Lip Gloss writer that uses the standard output
// and environment variables.
func Default() *Writer {
return output
}

// SetOutput sets the default Lip Gloss writer to the given writer.
func SetOutput(w io.Writer) {
mtx.Lock()
output.Forward = w
mtx.Unlock()
}

// SetProfile sets the default Lip Gloss writer's color profile to the given
// profile.
func SetProfile(p Profile) {
mtx.Lock()
output.Profile = p
mtx.Unlock()
}

// Print writes the given text to the default Lip Gloss writer.
func Print(text string) (n int, err error) {
return output.Print(text)
}

// Println writes the given text to the default Lip Gloss writer followed by a
// newline.
func Println(text string) (n int, err error) {
return output.Println(text)
}

// Printf writes the given text to the default Lip Gloss writer with the given
// format.
func Printf(format string, a ...interface{}) (n int, err error) {
return output.Printf(format, a...)
}

// NewWriter creates a new Lip Gloss writer that writes text to the given writer.
//
// It queries the given writer to determine if it supports ANSI escape codes.
// If it does, along with the given environment variables, it will determine
// the appropriate color profile to use for color formatting.
//
// This respects the NO_COLOR, CLICOLOR, and CLICOLOR_FORCE environment variables.
func NewWriter(w io.Writer, environ []string) *Writer {
return &Writer{
Forward: w,
Profile: DetectColorProfile(w, environ),
parser: ansi.NewParser(parser.MaxParamsSize, 0),
}
}

// When a Lip Gloss writer is created, it queries the given writer to determine
// if it supports ANSI escape codes. If it does, the Lip Gloss writer will
// write text with ANSI escape codes. If it does not, the Lip Gloss writer will
// write text without ANSI escape codes.
// It also determines the appropriate color profile to use based on the
// capabilities of the underlying writer and environment.

// Writer represents a Lip Gloss writer that writes text to the underlying
// writer.
type Writer struct {
parser *ansi.Parser

Forward io.Writer
Profile Profile
}

// Print writes the given text to the underlying writer.
func (w *Writer) Print(text string) (n int, err error) {
return fmt.Fprint(w, text)
}

// Println writes the given text to the underlying writer followed by a newline.
func (w *Writer) Println(text string) (n int, err error) {
return fmt.Fprintln(w, text)
}

// Printf writes the given text to the underlying writer with the given format.
func (w *Writer) Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(w, format, a...)
}

// Write writes the given text to the underlying writer.
func (w *Writer) Write(p []byte) (int, error) {
if w.Profile == TrueColor {
return w.Forward.Write(p)
} else if w.Profile == NoTTY {
return io.WriteString(w.Forward, ansi.Strip(string(p)))
}

if w.parser == nil {
w.parser = ansi.NewParser(parser.MaxParamsSize, 0)
}

convertColorAppend := func(c ansi.Color, sel colorSelector, pen *ansi.CsiSequence) {
if c := w.Profile.Convert(c); c != nil && c != noColor {
pen.Params = append(pen.Params, ansiColorToParams(c, sel)...)
}
}

var buf bytes.Buffer
for i, b := range p {
w.parser.Advance(func(s ansi.Sequence) {
pen := ansi.CsiSequence{Cmd: 'm'}
switch s := s.(type) {
case ansi.CsiSequence:
switch s.Cmd {
case 'm':
if w.Profile > Ascii {
return
}
for j := 0; j < len(s.Params); j++ {
param := s.Param(j)
switch param {
case 39: // default foreground color
fallthrough
case 30, 31, 32, 33, 34, 35, 36, 37: // 8-bit foreground color
if w.Profile > ANSI {
convertColorAppend(ansi.BasicColor(param-30), foreground, &pen)
continue
}
case 49: // default background color
fallthrough
case 40, 41, 42, 43, 44, 45, 46, 47: // 8-bit background color
if w.Profile > ANSI {
convertColorAppend(ansi.BasicColor(param-40), background, &pen)
continue
}
case 90, 91, 92, 93, 94, 95, 96, 97: // 8-bit bright foreground color
if w.Profile > ANSI {
convertColorAppend(ansi.BasicColor(param-90+8), foreground, &pen)
continue
}
case 100, 101, 102, 103, 104, 105, 106, 107: // 8-bit bright background color
if w.Profile > ANSI {
convertColorAppend(ansi.BasicColor(param-100+8), background, &pen)
continue
}
case 59: // default underline color
if w.Profile > ANSI {
continue
}
case 38: // 16 or 24-bit foreground color
fallthrough
case 48: // 16 or 24-bit background color
fallthrough
case 58: // 16 or 24-bit underline color
var sel colorSelector
switch param {
case 38:
sel = foreground
case 48:
sel = background
case 58:
sel = underline
}
if c := readColor(&j, &s); c != nil {
switch c.(type) {
case ansi.ExtendedColor:
if w.Profile > ANSI256 {
convertColorAppend(c, sel, &pen)
continue
}
default:
if w.Profile > TrueColor {
convertColorAppend(c, sel, &pen)
continue
}
}
}
}
pen.Params = append(pen.Params, param)
}
buf.Write(pen.Bytes())
return
}
}
buf.Write(s.Bytes())
}, b, i < len(p)-1)
}

n, err := buf.WriteTo(w.Forward)
return int(n), err
}

func readColor(idxp *int, seq *ansi.CsiSequence) (c ansi.Color) {
i := *idxp
paramsLen := len(seq.Params)
// Note: we accept both main and subparams here
switch seq.Param(i + 1) {
case 2: // RGB
if i+2 < paramsLen && i+3 < paramsLen && i+4 < paramsLen {
c = color.RGBA{
R: uint8(seq.Param(i + 2)),
G: uint8(seq.Param(i + 3)),
B: uint8(seq.Param(i + 4)),
A: 0xff,
}
*idxp += 4
}
case 5: // 256 colors
if i+2 < paramsLen {
c = ansi.ExtendedColor(seq.Param(i + 2))
*idxp += 2
}
}
return
}

type colorSelector uint8

const (
foreground colorSelector = iota
background
underline
)

func ansiColorToParams(c ansi.Color, sel colorSelector) []int {
switch c := c.(type) {
case ansi.BasicColor:
offset := 30
if c >= ansi.BrightBlack {
offset = 90
c -= ansi.BrightBlack
}
switch sel {
case foreground:
return []int{offset + int(c)}
case background:
return []int{offset + 10 + int(c)}
case underline:
// XXX: ANSI doesn't have underline colors, use ANSI256.
return []int{58, 5, int(c)}
}
case ansi.ExtendedColor:
switch sel {
case foreground:
return []int{38, 5, int(c)}
case background:
return []int{48, 5, int(c)}
case underline:
return []int{58, 5, int(c)}
}
default:
r, g, b, _ := c.RGBA()
r = r >> 8
g = g >> 8
b = b >> 8
switch sel {
case foreground:
return []int{38, 2, int(r), int(g), int(b)}
case background:
return []int{48, 2, int(r), int(g), int(b)}
case underline:
return []int{58, 2, int(r), int(g), int(b)}
}
}
return nil
}

0 comments on commit 0723bba

Please sign in to comment.