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 db791fa
Show file tree
Hide file tree
Showing 3 changed files with 296 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
284 changes: 284 additions & 0 deletions writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
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 30, 31, 32, 33, 34, 35, 36, 37: // 8-bit foreground color

Check failure on line 136 in writer.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 30, in <case> detected (gomnd)
if w.Profile > ANSI {
convertColorAppend(ansi.BasicColor(param-30), foreground, &pen)

Check failure on line 138 in writer.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 30, in <argument> detected (gomnd)
continue
}
case 39: // default foreground color

Check failure on line 141 in writer.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 39, in <case> detected (gomnd)
if w.Profile > ANSI {
continue
}
case 40, 41, 42, 43, 44, 45, 46, 47: // 8-bit background color
if w.Profile > ANSI {
convertColorAppend(ansi.BasicColor(param-40), background, &pen)

Check failure on line 147 in writer.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 40, in <argument> detected (gomnd)
continue
}
case 49: // default background color
if w.Profile > ANSI {
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)

Check failure on line 156 in writer.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 8, in <argument> detected (gomnd)
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)

Check failure on line 161 in writer.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 8, in <argument> detected (gomnd)
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)),

Check failure on line 219 in writer.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 2, in <argument> detected (gomnd)
G: uint8(seq.Param(i + 3)),

Check failure on line 220 in writer.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 3, in <argument> detected (gomnd)
B: uint8(seq.Param(i + 4)),

Check failure on line 221 in writer.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 4, in <argument> detected (gomnd)
A: 0xff,
}
*idxp += 4
}
case 5: // 256 colors
if i+2 < paramsLen {
c = ansi.ExtendedColor(seq.Param(i + 2))

Check failure on line 228 in writer.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 2, in <argument> detected (gomnd)
*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 db791fa

Please sign in to comment.